diff --git a/src/main/services/chat-agent-wake-service.ts b/src/main/services/chat-agent-wake-service.ts index 01f4fd4f..f227a5e8 100644 --- a/src/main/services/chat-agent-wake-service.ts +++ b/src/main/services/chat-agent-wake-service.ts @@ -130,7 +130,7 @@ export class ChatAgentWakeService { const taskId = this.organization.createTask({ orgId: input.orgId, title: direct ? `Reply to ${from}` : `Reply to ${from} in #${space.name}`, - description: this.mentionTaskDescription(input, from, space.name, direct), + description: this.mentionTaskDescription(from, space.name, direct), status: TASK_STATUS.BACKLOG, assigneeAgentId: agent.id, risk: RISK_LEVEL.LOW, @@ -296,15 +296,15 @@ export class ChatAgentWakeService { return `Chat mention: ${title.length > 64 ? `${title.slice(0, 61)}...` : title}`; } - private mentionTaskDescription(input: SendMessageInput, from: string, channel: string, direct: boolean): string { - const said = input.bodyMd.trim(); - const where = direct ? `${from} messaged you directly:` : `${from} addressed you in #${channel}:`; + private mentionTaskDescription(from: string, channel: string, direct: boolean): string { + const where = direct + ? `${from} messaged you directly; their full message is in the thread below.` + : `${from} addressed you in #${channel}; their full message is in the thread below.`; const how = direct ? `Reply to ${from} in your direct conversation: call send_message with to="${from}" and this thread_id.` : `Reply to ${from} in this thread.`; return [ where, - `"${said.length > 400 ? `${said.slice(0, 397)}...` : said}"`, "", `${how} Answer what they asked, give your honest take, push back if you disagree, or hand work over — speak as yourself, the way a colleague would, not as a status report.`, ].join("\n"); diff --git a/src/main/services/task-execution-service.ts b/src/main/services/task-execution-service.ts index 7e4a8f0e..63209cb4 100644 --- a/src/main/services/task-execution-service.ts +++ b/src/main/services/task-execution-service.ts @@ -665,10 +665,26 @@ export class TaskExecutionService { providerIds: [provider.id], scope: "task_run", credentialAccess: "provider_runtime_only", + ...this.chatWakeSource(orgId, task), }), }; } + /** A re-run of a chat wake answers the same thread. */ + private chatWakeSource(orgId: string, task: TaskExecutionTaskRow): { sourceThreadId: string; sourceMessageId: string } | Record { + if (task.origin !== "chat_wake") return {}; + const row = this.db.prepare(` + select json_extract(secret_scope_json, '$.sourceThreadId') as thread_id, + json_extract(secret_scope_json, '$.sourceMessageId') as message_id + from task_runs + where task_id = ? and org_id = ? and json_extract(secret_scope_json, '$.sourceThreadId') is not null + order by created_at desc + limit 1 + `).get(task.id, orgId) as { thread_id: string; message_id: string } | undefined; + if (!row) throw new Error(`Chat wake task ${task.id} has no recorded source thread.`); + return { sourceThreadId: row.thread_id, sourceMessageId: row.message_id }; + } + private requireServiceIdentity(orgId: string, agentId: string): string { const row = this.db.prepare("select id from service_identities where org_id = ? and agent_id = ?") .get(orgId, agentId) as { id: string } | undefined; diff --git a/src/main/services/task-prompts.ts b/src/main/services/task-prompts.ts index 22330a38..fc6c298e 100644 --- a/src/main/services/task-prompts.ts +++ b/src/main/services/task-prompts.ts @@ -14,6 +14,7 @@ export interface TaskExecutionTaskRow { priority: string; requires_filesystem: number; handoff_json: string | null; + origin: string | null; } export interface TaskExecutionAgentRow { diff --git a/tests/integration/chat-context.test.ts b/tests/integration/chat-context.test.ts index 9ccf9e83..7a1cee9d 100644 --- a/tests/integration/chat-context.test.ts +++ b/tests/integration/chat-context.test.ts @@ -66,7 +66,14 @@ async function founderLine() { chat.editMessage({ orgId, messageId: row.id, bodyMd: newBody, actorType: "employee", actorId: founderId }); }; const onWakeTurn = (hook: (turnIndex: number) => void) => { onWake = hook; }; - return { connection, wakePrompts, wakeInputs, say, runWake, edit, onWakeTurn }; + const runManually = async () => { + const wake = connection.db.prepare( + "select id, task_id from task_runs where org_id = ? and agent_id = ? and trigger = 'chat_mention' and state = 'queued'", + ).get(orgId, ceoId) as { id: string; task_id: string }; + connection.db.prepare("update task_runs set state = 'failed' where id = ?").run(wake.id); + await execution.runTask({ orgId, taskId: wake.task_id }); + }; + return { connection, wakePrompts, wakeInputs, say, runWake, runManually, edit, onWakeTurn }; } describe("chat wake context", () => { @@ -194,4 +201,27 @@ describe("chat wake context", () => { expect(earlier).toContain("13 earlier messages in this thread are not shown."); h.connection.close(); }); + + test("a long message reaches the run whole, and the task description does not truncate it", async () => { + const h = await founderLine(); + const long = `Plan: ${"step ".repeat(200)}END-OF-PLAN`; + h.say(long, null); + await h.runWake(); + const prompt = h.wakePrompts[0]; + expect(prompt).toContain(long); + expect(prompt).not.toContain("step ..."); + const description = (h.connection.db.prepare("select description from tasks where origin = 'chat_wake'").get() as { description: string }).description; + expect(description).not.toContain("step "); + expect(description).toContain("messaged you directly"); + h.connection.close(); + }); + + test("re-running a chat wake by hand answers the same thread", async () => { + const h = await founderLine(); + h.say("Please draft the Q3 hiring plan.", null); + await h.runManually(); + expect(h.wakePrompts).toHaveLength(1); + expect(h.wakePrompts[0]).toContain("Please draft the Q3 hiring plan."); + h.connection.close(); + }); });