@jskit-ai/assistant-core 0.1.226 → 0.1.227

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/examples/conversation/package.json +1 -1
  2. package/fixtures/responsive-assistant/ConversationFixture.vue +5 -3
  3. package/fixtures/responsive-assistant/DeliveryFixture.vue +9 -0
  4. package/package.json +1 -1
  5. package/src/client/conversation/AssistantConversationElement.vue +7 -4
  6. package/src/client/conversation/AssistantGoalControl.vue +30 -16
  7. package/src/client/conversation/AssistantTranscript.vue +14 -7
  8. package/src/client/conversation/useAssistantAttachments.js +6 -3
  9. package/src/server/conversation/claudeClient.js +22 -5
  10. package/src/server/conversation/claudeHistory.js +62 -5
  11. package/src/server/conversation/claudeProcess.js +7 -6
  12. package/src/server/conversation/claudeTurn.js +284 -57
  13. package/src/server/conversation/codexConfiguration.js +24 -1
  14. package/src/server/conversation/codexGoalCommands.js +8 -3
  15. package/src/server/conversation/codexTurnRecovery.js +18 -8
  16. package/src/server/conversation/continuity.js +54 -17
  17. package/src/server/conversation/providers/claudeDriver.js +6 -5
  18. package/src/server/conversation/providers/codexDriver.js +3 -3
  19. package/src/server/conversation/providers/scoped.js +31 -10
  20. package/src/server/conversation/runtime.js +48 -13
  21. package/src/server/conversation/streams.js +7 -3
  22. package/src/server/conversation/transcript.js +2 -1
  23. package/src/server/hosts/codexConfiguration.js +1 -0
  24. package/src/shared/conversation/longTextBlocks.js +0 -4
  25. package/test/assistantScroll.browser.test.js +74 -3
  26. package/test/claudeClient.test.js +73 -0
  27. package/test/claudeConversationSession.test.js +120 -4
  28. package/test/claudeProcess.test.js +61 -0
  29. package/test/claudeTurn.test.js +1315 -10
  30. package/test/codexConversationSession.test.js +9 -1
  31. package/test/codexGoal.test.js +47 -0
  32. package/test/codexModelCatalog.test.js +55 -0
  33. package/test/codexTurn.test.js +228 -0
  34. package/test/conversationAttachments.test.js +17 -0
  35. package/test/conversationRuntime.test.js +40 -0
  36. package/test/conversationStreaming.test.js +18 -0
  37. package/test/messageDelivery.browser.test.js +30 -0
@@ -1,8 +1,11 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
+ import { Duplex } from "node:stream";
4
+ import { createClaudeJsonClient } from "../src/server/conversation/claudeClient.js";
3
5
  import { setTimeout as delay } from "node:timers/promises";
4
6
  import { createClaudeConversationTurn, claudeNativeMessageId, runClaudeRenewalTurn,
5
7
  waitForClaudeConversationTurn } from "../src/server/conversation/claudeTurn.js";
8
+ import { readClaudeHistory } from "../src/server/conversation/claudeHistory.js";
6
9
 
7
10
  function fixture() {
8
11
  const events = [];
@@ -17,6 +20,131 @@ function fixture() {
17
20
  return { turn, events, client, send };
18
21
  }
19
22
 
23
+ test("native local command output retains protocol provenance through transcript storage", async () => {
24
+ const { createConversationTranscript, createMemoryConversationStorage } = await import("../src/server/conversation/index.js");
25
+ const { turn, send, events } = fixture();
26
+ await send();
27
+ await turn.receive({ type: "system", subtype: "local_command_output", uuid: "goal-confirmation", content: "Goal cleared: Finish checks" });
28
+ const message = events.find(event => event.type === "message").message;
29
+ assert.deepEqual(message.data, { native: { engineId: "claude", type: "system", subtype: "local_command_output" } });
30
+ const storage = createMemoryConversationStorage();
31
+ const transcript = createConversationTranscript({ storage });
32
+ await transcript.writeConversationUserMessage("test", { messageId: "user", text: "/goal clear" });
33
+ await transcript.writeConversationCommentaryMessage("test", { ...message, messageId: message.id });
34
+ const [saved] = await transcript.readConversationLog("test");
35
+ assert.deepEqual(saved.commentary[0].data, message.data);
36
+ message.data.native.subtype = "changed";
37
+ assert.equal((await transcript.readConversationLog("test"))[0].commentary[0].data.native.subtype, "local_command_output");
38
+ turn.stop();
39
+ });
40
+
41
+ test("actual Claude synthetic goal confirmations retain command provenance without relabelling ordinary replies", async () => {
42
+ const { turn, send, events } = fixture();
43
+ await send();
44
+ // Captured from Claude 2.1.289's stream-json /goal clear operation. Native
45
+ // history stores system/local_command, but the live protocol uses assistant.
46
+ await turn.receive({ type: "assistant", uuid: "clear-confirmation", local_command_run: { command: "goal", args: "clear" },
47
+ local_command_source: "<local-command-stdout>No goal set</local-command-stdout>",
48
+ message: { id: "synthetic-command", model: "<synthetic>", stop_reason: "end_turn", content: [{ type: "text", text: "No goal set" }] } });
49
+ await turn.receive({ type: "result", subtype: "success", local_command: "goal", result: "No goal set" });
50
+ const output = events.filter(event => event.type === "message" && event.message.complete);
51
+ assert.equal(output.length, 1, "The result does not duplicate its synthetic assistant carrier");
52
+ assert.deepEqual(output[0].message.data, { native: { engineId: "claude", type: "assistant", command: "goal", output: "stdout" } });
53
+ const transcript = createConversationTranscript({ storage: createMemoryConversationStorage() });
54
+ await transcript.writeConversationUserMessage("test", { messageId: "clear", text: "/goal clear" });
55
+ await transcript.writeConversationAssistantMessage("test", { ...output[0].message, messageId: output[0].message.id });
56
+ assert.deepEqual((await transcript.readConversationLog("test"))[0].assistant.data, output[0].message.data);
57
+ turn.begin({ id: "ordinary" });
58
+ await send("ordinary");
59
+ await turn.receive({ type: "assistant", uuid: "ordinary-answer", message: { id: "ordinary-api", model: "haiku", stop_reason: "end_turn",
60
+ content: [{ type: "text", text: "Goal set: I will work through the checks." }] } });
61
+ assert.equal(events.findLast(event => event.type === "message").message.data, undefined);
62
+ turn.stop();
63
+ });
64
+
65
+ test("Claude native no-response placeholders are excluded from live and cold replies without hiding errors or model answers", async t => {
66
+ const { turn, send, events } = fixture();
67
+ t.after(() => turn.stop());
68
+ await send();
69
+ // SAS26: native 2.1.289 appends this placeholder after a stopped request
70
+ // when changing models. Its own lI classifier uses this exact provenance.
71
+ const placeholder = { type: "assistant", uuid: "b53b36f5-0eca-4fea-9e70-5b8f67353256", isApiErrorMessage: false,
72
+ message: { id: "79b0fcfc-0510-4fd7-8f41-7623dba7697a", model: "<synthetic>", stop_reason: "stop_sequence",
73
+ usage: { input_tokens: 0, output_tokens: 0 }, content: [{ type: "text", text: "No response requested." }] } };
74
+ const frames = [placeholder,
75
+ { ...placeholder, uuid: "real-answer", message: { ...placeholder.message, id: "real-api", model: "claude-haiku-4-5-20251001" } },
76
+ { ...placeholder, uuid: "api-error", isApiErrorMessage: true, message: { ...placeholder.message, id: "error-api" } },
77
+ { ...placeholder, uuid: "other-synthetic", message: { ...placeholder.message, id: "other-api", content: [{ type: "text", text: "Different native notice" }] } }
78
+ ];
79
+ for (const frame of frames) await turn.receive(frame);
80
+ const expected = ["claude_real-answer_0", "claude_api-error_0", "claude_other-synthetic_0"];
81
+ assert.deepEqual(events.filter(event => event.type === "message").map(event => event.message.id), expected);
82
+ const root = await mkdtemp(path.join(os.tmpdir(), "claude-placeholder-"));
83
+ t.after(() => rm(root, { recursive: true, force: true }));
84
+ const directory = path.join(root, "projects", root.replace(/[^a-zA-Z0-9]/gu, "-"));
85
+ await mkdir(directory, { recursive: true });
86
+ const conversationId = "669021a9-a56e-48e8-b752-44e6accb1300";
87
+ await writeFile(path.join(directory, `${conversationId}.jsonl`), [
88
+ { type: "user", uuid: "original-request", message: { content: "Original request" } }, ...frames
89
+ ].map(JSON.stringify).join("\n") + "\n");
90
+ const history = await readClaudeHistory({ configRoot: root, workdir: root, conversationId });
91
+ assert.deepEqual(history.messages.map(message => message.id), expected);
92
+ assert.ok(history.messages.every(message => message.userId === "original-request"));
93
+ assert.deepEqual(history.userIds, ["original-request"]);
94
+ });
95
+
96
+ test("Claude successful native clear removes a failed goal without a clearing sentinel", async t => {
97
+ const root = await mkdtemp(path.join(os.tmpdir(), "claude-clear-goal-"));
98
+ t.after(() => rm(root, { recursive: true, force: true }));
99
+ const directory = path.join(root, "projects", root.replace(/[^a-zA-Z0-9]/gu, "-"));
100
+ await mkdir(directory, { recursive: true });
101
+ const conversationId = "cde87511-7341-4f9e-9d8a-6fe849a953a5";
102
+ const file = path.join(directory, `${conversationId}.jsonl`);
103
+ const failed = { type: "attachment", uuid: "failed", timestamp: "2026-10-16T18:08:11.407Z",
104
+ attachment: { type: "goal_status", condition: "Finish checks", met: false, failed: true } };
105
+ // Actual Claude 2.1.289 history after Cancel on an impossible goal. It emits
106
+ // no goal_status sentinel because its native checker already removed the goal.
107
+ const cleared = { type: "system", subtype: "local_command", uuid: "cleared",
108
+ timestamp: "2026-10-16T18:09:31.206Z", commandRun: { command: "goal", args: "clear" },
109
+ content: "<local-command-stdout>No goal set</local-command-stdout>" };
110
+ const frames = [failed,
111
+ { ...cleared, type: "assistant", uuid: "quoted", message: { content: "No goal set" } },
112
+ { ...cleared, uuid: "nested", parent_tool_use_id: "child" },
113
+ { ...cleared, uuid: "sidechain", isSidechain: true },
114
+ { ...cleared, uuid: "meta", isMeta: true },
115
+ { ...cleared, uuid: "other-command", commandRun: { command: "help", args: "clear" } },
116
+ { ...cleared, uuid: "status-only", commandRun: { command: "goal", args: "" } },
117
+ { ...cleared, uuid: "error", content: "<local-command-stderr>No goal set</local-command-stderr>" }
118
+ ];
119
+ await writeFile(file, frames.map(JSON.stringify).join("\n") + "\n");
120
+ const options = { configRoot: root, workdir: root, conversationId };
121
+ assert.equal((await readClaudeHistory(options)).goal.status, "blocked", "quotes, nested commands and failures cannot clear the goal");
122
+ await writeFile(file, JSON.stringify(cleared) + "\n", { flag: "a" });
123
+ const original = await readFile(file, "utf8");
124
+ assert.equal((await readClaudeHistory(options)).goal, null);
125
+ assert.equal((await readClaudeHistory(options)).goal, null, "a fresh read cannot resurrect the failed goal");
126
+ assert.equal(await readFile(file, "utf8"), original, "projection does not mutate native history");
127
+ await writeFile(file, JSON.stringify({ ...failed, uuid: "next", timestamp: "2026-10-16T18:10:00.000Z",
128
+ attachment: { type: "goal_status", condition: "Next checks", met: false, sentinel: true } }) + "\n", { flag: "a" });
129
+ assert.equal((await readClaudeHistory(options)).goal.objective, "Next checks", "a later goal still wins");
130
+ });
131
+
132
+ test("Claude local-command error output stays distinguishable and result-only success retains provenance", async () => {
133
+ const { turn, send, events } = fixture();
134
+ await send();
135
+ await turn.receive({ type: "assistant", uuid: "command-error", local_command_run: { command: "goal", args: "invalid" },
136
+ local_command_source: "<local-command-stderr>Goal cannot be changed</local-command-stderr>",
137
+ message: { id: "error-command", stop_reason: "end_turn", content: [{ type: "text", text: "Goal cannot be changed" }] } });
138
+ assert.deepEqual(events.find(event => event.type === "message").message.data,
139
+ { native: { engineId: "claude", type: "assistant", command: "goal", output: "stderr" } });
140
+ turn.stop();
141
+ turn.begin({ id: "result-only" });
142
+ await send("result-only");
143
+ await turn.receive({ type: "result", subtype: "success", local_command: "goal", result: "Goal cleared: Previous objective" });
144
+ assert.deepEqual(events.findLast(event => event.type === "message").message.data,
145
+ { native: { engineId: "claude", type: "result", command: "goal", output: "stdout" } });
146
+ });
147
+
20
148
  test("a pipe write is not admission; queued and replayed acknowledgements persist the user exactly once", async () => {
21
149
  const { turn, client, send, events } = fixture();
22
150
  client.send = async () => {};
@@ -51,6 +179,193 @@ test("steering waits for the generation's terminal event after the control ackno
51
179
  assert.equal(turn.read().status, "completed");
52
180
  });
53
181
 
182
+ test("interrupted Claude output keeps its input identity after a follow-up is accepted", async t => {
183
+ const f = await publishedClaudeFixture(t);
184
+ await f.owner.send(f.entry, { messageId: "first", message: "First request" });
185
+ const native = f.processes.at(-1);
186
+ await native.options.onEvent({ type: "stream_event", event: { type: "message_start", message: { id: "first-api" } } });
187
+ await native.options.onEvent({ type: "stream_event", event: { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } } });
188
+ await native.options.onEvent({ type: "stream_event", event: { type: "content_block_delta", index: 0, delta: { text: "FIRST-PARTIAL" } } });
189
+ await native.options.onEvent({ type: "assistant", uuid: "first-output", message: { id: "first-api", content: [{ type: "text", text: "FIRST-PARTIAL" }] } });
190
+ await f.owner.send(f.entry, { messageId: "second", message: "Second request" });
191
+ await native.options.onEvent({ type: "assistant", uuid: "second-output", message: { id: "second-api", content: [{ type: "text", text: "SECOND-OK" }], stop_reason: "end_turn" } });
192
+ await native.options.onEvent({ type: "result", subtype: "success", result: "SECOND-OK" });
193
+ const read = await f.owner.read(f.entry, { messageId: "second", requestMessageIds: ["first", "second"] });
194
+ assert.equal(read.messages.find(message => message.text === "FIRST-PARTIAL").requestMessageId, "first");
195
+ assert.equal(read.messages.find(message => message.text === "FIRST-PARTIAL").role, "commentary");
196
+ assert.equal(read.messages.find(message => message.text === "SECOND-OK").requestMessageId, "second");
197
+ // A cold read uses the original native input identities, without live state.
198
+ const directory = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"));
199
+ await mkdir(directory, { recursive: true });
200
+ await writeFile(path.join(directory, `${f.conversationId}.jsonl`), [
201
+ { type: "user", uuid: claudeNativeMessageId("first"), message: { content: "First request" } },
202
+ { type: "assistant", uuid: "first-output", message: { id: "first-api", content: [{ type: "text", text: "FIRST-PARTIAL" }] } },
203
+ { type: "user", uuid: claudeNativeMessageId("second"), message: { content: "Second request" } },
204
+ { type: "assistant", uuid: "second-output", message: { id: "second-api", content: [{ type: "text", text: "SECOND-OK" }] } }
205
+ ].map(JSON.stringify).join("\n") + "\n");
206
+ f.entry.nativeTurn.reset();
207
+ const restored = await f.owner.read(f.entry, { messageId: "second", requestMessageIds: ["first", "second"] });
208
+ assert.deepEqual(restored.messages.map(message => [message.text, message.requestMessageId]),
209
+ [["FIRST-PARTIAL", "first"], ["SECOND-OK", "second"]]);
210
+ });
211
+
212
+ for (const savedFrame of [false, true]) test(`verified Claude Stop retains partial output for transcript reads, savedFrame=${savedFrame}`, async t => {
213
+ const f = await publishedClaudeFixture(t);
214
+ await f.send("stopped-request");
215
+ await f.frame({ type: "stream_event", event: { type: "message_start", message: { id: "stopped-api" } } });
216
+ await f.frame({ type: "stream_event", event: { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } } });
217
+ await f.frame({ type: "stream_event", event: { type: "content_block_delta", index: 0, delta: { text: "Already visible partial answer" } } });
218
+ if (savedFrame) await f.frame({ type: "assistant", uuid: "stopped-output", message: { id: "stopped-api",
219
+ content: [{ type: "text", text: "Already visible partial answer" }] } });
220
+ // The control acknowledgement need not deliver a terminal generation frame
221
+ // before the managed process is drained, as observed in the live Stop case.
222
+ const native = f.entry.process;
223
+ native.client.interrupt = async () => {};
224
+ native.stop = async () => ({ scopeEmpty: false, exited: false });
225
+ await assert.rejects(f.owner.interrupt(f.entry), { code: "claude_stop_unconfirmed" });
226
+ const unconfirmed = await f.owner.read(f.entry, { messageId: "stopped-request" });
227
+ assert.equal(unconfirmed.messages.find(m => m.text === "Already visible partial answer").complete, false);
228
+ native.stop = async () => ({ scopeEmpty: true, exited: true });
229
+ await f.owner.interrupt(f.entry);
230
+ const stopped = await f.owner.read(f.entry, { messageId: "stopped-request" });
231
+ assert.equal(stopped.status, "interrupted");
232
+ assert.equal(stopped.messages.length, 1, "Saved-frame and streaming aliases are one interrupted output");
233
+ assert.deepEqual(stopped.messages.map(m => ({ text: m.text, role: m.role, complete: m.complete,
234
+ requestMessageId: m.requestMessageId, outputId: m.outputId })), [{
235
+ text: "Already visible partial answer", role: "commentary", complete: true,
236
+ requestMessageId: "stopped-request", outputId: "claude_stopped-api_0"
237
+ }]);
238
+ assert.equal(f.final(), null, "An interrupted partial is never a completed answer receipt");
239
+ await f.owner.interrupt(f.entry);
240
+ assert.deepEqual((await f.owner.read(f.entry, { messageId: "stopped-request" })).messages, stopped.messages);
241
+ const { readPersistentConversation, projectStoredPersistentConversation } = await import("../src/server/conversation/providers/scoped.js");
242
+ const transcript = createConversationTranscript({ storage: createMemoryConversationStorage() });
243
+ const store = Object.fromEntries(Object.entries(transcript).map(([name, operation]) =>
244
+ [name, (scope, ...args) => operation(JSON.stringify([scope.sessionId, scope.conversationId]), ...args)]));
245
+ store.readConversationStream = () => ({ messages: [] });
246
+ const scope = { sessionId: "session", conversationId: "temporary" };
247
+ await store.writeConversationUserMessage(scope, { messageId: "stopped-request", text: "Explain" });
248
+ const options = { ...scope, store, context: {},
249
+ record: { conversationId: "temporary", providerConversationId: f.conversationId, messageId: "stopped-request", status: "interrupted" },
250
+ operations: { readConversation: (_sessionId, input) => f.owner.read(f.entry, input) },
251
+ records: { input: (record, input) => ({ ...record, ...input }), save: async (_context, record, patch) => ({ ...record, ...patch }) },
252
+ receipts: { persisted: () => true },
253
+ projection: { context: context => context, messageScope: (_context, scope) => scope,
254
+ message: message => message, messageMetadata: () => ({}), snapshot: value => value }, publish: async () => {}
255
+ };
256
+ await readPersistentConversation(options);
257
+ await readPersistentConversation(options);
258
+ f.entry.nativeTurn.reset();
259
+ const restored = await projectStoredPersistentConversation(options);
260
+ assert.deepEqual(restored.messages.map(m => [m.role, m.text]), [
261
+ ["user", "Explain"], ["commentary", "Already visible partial answer"]
262
+ ], "Canonical history retains exactly one interrupted partial after live state is gone");
263
+ });
264
+
265
+ for (const historyFirst of [true, false]) test(`stopped Claude output and late native history remain one output, historyFirst=${historyFirst}`, async t => {
266
+ const f = await publishedClaudeFixture(t);
267
+ await f.send("stopped-race");
268
+ await f.frame({ type: "stream_event", event: { type: "message_start", message: { id: "race-api" } } });
269
+ await f.frame({ type: "stream_event", event: { type: "content_block_start", index: 1, content_block: { type: "text", text: "" } } });
270
+ await f.frame({ type: "stream_event", event: { type: "content_block_delta", index: 1, delta: { text: "Already visible" } } });
271
+ f.entry.process.client.interrupt = async () => {};
272
+ await f.owner.interrupt(f.entry);
273
+ const saveNative = async () => {
274
+ const directory = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"));
275
+ await mkdir(directory, { recursive: true });
276
+ await writeFile(path.join(directory, `${f.conversationId}.jsonl`), [
277
+ { type: "user", uuid: claudeNativeMessageId("stopped-race"), message: { content: "Explain" } },
278
+ { type: "assistant", uuid: "race-saved", apiBlockIndex: 1,
279
+ message: { id: "race-api", stop_reason: "end_turn", content: [{ type: "text", text: "Already visible, with later native text" }] } }
280
+ ].map(JSON.stringify).join("\n") + "\n");
281
+ };
282
+ if (historyFirst) await saveNative();
283
+ const read = await f.owner.read(f.entry, { messageId: "stopped-race" });
284
+ assert.equal(read.messages.length, 1, "One native API block cannot become both a saved answer and a live partial");
285
+ assert.equal(read.messages[0].role, "commentary");
286
+ assert.equal(read.messages[0].outputId, "claude_race-api_1");
287
+ assert.equal(read.messages[0].text, historyFirst ? "Already visible, with later native text" : "Already visible");
288
+ assert.equal(f.final(), null);
289
+ const { readPersistentConversation } = await import("../src/server/conversation/providers/scoped.js");
290
+ const transcript = createConversationTranscript({ storage: createMemoryConversationStorage() });
291
+ const store = Object.fromEntries(Object.entries(transcript).map(([name, operation]) =>
292
+ [name, (scope, ...args) => operation(JSON.stringify([scope.sessionId, scope.conversationId]), ...args)]));
293
+ store.readConversationStream = () => ({ messages: [] });
294
+ const scope = { sessionId: "session", conversationId: "temporary" };
295
+ await store.writeConversationUserMessage(scope, { messageId: "stopped-race", text: "Explain" });
296
+ const options = { ...scope, store, context: {},
297
+ record: { conversationId: "temporary", providerConversationId: f.conversationId, messageId: "stopped-race", status: "interrupted" },
298
+ operations: { readConversation: (_sessionId, input) => f.owner.read(f.entry, input) },
299
+ records: { input: (record, input) => ({ ...record, ...input }), save: async (_context, record, patch) => ({ ...record, ...patch }) },
300
+ receipts: { persisted: () => true },
301
+ projection: { context: context => context, messageScope: (_context, scope) => scope,
302
+ message: message => message, messageMetadata: () => ({}), snapshot: value => value }, publish: async () => {}
303
+ };
304
+ await readPersistentConversation(options);
305
+ if (!historyFirst) await saveNative();
306
+ await readPersistentConversation(options);
307
+ f.entry.nativeTurn.reset();
308
+ await readPersistentConversation(options);
309
+ const turns = await store.readConversationLog(scope);
310
+ assert.equal(turns.length, 1);
311
+ assert.equal(turns[0].assistant, null, "Reload cannot promote an already retained interruption into an answer");
312
+ assert.equal(turns[0].commentary.length, 1, "Later native identity cannot duplicate the saved output");
313
+ assert.equal(turns[0].commentary[0].text, read.messages[0].text);
314
+ });
315
+
316
+ // Claude 2.1.289 folds internal commands into the outer turn. Their lifecycle
317
+ // completes before result; the original input's completion follows result.
318
+ for (const background of [false, true]) test(`folded native command cannot strand its outer input, background=${background}`, async () => {
319
+ const { turn, send, events } = fixture();
320
+ await send();
321
+ const outer = claudeNativeMessageId("first");
322
+ await turn.receive({ type: "command_lifecycle", command_uuid: outer, state: "started" });
323
+ if (background) await turn.receive({ type: "system", subtype: "task_started", task_id: "background" });
324
+ await turn.receive({ type: "command_lifecycle", command_uuid: "internal-notification", state: "started" });
325
+ await turn.receive({ type: "command_lifecycle", command_uuid: "internal-notification", state: "completed" });
326
+ assert.equal(turn.read().active, true, "A folded command's completion does not complete its outer generation");
327
+ await turn.receive({ type: "result", subtype: "success", result: "Done" });
328
+ await turn.receive({ type: "command_lifecycle", command_uuid: outer, state: "completed" });
329
+ assert.equal(turn.read().pendingCommands, 0);
330
+ assert.equal(turn.read().active, background);
331
+ if (background) {
332
+ await turn.receive({ type: "system", subtype: "task_notification", task_id: "background", status: "completed" });
333
+ assert.equal(turn.read().active, true, "The main model still needs to consume the worker result");
334
+ await turn.receive({ type: "result", subtype: "success", result: "Worker checked" });
335
+ }
336
+ assert.equal(turn.read().status, "completed");
337
+ assert.equal(events.filter(event => event.type === "settled").length, 1);
338
+ });
339
+
340
+ test("folded lifecycle completion cannot acknowledge interruption before the generation result", async () => {
341
+ const { turn, send, client } = fixture();
342
+ await send();
343
+ await turn.receive({ type: "command_lifecycle", command_uuid: "internal-notification", state: "started" });
344
+ let confirmed = false;
345
+ const steering = turn.interruptGeneration(client).then(() => { confirmed = true; });
346
+ await turn.receive({ type: "command_lifecycle", command_uuid: "internal-notification", state: "completed" });
347
+ await Promise.resolve();
348
+ assert.equal(confirmed, false);
349
+ await turn.receive({ type: "result", subtype: "success", terminal_reason: "aborted_streaming", result: "" });
350
+ await steering;
351
+ await send("goal");
352
+ await turn.receive({ type: "result", subtype: "success", result: "Goal accepted" });
353
+ await turn.receive({ type: "command_lifecycle", command_uuid: claudeNativeMessageId("goal"), state: "completed" });
354
+ assert.equal(turn.read().pendingCommands, 0);
355
+ assert.equal(turn.read().status, "completed");
356
+ });
357
+
358
+ test("late input completion cannot settle a newer native continuation", async () => {
359
+ const { turn, send } = fixture();
360
+ await send();
361
+ await turn.receive({ type: "result", subtype: "success", result: "First answer" });
362
+ await turn.receive({ type: "command_lifecycle", command_uuid: "native-successor", state: "started" });
363
+ await turn.receive({ type: "command_lifecycle", command_uuid: claudeNativeMessageId("first"), state: "completed" });
364
+ assert.equal(turn.read().active, true);
365
+ await turn.receive({ type: "result", subtype: "success", result: "Successor answer" });
366
+ assert.equal(turn.read().status, "completed");
367
+ });
368
+
54
369
  test("Claude native follow-ups reactivate a completed turn without admitting another human message", async () => {
55
370
  for (const start of [
56
371
  { type: "user", uuid: "notification", origin: { kind: "task-notification" } },
@@ -93,6 +408,8 @@ test("Claude authoritative task snapshots repair membership and ignore ambient w
93
408
  await turn.receive({ type: "system", subtype: "background_tasks_changed", tasks: [
94
409
  { task_id: "watcher", task_type: "local_bash", ambient: true }
95
410
  ] });
411
+ assert.equal(turn.read().active, true);
412
+ await turn.receive({ type: "result", subtype: "success", result: "Background work checked" });
96
413
  assert.equal(turn.read().active, false);
97
414
  await turn.receive({ type: "system", subtype: "status", status: null });
98
415
  await turn.receive({ type: "system", subtype: "background_tasks_changed", tasks: [] });
@@ -100,6 +417,8 @@ test("Claude authoritative task snapshots repair membership and ignore ambient w
100
417
  await turn.receive({ type: "system", subtype: "background_tasks_changed", tasks: [{ task_id: "late", task_type: "local_bash" }] });
101
418
  assert.equal(turn.read().active, true, "A later task snapshot restores the working state");
102
419
  await turn.receive({ type: "system", subtype: "task_updated", task_id: "late", patch: { status: "completed" } });
420
+ assert.equal(turn.read().active, true);
421
+ await turn.receive({ type: "result", subtype: "success", result: "Late worker checked" });
103
422
  assert.equal(turn.read().active, false);
104
423
  });
105
424
 
@@ -397,7 +716,7 @@ test("Claude split text followed by a tool is commentary, never a final answer",
397
716
 
398
717
 
399
718
  import { createClaudeConversationOwner } from "../src/server/conversation/claudeTurn.js";
400
- import { mkdtemp, rm } from "node:fs/promises";
719
+ import { mkdtemp, mkdir, rm, readFile, writeFile, appendFile } from "node:fs/promises";
401
720
  import os from "node:os";
402
721
  import path from "node:path";
403
722
  import { createConversationRuntime, createMemoryConversationStorage, createConversationTranscript } from "../src/server/conversation/index.js";
@@ -419,7 +738,7 @@ async function publishedClaudeFixture(t, { publication = true, providerId = "ant
419
738
  ...(releaseExecution ? { releaseExecution } : {}),
420
739
  select() {}, read: (_context, id) => saved.get(id),
421
740
  async save(entry) { saved.set(entry.id, JSON.stringify({ turnId: entry.turn?.id,
422
- state: entry.turn?.state, accountIdentity: entry.accountIdentity })); },
741
+ state: entry.turn?.state, pausedGoal: entry.pausedGoal, blockedGoal: entry.blockedGoal, accountIdentity: entry.accountIdentity })); },
423
742
  async hasMessage() { return false; },
424
743
  readSessionConversations: () => ({ ids: [...saved.keys()], context })
425
744
  },
@@ -427,7 +746,7 @@ async function publishedClaudeFixture(t, { publication = true, providerId = "ant
427
746
  context: async () => context,
428
747
  entry: () => ({ options: { mainId: conversationId }, check() {} }),
429
748
  check() {}, account: () => ({ identity: behavior.account, providerId }),
430
- async configuration() { await behavior.configure?.(); return { systemPrompt: "Retained host instructions", settings: {}, model: providerId === "deepseek" ? "deepseek-flash" : "sonnet" }; },
749
+ async configuration() { await behavior.configure?.(); return { systemPrompt: behavior.systemPrompt || "Retained host instructions", settings: {}, model: providerId === "deepseek" ? "deepseek-flash" : "sonnet" }; },
431
750
  async message(_entry, _input, { message }) { return message; }
432
751
  },
433
752
  process: {
@@ -437,7 +756,35 @@ async function publishedClaudeFixture(t, { publication = true, providerId = "ant
437
756
  async stop() { return { scopeEmpty: true, exited: true }; },
438
757
  client: { async request() { return {}; },
439
758
  async send(_message, input) {
759
+ (behavior.nativeInputs ||= []).push(_message);
440
760
  await options.onEvent({ type: "user", uuid: input.messageId, session_id: options.sessionId });
761
+ if (_message.startsWith("/goal ")) {
762
+ behavior.goalAttachmentWork = (async () => {
763
+ await behavior.beforeGoalAttachment?.();
764
+ const directory = path.join(root, "projects", root.replace(/[^a-zA-Z0-9]/gu, "-"));
765
+ await mkdir(directory, { recursive: true });
766
+ const condition = _message.slice(6);
767
+ if (condition === "clear" && behavior.clearWithoutGoalSentinel) {
768
+ const user = { type: "user", uuid: input.messageId, message: { content: "<command-name>/goal</command-name>\n<command-args>clear</command-args>" } };
769
+ const receipt = { type: "system", subtype: "local_command", uuid: `${input.messageId}-clear`, parentUuid: input.messageId,
770
+ commandRun: { command: "goal", args: "clear" }, content: "<local-command-stdout>No goal set</local-command-stdout>" };
771
+ await appendFile(path.join(directory, `${options.sessionId}.jsonl`), [user, receipt].map(JSON.stringify).join("\n") + "\n");
772
+ await options.onEvent({ type: "result", subtype: "success", local_command: "goal", user_message_uuid: input.messageId });
773
+ return;
774
+ }
775
+ const marker = { type: "attachment", uuid: `${input.messageId}-goal`, timestamp: new Date().toISOString(),
776
+ attachment: { type: "goal_status", condition, sentinel: true, met: condition === "clear" } };
777
+ const user = { type: "user", uuid: input.messageId, parentUuid: marker.uuid, message: { content: _message } };
778
+ const caveat = { type: "user", uuid: `${input.messageId}-caveat`, parentUuid: marker.uuid,
779
+ message: { content: "<local-command-caveat>Native command context</local-command-caveat>" } };
780
+ const receipt = { type: "system", subtype: "local_command", uuid: `${input.messageId}-receipt`, parentUuid: input.messageId,
781
+ commandRun: { command: "goal", args: "clear" }, content: "<local-command-stdout>Goal cleared: prior goal</local-command-stdout>" };
782
+ await appendFile(path.join(directory, `${options.sessionId}.jsonl`), (condition === "clear"
783
+ ? [marker, caveat, { ...user, parentUuid: caveat.uuid }, receipt] : [marker, user]).map(JSON.stringify).join("\n") + "\n");
784
+ if (condition === "clear") await options.onEvent({ type: "result", subtype: "success", local_command: "goal", user_message_uuid: input.messageId });
785
+ })();
786
+ void behavior.goalAttachmentWork.catch(() => {});
787
+ }
441
788
  },
442
789
  async interrupt() {
443
790
  await options.onEvent({ type: "result", subtype: "success", result: "", terminal_reason: "aborted_streaming" });
@@ -478,6 +825,28 @@ async function publishedClaudeFixture(t, { publication = true, providerId = "ant
478
825
  async reopen() { entry = await owner.open(context, { mainId: conversationId }); return entry; } };
479
826
  }
480
827
 
828
+ test("Claude status retains active instructions and applies changed guidance once idle", async t => {
829
+ const f = await publishedClaudeFixture(t);
830
+ await f.send();
831
+ const original = f.entry.process;
832
+ f.behavior.systemPrompt = "Changed project guidance during the active turn.";
833
+ const state = await f.owner.ensureReady(f.entry);
834
+ assert.equal(state.turn.active, true);
835
+ assert.equal(f.entry.process, original);
836
+ assert.equal(f.processes.length, 1);
837
+ await f.frame({ type: "result", subtype: "success", result: "Done" });
838
+ await f.owner.ensureReady(f.entry);
839
+ assert.equal(f.processes.length, 2, "Idle readiness installs the changed instructions");
840
+ });
841
+
842
+ test("Claude active status still rejects account replacement", async t => {
843
+ const f = await publishedClaudeFixture(t);
844
+ await f.send();
845
+ f.behavior.account = "account-b";
846
+ await assert.rejects(f.owner.ensureReady(f.entry), { code: "claude_account_changed" });
847
+ assert.equal(f.entry.process, null);
848
+ });
849
+
481
850
  test("Claude continuation supersedes an older completion checkpoint", { timeout: 10_000 }, async t => {
482
851
  const f = await publishedClaudeFixture(t);
483
852
  await f.send();
@@ -569,6 +938,8 @@ test("Claude final receipt waits for original background settlement and preserve
569
938
  assert.equal(f.final(), null);
570
939
  assert.equal(f.entry.nativeTurn.read().active, true);
571
940
  await f.frame({ type: "system", subtype: "task_notification", task_id: "background", status: "completed" });
941
+ assert.equal(f.final(), null, "The main model must finish processing the task notification");
942
+ await f.frame({ type: "result", subtype: "success", result: "The answer" });
572
943
  assert.equal(f.final().itemId, "claude_saved-answer_0");
573
944
  assert.equal(f.final().outputId, "claude_api-answer_3");
574
945
  assert.equal(f.messages.length, 1, "Original duplicate result adds no synthetic assistant carrier");
@@ -921,11 +1292,11 @@ test("Claude verified native cleanup retains interrupted checkpoint refusal with
921
1292
 
922
1293
  // Retain the original supplied-store/native process fixture and drive it through
923
1294
  // the actual common runtime, including its completion-only admitted receipt.
924
- async function suppliedCommonFixture(t, { tools = true, onDeliveryTiming } = {}) {
1295
+ async function suppliedCommonFixture(t, { tools = true, onDeliveryTiming, representation = "canonical", beforeToolWrite, signal, executeTool = async () => ({}) } = {}) {
925
1296
  const f = await publishedClaudeFixture(t, { canonicalAdmission: true });
926
1297
  const storage = createMemoryConversationStorage();
927
1298
  const transcript = createConversationTranscript({ storage });
928
- const context = { actor: { id: "owner" }, surface: "app" };
1299
+ const context = { actor: { id: "owner" }, surface: "app", signal };
929
1300
  const admissions = [];
930
1301
  const checkpoints = [];
931
1302
  const actions = createActionCatalogue();
@@ -933,7 +1304,7 @@ async function suppliedCommonFixture(t, { tools = true, onDeliveryTiming } = {})
933
1304
  id: "lesson.inspect", version: 1, kind: "query", channels: ["automation"], surfaces: ["app"],
934
1305
  permission: { require: "authenticated" }, idempotency: "none",
935
1306
  input: { schema: createSchema({}), mode: "replace" },
936
- output: { schema: createSchema({}), mode: "replace" }, execute: async () => ({})
1307
+ output: { schema: createSchema({}), mode: "replace" }, execute: executeTool
937
1308
  }] });
938
1309
  let delivery;
939
1310
  f.behavior.admit = event => transcript.writeConversationUserMessage("lesson", {
@@ -958,13 +1329,15 @@ async function suppliedCommonFixture(t, { tools = true, onDeliveryTiming } = {})
958
1329
  transcript: { history: async () => [], readConversationLog: () => transcript.readConversationLog("lesson"),
959
1330
  hasMessage: id => transcript.conversationMessageIdExists("lesson", id),
960
1331
  writeUserMessage: input => transcript.writeConversationUserMessage("lesson", input) },
961
- ...(tools ? { applicationTools: { storage, prepareContext(actual, admission) {
1332
+ ...(tools ? { applicationTools: { storage: beforeToolWrite ? { ...storage, async write(...args) {
1333
+ await beforeToolWrite(); return storage.write(...args);
1334
+ } } : storage, prepareContext(actual, admission) {
962
1335
  admissions.push(admission); return actual;
963
1336
  } } } : {}) };
964
1337
  const runtime = createConversationRuntime({ ...(tools ? { actions } : {}), authorize: () => true, onDeliveryTiming,
965
1338
  host: { conversation: () => binding } });
966
1339
  t.after(() => runtime.close());
967
- const conversation = await runtime.open({ id: "lesson", context });
1340
+ const conversation = await runtime.open({ id: "lesson", context, representation });
968
1341
  f.setBeforeState((entry, event) => {
969
1342
  if (event.state !== "completed") return;
970
1343
  assert.equal(entry.turn.active, true, "Retained checkpoint still precedes saved idle state");
@@ -985,7 +1358,7 @@ async function suppliedCommonFixture(t, { tools = true, onDeliveryTiming } = {})
985
1358
  arguments: { actionId: "lesson.inspect", version: 1 }, _meta: { "claudecode/toolUseId": id } } }
986
1359
  }, { signal: new AbortController().signal });
987
1360
  }
988
- return { ...f, get entry() { return f.entry; }, runtime, conversation, binding, admissions, checkpoints, loadContract, testSignal: t.signal };
1361
+ return { ...f, get entry() { return f.entry; }, runtime, conversation, binding, admissions, checkpoints, loadContract, storage, toolContext: context, testSignal: t.signal };
989
1362
  }
990
1363
 
991
1364
  async function sendCommon(f, messageId) {
@@ -1022,6 +1395,51 @@ test("Claude fast follow-up completes while the earlier common checkpoint is hel
1022
1395
  assert.equal(f.entry.process.stopped, undefined);
1023
1396
  });
1024
1397
 
1398
+ for (const failedSave of [false, true]) test(`Claude goal waits for common final saving, failedSave=${failedSave}`, { timeout: 5_000 }, async t => {
1399
+ const entered = Promise.withResolvers();
1400
+ const release = Promise.withResolvers();
1401
+ let held = false;
1402
+ const f = await suppliedCommonFixture(t, { async beforeToolWrite() {
1403
+ if (!held && f.entry.turn.state === "completed") {
1404
+ held = true;
1405
+ entered.resolve();
1406
+ await release.promise;
1407
+ if (failedSave) throw new Error("Final save unavailable");
1408
+ }
1409
+ } });
1410
+ await sendCommon(f, "before-goal");
1411
+ await f.loadContract();
1412
+ const completing = f.frame({ type: "result", subtype: "success", result: "First answer" });
1413
+ await entered.promise;
1414
+ const command = { action: "set", objective: "Finish the checks", messageId: "finishing-goal",
1415
+ expectedSegmentId: `claude:${f.conversationId}` };
1416
+ f.setBeforeState(() => {});
1417
+ const goalChecked = Promise.withResolvers();
1418
+ f.binding.admission = (_context, operation) => { if (operation === "goal") goalChecked.resolve(); };
1419
+ let result;
1420
+ const setting = f.conversation.updateGoal(command).then(value => { result = value; return value; }, error => { result = error; throw error; });
1421
+ void setting.catch(() => {});
1422
+ try {
1423
+ await goalChecked.promise;
1424
+ await new Promise(resolve => setImmediate(resolve));
1425
+ assert.equal(result, undefined, "The goal waits for the original final save before dispatch");
1426
+ assert.equal(f.entry.command, null);
1427
+ } finally { release.resolve(); }
1428
+ await completing;
1429
+ if (failedSave) {
1430
+ await assert.rejects(setting, { code: "conversation_storage_unavailable" });
1431
+ assert.equal(f.entry.command, null, "Failed saving must not dispatch a goal");
1432
+ } else {
1433
+ assert.equal((await setting).status, "accepted");
1434
+ assert.equal(f.entry.command.messageId, command.messageId);
1435
+ await f.frame({ type: "result", subtype: "success", result: "Checks complete" });
1436
+ await f.conversation.wait();
1437
+ assert.equal((await f.conversation.updateGoal(command)).duplicate, true);
1438
+ const log = await f.binding.transcript.readConversationLog();
1439
+ assert.equal(log.flatMap(turn => turn.messages).filter(message => message.messageId === command.messageId).length, 1);
1440
+ }
1441
+ });
1442
+
1025
1443
  test("Claude delivery timing separates native acknowledgement from the held canonical receipt", { timeout: 10_000 }, async t => {
1026
1444
  const timings = [];
1027
1445
  const f = await suppliedCommonFixture(t, { onDeliveryTiming: event => timings.push(event) });
@@ -1060,9 +1478,24 @@ for (const tools of [true, false]) test(`Claude supplied common runtime checkpoi
1060
1478
  if (tools) f.admissions.at(-1).assertCompletionCurrent();
1061
1479
  });
1062
1480
 
1063
- test("Claude supplied common completion joins accepted steering and checkpoints only its own final", { timeout: 10_000 }, async t => {
1481
+ for (const sendNow of [false, true]) test(`Claude supplied common completion joins accepted steering and checkpoints only its own final, sendNow=${sendNow}`, { timeout: 10_000 }, async t => {
1064
1482
  const f = await suppliedCommonFixture(t);
1065
1483
  await sendCommon(f, "common-first");
1484
+ if (sendNow) {
1485
+ const native = f.entry.process;
1486
+ const send = native.client.send;
1487
+ native.client.supportsSendNow = () => true;
1488
+ native.client.interrupt = async () => { throw Error("Steering must preserve the background worker"); };
1489
+ await f.frame({ type: "system", subtype: "task_started", task_id: "common-worker" });
1490
+ native.client.send = async (message, input) => {
1491
+ assert.equal(input.sendNow, true);
1492
+ await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "queued" });
1493
+ await f.frame({ type: "result", subtype: "error_during_execution", is_error: true,
1494
+ terminal_reason: "aborted_tools", user_message_uuid: claudeNativeMessageId("common-first") });
1495
+ await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "started" });
1496
+ await send(message, input);
1497
+ };
1498
+ }
1066
1499
  await f.loadContract("first-contract");
1067
1500
  const first = f.admissions.at(-1);
1068
1501
  const legacyCompletion = f.entry.completion.promise;
@@ -1076,6 +1509,12 @@ test("Claude supplied common completion joins accepted steering and checkpoints
1076
1509
  await f.loadContract("successor-contract");
1077
1510
  assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
1078
1511
  await f.frame({ type: "result", subtype: "success", result: "The successor question", uuid: "successor-final" });
1512
+ if (sendNow) {
1513
+ assert.equal(f.checkpoints.length, 0, "Foreground completion cannot checkpoint live background work");
1514
+ await f.frame({ type: "system", subtype: "task_notification", task_id: "common-worker", status: "completed" });
1515
+ assert.equal(f.checkpoints.length, 0);
1516
+ await f.frame({ type: "result", subtype: "success", result: "The successor question", uuid: "successor-final" });
1517
+ }
1079
1518
  await f.conversation.wait();
1080
1519
  assert.equal((await legacyCompletion).status, "completed");
1081
1520
  assert.equal(f.checkpoints.length, 1);
@@ -1084,6 +1523,35 @@ test("Claude supplied common completion joins accepted steering and checkpoints
1084
1523
  assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
1085
1524
  });
1086
1525
 
1526
+ for (const preceding of [null, "task_updated", "background_tasks_changed"])
1527
+ test(`Claude background completion retains admitted tools until the main continuation answers: ${preceding}`, { timeout: 5_000 }, async t => {
1528
+ const f = await suppliedCommonFixture(t);
1529
+ await sendCommon(f, "background-authority");
1530
+ await f.loadContract("before-background");
1531
+ await f.frame({ type: "system", subtype: "task_started", task_id: "worker" });
1532
+ await f.frame({ type: "result", subtype: "success", result: "The worker is running." });
1533
+ if (preceding) {
1534
+ await f.frame(preceding === "task_updated"
1535
+ ? { type: "system", subtype: preceding, task_id: "worker", patch: { status: "completed" } }
1536
+ : { type: "system", subtype: preceding, tasks: [] });
1537
+ await new Promise(resolve => setImmediate(resolve));
1538
+ }
1539
+ await f.frame({ type: "system", subtype: "task_notification", task_id: "worker", status: "completed" });
1540
+ // The process delivers the task's completed notice before the main model
1541
+ // consumes it. Let the existing runtime finish any incorrectly queued cleanup.
1542
+ await new Promise(resolve => setImmediate(resolve));
1543
+ await new Promise(resolve => setImmediate(resolve));
1544
+ await f.frame({ type: "user", uuid: "worker-notice", origin: { kind: "task-notification" } });
1545
+ const response = await f.loadContract("after-background");
1546
+ assert.equal(JSON.parse(response.mcp_response.result.content[0].text).ok, true);
1547
+ assert.equal(f.checkpoints.length, 0, "Worker completion is not the main answer's checkpoint");
1548
+ await f.frame({ type: "result", subtype: "success", result: "Worker evidence checked." });
1549
+ await f.conversation.wait();
1550
+ assert.equal(f.checkpoints.length, 1);
1551
+ assert.equal(f.checkpoints[0].final.text, "Worker evidence checked.");
1552
+ assert.equal(f.entry.command, null, "The actual final still releases application authority");
1553
+ });
1554
+
1087
1555
  test("Claude supplied completion retains the original waiter when a reserved successor is rejected before dispatch", { timeout: 10_000 }, async t => {
1088
1556
  const f = await suppliedCommonFixture(t);
1089
1557
  await sendCommon(f, "unreplaced-first");
@@ -1223,3 +1691,840 @@ test("Claude Stop cancels configuration that has not started its native process"
1223
1691
  await assert.rejects(preparing, /stopped|closing/);
1224
1692
  assert.equal(f.processes.length, 0, "Stopped configuration must not launch Claude later");
1225
1693
  });
1694
+
1695
+ // The application has persisted dispatch intent, but the native guard has not
1696
+ // written anything. Its rejection must clear that intent, not trap Retry forever.
1697
+ test("Claude supplied runtime records rejection when the native turn ends before the pipe write", { timeout: 10_000 }, async t => {
1698
+ const f = await suppliedCommonFixture(t, { representation: "native" });
1699
+ let rejected = 0;
1700
+ let sent = 0;
1701
+ f.behavior.initialize = native => {
1702
+ const send = native.client.send;
1703
+ native.client.send = (...args) => { sent += 1; return send(...args); };
1704
+ };
1705
+ await assert.rejects(f.conversation.send({
1706
+ messageId: "stopped-before-write", text: "Do the work",
1707
+ onPromptSending() { f.entry.nativeTurn.stop(); },
1708
+ onPromptRejected() { rejected += 1; }
1709
+ }), error => error.delivery === "not-sent");
1710
+ assert.equal(sent, 0);
1711
+ assert.equal(rejected, 1);
1712
+ assert.equal(f.messages.length, 0);
1713
+ });
1714
+
1715
+ test("only natural completion marks a rejected steer eligible for ordinary Send; Stop and cancellation do not", async () => {
1716
+ for (const ending of ["complete", "stop", "cancel"]) {
1717
+ const f = fixture();
1718
+ await f.send();
1719
+ if (ending === "complete") await f.turn.receive({ type: "result", subtype: "success", result: "Done" });
1720
+ else if (ending === "stop") f.turn.stop();
1721
+ else await f.turn.interrupt(f.client);
1722
+ let writes = 0;
1723
+ f.client.send = async () => { writes += 1; };
1724
+ await assert.rejects(f.send("late"), error => {
1725
+ assert.equal(error.delivery, "not-sent");
1726
+ assert.equal(error.code, ending === "complete" ? "conversation_steer_turn_finished" : "assistant_claude_turn_failed");
1727
+ assert.equal(error.statusCode, ending === "complete" ? 409 : undefined);
1728
+ return true;
1729
+ });
1730
+ assert.equal(writes, 0);
1731
+ f.turn.stop();
1732
+ }
1733
+ });
1734
+
1735
+ test("interruption before a late steer distinguishes completed work from Stop, cancellation and idle state", async () => {
1736
+ for (const ending of ["complete", "stop", "cancel", "idle", "steering"]) {
1737
+ const f = fixture();
1738
+ await f.send();
1739
+ let firstInterrupt;
1740
+ if (ending === "complete" || ending === "idle") {
1741
+ await f.turn.receive({ type: "result", subtype: "success", result: "Done" });
1742
+ if (ending === "idle") f.turn.reset();
1743
+ } else if (ending === "stop") f.turn.stop();
1744
+ else if (ending === "cancel") await f.turn.interrupt(f.client);
1745
+ else {
1746
+ const began = Promise.withResolvers();
1747
+ f.client.interrupt = async () => { began.resolve(); };
1748
+ firstInterrupt = f.turn.interruptGeneration(f.client).catch(() => {});
1749
+ await began.promise;
1750
+ }
1751
+ let interrupts = 0;
1752
+ f.client.interrupt = async () => { interrupts += 1; };
1753
+ await assert.rejects(f.turn.interruptGeneration(f.client), error => {
1754
+ assert.equal(error.delivery, "not-sent");
1755
+ assert.equal(error.code, ending === "complete" ? "conversation_steer_turn_finished" : "assistant_claude_turn_failed");
1756
+ assert.equal(error.statusCode, ending === "complete" ? 409 : undefined);
1757
+ return true;
1758
+ });
1759
+ assert.equal(interrupts, 0);
1760
+ f.turn.stop();
1761
+ await firstInterrupt;
1762
+ }
1763
+ });
1764
+
1765
+ test("folded internal command preserves the original published answer owner", async t => {
1766
+ const f = await publishedClaudeFixture(t);
1767
+ await f.send();
1768
+ await f.frame({ type: "command_lifecycle", command_uuid: "internal-notification", state: "started" });
1769
+ await f.frame({ type: "command_lifecycle", command_uuid: "internal-notification", state: "completed" });
1770
+ await f.frame({ type: "result", subtype: "success", result: "The user's answer" });
1771
+ await f.frame({ type: "command_lifecycle", command_uuid: claudeNativeMessageId("request"), state: "completed" });
1772
+ assert.equal(f.entry.turn.active, false);
1773
+ assert.equal(f.final()?.text, "The user's answer");
1774
+ });
1775
+
1776
+
1777
+ test("failed interruption reports definite non-delivery after the original process cleanup", async t => {
1778
+ const f = await publishedClaudeFixture(t);
1779
+ await f.send("original-input");
1780
+ let writes = 0;
1781
+ f.entry.process.client.send = async () => { writes++; };
1782
+ f.entry.process.client.interrupt = async () => { throw new Error("Interrupt was not acknowledged"); };
1783
+ await assert.rejects(f.send("goal-input"), error => error.delivery === "not-sent" &&
1784
+ error.message === "Interrupt was not acknowledged");
1785
+ assert.equal(writes, 0);
1786
+ assert.equal(f.entry.process, null, "The original stop owner still proves cleanup before returning");
1787
+ });
1788
+
1789
+ test("interrupt confirmation timeout is a recoverable typed failure, not a generic server error", async () => {
1790
+ const { turn, send, client } = fixture();
1791
+ await send();
1792
+ await assert.rejects(turn.interruptGeneration(client, { timeoutMs: 5 }), error =>
1793
+ error.code === "assistant_claude_interrupt_timeout" && error.statusCode === 503 && /not sent/.test(error.message));
1794
+ turn.stop();
1795
+ });
1796
+
1797
+
1798
+ test("failed cleanup after interrupt failure retains the cleanup error and execution custody", async t => {
1799
+ const f = await publishedClaudeFixture(t);
1800
+ await f.send("original-input");
1801
+ const native = f.entry.process;
1802
+ const originalStop = native.stop;
1803
+ native.client.interrupt = async () => { throw new Error("Interrupt was not acknowledged"); };
1804
+ native.stop = async () => ({ scopeEmpty: false, exited: false });
1805
+ try {
1806
+ await assert.rejects(f.send("goal-input"), error => error.code === "claude_stop_unconfirmed" &&
1807
+ error.delivery !== "not-sent");
1808
+ assert.equal(f.entry.process, native);
1809
+ assert.ok(f.entry.executionId);
1810
+ } finally { native.stop = originalStop; }
1811
+ });
1812
+
1813
+ for (const stage of ["preparation", "native-ready"]) test(`cancelled Claude goal cannot dispatch after ${stage}`, { timeout: 5_000 }, async t => {
1814
+ const controller = new AbortController();
1815
+ const f = await suppliedCommonFixture(t, { tools: false, signal: controller.signal });
1816
+ const entered = Promise.withResolvers();
1817
+ const release = Promise.withResolvers();
1818
+ let sends = 0;
1819
+ if (stage === "preparation") f.binding.prepareInput = async input => {
1820
+ entered.resolve(); await release.promise; return input;
1821
+ };
1822
+ f.behavior.initialize = async native => {
1823
+ const send = native.client.send;
1824
+ native.client.send = (...args) => { sends++; return send(...args); };
1825
+ if (stage === "native-ready") { entered.resolve(); await release.promise; }
1826
+ };
1827
+ const operation = f.conversation.updateGoal({ action: "set", objective: "Finish the checks", messageId: "stopped-goal",
1828
+ expectedSegmentId: `claude:${f.conversationId}` });
1829
+ void operation.catch(() => {});
1830
+ await entered.promise;
1831
+ controller.abort(new Error("The person stopped this goal"));
1832
+ release.resolve();
1833
+ await assert.rejects(operation, error => error.delivery === "not-sent" && /goal command was stopped before delivery/.test(error.message));
1834
+ assert.equal(sends, 0, "No cancelled goal reaches the native input");
1835
+ assert.equal((await f.binding.transcript.readConversationLog()).length, 0, "An unsent goal has no accepted receipt");
1836
+ });
1837
+
1838
+ test("raw Claude goal cancellation during native preparation prevents its write", { timeout: 5_000 }, async t => {
1839
+ const f = await publishedClaudeFixture(t);
1840
+ const controller = new AbortController();
1841
+ const entered = Promise.withResolvers();
1842
+ const release = Promise.withResolvers();
1843
+ let sends = 0;
1844
+ f.behavior.initialize = async native => {
1845
+ native.client.send = async () => { sends++; };
1846
+ entered.resolve(); await release.promise;
1847
+ };
1848
+ const operation = f.owner.updateGoal(f.entry, { action: "set", objective: "Finish the checks" }, { signal: controller.signal });
1849
+ void operation.catch(() => {});
1850
+ await entered.promise;
1851
+ controller.abort(new Error("Goal stopped"));
1852
+ release.resolve();
1853
+ await assert.rejects(operation, error => error.delivery === "not-sent");
1854
+ assert.equal(sends, 0);
1855
+ });
1856
+
1857
+ test("aborting an admitted Claude goal preserves its receipt and final output", { timeout: 5_000 }, async t => {
1858
+ const controller = new AbortController();
1859
+ const f = await suppliedCommonFixture(t, { tools: false, signal: controller.signal });
1860
+ const result = await f.conversation.updateGoal({ action: "set", objective: "Finish the checks", messageId: "admitted-goal",
1861
+ expectedSegmentId: `claude:${f.conversationId}` });
1862
+ assert.equal(result.status, "accepted");
1863
+ controller.abort(new Error("The person stopped this goal"));
1864
+ await f.frame({ type: "result", subtype: "success", result: "Work already delivered" });
1865
+ await f.conversation.wait();
1866
+ const log = await f.binding.transcript.readConversationLog();
1867
+ assert.equal(log.flatMap(turn => turn.messages).filter(message => message.messageId === "admitted-goal").length, 1);
1868
+ assert.equal(f.entry.turn.state, "completed");
1869
+ });
1870
+
1871
+ test("Claude goal cancellation at the final native write preserves definite non-delivery", { timeout: 5_000 }, async t => {
1872
+ const controller = new AbortController();
1873
+ const f = await suppliedCommonFixture(t, { tools: false, signal: controller.signal });
1874
+ let sends = 0;
1875
+ f.behavior.initialize = async native => { native.client.send = async () => { sends++; }; };
1876
+ f.setBeforeState((_entry, event) => { if (event.state === "starting") controller.abort(new Error("Stopped at write boundary")); });
1877
+ await assert.rejects(f.conversation.updateGoal({ action: "set", objective: "Finish the checks", messageId: "write-stopped-goal",
1878
+ expectedSegmentId: `claude:${f.conversationId}` }), error => error.delivery === "not-sent");
1879
+ assert.equal(sends, 0);
1880
+ assert.equal((await f.binding.transcript.readConversationLog()).length, 0);
1881
+ });
1882
+
1883
+ for (const action of ["set", "resume"]) for (const finishCount of [1, 2]) test(`Claude goal re-admits a definite finished-turn race once, action=${action}, finishes=${finishCount}`, { timeout: 5_000 }, async t => {
1884
+ const f = await suppliedCommonFixture(t, { tools: false });
1885
+ let preparations = 0;
1886
+ let writes = 0;
1887
+ f.behavior.initialize = async native => {
1888
+ const send = native.client.send;
1889
+ native.client.send = async (...args) => { writes++; return send(...args); };
1890
+ };
1891
+ f.setBeforeState(async (_entry, event) => {
1892
+ // A resumed native process can finish its previous generation during the
1893
+ // host's asynchronous new-input checkpoint, before this input is written.
1894
+ if (event.state === "starting" && ++preparations <= finishCount) {
1895
+ await f.frame({ type: "result", subtype: "success", result: "Previous work finished" });
1896
+ }
1897
+ });
1898
+ let expectedGoalId;
1899
+ if (action === "resume") {
1900
+ const directory = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"));
1901
+ await mkdir(directory, { recursive: true });
1902
+ await writeFile(path.join(directory, `${f.conversationId}.jsonl`), `${JSON.stringify({ type: "attachment",
1903
+ timestamp: "2026-10-16T00:00:00.000Z", attachment: { type: "goal_status", condition: "Finish the checks", met: false } })}\n`);
1904
+ expectedGoalId = (await f.conversation.readGoal()).id;
1905
+ }
1906
+ const input = { action, objective: "Finish the checks", messageId: "racing-goal", expectedGoalId,
1907
+ expectedSegmentId: `claude:${f.conversationId}` };
1908
+ const pending = f.conversation.updateGoal(input);
1909
+ if (finishCount === 2) {
1910
+ await assert.rejects(pending, error => error.code === "conversation_steer_turn_finished" && error.delivery === "not-sent");
1911
+ assert.equal(writes, 0);
1912
+ } else {
1913
+ assert.equal((await pending).status, "accepted");
1914
+ assert.equal(writes, 1, "The same goal is written only after definite non-delivery");
1915
+ await f.frame({ type: "result", subtype: "success", result: "Checks finished" });
1916
+ await f.conversation.wait();
1917
+ assert.equal((await f.conversation.updateGoal(input)).duplicate, true);
1918
+ const log = await f.binding.transcript.readConversationLog();
1919
+ assert.equal(log.flatMap(turn => turn.messages).filter(message => message.messageId === input.messageId).length, 1);
1920
+ }
1921
+ assert.equal(preparations, 2, "Only one re-admission is allowed");
1922
+ });
1923
+
1924
+ for (const action of ["set", "resume"]) test(`Claude goal preparation failure permits a safe same-message retry, action=${action}`, { timeout: 5_000 }, async t => {
1925
+ const f = await suppliedCommonFixture(t, { tools: false });
1926
+ let expectedGoalId;
1927
+ if (action === "resume") {
1928
+ const directory = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"));
1929
+ await mkdir(directory, { recursive: true });
1930
+ await writeFile(path.join(directory, `${f.conversationId}.jsonl`), `${JSON.stringify({ type: "attachment",
1931
+ timestamp: "2026-10-16T00:00:00.000Z", attachment: { type: "goal_status", condition: "Finish the checks", met: false } })}\n`);
1932
+ expectedGoalId = (await f.conversation.readGoal()).id;
1933
+ }
1934
+ const input = { action, objective: "Finish the checks", messageId: "preparation-failed-goal", expectedGoalId,
1935
+ expectedSegmentId: `claude:${f.conversationId}` };
1936
+ let writes = 0;
1937
+ let failPreparation = true;
1938
+ f.behavior.initialize = native => {
1939
+ const send = native.client.send;
1940
+ native.client.send = (...args) => { writes++; return send(...args); };
1941
+ if (failPreparation) throw new Error("Claude preparation stopped before dispatch");
1942
+ };
1943
+ await assert.rejects(f.conversation.updateGoal(input), error =>
1944
+ error.delivery === "not-sent" && /preparation stopped/.test(error.message));
1945
+ assert.equal(writes, 0, "Preparation failed before the authored command reached the native pipe");
1946
+ assert.equal(await f.binding.transcript.hasMessage(input.messageId), false);
1947
+ while (f.entry.command) { f.testSignal.throwIfAborted(); await delay(1); }
1948
+ failPreparation = false;
1949
+ assert.equal((await f.conversation.updateGoal(input)).status, "accepted");
1950
+ assert.equal(writes, 1, "Retry sends the same authored command exactly once");
1951
+ assert.equal(await f.binding.transcript.hasMessage(input.messageId), true);
1952
+ await f.frame({ type: "result", subtype: "success", result: "Checks finished" });
1953
+ await f.conversation.wait();
1954
+ });
1955
+
1956
+ test("Claude goal does not re-admit uncertain native delivery", { timeout: 5_000 }, async t => {
1957
+ const f = await suppliedCommonFixture(t, { tools: false });
1958
+ let writes = 0;
1959
+ f.behavior.initialize = async native => {
1960
+ native.client.send = async () => { writes++; throw Object.assign(new Error("Receipt unknown"), { delivery: "uncertain" }); };
1961
+ };
1962
+ await assert.rejects(f.conversation.updateGoal({ action: "set", objective: "Finish the checks",
1963
+ messageId: "uncertain-goal", expectedSegmentId: `claude:${f.conversationId}` }),
1964
+ error => error.delivery === "uncertain");
1965
+ assert.equal(writes, 1);
1966
+ });
1967
+
1968
+ test("Claude goal re-admission preserves Stop during rejected dispatch cleanup", { timeout: 5_000 }, async t => {
1969
+ const controller = new AbortController();
1970
+ const f = await suppliedCommonFixture(t, { tools: false, signal: controller.signal });
1971
+ let preparations = 0;
1972
+ let writes = 0;
1973
+ f.behavior.initialize = async native => {
1974
+ const send = native.client.send;
1975
+ const stop = native.stop;
1976
+ native.client.send = async (...args) => { writes++; return send(...args); };
1977
+ native.stop = async () => { controller.abort(new Error("The person stopped this goal")); return stop(); };
1978
+ };
1979
+ f.setBeforeState(async (_entry, event) => {
1980
+ if (event.state === "starting") {
1981
+ preparations++;
1982
+ await f.frame({ type: "result", subtype: "success", result: "Previous work finished" });
1983
+ }
1984
+ });
1985
+ await assert.rejects(f.conversation.updateGoal({ action: "set", objective: "Finish the checks",
1986
+ messageId: "race-stopped-goal", expectedSegmentId: `claude:${f.conversationId}` }), /person stopped/);
1987
+ assert.equal(preparations, 1);
1988
+ assert.equal(writes, 0);
1989
+ });
1990
+
1991
+ for (const observation of ["read", "ensureReady", "recover", "wait"]) test(`Claude ${observation} preserves its own pending startup lease`, { timeout: 5_000 }, async t => {
1992
+ const f = await publishedClaudeFixture(t);
1993
+ const entered = Promise.withResolvers();
1994
+ const release = Promise.withResolvers();
1995
+ let stops = 0;
1996
+ f.behavior.initialize = async native => {
1997
+ const stop = native.stop;
1998
+ native.stop = async () => { stops++; return stop(); };
1999
+ entered.resolve();
2000
+ await release.promise;
2001
+ };
2002
+ const starting = f.owner.ensureProcess(f.entry);
2003
+ void starting.catch(() => {});
2004
+ try {
2005
+ await entered.promise;
2006
+ assert.ok(f.entry.executionId);
2007
+ assert.equal(f.entry.process, null);
2008
+ if (observation === "recover") await f.owner.recover([f.entry]);
2009
+ else if (observation === "wait") await waitForClaudeConversationTurn({ state: f.entry,
2010
+ stop: reason => f.entry.nativeTurn.stopProcess(reason), read: () => f.owner.read(f.entry) });
2011
+ else await f.owner[observation](f.entry);
2012
+ assert.equal(stops, 0, "A live startup's retained cleanup handle is not an orphaned execution");
2013
+ } finally { release.resolve(); }
2014
+ await starting;
2015
+ assert.ok(f.entry.process);
2016
+ await f.owner.close(f.entry);
2017
+ assert.equal(stops, 1, "Explicit close still proves cleanup through the original owner");
2018
+ });
2019
+
2020
+ test("Claude failed startup no longer exempts retained cleanup from reconciliation", { timeout: 5_000 }, async t => {
2021
+ const f = await publishedClaudeFixture(t);
2022
+ let stops = 0;
2023
+ f.behavior.initialize = async native => {
2024
+ const stop = native.stop;
2025
+ native.stop = async () => { stops++; return stop(); };
2026
+ throw new Error("Initialization failed with cleanup still owned");
2027
+ };
2028
+ await assert.rejects(f.owner.ensureProcess(f.entry), /Initialization failed/);
2029
+ await f.owner.recover([f.entry]);
2030
+ assert.equal(stops, 1);
2031
+ assert.equal(f.entry.executionId, "");
2032
+ });
2033
+
2034
+ test("Claude cancellation acknowledges its exact no-goal command receipt without waiting for a sentinel", { timeout: 5000 }, async t => {
2035
+ const f = await suppliedCommonFixture(t, { tools: false });
2036
+ await f.conversation.updateGoal({ action: "set", objective: "Finish checks", messageId: "cancel-impossible-goal",
2037
+ expectedSegmentId: `claude:${f.conversationId}` });
2038
+ const file = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"), `${f.conversationId}.jsonl`);
2039
+ await appendFile(file, JSON.stringify({ type: "attachment", timestamp: "2026-10-16T18:08:11.407Z",
2040
+ attachment: { type: "goal_status", condition: "Finish checks", met: false, failed: true } }) + "\n");
2041
+ const blocked = await f.conversation.readGoal();
2042
+ assert.equal(blocked.status, "blocked");
2043
+ f.behavior.clearWithoutGoalSentinel = true;
2044
+ await f.conversation.updateGoal({ action: "cancel", expectedGoalId: blocked.id,
2045
+ expectedSegmentId: `claude:${f.conversationId}`, messageId: "clear-impossible-goal" });
2046
+ assert.equal(await f.conversation.readGoal(), null);
2047
+ const history = await readClaudeHistory({ configRoot: f.context.workdir, workdir: f.context.workdir, conversationId: f.conversationId });
2048
+ assert.equal(history.goalCommandId, claudeNativeMessageId("clear-impossible-goal"));
2049
+ });
2050
+
2051
+ test("Claude goal retains a native impossible evaluation as blocked until explicit Resume", async t => {
2052
+ const f = await suppliedCommonFixture(t, { tools: false });
2053
+ await f.conversation.updateGoal({ action: "set", objective: "Finish checks", messageId: "impossible-goal",
2054
+ expectedSegmentId: `claude:${f.conversationId}` });
2055
+ const initial = await f.conversation.readGoal();
2056
+ const file = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"), `${f.conversationId}.jsonl`);
2057
+ const failed = { type: "attachment", timestamp: "2026-10-16T10:19:52.997Z",
2058
+ attachment: { type: "goal_status", condition: "Finish checks", met: false, failed: true,
2059
+ reason: "The remaining checks require credentials from the user." } };
2060
+ await writeFile(file, JSON.stringify({ ...failed, parent_tool_use_id: "nested" }) + "\n", { flag: "a" });
2061
+ assert.equal((await f.conversation.readGoal()).status, "active", "a nested failure cannot stop the parent goal");
2062
+ await writeFile(file, JSON.stringify(failed) + "\n", { flag: "a" });
2063
+ const blocked = await f.conversation.readGoal();
2064
+ assert.equal(blocked.status, "blocked");
2065
+ assert.equal(blocked.id, initial.id);
2066
+ assert.equal(blocked.objective, initial.objective);
2067
+ assert.equal(blocked.reason, failed.attachment.reason);
2068
+ await f.owner.close(f.entry);
2069
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, "blocked", "shutdown preserves native failure");
2070
+ await f.conversation.updateGoal({ action: "resume", expectedGoalId: blocked.id,
2071
+ expectedSegmentId: `claude:${f.conversationId}`, messageId: "resume-impossible-goal" });
2072
+ assert.equal((await f.conversation.readGoal()).status, "active", "explicit Resume establishes a new native goal attachment");
2073
+ });
2074
+
2075
+ test("Claude goal retains native loop-limit blocking across reads and clears it only on new native goal progress", async t => {
2076
+ const f = await suppliedCommonFixture(t, { tools: false });
2077
+ await f.conversation.updateGoal({ action: "set", objective: "Finish checks", messageId: "blocked-goal",
2078
+ expectedSegmentId: `claude:${f.conversationId}` });
2079
+ const initial = await f.conversation.readGoal();
2080
+ const file = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"), `${f.conversationId}.jsonl`);
2081
+ const append = async frame => writeFile(file, JSON.stringify(frame) + "\n", { flag: "a" });
2082
+ const content = "A hook blocked the turn from ending 9 consecutive times — overriding and ending turn. For Stop/SubagentStop hooks, check stop_hook_active in the input and return success while it's true.";
2083
+ await append({ type: "attachment", timestamp: "2026-10-16T10:19:52.997Z",
2084
+ attachment: { type: "goal_status", condition: "Finish checks", met: false, reason: "Six checks require credentials." } });
2085
+ await append({ type: "assistant", uuid: "quoted-warning", message: { content: [{ type: "text", text: content }] } });
2086
+ await append({ type: "system", subtype: "informational", parent_tool_use_id: "nested", content });
2087
+ assert.equal((await f.conversation.readGoal()).status, "active", "quoted and nested notices cannot block the parent goal");
2088
+ await append({ type: "system", subtype: "informational", timestamp: "2026-10-16T10:19:53.008Z", content });
2089
+ const blocked = await f.conversation.readGoal();
2090
+ assert.equal(blocked.status, "blocked");
2091
+ assert.equal(blocked.id, initial.id, "blocking preserves the original goal identity");
2092
+ assert.match(blocked.reason, /Six checks require credentials/);
2093
+ assert.match(blocked.reason, /Resolve the blocker/);
2094
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, "blocked", "all consumers use the original native history owner");
2095
+ await f.owner.close(f.entry);
2096
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, "blocked", "process shutdown cannot convert native blocking to a pause");
2097
+ await append({ type: "attachment", timestamp: "2026-10-16T10:20:00.000Z",
2098
+ attachment: { type: "goal_status", condition: "Finish checks", met: true, reason: "Checks passed." } });
2099
+ await append({ type: "system", subtype: "informational", timestamp: "2026-10-16T10:20:01.000Z", content });
2100
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, "complete", "a later hook failure cannot reopen completed work");
2101
+ await f.conversation.updateGoal({ action: "set", objective: "Next checks", messageId: "next-goal",
2102
+ expectedSegmentId: `claude:${f.conversationId}` });
2103
+ assert.equal((await f.conversation.readGoal()).status, "active", "new goal attachment supersedes the earlier block");
2104
+ });
2105
+
2106
+ for (const action of ["set", "resume"]) test(`Claude ${action} waits for its exact delayed goal attachment`, { timeout: 5000 }, async t => {
2107
+ const f = await suppliedCommonFixture(t, { tools: false });
2108
+ const directory = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"));
2109
+ await mkdir(directory, { recursive: true });
2110
+ await writeFile(path.join(directory, `${f.conversationId}.jsonl`), JSON.stringify({ type: "attachment", uuid: "previous-marker",
2111
+ timestamp: "2026-10-16T00:00:00.000Z", attachment: { type: "goal_status", condition: "Finish the checks", met: action === "set", sentinel: false } }) + "\n");
2112
+ const previous = await f.conversation.readGoal();
2113
+ const attached = Promise.withResolvers();
2114
+ f.behavior.beforeGoalAttachment = () => attached.promise;
2115
+ let finished = false;
2116
+ const updating = f.conversation.updateGoal({ action, objective: "Finish the checks", expectedGoalId: previous.id,
2117
+ messageId: "delayed-goal", expectedSegmentId: `claude:${f.conversationId}` }).then(result => { finished = true; return result; });
2118
+ while (!(await f.binding.transcript.hasMessage("delayed-goal"))) await delay(1);
2119
+ await delay(20);
2120
+ assert.equal(finished, false, "Admission must not return the previous goal as the command's result");
2121
+ attached.resolve();
2122
+ assert.equal((await updating).status, "accepted");
2123
+ const current = await f.owner.readGoal(f.entry);
2124
+ assert.equal(current.goalCommandId, claudeNativeMessageId("delayed-goal"));
2125
+ assert.notEqual(current.goal.createdAt, Date.parse("2026-10-16T00:00:00.000Z") / 1000);
2126
+ assert.equal(current.goal.status, "active");
2127
+ });
2128
+
2129
+ test("Claude goal attachment timeout retains accepted delivery instead of returning an older goal", { timeout: 5000 }, async t => {
2130
+ const f = await publishedClaudeFixture(t);
2131
+ const attached = Promise.withResolvers();
2132
+ f.behavior.beforeGoalAttachment = () => attached.promise;
2133
+ await assert.rejects(f.owner.updateGoal(f.entry, { action: "set", objective: "Finish checks", messageId: "timeout-goal" },
2134
+ { attachmentTimeoutMs: 10 }), { code: "assistant_claude_goal_attachment_pending", delivery: "accepted" });
2135
+ attached.resolve();
2136
+ await f.behavior.goalAttachmentWork;
2137
+ assert.equal((await f.owner.readGoal(f.entry)).goalCommandId, claudeNativeMessageId("timeout-goal"));
2138
+ });
2139
+
2140
+ test("Stop after Claude goal admission aborts attachment waiting without losing the accepted receipt", { timeout: 5000 }, async t => {
2141
+ const controller = new AbortController();
2142
+ const f = await suppliedCommonFixture(t, { tools: false, signal: controller.signal });
2143
+ const attached = Promise.withResolvers();
2144
+ f.behavior.beforeGoalAttachment = () => attached.promise;
2145
+ const updating = f.conversation.updateGoal({ action: "set", objective: "Finish checks", messageId: "stopped-attaching-goal",
2146
+ expectedSegmentId: `claude:${f.conversationId}` });
2147
+ const rejected = assert.rejects(updating, /abort|stopped/iu);
2148
+ while (!(await f.binding.transcript.hasMessage("stopped-attaching-goal"))) await delay(1);
2149
+ controller.abort(new Error("The goal was stopped"));
2150
+ await rejected;
2151
+ assert.equal(await f.binding.transcript.hasMessage("stopped-attaching-goal"), true);
2152
+ attached.resolve();
2153
+ await f.behavior.goalAttachmentWork;
2154
+ });
2155
+
2156
+ test("Claude goal command proof survives only evaluations of its own condition", { timeout: 5000 }, async t => {
2157
+ const f = await suppliedCommonFixture(t, { tools: false });
2158
+ await f.conversation.updateGoal({ action: "set", objective: "Finish checks", messageId: "condition-proof",
2159
+ expectedSegmentId: `claude:${f.conversationId}` });
2160
+ const file = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"), `${f.conversationId}.jsonl`);
2161
+ const append = condition => appendFile(file, JSON.stringify({ type: "attachment", timestamp: new Date().toISOString(),
2162
+ attachment: { type: "goal_status", condition, met: false } }) + "\n");
2163
+ await append("Finish checks");
2164
+ assert.equal((await f.owner.readGoal(f.entry)).goalCommandId, claudeNativeMessageId("condition-proof"));
2165
+ await append("A different condition");
2166
+ const changed = await f.owner.readGoal(f.entry);
2167
+ assert.equal(changed.goal.objective, "A different condition");
2168
+ assert.equal(changed.goalCommandId, null, "an old command cannot prove a different goal");
2169
+ });
2170
+
2171
+ for (const action of ["steer", "set", "resume"]) test(`Claude ${action} uses targeted Send now without cancelling background workers`, async t => {
2172
+ const f = await publishedClaudeFixture(t);
2173
+ if (action === "resume") await f.owner.updateGoal(f.entry, { action: "set", objective: "Finish checks", messageId: "first" });
2174
+ else await f.send("first");
2175
+ await f.frame({ type: "system", subtype: "task_started", task_id: "live-worker" });
2176
+ const native = f.entry.process;
2177
+ const send = native.client.send;
2178
+ native.client.supportsSendNow = () => true;
2179
+ let stops = 0;
2180
+ native.client.interrupt = async () => { stops++; throw Error("A steer must not send Stop"); };
2181
+ native.client.send = async (message, input) => {
2182
+ assert.equal(input.sendNow, true);
2183
+ // Native 2.1.289 may end the foreground generation before consuming the
2184
+ // queued goal. Its background task remains alive throughout this sequence.
2185
+ await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "queued" });
2186
+ await f.frame({ type: "result", subtype: "error_during_execution", is_error: true,
2187
+ terminal_reason: "aborted_tools", user_message_uuid: claudeNativeMessageId("first") });
2188
+ assert.equal(f.entry.nativeTurn.read().active, true, "Old result cannot finish a queued input or its worker");
2189
+ await f.frame({ type: "command_lifecycle", command_uuid: input.messageId, state: "started" });
2190
+ await send(message, input);
2191
+ };
2192
+ if (action === "steer") await f.send("next");
2193
+ else {
2194
+ const goal = (await f.owner.readGoal(f.entry)).goal;
2195
+ await f.owner.updateGoal(f.entry, { action, objective: "Finish checks", messageId: "next",
2196
+ ...(action === "resume" ? { threadId: f.conversationId, createdAt: goal.createdAt } : {}) });
2197
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, "active");
2198
+ }
2199
+ assert.equal(stops, 0);
2200
+ assert.equal(f.processes.length, 1, "No process replacement");
2201
+ await f.frame({ type: "result", subtype: "success", result: "New instruction accepted" });
2202
+ assert.equal(f.entry.nativeTurn.read().active, true, "Retained worker still owns activity");
2203
+ await f.frame({ type: "system", subtype: "task_notification", task_id: "live-worker", status: "completed" });
2204
+ assert.equal(f.entry.nativeTurn.read().active, true, "The main model still needs to consume the worker result");
2205
+ await f.frame({ type: "result", subtype: "success", result: "Worker result checked" });
2206
+ assert.equal(f.entry.nativeTurn.read().active, false, "The main response settles the completed worker's turn");
2207
+ });
2208
+
2209
+ test("Claude context inspection reuses the native owner and reports the selected model's current usage", async t => {
2210
+ const f = await publishedClaudeFixture(t);
2211
+ const requests = [];
2212
+ let usage = { model: "claude-opus", totalTokens: 12345, maxTokens: 1000000, autoCompactThreshold: 967000 };
2213
+ f.behavior.initialize = native => {
2214
+ native.client.request = async request => {
2215
+ requests.push(request);
2216
+ return request.subtype === "get_context_usage" ? usage : {};
2217
+ };
2218
+ };
2219
+ const first = await f.owner.readContextUsage(f.entry);
2220
+ assert.deepEqual(first, { model: "claude-opus", usedTokens: 12345, windowTokens: 967000 });
2221
+ usage = { model: "claude-haiku", totalTokens: 54321, maxTokens: 200000, autoCompactThreshold: 167000 };
2222
+ assert.deepEqual(await f.owner.readContextUsage(f.entry), { model: "claude-haiku", usedTokens: 54321, windowTokens: 167000 });
2223
+ assert.equal(f.processes.length, 1);
2224
+ assert.equal(f.entry.turn, null);
2225
+ assert.equal(f.messages.length, 0);
2226
+ assert.equal(requests.filter(value => value.subtype === "get_context_usage").length, 2);
2227
+ });
2228
+
2229
+ test("Claude context inspection refuses unavailable usage and honours cancellation before preparation", async t => {
2230
+ const f = await publishedClaudeFixture(t);
2231
+ const controller = new AbortController();
2232
+ controller.abort(new Error("cancelled context inspection"));
2233
+ await assert.rejects(f.owner.readContextUsage(f.entry, { signal: controller.signal }), /cancelled context inspection/);
2234
+ assert.equal(f.processes.length, 0);
2235
+ let usage;
2236
+ f.behavior.initialize = native => { native.client.request = async request => request.subtype === "get_context_usage" ? usage : {}; };
2237
+ for (const invalid of [undefined, {}, { totalTokens: -1, maxTokens: 200000 }, { totalTokens: "1", maxTokens: 200000 },
2238
+ { totalTokens: 1, maxTokens: 0 }, { totalTokens: 1, maxTokens: 200000, autoCompactThreshold: -1 }]) {
2239
+ usage = invalid;
2240
+ await assert.rejects(f.owner.readContextUsage(f.entry), /context usage is unavailable/);
2241
+ }
2242
+ assert.equal(f.messages.length, 0);
2243
+ });
2244
+
2245
+
2246
+ test("bound Main history budget transfers more than 30 complete messages through the original Claude sender", { timeout: 10_000 }, async t => {
2247
+ const f = await suppliedCommonFixture(t, { tools: false, representation: "native" });
2248
+ const history = Array.from({ length: 80 }, (_, index) => ({
2249
+ id: `history-${index}`, version: `v${index}`, role: index % 2 ? "assistant" : "user", text: `${index}:` + "visible text ".repeat(200)
2250
+ }));
2251
+ f.binding.transcript.history = async () => history;
2252
+ await f.binding.state.write({ lastEngine: "codex", engines: { codex: { seen: {} } } });
2253
+ let reads = 0;
2254
+ f.binding.maximumHistoryBytes = async ({ engineId, returning, context, signal, input }) => {
2255
+ reads++;
2256
+ assert.equal(engineId, "claude");
2257
+ assert.equal(returning, false);
2258
+ assert.equal(context.actor.id, "owner");
2259
+ assert.equal(signal.aborted, false);
2260
+ assert.equal(input.message, "Prepare the question.");
2261
+ return 300_000;
2262
+ };
2263
+ const sent = [];
2264
+ f.behavior.initialize = native => {
2265
+ const send = native.client.send;
2266
+ native.client.send = (message, ...args) => { sent.push(message); return send(message, ...args); };
2267
+ };
2268
+ await sendCommon(f, "full-history");
2269
+ assert.equal(reads, 1);
2270
+ assert.ok(sent[0].length > 128_000);
2271
+ for (const message of history) assert.ok(sent[0].includes(JSON.stringify(message.text)), message.id);
2272
+ await f.frame({ type: "result", subtype: "success", result: "Received." });
2273
+ await f.conversation.wait();
2274
+ await sendCommon(f, "ordinary-follow-up");
2275
+ assert.equal(reads, 1, "Unchanged history must not prepare another budget lookup");
2276
+ assert.equal(sent[1], "Prepare the question.");
2277
+ });
2278
+
2279
+ test("bound Main cannot dispatch or retain a pending prompt when its history budget is unavailable", { timeout: 10_000 }, async t => {
2280
+ const f = await suppliedCommonFixture(t, { tools: false, representation: "native" });
2281
+ f.binding.transcript.history = async () => [{ id: "earlier", version: "v1", role: "user", text: "Earlier work." }];
2282
+ await f.binding.state.write({ lastEngine: "codex", engines: { codex: { seen: {} } } });
2283
+ f.binding.maximumHistoryBytes = async () => { throw new Error("Current native context usage unavailable"); };
2284
+ await assert.rejects(f.conversation.send({ messageId: "no-budget", text: "Continue" }), /Current native context usage unavailable/u);
2285
+ assert.equal(f.processes.length, 0);
2286
+ assert.equal(f.messages.length, 0);
2287
+ assert.equal((await f.binding.state.read())?.engines?.claude?.pending, undefined);
2288
+ });
2289
+
2290
+
2291
+ for (const retention of [{ action: "pause", status: "paused" }, { action: "block", status: "blocked", reason: "The human must supply the approval code." }])
2292
+ for (const background of [false, true])
2293
+ test(`Claude common runtime saves a successful self-${retention.action} tool receipt and admits the next request, background=${background}`, { timeout: 5_000 }, async t => {
2294
+ let effects = 0;
2295
+ const f = await suppliedCommonFixture(t, { async executeTool() {
2296
+ effects++;
2297
+ const native = await f.runtime.open({ id: "lesson", context: f.toolContext, representation: "native" });
2298
+ const goal = await native.readGoal();
2299
+ const result = await native.updateGoal({ action: retention.action, reason: retention.reason, threadId: f.conversationId,
2300
+ createdAt: goal.goal.createdAt, objective: goal.goal.objective });
2301
+ assert.equal(result.goal.status, retention.status);
2302
+ return {};
2303
+ } });
2304
+ const directory = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"));
2305
+ await mkdir(directory, { recursive: true });
2306
+ await writeFile(path.join(directory, `${f.conversationId}.jsonl`), JSON.stringify({
2307
+ type: "attachment", uuid: "common-self-pause-goal", timestamp: "2026-10-17T02:00:00.000Z",
2308
+ attachment: { type: "goal_status", condition: "Finish the assigned checks", sentinel: true, met: false }
2309
+ }) + "\n");
2310
+ const original = await f.owner.readGoal(f.entry);
2311
+ const receipt = await sendCommon(f, "common-self-pause");
2312
+ await f.loadContract();
2313
+ if (background) {
2314
+ await f.frame({ type: "system", subtype: "task_started", task_id: "pause-worker" });
2315
+ await f.frame({ type: "result", subtype: "success", result: "Waiting for the worker checkpoint." });
2316
+ await f.frame({ type: "system", subtype: "background_tasks_changed", tasks: [] });
2317
+ await f.frame({ type: "system", subtype: "task_updated", task_id: "pause-worker", patch: { status: "completed" } });
2318
+ await new Promise(resolve => setImmediate(resolve));
2319
+ await f.frame({ type: "system", subtype: "task_notification", task_id: "pause-worker", status: "completed" });
2320
+ await f.frame({ type: "user", uuid: "pause-worker-notice", origin: { kind: "task-notification" } });
2321
+ await f.loadContract("after-worker-contract");
2322
+ }
2323
+ const native = f.entry.process;
2324
+ const input = { actionId: "lesson.inspect", version: 1, input: {} };
2325
+ await f.frame({ type: "assistant", uuid: "self-pause-tool-use", message: { content: [
2326
+ { type: "tool_use", id: "pause-self", name: "mcp__application__assistant_action_execute", input }
2327
+ ] } });
2328
+ const response = await native.options.onControlRequest({ subtype: "mcp_message", server_name: "application",
2329
+ message: { jsonrpc: "2.0", id: "pause-self", method: "tools/call", params: {
2330
+ name: "assistant_action_execute", arguments: input, _meta: { "claudecode/toolUseId": "pause-self" }
2331
+ } }
2332
+ }, { signal: new AbortController().signal });
2333
+ assert.equal(JSON.parse(response.mcp_response.result.content[0].text).ok, true);
2334
+ assert.equal(f.entry.process, native, "The returning native tool still owns its connection");
2335
+ await f.frame({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "pause-self", is_error: false }] } });
2336
+ await f.conversation.wait();
2337
+ assert.equal(effects, 1);
2338
+ const saved = await f.storage.read("lesson", transaction => transaction.readTurn(receipt.turnId));
2339
+ const call = saved.metadata.applicationTools.find(call => call.id.endsWith("pause-self"));
2340
+ assert.equal(call.status, "complete", "The native process stopping must not discard its tool receipt");
2341
+ assert.equal(call.result.ok, true);
2342
+ assert.equal(f.entry.process, null);
2343
+ assert.equal(f.entry.command, null);
2344
+ const retained = await f.owner.readGoal(f.entry);
2345
+ assert.equal(retained.goal.status, retention.status);
2346
+ assert.equal(retained.goal.createdAt, original.goal.createdAt);
2347
+ assert.equal(retained.goal.objective, original.goal.objective);
2348
+ const next = await sendCommon(f, "after-self-pause");
2349
+ assert.equal(next.status, "accepted");
2350
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, retention.status);
2351
+ assert.deepEqual(f.behavior.nativeInputs.slice(-2), ["/goal clear", "Prepare the question."]);
2352
+ assert.equal((await readClaudeHistory({ configRoot: f.context.workdir, workdir: f.context.workdir,
2353
+ conversationId: f.conversationId })).goal, null, "The native checker is disarmed, not merely labelled paused");
2354
+ await f.frame({ type: "result", subtype: "success", result: "Next request completed." });
2355
+ await f.conversation.wait();
2356
+ });
2357
+
2358
+ for (const retention of [{ action: "pause", status: "paused" }, { action: "block", status: "blocked", reason: "An approval code is required." }])
2359
+ for (const ending of ["receipt", "receipt timeout", "human Pause"])
2360
+ test(`Claude self-${retention.action} delivers its real control response before draining its process: ${ending}`, { timeout: 5_000 }, async t => {
2361
+ const f = await claudeSuppliedToolFixture(t);
2362
+ const delivered = Promise.withResolvers();
2363
+ const wire = [];
2364
+ let stream;
2365
+ f.behavior.initialize = native => {
2366
+ const original = native.client;
2367
+ stream = new Duplex({ read() {}, write(chunk, _encoding, callback) {
2368
+ const frame = JSON.parse(String(chunk));
2369
+ wire.push(frame);
2370
+ Promise.resolve().then(async () => {
2371
+ if (frame.type === "user") await original.send(frame.message.content, { messageId: frame.uuid });
2372
+ if (frame.type === "control_response") delivered.resolve(frame);
2373
+ if (frame.type === "control_request") {
2374
+ assert.ok(["interrupt", "apply_flag_settings", "set_model"].includes(frame.request.subtype));
2375
+ this.push(JSON.stringify({ type: "control_response", response: { subtype: "success", request_id: frame.request_id } }) + "\n");
2376
+ if (frame.request.subtype === "interrupt") this.push(JSON.stringify({ type: "result", subtype: "success", terminal_reason: "aborted_streaming" }) + "\n");
2377
+ }
2378
+ }).then(() => callback(), callback);
2379
+ } });
2380
+ native.client = createClaudeJsonClient({ stream, onEvent: native.options.onEvent, onControlRequest: native.options.onControlRequest,
2381
+ onFailure: native.options.onFailure });
2382
+ const stop = native.stop;
2383
+ native.stop = async () => { native.client.close(); return stop(); };
2384
+ };
2385
+ const directory = path.join(f.context.workdir, "projects", f.context.workdir.replace(/[^a-zA-Z0-9]/gu, "-"));
2386
+ await mkdir(directory, { recursive: true });
2387
+ await writeFile(path.join(directory, `${f.conversationId}.jsonl`), JSON.stringify({
2388
+ type: "attachment", uuid: "self-pause-goal", timestamp: "2026-10-17T02:00:00.000Z",
2389
+ attachment: { type: "goal_status", condition: "Finish the assigned checks", sentinel: true, met: false }
2390
+ }) + "\n");
2391
+ const original = await f.owner.readGoal(f.entry);
2392
+ const effects = [];
2393
+ f.tools.execute = async (_input, options) => {
2394
+ assert.equal(options.messageId, "pause-at-checkpoint");
2395
+ effects.push("pause");
2396
+ const result = await f.owner.updateGoal(f.entry, {
2397
+ action: retention.action, reason: retention.reason, threadId: f.conversationId,
2398
+ createdAt: original.goal.createdAt, objective: original.goal.objective
2399
+ });
2400
+ effects.push("paused");
2401
+ return { ok: true, status: result.goal.status };
2402
+ };
2403
+ const running = f.owner.run(f.control, f.command("pause-at-checkpoint"), f.options);
2404
+ void running.catch(() => {});
2405
+ while (!f.accepted.length) {
2406
+ t.signal.throwIfAborted();
2407
+ await new Promise(resolve => setImmediate(resolve));
2408
+ }
2409
+ await f.nativeTool("pause-self");
2410
+ if (ending === "receipt timeout") t.mock.timers.enable({ apis: ["setTimeout"] });
2411
+ const native = f.entry.process;
2412
+ stream.push(JSON.stringify({ type: "control_request", request_id: "pause-control", request: {
2413
+ subtype: "mcp_message", server_name: "application", message: { jsonrpc: "2.0", id: "pause-self", method: "tools/call",
2414
+ params: { name: "inspect_lesson", arguments: { attemptId: "saved-attempt" }, _meta: { "claudecode/toolUseId": "pause-self" } } }
2415
+ } }) + "\n");
2416
+ const response = await delivered.promise;
2417
+ assert.equal(response.response.subtype, "success");
2418
+ assert.deepEqual(JSON.parse(response.response.response.mcp_response.result.content[0].text), { ok: true, status: retention.status });
2419
+ assert.equal(f.entry.process, native, "A successful write alone is not the CLI's tool receipt");
2420
+ assert.equal(wire.some(frame => frame.request?.subtype === "interrupt"), false, "No interrupt overtakes the tool response");
2421
+ await f.frame({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "unrelated-tool" }] } });
2422
+ assert.equal(f.entry.process, native);
2423
+ await assert.rejects(f.call("pause-self"), /retired input/, "No more application effects after the retained stop");
2424
+ if (ending === "receipt") {
2425
+ stream.push(JSON.stringify({ type: "user", session_id: f.conversationId,
2426
+ message: { content: [{ type: "tool_result", tool_use_id: "pause-self", is_error: false }] } }) + "\n");
2427
+ } else if (ending === "human Pause") {
2428
+ await f.owner.updateGoal(f.entry, { ...original.goal, action: "pause" });
2429
+ } else t.mock.timers.tick(30_000);
2430
+ await running;
2431
+ if (ending === "receipt timeout") assert.match(f.entry.turn.error, /did not acknowledge its goal tool response/);
2432
+ assert.deepEqual(effects, ["pause", "paused"]);
2433
+ assert.equal(f.entry.process, null, "Pause proves native process cleanup before releasing its handle");
2434
+ assert.equal(f.entry.command, null, "The enclosing tool/turn owner drains without a self-wait");
2435
+ assert.equal(f.entry.turn.active, false);
2436
+ const retained = await f.owner.readGoal(f.entry);
2437
+ assert.equal(retained.goal.status, ending === "human Pause" ? "paused" : retention.status);
2438
+ assert.equal(f.entry.goalToolStop, null);
2439
+ assert.equal(retained.goal.createdAt, original.goal.createdAt);
2440
+ assert.equal(retained.goal.objective, original.goal.objective);
2441
+ });
2442
+
2443
+
2444
+ for (const retention of [{ action: "pause", status: "paused" }, { action: "block", status: "blocked", reason: "A new human approval code is required." }])
2445
+ test(`Claude ${retention.action} survives ordinary messages and owner recreation; only Resume restores the goal`, { timeout: 5_000 }, async t => {
2446
+ const f = await publishedClaudeFixture(t);
2447
+ await f.owner.updateGoal(f.entry, { action: "set", objective: "Finish every check", messageId: "lifecycle-set" });
2448
+ const original = (await f.owner.readGoal(f.entry)).goal;
2449
+ await f.owner.updateGoal(f.entry, { ...original, action: retention.action, reason: retention.reason });
2450
+ assert.equal(f.entry.process, null);
2451
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, retention.status);
2452
+ await f.send("ordinary-after-pause");
2453
+ assert.deepEqual(f.behavior.nativeInputs.slice(-2), ["/goal clear", "Explain"]);
2454
+ assert.equal((await readClaudeHistory({ configRoot: f.context.workdir, workdir: f.context.workdir, conversationId: f.conversationId })).goal, null);
2455
+ assert.deepEqual((await f.owner.readGoal(f.entry)).goal, { ...original, status: retention.status, ...(retention.reason ? { reason: retention.reason } : {}) });
2456
+ await f.frame({ type: "result", subtype: "success", result: "Ordinary answer" });
2457
+ await f.owner.close(f.entry);
2458
+ await f.reopen();
2459
+ assert.deepEqual((await f.owner.readGoal(f.entry)).goal, { ...original, status: retention.status, ...(retention.reason ? { reason: retention.reason } : {}) });
2460
+ f.behavior.clearWithoutGoalSentinel = true;
2461
+ await f.send("ordinary-after-restart");
2462
+ assert.deepEqual(f.behavior.nativeInputs.slice(-2), ["/goal clear", "Explain"]);
2463
+ await f.frame({ type: "result", subtype: "success", result: "Still ordinary" });
2464
+ await f.owner.updateGoal(f.entry, { action: "resume", ...original, messageId: "lifecycle-resume" });
2465
+ assert.equal(f.entry.pausedGoal, null);
2466
+ assert.equal(f.entry.blockedGoal, null);
2467
+ const resumed = (await f.owner.readGoal(f.entry)).goal;
2468
+ assert.equal(resumed.status, "active");
2469
+ assert.equal(resumed.objective, original.objective);
2470
+ await f.owner.updateGoal(f.entry, { action: "pause", ...resumed });
2471
+ await f.owner.updateGoal(f.entry, { action: "cancel", ...resumed, messageId: "lifecycle-cancel" });
2472
+ assert.equal((await f.owner.readGoal(f.entry)).goal, null);
2473
+ assert.equal(f.entry.pausedGoal, null);
2474
+ assert.equal(f.entry.blockedGoal, null);
2475
+ });
2476
+
2477
+
2478
+ test("Claude refuses ordinary input when the paused native goal could not be cleared, then permits a safe retry", { timeout: 5_000 }, async t => {
2479
+ const f = await publishedClaudeFixture(t);
2480
+ await f.owner.updateGoal(f.entry, { action: "set", objective: "Finish every check", messageId: "refused-set" });
2481
+ const goal = (await f.owner.readGoal(f.entry)).goal;
2482
+ await f.owner.updateGoal(f.entry, { action: "pause", ...goal });
2483
+ f.behavior.initialize = native => {
2484
+ native.client.send = async (message, input) => {
2485
+ assert.equal(message, "/goal clear");
2486
+ await native.options.onEvent({ type: "result", subtype: "error_during_execution", is_error: true,
2487
+ local_command: "goal", user_message_uuid: input.messageId });
2488
+ };
2489
+ };
2490
+ await assert.rejects(f.send("ordinary-refused"), error => error.delivery === "not-sent");
2491
+ assert.equal(f.entry.process, null);
2492
+ assert.deepEqual((await f.owner.readGoal(f.entry)).goal, { ...goal, status: "paused" });
2493
+ f.behavior.initialize = null;
2494
+ await f.send("ordinary-refused");
2495
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, "paused");
2496
+ assert.deepEqual(f.behavior.nativeInputs.slice(-2), ["/goal clear", "Explain"]);
2497
+ await f.frame({ type: "result", subtype: "success", result: "Ordinary retry" });
2498
+ });
2499
+
2500
+
2501
+ test("Pausing an idle Claude goal retires its warm process before the next ordinary message", { timeout: 5_000 }, async t => {
2502
+ const f = await publishedClaudeFixture(t);
2503
+ await f.owner.updateGoal(f.entry, { action: "set", objective: "Finish all checks", messageId: "idle-goal" });
2504
+ await f.frame({ type: "result", subtype: "success", result: "Waiting for a prerequisite" });
2505
+ const goal = (await f.owner.readGoal(f.entry)).goal;
2506
+ assert.ok(f.entry.process);
2507
+ assert.equal(f.entry.turn.active, false);
2508
+ await f.owner.updateGoal(f.entry, { action: "pause", ...goal });
2509
+ assert.equal(f.entry.process, null);
2510
+ await f.send("ordinary-after-idle-pause");
2511
+ assert.deepEqual(f.behavior.nativeInputs.slice(-2), ["/goal clear", "Explain"]);
2512
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, "paused");
2513
+ await f.frame({ type: "result", subtype: "success", result: "Ordinary answer" });
2514
+ });
2515
+
2516
+
2517
+ test("Claude Block refuses absent reasons and stale identity before interrupting", { timeout: 5_000 }, async t => {
2518
+ const f = await publishedClaudeFixture(t);
2519
+ await f.owner.updateGoal(f.entry, { action: "set", objective: "Finish checks", messageId: "block-validation" });
2520
+ const goal = (await f.owner.readGoal(f.entry)).goal;
2521
+ const process = f.entry.process;
2522
+ for (const input of [{ reason: "" }, { reason: "x".repeat(1001) }, { reason: "Missing code", createdAt: goal.createdAt + 1 }]) {
2523
+ await assert.rejects(f.owner.updateGoal(f.entry, { ...goal, action: "block", ...input }), {
2524
+ code: "conversation_invalid_goal", delivery: "not-sent"
2525
+ });
2526
+ assert.equal(f.entry.process, process);
2527
+ assert.equal((await f.owner.readGoal(f.entry)).goal.status, "active");
2528
+ assert.equal(f.entry.blockedGoal, null);
2529
+ }
2530
+ });