@jskit-ai/assistant-core 0.1.239 → 0.1.241
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/client/conversation/AssistantTranscript.vue +26 -29
- package/src/client/conversation/LongTextPreviewBlocks.vue +11 -0
- package/src/client/conversation/messageDelivery.js +8 -2
- package/src/client/conversation/useAssistantAttachments.js +1 -1
- package/src/server/conversation/agentRun.js +12 -198
- package/src/server/conversation/attachments.js +6 -5
- package/src/server/conversation/boundedToolLoop.js +160 -0
- package/src/server/conversation/claudeHistory.js +20 -16
- package/src/server/conversation/claudeProcess.js +2 -2
- package/src/server/conversation/claudeTurn.js +51 -40
- package/src/server/conversation/codexAuthPreflight.js +104 -0
- package/src/server/conversation/codexClient.js +11 -16
- package/src/server/conversation/codexConfiguration.js +112 -98
- package/src/server/conversation/codexConversationStore.js +189 -0
- package/src/server/conversation/codexConversations.js +47 -44
- package/src/server/conversation/codexDetachedTurn.js +12 -11
- package/src/server/conversation/codexEvents.js +95 -99
- package/src/server/conversation/codexGoalCommands.js +1 -1
- package/src/server/conversation/codexHelperExecution.js +3 -13
- package/src/server/conversation/codexHelperThreadLedger.js +7 -13
- package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
- package/src/server/conversation/codexHelperThreads.js +5 -6
- package/src/server/conversation/codexHistoryAdapter.js +32 -26
- package/src/server/conversation/codexMessageCommands.js +22 -22
- package/src/server/conversation/codexMessageDelivery.js +12 -7
- package/src/server/conversation/codexModelCatalog.js +97 -0
- package/src/server/conversation/codexNotificationObserver.js +24 -40
- package/src/server/conversation/codexNotificationQueue.js +2 -3
- package/src/server/conversation/codexOutput.js +20 -27
- package/src/server/conversation/codexProcess.js +123 -127
- package/src/server/conversation/codexProcessLeader.js +3 -3
- package/src/server/conversation/codexProvider.js +31 -614
- package/src/server/conversation/codexProviderOwner.js +2 -2
- package/src/server/conversation/codexProviderSelection.js +38 -40
- package/src/server/conversation/codexRenewalCommands.js +13 -10
- package/src/server/conversation/codexRenewalThreads.js +412 -0
- package/src/server/conversation/codexRunJournal.js +24 -42
- package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
- package/src/server/conversation/codexSessionCleanup.js +11 -10
- package/src/server/conversation/codexSessionReconciliation.js +6 -6
- package/src/server/conversation/codexThreadReadiness.js +19 -33
- package/src/server/conversation/codexThreadStatus.js +3 -3
- package/src/server/conversation/codexTurn.js +124 -297
- package/src/server/conversation/codexTurnRecovery.js +49 -28
- package/src/server/conversation/codexTurnState.js +34 -13
- package/src/server/conversation/commandWrapper.js +3 -0
- package/src/server/conversation/deliveryTiming.js +14 -5
- package/src/server/conversation/index.js +1 -0
- package/src/server/conversation/localExecution.js +13 -17
- package/src/server/conversation/normalize.js +8 -3
- package/src/server/conversation/openCodeClient.js +9 -10
- package/src/server/conversation/openCodeProcess.js +9 -5
- package/src/server/conversation/openCodeRuntime.js +10 -11
- package/src/server/conversation/openCodeTurn.js +43 -29
- package/src/server/conversation/providers/claudeDriver.js +10 -16
- package/src/server/conversation/providers/codexDriver.js +27 -43
- package/src/server/conversation/providers/opencodeDriver.js +17 -42
- package/src/server/conversation/providers/scoped.js +48 -4
- package/src/server/conversation/runtime.js +50 -49
- package/src/server/conversation/runtimeStateUpgrade.js +34 -22
- package/src/server/conversation/transcript.js +10 -10
- package/src/server/hosts/codexConfiguration.js +1 -0
- package/src/server/hosts/codexProvider.js +8 -6
- package/src/server/index.js +2 -1
- package/src/server/lib/assistantToolLoop.js +1 -153
- package/src/server/lib/serviceToolCatalog.js +17 -42
- package/src/shared/assistantResource.js +4 -2
- package/src/shared/conversation/attachments.js +4 -0
- package/src/shared/conversation/longTextBlocks.js +54 -19
- package/src/shared/conversation/numberedQuestionSugar.js +17 -10
- package/src/shared/conversation/pagination.js +4 -6
- package/src/shared/conversation/patches.js +1 -4
- package/src/shared/conversation/streaming.js +3 -11
- package/src/shared/conversation/turns.js +11 -0
- package/src/shared/index.js +1 -0
- package/test/assistantBoundedToolLoop.test.js +2 -2
- package/test/assistantResource.test.js +4 -4
- package/test/assistantScroll.browser.test.js +29 -6
- package/test/assistantSettingsResource.test.js +2 -2
- package/test/claudeConversationSession.test.js +20 -3
- package/test/claudeTurn.test.js +27 -7
- package/test/codexClient.test.js +20 -0
- package/test/codexConversationSession.test.js +5 -3
- package/test/codexConversationsCheckpoint.test.js +51 -0
- package/test/codexModelCatalog.test.js +1 -1
- package/test/codexProviderHome.test.js +51 -0
- package/test/codexProviderSelection.test.js +71 -0
- package/test/codexTurn.test.js +222 -6
- package/test/conversationAgentRun.test.js +2 -1
- package/test/conversationAttachments.test.js +27 -2
- package/test/conversationDeliveryTiming.test.js +21 -0
- package/test/conversationNormalize.test.js +11 -0
- package/test/fixtures/nestedListQuestionMessage.md +37 -0
- package/test/frameworkActions.test.js +2 -2
- package/test/longTextBlocks.test.js +102 -0
- package/test/messageDelivery.test.js +7 -1
- package/test/openCodeClient.test.js +20 -0
- package/test/openCodeRuntime.test.js +1 -1
- package/test/openCodeTurn.test.js +90 -0
- package/test/opencodeConversationSession.test.js +1 -1
- package/test/serviceToolCatalog.test.js +27 -0
|
@@ -124,15 +124,26 @@ export function createCodexTurnRecovery({
|
|
|
124
124
|
const delayMs = failureDetail
|
|
125
125
|
? Math.max(0, failureDetail.expiresAt - Date.now())
|
|
126
126
|
: codexAppServerFinalizingRemainingMs({ completedAt, state: "finalizing", updatedAt });
|
|
127
|
-
|
|
127
|
+
// One record per pending deadline: `recovery` is the in-flight expiry
|
|
128
|
+
// (retained-timer policy only), `expiresAt` the first deadline that a later
|
|
129
|
+
// failure-detail grace may not exceed, `failureDetail` a deferred provider
|
|
130
|
+
// failure, and `startedAt` the post-read clock that rereads must keep.
|
|
131
|
+
const entry = {
|
|
132
|
+
timer: null,
|
|
133
|
+
recovery: null,
|
|
134
|
+
expiresAt: failureDetailGraceMs > 0 ? Date.now() + delayMs : null,
|
|
135
|
+
failureDetail,
|
|
136
|
+
startedAt: !failureDetail && finalizingGraceAfterHistoryRead ? completedAt : ""
|
|
137
|
+
};
|
|
138
|
+
entry.timer = setTimeout(() => {
|
|
128
139
|
if (failureDetail) {
|
|
129
140
|
notificationQueue.run({ projectContext, provider: failureDetail.provider, sessionId: normalizedSessionId }, async () => {
|
|
130
|
-
if (codexAppServerFinalizingTimers.get(key) !==
|
|
141
|
+
if (codexAppServerFinalizingTimers.get(key) !== entry) return;
|
|
131
142
|
try {
|
|
132
143
|
return await stopCodexAppServerTurnWithProviderFailure(normalizedSessionId, normalizedThreadId, normalizedTurnId,
|
|
133
144
|
{ ...failureDetail, failureDetailReady: true });
|
|
134
145
|
} finally {
|
|
135
|
-
if (codexAppServerFinalizingTimers.get(key) ===
|
|
146
|
+
if (codexAppServerFinalizingTimers.get(key) === entry) {
|
|
136
147
|
clearCodexAppServerFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
137
148
|
}
|
|
138
149
|
}
|
|
@@ -151,11 +162,11 @@ export function createCodexTurnRecovery({
|
|
|
151
162
|
)
|
|
152
163
|
);
|
|
153
164
|
if (retainFinalizingTimer) {
|
|
154
|
-
|
|
165
|
+
entry.recovery = recovery;
|
|
155
166
|
void recovery.then(
|
|
156
|
-
() => { if (
|
|
167
|
+
() => { if (entry.recovery === recovery) entry.recovery = null; },
|
|
157
168
|
error => {
|
|
158
|
-
if (
|
|
169
|
+
if (entry.recovery === recovery) entry.recovery = null;
|
|
159
170
|
debugLog("appServerFinalizingRecovery.error", {
|
|
160
171
|
error: debugError(error), sessionId: normalizedSessionId,
|
|
161
172
|
threadId: normalizedThreadId, turnId: normalizedTurnId
|
|
@@ -166,11 +177,8 @@ export function createCodexTurnRecovery({
|
|
|
166
177
|
void recovery;
|
|
167
178
|
}
|
|
168
179
|
}, delayMs);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
else if (finalizingGraceAfterHistoryRead) timer.finalizingStartedAt = completedAt;
|
|
172
|
-
timer.unref?.();
|
|
173
|
-
codexAppServerFinalizingTimers.set(key, timer);
|
|
180
|
+
entry.timer.unref?.();
|
|
181
|
+
codexAppServerFinalizingTimers.set(key, entry);
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
async function reconcileCodexAppServerActiveTurn(session = {}, {
|
|
@@ -194,8 +202,8 @@ export function createCodexTurnRecovery({
|
|
|
194
202
|
status: trackedTurn.status || "completed"
|
|
195
203
|
}
|
|
196
204
|
);
|
|
197
|
-
const
|
|
198
|
-
return
|
|
205
|
+
const activeRuntime = runtime || await createRuntime();
|
|
206
|
+
return activeRuntime.getSession(sessionId);
|
|
199
207
|
}
|
|
200
208
|
return session;
|
|
201
209
|
}
|
|
@@ -313,12 +321,13 @@ export function createCodexTurnRecovery({
|
|
|
313
321
|
scheduleCodexAppServerActiveRecovery(sessionId);
|
|
314
322
|
return currentSession;
|
|
315
323
|
}
|
|
316
|
-
|
|
317
|
-
return failOrphanedCodexAppServerPromptDelivery(activeRuntime, currentSession, currentTurn);
|
|
318
|
-
}
|
|
324
|
+
// A local prompt delivery owns the outcome of its own starting turn.
|
|
319
325
|
if (promptDeliveryIsLocal) {
|
|
320
326
|
return currentSession;
|
|
321
327
|
}
|
|
328
|
+
if (!providerTurnId || !codexAppServerTurnStatusIsComplete(status)) {
|
|
329
|
+
return failOrphanedCodexAppServerPromptDelivery(activeRuntime, currentSession, currentTurn);
|
|
330
|
+
}
|
|
322
331
|
await markCodexAppServerProviderTurnActive(sessionId, {
|
|
323
332
|
source: "active_reconciliation",
|
|
324
333
|
status: "inProgress",
|
|
@@ -603,7 +612,7 @@ export function createCodexTurnRecovery({
|
|
|
603
612
|
const normalizedTurnId = normalizeCodexRunText(turnId);
|
|
604
613
|
const key = codexAppServerResultFinalizationKey(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
605
614
|
const priorTimer = retainFinalizingTimer ? codexAppServerFinalizingTimers.get(key) : null;
|
|
606
|
-
if (!timerRecovery && priorTimer?.
|
|
615
|
+
if (!timerRecovery && priorTimer?.recovery) return priorTimer.recovery;
|
|
607
616
|
const runtime = await createRuntime();
|
|
608
617
|
const session = await runtime.getSession(normalizedSessionId);
|
|
609
618
|
const turn = turnState(session);
|
|
@@ -651,9 +660,9 @@ export function createCodexTurnRecovery({
|
|
|
651
660
|
clearCodexAppServerFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
652
661
|
return { ok: true, processed: false, reason: "goal_continuation_pending" };
|
|
653
662
|
}
|
|
654
|
-
if (!timerRecovery && timer?.
|
|
663
|
+
if (!timerRecovery && timer?.recovery) return timer.recovery;
|
|
655
664
|
// This local clock never changes the native completion timestamp.
|
|
656
|
-
graceTurn = { ...currentTurn, completedAt: timer?.
|
|
665
|
+
graceTurn = { ...currentTurn, completedAt: timer?.startedAt || readReturnedAt };
|
|
657
666
|
}
|
|
658
667
|
if (!codexAppServerFinalizingExpired(graceTurn)) {
|
|
659
668
|
scheduleCodexAppServerFinalizingRecovery(normalizedSessionId, normalizedThreadId, normalizedTurnId, {
|
|
@@ -920,9 +929,11 @@ export function createCodexTurnRecovery({
|
|
|
920
929
|
}
|
|
921
930
|
}
|
|
922
931
|
const runtime = await createRuntime();
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
932
|
+
const staleFailure = async () => {
|
|
933
|
+
const turn = turnState(await runtime.getSession(normalizedSessionId));
|
|
934
|
+
if (codexAppServerTurnCanReceiveProviderCompletion(turn, normalizedThreadId, normalizedTurnId)) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
926
937
|
debugLog("appServerTurn.failure.stale", {
|
|
927
938
|
currentState: turn.state,
|
|
928
939
|
currentStatus: turn.status,
|
|
@@ -938,6 +949,10 @@ export function createCodexTurnRecovery({
|
|
|
938
949
|
processed: false,
|
|
939
950
|
reason: "stale_turn_state"
|
|
940
951
|
};
|
|
952
|
+
};
|
|
953
|
+
const staleBeforeRecovery = await staleFailure();
|
|
954
|
+
if (staleBeforeRecovery) {
|
|
955
|
+
return staleBeforeRecovery;
|
|
941
956
|
}
|
|
942
957
|
const key = codexAppServerResultFinalizationKey(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
943
958
|
const pendingTimer = codexAppServerFinalizingTimers.get(key);
|
|
@@ -949,7 +964,7 @@ export function createCodexTurnRecovery({
|
|
|
949
964
|
failureDetail: { error, ok, outcome: resolvedOutcome, provider, status: normalizedStatus,
|
|
950
965
|
usageLimitExceeded, verifyInactive,
|
|
951
966
|
expiresAt: Math.min(observedAt + failureDetailGraceMs,
|
|
952
|
-
pendingTimer?.
|
|
967
|
+
pendingTimer?.expiresAt ?? Infinity) }
|
|
953
968
|
});
|
|
954
969
|
}
|
|
955
970
|
return { ok: true, processed: false, reason: "failure_detail_pending", status: normalizedStatus };
|
|
@@ -972,8 +987,13 @@ export function createCodexTurnRecovery({
|
|
|
972
987
|
return recovered;
|
|
973
988
|
}
|
|
974
989
|
}
|
|
975
|
-
// History recovery awaited the provider. Confirm
|
|
976
|
-
// publishing a failure,
|
|
990
|
+
// History recovery awaited the provider. Confirm ownership and activity
|
|
991
|
+
// again before publishing a failure: a successor, Stop or goal
|
|
992
|
+
// continuation may own the run after that gap.
|
|
993
|
+
const staleAfterRecovery = await staleFailure();
|
|
994
|
+
if (staleAfterRecovery) {
|
|
995
|
+
return staleAfterRecovery;
|
|
996
|
+
}
|
|
977
997
|
if (verifyInactive && await codexAppServerProviderBlocksTurnRelease(normalizedSessionId, provider, normalizedThreadId, normalizedTurnId, {
|
|
978
998
|
source: "provider_failure"
|
|
979
999
|
})) {
|
|
@@ -1018,9 +1038,6 @@ export function createCodexTurnRecovery({
|
|
|
1018
1038
|
const normalizedThreadId = normalizeCodexRunText(threadId);
|
|
1019
1039
|
const normalizedStatus = normalizeCodexRunText(status) || "completed";
|
|
1020
1040
|
const normalizedTurnId = await resolveCodexAppServerTurnId(normalizedSessionId, normalizedThreadId, turnId);
|
|
1021
|
-
const runtime = await createRuntime();
|
|
1022
|
-
const currentSession = await runtime.getSession(normalizedSessionId);
|
|
1023
|
-
const currentTurn = turnState(currentSession);
|
|
1024
1041
|
const recovered = await recoverCodexAppServerFinalResponseBeforeOutcome(
|
|
1025
1042
|
normalizedSessionId,
|
|
1026
1043
|
normalizedThreadId,
|
|
@@ -1030,6 +1047,10 @@ export function createCodexTurnRecovery({
|
|
|
1030
1047
|
if (recovered?.processed) {
|
|
1031
1048
|
return recovered;
|
|
1032
1049
|
}
|
|
1050
|
+
// The history read can outlive this turn: a successor or Stop may own the
|
|
1051
|
+
// run by now, so the outcome is decided against the run read after it.
|
|
1052
|
+
const runtime = await createRuntime();
|
|
1053
|
+
const currentTurn = turnState(await runtime.getSession(normalizedSessionId));
|
|
1033
1054
|
if (!codexAppServerTurnCanReceiveProviderCompletion(currentTurn, normalizedThreadId, normalizedTurnId)) {
|
|
1034
1055
|
codexAppServerFinalizedTurns.add(codexAppServerResultFinalizationKey(
|
|
1035
1056
|
normalizedSessionId,
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
2
3
|
import { codexAppServerStatusFromValue, codexAppServerThreadRawValue } from "./codexEvents.js";
|
|
3
4
|
|
|
4
5
|
function codexAppServerTurnStatusIsActive(status = "") {
|
|
5
|
-
return
|
|
6
|
+
return normalizeNullableText(status) === "inProgress";
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
function codexAppServerTurnStatusIsComplete(status = "") {
|
|
9
|
-
return ["completed", "interrupted", "failed"].includes(
|
|
10
|
+
return ["completed", "interrupted", "failed"].includes(normalizeNullableText(status));
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function codexAppServerTurnStatusIsSuccessfulComplete(status = "") {
|
|
13
|
-
return
|
|
14
|
+
return normalizeNullableText(status) === "completed";
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function codexAppServerTurnStatusIsProviderFailure(status = "") {
|
|
17
|
-
return ["failed", "interrupted"].includes(
|
|
18
|
+
return ["failed", "interrupted"].includes(normalizeNullableText(status));
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function codexAppServerThreadStatus(thread = {}) {
|
|
@@ -26,7 +27,7 @@ function codexAppServerThreadStatus(thread = {}) {
|
|
|
26
27
|
if (codexAppServerTurnStatusIsActive(liveStatus)) {
|
|
27
28
|
return liveStatus;
|
|
28
29
|
}
|
|
29
|
-
if (
|
|
30
|
+
if (isRecord(thread.observedTurn)) {
|
|
30
31
|
const observedStatus = codexAppServerStatusFromValue(thread.observedTurn.status);
|
|
31
32
|
if (observedStatus) {
|
|
32
33
|
return observedStatus;
|
|
@@ -38,7 +39,7 @@ function codexAppServerThreadStatus(thread = {}) {
|
|
|
38
39
|
async function codexAppServerReadThreadStatus(provider = null, threadId = "", {
|
|
39
40
|
observeLatestTurn = false
|
|
40
41
|
} = {}) {
|
|
41
|
-
const normalizedThreadId =
|
|
42
|
+
const normalizedThreadId = normalizeNullableText(threadId || "");
|
|
42
43
|
if (!normalizedThreadId) {
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
@@ -54,7 +55,7 @@ async function codexAppServerReadThreadStatus(provider = null, threadId = "", {
|
|
|
54
55
|
});
|
|
55
56
|
const observedTurn = Array.isArray(response?.data) ? response.data[0] : null;
|
|
56
57
|
if (provider.isControlProbeTurn?.(normalizedThreadId, observedTurn?.id)) return thread;
|
|
57
|
-
return
|
|
58
|
+
return isRecord(observedTurn)
|
|
58
59
|
? {
|
|
59
60
|
...thread,
|
|
60
61
|
observedTurn
|
|
@@ -199,7 +200,9 @@ export function codexAppServerTurnState(session = {}, normalizeRunState = normal
|
|
|
199
200
|
error: "",
|
|
200
201
|
goalStatus: "",
|
|
201
202
|
goalThreadId: "",
|
|
203
|
+
inputSource: "",
|
|
202
204
|
outerTurnId: "",
|
|
205
|
+
phase: "",
|
|
203
206
|
runId: "",
|
|
204
207
|
runState: "",
|
|
205
208
|
startedAt: "",
|
|
@@ -298,6 +301,12 @@ export function codexAppServerTurnKey(threadId = "", turnId = "") {
|
|
|
298
301
|
return `${normalizeCodexRunText(threadId)}:${normalizeCodexRunText(turnId)}`;
|
|
299
302
|
}
|
|
300
303
|
|
|
304
|
+
// Identity of a native outer turn. Terminal turns and persistent-conversation
|
|
305
|
+
// checkpoints must agree on it, so both build it here.
|
|
306
|
+
export function codexAppServerNativeOuterTurnId(threadId = "", turnId = "") {
|
|
307
|
+
return `codex:${threadId}:${turnId}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
301
310
|
export function codexAppServerAgentRunPatch({
|
|
302
311
|
error = "",
|
|
303
312
|
inputSource = "",
|
|
@@ -336,7 +345,7 @@ export function codexAppServerAgentRunPatch({
|
|
|
336
345
|
// owner's id would checkpoint two different turns with one identity.
|
|
337
346
|
const normalizedOuterTurnId = normalizeCodexRunText(outerTurnId) ||
|
|
338
347
|
(normalizedInputSource === "terminal" && normalizedRunState === CODEX_APP_SERVER_RUN_STATE.ACTIVE && !sameActiveTurn
|
|
339
|
-
?
|
|
348
|
+
? codexAppServerNativeOuterTurnId(normalizeCodexRunText(threadId), normalizeCodexRunText(turnId))
|
|
340
349
|
: normalizeCodexRunText(currentRun?.outerTurnId));
|
|
341
350
|
if (normalizedOuterTurnId) {
|
|
342
351
|
patch.outerTurnId = normalizedOuterTurnId;
|
|
@@ -387,6 +396,18 @@ export function codexAppServerRunIdentityForPatch(session = {}, {
|
|
|
387
396
|
};
|
|
388
397
|
}
|
|
389
398
|
|
|
399
|
+
// A settling writer names the turn it settles. While another turn owns the
|
|
400
|
+
// active run, that write belongs to a predecessor and must not end the run.
|
|
401
|
+
export function codexAppServerRunPatchSettlesOtherActiveTurn(currentTurn = {}, patch = {}) {
|
|
402
|
+
const currentTurnId = normalizeCodexRunText(currentTurn.turnId);
|
|
403
|
+
const patchTurnId = normalizeCodexRunText(patch.providerTurnId);
|
|
404
|
+
return currentTurn.active === true &&
|
|
405
|
+
!codexAppServerRunStateIsActive(patch.state) &&
|
|
406
|
+
Boolean(currentTurnId) &&
|
|
407
|
+
Boolean(patchTurnId) &&
|
|
408
|
+
currentTurnId !== patchTurnId;
|
|
409
|
+
}
|
|
410
|
+
|
|
390
411
|
export function codexAppServerRunPatchIsStaleAfterTerminalState(currentTurn = {}, patch = {}, normalizeRunState = normalizeCodexAppServerRunState) {
|
|
391
412
|
const currentRunState = normalizeRunState(currentTurn.runState);
|
|
392
413
|
const patchRunState = normalizeRunState(patch.state);
|
|
@@ -413,7 +434,7 @@ function codexAppServerErrorMessage(value, fallback = "Codex could not be prepar
|
|
|
413
434
|
}
|
|
414
435
|
|
|
415
436
|
export function codexAppServerSteerFailure(result = {}, steerFailedCode = "codex_turn_steer_failed") {
|
|
416
|
-
if (!
|
|
437
|
+
if (!isRecord(result)) {
|
|
417
438
|
return null;
|
|
418
439
|
}
|
|
419
440
|
if (result.ok !== false) {
|
|
@@ -464,7 +485,7 @@ function codexAppServerMessageDeferred({
|
|
|
464
485
|
}
|
|
465
486
|
|
|
466
487
|
export function codexAppServerInterruptFailure(result = {}, interruptFailedCode = "codex_turn_interrupt_failed") {
|
|
467
|
-
if (!
|
|
488
|
+
if (!isRecord(result)) {
|
|
468
489
|
return null;
|
|
469
490
|
}
|
|
470
491
|
if (result.ok !== false && result.interrupted !== false) {
|
|
@@ -522,14 +543,14 @@ export function codexAppServerMessageText(input = {}) {
|
|
|
522
543
|
if (typeof input === "string") {
|
|
523
544
|
return normalizeCodexRunText(input);
|
|
524
545
|
}
|
|
525
|
-
if (!
|
|
546
|
+
if (!isRecord(input)) {
|
|
526
547
|
return "";
|
|
527
548
|
}
|
|
528
549
|
return normalizeCodexRunText(input.message);
|
|
529
550
|
}
|
|
530
551
|
|
|
531
552
|
export function codexAppServerMessageDisplayText(input = {}, fallback = "") {
|
|
532
|
-
if (!
|
|
553
|
+
if (!isRecord(input)) {
|
|
533
554
|
return normalizeCodexRunText(fallback || input);
|
|
534
555
|
}
|
|
535
556
|
return normalizeCodexRunText(input.displayMessage || input.message || fallback);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
|
+
// Always single-quotes, unlike codexProvider's shellQuote, which leaves plain
|
|
5
|
+
// tokens bare. Wrapped hook commands keep their exact quoted form, which
|
|
6
|
+
// conversationCommandWrapper.test.js asserts, so the two are not merged.
|
|
4
7
|
const shellQuote = value => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
5
8
|
|
|
6
9
|
export function validateCommandWrapper(wrapper) {
|
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
// Finer marks within each phase locate slow work. Hosts
|
|
2
|
-
//
|
|
1
|
+
// Finer marks within each phase locate slow work. Hosts declare any marks of
|
|
2
|
+
// their own preparation through hostStages.
|
|
3
3
|
const stages = new Set([
|
|
4
4
|
"received", "preparation-started", "authorized", "history-read", "state-read", "request-saved",
|
|
5
5
|
"input-prepared", "provider-started", "duplicate-checked", "native-history-read", "process-ready",
|
|
6
|
-
"
|
|
6
|
+
"prompt-rendered", "metadata-ready", "runtime-ready", "dispatch-started",
|
|
7
7
|
"native-acknowledged", "receipt-written", "state-active", "receipt-persisted", "admission-authorized",
|
|
8
8
|
"turn-located", "accepted", "first-response", "failed"
|
|
9
9
|
]);
|
|
10
|
+
const hostStagePattern = /^[a-z][a-z-]{0,63}$/u;
|
|
11
|
+
|
|
12
|
+
export function normalizeConversationDeliveryHostStages(hostStages = []) {
|
|
13
|
+
if (!Array.isArray(hostStages) || hostStages.some(stage => typeof stage !== "string" || !hostStagePattern.test(stage))) {
|
|
14
|
+
throw new TypeError("Host delivery stages must be lowercase hyphenated names.");
|
|
15
|
+
}
|
|
16
|
+
return Object.freeze([...new Set(hostStages)]);
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
// Observer results are never awaited and failures cannot change delivery.
|
|
12
|
-
export function createConversationDeliveryTiming({ engine, conversationId, messageId, report,
|
|
20
|
+
export function createConversationDeliveryTiming({ engine, conversationId, messageId, report, hostStages = [],
|
|
21
|
+
clock = () => performance.now() }) {
|
|
13
22
|
const startedAt = clock();
|
|
14
23
|
let previousAt = startedAt;
|
|
15
24
|
let previousStage = "";
|
|
16
25
|
const timings = {};
|
|
17
26
|
return stage => {
|
|
18
|
-
if (!report || !stages.has(stage) || Object.hasOwn(timings, stage)) return;
|
|
27
|
+
if (!report || !(stages.has(stage) || hostStages.includes(stage)) || Object.hasOwn(timings, stage)) return;
|
|
19
28
|
const at = clock();
|
|
20
29
|
timings[stage] = Math.max(0, at - startedAt);
|
|
21
30
|
const event = { engine, conversationId, messageId, stage, previousStage,
|
|
@@ -14,3 +14,4 @@ export { createConversationHookBridge } from "./hookBridge.js";
|
|
|
14
14
|
export { nativeConversationStoragePolicy } from "./nativeStorage.js";
|
|
15
15
|
export { createConversationProcessIdentity } from "./localExecution.js";
|
|
16
16
|
export { validateConversationOutputSchema } from "./structuredOutput.js";
|
|
17
|
+
export { conversationAttachmentIds } from "./attachments.js";
|
|
@@ -2,7 +2,8 @@ import { execFile, spawn } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { closeSync, openSync } from "node:fs";
|
|
4
4
|
import { mkdtemp, open, readFile, readdir, rm } from "node:fs/promises";
|
|
5
|
-
import {
|
|
5
|
+
import { isRecord, normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
6
7
|
import { isAbsolute, join } from "node:path";
|
|
7
8
|
import { tmpdir } from "node:os";
|
|
8
9
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -197,11 +198,6 @@ export function createConversationProcessIdentity({
|
|
|
197
198
|
ABSENT: "absent", AMBIGUOUS: "ambiguous", EXACT: "exact", INVALID: "invalid", MISMATCH: "mismatch"
|
|
198
199
|
});
|
|
199
200
|
|
|
200
|
-
function normalizePositiveInteger(value, fallback) {
|
|
201
|
-
const number = Number(value);
|
|
202
|
-
return Number.isSafeInteger(number) && number > 0 ? number : fallback;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
201
|
function processMetadataIsIdentifiable(metadata = {}) {
|
|
206
202
|
return Boolean(
|
|
207
203
|
Number.isSafeInteger(Number(metadata.pid)) && Number(metadata.pid) > 0 &&
|
|
@@ -283,12 +279,12 @@ export function createConversationProcessIdentity({
|
|
|
283
279
|
}
|
|
284
280
|
|
|
285
281
|
function normalizeProcessIdentity(value = {}) {
|
|
286
|
-
const normalized =
|
|
282
|
+
const normalized = isRecord(value) ? value : {};
|
|
287
283
|
return {
|
|
288
|
-
commandHash:
|
|
289
|
-
platform:
|
|
290
|
-
runtimeToken:
|
|
291
|
-
startTimeTicks:
|
|
284
|
+
commandHash: normalizeNullableText(normalized.commandHash),
|
|
285
|
+
platform: normalizeNullableText(normalized.platform),
|
|
286
|
+
runtimeToken: normalizeNullableText(normalized.runtimeToken),
|
|
287
|
+
startTimeTicks: normalizeNullableText(normalized.startTimeTicks),
|
|
292
288
|
version: Number(normalized.version || 0)
|
|
293
289
|
};
|
|
294
290
|
}
|
|
@@ -535,11 +531,11 @@ export function createConversationProcessIdentity({
|
|
|
535
531
|
async function waitForProcessIdentityToSettle(metadata = {}, options = {}) {
|
|
536
532
|
const timeoutMs = normalizePositiveInteger(
|
|
537
533
|
options.processIdentitySettleTimeoutMs,
|
|
538
|
-
PROCESS_IDENTITY_SETTLE_TIMEOUT_MS
|
|
534
|
+
{ fallback: PROCESS_IDENTITY_SETTLE_TIMEOUT_MS }
|
|
539
535
|
);
|
|
540
536
|
const pollMs = normalizePositiveInteger(
|
|
541
537
|
options.processIdentitySettlePollMs,
|
|
542
|
-
PROCESS_IDENTITY_SETTLE_POLL_MS
|
|
538
|
+
{ fallback: PROCESS_IDENTITY_SETTLE_POLL_MS }
|
|
543
539
|
);
|
|
544
540
|
const deadline = Date.now() + timeoutMs;
|
|
545
541
|
let inspection = await inspectProcessIdentity(metadata, options);
|
|
@@ -648,7 +644,7 @@ export function createConversationProcessIdentity({
|
|
|
648
644
|
let after = await waitForProcessIdentityExit(
|
|
649
645
|
metadata,
|
|
650
646
|
options,
|
|
651
|
-
normalizePositiveInteger(options.termTimeoutMs, 3000)
|
|
647
|
+
normalizePositiveInteger(options.termTimeoutMs, { fallback: 3000 })
|
|
652
648
|
);
|
|
653
649
|
let signaledProcessGroups = [...term.signaledProcessGroups];
|
|
654
650
|
if (after.status === PROCESS_IDENTITY_STATUS.EXACT) {
|
|
@@ -668,7 +664,7 @@ export function createConversationProcessIdentity({
|
|
|
668
664
|
after = await waitForProcessIdentityExit(
|
|
669
665
|
metadata,
|
|
670
666
|
options,
|
|
671
|
-
normalizePositiveInteger(options.killTimeoutMs, 1000)
|
|
667
|
+
normalizePositiveInteger(options.killTimeoutMs, { fallback: 1000 })
|
|
672
668
|
);
|
|
673
669
|
}
|
|
674
670
|
const processExitVerified = [
|
|
@@ -702,8 +698,8 @@ export function createConversationProcessIdentity({
|
|
|
702
698
|
reportedIdentity = null,
|
|
703
699
|
runtimeToken = ""
|
|
704
700
|
} = {}) {
|
|
705
|
-
const expectedCommandHash =
|
|
706
|
-
const expectedRuntimeToken =
|
|
701
|
+
const expectedCommandHash = normalizeNullableText(commandHash);
|
|
702
|
+
const expectedRuntimeToken = normalizeNullableText(runtimeToken);
|
|
707
703
|
const reported = normalizeProcessIdentity(reportedIdentity);
|
|
708
704
|
if (
|
|
709
705
|
processIdentityIsWellFormed(reported) &&
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
// Unlike the kernel's normalizeText, only null and undefined become blank:
|
|
4
|
+
// 0 and false keep their text, as native protocol fields require.
|
|
5
|
+
export function normalizeNullableText(value) {
|
|
2
6
|
return String(value ?? "").trim();
|
|
3
7
|
}
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/** SHA-256 of the value's JSON text, used for stable configuration identities. */
|
|
10
|
+
export function jsonHash(value) {
|
|
11
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
7
12
|
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
4
6
|
import { createNativeHistoryExport, readStoppedNativeDatabase, retireNativeConversation } from "./nativeHistoryExport.js";
|
|
5
7
|
|
|
6
8
|
const OPENCODE_RESPONSE_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
7
9
|
const OPENCODE_CATALOG_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
8
10
|
const OPENCODE_EVENT_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function record(value = null) {
|
|
15
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
16
|
-
}
|
|
12
|
+
const text = normalizeNullableText;
|
|
13
|
+
const record = value => isRecord(value) ? value : {};
|
|
17
14
|
|
|
18
15
|
function openCodeServerError(message, {
|
|
19
16
|
body = null,
|
|
@@ -594,12 +591,14 @@ function createOpenCodeServerClient({
|
|
|
594
591
|
return statuses?.[text(sessionId)] || { type: "idle" };
|
|
595
592
|
},
|
|
596
593
|
async messages(sessionId = "", input = {}, { signal } = {}) {
|
|
597
|
-
|
|
594
|
+
// A limited read is the newest page; `before` continues with older rows.
|
|
595
|
+
const { source, headers } = await requestResponse(
|
|
598
596
|
"GET",
|
|
599
|
-
`${stableSessionPath(sessionId, "/message")}${queryString({ limit: input.limit })}`,
|
|
597
|
+
`${stableSessionPath(sessionId, "/message")}${queryString({ limit: input.limit, before: input.before })}`,
|
|
600
598
|
{ signal }
|
|
601
599
|
);
|
|
602
|
-
|
|
600
|
+
const nextCursor = headers.get("x-next-cursor");
|
|
601
|
+
return { data: normalizedMessageRows(parsedJson(source)), ...(nextCursor ? { nextCursor } : {}) };
|
|
603
602
|
},
|
|
604
603
|
async prompt(sessionId = "", input = {}, { signal } = {}) {
|
|
605
604
|
const body = stablePromptBody(input);
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
6
6
|
import { createOpenCodeServerClient, readBoundedResponse } from "./openCodeClient.js";
|
|
7
7
|
import { createLocalConversationExecution } from "./localExecution.js";
|
|
8
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
8
9
|
import { DISCOVERY_TOOL_DESCRIPTORS } from "../lib/serviceToolCatalog.js";
|
|
9
10
|
export { createOpenCodeSharedRuntime, openCodeServerForDirectory, readOpenCodeEnvironments, openCodeEnvironmentForDirectory,
|
|
10
11
|
openCodeEnvironmentForSession, assertOpenCodeModelProvider, limitOpenCodeModelOutput,
|
|
@@ -25,9 +26,7 @@ const OPENCODE_ZEN_MODELS_URL = "https://opencode.ai/zen/v1/models";
|
|
|
25
26
|
const OPENCODE_ZEN_PROVIDER_ID = "opencode";
|
|
26
27
|
const OPENCODE_ZEN_PUBLIC_API_KEY = "public";
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
return String(value ?? "").trim();
|
|
30
|
-
}
|
|
29
|
+
const text = normalizeNullableText;
|
|
31
30
|
|
|
32
31
|
export function openCodeProcessEnvironment(baseEnv = {}, {
|
|
33
32
|
cacheRoot = "", dbPath = "", inlineConfig = "", outputTokenMax = 0, password = ""
|
|
@@ -348,6 +347,11 @@ export async function readOpenCodeZenModelIds({
|
|
|
348
347
|
}
|
|
349
348
|
}
|
|
350
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Read OpenCode's provider and agent catalogue through a temporary server.
|
|
352
|
+
* `createServerProcess({ dbPath, privateRoot, providerConnections, workdir })`
|
|
353
|
+
* starts the host's server; it must resolve to `{ client, stop }`.
|
|
354
|
+
*/
|
|
351
355
|
export async function readOpenCodeCatalog({
|
|
352
356
|
createServerProcess,
|
|
353
357
|
privateRoot = "",
|
|
@@ -380,8 +384,8 @@ export async function readOpenCodeCatalog({
|
|
|
380
384
|
let readError;
|
|
381
385
|
try {
|
|
382
386
|
const [providers, agents] = await Promise.all([
|
|
383
|
-
server.client.providers({ directory:
|
|
384
|
-
server.client.agents({ directory:
|
|
387
|
+
server.client.providers({ directory: normalizedWorkdir }),
|
|
388
|
+
server.client.agents({ directory: normalizedWorkdir })
|
|
385
389
|
]);
|
|
386
390
|
catalog = { agents, providers };
|
|
387
391
|
} catch (error) {
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OPENCODE_INTERRUPT_TIMEOUT_MS, waitForOpenCodeFinalResponse, openCodeLastAssistantResult, inspectOpenCodeMessageAdmission, observeOpenCodeEvents,
|
|
3
|
-
openCodeMessageRows, openCodeRowsForInput, openCodePersistentConversationMessages, openCodeStructuredOutput, projectOpenCodeConversationMessages, waitForOpenCodeMessages, steerOpenCodeTurn, dispatchOpenCodeTurn, observeOpenCodeTurnCompletion, runOpenCodeConversationTurn
|
|
3
|
+
openCodeMessageRows, openCodeRowsForInput, readOpenCodeMessagesUntil, openCodePersistentConversationMessages, openCodeStructuredOutput, projectOpenCodeConversationMessages, waitForOpenCodeMessages, steerOpenCodeTurn, dispatchOpenCodeTurn, observeOpenCodeTurnCompletion, runOpenCodeConversationTurn
|
|
4
4
|
} from "./openCodeTurn.js";
|
|
5
5
|
import { openCodeAssistantMessageText as assistantMessageText } from "./openCodeClient.js";
|
|
6
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
6
8
|
import { randomUUID } from "node:crypto";
|
|
7
9
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
8
10
|
import path from "node:path";
|
|
9
11
|
|
|
10
|
-
const text =
|
|
12
|
+
const text = normalizeNullableText;
|
|
11
13
|
const sharedRuntimes = new Map();
|
|
12
14
|
|
|
13
15
|
function openCodeTurnSnapshot(turn = null, threadId = "") {
|
|
14
|
-
const source =
|
|
16
|
+
const source = isRecord(turn) ? turn : {};
|
|
15
17
|
const active = source.active === true;
|
|
16
18
|
return active || text(source.id)
|
|
17
19
|
? {
|
|
@@ -466,7 +468,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
466
468
|
const targets = [...processes.values()];
|
|
467
469
|
const providerId = text(input.modelProviderId);
|
|
468
470
|
if (!providerId || targets.some(target => text(target.modelProviderId) !== providerId || !application.ownsTarget(target))) {
|
|
469
|
-
throw Object.assign(new Error("Close other OpenCode
|
|
471
|
+
throw Object.assign(new Error("Close the other OpenCode conversations that use this connection, then retry this connection change. The saved connection has not changed."), {
|
|
470
472
|
code: "assistant_opencode_connection_in_use", statusCode: 409
|
|
471
473
|
});
|
|
472
474
|
}
|
|
@@ -734,11 +736,8 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
734
736
|
}
|
|
735
737
|
|
|
736
738
|
async function readConversation(target = {}, conversationId = "", tracked = null, options = {}, readError) {
|
|
737
|
-
const messages = await target.server.client
|
|
738
|
-
|
|
739
|
-
order: "desc"
|
|
740
|
-
}, options);
|
|
741
|
-
options.signal?.throwIfAborted();
|
|
739
|
+
const messages = await readOpenCodeMessagesUntil(target.server.client, conversationId,
|
|
740
|
+
rows => rows.some(row => row?.type === "assistant"), options);
|
|
742
741
|
const result = openCodeLastAssistantResult(messages, { readError });
|
|
743
742
|
return {
|
|
744
743
|
conversationId,
|
|
@@ -829,7 +828,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
829
828
|
: options.observe(observedTarget, turnAbort, signal);
|
|
830
829
|
let admitted;
|
|
831
830
|
try {
|
|
832
|
-
admitted = await dispatchOpenCodeTurn(
|
|
831
|
+
admitted = await dispatchOpenCodeTurn(tracked, {
|
|
833
832
|
get agent() { return input.agent; },
|
|
834
833
|
id: inputMessageId,
|
|
835
834
|
get model() { return input.model; },
|
|
@@ -1160,7 +1159,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
1160
1159
|
}, application.monitor);
|
|
1161
1160
|
dispatchTurn = turns.get(key);
|
|
1162
1161
|
}
|
|
1163
|
-
admitted = await dispatchOpenCodeTurn(
|
|
1162
|
+
admitted = await dispatchOpenCodeTurn(dispatchTurn, {
|
|
1164
1163
|
get agent() { return prompt.agent; },
|
|
1165
1164
|
delivery: currentMonitor ? "steer" : "queue",
|
|
1166
1165
|
id: providerMessageId,
|