@jskit-ai/assistant-core 0.2.18 → 0.2.19

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.
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.2.18",
12
- "@jskit-ai/http-runtime": "0.2.8",
13
- "@jskit-ai/kernel": "0.2.8",
11
+ "@jskit-ai/assistant-core": "0.2.19",
12
+ "@jskit-ai/http-runtime": "0.2.9",
13
+ "@jskit-ai/kernel": "0.2.9",
14
14
  "@mdi/js": "^7.4.47",
15
15
  "vue": "^3.5.13",
16
16
  "vuetify": "^4.0.0",
17
- "@jskit-ai/connectors-core": "0.2.9",
18
- "@jskit-ai/connectors-catalog": "0.2.9"
17
+ "@jskit-ai/connectors-core": "0.2.10",
18
+ "@jskit-ai/connectors-catalog": "0.2.10"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@vitejs/plugin-vue": "^6.0.7",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -51,8 +51,8 @@
51
51
  "@ai-sdk/togetherai": "^3.0.49",
52
52
  "@ai-sdk/xai": "^4.0.58",
53
53
  "@jskit-ai/agent": "0.1.1",
54
- "@jskit-ai/resource-core": "0.1.199",
55
- "@jskit-ai/resource-crud-core": "0.1.199",
54
+ "@jskit-ai/resource-core": "0.1.200",
55
+ "@jskit-ai/resource-crud-core": "0.1.200",
56
56
  "@mdi/js": "^7.4.47",
57
57
  "@openrouter/ai-sdk-provider": "^3.0.0",
58
58
  "ai": "^7.0.152",
@@ -61,8 +61,8 @@
61
61
  "ws": "^8.20.1"
62
62
  },
63
63
  "peerDependencies": {
64
- "@jskit-ai/http-runtime": "0.2.8",
65
- "@jskit-ai/kernel": "0.2.8",
64
+ "@jskit-ai/http-runtime": "0.2.9",
65
+ "@jskit-ai/kernel": "0.2.9",
66
66
  "vue": "^3.5.13",
67
67
  "vuetify": "^4.0.0"
68
68
  },
@@ -1,4 +1,4 @@
1
- import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, CODEX_APP_SERVER_AGENT_RUN_ID, CODEX_AGENT_PROVIDER, CODEX_APP_SERVER_RUN_STATE } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerTurnKey, CODEX_APP_SERVER_AGENT_RUN_ID, CODEX_AGENT_PROVIDER, CODEX_APP_SERVER_RUN_STATE } from "./codexTurnState.js";
2
2
  import { codexAppServerProviderConnectionGeneration } from "./codexModelCatalog.js";
3
3
  import {
4
4
  codexAppServerAssistantItemText,
@@ -239,6 +239,8 @@ export function createCodexNotificationObserver({
239
239
  threadId: normalizedThreadId,
240
240
  turnId: codexAppServerNotificationTurnId(notification)
241
241
  };
242
+ // Anything Codex reports about a turn means it has started that turn.
243
+ journal.unstartedTurns.delete(codexAppServerTurnKey(normalizedThreadId, notificationContext.turnId));
242
244
  if (method === "thread/tokenUsage/updated") {
243
245
  notificationQueue.run(notificationContext, async () => {
244
246
  const store = await createStore(normalizedSessionId);
@@ -1,4 +1,4 @@
1
- import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerMessageRequiresNewTurn, codexAppServerMessageDeferred } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerMessageRequiresNewTurn, codexAppServerMessageDeferred, codexAppServerTurnKey } from "./codexTurnState.js";
2
2
  import { codexAppServerRequestIsInvalid } from "./codexProviderThreads.js";
3
3
  import { codexLocalImageInput, codexAppServerDeliveredTurn, sendCodexAppServerPrompt } from "./codexProviderTurns.js";
4
4
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
@@ -74,6 +74,10 @@ export function createCodexPromptDispatch({
74
74
  if (!deliveredTurnId) {
75
75
  throw new Error("Codex app-server accepted the prompt without returning a turn id.");
76
76
  }
77
+ // Codex answers turn/start before it starts the turn; its notifications about the turn say when it has.
78
+ if (!codexAppServerTurnStatusIsComplete(deliveredTurnStatus)) {
79
+ journal.unstartedTurns.add(codexAppServerTurnKey(threadId, deliveredTurnId));
80
+ }
77
81
  delivery.pendingUserMessage(promptSessionId, clientUserMessageId)?.onNativeReceipt();
78
82
  await recovery.markTurnActive(sessionId, {
79
83
  requireTrackedTurn: true,
@@ -88,6 +88,8 @@ export function createCodexRunJournal({
88
88
  isClosing
89
89
  }) {
90
90
  const codexAppServerCompletedTurns = new Set();
91
+ // Turns Codex accepted with turn/start but has not yet reported: until it does, its thread still reads idle.
92
+ const codexAppServerUnstartedTurns = new Set();
91
93
  const codexAppServerProcessedTurns = new Set();
92
94
  const codexAppServerActiveTimers = new Map();
93
95
  const codexAppServerFinalizingTimers = new Map();
@@ -637,6 +639,7 @@ export function createCodexRunJournal({
637
639
 
638
640
  return Object.freeze({
639
641
  completedTurns: codexAppServerCompletedTurns,
642
+ unstartedTurns: codexAppServerUnstartedTurns,
640
643
  processedTurns: codexAppServerProcessedTurns,
641
644
  activeTimers: codexAppServerActiveTimers,
642
645
  finalizingTimers: codexAppServerFinalizingTimers,
@@ -1,4 +1,4 @@
1
- import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerTurnCanReceiveProviderCompletion, codexAppServerTurnCanAdoptSuccessor, codexAppServerErrorMessage } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerTurnCanReceiveProviderCompletion, codexAppServerTurnCanAdoptSuccessor, codexAppServerErrorMessage, codexAppServerTurnKey } from "./codexTurnState.js";
2
2
  import { isRecord, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
3
  import { codexAppServerStatusFromValue } from "./codexEvents.js";
4
4
  import { codexAppServerThreadRawValue, codexAppServerThreadTurnId, codexAppServerThreadError } from "./codexThreadSnapshot.js";
@@ -200,6 +200,27 @@ export function createCodexThreadStatus({ journal, settlement, recovery, output,
200
200
  turnId
201
201
  };
202
202
  }
203
+ // Codex has accepted the tracked turn but not started it. Until it does, its thread reads idle and lists the
204
+ // turn as interrupted, so only Codex's own report about the turn settles it.
205
+ if (
206
+ trackedProviderTurn?.state === "active" &&
207
+ !codexAppServerTurnStatusIsActive(status) &&
208
+ journal.unstartedTurns.has(codexAppServerTurnKey(trackedProviderTurn.threadId, trackedProviderTurn.turnId))
209
+ ) {
210
+ debugLog("appServerThread.reconcile.awaitingProviderStart", {
211
+ sessionId: normalizedSessionId,
212
+ source: normalizeText(source),
213
+ status,
214
+ threadId: trackedProviderTurn.threadId,
215
+ turnId: trackedProviderTurn.turnId
216
+ });
217
+ return {
218
+ ok: true,
219
+ reason: "provider_turn_not_started",
220
+ status: trackedProviderTurn.status || "inProgress",
221
+ turnId: trackedProviderTurn.turnId
222
+ };
223
+ }
203
224
  if (
204
225
  trackedProviderTurn &&
205
226
  turnId &&
@@ -1,4 +1,4 @@
1
- import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerPendingUserMessageOwnership, codexAppServerTurnCanAdoptSuccessor } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerPendingUserMessageOwnership, codexAppServerTurnCanAdoptSuccessor, codexAppServerTurnKey } from "./codexTurnState.js";
2
2
  import { codexAppServerProviderTurnForOperation, codexAppServerThreadTurnId, codexAppServerThreadError } from "./codexThreadSnapshot.js";
3
3
  import { createCodexTurnActivity } from "./codexTurnActivity.js";
4
4
  import { createCodexTurnEndings } from "./codexTurnEndings.js";
@@ -270,6 +270,17 @@ export function createCodexTurnRecovery({
270
270
  if (!codexAppServerTurnStatusIsComplete(status)) {
271
271
  return currentSession;
272
272
  }
273
+ if (journal.unstartedTurns.has(codexAppServerTurnKey(currentTurn.threadId, currentTurn.turnId))) {
274
+ // Codex has accepted this turn but not started it, and its thread reads idle until it does.
275
+ debugLog("appServerTurn.reconcile.awaitingProviderStart", {
276
+ sessionId,
277
+ status,
278
+ threadId: currentTurn.threadId,
279
+ turnId: currentTurn.turnId
280
+ });
281
+ scheduleCodexAppServerActiveRecovery(sessionId);
282
+ return currentSession;
283
+ }
273
284
  const completedTurnId = providerTurnId || currentTurn.turnId;
274
285
  debugLog("appServerTurn.reconcile.complete", {
275
286
  sessionId,
@@ -1910,6 +1910,51 @@ test("original dispatch steering preserves deferred versus new-turn recovery and
1910
1910
  }
1911
1911
  });
1912
1912
 
1913
+ // Codex answers turn/start before it starts the turn. Until its turn/started notification, codex-cli 0.163.0 reads
1914
+ // the thread as idle and lists the new turn as interrupted, so neither read means the turn ended. On 2026-11-06 the
1915
+ // starting watchdog's status read landed in that gap: the turn was marked finished 0.46 s after delivery, and Codex's
1916
+ // answer 33 s later was dropped behind "Codex finished, but Vibe64 could not recover its final response".
1917
+ for (const read of ["starting watchdog", "status check"]) {
1918
+ test(`a delivered turn Codex has not started stays active through a ${read} and keeps its answer`, async t => {
1919
+ const notices = [];
1920
+ const f = await receiptFixture({
1921
+ finalizingGraceMs: 30,
1922
+ outcomeNotice(_runtime, _sessionId, _threadId, _turnId, outcome) { notices.push(outcome); }
1923
+ });
1924
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1925
+ const context = dispatchContext(f);
1926
+ let codexStatus = "idle";
1927
+ f.run = { ...f.run, state: "starting", providerTurnId: "", providerStatus: "", outerTurnId: context.messageId };
1928
+ f.provider.sendTurn = async () => ({ id: f.turnId, status: "inProgress" });
1929
+ f.provider.readThreadStatus = async () => ({ raw: { status: { type: codexStatus } } });
1930
+ f.history = [turn(f.turnId, "interrupted")];
1931
+ f.observe();
1932
+ assert.equal((await f.owner.startTurn(f.sessionId, {}, context)).providerFailure, "");
1933
+ assert.equal(f.run.state, "active");
1934
+ if (read === "starting watchdog") {
1935
+ await f.owner.recoverActiveTurn(f.sessionId, { provider: f.provider, retryOnError: false });
1936
+ } else {
1937
+ await f.owner.reconcileThreadStatus(f.sessionId, f.provider, f.threadId, {
1938
+ observeLatestTurn: true, source: "wellbeing"
1939
+ });
1940
+ }
1941
+ await f.drain();
1942
+ assert.equal(f.run.state, "active", "An idle read before turn/started does not end the turn");
1943
+ assert.equal(f.run.providerTurnId, f.turnId);
1944
+ codexStatus = "active";
1945
+ f.history = [turn(f.turnId, "inProgress")];
1946
+ f.emit({ method: "turn/started", params: { threadId: f.threadId, turn: { id: f.turnId, status: "inProgress" } } });
1947
+ await f.drain();
1948
+ codexStatus = "idle";
1949
+ f.history = [turn(f.turnId, "completed", "The complete reply.")];
1950
+ f.emit({ method: "turn/completed", params: { threadId: f.threadId, turn: { id: f.turnId, status: "completed" } } });
1951
+ await f.drain();
1952
+ assert.equal(f.run.state, "completed");
1953
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["The complete reply."]);
1954
+ assert.deepEqual(notices, []);
1955
+ });
1956
+ }
1957
+
1913
1958
  function controlContext(f, overrides = {}) {
1914
1959
  return {
1915
1960
  runtime: f.runtime,