@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.
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
- package/package.json +5 -5
- package/src/client/conversation/AssistantConversationElement.vue +5 -1
- package/src/client/conversation/AssistantTranscript.vue +10 -1
- package/src/server/conversation/agentRun.js +38 -11
- package/src/server/conversation/claudeHistory.js +40 -5
- package/src/server/conversation/claudeProcess.js +16 -2
- package/src/server/conversation/claudeTurn.js +28 -8
- package/src/server/conversation/codexConfiguration.js +1 -1
- package/src/server/conversation/codexHistoryAdapter.js +11 -4
- package/src/server/conversation/codexNotificationQueue.js +11 -2
- package/src/server/conversation/codexObservationControl.js +8 -8
- package/src/server/conversation/codexOutput.js +20 -15
- package/src/server/conversation/codexProvider.js +40 -3
- package/src/server/conversation/codexProviderOwner.js +110 -1
- package/src/server/conversation/codexTurn.js +3 -0
- package/src/server/conversation/continuity.js +24 -6
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/nativeHistoryExport.js +97 -0
- package/src/server/conversation/openCodeClient.js +47 -1
- package/src/server/conversation/openCodeRuntime.js +132 -24
- package/src/server/conversation/openCodeTurn.js +65 -24
- package/src/server/conversation/providers/api.js +7 -0
- package/src/server/conversation/providers/claudeDriver.js +52 -7
- package/src/server/conversation/providers/codex.js +19 -2
- package/src/server/conversation/providers/codexDriver.js +92 -14
- package/src/server/conversation/providers/opencodeDriver.js +31 -9
- package/src/server/conversation/runtime.js +418 -64
- package/src/server/conversation/runtimeStateUpgrade.js +77 -2
- package/src/server/conversation/streams.js +25 -5
- package/src/server/conversation/tools.js +2 -2
- package/src/server/conversation/transcript.js +23 -8
- package/src/server/hosts/claudeHistory.js +1 -0
- package/src/server/hosts/claudeProcess.js +1 -0
- package/src/server/hosts/codexProvider.js +3 -0
- package/src/server/hosts/nativeHistory.js +1 -0
- package/src/server/hosts/openCodeClient.js +1 -0
- package/src/server/hosts/openCodeTurn.js +1 -0
- package/src/server/index.js +1 -1
- package/src/server/lib/assistantToolLoop.js +99 -2
- package/test/aiConnectionClient.test.js +57 -0
- package/test/assistantBoundedToolLoop.test.js +332 -0
- package/test/assistantScroll.browser.test.js +151 -0
- package/test/claudeConversationSession.test.js +1329 -3
- package/test/claudeProcess.test.js +101 -0
- package/test/claudeTurn.test.js +31 -0
- package/test/codexConversationSession.test.js +1257 -12
- package/test/codexHistoryAdapter.test.js +74 -1
- package/test/codexTurn.test.js +282 -1
- package/test/conversation.test.js +83 -0
- package/test/conversationAgentRun.test.js +181 -0
- package/test/conversationRuntime.test.js +95 -0
- package/test/conversationRuntimeStateUpgrade.test.js +89 -0
- package/test/conversationStreaming.test.js +85 -0
- package/test/conversationTools.test.js +112 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +78 -0
- package/test/opencodeConversationSession.test.js +637 -5
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.214",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.234",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.236",
|
|
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.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.53",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.53"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -16,6 +16,8 @@ const previewCanTake = ref(true);
|
|
|
16
16
|
const previewDelivery = createAssistantMessageDelivery();
|
|
17
17
|
const avatarFixture = new URLSearchParams(location.search).has("avatar");
|
|
18
18
|
const avatarToolsFixture = new URLSearchParams(location.search).has("avatarTools");
|
|
19
|
+
const updateErrorFixture = new URLSearchParams(location.search).has("update-errors");
|
|
20
|
+
const errorReloads = ref(0);
|
|
19
21
|
const showAvatar = ref(avatarFixture);
|
|
20
22
|
const avatarSize = ref("compact");
|
|
21
23
|
const short = ref(avatarFixture);
|
|
@@ -136,7 +138,8 @@ const adapter = reactive({
|
|
|
136
138
|
assistant: { messageId: `answer-${index}`, outputId: `output-${index}`, role: "assistant",
|
|
137
139
|
text: `Support answer ${index + 1}: Keep your recovery email available.\n\nRead the **account instructions** and follow the [recovery link](https://example.com/help). A long message wraps inside this narrow conversation without taking space from the composer.` }
|
|
138
140
|
})) : [],
|
|
139
|
-
visible: true, reloadable: avatarFixture,
|
|
141
|
+
visible: true, reloadable: avatarFixture && !updateErrorFixture, error: "", errorReloadable: updateErrorFixture,
|
|
142
|
+
scrollKey: "fixture", welcomeMessage: "Composer responsiveness fixture"
|
|
140
143
|
},
|
|
141
144
|
composer: {
|
|
142
145
|
draft,
|
|
@@ -153,6 +156,7 @@ const adapter = reactive({
|
|
|
153
156
|
models: capabilitiesEnabled ? models : undefined,
|
|
154
157
|
questions: computed(() => showQuestions.value ? questionState : undefined),
|
|
155
158
|
actions: {
|
|
159
|
+
reload() { errorReloads.value += 1; adapter.conversation.error = ""; },
|
|
156
160
|
setDraft(value) { draft.value = value; },
|
|
157
161
|
submit(payload) { attachmentReceipts.value = payload.attachments; attachments.clearAttachments(); submissions.value += 1; draft.value = ""; phase.value = "active"; },
|
|
158
162
|
stop() { phase.value = "stopping"; }
|
|
@@ -166,6 +170,8 @@ const adapter = reactive({
|
|
|
166
170
|
<button v-if="attribution" @click="assistantLabel = 'Next assistant'">Change assistant</button>
|
|
167
171
|
<button v-for="state in ['idle', 'active', 'reconnecting', 'stopping', 'stopped']" :key="state" @click="phase = state">External {{ state }}</button>
|
|
168
172
|
<button @click="narrow = !narrow">Resize pane</button>
|
|
173
|
+
<button v-if="updateErrorFixture" @click="adapter.conversation.error = 'Conversation updates are temporarily unavailable. Reload to reconnect; the assistant may still be working.'">Show update error</button>
|
|
174
|
+
<output v-if="updateErrorFixture">Error reloads: {{ errorReloads }}</output>
|
|
169
175
|
<button @click="feedback = !feedback">Toggle action feedback</button>
|
|
170
176
|
<button v-if="supportEnabled" @click="suggestionModel = 'other-model'">Change suggestion model</button>
|
|
171
177
|
<button v-if="supportEnabled" @click="customActivity = !customActivity">Custom activity</button>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.214",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
48
48
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
49
49
|
"@ai-sdk/xai": "^4.0.58",
|
|
50
|
-
"@jskit-ai/resource-core": "0.1.
|
|
51
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
50
|
+
"@jskit-ai/resource-core": "0.1.178",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.178",
|
|
52
52
|
"@mdi/js": "^7.4.47",
|
|
53
53
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
54
54
|
"ai": "^7.0.101",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"ws": "^8.20.1"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
61
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/http-runtime": "0.1.234",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.236",
|
|
62
62
|
"vue": "^3.5.13",
|
|
63
63
|
"vuetify": "^4.0.0"
|
|
64
64
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section ref="container" class="assistant-conversation" :aria-label="label">
|
|
2
|
+
<section ref="container" class="assistant-conversation" :class="{ 'assistant-conversation--error': conversation.error }" :aria-label="label">
|
|
3
3
|
<section
|
|
4
4
|
v-if="$slots.avatar" class="assistant-conversation__avatar"
|
|
5
5
|
:class="{ 'assistant-conversation__avatar--hidden': avatarSize === 'hidden' }" aria-label="Conversation avatar"
|
|
@@ -389,7 +389,10 @@ defineExpose({ focus: () => input.value?.focus(), submit, stop });
|
|
|
389
389
|
<style scoped>
|
|
390
390
|
.assistant-conversation__preview-actions { display: flex; justify-content: flex-end; }
|
|
391
391
|
.assistant-conversation { position: relative; display: flex; flex-direction: column; min-height: 0; min-width: 0; height: 100%; gap: 0; container: assistant-conversation / inline-size; }
|
|
392
|
+
.assistant-conversation--error { overflow-y: auto; }
|
|
392
393
|
.assistant-conversation__avatar { position: absolute; top: 0; right: 0; z-index: 3; max-width: 100%; min-width: 0; }
|
|
394
|
+
.assistant-conversation--error .assistant-conversation__avatar { position: relative; align-self: flex-end; flex: 0 0 auto; }
|
|
395
|
+
.assistant-conversation--error .assistant-conversation__avatar-controls { position: static; }
|
|
393
396
|
.assistant-conversation__avatar-controls { position: absolute; top: 0; right: 100%; display: flex; align-items: center; justify-content: flex-end; min-height: 44px; }
|
|
394
397
|
.assistant-conversation__avatar--hidden .assistant-conversation__avatar-controls { right: 0; }
|
|
395
398
|
.assistant-conversation__avatar-size { min-width: 44px; min-height: 44px; }
|
|
@@ -401,6 +404,7 @@ defineExpose({ focus: () => input.value?.focus(), submit, stop });
|
|
|
401
404
|
.assistant-conversation__avatar-visual :deep(> *) { max-width: 100%; max-height: 100%; }
|
|
402
405
|
.assistant-conversation__goal { display: flex; justify-content: flex-end; flex: 0 0 auto; }
|
|
403
406
|
.assistant-conversation__transcript { flex: 1 1 auto; min-height: 0; }
|
|
407
|
+
.assistant-conversation--error .assistant-conversation__transcript { min-height: min-content; }
|
|
404
408
|
.assistant-conversation__composer { flex: 0 0 auto; }
|
|
405
409
|
.assistant-conversation__attachment-error { flex-basis: 100%; margin: 0; }
|
|
406
410
|
.assistant-conversation__setting { min-width: 8rem; max-width: 16rem; }
|
|
@@ -440,6 +440,9 @@ const KEYBOARD_SCROLL_KEYS = new Set([
|
|
|
440
440
|
"Spacebar"
|
|
441
441
|
]);
|
|
442
442
|
const timeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
443
|
+
year: "numeric",
|
|
444
|
+
month: "short",
|
|
445
|
+
day: "2-digit",
|
|
443
446
|
hour: "2-digit",
|
|
444
447
|
minute: "2-digit"
|
|
445
448
|
});
|
|
@@ -1103,7 +1106,12 @@ watch(() => props.visible, (visible) => {
|
|
|
1103
1106
|
}
|
|
1104
1107
|
|
|
1105
1108
|
.assistant-transcript--error {
|
|
1106
|
-
grid-template-rows:
|
|
1109
|
+
grid-template-rows: max-content minmax(0, 1fr);
|
|
1110
|
+
min-height: min-content;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.assistant-transcript--error .assistant-transcript__body {
|
|
1114
|
+
contain: size;
|
|
1107
1115
|
}
|
|
1108
1116
|
|
|
1109
1117
|
.assistant-transcript__error {
|
|
@@ -1115,6 +1123,7 @@ watch(() => props.visible, (visible) => {
|
|
|
1115
1123
|
.assistant-transcript__error-content {
|
|
1116
1124
|
align-items: flex-start;
|
|
1117
1125
|
display: flex;
|
|
1126
|
+
flex-direction: column;
|
|
1118
1127
|
gap: 0.5rem;
|
|
1119
1128
|
}
|
|
1120
1129
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isPlainObject, normalizeText } from "./normalize.js";
|
|
2
2
|
import { createReentrantConversationStorage } from "./storage.js";
|
|
3
|
+
import { isCompletedEnvelopeTurn } from "./transcript.js";
|
|
3
4
|
import { normalizeCodexAppServerRunState, codexAppServerRunStateIsActive,
|
|
4
5
|
codexAppServerRunStateIsTerminal } from "./codexTurn.js";
|
|
5
6
|
|
|
@@ -134,7 +135,7 @@ export function createCodexConversationStore({ storage, scope, segmentId, isCurr
|
|
|
134
135
|
assertCurrent(sessionId);
|
|
135
136
|
return transcript[name](sessionId, ...args);
|
|
136
137
|
};
|
|
137
|
-
async function
|
|
138
|
+
async function nativeAuthorship(transaction, identity) {
|
|
138
139
|
if (!identity?.threadId || !identity.turnId) return undefined;
|
|
139
140
|
const { runtime } = await transaction.readMetadata();
|
|
140
141
|
const run = runtime.binding?.codexAppServerRun;
|
|
@@ -143,17 +144,32 @@ export function createCodexConversationStore({ storage, scope, segmentId, isCurr
|
|
|
143
144
|
for (const turnId of (await transaction.listTurnIds()).reverse()) {
|
|
144
145
|
const turn = await transaction.readTurn(turnId);
|
|
145
146
|
if (turn?.metadata?.runtime?.segmentId !== segmentId) continue;
|
|
146
|
-
if (turn.user?.messageId === run.outerTurnId) return "user";
|
|
147
|
-
if (turn.system?.messageId === run.outerTurnId && turn.system.origin === "application")
|
|
147
|
+
if (turn.user?.messageId === run.outerTurnId) return { origin: "user", messageId: run.outerTurnId, turnId };
|
|
148
|
+
if (turn.system?.messageId === run.outerTurnId && turn.system.origin === "application") {
|
|
149
|
+
return { origin: "application", messageId: run.outerTurnId, turnId,
|
|
150
|
+
...(isCompletedEnvelopeTurn(turn) ? { completedEnvelope: true } : {}) };
|
|
151
|
+
}
|
|
148
152
|
}
|
|
149
153
|
const request = runtime.request;
|
|
150
|
-
if (request?.messageId === run.outerTurnId && ["user", "application"].includes(request.origin))
|
|
154
|
+
if (request?.messageId === run.outerTurnId && ["user", "application"].includes(request.origin)) {
|
|
155
|
+
return { origin: request.origin, messageId: request.messageId,
|
|
156
|
+
...(request.origin === "application" && request.completedEnvelope === true ? { completedEnvelope: true } : {}) };
|
|
157
|
+
}
|
|
151
158
|
return undefined;
|
|
152
159
|
}
|
|
153
|
-
const writer = name => (sessionId, input) => runStore.mutateSession(sessionId, async transaction => {
|
|
154
|
-
const
|
|
160
|
+
const writer = name => (sessionId, input = {}) => runStore.mutateSession(sessionId, async transaction => {
|
|
161
|
+
const authorship = await nativeAuthorship(transaction, input.nativeIdentity);
|
|
162
|
+
if (authorship?.completedEnvelope && !authorship.turnId) {
|
|
163
|
+
throw new Error("The native output's original turn no longer exists.");
|
|
164
|
+
}
|
|
165
|
+
// Queued human rows do not own an already admitted private native response.
|
|
166
|
+
// Corrections retain their original explicit target and message identity.
|
|
167
|
+
const written = await transcript[name](sessionId, {
|
|
168
|
+
...input, ...(name !== "upsertConversationAssistantMessage" && authorship?.completedEnvelope && authorship.turnId
|
|
169
|
+
? { turnId: authorship.turnId } : {})
|
|
170
|
+
});
|
|
155
171
|
if (!written || written.metadata?.runtime?.segmentId) return written;
|
|
156
|
-
const origin =
|
|
172
|
+
const origin = authorship?.origin;
|
|
157
173
|
await transaction.updateTurnMetadata(written.turnId, { runtime: {
|
|
158
174
|
status: "complete", engine: "codex", segmentId, ...(origin ? { origin } : {}),
|
|
159
175
|
...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {})
|
|
@@ -191,6 +207,8 @@ export function createCodexConversationStore({ storage, scope, segmentId, isCurr
|
|
|
191
207
|
...(request.data !== undefined ? { data: request.data } : {}), ...(request.goal ? { goal: request.goal } : {}),
|
|
192
208
|
turnMetadata: { ...input.turnMetadata, runtime: { ...input.turnMetadata?.runtime,
|
|
193
209
|
status: "running", engine: "codex", segmentId, origin: request.origin || "user",
|
|
210
|
+
...(metadata.runtime.request?.messageId === input.messageId && metadata.runtime.request.completedEnvelope === true
|
|
211
|
+
? { completedEnvelope: true } : {}),
|
|
194
212
|
...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {}),
|
|
195
213
|
...(request.goal ? { goalMessageId: request.messageId } : {}) } }
|
|
196
214
|
});
|
|
@@ -212,11 +230,20 @@ export function createCodexConversationStore({ storage, scope, segmentId, isCurr
|
|
|
212
230
|
});
|
|
213
231
|
},
|
|
214
232
|
updateConversationStream(sessionId, input) {
|
|
215
|
-
return runStore.mutateSession(sessionId, async transaction =>
|
|
216
|
-
|
|
217
|
-
|
|
233
|
+
return runStore.mutateSession(sessionId, async transaction => {
|
|
234
|
+
const authorship = await nativeAuthorship(transaction, input.nativeIdentity);
|
|
235
|
+
return streams.update(sessionId, { ...input, origin: authorship?.origin, authorship });
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
completeConversationStreamMessage(sessionId, messageId, options) {
|
|
239
|
+
assertCurrent(sessionId);
|
|
240
|
+
if (options?.nativeIdentity) return runStore.mutateSession(sessionId, async transaction => {
|
|
241
|
+
const authorship = await nativeAuthorship(transaction, options.nativeIdentity);
|
|
242
|
+
return streams.complete(sessionId, messageId, { ...options, authorship,
|
|
243
|
+
...(!authorship ? { text: undefined } : {}) });
|
|
244
|
+
});
|
|
245
|
+
return streams.complete(sessionId, messageId, options);
|
|
218
246
|
},
|
|
219
|
-
completeConversationStreamMessage(sessionId, messageId) { assertCurrent(sessionId); return streams.complete(sessionId, messageId); },
|
|
220
247
|
clearConversationStream(sessionId) { assertCurrent(sessionId); return streams.clear(sessionId); }
|
|
221
248
|
});
|
|
222
249
|
}
|
|
@@ -87,7 +87,11 @@ function claudeMessageBlocks(frame, { includeNested = false } = {}) {
|
|
|
87
87
|
|
|
88
88
|
async function readClaudeHistory(options) {
|
|
89
89
|
const file = await claudeHistoryPath(options);
|
|
90
|
-
|
|
90
|
+
const inspected = {
|
|
91
|
+
...(options.includeUserMessages ? { userMessages: [] } : {}),
|
|
92
|
+
...(options.includeFinalCarriers ? { completedResults: [], failedResults: [], structuredOutputs: [] } : {})
|
|
93
|
+
};
|
|
94
|
+
if (!file || file.size === 0) return { exists: false, messages: [], userIds: [], text: "", ...inspected };
|
|
91
95
|
if (file.size > 256 * 1024 * 1024) throw new Error("Claude history exceeds the supported read limit.");
|
|
92
96
|
const messages = new Map();
|
|
93
97
|
const userIds = [];
|
|
@@ -98,9 +102,10 @@ async function readClaudeHistory(options) {
|
|
|
98
102
|
let leafUuid = "";
|
|
99
103
|
let rewound = false;
|
|
100
104
|
// Snapshot the file length. A writer can be in the middle of its last frame;
|
|
101
|
-
//
|
|
105
|
+
// Live history reads allow that unfinished tail. Stopped maintenance can
|
|
106
|
+
// require the existing strict parser without changing live observation.
|
|
102
107
|
for await (const frame of readClaudeJsonFrames(createReadStream(file.path, { end: file.size - 1 }), {
|
|
103
|
-
allowIncompleteTail: true
|
|
108
|
+
allowIncompleteTail: options.allowIncompleteTail ?? true
|
|
104
109
|
})) {
|
|
105
110
|
if (frame.isSidechain) continue;
|
|
106
111
|
frames.push(frame);
|
|
@@ -139,12 +144,42 @@ async function readClaudeHistory(options) {
|
|
|
139
144
|
(typeof frame.message?.content === "string" || frame.message?.content?.some((block) => block.type === "text"))) {
|
|
140
145
|
userIds.push(frame.uuid);
|
|
141
146
|
lastUserId = frame.uuid;
|
|
147
|
+
if (options.includeUserMessages && !frame.parent_tool_use_id) {
|
|
148
|
+
const content = frame.message.content;
|
|
149
|
+
const text = typeof content === "string" ? content : content
|
|
150
|
+
.filter(block => block.type === "text" && typeof block.text === "string").map(block => block.text).join("\n\n");
|
|
151
|
+
inspected.userMessages.push({ id: frame.uuid, text });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (options.includeFinalCarriers && !frame.parent_tool_use_id && !frame.isMeta) {
|
|
155
|
+
if (frame.type === "result" && frame.subtype === "success" && !frame.is_error &&
|
|
156
|
+
!["aborted_streaming", "aborted_tools"].includes(frame.terminal_reason)) {
|
|
157
|
+
// Same final normalization as the live native turn; this is an actual
|
|
158
|
+
// saved result, never a result invented from a tool-use candidate.
|
|
159
|
+
const text = typeof frame.structured_output === "object" ? JSON.stringify(frame.structured_output) : String(frame.result || "");
|
|
160
|
+
inspected.completedResults.push({ id: frame.uuid || "", userId: lastUserId, text });
|
|
161
|
+
} else if (frame.type === "result") {
|
|
162
|
+
// Preserve the live terminal-failure/interruption contradiction without
|
|
163
|
+
// copying native error details into stopped application inspection.
|
|
164
|
+
inspected.failedResults.push({ id: frame.uuid || "", userId: lastUserId });
|
|
165
|
+
}
|
|
166
|
+
if (frame.type === "assistant") {
|
|
167
|
+
for (const block of frame.message?.content || []) {
|
|
168
|
+
if (block.type === "tool_use" && block.name === "StructuredOutput" && typeof block.id === "string" && block.id &&
|
|
169
|
+
block.input && typeof block.input === "object" && !Array.isArray(block.input)) {
|
|
170
|
+
// Raw native carrier only. A stopped importer must independently
|
|
171
|
+
// verify the original completed receipt and exact final/user custody.
|
|
172
|
+
inspected.structuredOutputs.push({ id: frame.uuid || "", userId: lastUserId,
|
|
173
|
+
toolUseId: block.id, text: JSON.stringify(block.input) });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
142
177
|
}
|
|
143
178
|
for (const message of claudeMessageBlocks(frame)) {
|
|
144
179
|
messages.set(message.id, { ...message, userId: lastUserId });
|
|
145
180
|
}
|
|
146
181
|
}
|
|
147
|
-
return { exists: true, messages: [...messages.values()], userIds, goal,
|
|
182
|
+
return { exists: true, messages: [...messages.values()], userIds, goal, ...inspected,
|
|
148
183
|
text: [...messages.values()].filter((message) => message.role === "assistant" && message.userId === lastUserId)
|
|
149
184
|
.map((message) => message.text).join("\n") };
|
|
150
185
|
}
|
|
@@ -155,7 +190,7 @@ export async function deleteClaudeConversationHistory({ configRoot, workdir, con
|
|
|
155
190
|
if (file) await rm(file.path, { force: true });
|
|
156
191
|
}
|
|
157
192
|
|
|
158
|
-
export { claudeMessageBlocks, readClaudeHistory, requireClaudeSessionId, archivedClaudeProjectDirectory, matchingClaudeProjectDirectories };
|
|
193
|
+
export { claudeHistoryPath, claudeMessageBlocks, readClaudeHistory, requireClaudeSessionId, archivedClaudeProjectDirectory, matchingClaudeProjectDirectories };
|
|
159
194
|
|
|
160
195
|
const TRANSCRIPT_PATH_PATTERN = /\.jsonl(?:\.(?:superseded|orphaned)-.*)?$/u;
|
|
161
196
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { nativeAiProvider } from "../../shared/nativeProviders.js";
|
|
2
3
|
import { Duplex } from "node:stream";
|
|
3
4
|
import { stat } from "node:fs/promises";
|
|
@@ -65,6 +66,11 @@ async function readNativeClaudeCodeAuthStatus({ env, credentialHome, command, co
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** Exact existing native connection pin, shared with stopped metadata conversion. */
|
|
70
|
+
export function claudeConnectionIdentity(configRoot, providerId, baseUrl, apiKey) {
|
|
71
|
+
return createHash("sha256").update(JSON.stringify([configRoot, providerId, baseUrl, apiKey])).digest("hex");
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
/** Native provider routing; the application supplies only its authorized selection. */
|
|
69
75
|
export function claudeModelConfiguration({ providerId, model: modelId }, connection) {
|
|
70
76
|
const provider = nativeAiProvider(providerId);
|
|
@@ -305,7 +311,7 @@ export async function createClaudeCodeProcess({
|
|
|
305
311
|
signal?.throwIfAborted();
|
|
306
312
|
native = await execution.start({ command, args: claudeCodeArguments(options), cwd: workdir,
|
|
307
313
|
env: environment, stream: true, limits: executionLimits });
|
|
308
|
-
await onStarted?.(native.id);
|
|
314
|
+
await onStarted?.(native.id, stop);
|
|
309
315
|
signal?.throwIfAborted();
|
|
310
316
|
client = createClaudeJsonClient({ stream: Duplex.from({ readable: native.stdout, writable: native.stdin }), onEvent, onFailure,
|
|
311
317
|
onControlRequest(request, context) {
|
|
@@ -317,9 +323,17 @@ export async function createClaudeCodeProcess({
|
|
|
317
323
|
}
|
|
318
324
|
});
|
|
319
325
|
signal?.addEventListener("abort", abort, { once: true });
|
|
320
|
-
const
|
|
326
|
+
const initializing = client.initialize(commandWrapper ? { hooks: {
|
|
321
327
|
PreToolUse: [{ matcher: "^Bash$", hookCallbackIds: ["jskit-command-wrapper"], timeout: 30 }]
|
|
322
328
|
} } : {});
|
|
329
|
+
const initialization = await (native.exited ? Promise.race([
|
|
330
|
+
initializing,
|
|
331
|
+
native.exited.then(processExit => {
|
|
332
|
+
throw Object.assign(new Error("Claude exited before initialization completed."), {
|
|
333
|
+
code: "assistant_claude_startup_exited", processExit
|
|
334
|
+
});
|
|
335
|
+
})
|
|
336
|
+
]) : initializing);
|
|
323
337
|
return { client, initialization, executionId: native.id, stop };
|
|
324
338
|
} catch (error) {
|
|
325
339
|
// A rejected start may already own an execution and its cleanup proof.
|
|
@@ -203,7 +203,7 @@ export function createClaudeConversationOwner({
|
|
|
203
203
|
if (saved && typeof saved.then === "function") saved = await saved;
|
|
204
204
|
if (!main && !saved && !create && !recoveringCleanup) throw createError("This Claude conversation is unavailable.");
|
|
205
205
|
const state = saved ? JSON.parse(saved) : {};
|
|
206
|
-
const entry = { id, key, main, persistent: state.persistent === true, context, process: null,
|
|
206
|
+
const entry = { id, key, main, persistent: state.persistent === true, context, process: null, startupCleanup: null,
|
|
207
207
|
executionId: state.executionId || (recoveringCleanup ? cleanupExecutionId : ""),
|
|
208
208
|
accountIdentities: state.accountIdentities || (state.accountIdentity ? { anthropic: state.accountIdentity } : {}),
|
|
209
209
|
accountIdentity: state.accountIdentity || "", sent: state.sent === true, nativeWorkdir: state.nativeWorkdir || nativeWorkdir,
|
|
@@ -312,7 +312,9 @@ export function createClaudeConversationOwner({
|
|
|
312
312
|
if (options && typeof options.then === "function") options = await options;
|
|
313
313
|
try { entry.process = await createProcess({ ...options,
|
|
314
314
|
workdir: entry.nativeWorkdir, sessionId: entry.id, resume: entry.sent,
|
|
315
|
-
onStarted: async executionId => {
|
|
315
|
+
onStarted: async (executionId, stop) => {
|
|
316
|
+
if (typeof stop === "function") entry.startupCleanup = { executionId, stop };
|
|
317
|
+
if (entry.executionId === executionId) return;
|
|
316
318
|
entry.executionId = executionId;
|
|
317
319
|
await store.save(entry, { execution: true });
|
|
318
320
|
await onEvent(entry, { type: "execution", context, executionId });
|
|
@@ -380,9 +382,13 @@ export function createClaudeConversationOwner({
|
|
|
380
382
|
await onEvent(entry, { type: "execution", context, executionId: entry.executionId });
|
|
381
383
|
} catch (bindingError) { error.bindingError = bindingError; }
|
|
382
384
|
}
|
|
383
|
-
if (store.releaseExecution && error.stopProof?.scopeEmpty)
|
|
385
|
+
if (store.releaseExecution && error.stopProof?.scopeEmpty) {
|
|
386
|
+
await store.releaseExecution(entry);
|
|
387
|
+
if (entry.startupCleanup?.executionId === error.executionId) entry.startupCleanup = null;
|
|
388
|
+
}
|
|
384
389
|
throw error;
|
|
385
390
|
}
|
|
391
|
+
if (entry.startupCleanup?.executionId === entry.executionId) entry.startupCleanup = null;
|
|
386
392
|
return entry.process;
|
|
387
393
|
}
|
|
388
394
|
|
|
@@ -742,10 +748,10 @@ export function createClaudeConversationOwner({
|
|
|
742
748
|
async function run(control, command, { acquire, reportFailure, entry: selectedEntry, maximumOutput, maximumToolCalls = 32, timeoutMs } = {}) {
|
|
743
749
|
if (control.disposed || control.current) throw new Error("This Claude conversation is closed or already working.");
|
|
744
750
|
const binding = Boolean(store.releaseExecution);
|
|
745
|
-
const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent } = command;
|
|
751
|
+
const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent, onNativeToolUse } = command;
|
|
746
752
|
const completion = Promise.withResolvers();
|
|
747
753
|
completion.promise.catch(() => {});
|
|
748
|
-
const active = { completion, onMessage, onEvent, signal, tools, admitted: false, nativeTools: new Map(),
|
|
754
|
+
const active = { completion, onMessage, onEvent, onNativeToolUse, signal, tools, admitted: false, nativeTools: new Map(),
|
|
749
755
|
messageId: input.messageId, goal: input.goal, nativeToolCount: 0, toolWork: new Set(), maximumToolCalls,
|
|
750
756
|
context, failure: Promise.withResolvers() };
|
|
751
757
|
let failure;
|
|
@@ -947,10 +953,11 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
947
953
|
const active = read().active;
|
|
948
954
|
if (!binding) entry.stopping = true;
|
|
949
955
|
const operation = async () => {
|
|
956
|
+
const startup = entry.startupCleanup?.executionId === entry.executionId ? entry.startupCleanup : null;
|
|
950
957
|
let proof;
|
|
951
958
|
try {
|
|
952
959
|
if (!binding || entry.process || entry.executionId) {
|
|
953
|
-
proof = await stopClaudeCodeProcess({ process: entry.process, executionId: entry.executionId,
|
|
960
|
+
proof = await stopClaudeCodeProcess({ process: entry.process || startup, executionId: entry.executionId,
|
|
954
961
|
stopExecution: process.stopExecution });
|
|
955
962
|
}
|
|
956
963
|
} catch (error) {
|
|
@@ -966,12 +973,25 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
966
973
|
entry.exitProof = proof;
|
|
967
974
|
stop(binding ? reason : reason || "Claude stopped before acknowledging the prompt.");
|
|
968
975
|
if (binding) {
|
|
976
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
969
977
|
if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
|
|
970
978
|
return;
|
|
971
979
|
}
|
|
972
|
-
if (entry.turn?.active)
|
|
973
|
-
|
|
980
|
+
if (entry.turn?.active) {
|
|
981
|
+
try { await updateState("interrupted", reason); }
|
|
982
|
+
catch (error) {
|
|
983
|
+
// Native cleanup is verified even when the host refuses its checkpoint.
|
|
984
|
+
// Retain that refusal without publishing a completed answer or active work.
|
|
985
|
+
setState("interrupted", error.message);
|
|
986
|
+
await onEvent({ type: "save" });
|
|
987
|
+
await onEvent({ type: "state", state: "interrupted", active: false, message: error.message });
|
|
988
|
+
entry.completion?.resolve(readResult());
|
|
989
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
} else await onEvent({ type: "save" });
|
|
974
993
|
entry.completion?.resolve(readResult());
|
|
994
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
975
995
|
return entry.exitProof;
|
|
976
996
|
};
|
|
977
997
|
if (!binding) return operation();
|
|
@@ -943,7 +943,7 @@ export function codexProviderModelCatalog({ models }) {
|
|
|
943
943
|
supports_reasoning_summaries: false,
|
|
944
944
|
default_reasoning_summary: "none",
|
|
945
945
|
support_verbosity: false,
|
|
946
|
-
truncation_policy: { mode: "
|
|
946
|
+
truncation_policy: { mode: "tokens", limit: 10000 },
|
|
947
947
|
context_window: model.contextWindow,
|
|
948
948
|
max_context_window: model.contextWindow,
|
|
949
949
|
effective_context_window_percent: 95,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { open, realpath } from "node:fs/promises";
|
|
2
|
+
import { isUtf8 } from "node:buffer";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { Readable } from "node:stream";
|
|
@@ -120,8 +121,9 @@ function retainedUserRecord(item) {
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
// Scan a fixed snapshot without retaining the lifetime transcript. A single
|
|
123
|
-
// record still has a transport-sized bound; incomplete
|
|
124
|
-
|
|
124
|
+
// record still has a transport-sized bound; recovery ignores incomplete appends.
|
|
125
|
+
// Stopped offline inspection can require complete rows without changing recovery.
|
|
126
|
+
async function* readCodexHistoryRows(file, start, end, signal, { strictComplete = false } = {}) {
|
|
125
127
|
const chunk = Buffer.alloc(64 * 1024);
|
|
126
128
|
let pending = [];
|
|
127
129
|
let size = 0;
|
|
@@ -141,7 +143,11 @@ async function* readCodexHistoryRows(file, start, end, signal) {
|
|
|
141
143
|
pending.push(Buffer.from(chunk.subarray(from, until)));
|
|
142
144
|
if (complete) {
|
|
143
145
|
signal.throwIfAborted();
|
|
144
|
-
if (size)
|
|
146
|
+
if (size) {
|
|
147
|
+
const bytes = Buffer.concat(pending, size);
|
|
148
|
+
if (strictComplete && !isUtf8(bytes)) throw compactionHistoryError("the saved history contains invalid UTF-8.");
|
|
149
|
+
yield { row: JSON.parse(bytes.toString("utf8")), offset: lineStart };
|
|
150
|
+
}
|
|
145
151
|
pending = [];
|
|
146
152
|
size = 0;
|
|
147
153
|
lineStart = offset + newline + 1;
|
|
@@ -150,6 +156,7 @@ async function* readCodexHistoryRows(file, start, end, signal) {
|
|
|
150
156
|
}
|
|
151
157
|
offset += bytesRead;
|
|
152
158
|
}
|
|
159
|
+
if (strictComplete && size) throw compactionHistoryError("the saved history has an incomplete final record.");
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
// Native compaction keeps the original rollout on disk. Foreign providers
|
|
@@ -456,4 +463,4 @@ async function prepareCodexHistory(params, client, { baseUrl, modelProviderId, .
|
|
|
456
463
|
} };
|
|
457
464
|
}
|
|
458
465
|
|
|
459
|
-
export { startCodexHistoryAdapter, translateCodexHistory, prepareCodexHistory };
|
|
466
|
+
export { startCodexHistoryAdapter, translateCodexHistory, prepareCodexHistory, readCodexHistoryRows };
|
|
@@ -40,8 +40,7 @@ export function createCodexAppServerNotificationQueue({
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
async function
|
|
44
|
-
const taskSessionKey = namespace(sessionId);
|
|
43
|
+
async function drainCodexAppServerNotificationTasksForKey(taskSessionKey) {
|
|
45
44
|
while (true) {
|
|
46
45
|
const task = codexAppServerNotificationTasks.get(taskSessionKey);
|
|
47
46
|
if (!task) {
|
|
@@ -51,6 +50,15 @@ export function createCodexAppServerNotificationQueue({
|
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
|
|
53
|
+
async function drainCodexAppServerNotificationTasks(sessionId = "") {
|
|
54
|
+
return drainCodexAppServerNotificationTasksForKey(namespace(sessionId));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function captureCodexAppServerNotificationDrain(sessionId = "") {
|
|
58
|
+
const taskSessionKey = namespace(sessionId);
|
|
59
|
+
return () => drainCodexAppServerNotificationTasksForKey(taskSessionKey);
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
function queueCodexAppServerStream(context, classification, deliver) {
|
|
55
63
|
if (isClosing() || !classification.itemId || !classification.turnId) return;
|
|
56
64
|
const pending = codexAppServerPendingStreams.get(context.sessionKey);
|
|
@@ -97,6 +105,7 @@ export function createCodexAppServerNotificationQueue({
|
|
|
97
105
|
pendingReasoning: codexAppServerPendingReasoning,
|
|
98
106
|
run: runCodexAppServerNotificationTask,
|
|
99
107
|
drain: drainCodexAppServerNotificationTasks,
|
|
108
|
+
captureDrain: captureCodexAppServerNotificationDrain,
|
|
100
109
|
stream: queueCodexAppServerStream,
|
|
101
110
|
reasoning: queueCodexAppServerReasoning
|
|
102
111
|
};
|
|
@@ -39,19 +39,19 @@ export function createCodexObservationControl({
|
|
|
39
39
|
function createCodexAppServerObservation(sessionId, context = {}) {
|
|
40
40
|
const { projectContext, target, retireProvider } = context;
|
|
41
41
|
return {
|
|
42
|
-
prepare({ provider, error }) {
|
|
42
|
+
prepare({ provider, error, threadIds }) {
|
|
43
43
|
return runInContext(projectContext, async () => {
|
|
44
44
|
const managed = target(error);
|
|
45
45
|
const threads = new Map();
|
|
46
46
|
for (const entry of codexAppServerConversations.get(namespace(sessionId))?.values() || []) {
|
|
47
|
-
if (entry.provider !== provider) continue;
|
|
47
|
+
if (entry.provider !== provider || threadIds && !threadIds.includes(entry.conversationId)) continue;
|
|
48
48
|
threads.set(entry.conversationId, entry.runId);
|
|
49
49
|
if (codexAppServerConversationTurnIsActive(entry.status) || entry.goal?.status === "active") {
|
|
50
50
|
entry.error = managed.pendingMessage;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
// A helper owns only its own threads. Main chat belongs to its managed provider.
|
|
54
|
-
if (managed.threadId) {
|
|
54
|
+
if (managed.threadId && (!threadIds || threadIds.includes(managed.threadId))) {
|
|
55
55
|
const runtime = await createRuntime();
|
|
56
56
|
const turn = turnState(await runtime.getSession(sessionId));
|
|
57
57
|
threads.set(managed.threadId, turn.turnId);
|
|
@@ -66,10 +66,10 @@ export function createCodexObservationControl({
|
|
|
66
66
|
return threads;
|
|
67
67
|
});
|
|
68
68
|
},
|
|
69
|
-
barrier({ error }) {
|
|
69
|
+
barrier({ error, threadIds }) {
|
|
70
70
|
return runInContext(projectContext, async () => {
|
|
71
71
|
const managed = target(error);
|
|
72
|
-
if (!managed.threadId) return;
|
|
72
|
+
if (!managed.threadId || threadIds && !threadIds.includes(managed.threadId)) return;
|
|
73
73
|
const runtime = await createRuntime();
|
|
74
74
|
const turn = turnState(await runtime.getSession(sessionId));
|
|
75
75
|
if (!turn.active && !codexAppServerTurnOwnsActiveGoal(turn, managed.threadId)) return;
|
|
@@ -79,12 +79,12 @@ export function createCodexObservationControl({
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
},
|
|
82
|
-
complete({ provider, error, sharedStop }) {
|
|
82
|
+
complete({ provider, error, sharedStop, threadIds }) {
|
|
83
83
|
return runInContext(projectContext, async () => {
|
|
84
84
|
const managed = target(error);
|
|
85
85
|
let stoppedRun = null;
|
|
86
86
|
let conversationStream = null;
|
|
87
|
-
if (managed.threadId) {
|
|
87
|
+
if (managed.threadId && (!threadIds || threadIds.includes(managed.threadId))) {
|
|
88
88
|
const runtime = await createRuntime();
|
|
89
89
|
stoppedRun = await runtime.store.mutateSession(sessionId, async () => {
|
|
90
90
|
const run = await readCodexAppServerAgentRunForSession(runtime.store, sessionId);
|
|
@@ -105,7 +105,7 @@ export function createCodexObservationControl({
|
|
|
105
105
|
if (sharedStop) await retireProvider(provider);
|
|
106
106
|
else provider.observationFailure = null;
|
|
107
107
|
for (const entry of codexAppServerConversations.get(namespace(sessionId))?.values() || []) {
|
|
108
|
-
if (entry.provider === provider && (
|
|
108
|
+
if (entry.provider === provider && (!threadIds || threadIds.includes(entry.conversationId)) && (
|
|
109
109
|
codexAppServerConversationTurnIsActive(entry.status) || entry.goal?.status === "active"
|
|
110
110
|
)) {
|
|
111
111
|
entry.status = "interrupted";
|