@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.
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- package/src/server/conversation/codexNotificationObserver.js +3 -1
- package/src/server/conversation/codexPromptDispatch.js +5 -1
- package/src/server/conversation/codexRunJournal.js +3 -0
- package/src/server/conversation/codexThreadStatus.js +22 -1
- package/src/server/conversation/codexTurnRecovery.js +12 -1
- package/test/codexTurn.test.js +45 -0
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.2.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
13
|
-
"@jskit-ai/kernel": "0.2.
|
|
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.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.2.
|
|
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.
|
|
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.
|
|
55
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
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.
|
|
65
|
-
"@jskit-ai/kernel": "0.2.
|
|
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,
|
package/test/codexTurn.test.js
CHANGED
|
@@ -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,
|