@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
@@ -230,7 +230,7 @@ async function fixture(t, options = {}) {
230
230
  connections: { resolve: options.resolve || (async () => ({ providerId: "test", model: "test-model", sdkPackage: "@ai-sdk/openai-compatible",
231
231
  modelLimits: { context: 32000, output: 4000 }, apiKey: key })) },
232
232
  host: { workdir: directory, env: { ...process.env, UNRELATED_SECRET: "must-not-inherit" },
233
- commands: { opencode: command }, execution: options.execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper },
233
+ commands: { opencode: command }, execution: options.execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper, opencode: options.opencode },
234
234
  limits: { admissionTimeoutMs: 250, ...options.limits } });
235
235
  runtimes.push(value);
236
236
  return value;
@@ -242,7 +242,7 @@ async function fixture(t, options = {}) {
242
242
  connections: { resolve: options.resolve || (async () => ({ providerId: "test", model: "test-model", sdkPackage: "@ai-sdk/openai-compatible",
243
243
  modelLimits: { context: 32000, output: 4000 }, apiKey: key })) },
244
244
  host: { workdir: directory, env: { ...process.env, UNRELATED_SECRET: "must-not-inherit" },
245
- commands: { opencode: command }, execution: options.execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper },
245
+ commands: { opencode: command }, execution: options.execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper, opencode: options.opencode },
246
246
  limits: { admissionTimeoutMs: 250, ...options.limits }
247
247
  },
248
248
  binding: () => storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding),
@@ -1056,7 +1056,7 @@ for (const savedStatus of ["cancelled", "failed", "unknown-tool", "interrupted"]
1056
1056
 
1057
1057
  // Opt-in bound host uses the same native server, registry and Send/monitor owner.
1058
1058
  // The original standalone fixture above remains byte-for-byte unchanged.
1059
- async function boundToolFixture(t) {
1059
+ async function boundToolFixture(t, { runInContext, monitorRunInContext, monitorContext } = {}) {
1060
1060
  const { createOpenCodeConversationServer, openCodeApplicationToolSchemas } = await import("../src/server/conversation/openCodeProcess.js");
1061
1061
  const { createOpenCodeSharedRuntime, ensureOpenCodeSession } = await import("../src/server/conversation/openCodeRuntime.js");
1062
1062
  const { observeOpenCodeEvents } = await import("../src/server/conversation/openCodeTurn.js");
@@ -1086,7 +1086,7 @@ async function boundToolFixture(t) {
1086
1086
  const driver = createOpenCodeConversationDriver(f.driverOptions);
1087
1087
  const handle = await driver.open({ onFailure() {}, conversation: {
1088
1088
  publish() {}, native: { owner,
1089
- acquire: async context => ({ key, options: context || {} }),
1089
+ acquire: async context => ({ key, options: context || {}, runInContext }),
1090
1090
  preparation: {
1091
1091
  cleanup: () => ({ sessionId: id, options: {}, application: {
1092
1092
  closeTerminals: async () => ({ ok: true }), beforeRelease: async () => {},
@@ -1120,12 +1120,16 @@ async function boundToolFixture(t) {
1120
1120
  prompt: { text: input.message }, beforeDispatch: threadId => input.onPromptSending({ threadId }) }),
1121
1121
  commit: async admitted => { const turn = { turnId: input.messageId, messages: [{ role: "user", messageId: input.messageId, text: input.message }] }; receipts.push({ admitted, turn }); return turn; },
1122
1122
  monitor: {
1123
+ runInContext: monitorRunInContext,
1123
1124
  prepare: () => ({ fields: {} }),
1124
1125
  create(target, turn, _metadata, options) { return {
1125
1126
  observe: (_turn, signal) => observeOpenCodeEvents(target.server.client, target.upstreamSessionId,
1126
1127
  { signal, abortController: turn.abortController, eventStartedAt: turn.eventStartedAt }),
1127
1128
  eventReady: options.eventReady, finalResponse: { agent: "jskit-assistant-actions", model: { id: "test-model", providerID: "test" }, recoveryMessageId: `msg_recovery_${nativeId}` },
1128
- async writeRun() {}, projectMessages: () => ({ failure: "", providerApiFailure: false }), completeResult: (_turn, result) => result.failure, onRetired() {}
1129
+ async writeRun() { await monitorContext?.("writeRun"); },
1130
+ async projectMessages() { await monitorContext?.("projectMessages"); return { failure: "", providerApiFailure: false }; },
1131
+ completeResult: (_turn, result) => result.failure,
1132
+ onRetired() { monitorContext?.("onRetired"); }
1129
1133
  }; }
1130
1134
  }
1131
1135
  }, project: value => value };
@@ -1340,3 +1344,631 @@ test("opted bound OpenCode does not abort newer same-turn input when an already
1340
1344
  await peer.handle.cancel(); await waiting.completion;
1341
1345
  }
1342
1346
  });
1347
+
1348
+ // Observe the original standalone HTTP/plugin fixture and its canonical writer.
1349
+ // Completed rows always come from the native prompt handler, never a seeded turn.
1350
+ const completedOpenCodeValue = { kind: "tool", text: "Checking.", toolName: "numbers_read", arguments: "{}" };
1351
+ const completedOpenCodeSchema = { type: "object", additionalProperties: false,
1352
+ required: ["kind", "text", "toolName", "arguments"],
1353
+ properties: { kind: { type: "string", enum: ["reply", "tool"] }, text: { type: "string", maxLength: 64 },
1354
+ toolName: { type: "string", maxLength: 64 }, arguments: { type: "string", maxLength: 64 } } };
1355
+ async function completedOpenCodeFixture(t, options = {}) {
1356
+ const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
1357
+ let effects = 0;
1358
+ let accountResolutions = 0;
1359
+ const appendedRows = [];
1360
+ const actions = applicationActions(async () => { effects++; return { value: 42 }; });
1361
+ const f = await fixture(t, { ...options, context, actions,
1362
+ assistantResponses: options.assistantResponses || [{ text: JSON.stringify(completedOpenCodeValue) }],
1363
+ configuration: { ...configuration, outputSchema: completedOpenCodeSchema, ...options.configuration } });
1364
+ await f.first.close();
1365
+ const storage = { ...f.storage,
1366
+ write: (id, callback) => f.storage.write(id, transaction => callback({ ...transaction,
1367
+ async appendMessage(turnId, message) {
1368
+ await transaction.appendMessage(turnId, message);
1369
+ if (message.role === "system" && message.origin === "application") {
1370
+ appendedRows.push(structuredClone(await transaction.readTurn(turnId)));
1371
+ }
1372
+ }
1373
+ })) };
1374
+ let runtime;
1375
+ let conversation;
1376
+ async function close() {
1377
+ if (runtime) { await runtime.close(); runtime = null; }
1378
+ }
1379
+ async function reopen() {
1380
+ await close();
1381
+ runtime = createConversationRuntime({ engine: "opencode", storage, actions, authorize: () => true,
1382
+ completedEnvelope: true, ...f.driverOptions,
1383
+ connections: { resolve: async input => { accountResolutions++; return f.driverOptions.connections.resolve(input); } } });
1384
+ try { conversation = await runtime.open({ id: "conversation", context }); }
1385
+ catch (error) { await close(); throw error; }
1386
+ return conversation;
1387
+ }
1388
+ await reopen();
1389
+ const controller = new AbortController();
1390
+ return { ...f, context, controller, appendedRows, actions, close, reopen,
1391
+ effects: () => effects, accountResolutions: () => accountResolutions,
1392
+ get conversation() { return conversation; },
1393
+ async complete(messageId = "completed-opencode-response") {
1394
+ const receipt = await conversation.wake({ messageId, text: "Read a number" });
1395
+ const state = await conversation.wait();
1396
+ const turn = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1397
+ assert.equal(turn.metadata.runtime.status, "complete", state.error);
1398
+ assert.equal(turn.metadata.runtime.completedEnvelope, true);
1399
+ assert.equal(turn.assistant.text, JSON.stringify(completedOpenCodeValue));
1400
+ return { messageId, turnId: receipt.turnId };
1401
+ },
1402
+ prepare: receipt => conversation.prepareCompletedResponse(receipt, { signal: controller.signal }) };
1403
+ }
1404
+ const executeCompletedOpenCode = (prepared, argumentsText = "{}") => prepared.tools.execute({
1405
+ id: prepared.toolCallId, name: "numbers_read", arguments: argumentsText
1406
+ });
1407
+
1408
+ test("completed OpenCode canonical admission preserves its trusted marker and actual native association", async t => {
1409
+ const f = await completedOpenCodeFixture(t);
1410
+ try {
1411
+ const receipt = await f.complete();
1412
+ assert.equal(f.appendedRows.length, 1);
1413
+ const admitted = f.appendedRows[0];
1414
+ assert.equal(admitted.turnId, receipt.turnId);
1415
+ assert.equal(admitted.system.messageId, receipt.messageId);
1416
+ assert.equal(admitted.system.origin, "application");
1417
+ assert.equal(admitted.user, null);
1418
+ assert.equal(admitted.metadata.runtime.completedEnvelope, true);
1419
+ assert.equal(admitted.metadata.runtime.status, "running");
1420
+ const canonical = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1421
+ assert.equal(canonical.system.messageId, admitted.system.messageId);
1422
+ assert.equal(canonical.metadata.runtime.segmentId, admitted.metadata.runtime.segmentId);
1423
+ assert.equal(canonical.metadata.runtime.engine, "opencode");
1424
+ assert.equal(canonical.metadata.runtime.status, "complete");
1425
+ assert.equal(canonical.metadata.runtime.completedEnvelope, true);
1426
+ const binding = await f.binding();
1427
+ assert.match(binding.sessionId, /^ses_/);
1428
+ assert.equal(typeof binding.accountIdentity, "string");
1429
+ assert.ok(binding.accountIdentity);
1430
+ const trace = await f.trace();
1431
+ const prompts = trace.filter(row => row.url?.endsWith("/prompt_async"));
1432
+ assert.equal(prompts.length, 1);
1433
+ assert.equal(prompts[0].url, `/session/${binding.sessionId}/prompt_async`);
1434
+ assert.equal(prompts[0].body.agent, "jskit-assistant");
1435
+ assert.equal(prompts[0].body.model.modelID, "test-model");
1436
+ assert.match(prompts[0].body.parts[0].text, /Return only one JSON value matching this JSON Schema/);
1437
+ const sessions = JSON.parse(await readFile(binding.databasePath, "utf8"));
1438
+ const native = sessions.find(row => row.id === binding.sessionId);
1439
+ assert.ok(native);
1440
+ const authored = native.messages.find(row => row.info.role === "user" && row.info.id === prompts[0].body.messageID);
1441
+ assert.ok(authored, "The native owner recorded the actual admitted prompt ID");
1442
+ const answer = native.messages.find(row => row.info.role === "assistant" && row.info.parentID === authored.info.id);
1443
+ assert.ok(answer);
1444
+ assert.equal(answer.info.finish, "stop");
1445
+ assert.equal(answer.parts[0].text, JSON.stringify(completedOpenCodeValue));
1446
+ assert.equal(canonical.assistant.messageId, `${receipt.turnId}:${answer.info.id}:assistant`);
1447
+ const prepared = await f.prepare(receipt);
1448
+ assert.equal(prepared.text, canonical.assistant.text);
1449
+ assert.equal(prepared.toolCallId, `${receipt.messageId}:operation`);
1450
+ assert.equal(f.effects(), 0);
1451
+ assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
1452
+ } finally { await f.close(); }
1453
+ });
1454
+
1455
+ test("completed OpenCode prepared handles and reopen retain one durable effect without native replay", async t => {
1456
+ const f = await completedOpenCodeFixture(t);
1457
+ try {
1458
+ const receipt = await f.complete();
1459
+ const originalBinding = await f.binding();
1460
+ const readsBefore = (await f.trace()).filter(row => row.url?.includes("/message")).length;
1461
+ const one = await f.prepare(receipt), two = await f.prepare(receipt);
1462
+ assert.equal(one.text, JSON.stringify(completedOpenCodeValue));
1463
+ assert.equal(one.toolCallId, `${receipt.messageId}:operation`);
1464
+ assert.equal(two.toolCallId, one.toolCallId);
1465
+ const [first, second] = await Promise.all([executeCompletedOpenCode(one), executeCompletedOpenCode(two)]);
1466
+ assert.equal(first.ok, true);
1467
+ assert.deepEqual(second, first);
1468
+ assert.equal(f.effects(), 1);
1469
+ await assert.rejects(executeCompletedOpenCode(two, '{"changed":true}'), /different arguments/);
1470
+ const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1471
+ assert.equal(saved.metadata.applicationTools.length, 1);
1472
+ assert.equal(saved.metadata.applicationTools[0].id, one.toolCallId);
1473
+ assert.equal(saved.metadata.applicationTools[0].status, "complete");
1474
+ await f.reopen();
1475
+ assert.deepEqual(await executeCompletedOpenCode(await f.prepare(receipt)), first);
1476
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), saved);
1477
+ const restored = await f.binding();
1478
+ for (const key of ["sessionId", "accountIdentity", "workdir", "directory", "databasePath", "runtimeDirectory"]) {
1479
+ assert.equal(restored[key], originalBinding[key]);
1480
+ }
1481
+ assert.equal(f.effects(), 1);
1482
+ const trace = await f.trace();
1483
+ assert.ok(trace.filter(row => row.url?.includes("/message")).length >= readsBefore + 4,
1484
+ "Both prepared handles and the cold owner inspect the real native input history");
1485
+ assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1486
+ assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
1487
+ } finally { await f.close(); }
1488
+ });
1489
+
1490
+ test("completed OpenCode cached bindings cannot mask missing or changed persisted native fingerprints", async t => {
1491
+ const f = await completedOpenCodeFixture(t);
1492
+ try {
1493
+ const receipt = await f.complete();
1494
+ const prepared = await f.prepare(receipt);
1495
+ const before = await f.storage.read("conversation", tx => tx.readMetadata());
1496
+ for (const missing of [true, false]) {
1497
+ const changed = structuredClone(before);
1498
+ if (missing) delete changed.runtime.binding.accountIdentity;
1499
+ else changed.runtime.binding.accountIdentity = "different-persisted-fixture-identity";
1500
+ await f.storage.write("conversation", tx => tx.writeMetadata(changed));
1501
+ try {
1502
+ for (let attempt = 0; attempt < 2; attempt++) {
1503
+ await assert.rejects(f.prepare(receipt), missing ? /no saved account fingerprint/ : /different account binding/);
1504
+ await assert.rejects(executeCompletedOpenCode(prepared), /identity changed|no saved account fingerprint|different account binding/);
1505
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), changed);
1506
+ }
1507
+ } finally { await f.storage.write("conversation", tx => tx.writeMetadata(before)); }
1508
+ }
1509
+ assert.equal(f.effects(), 0);
1510
+ assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1511
+ assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
1512
+ } finally { await f.close(); }
1513
+ });
1514
+
1515
+ test("completed OpenCode freshly resolved accounts cannot inherit cached completion authority", async t => {
1516
+ const f = await completedOpenCodeFixture(t);
1517
+ try {
1518
+ const receipt = await f.complete();
1519
+ const prepared = await f.prepare(receipt);
1520
+ const before = await f.storage.read("conversation", tx => tx.readMetadata());
1521
+ const resolutions = f.accountResolutions();
1522
+ f.setKey("changed-opencode-fixture-account");
1523
+ try {
1524
+ for (let attempt = 0; attempt < 2; attempt++) {
1525
+ await assert.rejects(f.prepare(receipt), /different account binding/);
1526
+ await assert.rejects(executeCompletedOpenCode(prepared), /different account binding/);
1527
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), before);
1528
+ }
1529
+ assert.ok(f.accountResolutions() >= resolutions + 4, "Every preparation and effect checks the original fresh connection resolver");
1530
+ assert.equal(f.effects(), 0);
1531
+ assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1532
+ assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
1533
+ } finally { f.setKey("test-key"); }
1534
+ } finally { await f.close(); }
1535
+ });
1536
+
1537
+ test("completed OpenCode requires its actual admitted native input on cold inspection", async t => {
1538
+ const f = await completedOpenCodeFixture(t);
1539
+ try {
1540
+ const receipt = await f.complete();
1541
+ await f.prepare(receipt);
1542
+ const canonical = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1543
+ const binding = await f.binding();
1544
+ const prompt = (await f.trace()).find(row => row.url?.endsWith("/prompt_async"));
1545
+ await f.close();
1546
+ const nativeSource = await readFile(binding.databasePath, "utf8");
1547
+ const sessions = JSON.parse(nativeSource);
1548
+ const session = sessions.find(row => row.id === binding.sessionId);
1549
+ const accepted = session.messages.filter(row => row.info.role === "user" && row.info.id === prompt.body.messageID);
1550
+ assert.equal(accepted.length, 1, "Remove only evidence that the original native prompt handler actually wrote");
1551
+ session.messages = session.messages.filter(row => row !== accepted[0]);
1552
+ await writeFile(binding.databasePath, JSON.stringify(sessions));
1553
+ try {
1554
+ const readsBefore = (await f.trace()).filter(row => row.url?.includes("/message")).length;
1555
+ // If the original cold owner refuses before inspection, keep that real
1556
+ // failure visible; do not seed or replace a native/canonical completion.
1557
+ await f.reopen();
1558
+ for (let attempt = 0; attempt < 2; attempt++) {
1559
+ await assert.rejects(f.prepare(receipt), /could not verify this response association/);
1560
+ }
1561
+ assert.ok((await f.trace()).filter(row => row.url?.includes("/message")).length >= readsBefore + 2);
1562
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), canonical);
1563
+ assert.equal(f.effects(), 0);
1564
+ assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1565
+ assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
1566
+ } finally { await f.close(); await writeFile(binding.databasePath, nativeSource); }
1567
+ } finally { await f.close(); }
1568
+ });
1569
+
1570
+ test("completed OpenCode response custody refuses foreign native conversation and host scope before effects", async t => {
1571
+ const f = await completedOpenCodeFixture(t);
1572
+ try {
1573
+ const receipt = await f.complete();
1574
+ const prepared = await f.prepare(receipt);
1575
+ const before = await f.storage.read("conversation", tx => tx.readMetadata());
1576
+ const canonical = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1577
+ for (const [key, value] of [
1578
+ ["sessionId", "ses_foreign_fixture_session"], ["workdir", "/different-fixture-workdir"],
1579
+ ["directory", "/different-fixture-directory"], ["databasePath", "/different-fixture-history"],
1580
+ ["runtimeDirectory", "/different-fixture-runtime"]
1581
+ ]) {
1582
+ const changed = structuredClone(before);
1583
+ changed.runtime.binding[key] = value;
1584
+ await f.storage.write("conversation", tx => tx.writeMetadata(changed));
1585
+ try {
1586
+ await assert.rejects(f.prepare(receipt), /different native conversation or host scope/);
1587
+ await assert.rejects(executeCompletedOpenCode(prepared), /identity changed|different native conversation or host scope/);
1588
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), changed);
1589
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), canonical);
1590
+ } finally { await f.storage.write("conversation", tx => tx.writeMetadata(before)); }
1591
+ }
1592
+ assert.equal(f.effects(), 0);
1593
+ assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1594
+ assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
1595
+ } finally { await f.close(); }
1596
+ });
1597
+
1598
+ test("completed OpenCode caller markers preserve ordinary native discovery without completion authority", async t => {
1599
+ const f = await completedOpenCodeFixture(t, { assistantResponses: [], configuration: { outputSchema: undefined } });
1600
+ try {
1601
+ const receipt = await f.conversation.send({ messageId: "ordinary-opencode", text: "tools", data: { completedEnvelope: true } });
1602
+ const state = await f.conversation.wait();
1603
+ const turn = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1604
+ assert.equal(turn.metadata.runtime.status, "complete", state.error);
1605
+ assert.equal(turn.metadata.runtime.completedEnvelope, undefined);
1606
+ assert.equal(f.effects(), 0, "Caller data is not a completed-envelope effect authorization");
1607
+ const initialTrace = await f.trace();
1608
+ const ordinaryPrompt = initialTrace.find(row => row.url?.endsWith("/prompt_async"));
1609
+ assert.equal(ordinaryPrompt.body.agent, "jskit-assistant-actions");
1610
+ assert.match(ordinaryPrompt.body.parts[0].text, /\[Application data\]/);
1611
+ assert.deepEqual(initialTrace[0].configuration.agent[ordinaryPrompt.body.agent].permission, {
1612
+ "*": "ask", assistant_action_search: "allow", assistant_action_contract: "allow", assistant_action_execute: "allow"
1613
+ });
1614
+ await assert.rejects(f.prepare({ messageId: "ordinary-opencode", turnId: receipt.turnId }), /no current verified receipt/);
1615
+ // The original native fixture invokes discovery only for exact plain tools.
1616
+ const toolReceipt = await f.conversation.send({ messageId: "ordinary-opencode-tools", text: "tools" });
1617
+ const toolsState = await f.conversation.wait();
1618
+ const toolsTurn = await f.storage.read("conversation", tx => tx.readTurn(toolReceipt.turnId));
1619
+ assert.equal(toolsTurn.metadata.runtime.status, "complete", toolsState.error);
1620
+ assert.equal(toolsTurn.metadata.runtime.completedEnvelope, undefined);
1621
+ assert.equal(toolsTurn.metadata.applicationTools.length, 3);
1622
+ assert.equal(f.effects(), 1);
1623
+ assert.equal(JSON.parse(toolsTurn.assistant.text).result.result.value, 42);
1624
+ await assert.rejects(f.prepare({ messageId: "ordinary-opencode-tools", turnId: toolReceipt.turnId }), /no current verified receipt/);
1625
+ const trace = await f.trace();
1626
+ const prompts = trace.filter(row => row.url?.endsWith("/prompt_async"));
1627
+ assert.equal(prompts.length, 2);
1628
+ assert.equal(prompts[1].body.parts[0].text, "tools");
1629
+ assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
1630
+ } finally { await f.close(); }
1631
+ });
1632
+
1633
+
1634
+ test("completed OpenCode prepared reply preserves native receipt and publishes one plain final across reopen", async t => {
1635
+ const value = { kind: "reply", text: " Verified OpenCode reply. ", toolName: "", arguments: "" };
1636
+ const f = await completedOpenCodeFixture(t, { assistantResponses: [{ text: JSON.stringify(value) }] });
1637
+ try {
1638
+ const { createConversationTranscript } = await import("../src/server/conversation/transcript.js");
1639
+ const transcript = createConversationTranscript({ storage: f.storage });
1640
+ const plain = await transcript.writeConversationUserMessage("conversation", {
1641
+ messageId: "opencode-product-user", text: "Actual product request", data: { original: true }
1642
+ });
1643
+ const receipt = await f.conversation.wake({ messageId: "opencode-internal-reply", text: "Give a reply" },
1644
+ { excludedMessageIds: [plain.user.messageId] });
1645
+ const state = await f.conversation.wait();
1646
+ assert.equal(state.status, "ready", state.error);
1647
+ const raw = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1648
+ assert.equal(raw.metadata.runtime.status, "complete");
1649
+ assert.equal(raw.metadata.runtime.completedEnvelope, true);
1650
+ assert.equal(raw.assistant.text, JSON.stringify(value));
1651
+ const binding = await f.binding();
1652
+ const nativeBefore = JSON.parse(await readFile(binding.databasePath, "utf8")).find(row => row.id === binding.sessionId);
1653
+ const prompt = (await f.trace()).find(row => row.url?.endsWith("/prompt_async"));
1654
+ const authored = nativeBefore.messages.find(row => row.info.role === "user" && row.info.id === prompt.body.messageID);
1655
+ assert.ok(authored);
1656
+ const answer = nativeBefore.messages.find(row => row.info.role === "assistant" && row.info.parentID === authored.info.id);
1657
+ assert.ok(answer);
1658
+ assert.equal(answer.info.finish, "stop");
1659
+ assert.equal(answer.parts[0].text, JSON.stringify(value));
1660
+ assert.equal(raw.assistant.messageId, `${receipt.turnId}:${answer.info.id}:assistant`);
1661
+ const before = await f.storage.read("conversation", tx => tx.readMetadata());
1662
+ const first = await f.prepare(receipt), second = await f.prepare(receipt);
1663
+ const [one, two] = await Promise.all([first.publishReply({ turnId: plain.turnId }), second.publishReply({ turnId: plain.turnId })]);
1664
+ assert.deepEqual(two, one);
1665
+ assert.equal(one.assistant.text, "Verified OpenCode reply.");
1666
+ assert.deepEqual(one.user, plain.user);
1667
+ assert.equal(one.metadata?.runtime?.engine, undefined);
1668
+ const { conversationMessageIdentity, conversationMessageVersion } = await import("../src/server/conversation/continuity.js");
1669
+ const assistantIdentity = conversationMessageIdentity(plain.turnId, one.assistant);
1670
+ const after = await f.storage.read("conversation", tx => tx.readMetadata());
1671
+ assert.deepEqual(after.runtime.seen, { ...before.runtime.seen,
1672
+ [assistantIdentity]: conversationMessageVersion(one.assistant, { includeData: true }) });
1673
+ const savedRaw = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1674
+ assert.deepEqual(savedRaw, { ...raw, metadata: { ...raw.metadata,
1675
+ runtime: { ...raw.metadata.runtime, publishedReplyTurnId: plain.turnId } } });
1676
+ await f.reopen();
1677
+ assert.deepEqual(await (await f.prepare(receipt)).publishReply({ turnId: plain.turnId }), one);
1678
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), savedRaw);
1679
+ assert.deepEqual((await f.storage.read("conversation", tx => tx.readMetadata())).runtime.seen, after.runtime.seen);
1680
+ const restored = await f.binding();
1681
+ for (const key of ["sessionId", "accountIdentity", "workdir", "directory", "databasePath", "runtimeDirectory"]) {
1682
+ assert.equal(restored[key], binding[key]);
1683
+ }
1684
+ assert.deepEqual(JSON.parse(await readFile(restored.databasePath, "utf8")).find(row => row.id === restored.sessionId).messages,
1685
+ nativeBefore.messages, "Publication and cold replay preserve the actual native input/answer identities and history");
1686
+ assert.deepEqual((await f.conversation.read()).conversationLog.map(turn => turn.assistant?.text), ["Verified OpenCode reply."]);
1687
+ const trace = await f.trace();
1688
+ assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1689
+ assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
1690
+ assert.equal(f.effects(), 0);
1691
+ } finally { await f.close(); }
1692
+ });
1693
+
1694
+ test("completed OpenCode prepared reply rechecks the original fresh account before publication", async t => {
1695
+ const value = { kind: "reply", text: "Verified OpenCode reply.", toolName: "", arguments: "" };
1696
+ const f = await completedOpenCodeFixture(t, { assistantResponses: [{ text: JSON.stringify(value) }] });
1697
+ try {
1698
+ const { createConversationTranscript } = await import("../src/server/conversation/transcript.js");
1699
+ const transcript = createConversationTranscript({ storage: f.storage });
1700
+ const plain = await transcript.writeConversationUserMessage("conversation", {
1701
+ messageId: "opencode-refused-product-user", text: "Actual product request"
1702
+ });
1703
+ const receipt = await f.conversation.wake({ messageId: "opencode-account-reply", text: "Give a reply" },
1704
+ { excludedMessageIds: [plain.user.messageId] });
1705
+ const state = await f.conversation.wait();
1706
+ assert.equal(state.status, "ready", state.error);
1707
+ const raw = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1708
+ assert.equal(raw.metadata.runtime.status, "complete");
1709
+ assert.equal(raw.assistant.text, JSON.stringify(value));
1710
+ const prepared = await f.prepare(receipt);
1711
+ const before = await f.storage.read("conversation", tx => tx.readMetadata());
1712
+ const resolutions = f.accountResolutions();
1713
+ f.setKey("changed-opencode-fixture-account");
1714
+ try {
1715
+ for (let attempt = 0; attempt < 2; attempt++) {
1716
+ await assert.rejects(prepared.publishReply({ turnId: plain.turnId }), /different account binding/);
1717
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(plain.turnId)), plain);
1718
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), raw);
1719
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), before);
1720
+ }
1721
+ assert.ok(f.accountResolutions() >= resolutions + 2, "Publication uses the actual fresh connection resolver, not the prepared cached account");
1722
+ const trace = await f.trace();
1723
+ assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1724
+ assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
1725
+ assert.equal(f.effects(), 0);
1726
+ } finally { f.setKey("test-key"); }
1727
+ } finally { await f.close(); }
1728
+ });
1729
+
1730
+
1731
+ // Preserve the original fixed-envelope decoded limits at the native wire boundary.
1732
+ const completedOpenCodeWireSchema = { type: "object", additionalProperties: false,
1733
+ required: ["kind", "text", "toolName", "arguments"], properties: {
1734
+ kind: { type: "string", enum: ["reply", "tool"] }, text: { type: "string", maxLength: 16000 },
1735
+ toolName: { type: "string", maxLength: 256 }, arguments: { type: "string", maxLength: 262144 }
1736
+ } };
1737
+
1738
+ for (const kind of ["tool", "reply"]) test(`completed OpenCode ${kind} wire retains large decoded fields through native storage and preparation`, async t => {
1739
+ const handover = "😀".repeat(20000);
1740
+ const argumentsText = JSON.stringify({ value: handover }).replace(/[\u0080-\uffff]/g,
1741
+ character => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
1742
+ const value = kind === "tool" ? { kind, text: "", toolName: "numbers_read", arguments: argumentsText }
1743
+ : { kind, text: "\u0000".repeat(16000), toolName: "", arguments: "" };
1744
+ const raw = JSON.stringify(value);
1745
+ assert.ok(raw.length > (kind === "tool" ? 280000 : 16000));
1746
+ if (kind === "tool") assert.equal(value.arguments.length, 240012);
1747
+ else assert.equal(value.text.length, 16000);
1748
+ const f = await completedOpenCodeFixture(t, { assistantResponses: [{ text: raw }],
1749
+ configuration: { outputSchema: completedOpenCodeWireSchema },
1750
+ limits: { maxFinalReplyCharacters: 16000, maxOutputCharacters: 1670455 } });
1751
+ try {
1752
+ const receipt = await f.conversation.wake({ messageId: `opencode-large-${kind}-wire`, text: "Read the completed envelope" });
1753
+ const state = await f.conversation.wait();
1754
+ assert.equal(state.status, "ready", state.error);
1755
+ const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1756
+ assert.equal(saved.metadata.runtime.status, "complete");
1757
+ assert.equal(saved.metadata.runtime.completedEnvelope, true);
1758
+ assert.equal(saved.system.origin, "application");
1759
+ assert.equal(saved.system.messageId, receipt.messageId);
1760
+ assert.equal(f.appendedRows.length, 1);
1761
+ assert.equal(f.appendedRows[0].metadata.runtime.completedEnvelope, true);
1762
+ assert.equal(saved.assistant.text, raw);
1763
+ const binding = await f.binding();
1764
+ const nativeBefore = JSON.parse(await readFile(binding.databasePath, "utf8")).find(row => row.id === binding.sessionId);
1765
+ const prompt = (await f.trace()).find(row => row.url?.endsWith("/prompt_async"));
1766
+ const authored = nativeBefore.messages.find(row => row.info.role === "user" && row.info.id === prompt.body.messageID);
1767
+ assert.ok(authored);
1768
+ const answer = nativeBefore.messages.find(row => row.info.role === "assistant" && row.info.parentID === authored.info.id);
1769
+ assert.ok(answer);
1770
+ assert.equal(answer.info.finish, "stop");
1771
+ assert.equal(answer.parts[0].text, raw);
1772
+ assert.equal(saved.assistant.messageId, `${receipt.turnId}:${answer.info.id}:assistant`);
1773
+ const prepared = await f.prepare(receipt);
1774
+ assert.equal(prepared.text, raw);
1775
+ const { readAssistantResponseEnvelope } = await import("../src/server/lib/assistantToolLoop.js");
1776
+ const decoded = readAssistantResponseEnvelope(prepared.text);
1777
+ assert.deepEqual(decoded, value);
1778
+ if (kind === "tool") assert.equal(JSON.parse(decoded.arguments).value, handover);
1779
+ assert.equal(f.effects(), 0, "Preparing and decoding do not execute the requested action");
1780
+ await f.reopen();
1781
+ const restored = await f.prepare(receipt);
1782
+ assert.equal(restored.text, raw);
1783
+ assert.equal(restored.toolCallId, prepared.toolCallId);
1784
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), saved);
1785
+ const reopenedBinding = await f.binding();
1786
+ assert.equal(reopenedBinding.sessionId, binding.sessionId);
1787
+ assert.deepEqual(JSON.parse(await readFile(reopenedBinding.databasePath, "utf8")).find(row => row.id === binding.sessionId).messages,
1788
+ nativeBefore.messages);
1789
+ const trace = await f.trace();
1790
+ assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1791
+ assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
1792
+ assert.deepEqual((await f.conversation.read()).conversationLog, []);
1793
+ assert.equal(f.effects(), 0);
1794
+ } finally { await f.close(); }
1795
+ });
1796
+
1797
+ test("ordinary OpenCode forged-data wire retains the human cap in completed mode", async t => {
1798
+ const value = { kind: "reply", text: "\u0000".repeat(16000), toolName: "", arguments: "" };
1799
+ const raw = JSON.stringify(value);
1800
+ const f = await completedOpenCodeFixture(t, { assistantResponses: [{ text: raw }],
1801
+ configuration: { outputSchema: completedOpenCodeWireSchema },
1802
+ limits: { maxFinalReplyCharacters: 16000, maxOutputCharacters: 1670455 } });
1803
+ try {
1804
+ const receipt = await f.conversation.send({ messageId: "opencode-ordinary-forged-wire", text: "An ordinary request",
1805
+ data: { completedEnvelope: true } });
1806
+ await f.conversation.wait();
1807
+ const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
1808
+ assert.equal(saved.metadata.runtime.status, "failed");
1809
+ assert.match(saved.metadata.runtime.error, /final reply limit/);
1810
+ assert.equal(saved.metadata.runtime.completedEnvelope, undefined);
1811
+ assert.equal(saved.user.origin, "user");
1812
+ assert.deepEqual(saved.user.data, { completedEnvelope: true });
1813
+ assert.notEqual(saved.assistant?.text, raw);
1814
+ assert.equal((await f.conversation.inspectDelivery({ messageId: receipt.messageId })).status, "accepted");
1815
+ assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
1816
+ assert.equal(f.effects(), 0);
1817
+ } finally { await f.close(); }
1818
+ });
1819
+
1820
+
1821
+ test("managed OpenCode refreshes its own key in the retained native scope without transferring completed receipt authority", async t => {
1822
+ const { createOpenCodeConversationServer } = await import("../src/server/conversation/openCodeProcess.js");
1823
+ const { createOpenCodeSharedRuntime } = await import("../src/server/conversation/openCodeRuntime.js");
1824
+ let f, owner, foreignScope = false;
1825
+ const starts = [], stops = [];
1826
+ f = await completedOpenCodeFixture(t, { opencode: async ({ connection }) => {
1827
+ const runtimeDirectory = path.join(f.directory, "managed-native");
1828
+ owner ||= createOpenCodeSharedRuntime({ scope: runtimeDirectory, onStop: event => stops.push(event) });
1829
+ const selected = { modelProviderId: connection.providerId, canonicalUrl: connection.baseURL || "",
1830
+ endpointCode: connection.sdkPackage || "", fingerprint: connection.apiKey };
1831
+ return { runtime: owner, runtimeDirectory, allowCredentialRefresh: true,
1832
+ registryPath: path.join(runtimeDirectory, "environments.json"),
1833
+ databasePath: path.join(runtimeDirectory, foreignScope ? "foreign-native.json" : "native.json"), selected,
1834
+ prepareServer: async () => ({ connections: [selected],
1835
+ options: { command: f.driverOptions.host.commands.opencode, workdir: f.directory,
1836
+ stateDirectory: runtimeDirectory, databasePath: path.join(runtimeDirectory, "native.json"),
1837
+ sessionEnvironmentRegistry: path.join(runtimeDirectory, "environments.json"),
1838
+ connection, env: f.driverOptions.host.env },
1839
+ create: async options => {
1840
+ const server = await createOpenCodeConversationServer(options);
1841
+ starts.push({ options, server });
1842
+ return server;
1843
+ }
1844
+ }) };
1845
+ } });
1846
+ try {
1847
+ const first = await f.complete("managed-first");
1848
+ const firstCanonical = await f.storage.read("conversation", tx => tx.readTurn(first.turnId));
1849
+ const before = await f.binding();
1850
+ const prior = JSON.parse(await readFile(before.databasePath, "utf8"));
1851
+ const prepared = await f.prepare(first);
1852
+ f.setKey("managed-replacement-key");
1853
+ await assert.rejects(f.prepare(first), /different account binding/);
1854
+ await assert.rejects(executeCompletedOpenCode(prepared), /different account binding/);
1855
+ assert.deepEqual(await f.binding(), before);
1856
+ assert.equal(f.effects(), 0);
1857
+ const second = await f.complete("managed-second");
1858
+ const secondCanonical = await f.storage.read("conversation", tx => tx.readTurn(second.turnId));
1859
+ const after = await f.binding();
1860
+ assert.equal(after.sessionId, before.sessionId);
1861
+ assert.equal(starts.length, 2);
1862
+ assert.equal(starts[0].options.databasePath, starts[1].options.databasePath);
1863
+ assert.equal(starts[0].options.workdir, starts[1].options.workdir);
1864
+ assert.equal(starts[0].options.connection.apiKey, "test-key");
1865
+ assert.equal(starts[1].options.connection.apiKey, "managed-replacement-key");
1866
+ assert.ok(stops.some(event => event.reason === "opencode-connection-changed"));
1867
+ await assert.rejects(starts[0].server.client.health({ signal: AbortSignal.timeout(1000) }));
1868
+ const trace = await f.trace();
1869
+ assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
1870
+ assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 2);
1871
+ const current = JSON.parse(await readFile(after.databasePath, "utf8")).find(row => row.id === after.sessionId);
1872
+ const original = prior.find(row => row.id === before.sessionId);
1873
+ assert.deepEqual(current.messages.slice(0, original.messages.length), original.messages);
1874
+ assert.equal(f.appendedRows.length, 2);
1875
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(first.turnId)), firstCanonical);
1876
+ foreignScope = true;
1877
+ await assert.rejects(f.conversation.send({ messageId: "foreign-store", text: "Again" }), /another native history store or runtime/);
1878
+ const refused = await f.binding();
1879
+ assert.deepEqual(refused, { ...after, executionId: "", processDirectory: "" });
1880
+ await assert.rejects(starts[1].server.client.health({ signal: AbortSignal.timeout(1000) }));
1881
+ assert.deepEqual(JSON.parse(await readFile(after.databasePath, "utf8")).find(row => row.id === after.sessionId), current);
1882
+ assert.equal(starts.length, 2);
1883
+ assert.equal(f.appendedRows.length, 2);
1884
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(first.turnId)), firstCanonical);
1885
+ assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(second.turnId)), secondCanonical);
1886
+ assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 2);
1887
+ assert.equal(f.effects(), 0);
1888
+ } finally {
1889
+ await f.close();
1890
+ await owner?.stop("managed-fixture-close");
1891
+ }
1892
+ });
1893
+
1894
+
1895
+ test("opted bound OpenCode re-enters supplied callback context after admission ends", async t => {
1896
+ const { AsyncLocalStorage } = await import("node:async_hooks");
1897
+ const context = new AsyncLocalStorage();
1898
+ const admission = { scope: "admission" }, control = { scope: "control" };
1899
+ const released = Promise.withResolvers();
1900
+ t.after(() => released.resolve());
1901
+ let reentries = 0;
1902
+ const f = await boundToolFixture(t, { runInContext(operation) {
1903
+ reentries++;
1904
+ return context.run(control, operation);
1905
+ } });
1906
+ const calls = [];
1907
+ const main = await f.open("delayed-context-main", async call => {
1908
+ await released.promise;
1909
+ assert.equal(context.getStore(), control, "A retained callback must re-enter the supplied context");
1910
+ calls.push(call.name);
1911
+ return {};
1912
+ });
1913
+ const running = context.run(admission, () => main.send("delayed-context-words", "tools"));
1914
+ await running.accepted;
1915
+ assert.equal(context.getStore(), undefined, "The authored admission callback has ended");
1916
+ released.resolve();
1917
+ await running.completion;
1918
+ assert.deepEqual(calls, ["assistant_action_search", "assistant_action_contract", "assistant_action_execute"]);
1919
+ assert.equal(reentries, 3);
1920
+ assert.equal(main.receipts.length, 1);
1921
+ assert.equal(main.receipts[0].turn.messages[0].text, "tools");
1922
+ assert.equal(f.owner.turns.get(main.key).active, false);
1923
+ });
1924
+
1925
+
1926
+ test("opted bound OpenCode retains supplied control through delayed monitor publication and retirement", async t => {
1927
+ const { AsyncLocalStorage } = await import("node:async_hooks");
1928
+ const context = new AsyncLocalStorage();
1929
+ const admission = { scope: "admission" }, control = { scope: "control" };
1930
+ const released = Promise.withResolvers();
1931
+ t.after(() => released.resolve());
1932
+ const phases = [];
1933
+ const f = await boundToolFixture(t, {
1934
+ runInContext: operation => context.run(control, operation),
1935
+ monitorRunInContext: async operation => context.run(control, operation),
1936
+ async monitorContext(phase) {
1937
+ if (phase !== "onRetired") {
1938
+ await released.promise;
1939
+ assert.equal(context.getStore(), control, "The retained monitor must inherit supplied CONTROL");
1940
+ }
1941
+ phases.push({ phase, context: context.getStore() });
1942
+ }
1943
+ });
1944
+ const calls = [];
1945
+ const main = await f.open("delayed-monitor-main", async call => { calls.push(call.name); return {}; });
1946
+ const running = context.run(admission, () => main.send("delayed-monitor-words", "tools"));
1947
+ await running.accepted;
1948
+ assert.equal(context.getStore(), undefined, "Send admission ended before delayed publication");
1949
+ const retainedMonitor = f.owner.monitors.get(main.key);
1950
+ assert.equal(f.owner.beginMessageMonitor(main.key, f.owner.processes.get(main.key), {}, {}, {
1951
+ runInContext: async operation => context.run(control, operation),
1952
+ prepare: () => assert.fail("An existing monitor must not be recreated")
1953
+ }), retainedMonitor, "An async scope wrapper must preserve the original monitor promise");
1954
+ const preparationFailure = new Error("Exact synchronous monitor preparation failure");
1955
+ const scopeErrors = [];
1956
+ assert.throws(() => f.owner.beginMessageMonitor("failed-monitor-preparation", {}, {}, {}, {
1957
+ runInContext: async operation => context.run(control, operation),
1958
+ prepare() { assert.equal(context.getStore(), control); throw preparationFailure; },
1959
+ onError: error => scopeErrors.push(error)
1960
+ }), error => error === preparationFailure);
1961
+ await new Promise(resolve => setImmediate(resolve));
1962
+ assert.deepEqual(scopeErrors, [], "A synchronous creation failure is not reported again by the scope wrapper");
1963
+ released.resolve();
1964
+ await running.completion;
1965
+ assert.deepEqual(calls, ["assistant_action_search", "assistant_action_contract", "assistant_action_execute"]);
1966
+ assert.ok(phases.some(value => value.phase === "writeRun"));
1967
+ assert.ok(phases.some(value => value.phase === "projectMessages"));
1968
+ assert.ok(phases.some(value => value.phase === "onRetired"));
1969
+ assert.ok(phases.every(value => value.context === control));
1970
+ assert.equal(main.receipts.length, 1);
1971
+ assert.equal(main.receipts[0].turn.messages[0].text, "tools");
1972
+ assert.equal(f.owner.turns.get(main.key).active, false);
1973
+ assert.equal(f.owner.turns.get(main.key).state, "completed");
1974
+ });