@jskit-ai/assistant-core 0.1.212 → 0.1.214

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 (60) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
  3. package/package.json +5 -5
  4. package/src/client/conversation/AssistantConversationElement.vue +5 -1
  5. package/src/client/conversation/AssistantTranscript.vue +10 -1
  6. package/src/server/conversation/agentRun.js +38 -11
  7. package/src/server/conversation/claudeHistory.js +40 -5
  8. package/src/server/conversation/claudeProcess.js +28 -4
  9. package/src/server/conversation/claudeTools.js +11 -2
  10. package/src/server/conversation/claudeTurn.js +213 -23
  11. package/src/server/conversation/codexConfiguration.js +1 -1
  12. package/src/server/conversation/codexHistoryAdapter.js +11 -4
  13. package/src/server/conversation/codexNotificationQueue.js +11 -2
  14. package/src/server/conversation/codexObservationControl.js +8 -8
  15. package/src/server/conversation/codexOutput.js +20 -15
  16. package/src/server/conversation/codexProvider.js +40 -3
  17. package/src/server/conversation/codexProviderOwner.js +110 -1
  18. package/src/server/conversation/codexTurn.js +3 -0
  19. package/src/server/conversation/continuity.js +24 -6
  20. package/src/server/conversation/index.js +1 -1
  21. package/src/server/conversation/nativeHistoryExport.js +97 -0
  22. package/src/server/conversation/openCodeClient.js +47 -1
  23. package/src/server/conversation/openCodeRuntime.js +183 -28
  24. package/src/server/conversation/openCodeTurn.js +65 -24
  25. package/src/server/conversation/providers/api.js +7 -0
  26. package/src/server/conversation/providers/claudeDriver.js +52 -7
  27. package/src/server/conversation/providers/codex.js +19 -2
  28. package/src/server/conversation/providers/codexDriver.js +92 -14
  29. package/src/server/conversation/providers/opencodeDriver.js +31 -9
  30. package/src/server/conversation/runtime.js +424 -65
  31. package/src/server/conversation/runtimeStateUpgrade.js +77 -2
  32. package/src/server/conversation/streams.js +25 -5
  33. package/src/server/conversation/tools.js +2 -2
  34. package/src/server/conversation/transcript.js +23 -8
  35. package/src/server/hosts/claudeHistory.js +1 -0
  36. package/src/server/hosts/claudeProcess.js +1 -0
  37. package/src/server/hosts/codexProvider.js +3 -0
  38. package/src/server/hosts/nativeHistory.js +1 -0
  39. package/src/server/hosts/openCodeClient.js +1 -0
  40. package/src/server/hosts/openCodeTurn.js +1 -0
  41. package/src/server/index.js +1 -1
  42. package/src/server/lib/assistantToolLoop.js +99 -2
  43. package/test/aiConnectionClient.test.js +57 -0
  44. package/test/assistantBoundedToolLoop.test.js +332 -0
  45. package/test/assistantScroll.browser.test.js +151 -0
  46. package/test/claudeConversationSession.test.js +1417 -3
  47. package/test/claudeProcess.test.js +175 -0
  48. package/test/claudeTurn.test.js +466 -0
  49. package/test/codexConversationSession.test.js +1257 -12
  50. package/test/codexHistoryAdapter.test.js +74 -1
  51. package/test/codexTurn.test.js +282 -1
  52. package/test/conversation.test.js +83 -0
  53. package/test/conversationAgentRun.test.js +181 -0
  54. package/test/conversationRuntime.test.js +95 -0
  55. package/test/conversationRuntimeStateUpgrade.test.js +89 -0
  56. package/test/conversationStreaming.test.js +85 -0
  57. package/test/conversationTools.test.js +165 -1
  58. package/test/openCodeClient.test.js +186 -0
  59. package/test/openCodeRuntime.test.js +372 -0
  60. package/test/opencodeConversationSession.test.js +637 -5
@@ -4,6 +4,7 @@ import os, { tmpdir } from "node:os";
4
4
  import path, { join } from "node:path";
5
5
  import { test } from "node:test";
6
6
  import { claudePlanUsage, claudeCodeArguments, claudeModelConfiguration, createClaudeCodeProcess, readClaudeCodeAuthStatus, stopClaudeCodeProcess } from "../src/server/conversation/claudeProcess.js";
7
+ import { createLocalConversationExecution } from "../src/server/conversation/localExecution.js";
7
8
 
8
9
  test("native command construction preserves tool-free constraints without granting permissions by default", () => {
9
10
  const args = claudeCodeArguments({ sessionId: "session", resume: true, model: "sonnet", effort: "high", toolFree: true });
@@ -202,3 +203,177 @@ test("Claude plan usage retains real windows and never invents an allowance afte
202
203
  { id: "five_hour", remainingPercent: 75 }, { id: "seven_day", remainingPercent: 0 }
203
204
  ]);
204
205
  });
206
+
207
+
208
+ test("normal Claude application MCP declaration preserves native source tools and permission defaults", () => {
209
+ const ordinary = claudeCodeArguments({ sessionId: "native", model: "sonnet" });
210
+ const configured = claudeCodeArguments({ sessionId: "native", model: "sonnet", applicationTools: true });
211
+ const position = configured.indexOf("--mcp-config");
212
+ assert.ok(position >= 0);
213
+ assert.deepEqual(JSON.parse(configured[position + 1]), { mcpServers: { application: { type: "sdk", name: "application" } } });
214
+ assert.deepEqual(configured.filter((_value, index) => index !== position && index !== position + 1), ordinary);
215
+ for (const flag of ["--safe-mode", "--strict-mcp-config", "--restricted", "--tools", "--permission-mode", "--allowedTools", "--disallowedTools"]) {
216
+ assert.equal(configured.includes(flag), ordinary.includes(flag), flag);
217
+ }
218
+ assert.equal(configured.includes("bypassPermissions"), false);
219
+ });
220
+
221
+ test("Claude preserves the execution host's failed-start identity and cleanup proof", async () => {
222
+ for (const scopeEmpty of [false, true]) {
223
+ const cleanupError = new Error("Managed cleanup was unavailable");
224
+ const failure = Object.assign(new Error("Managed stream socket did not open"), {
225
+ executionId: "admitted-execution", stopProof: { scopeEmpty },
226
+ ...(!scopeEmpty ? { cleanupError, cleanupFailed: true } : {})
227
+ });
228
+ const stopProof = failure.stopProof;
229
+ let stops = 0;
230
+ const execution = {
231
+ async start() { throw failure; },
232
+ async stop() { stops += 1; return { scopeEmpty: true }; }
233
+ };
234
+ await assert.rejects(createClaudeCodeProcess({ execution }), error => {
235
+ assert.equal(error, failure);
236
+ assert.equal(error.executionId, "admitted-execution");
237
+ assert.equal(error.stopProof, stopProof);
238
+ assert.equal(error.stopProof.scopeEmpty, scopeEmpty);
239
+ if (!scopeEmpty) {
240
+ assert.equal(error.cleanupError, cleanupError);
241
+ assert.equal(error.cleanupFailed, true);
242
+ }
243
+ return true;
244
+ });
245
+ assert.equal(stops, 0);
246
+ }
247
+ });
248
+
249
+ test("Claude handshake failure retains acquired execution custody when cleanup is unconfirmed", { skip: process.platform === "win32" }, async t => {
250
+ for (const throws of [false, true]) {
251
+ const setup = await fixture(t, 'process.stdin.once("data", () => process.stdout.write("invalid frame\\n"));');
252
+ const local = createLocalConversationExecution();
253
+ let executionId;
254
+ const cleanupError = new Error("Managed stop acknowledgement was unavailable");
255
+ const execution = {
256
+ async start(input) {
257
+ const native = await local.start(input);
258
+ executionId = native.id;
259
+ return native;
260
+ },
261
+ async stop(id) {
262
+ assert.equal(id, executionId);
263
+ await local.stop(id);
264
+ if (throws) throw cleanupError;
265
+ return { scopeEmpty: false };
266
+ }
267
+ };
268
+ t.after(() => local.close());
269
+ await assert.rejects(createClaudeCodeProcess({ ...setup, execution }), error => {
270
+ assert.match(error.message, /invalid UTF-8 or JSON/);
271
+ assert.equal(error.executionId, executionId);
272
+ assert.equal(error.stopProof.scopeEmpty, false);
273
+ assert.equal(error.cleanupFailed, true);
274
+ if (throws) assert.equal(error.cleanupError, cleanupError);
275
+ return true;
276
+ });
277
+ }
278
+ });
279
+
280
+
281
+ test("Claude initialization rejects confirmed execution exit before half-open protocol EOF", async () => {
282
+ const { PassThrough, Writable } = await import("node:stream");
283
+ for (const cleanup of ["verified", "unconfirmed", "rejected"]) {
284
+ const requested = Promise.withResolvers();
285
+ const exited = Promise.withResolvers();
286
+ const stdout = new PassThrough();
287
+ const stdin = new Writable({ write(chunk, _encoding, done) {
288
+ requested.resolve(JSON.parse(chunk.toString()));
289
+ done();
290
+ } });
291
+ const cleanupError = new Error("Owned cleanup refused");
292
+ const stops = [];
293
+ const execution = {
294
+ async start() { return { id: "half-open-startup", stdin, stdout, exited: exited.promise }; },
295
+ async stop(id) {
296
+ stops.push(id);
297
+ if (cleanup === "rejected") throw cleanupError;
298
+ return { scopeEmpty: cleanup === "verified" };
299
+ }
300
+ };
301
+ const pending = createClaudeCodeProcess({ execution });
302
+ let failure;
303
+ void pending.catch(error => { failure = error; });
304
+ try {
305
+ assert.equal((await requested.promise).request.subtype, "initialize");
306
+ assert.equal(stdout.readableEnded, false);
307
+ exited.resolve({ code: 1, signal: null });
308
+ // The supplied process receipt must settle startup without a protocol EOF
309
+ // or advancement of the original 30-second control deadline.
310
+ for (let index = 0; index < 5; index += 1) await new Promise(resolve => setImmediate(resolve));
311
+ assert.ok(failure, "Confirmed native exit settles initialization promptly");
312
+ assert.equal(failure.code, "assistant_claude_startup_exited");
313
+ assert.deepEqual(failure.processExit, { code: 1, signal: null });
314
+ assert.equal(failure.executionId, "half-open-startup");
315
+ assert.deepEqual(stops, ["half-open-startup"]);
316
+ assert.equal(failure.stopProof.scopeEmpty, cleanup === "verified");
317
+ assert.equal(failure.cleanupFailed === true, cleanup !== "verified");
318
+ if (cleanup === "rejected") assert.equal(failure.cleanupError, cleanupError);
319
+ } finally {
320
+ stdout.destroy();
321
+ stdin.destroy();
322
+ await pending.catch(() => {});
323
+ }
324
+ }
325
+ });
326
+
327
+ test("Claude initialization preserves rejected execution observation and exact cleanup custody", async () => {
328
+ const { PassThrough, Writable } = await import("node:stream");
329
+ const requested = Promise.withResolvers();
330
+ const exited = Promise.withResolvers();
331
+ const stdout = new PassThrough();
332
+ const stdin = new Writable({ write(_chunk, _encoding, done) { requested.resolve(); done(); } });
333
+ const failure = new Error("Execution observation failed");
334
+ const stops = [];
335
+ const pending = createClaudeCodeProcess({ execution: {
336
+ async start() { return { id: "observed-startup", stdin, stdout, exited: exited.promise }; },
337
+ async stop(id) { stops.push(id); return { scopeEmpty: true }; }
338
+ } });
339
+ const rejected = assert.rejects(pending, error => {
340
+ assert.equal(error, failure);
341
+ assert.equal(error.executionId, "observed-startup");
342
+ assert.deepEqual(error.stopProof, { scopeEmpty: true, exited: true });
343
+ assert.equal(error.cleanupFailed, undefined);
344
+ return true;
345
+ });
346
+ try {
347
+ await requested.promise;
348
+ exited.reject(failure);
349
+ await rejected;
350
+ assert.deepEqual(stops, ["observed-startup"]);
351
+ } finally {
352
+ stdout.destroy();
353
+ stdin.destroy();
354
+ }
355
+ });
356
+
357
+
358
+ test("Claude supplied execution hosts retain initialization without an optional exit receipt", { skip: process.platform === "win32" }, async t => {
359
+ const setup = await fixture(t, `
360
+ import { createInterface } from "node:readline";
361
+ createInterface({ input: process.stdin }).on("line", line => {
362
+ const frame = JSON.parse(line);
363
+ process.stdout.write(JSON.stringify({ type: "control_response", response: {
364
+ subtype: "success", request_id: frame.request_id, response: { originalHost: true }
365
+ } }) + "\\n");
366
+ });
367
+ `);
368
+ const local = createLocalConversationExecution();
369
+ t.after(() => local.close());
370
+ const native = await createClaudeCodeProcess({ ...setup, execution: {
371
+ async start(input) {
372
+ const { id, stdin, stdout } = await local.start(input);
373
+ return { id, stdin, stdout };
374
+ },
375
+ stop: id => local.stop(id)
376
+ } });
377
+ assert.equal(native.initialization.originalHost, true);
378
+ assert.deepEqual(await native.stop(), { scopeEmpty: true, exited: true });
379
+ });
@@ -269,3 +269,469 @@ test("Claude output identity is absent without unique live proof or with a confl
269
269
  else assert.equal(Object.hasOwn(message, "outputId"), false, mode);
270
270
  }
271
271
  });
272
+
273
+
274
+ import { createClaudeConversationOwner } from "../src/server/conversation/claudeTurn.js";
275
+ import { mkdtemp, rm } from "node:fs/promises";
276
+ import os from "node:os";
277
+ import path from "node:path";
278
+
279
+ // Exercise the original supplied-store Send/process/event owner, without the
280
+ // standalone execution-release capability or application-tool branch.
281
+ async function publishedClaudeFixture(t, { publication = true, providerId = "anthropic", releaseExecution, canonicalAdmission = false } = {}) {
282
+ const root = await mkdtemp(path.join(os.tmpdir(), "claude-final-receipt-"));
283
+ const context = { key: root, workdir: root };
284
+ const saved = new Map();
285
+ const messages = [];
286
+ const processes = [];
287
+ const behavior = { account: "account-a", publication };
288
+ let beforeState;
289
+ const owner = createClaudeConversationOwner({ configRoot: root,
290
+ store: {
291
+ ...(releaseExecution ? { releaseExecution } : {}),
292
+ select() {}, read: (_context, id) => saved.get(id),
293
+ async save(entry) { saved.set(entry.id, JSON.stringify({ turnId: entry.turn?.id,
294
+ state: entry.turn?.state, accountIdentity: entry.accountIdentity })); },
295
+ async hasMessage() { return false; }
296
+ },
297
+ preparation: {
298
+ check() {}, account: () => ({ identity: behavior.account, providerId }),
299
+ async configuration() { return { systemPrompt: "Retained host instructions", settings: {}, model: providerId === "deepseek" ? "deepseek-flash" : "sonnet" }; },
300
+ async message(_entry, _input, { message }) { return message; }
301
+ },
302
+ process: {
303
+ configure() { return {}; },
304
+ async create(options) {
305
+ const native = { executionId: `execution-${processes.length}`, options,
306
+ async stop() { return { scopeEmpty: true, exited: true }; },
307
+ client: { async request() { return {}; },
308
+ async send(_message, input) {
309
+ await options.onEvent({ type: "user", uuid: input.messageId, session_id: options.sessionId });
310
+ },
311
+ async interrupt() {
312
+ await options.onEvent({ type: "result", subtype: "success", result: "", terminal_reason: "aborted_streaming" });
313
+ } }
314
+ };
315
+ processes.push(native);
316
+ await options.onStarted(native.executionId);
317
+ return native;
318
+ }
319
+ },
320
+ async onEvent(entry, event) {
321
+ if (event.type === "before-state") await beforeState?.(entry, event);
322
+ if (event.type === "admit-message" && canonicalAdmission) {
323
+ await behavior.beforeAdmit?.();
324
+ return { turnId: `authored-${event.messageId}`, messages: [{ role: "user", messageId: event.messageId, text: event.message }] };
325
+ }
326
+ if (event.type === "message" && event.message.complete && event.message.role === "assistant") {
327
+ await behavior.beforePublish?.();
328
+ const turn = { id: `host-${messages.length}`, messages: [{ messageId: event.message.id,
329
+ outputId: event.message.outputId || event.message.id, role: "assistant", text: event.message.text }] };
330
+ messages.push(turn);
331
+ return behavior.publication ? turn : undefined;
332
+ }
333
+ }
334
+ });
335
+ const conversationId = "12345678-1234-4234-8234-123456789abc";
336
+ let entry = await owner.open(context, { mainId: conversationId });
337
+ t.after(async () => { for (const retained of owner.entries.values()) await owner.close(retained);
338
+ await rm(root, { recursive: true, force: true }); });
339
+ const send = (id = "request") => owner.send(entry, { message: "Explain", messageId: id });
340
+ const frame = input => entry.process.options.onEvent({ session_id: conversationId, ...input });
341
+ const final = () => owner.readFinalAssistantResult(context.key, conversationId, entry.turn?.id || "");
342
+ return { owner, context, conversationId, messages, processes, behavior, send, frame, final,
343
+ get entry() { return entry; }, setBeforeState(callback) { beforeState = callback; },
344
+ async reopen() { entry = await owner.open(context, { mainId: conversationId }); return entry; } };
345
+ }
346
+
347
+ test("Claude final receipt uses the original supplied-store publication and is available before retained idle checkpoint", async t => {
348
+ const f = await publishedClaudeFixture(t);
349
+ await f.send();
350
+ assert.equal(f.final(), null);
351
+ let atCheckpoint;
352
+ f.setBeforeState((entry, event) => {
353
+ if (event.state === "completed") {
354
+ assert.equal(entry.turn.active, true, "Original checkpoint precedes retained idle mutation");
355
+ assert.equal(entry.nativeTurn.read().active, false, "Original native settlement already completed");
356
+ atCheckpoint = f.final();
357
+ }
358
+ });
359
+ await f.frame({ type: "result", subtype: "success", result: "The answer", uuid: "result-native-id" });
360
+ const result = f.final();
361
+ assert.deepEqual(result, atCheckpoint);
362
+ assert.equal(result.threadId, f.conversationId);
363
+ assert.equal(result.turnId, claudeNativeMessageId("request"));
364
+ assert.equal(result.inputMessageId, claudeNativeMessageId("request"));
365
+ assert.equal(result.itemId, "claude_result-native-id_result");
366
+ assert.equal(result.outputId, result.itemId);
367
+ assert.deepEqual(result.conversationTurn, f.messages[0]);
368
+ result.conversationTurn.messages[0].text = "Mutated caller copy";
369
+ assert.equal(f.final().conversationTurn.messages[0].text, "The answer");
370
+ assert.equal(f.owner.readFinalAssistantResult("foreign-context", f.conversationId, result.turnId), null);
371
+ assert.equal(f.owner.readFinalAssistantResult(f.context.key, "foreign-thread", result.turnId), null);
372
+ assert.equal(f.owner.readFinalAssistantResult(f.context.key, f.conversationId, "foreign-turn"), null);
373
+ });
374
+
375
+ test("Claude final receipt waits for original background settlement and preserves saved split output identity", async t => {
376
+ const f = await publishedClaudeFixture(t);
377
+ await f.send();
378
+ await f.frame({ type: "system", subtype: "task_started", task_type: "local_agent", task_id: "background" });
379
+ await f.frame({ type: "stream_event", event: { type: "message_start", message: { id: "api-answer" } } });
380
+ await f.frame({ type: "stream_event", event: { type: "content_block_start", index: 3,
381
+ content_block: { type: "text", text: "" } } });
382
+ await f.frame({ type: "stream_event", event: { type: "content_block_delta", index: 3, delta: { text: "The answer" } } });
383
+ await f.frame({ type: "assistant", uuid: "saved-answer", message: { id: "api-answer",
384
+ content: [{ type: "text", text: "The answer" }] } });
385
+ await f.frame({ type: "result", subtype: "success", result: "The answer" });
386
+ assert.equal(f.final(), null);
387
+ assert.equal(f.entry.nativeTurn.read().active, true);
388
+ await f.frame({ type: "system", subtype: "task_notification", task_id: "background", status: "completed" });
389
+ assert.equal(f.final().itemId, "claude_saved-answer_0");
390
+ assert.equal(f.final().outputId, "claude_api-answer_3");
391
+ assert.equal(f.messages.length, 1, "Original duplicate result adds no synthetic assistant carrier");
392
+ });
393
+
394
+ test("Claude default sink and failed publication cannot supply a canonical final receipt", async t => {
395
+ const f = await publishedClaudeFixture(t, { publication: false });
396
+ await f.send();
397
+ await f.frame({ type: "result", subtype: "success", result: "No host receipt" });
398
+ assert.equal(f.final(), null);
399
+ f.behavior.publication = true;
400
+ await f.send("publication-failure");
401
+ f.behavior.beforePublish = async () => { throw new Error("Host publication failed"); };
402
+ await assert.rejects(f.frame({ type: "result", subtype: "success", result: "Not published" }), /Host publication failed/);
403
+ assert.equal(f.final(), null);
404
+ await f.owner.interrupt(f.entry);
405
+ f.behavior.beforePublish = undefined;
406
+ await f.send("checkpoint-failure");
407
+ f.setBeforeState((_entry, event) => { if (event.state === "completed") throw new Error("Checkpoint failed"); });
408
+ await assert.rejects(f.frame({ type: "result", subtype: "success", result: "Written but not settled" }), /Checkpoint failed/);
409
+ assert.equal(f.final(), null);
410
+ await f.owner.interrupt(f.entry);
411
+ });
412
+
413
+ test("Claude receipt refuses Stop, account/process replacement and closed restored entries", async t => {
414
+ const f = await publishedClaudeFixture(t, { providerId: "deepseek" });
415
+ await f.send();
416
+ await f.frame({ type: "result", subtype: "success", result: "The answer" });
417
+ const originalProcess = f.entry.process;
418
+ f.entry.process = { ...originalProcess };
419
+ assert.equal(f.final(), null);
420
+ f.entry.process = originalProcess;
421
+ f.entry.executionId = "replacement-execution";
422
+ assert.equal(f.final(), null);
423
+ f.entry.executionId = originalProcess.executionId;
424
+ assert.ok(f.final());
425
+ f.behavior.account = "account-b";
426
+ await f.owner.ensureProcess(f.entry);
427
+ assert.equal(f.final(), null);
428
+ f.behavior.account = "account-a";
429
+ await f.owner.ensureProcess(f.entry);
430
+ assert.equal(f.final(), null, "Original account change-back cannot revive the old receipt");
431
+ await f.owner.close(f.entry);
432
+ assert.equal(f.final(), null);
433
+ await f.reopen();
434
+ assert.equal(f.entry.turn.state, "completed", "Existing retained metadata may still describe completion");
435
+ assert.equal(f.final(), null, "Saved completion cannot reconstruct native receipt proof");
436
+ await f.send("next-input");
437
+ await f.owner.interrupt(f.entry);
438
+ assert.equal(f.final(), null);
439
+ });
440
+
441
+ test("Claude steering never adopts an older same-text carrier and current accepted input can publish its own final", async t => {
442
+ const f = await publishedClaudeFixture(t);
443
+ await f.send();
444
+ await f.frame({ type: "assistant", uuid: "first-answer", message: { content: [{ type: "text", text: "Same words" }] } });
445
+ await f.send("steer");
446
+ await f.frame({ type: "result", subtype: "success", result: "Same words" });
447
+ assert.equal(f.final(), null, "Original result deduplication is not evidence for a newer admitted command");
448
+ assert.equal(f.messages.length, 1);
449
+ await f.send("current");
450
+ await f.frame({ type: "result", subtype: "success", result: "Current answer" });
451
+ assert.equal(f.final().inputMessageId, claudeNativeMessageId("current"));
452
+ assert.equal(f.final().text, "Current answer");
453
+ });
454
+
455
+
456
+ test("Claude final receipt refuses original generic execution release while native exit is pending", async t => {
457
+ let releases = 0;
458
+ const f = await publishedClaudeFixture(t, { releaseExecution: async () => { releases++; } });
459
+ await f.send();
460
+ await f.frame({ type: "result", subtype: "success", result: "Completed before exit" });
461
+ assert.ok(f.final());
462
+ const exit = Promise.withResolvers();
463
+ const entered = Promise.withResolvers();
464
+ f.entry.process.stop = async () => { entered.resolve(); return exit.promise; };
465
+ const stopping = f.entry.nativeTurn.stopProcess("Closing generic execution");
466
+ try {
467
+ await entered.promise;
468
+ assert.equal(f.entry.stopping, false, "Original generic release does not set retained-host stopping");
469
+ assert.ok(f.entry.stopPending, "The original pending-stop owner is visible before exit proof");
470
+ assert.equal(f.final(), null, "Completed receipt cannot be used during native cleanup");
471
+ assert.equal(releases, 0, "Original execution release still waits for the native exit proof");
472
+ } finally {
473
+ exit.resolve({ exited: true, scopeEmpty: true });
474
+ await stopping;
475
+ }
476
+ assert.equal(releases, 1);
477
+ assert.equal(f.entry.process, null);
478
+ assert.equal(f.entry.stopPending, null);
479
+ assert.equal(f.final(), null);
480
+ });
481
+
482
+
483
+ test("Claude failed generic native exit cannot revive a completed receipt and original cleanup retries", async t => {
484
+ let releases = 0;
485
+ const f = await publishedClaudeFixture(t, { releaseExecution: async () => { releases++; } });
486
+ await f.send();
487
+ await f.frame({ type: "result", subtype: "success", result: "Completed before failed exit" });
488
+ assert.ok(f.final());
489
+ const native = f.entry.process;
490
+ const exit = Promise.withResolvers();
491
+ const entered = Promise.withResolvers();
492
+ const problem = new Error("Native exit unavailable");
493
+ native.stop = async () => { entered.resolve(); return exit.promise; };
494
+ const stopping = f.entry.nativeTurn.stopProcess("Closing generic execution");
495
+ const failed = assert.rejects(stopping, error => error === problem);
496
+ try {
497
+ await entered.promise;
498
+ assert.equal(f.entry.stopping, false);
499
+ assert.ok(f.entry.stopPending);
500
+ assert.equal(f.final(), null);
501
+ assert.equal(releases, 0);
502
+ exit.reject(problem);
503
+ await failed;
504
+ assert.equal(f.entry.stopPending, null, "Original failed-stop finally clears its pending handle");
505
+ assert.equal(f.entry.process, native, "Original failed-exit ownership remains available for retry");
506
+ assert.equal(f.final(), null, "Failed native exit cannot revive the old completed receipt");
507
+ assert.equal(releases, 0, "Failed exit must not release execution ownership");
508
+ } finally {
509
+ exit.reject(problem);
510
+ await failed;
511
+ native.stop = async () => ({ exited: true, scopeEmpty: true });
512
+ }
513
+ await f.entry.nativeTurn.stopProcess("Retry original native cleanup");
514
+ assert.equal(releases, 1);
515
+ assert.equal(f.entry.process, null);
516
+ assert.equal(f.entry.stopPending, null);
517
+ assert.equal(f.final(), null);
518
+ });
519
+
520
+
521
+ async function claudeSuppliedToolFixture(t) {
522
+ const f = await publishedClaudeFixture(t, { canonicalAdmission: true });
523
+ const control = { current: null, disposed: false };
524
+ const signal = new AbortController();
525
+ const accepted = [];
526
+ const effects = [];
527
+ const schemas = [{ type: "function", function: { name: "inspect_lesson", description: "Inspect the admitted lesson",
528
+ parameters: { type: "object", properties: { attemptId: { type: "string" } }, additionalProperties: false } } }];
529
+ const tools = { schemas, async execute(input, options) {
530
+ assert.ok(accepted.some(value => value.messageId === options.messageId), "The same Core accept callback precedes effects");
531
+ effects.push({ input, messageId: options.messageId });
532
+ await f.behavior.toolWait;
533
+ if (f.behavior.toolError) throw f.behavior.toolError;
534
+ return { ok: true, result: { lesson: "current" } };
535
+ } };
536
+ function command(messageId) {
537
+ return { context: f.context, input: { messageId, nativeMessage: { messageId, message: "Teach this lesson" } },
538
+ signal: signal.signal, tools,
539
+ async beforeDispatch(native) { assert.equal(native.threadId, f.conversationId); },
540
+ async accept(value) {
541
+ assert.equal(value.conversationTurn.turnId, `authored-${messageId}`);
542
+ assert.equal(value.nativeTurnId, f.entry.turn.id);
543
+ accepted.push({ messageId, value });
544
+ } };
545
+ }
546
+ const acquire = async () => f.entry;
547
+ const options = { acquire, reportFailure: async () => {}, maximumToolCalls: 2 };
548
+ function call(id, name = "inspect_lesson", input = { attemptId: "saved-attempt" }) {
549
+ return f.entry.process.options.onControlRequest({ subtype: "mcp_message", server_name: "application",
550
+ message: { jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: input,
551
+ _meta: { "claudecode/toolUseId": id } } } }, { signal: signal.signal });
552
+ }
553
+ async function nativeTool(id, input = { attemptId: "saved-attempt" }) {
554
+ await f.frame({ type: "assistant", uuid: `native-${id}`, message: { content: [
555
+ { type: "tool_use", id, name: "mcp__application__inspect_lesson", input }
556
+ ] } });
557
+ }
558
+ return { ...f, get entry() { return f.entry; }, control, signal, accepted, effects, tools, command, options, call, nativeTool };
559
+ }
560
+
561
+ test("Claude supplied store adopts the actual canonical ACK before existing native application tools", async t => {
562
+ const f = await claudeSuppliedToolFixture(t);
563
+ const admitted = Promise.withResolvers();
564
+ f.behavior.beforeAdmit = async () => { admitted.resolve(); await f.behavior.admissionWait; };
565
+ const hold = Promise.withResolvers();
566
+ f.behavior.admissionWait = hold.promise;
567
+ const running = f.owner.run(f.control, f.command("tools-request"), f.options);
568
+ void running.catch(() => {});
569
+ await admitted.promise;
570
+ assert.equal(f.entry.command.admitted, false);
571
+ assert.equal(f.accepted.length, 0);
572
+ await f.nativeTool("early-call");
573
+ await assert.rejects(f.call("early-call"), /admitted native tool use/);
574
+ assert.equal(f.effects.length, 0);
575
+ hold.resolve();
576
+ while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
577
+ const native = f.entry.process;
578
+ assert.equal(typeof native.options.onControlRequest, "function");
579
+ assert.equal(f.owner.applicationToolsSupported, true);
580
+ await f.nativeTool("current-call");
581
+ const response = await f.call("current-call");
582
+ assert.equal(JSON.parse(response.mcp_response.result.content[0].text).ok, true);
583
+ assert.equal(f.effects[0].messageId, "tools-request");
584
+ await assert.rejects(f.call("forged-call"), /admitted native tool use/);
585
+ await assert.rejects(f.call("current-call", "inspect_lesson", { attemptId: "foreign" }), /admitted native tool use/);
586
+ await f.frame({ type: "result", subtype: "success", result: "Current lesson" });
587
+ await running;
588
+ assert.equal(f.entry.command, null);
589
+ assert.equal(f.entry.process, native, "Original supplied-store completion retains its process");
590
+ assert.equal(f.entry.turn.state, "completed");
591
+ });
592
+
593
+ test("Claude supplied tools retain the originating tool message while steering adopts its own canonical ACK", async t => {
594
+ const f = await claudeSuppliedToolFixture(t);
595
+ const running = f.owner.run(f.control, f.command("first-tool-request"), f.options);
596
+ void running.catch(() => {});
597
+ while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
598
+ await f.nativeTool("first-owned-call");
599
+ await f.owner.steer(f.control, f.command("steered-tool-request"), f.options);
600
+ await f.nativeTool("steered-owned-call");
601
+ await assert.rejects(f.call("first-owned-call"), /retired input/);
602
+ await f.call("steered-owned-call");
603
+ assert.deepEqual(f.effects.map(value => value.messageId), ["steered-tool-request"]);
604
+ assert.deepEqual(f.accepted.map(value => value.messageId), ["first-tool-request", "steered-tool-request"]);
605
+ await f.frame({ type: "result", subtype: "success", result: "Steered lesson" });
606
+ await running;
607
+ assert.equal(f.entry.command, null);
608
+ });
609
+
610
+ test("Claude supplied run joins existing native tool work before dropping its command custody", async t => {
611
+ const f = await claudeSuppliedToolFixture(t);
612
+ const running = f.owner.run(f.control, f.command("held-tool-request"), f.options);
613
+ void running.catch(() => {});
614
+ while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
615
+ const finishTool = Promise.withResolvers();
616
+ f.behavior.toolWait = finishTool.promise;
617
+ await f.nativeTool("held-native-call");
618
+ const tool = f.call("held-native-call");
619
+ while (!f.effects.length) await new Promise(resolve => setImmediate(resolve));
620
+ let finished = false;
621
+ const finishedRun = running.then(() => { finished = true; });
622
+ await f.frame({ type: "result", subtype: "success", result: "Cannot overtake tool work" });
623
+ await new Promise(resolve => setImmediate(resolve));
624
+ assert.equal(finished, false);
625
+ assert.ok(f.entry.command);
626
+ assert.equal(f.final(), null, "A result observed before its original tool work finishes has no usable final proof");
627
+ finishTool.resolve();
628
+ await tool;
629
+ await finishedRun;
630
+ assert.equal(f.entry.command, null);
631
+ });
632
+
633
+
634
+ test("Claude current supplied turn reader refuses restored/process/account/closing stand-ins", async t => {
635
+ const f = await claudeSuppliedToolFixture(t);
636
+ const read = () => f.owner.readRetainedTurn(f.context.key, f.conversationId, { saved: { turnId: "saved", state: "active" }, current: true });
637
+ assert.equal(read(), null);
638
+ const running = f.owner.run(f.control, f.command("live-reader-request"), f.options);
639
+ void running.catch(() => {});
640
+ while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
641
+ assert.equal(read().id, f.entry.turn.id);
642
+ const native = f.entry.process;
643
+ f.entry.process = null;
644
+ assert.equal(read(), null);
645
+ f.entry.process = native;
646
+ const account = f.entry.accountIdentity;
647
+ f.entry.accountIdentity = "different-account";
648
+ assert.equal(read(), null);
649
+ f.entry.accountIdentity = account;
650
+ f.entry.stopping = true;
651
+ assert.equal(read(), null);
652
+ f.entry.stopping = false;
653
+ assert.equal(read().active, true);
654
+ await f.frame({ type: "result", subtype: "success", result: "Complete live turn" });
655
+ await running;
656
+ assert.equal(read(), null, "A retained completed turn cannot admit a new effect");
657
+ assert.equal(f.owner.readRetainedTurn(f.context.key, f.conversationId, { saved: null }).state, "completed",
658
+ "Original read-only retained presentation remains unchanged");
659
+ });
660
+
661
+ test("Claude supplied current executor failure uses original Stop and command drain", async t => {
662
+ const f = await claudeSuppliedToolFixture(t);
663
+ const running = f.owner.run(f.control, f.command("current-effect-failure"), f.options);
664
+ void running.catch(() => {});
665
+ while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
666
+ const native = f.entry.process;
667
+ let stops = 0;
668
+ const stop = native.stop;
669
+ native.stop = async () => { stops++; return stop.call(native); };
670
+ f.behavior.toolError = new Error("Actual supplied executor failure");
671
+ await f.nativeTool("owned-failure");
672
+ await assert.rejects(f.call("owned-failure"), /Actual supplied executor failure/);
673
+ await assert.rejects(running, /Actual supplied executor failure/);
674
+ assert.equal(stops, 1);
675
+ assert.equal(f.entry.process, null);
676
+ assert.equal(f.entry.command, null);
677
+ assert.equal(f.final(), null);
678
+ });
679
+
680
+ test("Claude admitted old executor failure cannot stop its accepted steering successor", async t => {
681
+ const f = await claudeSuppliedToolFixture(t);
682
+ const running = f.owner.run(f.control, f.command("old-held-effect"), f.options);
683
+ void running.catch(() => {});
684
+ while (!f.accepted.length) await new Promise(resolve => setImmediate(resolve));
685
+ const native = f.entry.process;
686
+ let stops = 0;
687
+ const stop = native.stop;
688
+ native.stop = async () => { stops++; return stop.call(native); };
689
+ const held = Promise.withResolvers();
690
+ f.behavior.toolWait = held.promise;
691
+ await f.nativeTool("old-held-call");
692
+ const old = f.call("old-held-call");
693
+ void old.catch(() => {});
694
+ while (!f.effects.length) await new Promise(resolve => setImmediate(resolve));
695
+ await f.owner.steer(f.control, f.command("accepted-successor"), f.options);
696
+ held.reject(new Error("Old admitted effect failed late"));
697
+ await assert.rejects(old, /Old admitted effect failed late/);
698
+ assert.equal(f.entry.command.messageId, "accepted-successor");
699
+ assert.equal(f.entry.command.toolFailure, undefined);
700
+ assert.equal(stops, 0);
701
+ f.behavior.toolWait = undefined;
702
+ await f.frame({ type: "result", subtype: "success", result: "Successor completed" });
703
+ await running;
704
+ assert.equal(stops, 0);
705
+ assert.equal(f.entry.process, native);
706
+ });
707
+
708
+
709
+ test("Claude verified native cleanup retains interrupted checkpoint refusal without leaving saved work active", async t => {
710
+ const f = await publishedClaudeFixture(t);
711
+ await f.send("checkpoint-refused-stop");
712
+ const native = f.entry.process;
713
+ const refusal = new Error("Saved lesson checkpoint refused");
714
+ let stopped = false;
715
+ native.stop = async () => { stopped = true; return { exited: true, scopeEmpty: true }; };
716
+ f.setBeforeState((_entry, event) => {
717
+ if (event.state === "interrupted") {
718
+ assert.equal(stopped, true, "Original native cleanup precedes the host checkpoint");
719
+ throw refusal;
720
+ }
721
+ });
722
+ const completion = f.entry.completion.promise;
723
+ await assert.rejects(f.entry.nativeTurn.stopProcess("Broken stream"), error => error === refusal);
724
+ assert.equal(f.entry.process, null);
725
+ assert.equal(f.entry.executionId, "");
726
+ assert.deepEqual(f.entry.exitProof, { exited: true, scopeEmpty: true });
727
+ assert.equal(f.entry.turn.active, false);
728
+ assert.equal(f.entry.turn.state, "interrupted");
729
+ assert.equal(f.entry.turn.error, refusal.message);
730
+ const settled = await completion;
731
+ assert.equal(settled.status, "interrupted");
732
+ assert.equal(settled.error, refusal.message);
733
+ assert.equal(f.final(), null, "Refused checkpoint supplies no completed answer receipt");
734
+ await f.reopen();
735
+ assert.equal(f.entry.turn.active, false, "Original supplied store retained the truthful interruption");
736
+ assert.equal(f.entry.turn.state, "interrupted");
737
+ });