@jskit-ai/assistant-core 0.2.16 → 0.2.18

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 (33) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/examples/conversation/package.json +5 -5
  3. package/package.json +6 -6
  4. package/src/server/conversation/claudeNativeTurn.js +9 -1
  5. package/src/server/conversation/claudeSessionCleanup.js +9 -1
  6. package/src/server/conversation/claudeTurn.js +1 -0
  7. package/src/server/conversation/codexConversations.js +65 -6
  8. package/src/server/conversation/codexRunJournal.js +33 -1
  9. package/src/server/conversation/codexRuntimeLifecycle.js +20 -1
  10. package/src/server/conversation/codexTurn.js +41 -4
  11. package/src/server/conversation/codexTurnEndings.js +9 -6
  12. package/src/server/conversation/nativeConversationLifecycle.js +22 -0
  13. package/src/server/conversation/openCodeProcessOwner.js +11 -5
  14. package/src/server/conversation/openCodeRuntime.js +1 -0
  15. package/src/server/conversation/openCodeTurn.js +10 -5
  16. package/src/server/conversation/openCodeTurnMonitor.js +4 -2
  17. package/src/server/conversation/providers/claudeDriver.js +5 -0
  18. package/src/server/conversation/providers/codexDriver.js +5 -0
  19. package/src/server/conversation/providers/opencodeDriver.js +5 -0
  20. package/src/server/conversation/runtime.js +1 -0
  21. package/test/apiConversationDriver.test.js +8 -4
  22. package/test/claudeLifecycle.test.js +28 -0
  23. package/test/codexConversationsCheckpoint.test.js +2 -0
  24. package/test/codexLifecycleIsolation.test.js +2 -0
  25. package/test/codexShutdownStop.test.js +168 -0
  26. package/test/codexTemporaryProviderRelease.test.js +2 -0
  27. package/test/codexTurn.test.js +57 -0
  28. package/test/conversationApiFacilities.test.js +4 -3
  29. package/test/conversationAttachments.test.js +4 -4
  30. package/test/conversationReplacement.test.js +1 -1
  31. package/test/conversationSession.test.js +40 -10
  32. package/test/conversationTools.test.js +2 -2
  33. package/test/openCodeLifecycle.test.js +55 -3
@@ -87,7 +87,7 @@ test("split internal tags and tool arguments never become common answer deltas",
87
87
  await f.conversation.send(message);
88
88
  const result = await f.conversation.wait();
89
89
  assert.deepEqual(f.observed.executions, [{ left: 2, right: 3 }]);
90
- assert.equal(f.observed.requests[1].messages.at(-1).content, checkTexts("the app").markup);
90
+ assert.equal(f.observed.requests[1].messages.at(-2).content, checkTexts("the app").markup, "The check comes last, before the engine's state note");
91
91
  assert.deepEqual(f.observed.events.filter(event => event.type === "message" && event.status !== "complete")
92
92
  .map(event => event.text), ["The search is complete."]);
93
93
  assert.equal(result.conversationLog[0].assistant.text, "The search is complete.");
@@ -101,7 +101,7 @@ test("text-encoded calls never run or reach the person, even when written again
101
101
  const result = await f.conversation.wait();
102
102
  assert.deepEqual(f.observed.executions, []);
103
103
  assert.equal(f.observed.requests.length, 2, "The check sends the reply back once");
104
- assert.equal(f.observed.requests[1].messages.at(-1).content, checkTexts("the app").markup);
104
+ assert.equal(f.observed.requests[1].messages.at(-2).content, checkTexts("the app").markup, "The check comes last, before the engine's state note");
105
105
  assert.deepEqual(result.conversationLog[0].metadata.applicationTools, []);
106
106
  assert.equal(f.observed.events.some(event => event.type === "message" && /invoke|function_calls/u.test(event.text)), false);
107
107
  });
@@ -13,8 +13,8 @@ const ABORTED = "The operation was aborted.";
13
13
  const PROVIDER_FAILURE = "The provider refused this request.";
14
14
 
15
15
  // OpenCode's native history outlives each server that runs it. The turn's reply
16
- // is still being written until an interrupt ends it as aborted, or its provider
17
- // fails it.
16
+ // is still being written until an interrupt ends it as aborted, its provider
17
+ // fails it, or it completes on its own.
18
18
  function nativeOpenCode() {
19
19
  const rows = [
20
20
  { id: INPUT_ID, type: "user", time: { created: 1 } },
@@ -46,7 +46,11 @@ function nativeOpenCode() {
46
46
  rows[1] = { ...rows[1], error: { name: "APIError", data: { message: PROVIDER_FAILURE } }, time: { created: 2, completed: 3 } };
47
47
  busy = false;
48
48
  }
49
- return { calls, create, fail };
49
+ function complete() {
50
+ rows[1] = { ...rows[1], text: "Done.", finish: "stop", time: { created: 2, completed: 3 } };
51
+ busy = false;
52
+ }
53
+ return { calls, create, fail, complete };
50
54
  }
51
55
 
52
56
  // One server with Vibe64's run rules: the session record keeps the OpenCode run
@@ -108,6 +112,8 @@ function server(native, saved, { turnEndCheckpoint = async () => {} } = {}) {
108
112
  return {
109
113
  runtime, calls, release,
110
114
  shutdown: () => runtime.invalidateRuntimes({ reason: "server-shutdown" }, { release }),
115
+ // The server's shutdown begins: nothing stops yet.
116
+ begin: () => runtime.beginShutdown(),
111
117
  async running() {
112
118
  const target = await runtime.acquirePrepared(processPreparation);
113
119
  void runtime.beginMessageMonitor(KEY, target, { id: INPUT_ID, eventStartedAt: 1, startedAt: "2026-11-05T00:00:00.000Z" }, {}, monitor);
@@ -284,3 +290,49 @@ test("OpenCode shutdown ends a temporary conversation's turn as interrupted, and
284
290
  await closed.entry.completion.catch(() => null);
285
291
  assert.deepEqual(closed.closes, [{ stopped: true, outcome: "failed", stoppedByShutdown: false }]);
286
292
  });
293
+
294
+ test("once OpenCode's shutdown has begun, a main turn that ends on its own is left to the next server, which records it with one checkpoint", { timeout: 5_000 }, async t => {
295
+ const native = nativeOpenCode();
296
+ const saved = { run: null };
297
+ const first = server(native, saved);
298
+ t.after(() => first.shutdown());
299
+ await first.running();
300
+ first.begin();
301
+ assert.deepEqual({ interrupts: native.calls.interrupts, stops: native.calls.stops }, { interrupts: [], stops: 0 },
302
+ "beginning the shutdown stopped nothing");
303
+ native.complete();
304
+ await first.runtime.monitors.get(KEY);
305
+ assert.deepEqual(first.calls.checkpoints, [], "the turn's end made no checkpoint");
306
+ assert.deepEqual(runOf(saved), { active: true, state: "active", error: "", stoppedByShutdown: true, turnId: INPUT_ID });
307
+ assert.equal((await first.shutdown()).ok, true);
308
+ assert.equal(native.calls.stops, 1, "the full stop still proved the OpenCode process exit");
309
+ assert.deepEqual(first.calls.checkpoints, []);
310
+
311
+ const next = server(native, saved);
312
+ t.after(() => next.shutdown());
313
+ assert.equal((await next.ready()).ok, true);
314
+ assert.equal((await next.ready()).ok, true);
315
+ assert.deepEqual(next.calls.checkpoints, [`${INPUT_ID}:interrupted`], "the next server made the checkpoint once");
316
+ assert.deepEqual(runOf(saved), { active: false, state: "interrupted", error: "", stoppedByShutdown: true, turnId: INPUT_ID });
317
+ });
318
+
319
+ test("once OpenCode's shutdown has begun, a temporary conversation's turn that ends on its own leaves its checkpoint to the next server", { timeout: 5_000 }, async t => {
320
+ const native = nativeOpenCode();
321
+ const first = server(native, { run: null });
322
+ t.after(() => first.shutdown());
323
+ const { entry, closes } = await first.temporary("ses_temp");
324
+ first.begin();
325
+ native.complete();
326
+ assert.deepEqual(await entry.completion, { interrupted: true });
327
+ assert.deepEqual(native.calls.interrupts, [], "nothing interrupted the native turn");
328
+ assert.deepEqual(closes, [{ stopped: true, outcome: "interrupted", stoppedByShutdown: true }],
329
+ "the application is told to leave the turn's checkpoint to the next server");
330
+ assert.equal((await first.shutdown()).ok, true);
331
+
332
+ const next = server(native, { run: null });
333
+ t.after(() => next.shutdown());
334
+ const ended = [];
335
+ const read = await next.readTemporary("ses_temp", { runId: INPUT_ID, async ended() { ended.push(INPUT_ID); } });
336
+ assert.deepEqual({ status: read.status, error: read.error }, { status: "interrupted", error: "" });
337
+ assert.deepEqual(ended, [INPUT_ID], "recording the run lets the application make its checkpoint");
338
+ });