@jskit-ai/assistant-core 0.1.212 → 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 +28 -4
- package/src/server/conversation/claudeTools.js +11 -2
- package/src/server/conversation/claudeTurn.js +213 -23
- 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 +183 -28
- 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 +424 -65
- 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 +1417 -3
- package/test/claudeProcess.test.js +175 -0
- package/test/claudeTurn.test.js +466 -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 +165 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +372 -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);
|
|
@@ -147,6 +153,10 @@ export function claudeCodeArguments({
|
|
|
147
153
|
} : {} }));
|
|
148
154
|
if (!applicationTools) args.push("--disallowedTools", "mcp__*");
|
|
149
155
|
if (!toolFree) args.push("--tools", "Bash,Read,Edit,Write,Glob,Grep,NotebookEdit");
|
|
156
|
+
} else if (applicationTools) {
|
|
157
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers: {
|
|
158
|
+
[CLAUDE_APPLICATION_TOOL_SERVER]: { type: "sdk", name: CLAUDE_APPLICATION_TOOL_SERVER }
|
|
159
|
+
} }));
|
|
150
160
|
}
|
|
151
161
|
if (outputSchema) args.push("--json-schema", JSON.stringify(outputSchema));
|
|
152
162
|
return args;
|
|
@@ -301,7 +311,7 @@ export async function createClaudeCodeProcess({
|
|
|
301
311
|
signal?.throwIfAborted();
|
|
302
312
|
native = await execution.start({ command, args: claudeCodeArguments(options), cwd: workdir,
|
|
303
313
|
env: environment, stream: true, limits: executionLimits });
|
|
304
|
-
await onStarted?.(native.id);
|
|
314
|
+
await onStarted?.(native.id, stop);
|
|
305
315
|
signal?.throwIfAborted();
|
|
306
316
|
client = createClaudeJsonClient({ stream: Duplex.from({ readable: native.stdout, writable: native.stdin }), onEvent, onFailure,
|
|
307
317
|
onControlRequest(request, context) {
|
|
@@ -313,13 +323,27 @@ export async function createClaudeCodeProcess({
|
|
|
313
323
|
}
|
|
314
324
|
});
|
|
315
325
|
signal?.addEventListener("abort", abort, { once: true });
|
|
316
|
-
const
|
|
326
|
+
const initializing = client.initialize(commandWrapper ? { hooks: {
|
|
317
327
|
PreToolUse: [{ matcher: "^Bash$", hookCallbackIds: ["jskit-command-wrapper"], timeout: 30 }]
|
|
318
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);
|
|
319
337
|
return { client, initialization, executionId: native.id, stop };
|
|
320
338
|
} catch (error) {
|
|
321
|
-
|
|
322
|
-
|
|
339
|
+
// A rejected start may already own an execution and its cleanup proof.
|
|
340
|
+
// Only a returned native handle transfers that cleanup to this owner.
|
|
341
|
+
if (native) {
|
|
342
|
+
error.executionId = native.id;
|
|
343
|
+
try { error.stopProof = await stop(); }
|
|
344
|
+
catch (cleanupError) { error.cleanupError = cleanupError; error.stopProof = { scopeEmpty: false }; }
|
|
345
|
+
if (error.stopProof?.scopeEmpty !== true) error.cleanupFailed = true;
|
|
346
|
+
}
|
|
323
347
|
throw error;
|
|
324
348
|
} finally {
|
|
325
349
|
signal?.removeEventListener("abort", abort);
|
|
@@ -4,7 +4,7 @@ export const CLAUDE_APPLICATION_TOOL_SERVER = "application";
|
|
|
4
4
|
|
|
5
5
|
// The CLI carries this SDK MCP exchange over its existing control pipe. There
|
|
6
6
|
// is no listening socket or separate tool process. The shared executor owns effects.
|
|
7
|
-
export async function claudeApplicationToolResponse(request, { schemas, turn, signal }) {
|
|
7
|
+
export async function claudeApplicationToolResponse(request, { schemas, turn, signal, assertCurrent, onExecutionFailure }) {
|
|
8
8
|
if (request?.subtype !== "mcp_message" || request.server_name !== CLAUDE_APPLICATION_TOOL_SERVER ||
|
|
9
9
|
request.message?.jsonrpc !== "2.0") throw new Error("This Claude control request is not an application tool exchange.");
|
|
10
10
|
const message = request.message;
|
|
@@ -31,7 +31,16 @@ export async function claudeApplicationToolResponse(request, { schemas, turn, si
|
|
|
31
31
|
!isDeepStrictEqual(native.input, params.arguments || {})) {
|
|
32
32
|
throw new Error("Claude's application call does not match an admitted native tool use. Update Claude if its tool identity is unavailable.");
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
// Optional supplied-owner checks run after the unchanged native-use
|
|
35
|
+
// validation. Protocol/stale refusals are not owned executor failures.
|
|
36
|
+
assertCurrent?.(native);
|
|
37
|
+
let value;
|
|
38
|
+
try {
|
|
39
|
+
value = await turn.tools.execute({ id, name: params.name, arguments: JSON.stringify(params.arguments || {}) }, { signal, messageId: native.messageId });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
onExecutionFailure?.(error, native);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
35
44
|
result = { content: [{ type: "text", text: JSON.stringify(value) }], isError: value.ok === false };
|
|
36
45
|
break;
|
|
37
46
|
}
|