@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
@@ -277,6 +277,39 @@ test("repeated calls in one admitted turn reuse receipts; interrupted reservatio
277
277
  assert.deepEqual(completed.records(), first.records(), "Restored receipts remain part of the turn's saved state");
278
278
  });
279
279
 
280
+ test("completed-response tools retain discovery contracts only in their supplied worker catalogue set", async () => {
281
+ let executions = 0, allowed = true;
282
+ const catalog = createServiceToolCatalog(actionCatalog(async () => { executions++; return { total: 5 }; }),
283
+ { maxDirectTools: 0, isActionAvailable: () => allowed });
284
+ const toolSet = catalog.resolveToolSet(context);
285
+ const saved = [];
286
+ const create = retainedToolSet => createConversationTools({ catalog, toolSet: retainedToolSet, context,
287
+ signal: new AbortController().signal, authorize: async () => {},
288
+ save: async calls => saved.push(structuredClone(calls)), emit: async () => {} });
289
+ const contract = { id: "contract-response:operation", name: "assistant_action_contract",
290
+ arguments: JSON.stringify({ actionId: "numbers.add", version: 1 }) };
291
+ const execute = { id: "execute-response:operation", name: "assistant_action_execute",
292
+ arguments: JSON.stringify({ actionId: "numbers.add", version: 1, input: { left: 2, right: 3 } }) };
293
+ assert.equal((await create(toolSet).execute(contract)).ok, true);
294
+ const isolated = await create().execute(execute);
295
+ assert.equal(isolated.error.code, "assistant_action_contract_required");
296
+ assert.equal(executions, 0, "A separate worker must load its own contract");
297
+ const nextResponse = create(toolSet);
298
+ const result = await nextResponse.execute(execute);
299
+ assert.deepEqual(result, { ok: true, result: { actionId: "numbers.add", version: 1, result: { total: 5 } } });
300
+ assert.deepEqual(await nextResponse.execute(execute), result);
301
+ assert.equal(executions, 1, "Retaining the contract does not repeat the durable response operation");
302
+ assert.ok(saved.every(calls => calls.length === 1), "Each response retains its own durable operation receipt");
303
+ allowed = false;
304
+ const revoked = await create(toolSet).execute({ ...execute, id: "revoked-response:operation" });
305
+ assert.equal(revoked.error.code, "assistant_action_unknown");
306
+ assert.equal(executions, 1, "A retained contract never overrides current action availability");
307
+ allowed = true;
308
+ const reset = await create(catalog.resolveToolSet(context)).execute({ ...execute, id: "reset-response:operation" });
309
+ assert.equal(reset.error.code, "assistant_action_contract_required");
310
+ assert.equal(executions, 1, "A new worker or scope starts with no loaded contracts");
311
+ });
312
+
280
313
  test("a later admitted turn can reuse a model's tool id with different arguments", async t => {
281
314
  const f = await fixture(t, { execute: ({ left, right }) => ({ total: left + right }),
282
315
  response: (_body, round, name) => round % 2 ? new Response(frame({ tool_calls: [{
@@ -529,7 +562,8 @@ test("bound tools use exact accepted host rows and fresh server context without
529
562
  assert.equal(f.observed.mappings.length, 2);
530
563
  assert.deepEqual(f.observed.mappings[1].admission, { conversationId: "one", turnId: before.turnId,
531
564
  messageId: message.messageId, nativeTurnId: "native-turn", nativeThreadId: "native-thread", origin: "user",
532
- assertCurrent: f.observed.mappings[1].admission.assertCurrent });
565
+ assertCurrent: f.observed.mappings[1].admission.assertCurrent,
566
+ assertCompletionCurrent: f.observed.mappings[1].admission.assertCompletionCurrent });
533
567
  assert.equal(f.observed.mappings[0].actual, context);
534
568
  assert.deepEqual(f.observed.contexts[0].boundAdmission, f.observed.mappings[1].admission);
535
569
  await f.tools().execute(f.nativeCall);
@@ -726,3 +760,133 @@ test("bound admission guard preserves the original Stop cancellation reason", as
726
760
  assert.equal(command.signal.reason.message, "Work stopped.");
727
761
  assert.equal(f.observed.executions.length, 0, "Stop and its authority guard must not execute an application effect");
728
762
  });
763
+
764
+
765
+ test("an invoked bound effect failing after accepted steering retains its error without failing the successor", async t => {
766
+ const entered = Promise.withResolvers(), effect = Promise.withResolvers();
767
+ const f = await boundToolFixture(t, { execute: async (_input, actual) => {
768
+ entered.resolve(actual);
769
+ return effect.promise;
770
+ } });
771
+ const events = [];
772
+ f.conversation.subscribe(event => events.push(event));
773
+ await f.conversation.send(message);
774
+ await f.currentCommand();
775
+ await f.loadContract();
776
+ const executing = f.tools().execute(f.nativeCall);
777
+ void executing.catch(() => {});
778
+ const origin = await entered.promise;
779
+ await f.conversation.send({ messageId: "late-effect-successor", text: "Continue with this current instruction.", steer: true });
780
+ await f.currentCommand(1);
781
+ effect.reject(Object.assign(new Error("Old effect acknowledgement failed"), { code: "old_effect_failure", statusCode: 503 }));
782
+ await assert.rejects(executing, { code: "conversation_tool_outcome_unknown" });
783
+ f.finish.resolve();
784
+ const result = await f.conversation.wait();
785
+ assert.equal(events.findLast(event => event.type === "settled").status, "complete");
786
+ assert.equal(result.error, "");
787
+ const old = result.conversationLog.find(turn => turn.turnId === origin.boundAdmission.turnId);
788
+ const successor = result.conversationLog.find(turn => turn.messages.some(message => message.messageId === "late-effect-successor"));
789
+ const receipt = old.metadata.applicationTools.find(call => call.id === f.nativeCall.id);
790
+ assert.equal(receipt.status, "unknown");
791
+ assert.equal(receipt.result.error.code, "old_effect_failure");
792
+ assert.equal(successor.metadata.applicationTools, undefined, "The earlier effect error stays on its originating receipt");
793
+ assert.equal(origin.boundAdmission.messageId, message.messageId);
794
+ assert.equal(f.observed.executions.length, 1, "Neither steering nor failed acknowledgement repeats the admitted effect");
795
+ });
796
+
797
+ test("a current bound effect failure still fails its own run and retains its original error receipt", async t => {
798
+ const f = await boundToolFixture(t, { execute: async () => {
799
+ throw Object.assign(new Error("Current effect acknowledgement failed"), { code: "current_effect_failure", statusCode: 503 });
800
+ } });
801
+ const events = [];
802
+ f.conversation.subscribe(event => events.push(event));
803
+ await f.conversation.send(message);
804
+ await f.currentCommand();
805
+ await f.loadContract();
806
+ await assert.rejects(f.tools().execute(f.nativeCall), { code: "conversation_tool_outcome_unknown" });
807
+ f.finish.resolve();
808
+ const result = await f.conversation.wait();
809
+ assert.equal(events.findLast(event => event.type === "settled").status, "failed");
810
+ assert.match(result.error, /Inspect its target/);
811
+ const receipt = result.conversationLog[0].metadata.applicationTools.find(call => call.id === f.nativeCall.id);
812
+ assert.equal(receipt.status, "unknown");
813
+ assert.equal(receipt.result.error.code, "current_effect_failure");
814
+ assert.equal(f.observed.executions.length, 1);
815
+ });
816
+
817
+
818
+ test("bound admission completion guard retains only its latest successful native settlement", async t => {
819
+ for (const boundary of ["completed", "steered", "stopped", "closed", "successor-active", "successor-completed", "recovered-successor", "rejected-successor"]) {
820
+ await t.test(boundary, async t => {
821
+ const f = await boundToolFixture(t);
822
+ await f.conversation.send(message);
823
+ const command = await f.currentCommand();
824
+ await f.loadContract();
825
+ await f.tools().execute(f.nativeCall);
826
+ const original = f.observed.mappings[0].admission;
827
+ assert.equal(typeof original.assertCompletionCurrent, "function");
828
+ assert.equal(JSON.parse(JSON.stringify(original)).assertCompletionCurrent, undefined);
829
+ assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_tool_request_retired" },
830
+ "A working native turn cannot authorize completion");
831
+ if (boundary === "stopped") {
832
+ await f.conversation.cancel();
833
+ assert.throws(() => original.assertCompletionCurrent(), error => error === command.signal.reason);
834
+ return;
835
+ }
836
+ if (boundary === "steered") {
837
+ await f.conversation.send({ messageId: "completion-steer", text: "Use this newer instruction.", steer: true });
838
+ await f.currentCommand(1);
839
+ }
840
+ f.finish.resolve();
841
+ await f.conversation.wait();
842
+ assert.throws(() => original.assertCurrent(), { code: "conversation_tool_request_retired" },
843
+ "Native settlement never restores live tool authority");
844
+ if (boundary === "steered") {
845
+ assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
846
+ return;
847
+ }
848
+ original.assertCompletionCurrent();
849
+ if (boundary === "closed") {
850
+ await f.runtime.close();
851
+ assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
852
+ } else if (boundary === "rejected-successor") {
853
+ f.binding.prepareInput = async () => { throw new Error("The newer input was rejected before native dispatch."); };
854
+ await assert.rejects(f.conversation.send({ messageId: "unaccepted-successor", text: "Never admitted." }),
855
+ /rejected before native dispatch/u);
856
+ await f.conversation.wait();
857
+ original.assertCompletionCurrent();
858
+ assert.equal(f.observed.commands.length, 1, "Rejected input cannot replace the accepted receipt owner");
859
+ } else if (boundary === "recovered-successor") {
860
+ f.binding.native.owner.run = async (_control, current) => {
861
+ await current.beforeDispatch({ threadId: "native-thread" });
862
+ await f.binding.transcript.writeUserMessage({ messageId: current.input.messageId, text: current.input.text });
863
+ return { value: { ok: true, delivered: true } };
864
+ };
865
+ const receipt = await f.conversation.send({ messageId: "recovered-successor", text: "An original recovered native receipt." });
866
+ assert.equal(receipt.recovered, true);
867
+ await f.conversation.wait();
868
+ assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_tool_request_retired" },
869
+ "Verified recovered admission also retires the previous completion receipt");
870
+ } else if (boundary.startsWith("successor-")) {
871
+ const gate = Promise.withResolvers();
872
+ const run = f.binding.native.owner.run;
873
+ f.binding.native.owner.run = async (...args) => {
874
+ await run(...args);
875
+ if (boundary === "successor-active") await gate.promise;
876
+ };
877
+ try {
878
+ await f.conversation.send({ messageId: "accepted-successor", text: "A genuinely accepted later input." });
879
+ await f.currentCommand(1);
880
+ if (boundary === "successor-active") {
881
+ assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
882
+ gate.resolve();
883
+ }
884
+ await f.conversation.wait();
885
+ assert.throws(() => original.assertCompletionCurrent(), { code: "conversation_tool_request_retired" },
886
+ "A later accepted receipt stays current after its active slot clears");
887
+ } finally { gate.resolve(); }
888
+ }
889
+ assert.equal(f.observed.executions.length, 1, "Completion checks cannot dispatch or replay application effects");
890
+ });
891
+ }
892
+ });
@@ -513,3 +513,189 @@ test("OpenCode retained storage validates complete inventories and bounded cance
513
513
  await assert.rejects(cancelledPage, { name: "AbortError" });
514
514
 
515
515
  });
516
+
517
+
518
+ test("stopped OpenCode native database retains the original MessageV2 projection without native writes", async t => {
519
+ const { mkdtemp, readFile, lstat, readdir, rm } = await import("node:fs/promises");
520
+ const { tmpdir } = await import("node:os");
521
+ const path = await import("node:path");
522
+ const { DatabaseSync } = await import("node:sqlite");
523
+ const { readOpenCodeConversationDatabase } = await import("../src/server/hosts/openCodeClient.js");
524
+ const { readStoppedNativeDatabase } = await import("../src/server/hosts/nativeHistory.js");
525
+ const root = await mkdtemp(path.join(tmpdir(), "assistant-native-opencode-"));
526
+ t.after(() => rm(root, { recursive: true, force: true }));
527
+ const file = path.join(root, "native space.db");
528
+ const db = new DatabaseSync(file);
529
+ db.exec(`CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT, project_id TEXT);
530
+ CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
531
+ CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, data TEXT);
532
+ CREATE TABLE session_input (session_id TEXT, promoted_seq INTEGER);`);
533
+ db.prepare("INSERT INTO session VALUES (?, ?, ?)").run("ses_original", "/private/actor/colleague_scope", "project_original");
534
+ const insert = db.prepare("INSERT INTO message VALUES (?, ?, ?, ?)");
535
+ insert.run("msg_original_user", "ses_original", 1, JSON.stringify({ id: "untrusted-payload-id", role: "user", time: { created: 1 } }));
536
+ insert.run("msg_original_answer", "ses_original", 2, JSON.stringify({ role: "assistant", parentID: "msg_original_user", finish: "stop", time: { created: 2, completed: 3 } }));
537
+ db.prepare("INSERT INTO part VALUES (?, ?, ?, ?)").run("prt_original_text", "msg_original_answer", "ses_original",
538
+ JSON.stringify({ type: "text", text: "Exact answer 🦊", id: "untrusted-part-id" }));
539
+ db.close();
540
+ const before = await readFile(file);
541
+ const stat = await lstat(file);
542
+ const messages = [];
543
+ const result = await readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage: row => messages.push(row) });
544
+ assert.equal(result.session.directory, "/private/actor/colleague_scope");
545
+ assert.equal(result.session.project_id, "project_original");
546
+ assert.equal(result.recordCount, 2);
547
+ assert.match(result.revision, /^[a-f0-9]{64}$/u);
548
+ assert.deepEqual(messages.map(row => [row.id, row.sessionID, row.type]), [
549
+ ["msg_original_user", "ses_original", "user"], ["msg_original_answer", "ses_original", "assistant"]
550
+ ]);
551
+ assert.equal(messages[1].parentID, "msg_original_user");
552
+ assert.deepEqual(messages[1].content, [{ type: "text", text: "Exact answer 🦊", id: "prt_original_text", sessionID: "ses_original", messageID: "msg_original_answer" }]);
553
+ await assert.rejects(readStoppedNativeDatabase(file, database => database.exec("DELETE FROM message")), /readonly|read-only/iu);
554
+ const cancelled = new Error("Caller stopped original inspection");
555
+ await assert.rejects(readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage() {}, signal: AbortSignal.abort(cancelled) }), error => error === cancelled);
556
+ assert.deepEqual(await readFile(file), before);
557
+ const after = await lstat(file);
558
+ for (const key of ["dev", "ino", "size", "mtimeMs", "ctimeMs", "mode"]) assert.equal(after[key], stat[key]);
559
+ assert.deepEqual(await readdir(root), ["native space.db"]);
560
+ });
561
+
562
+ test("stopped OpenCode database refuses pending native work and foreign parts with no storage changes", async t => {
563
+ const { mkdtemp, readFile, rm } = await import("node:fs/promises");
564
+ const { tmpdir } = await import("node:os");
565
+ const path = await import("node:path");
566
+ const { DatabaseSync } = await import("node:sqlite");
567
+ const { readOpenCodeConversationDatabase } = await import("../src/server/hosts/openCodeClient.js");
568
+ const root = await mkdtemp(path.join(tmpdir(), "assistant-native-opencode-refusal-"));
569
+ t.after(() => rm(root, { recursive: true, force: true }));
570
+ const file = path.join(root, "native.db");
571
+ const db = new DatabaseSync(file);
572
+ db.exec(`CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT);
573
+ CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
574
+ CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, data TEXT);
575
+ CREATE TABLE session_input (session_id TEXT, promoted_seq INTEGER);
576
+ INSERT INTO session VALUES ('ses_original', '/private/actor/scope');
577
+ INSERT INTO session_input VALUES ('ses_original', NULL);`);
578
+ db.close();
579
+ let seen = 0;
580
+ const before = await readFile(file);
581
+ await assert.rejects(readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage() { seen += 1; } }), /unpromoted native request/u);
582
+ assert.equal(seen, 0);
583
+ assert.deepEqual(await readFile(file), before);
584
+ const edited = new DatabaseSync(file);
585
+ edited.exec("DELETE FROM session_input");
586
+ edited.prepare("INSERT INTO message VALUES (?, ?, ?, ?)").run("msg_original", "ses_original", 1, JSON.stringify({ role: "assistant", finish: "stop" }));
587
+ edited.prepare("INSERT INTO part VALUES (?, ?, ?, ?)").run("prt_foreign", "msg_original", "ses_other", JSON.stringify({ type: "text", text: "Other private owner" }));
588
+ edited.close();
589
+ const foreignBefore = await readFile(file);
590
+ await assert.rejects(readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage() { seen += 1; } }), /could not be inspected completely/u);
591
+ assert.equal(seen, 0);
592
+ assert.deepEqual(await readFile(file), foreignBefore);
593
+ });
594
+
595
+ test("stopped native inspection rejects live journals and noncanonical files before opening storage", async t => {
596
+ const { mkdtemp, readFile, writeFile, readdir, symlink, rm } = await import("node:fs/promises");
597
+ const { tmpdir } = await import("node:os");
598
+ const path = await import("node:path");
599
+ const { DatabaseSync } = await import("node:sqlite");
600
+ const { readStoppedNativeDatabase } = await import("../src/server/hosts/nativeHistory.js");
601
+ const root = await mkdtemp(path.join(tmpdir(), "assistant-native-journal-"));
602
+ t.after(() => rm(root, { recursive: true, force: true }));
603
+ const file = path.join(root, "native.db");
604
+ const db = new DatabaseSync(file);
605
+ db.exec("CREATE TABLE retained (id TEXT)");
606
+ db.close();
607
+ const before = await readFile(file);
608
+ const sidecar = Buffer.from("Uncheckpointed native storage, retained exactly");
609
+ await writeFile(`${file}-wal`, sidecar);
610
+ let read = false;
611
+ await assert.rejects(readStoppedNativeDatabase(file, () => { read = true; }), /journal sidecar/u);
612
+ assert.equal(read, false);
613
+ assert.deepEqual(await readFile(file), before);
614
+ assert.deepEqual(await readFile(`${file}-wal`), sidecar);
615
+ await symlink(file, path.join(root, "link.db"));
616
+ await assert.rejects(readStoppedNativeDatabase(path.join(root, "link.db"), () => { read = true; }), /regular file.*canonical path/u);
617
+ assert.equal(read, false);
618
+ assert.deepEqual((await readdir(root)).sort(), ["link.db", "native.db", "native.db-wal"]);
619
+ });
620
+
621
+
622
+ test("stopped native WAL inspection reads newest committed OpenCode rows without changing native files and removes private snapshots", async t => {
623
+ const { mkdtemp, readFile, writeFile, lstat, readdir, rm } = await import("node:fs/promises");
624
+ const { tmpdir } = await import("node:os");
625
+ const path = await import("node:path");
626
+ const { spawnSync } = await import("node:child_process");
627
+ const { DatabaseSync } = await import("node:sqlite");
628
+ const { readOpenCodeConversationDatabase } = await import("../src/server/hosts/openCodeClient.js");
629
+ const { readStoppedNativeDatabase } = await import("../src/server/hosts/nativeHistory.js");
630
+ const root = await mkdtemp(path.join(tmpdir(), "assistant-native-stopped-wal-"));
631
+ t.after(() => rm(root, { recursive: true, force: true }));
632
+ const file = path.join(root, "native.db");
633
+ const db = new DatabaseSync(file);
634
+ db.exec(`CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT);
635
+ CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
636
+ CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, data TEXT);
637
+ INSERT INTO session VALUES ('ses_original', '/private/actor/colleague_scope');`);
638
+ db.close();
639
+ // Leave real committed SQLite WAL frames after the only fixture writer exits.
640
+ // The base contains the schema, but neither of these newly committed messages.
641
+ const writer = spawnSync(process.execPath, ["--input-type=module", "-e", `
642
+ import { DatabaseSync } from "node:sqlite";
643
+ const database = new DatabaseSync(process.argv[1]);
644
+ database.exec("PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=0; BEGIN");
645
+ const insert = database.prepare("INSERT INTO message VALUES (?, ?, ?, ?)");
646
+ insert.run("msg_user", "ses_original", 1, JSON.stringify({ role: "user", time: { created: 1 } }));
647
+ insert.run("msg_answer", "ses_original", 2, JSON.stringify({ role: "assistant", parentID: "msg_user", finish: "stop", time: { created: 2, completed: 3 } }));
648
+ database.prepare("INSERT INTO part VALUES (?, ?, ?, ?)").run("prt_text", "msg_answer", "ses_original", JSON.stringify({ type: "text", text: "Newest committed answer 🦊" }));
649
+ database.exec("COMMIT");
650
+ process.kill(process.pid, "SIGKILL");
651
+ `, file], { encoding: "utf8", timeout: 10_000 });
652
+ assert.equal(writer.signal, "SIGKILL", writer.stderr);
653
+ const before = {};
654
+ for (const suffix of ["", "-wal", "-shm"]) before[suffix] = { bytes: await readFile(file + suffix), stat: await lstat(file + suffix) };
655
+ assert.ok(before["-wal"].bytes.length > 32);
656
+ const nativeNames = await readdir(root);
657
+ let snapshot;
658
+ const count = await readStoppedNativeDatabase(file, database => {
659
+ snapshot = database.prepare("PRAGMA database_list").get().file;
660
+ assert.notEqual(snapshot, file, "WAL inspection opens only the private snapshot");
661
+ assert.equal(database.prepare("SELECT COUNT(*) AS count FROM message").get().count, 2);
662
+ assert.throws(() => database.exec("DELETE FROM message"), /readonly|read-only/iu);
663
+ return 2;
664
+ });
665
+ assert.equal(count, 2);
666
+ await assert.rejects(lstat(path.dirname(snapshot)), { code: "ENOENT" });
667
+ const messages = [];
668
+ const result = await readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage: row => messages.push(row) });
669
+ assert.equal(result.recordCount, 2);
670
+ assert.deepEqual(messages.map(row => row.id), ["msg_user", "msg_answer"]);
671
+ assert.equal(messages[1].content[0].text, "Newest committed answer 🦊");
672
+ const cancelled = new Error("Cancel the private stopped snapshot");
673
+ const controller = new AbortController();
674
+ await assert.rejects(readStoppedNativeDatabase(file, database => {
675
+ snapshot = database.prepare("PRAGMA database_list").get().file;
676
+ controller.abort(cancelled);
677
+ }, { signal: controller.signal }), error => error === cancelled);
678
+ await assert.rejects(lstat(path.dirname(snapshot)), { code: "ENOENT" });
679
+ const interruptedProjection = new AbortController();
680
+ await assert.rejects(readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", signal: interruptedProjection.signal,
681
+ onMessage() { interruptedProjection.abort(cancelled); } }), error => error === cancelled);
682
+ for (const suffix of ["", "-wal", "-shm"]) {
683
+ assert.deepEqual(await readFile(file + suffix), before[suffix].bytes);
684
+ const after = await lstat(file + suffix);
685
+ for (const key of ["dev", "ino", "size", "mtimeMs", "ctimeMs", "mode"]) assert.equal(after[key], before[suffix].stat[key]);
686
+ }
687
+ assert.deepEqual(await readdir(root), nativeNames);
688
+ const invalidWal = Buffer.from(before["-wal"].bytes);
689
+ invalidWal.writeUInt32BE(0, 0);
690
+ await writeFile(file + "-wal", invalidWal);
691
+ let invalidRead = false;
692
+ await assert.rejects(readStoppedNativeDatabase(file, () => { invalidRead = true; }), /journal sidecar/u);
693
+ assert.equal(invalidRead, false, "Malformed paired WAL refuses before the reader callback");
694
+ assert.deepEqual(await readFile(file + "-wal"), invalidWal);
695
+ await writeFile(file + "-wal", before["-wal"].bytes);
696
+ await assert.rejects(readStoppedNativeDatabase(file, async database => {
697
+ snapshot = database.prepare("PRAGMA database_list").get().file;
698
+ await writeFile(file + "-wal", Buffer.concat([before["-wal"].bytes, Buffer.from("Fixture writer changed native storage")]));
699
+ }), /Native storage changed during inspection/u);
700
+ await assert.rejects(lstat(path.dirname(snapshot)), { code: "ENOENT" });
701
+ });