fix(slack): preserve explicit DM task controls

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
cryppadottaandPaperclip committed 2026-11-06 16:14:02 +00:00
1 parent 78108efc0c
commit 367b6f9b3f
6 files changed
+33 -26

No files matched your search

@@ -58500,7 +58500,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
]);
});
it("binds Slack DM slash controls to the native root returned for the latest task", async () => {
it.each(["new", "close"] as const)("binds Slack DM slash controls to the native root returned for the latest task (%s)", async (control) => {
const fixture = await seedCompany();
const { callbacks, endpoint, runtime, service } =
await configuredSlackEndpoint(fixture);
@@ -58564,7 +58564,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
threadId: starterThreadId,
text: expect.stringMatching(/— todo$/),
});
await invoke("close");
await invoke(control);
await expect(service.listConversations(endpoint.id)).resolves.toEqual([
expect.objectContaining({
externalThreadId: starterThreadId,
@@ -58578,9 +58578,21 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
state: "completed",
}),
]);
expect(
await readIssuesByCompanyId(fixture.companyId),
).toHaveLength(1);
expect(await readIssuesByCompanyId(fixture.companyId)).toHaveLength(1);
const providerTime = Date.now();
const providerTimestamp = `${Math.floor(providerTime / 1_000)}.${String((providerTime % 1_000) * 1_000).padStart(6, "0")}`;
await deliverMessage({
callbacks, endpointId: endpoint.id,
thread: makeThread({ channelId: "D09SLASHTASK", id: starterThreadId, isDM: true }).thread,
message: makeMessage({ id: providerTimestamp, raw: { ts: providerTimestamp }, text: "An explicitly requested new task", userId: "U-DM-SLASH-TASK" }),
trigger: "direct_message",
});
const conversations = await service.listConversations(endpoint.id);
expect(conversations).toHaveLength(2);
expect(conversations.find(row => row.state === "active")).toMatchObject({
externalThreadId: starterThreadId, sessionGeneration: 2,
});
expect(await readIssuesByCompanyId(fixture.companyId)).toHaveLength(2);
});
it("acknowledges a Slack retry while one durable worker owns the unrecorded command root", async () => {
+6 -2
View File
@@ -1749,13 +1749,17 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
let { conversation: existingConversation, issue: existingIssue, latestConversation, isLinear } =
await resolveInboundConversationBinding(db, {
endpoint, thread,
isLinear: () => endpoint.provider === "speko" ? SPEKO_CONVERSATION_IS_LINEAR : surfaceKind !== "native_thread",
isLinear: () => endpoint.provider === "speko"
? SPEKO_CONVERSATION_IS_LINEAR
: surfaceKind !== "native_thread" || (endpoint.provider === "slack" && thread.isDM),
lookupConversation: endpoint.provider === "microsoft-teams"
? () => conversationForThread(endpoint.id, thread.id)
: undefined,
conversationIsComplete: endpoint.provider === "imessage-photon"
? (conversationId) => photonConversationBindingIsComplete(conversationId, hasCommittedTaskControlCompletion)
: undefined,
: endpoint.provider === "slack" && thread.isDM && surfaceKind === "native_thread"
? async (conversationId, state) => state === "completed" && hasCommittedTaskControlCompletion(conversationId)
: undefined,
});
const telegramControls = endpoint.provider === "telegram"
? telegramInboundControls(message.text, surfaceKind)
@@ -9,7 +9,7 @@ type ConversationBindingInput = {
thread: { id: string; channelId: string };
isLinear: () => boolean;
lookupConversation?: () => Promise<ConversationRow | null>;
conversationIsComplete?: (conversationId: string) => Promise<boolean>;
conversationIsComplete?: (conversationId: string, state: ConversationRow["state"]) => Promise<boolean>;
};
export async function resolveInboundConversationBinding(
@@ -54,7 +54,7 @@ export async function resolveInboundConversationBinding(
isLinear &&
existingConversation &&
(conversationIsComplete
? await conversationIsComplete(existingConversation.id)
? await conversationIsComplete(existingConversation.id, existingConversation.state)
: existingConversation.state === "completed" ||
existingIssue?.status === "done" ||
existingIssue?.status === "cancelled")