Q11: deleted messages and withdrawn commands never reach the model
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
31ab919334
commit
22632a4695
8 files changed
+91
-28
No files matched your search
@@ -0,0 +1,9 @@
|
||||
export const chatWakeTextPurgeSchema = `
|
||||
update tasks
|
||||
set title = case when title like 'Chat mention:%' then 'Reply in chat' else title end,
|
||||
description = 'Reply in the source thread with send_message and its thread_id; the full message is in the thread below.'
|
||||
where origin = 'chat_wake';
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (60, 'chat_wake_text_purge', datetime('now'));
|
||||
`;
|
||||
@@ -58,6 +58,7 @@ import { kimiProvidersSchema } from "./migrations/056-kimi-providers";
|
||||
import { kimiCodeProviderSchema } from "./migrations/057-kimi-code-provider";
|
||||
import { integrationKeysSchema } from "./migrations/058-integration-keys";
|
||||
import { taskHandoffSchema } from "./migrations/059-task-handoff";
|
||||
import { chatWakeTextPurgeSchema } from "./migrations/060-chat-wake-text-purge";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -125,6 +126,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 57, name: "kimi_code_provider", sql: kimiCodeProviderSchema },
|
||||
{ version: 58, name: "integration_keys", sql: integrationKeysSchema },
|
||||
{ version: 59, name: "task_handoff", sql: taskHandoffSchema },
|
||||
{ version: 60, name: "chat_wake_text_purge", sql: chatWakeTextPurgeSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -150,10 +150,13 @@ export class ChatAgentWakeService {
|
||||
const targetId = this.requireAgentMention(mentions);
|
||||
const agent = this.requireRunnableAgent(input.orgId, targetId, "Slash command /agent");
|
||||
const workspace = this.workspaceForAgent(input.orgId, agent.workspace_id);
|
||||
const from = this.principalName(input.orgId, input.senderType, input.senderId);
|
||||
const space = this.spaceRow(input.orgId, input.spaceId);
|
||||
const direct = space.space_type === "direct";
|
||||
const taskId = this.organization.createTask({
|
||||
orgId: input.orgId,
|
||||
title: this.taskTitle(agent.name, slash),
|
||||
description: this.taskDescription(input, slash, messageId, threadId),
|
||||
title: direct ? `Reply to ${from}` : `Reply to ${from} in #${space.name}`,
|
||||
description: this.mentionTaskDescription(from, space.name, direct),
|
||||
status: TASK_STATUS.BACKLOG,
|
||||
assigneeAgentId: agent.id,
|
||||
risk: RISK_LEVEL.LOW,
|
||||
@@ -290,12 +293,6 @@ export class ChatAgentWakeService {
|
||||
`).run("agent", agentId, "thinking", slash.args.trim().slice(0, 160), now);
|
||||
}
|
||||
|
||||
private taskTitle(agentName: string, slash: SlashInvocation): string {
|
||||
const details = slash.args.replace(/^@[^\s]+\s*/, "").trim();
|
||||
const title = details || `reply as ${agentName}`;
|
||||
return `Chat mention: ${title.length > 64 ? `${title.slice(0, 61)}...` : title}`;
|
||||
}
|
||||
|
||||
private mentionTaskDescription(from: string, channel: string, direct: boolean): string {
|
||||
const where = direct
|
||||
? `${from} messaged you directly; their full message is in the thread below.`
|
||||
@@ -328,16 +325,4 @@ export class ChatAgentWakeService {
|
||||
if (!row) throw new Error("Chat space was not found.");
|
||||
return row;
|
||||
}
|
||||
|
||||
private taskDescription(input: SendMessageInput, slash: SlashInvocation, messageId: string, threadId: string): string {
|
||||
return [
|
||||
`Chat command: /agent ${slash.args.trim()}`,
|
||||
`Source message: ${messageId}`,
|
||||
`Source thread: ${threadId}`,
|
||||
`Source space: ${input.spaceId}`,
|
||||
`Sender: ${input.senderType}${input.senderId ? `:${input.senderId}` : ""}`,
|
||||
"",
|
||||
"Reply in the source thread with send_message and pass the thread_id above.",
|
||||
].join("\n");
|
||||
}
|
||||
}
|
||||
@@ -30,15 +30,16 @@ export function loadChatWakeContext(db: DatabaseSync, orgId: string, agentId: st
|
||||
if (!thread) throw new Error(`Chat wake thread ${scope.sourceThreadId} no longer exists.`);
|
||||
const lastReplyAt = (db.prepare(`
|
||||
select coalesce(max(created_at), '') as at from chat_messages
|
||||
where thread_id = ? and org_id = ? and sender_type = 'agent' and sender_id = ?
|
||||
where thread_id = ? and org_id = ? and sender_type = 'agent' and sender_id = ? and deleted_at is null
|
||||
`).get(scope.sourceThreadId, orgId, agentId) as { at: string }).at;
|
||||
const isFresh = `not (m.sender_type = 'agent' and m.sender_id = ?) and (m.created_at > ? or coalesce(m.edited_at, '') > ?)`;
|
||||
const inThread = "m.thread_id = ? and m.org_id = ? and m.deleted_at is null";
|
||||
const args = [scope.sourceThreadId, orgId, agentId, lastReplyAt, lastReplyAt];
|
||||
const fresh = db.prepare(`${MESSAGE_SELECT} where m.thread_id = ? and m.org_id = ? and ${isFresh} order by m.created_at asc`)
|
||||
const fresh = db.prepare(`${MESSAGE_SELECT} where ${inThread} and ${isFresh} order by m.created_at asc`)
|
||||
.all(...args) as unknown as ThreadMessageRow[];
|
||||
const history = (db.prepare(`${MESSAGE_SELECT} where m.thread_id = ? and m.org_id = ? and not (${isFresh}) order by m.created_at desc limit ?`)
|
||||
const history = (db.prepare(`${MESSAGE_SELECT} where ${inThread} and not (${isFresh}) order by m.created_at desc limit ?`)
|
||||
.all(...args, HISTORY_LIMIT) as unknown as ThreadMessageRow[]).reverse();
|
||||
const historyTotal = (db.prepare(`select count(*) as n from chat_messages m where m.thread_id = ? and m.org_id = ? and not (${isFresh})`)
|
||||
const historyTotal = (db.prepare(`select count(*) as n from chat_messages m where ${inThread} and not (${isFresh})`)
|
||||
.get(...args) as { n: number }).n;
|
||||
if (fresh.length === 0 && history.length === 0) return null;
|
||||
const omitted = historyTotal - history.length;
|
||||
|
||||
@@ -73,7 +73,11 @@ async function founderLine() {
|
||||
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 };
|
||||
const remove = (bodyMd: string) => {
|
||||
const row = connection.db.prepare("select id from chat_messages where body_md = ?").get(bodyMd) as { id: string };
|
||||
chat.deleteMessage({ orgId, messageId: row.id, actorType: "employee", actorId: founderId });
|
||||
};
|
||||
return { connection, wakePrompts, wakeInputs, say, runWake, runManually, edit, remove, onWakeTurn };
|
||||
}
|
||||
|
||||
describe("chat wake context", () => {
|
||||
@@ -224,4 +228,33 @@ describe("chat wake context", () => {
|
||||
expect(h.wakePrompts[0]).toContain("Please draft the Q3 hiring plan.");
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("deleted messages never reach the model", async () => {
|
||||
const h = await founderLine();
|
||||
const threadId = h.say("Old plan: launch in March.", null);
|
||||
await h.runWake();
|
||||
h.remove("Old plan: launch in March.");
|
||||
h.say("Wrong number, ignore it: 4242.", threadId);
|
||||
h.remove("Wrong number, ignore it: 4242.");
|
||||
h.say("What is the launch date now?", threadId);
|
||||
await h.runWake();
|
||||
const prompt = h.wakePrompts[1];
|
||||
expect(prompt).toContain("What is the launch date now?");
|
||||
expect(prompt).not.toContain("Old plan: launch in March.");
|
||||
expect(prompt).not.toContain("4242");
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a deleted /agent command leaves no trace in the task or the prompt", async () => {
|
||||
const h = await founderLine();
|
||||
const threadId = h.say("/agent @ceo wire the payroll to account 99-1234", null);
|
||||
h.remove("/agent @ceo wire the payroll to account 99-1234");
|
||||
h.say("Never mind that. How is hiring going?", threadId);
|
||||
await h.runWake();
|
||||
const prompt = h.wakePrompts[0];
|
||||
expect(prompt).toContain("How is hiring going?");
|
||||
expect(prompt).not.toContain("99-1234");
|
||||
expect(h.connection.db.prepare("select count(*) as n from tasks where title like '%99-1234%' or description like '%99-1234%'").get()).toEqual({ n: 0 });
|
||||
h.connection.close();
|
||||
});
|
||||
});
|
||||
@@ -328,7 +328,8 @@ describe("chat governance", () => {
|
||||
expect(slashByMessage.get(fileMessageId)).toMatchObject({ command: "file", resultRef: fileId });
|
||||
expect(slashByMessage.get(runbookMessageId)).toMatchObject({ command: "runbook", resultRef: runbookMessageId });
|
||||
expect(slashByMessage.get(handoffMessageId)).toMatchObject({ command: "handoff", resultRef: fileId });
|
||||
const agentTask = state.tasks.find((item) => item.assigneeAgentId === agentId && item.title === "Chat mention: review the gate");
|
||||
const wakeTaskId = (connection.db.prepare("select id from tasks where assignee_agent_id = ? and origin = 'chat_wake'").get(agentId) as { id: string }).id;
|
||||
const agentTask = state.tasks.find((item) => item.id === wakeTaskId);
|
||||
expect(agentTask).toMatchObject({
|
||||
orgId,
|
||||
assigneeAgentId: agentId,
|
||||
@@ -337,7 +338,9 @@ describe("chat governance", () => {
|
||||
priority: "high",
|
||||
requiresFilesystem: false,
|
||||
});
|
||||
expect(agentTask?.description).toContain(`Source message: ${agentMessageId}`);
|
||||
expect(agentTask?.title).toMatch(/^Reply to /);
|
||||
expect(agentTask?.description).toContain("their full message is in the thread below");
|
||||
expect(agentTask?.description).not.toContain("review the gate");
|
||||
const agentRun = state.taskRuns.find((item) => item.taskId === agentTask?.id);
|
||||
expect(agentRun).toMatchObject({
|
||||
orgId,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { chatWakeTextPurgeSchema } from "../../src/main/database/migrations/060-chat-wake-text-purge";
|
||||
|
||||
describe("chat wake text purge migration", () => {
|
||||
test("strips copied message text from chat-wake tasks and leaves real work alone", () => {
|
||||
const db = new DatabaseSync(join(tmpdir(), `agentcompany-purge-migration-${randomUUID()}.db`));
|
||||
db.exec(`
|
||||
create table schema_migrations(version integer primary key, name text not null, applied_at text not null);
|
||||
create table tasks(id text primary key, title text not null, description text not null, origin text);
|
||||
insert into tasks values ('tsk_slash', 'Chat mention: wire payroll to 99-1234', 'Chat command: /agent @ceo wire payroll to 99-1234', 'chat_wake');
|
||||
insert into tasks values ('tsk_mention', 'Reply to Yang', 'Yang messaged you directly:\n"secret plan..."', 'chat_wake');
|
||||
insert into tasks values ('tsk_real', 'Chat mention: keep', 'Real description', null);
|
||||
`);
|
||||
db.exec(chatWakeTextPurgeSchema);
|
||||
const rows = Object.fromEntries((db.prepare("select id, title, description from tasks").all() as Array<{ id: string; title: string; description: string }>)
|
||||
.map((row) => [row.id, [row.title, row.description]]));
|
||||
const generic = "Reply in the source thread with send_message and its thread_id; the full message is in the thread below.";
|
||||
expect(rows).toEqual({
|
||||
tsk_slash: ["Reply in chat", generic],
|
||||
tsk_mention: ["Reply to Yang", generic],
|
||||
tsk_real: ["Chat mention: keep", "Real description"],
|
||||
});
|
||||
expect(db.prepare("select version from schema_migrations").all()).toEqual([{ version: 60 }]);
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user