@jskit-ai/assistant-core 0.1.213 → 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 (59) 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 +16 -2
  9. package/src/server/conversation/claudeTurn.js +28 -8
  10. package/src/server/conversation/codexConfiguration.js +1 -1
  11. package/src/server/conversation/codexHistoryAdapter.js +11 -4
  12. package/src/server/conversation/codexNotificationQueue.js +11 -2
  13. package/src/server/conversation/codexObservationControl.js +8 -8
  14. package/src/server/conversation/codexOutput.js +20 -15
  15. package/src/server/conversation/codexProvider.js +40 -3
  16. package/src/server/conversation/codexProviderOwner.js +110 -1
  17. package/src/server/conversation/codexTurn.js +3 -0
  18. package/src/server/conversation/continuity.js +24 -6
  19. package/src/server/conversation/index.js +1 -1
  20. package/src/server/conversation/nativeHistoryExport.js +97 -0
  21. package/src/server/conversation/openCodeClient.js +47 -1
  22. package/src/server/conversation/openCodeRuntime.js +132 -24
  23. package/src/server/conversation/openCodeTurn.js +65 -24
  24. package/src/server/conversation/providers/api.js +7 -0
  25. package/src/server/conversation/providers/claudeDriver.js +52 -7
  26. package/src/server/conversation/providers/codex.js +19 -2
  27. package/src/server/conversation/providers/codexDriver.js +92 -14
  28. package/src/server/conversation/providers/opencodeDriver.js +31 -9
  29. package/src/server/conversation/runtime.js +418 -64
  30. package/src/server/conversation/runtimeStateUpgrade.js +77 -2
  31. package/src/server/conversation/streams.js +25 -5
  32. package/src/server/conversation/tools.js +2 -2
  33. package/src/server/conversation/transcript.js +23 -8
  34. package/src/server/hosts/claudeHistory.js +1 -0
  35. package/src/server/hosts/claudeProcess.js +1 -0
  36. package/src/server/hosts/codexProvider.js +3 -0
  37. package/src/server/hosts/nativeHistory.js +1 -0
  38. package/src/server/hosts/openCodeClient.js +1 -0
  39. package/src/server/hosts/openCodeTurn.js +1 -0
  40. package/src/server/index.js +1 -1
  41. package/src/server/lib/assistantToolLoop.js +99 -2
  42. package/test/aiConnectionClient.test.js +57 -0
  43. package/test/assistantBoundedToolLoop.test.js +332 -0
  44. package/test/assistantScroll.browser.test.js +151 -0
  45. package/test/claudeConversationSession.test.js +1329 -3
  46. package/test/claudeProcess.test.js +101 -0
  47. package/test/claudeTurn.test.js +31 -0
  48. package/test/codexConversationSession.test.js +1257 -12
  49. package/test/codexHistoryAdapter.test.js +74 -1
  50. package/test/codexTurn.test.js +282 -1
  51. package/test/conversation.test.js +83 -0
  52. package/test/conversationAgentRun.test.js +181 -0
  53. package/test/conversationRuntime.test.js +95 -0
  54. package/test/conversationRuntimeStateUpgrade.test.js +89 -0
  55. package/test/conversationStreaming.test.js +85 -0
  56. package/test/conversationTools.test.js +112 -1
  57. package/test/openCodeClient.test.js +186 -0
  58. package/test/openCodeRuntime.test.js +78 -0
  59. package/test/opencodeConversationSession.test.js +637 -5
@@ -1,12 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { randomUUID } from "node:crypto";
3
- import { appendFile, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
3
+ import { appendFile, mkdir, mkdtemp, open, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
4
4
  import { request } from "node:http";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import test from "node:test";
8
8
  import { gzipSync, zstdCompressSync } from "node:zlib";
9
9
  import { startCodexHistoryAdapter, translateCodexHistory, prepareCodexHistory } from "../src/server/conversation/codexHistoryAdapter.js";
10
+ import { readCodexHistoryRows, readCodexNativeGoal } from "../src/server/hosts/codexProvider.js";
10
11
  import { nativeAiProvider } from "../src/shared/nativeProviders.js";
11
12
 
12
13
  const foreign = { type: "reasoning", id: "foreign-id", encrypted_content: "foreign-opaque-state",
@@ -748,3 +749,75 @@ test("DeepSeek exact numeric context rejection exposes native recovery code with
748
749
  }
749
750
  assert.equal(attempts, cases.length + 2, "exactly one upstream admission per request");
750
751
  });
752
+
753
+ test("the original fixed-snapshot rollout reader keeps tolerant recovery and supports strict offline inspection", async t => {
754
+ const root = await mkdtemp(path.join(os.tmpdir(), "jskit-offline-rollout-"));
755
+ t.after(() => rm(root, { recursive: true, force: true }));
756
+ const history = path.join(root, "rollout.jsonl");
757
+ const row = { type: "response_item", payload: { type: "message", role: "assistant",
758
+ content: [{ type: "output_text", text: "😀".repeat(20_000) }] } };
759
+ const complete = Buffer.from(`${JSON.stringify(row)}\n`);
760
+ await writeFile(history, complete);
761
+ const file = await open(history, "r");
762
+ t.after(() => file.close());
763
+ const read = async (end, options, signal = new AbortController().signal) => {
764
+ const rows = [];
765
+ for await (const entry of readCodexHistoryRows(file, 0, end, signal, options)) rows.push(entry);
766
+ return rows;
767
+ };
768
+ assert.deepEqual(await read(complete.length, { strictComplete: true }), [{ row, offset: 0 }]);
769
+ await appendFile(history, '{"type":');
770
+ assert.deepEqual(await read(complete.length, { strictComplete: true }), [{ row, offset: 0 }],
771
+ "inspection reads only the captured snapshot, not later appends");
772
+ assert.deepEqual(await read((await file.stat()).size), [{ row, offset: 0 }],
773
+ "original compaction recovery still ignores an unfinished native append");
774
+ await assert.rejects(read((await file.stat()).size, { strictComplete: true }), /incomplete final record/);
775
+ await writeFile(history, Buffer.from('{"text":"\xff"}\n', "latin1"));
776
+ await assert.rejects(read((await file.stat()).size, { strictComplete: true }), /invalid UTF-8/);
777
+ await writeFile(history, 'malformed saved record\n');
778
+ await assert.rejects(read((await file.stat()).size, { strictComplete: true }), SyntaxError);
779
+ const aborted = AbortSignal.abort(new Error("offline inspection cancelled"));
780
+ await assert.rejects(read((await file.stat()).size, { strictComplete: true }, aborted), /offline inspection cancelled/);
781
+ const { DatabaseSync } = await import("node:sqlite");
782
+ const goals = path.join(root, "goals_1.sqlite");
783
+ const threadId = randomUUID(), goalId = randomUUID();
784
+ const db = new DatabaseSync(goals);
785
+ db.exec(`CREATE TABLE thread_goals (thread_id TEXT PRIMARY KEY, goal_id TEXT NOT NULL,
786
+ objective TEXT NOT NULL, status TEXT NOT NULL, token_budget INTEGER,
787
+ tokens_used INTEGER NOT NULL, time_used_seconds INTEGER NOT NULL,
788
+ created_at_ms INTEGER NOT NULL, updated_at_ms INTEGER NOT NULL)`);
789
+ db.close();
790
+ assert.equal(await readCodexNativeGoal(goals, { threadId }), null);
791
+ for (const status of ["active", "paused", "blocked", "usage_limited", "budget_limited", "complete"]) {
792
+ const db = new DatabaseSync(goals);
793
+ try { db.prepare(`INSERT OR REPLACE INTO thread_goals VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
794
+ .run(threadId, goalId, "Original native objective", status, 500, 30, 2, 1000, 2000); }
795
+ finally { db.close(); }
796
+ const before = await readFile(goals);
797
+ const goal = await readCodexNativeGoal(goals, { threadId });
798
+ assert.equal(goal.thread_id, threadId);
799
+ assert.equal(goal.goal_id, goalId);
800
+ assert.equal(goal.status, status, "unfinished goals are retained, never projected as absent");
801
+ assert.equal(goal.objective, "Original native objective");
802
+ assert.equal(goal.token_budget, 500);
803
+ assert.equal(goal.tokens_used, 30);
804
+ assert.equal(goal.time_used_seconds, 2);
805
+ assert.equal(goal.created_at_ms, 1000);
806
+ assert.equal(goal.updated_at_ms, 2000);
807
+ assert.deepEqual(await readFile(goals), before, "offline goal inspection never writes native storage");
808
+ }
809
+ const invalid = new DatabaseSync(goals);
810
+ try { invalid.prepare("UPDATE thread_goals SET status = ?").run("unsupported"); }
811
+ finally { invalid.close(); }
812
+ await assert.rejects(readCodexNativeGoal(goals, { threadId }), /unsupported or ambiguous/);
813
+ const unknownSchema = new DatabaseSync(goals);
814
+ try { unknownSchema.exec("DROP TABLE thread_goals; CREATE TABLE thread_goals (thread_id TEXT)"); }
815
+ finally { unknownSchema.close(); }
816
+ await assert.rejects(readCodexNativeGoal(goals, { threadId }), /schema is unsupported/);
817
+ await writeFile(`${goals}-wal`, "An original owner still has a journal.");
818
+ await assert.rejects(readCodexNativeGoal(goals, { threadId }), /journal sidecar/);
819
+ await rm(`${goals}-wal`);
820
+ await rm(goals);
821
+ await assert.rejects(readCodexNativeGoal(goals, { threadId }), { code: "ENOENT" });
822
+
823
+ });
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import assert from "node:assert/strict";
2
3
  import { mkdtemp, readFile, rm } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
@@ -538,6 +539,54 @@ test("native notification draining follows same-scope successors while another s
538
539
  assert.equal(owner.tasks.size, 0);
539
540
  });
540
541
 
542
+ test("captured native notification drain retains exact scope after admission expiry", { timeout: 3_000 }, async () => {
543
+ const scopes = new AsyncLocalStorage();
544
+ const expiry = Object.assign(new Error("The original admission expired"), { code: "scope_expired" });
545
+ const namespace = id => {
546
+ const scope = scopes.getStore();
547
+ if (!scope?.active) throw expiry;
548
+ return `${scope.project}:${id}`;
549
+ };
550
+ const owner = createCodexAppServerNotificationQueue({ namespace,
551
+ runInContext: (context, operation) => scopes.run(context, operation) });
552
+ const alpha = { project: "alpha", active: true, access: "write" };
553
+ const beta = { project: "beta", active: true, access: "write" };
554
+ const entered = Promise.withResolvers(), release = Promise.withResolvers();
555
+ const betaEntered = Promise.withResolvers(), betaRelease = Promise.withResolvers();
556
+ const output = [];
557
+ let drainAlpha;
558
+ scopes.run(alpha, () => {
559
+ drainAlpha = owner.captureDrain("shared-session");
560
+ const context = { sessionId: "shared-session", projectContext: { ...alpha, access: "control" } };
561
+ owner.run(context, async () => {
562
+ entered.resolve();
563
+ await release.promise;
564
+ assert.equal(scopes.getStore().access, "control", "waiting retains no write admission");
565
+ output.push("alpha");
566
+ owner.run(context, () => { output.push("alpha-successor"); });
567
+ });
568
+ });
569
+ scopes.run(beta, () => owner.run({ sessionId: "shared-session", projectContext: { ...beta, access: "control" } },
570
+ async () => { betaEntered.resolve(); await betaRelease.promise; output.push("beta"); }));
571
+ await Promise.all([entered.promise, betaEntered.promise]);
572
+ alpha.active = false;
573
+ try {
574
+ await scopes.run(alpha, () => assert.rejects(owner.drain("shared-session"), error => error === expiry));
575
+ scopes.run(alpha, () => assert.throws(() => owner.captureDrain("shared-session"), error => error === expiry));
576
+ const draining = scopes.run(alpha, () => drainAlpha());
577
+ release.resolve();
578
+ await draining;
579
+ assert.deepEqual(output, ["alpha", "alpha-successor"], "captured alpha follows its successor without waiting for beta");
580
+ assert.equal(owner.tasks.has("alpha:shared-session"), false);
581
+ assert.equal(owner.tasks.has("beta:shared-session"), true, "equal session IDs retain independent scope ownership");
582
+ } finally {
583
+ release.resolve(); betaRelease.resolve();
584
+ await scopes.run(beta, () => owner.drain("shared-session"));
585
+ }
586
+ assert.deepEqual(output, ["alpha", "alpha-successor", "beta"]);
587
+ assert.equal(owner.tasks.size, 0);
588
+ });
589
+
541
590
  test("closing prevents new native notification batches while previously admitted queue work drains", async () => {
542
591
  let closing = false;
543
592
  const owner = createCodexAppServerNotificationQueue({ isClosing: () => closing });
@@ -558,7 +607,7 @@ test("closing prevents new native notification batches while previously admitted
558
607
  // Native output assertions are carried from the original public lifecycle cases.
559
608
  // The existing shared transcript/stream implementations replace only the public
560
609
  // filesystem fixture; public tests retain subscription, Stop and restart coverage.
561
- async function outputFixture() {
610
+ async function outputFixture({ persistCommentary } = {}) {
562
611
  const sessionId = "session-1";
563
612
  const threadId = "thread-1";
564
613
  const turnId = "turn-1";
@@ -610,6 +659,7 @@ async function outputFixture() {
610
659
  f.store = store;
611
660
  f.provider = provider;
612
661
  f.createOwner = () => createCodexAppServerRunOwner({
662
+ persistCommentary,
613
663
  createRuntime: async () => { f.hydrations += 1; return runtime; },
614
664
  createStore: async id => { assert.equal(id, sessionId); return store; },
615
665
  async acquireProvider(input) {
@@ -2899,3 +2949,234 @@ test("native changeover retries retain original application data including its a
2899
2949
  assert.deepEqual(sent.data, data);
2900
2950
  }
2901
2951
  });
2952
+
2953
+ test("initial native seed limits preserve generic history, full returning corrections and delivery ownership", async () => {
2954
+ const { createConversationChangeover } = await import("../src/server/conversation/continuity.js");
2955
+ const attachment = { attachmentId: "retained-file", name: "retained.txt" };
2956
+ const messages = Array.from({ length: 31 }, (_, index) => ({
2957
+ id: `stored-${index}`, messageId: `authored-${index}`, role: index % 2 ? "assistant" : "user",
2958
+ text: `${index}:` + "x".repeat(2_100), version: `version-${index}`, engineId: "claude",
2959
+ ...(index === 30 ? { attachments: [attachment] } : {})
2960
+ }));
2961
+ const original = structuredClone(messages);
2962
+ const words = "My current words remain complete: " + "y".repeat(2_100);
2963
+ const input = { messageId: "current-input", message: words, displayAttachments: [attachment], data: { reference: "current" } };
2964
+ for (const scenario of ["generic", "one-current", "two-current", "wake", "empty", "returning"]) {
2965
+ const allowance = scenario === "two-current" ? 22 : scenario === "wake" ? 24 : scenario === "empty" ? 0 : 23;
2966
+ let value = { lastEngine: "claude", engines: { codex: { seen: scenario === "returning"
2967
+ ? { "stored-0": "old-version", removed: "removed-version" } : {} } } };
2968
+ let sent;
2969
+ const changeover = createConversationChangeover({
2970
+ state: { async read() { return value; }, async write(next) { value = next; } },
2971
+ transcript: { async hasMessage() { return false; } },
2972
+ agent: { async sendMessage(actual) { sent = actual; return { ok: true, delivered: true }; } },
2973
+ captureContext: true,
2974
+ ...(scenario !== "generic" ? { maximumInitialMessages: allowance, maximumInitialMessageCharacters: 2_000 } : {})
2975
+ });
2976
+ assert.equal((await changeover.send({ engineId: "codex", messages, input })).delivered, true);
2977
+ const carried = sent.contextText.split("\n").filter(line => line.startsWith("{")).map(JSON.parse)[0];
2978
+ const selected = scenario === "returning" ? messages : scenario === "empty" ? []
2979
+ : messages.slice(scenario === "generic" ? -30 : -allowance);
2980
+ assert.deepEqual(carried.messages.map(message => [message.id, message.text]), selected.map(message => [message.id,
2981
+ scenario !== "generic" && scenario !== "returning" ? message.text.slice(0, 2_000) : message.text]));
2982
+ assert.equal(carried.messages.some(message => message.messageId === input.messageId), false);
2983
+ if (scenario !== "empty") assert.deepEqual(carried.messages.at(-1).attachments, [attachment]);
2984
+ assert.deepEqual(sent.contextAttachments, scenario === "empty" ? [] : [attachment]);
2985
+ assert.deepEqual(sent.displayAttachments, input.displayAttachments);
2986
+ assert.deepEqual(sent.data, input.data);
2987
+ assert.ok(sent.message.endsWith(words));
2988
+ assert.deepEqual(carried.removedMessageIds, scenario === "returning" ? ["removed"] : []);
2989
+ if (scenario === "returning") assert.equal(carried.messages[0].corrected, true);
2990
+ assert.deepEqual(value.engines.codex.seen, Object.fromEntries(messages.map(message => [message.id, message.version])));
2991
+ assert.equal(value.engines.codex.pending, undefined);
2992
+ assert.equal(value.lastEngine, "codex");
2993
+ assert.deepEqual(messages, original, "clipping a fresh seed must not edit stored history or its fingerprints");
2994
+ }
2995
+ });
2996
+
2997
+ test("initial native seed options refuse invalid limits before delivery", async () => {
2998
+ const { createConversationChangeover } = await import("../src/server/conversation/continuity.js");
2999
+ for (const name of ["maximumInitialMessages", "maximumInitialMessageCharacters"]) {
3000
+ for (const value of [...(name === "maximumInitialMessageCharacters" ? [0] : []), -1, 1.5, "24", NaN]) {
3001
+ assert.throws(() => createConversationChangeover({ [name]: value }), /non-negative message limit and a positive text limit/);
3002
+ }
3003
+ }
3004
+ });
3005
+
3006
+ // Original Colleague first-progress test261 requires transient intent even
3007
+ // while the application effect waits. Other consumers keep default persistence.
3008
+ test("original output owner can publish transient commentary without losing native deduplication", async () => {
3009
+ const f = await outputFixture({ persistCommentary: false });
3010
+ const { owner, sessionId, threadId, turnId, store } = f;
3011
+ const progress = "Checking the source.";
3012
+ await owner.writeStream(sessionId, { threadId, turnId, itemId: "transient-progress", role: "commentary", delta: progress });
3013
+ const complete = itemId => owner.writeLiveProgress(sessionId, threadId, { method: "item/completed", params: {
3014
+ threadId, turnId, item: outputItem(itemId, progress, "commentary")
3015
+ } });
3016
+ await complete("transient-progress");
3017
+ assert.deepEqual((await store.readConversationLog(sessionId)).flatMap(row => row.commentary || []), []);
3018
+ assert.deepEqual(store.readConversationStream(sessionId).messages, []);
3019
+ assert.equal(f.publications.at(-1).reason, "codex-app-server-commentary");
3020
+ assert.ok(f.publications.at(-1).payload.conversationStream);
3021
+ assert.equal(f.publications.at(-1).payload.conversationLogPatch, undefined);
3022
+ assert.equal(owner.liveProgressItems.size, 1);
3023
+ assert.equal(owner.liveProgressFingerprints.size, 1);
3024
+ await complete("transient-progress");
3025
+ await complete("same-progress-another-item");
3026
+ assert.equal(owner.liveProgressItems.size, 1, "repeated completed item and matching text do not lose their native deduplication");
3027
+ assert.equal(owner.liveProgressFingerprints.size, 1);
3028
+ assert.deepEqual((await store.readConversationLog(sessionId)).flatMap(row => row.commentary || []), []);
3029
+ f.history = [{ id: turnId, status: "completed", items: [
3030
+ outputItem("transient-progress", progress, "commentary"), outputItem("final-answer", "Checked.")
3031
+ ] }];
3032
+ await f.final("final-answer", "Checked.");
3033
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["Checked."]);
3034
+ assert.deepEqual((await store.readConversationLog(sessionId)).flatMap(row => row.commentary || []), []);
3035
+ assert.equal(f.history[0].items[0].text, progress, "canonical policy does not change native history");
3036
+ });
3037
+
3038
+ test("completed envelope carriers stay raw while native history versions and API briefings use product rows", async () => {
3039
+ const { conversationNativeMessages, conversationHistoryVersions, conversationContinuity } = await import("../src/server/conversation/continuity.js");
3040
+ const storage = createMemoryConversationStorage();
3041
+ const transcript = createConversationTranscript({ storage });
3042
+ await storage.write("projection", async tx => {
3043
+ await tx.appendMessage("000001", { role: "user", messageId: "actual-user", text: "My visible words", at: "2026-10-10" });
3044
+ await tx.appendMessage("000001", { role: "assistant", messageId: "actual-reply", text: "My plain answer", at: "2026-10-10" });
3045
+ await tx.appendMessage("000002", { role: "system", messageId: "internal-native-input", text: "Private generated prompt", at: "2026-10-10",
3046
+ turnMetadata: { runtime: { engine: "codex", origin: "application", completedEnvelope: true, status: "complete" },
3047
+ applicationTools: [{ id: "internal-native-input:operation", status: "unknown" }] } });
3048
+ await tx.appendMessage("000002", { role: "assistant", messageId: "internal-final", text: '{"kind":"tool","arguments":"private"}', at: "2026-10-10" });
3049
+ await tx.appendMessage("000003", { role: "user", messageId: "authored-json", text: '{"completedEnvelope":true}',
3050
+ data: { completedEnvelope: true }, at: "2026-10-10" });
3051
+ });
3052
+ const raw = await transcript.readConversationLog("projection");
3053
+ const before = structuredClone(raw);
3054
+ assert.deepEqual(conversationNativeMessages(raw).map(row => row.messageId), ["actual-user", "actual-reply", "authored-json"]);
3055
+ assert.deepEqual(Object.keys(conversationHistoryVersions(raw)), ["000001/user/", "000001/assistant/", "000003/user/"]);
3056
+ const briefing = conversationContinuity({ history: raw }).text;
3057
+ assert.match(briefing, /My visible words/);
3058
+ assert.match(briefing, /My plain answer/);
3059
+ assert.equal(briefing.includes("Private generated prompt"), false);
3060
+ assert.equal(briefing.includes("internal-final"), false);
3061
+ assert.deepEqual(raw, before);
3062
+ assert.deepEqual(await transcript.readConversationLog("projection"), before, "Private native carriers and durable outcome custody are never rewritten");
3063
+ });
3064
+
3065
+ test("completed envelope native seed slices the original 24 rows before excluding the current batch and commits seen only after admission", async () => {
3066
+ const { createConversationChangeover } = await import("../src/server/conversation/continuity.js");
3067
+ const messages = Array.from({ length: 30 }, (_, index) => ({ id: `stored-${index}`, messageId: `authored-${index}`,
3068
+ role: "user", text: `${index}:` + "x".repeat(2100), version: `version-${index}`, engineId: "claude" }));
3069
+ const before = structuredClone(messages);
3070
+ let value = { lastEngine: "", engines: { codex: { seen: {} } } };
3071
+ const entered = Promise.withResolvers();
3072
+ const admitted = Promise.withResolvers();
3073
+ const sent = [];
3074
+ const changeover = createConversationChangeover({
3075
+ state: { async read() { return value; }, async write(next) { value = next; } },
3076
+ transcript: { async hasMessage() { return false; } }, captureContext: true,
3077
+ maximumInitialMessages: 24, maximumInitialMessageCharacters: 2000,
3078
+ agent: { async sendMessage(input) {
3079
+ sent.push(input);
3080
+ if (sent.length === 1) {
3081
+ await input.onPromptSending({ threadId: "same-native-thread" });
3082
+ entered.resolve();
3083
+ await admitted.promise;
3084
+ }
3085
+ return { ok: true, delivered: true };
3086
+ } }
3087
+ });
3088
+ const sending = changeover.send({ engineId: "codex", messages, completedEnvelope: true,
3089
+ excludedMessageIds: ["authored-28", "authored-29"],
3090
+ input: { messageId: "internal-1", message: JSON.stringify({ userMessages: ["authored-28", "authored-29"] }) } });
3091
+ await entered.promise;
3092
+ const carried = sent[0].contextText.split("\n").filter(line => line.startsWith("{")).map(JSON.parse)[0];
3093
+ assert.deepEqual(carried.messages.map(row => row.messageId), messages.slice(-24).slice(0, -2).map(row => row.messageId));
3094
+ assert.equal(carried.messages.length, 22, "The original 24-row window is not expanded to 24 older rows plus the batch");
3095
+ assert.equal(carried.messages[0].text.length, 2000);
3096
+ assert.equal(sent[0].message.split("authored-28").length - 1, 1);
3097
+ assert.equal(sent[0].message.split("authored-29").length - 1, 1);
3098
+ assert.deepEqual(value.engines.codex.seen, {}, "Prepared history does not claim delivery before the actual native ACK");
3099
+ const snapshot = Object.fromEntries(messages.map(row => [row.id, row.version]));
3100
+ assert.deepEqual(value.engines.codex.pending.seen, snapshot);
3101
+ admitted.resolve();
3102
+ assert.equal((await sending).delivered, true);
3103
+ assert.deepEqual(value.engines.codex.seen, snapshot, "The genuinely admitted internal prompt consumed the product batch history");
3104
+ await changeover.send({ engineId: "codex", messages, input: { messageId: "internal-2", message: "Continue from the saved operation result" } });
3105
+ assert.equal(sent[1].contextText, undefined, "The next native response does not replay the already included B/C batch as catch-up");
3106
+ assert.equal(sent[1].message, "Continue from the saved operation result");
3107
+ assert.deepEqual(sent.map(input => input.messageId), ["internal-1", "internal-2"], "Only real internal inputs have native receipts");
3108
+ assert.deepEqual(messages, before);
3109
+ });
3110
+
3111
+ test("completed envelope native retries preserve the exact prepared history snapshot without recapture", async () => {
3112
+ const { createConversationChangeover } = await import("../src/server/conversation/continuity.js");
3113
+ let value = { lastEngine: "", engines: { codex: { seen: {} } } };
3114
+ let accepted = false;
3115
+ let dispatches = 0;
3116
+ const original = [{ id: "row-B", messageId: "authored-B", role: "user", text: "Original B", version: "original-B" }];
3117
+ const changeover = createConversationChangeover({
3118
+ state: { async read() { return value; }, async write(next) { value = next; } },
3119
+ captureContext: true, maximumInitialMessages: 24, maximumInitialMessageCharacters: 2000,
3120
+ transcript: { async hasMessage() { return false; }, async writeUserMessage() {} },
3121
+ agent: { async sendMessage(input) { dispatches++; await input.onPromptSending({ threadId: "exact-thread" }); return { ok: false, delivered: false }; },
3122
+ async inspectMessageAdmission() { return { admission: accepted ? "accepted" : "unknown", threadId: "exact-thread" }; } }
3123
+ });
3124
+ const input = { messageId: "internal-retry", message: "Generated original B prompt" };
3125
+ await changeover.send({ engineId: "codex", messages: original, input, completedEnvelope: true, excludedMessageIds: ["authored-B"] });
3126
+ const pending = structuredClone(value.engines.codex.pending);
3127
+ const updated = [{ ...original[0], text: "Changed B", version: "changed-B" }];
3128
+ const unknown = await changeover.send({ engineId: "codex", messages: updated, input,
3129
+ completedEnvelope: true, excludedMessageIds: ["different-new-batch"] });
3130
+ assert.equal(unknown.delivered, false);
3131
+ assert.deepEqual(value.engines.codex.pending, pending);
3132
+ assert.deepEqual(value.engines.codex.seen, {});
3133
+ accepted = true;
3134
+ assert.equal((await changeover.send({ engineId: "codex", messages: updated, input,
3135
+ completedEnvelope: true, excludedMessageIds: [] })).delivered, true);
3136
+ assert.deepEqual(value.engines.codex.seen, pending.seen);
3137
+ assert.equal(value.engines.codex.pending, undefined);
3138
+ assert.equal(dispatches, 1, "Inspecting the same native input never resubmits or substitutes its batch/history snapshot");
3139
+ });
3140
+
3141
+ test("completed envelope history exclusions require explicit tracked input and exact nonduplicate authored IDs", async () => {
3142
+ const { createConversationChangeover } = await import("../src/server/conversation/continuity.js");
3143
+ let reads = 0;
3144
+ let sent;
3145
+ const messages = [{ id: "row-B", messageId: "authored-B", role: "user", text: "Visible B", version: "version-B" }];
3146
+ const changeover = createConversationChangeover({
3147
+ state: { async read() { reads++; return { lastEngine: "", engines: { codex: { seen: {} } } }; }, async write() {} },
3148
+ captureContext: true,
3149
+ transcript: { async hasMessage() { return false; } },
3150
+ agent: { async sendMessage(input) { sent = input; return { ok: true, delivered: true }; } }
3151
+ });
3152
+ const input = { messageId: "internal-input", message: "Current generated prompt", data: { completedEnvelope: true, excludedMessageIds: ["authored-B"] } };
3153
+ for (const excludedMessageIds of [["authored-B", "authored-B"], [""], [" "], ["x".repeat(129)], [1], "authored-B"]) {
3154
+ await assert.rejects(changeover.send({ engineId: "codex", messages, input, completedEnvelope: true, excludedMessageIds }), /tracked completed envelope and exact message IDs/);
3155
+ }
3156
+ await assert.rejects(changeover.send({ engineId: "codex", messages, input, excludedMessageIds: ["authored-B"] }), /tracked completed envelope/);
3157
+ assert.equal(reads, 0, "Invalid exclusions cannot enter existing state or delivery owners");
3158
+ await changeover.send({ engineId: "codex", messages, input });
3159
+ const carried = sent.contextText.split("\n").filter(line => line.startsWith("{")).map(JSON.parse)[0];
3160
+ assert.deepEqual(carried.messages.map(row => row.messageId), ["authored-B"], "Caller data is never a history-exclusion grant");
3161
+ assert.deepEqual(sent.data, input.data);
3162
+ });
3163
+
3164
+ test("completed envelope returning corrections remain full while the current batch is excluded only from rendering", async () => {
3165
+ const { createConversationChangeover } = await import("../src/server/conversation/continuity.js");
3166
+ const messages = [{ id: "old-row", messageId: "old-authored", role: "user", text: "Edited " + "x".repeat(2100), version: "edited" },
3167
+ { id: "current-row", messageId: "current-authored", role: "user", text: "Current B", version: "current" }];
3168
+ let value = { lastEngine: "codex", engines: { codex: { seen: { "old-row": "before-edit", removed: "removed-version" } } } };
3169
+ let sent;
3170
+ const changeover = createConversationChangeover({
3171
+ state: { async read() { return value; }, async write(next) { value = next; } },
3172
+ maximumInitialMessages: 24, maximumInitialMessageCharacters: 2000, captureContext: true,
3173
+ transcript: { async hasMessage() { return false; } },
3174
+ agent: { async sendMessage(input) { sent = input; return { ok: true, delivered: true }; } }
3175
+ });
3176
+ await changeover.send({ engineId: "codex", messages, input: { messageId: "internal-correction", message: "Current B prompt" },
3177
+ completedEnvelope: true, excludedMessageIds: ["current-authored"] });
3178
+ const carried = sent.contextText.split("\n").filter(line => line.startsWith("{")).map(JSON.parse)[0];
3179
+ assert.deepEqual(carried.messages.map(row => [row.messageId, row.text, row.corrected]), [["old-authored", messages[0].text, true]]);
3180
+ assert.deepEqual(carried.removedMessageIds, ["removed"]);
3181
+ assert.deepEqual(value.engines.codex.seen, { "old-row": "edited", "current-row": "current" });
3182
+ });
@@ -165,3 +165,86 @@ test("application transcript opt-in opens only authored application system turns
165
165
  assert.equal((await transcript.writeConversationAssistantMessage("one", { text: "Answer" })).turnId, authored.turnId);
166
166
  }
167
167
  });
168
+
169
+ async function completedEnvelopeTranscriptRows(storage) {
170
+ await storage.write("projected", async transaction => {
171
+ for (let index = 1; index <= 9; index++) {
172
+ const internal = index % 3 !== 1;
173
+ const turnId = String(index).padStart(6, "0");
174
+ await transaction.appendMessage(turnId, { role: internal ? "system" : "user", messageId: `authored-${index}`,
175
+ text: internal ? `Private prompt ${index}` : `Visible question ${index}`, at: "2026-10-10",
176
+ ...(internal ? { origin: "application" } : { attachments: [{ attachmentId: "retained" }] }),
177
+ turnMetadata: { runtime: { origin: internal ? "application" : "user", status: "complete",
178
+ ...(internal ? { completedEnvelope: true } : {}) } } });
179
+ await transaction.appendMessage(turnId, { role: "assistant", messageId: `answer-${index}`,
180
+ text: internal ? '{"kind":"reply","text":"private raw carrier"}' : `Visible answer ${index}`, at: "2026-10-10" });
181
+ if (internal) await transaction.updateTurnMetadata(turnId, { applicationTools: [{ id: `effect-${index}`, status: "unknown" }] });
182
+ else await transaction.appendMessage(turnId, { role: "commentary", messageId: `progress-${index}`, text: "Checking.", at: "2026-10-10" });
183
+ }
184
+ });
185
+ }
186
+
187
+ test("completed envelope presentation reads retain raw canonical receipts and visible message identity", async () => {
188
+ const storage = createMemoryConversationStorage();
189
+ const transcript = createConversationTranscript({ storage });
190
+ await completedEnvelopeTranscriptRows(storage);
191
+ const before = await transcript.readConversationLog("projected");
192
+ const visible = await transcript.readConversationLog("projected", { presentation: true });
193
+ assert.deepEqual(visible.map(turn => turn.turnId), ["000001", "000004", "000007"]);
194
+ assert.deepEqual(visible, before.filter((_, index) => index % 3 === 0));
195
+ assert.deepEqual(visible[0].user.attachments, [{ attachmentId: "retained" }]);
196
+ assert.equal(visible[0].commentary[0].messageId, "progress-1");
197
+ assert.equal(visible[0].assistant.messageId, "answer-1");
198
+ const reopened = createConversationTranscript({ storage });
199
+ assert.deepEqual(await reopened.readConversationLog("projected", { presentation: true }), visible);
200
+ assert.deepEqual(await transcript.readConversationLog("projected"), before, "Presentation never changes stored carriers or unknown effect receipts");
201
+ assert.equal(await transcript.conversationMessageIdExists("projected", "authored-2"), true);
202
+ assert.deepEqual(before[1].metadata.applicationTools, [{ id: "effect-2", status: "unknown" }]);
203
+ });
204
+
205
+ test("completed envelope presentation pagination counts visible turns before selecting a page", async () => {
206
+ const storage = createMemoryConversationStorage();
207
+ const transcript = createConversationTranscript({ storage });
208
+ await completedEnvelopeTranscriptRows(storage);
209
+ const newest = await transcript.readConversationLogPage("projected", { limit: 2, presentation: true });
210
+ assert.deepEqual(newest.conversationLog.map(turn => turn.turnId), ["000004", "000007"]);
211
+ assert.deepEqual(newest.pagination, { beforeTurnId: "", count: 2, hasMoreBefore: true, limit: 2,
212
+ newestTurnId: "000007", nextBeforeTurnId: "000004", oldestTurnId: "000004", totalTurnCount: 3 });
213
+ const older = await transcript.readConversationLogPage("projected", {
214
+ beforeTurnId: newest.pagination.nextBeforeTurnId, limit: 2, presentation: true
215
+ });
216
+ assert.deepEqual(older.conversationLog.map(turn => turn.turnId), ["000001"]);
217
+ assert.deepEqual(older.pagination, { beforeTurnId: "000004", count: 1, hasMoreBefore: false, limit: 2,
218
+ newestTurnId: "000001", nextBeforeTurnId: "", oldestTurnId: "000001", totalTurnCount: 3 });
219
+ const raw = await transcript.readConversationLogPage("projected", { limit: 2 });
220
+ assert.deepEqual(raw.conversationLog.map(turn => turn.turnId), ["000008", "000009"]);
221
+ assert.equal(raw.pagination.totalTurnCount, 9);
222
+ });
223
+
224
+ test("completed envelope presentation requires the trusted application marker rather than data or reply content", async () => {
225
+ const storage = createMemoryConversationStorage();
226
+ const transcript = createConversationTranscript({ storage });
227
+ await storage.write("markers", async transaction => {
228
+ for (const [index, runtime] of [undefined, { origin: "application" }, { origin: "user", completedEnvelope: true },
229
+ { origin: "application", completedEnvelope: "true" }, { origin: "application", completedEnvelope: true }].entries()) {
230
+ await transaction.appendMessage(String(index), { role: "user", messageId: `identity-${index}`, text: '{"kind":"reply","text":"plain authored JSON"}',
231
+ data: { completedEnvelope: true }, at: "2026-10-10", ...(runtime ? { turnMetadata: { runtime } } : {}) });
232
+ }
233
+ });
234
+ assert.deepEqual((await transcript.readConversationLog("markers", { presentation: true })).map(turn => turn.turnId), ["0", "1", "2", "3"]);
235
+ assert.equal((await transcript.readConversationLog("markers")).length, 5);
236
+ await assert.rejects(transcript.readConversationLog("markers", { presentation: "true" }), /explicit boolean/);
237
+ await assert.rejects(transcript.readConversationLogPage("markers", { presentation: 1 }), /explicit boolean/);
238
+ });
239
+
240
+ test("completed envelope presentation has an honest empty page when all canonical turns are internal", async () => {
241
+ const storage = createMemoryConversationStorage();
242
+ const transcript = createConversationTranscript({ storage });
243
+ await storage.write("empty", tx => tx.appendMessage("000001", { role: "system", text: "Private response input", at: "2026-10-10",
244
+ turnMetadata: { runtime: { origin: "application", completedEnvelope: true } } }));
245
+ const page = await transcript.readConversationLogPage("empty", { presentation: true, limit: 50 });
246
+ assert.deepEqual(page.conversationLog, []);
247
+ assert.deepEqual(page.pagination, { beforeTurnId: "", count: 0, hasMoreBefore: false, limit: 50,
248
+ newestTurnId: "", nextBeforeTurnId: "", oldestTurnId: "", totalTurnCount: 0 });
249
+ assert.equal((await transcript.readConversationLog("empty")).length, 1);
250
+ });