@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.
Files changed (60) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
  3. package/package.json +5 -5
  4. package/src/client/conversation/AssistantConversationElement.vue +5 -1
  5. package/src/client/conversation/AssistantTranscript.vue +10 -1
  6. package/src/server/conversation/agentRun.js +38 -11
  7. package/src/server/conversation/claudeHistory.js +40 -5
  8. package/src/server/conversation/claudeProcess.js +28 -4
  9. package/src/server/conversation/claudeTools.js +11 -2
  10. package/src/server/conversation/claudeTurn.js +213 -23
  11. package/src/server/conversation/codexConfiguration.js +1 -1
  12. package/src/server/conversation/codexHistoryAdapter.js +11 -4
  13. package/src/server/conversation/codexNotificationQueue.js +11 -2
  14. package/src/server/conversation/codexObservationControl.js +8 -8
  15. package/src/server/conversation/codexOutput.js +20 -15
  16. package/src/server/conversation/codexProvider.js +40 -3
  17. package/src/server/conversation/codexProviderOwner.js +110 -1
  18. package/src/server/conversation/codexTurn.js +3 -0
  19. package/src/server/conversation/continuity.js +24 -6
  20. package/src/server/conversation/index.js +1 -1
  21. package/src/server/conversation/nativeHistoryExport.js +97 -0
  22. package/src/server/conversation/openCodeClient.js +47 -1
  23. package/src/server/conversation/openCodeRuntime.js +183 -28
  24. package/src/server/conversation/openCodeTurn.js +65 -24
  25. package/src/server/conversation/providers/api.js +7 -0
  26. package/src/server/conversation/providers/claudeDriver.js +52 -7
  27. package/src/server/conversation/providers/codex.js +19 -2
  28. package/src/server/conversation/providers/codexDriver.js +92 -14
  29. package/src/server/conversation/providers/opencodeDriver.js +31 -9
  30. package/src/server/conversation/runtime.js +424 -65
  31. package/src/server/conversation/runtimeStateUpgrade.js +77 -2
  32. package/src/server/conversation/streams.js +25 -5
  33. package/src/server/conversation/tools.js +2 -2
  34. package/src/server/conversation/transcript.js +23 -8
  35. package/src/server/hosts/claudeHistory.js +1 -0
  36. package/src/server/hosts/claudeProcess.js +1 -0
  37. package/src/server/hosts/codexProvider.js +3 -0
  38. package/src/server/hosts/nativeHistory.js +1 -0
  39. package/src/server/hosts/openCodeClient.js +1 -0
  40. package/src/server/hosts/openCodeTurn.js +1 -0
  41. package/src/server/index.js +1 -1
  42. package/src/server/lib/assistantToolLoop.js +99 -2
  43. package/test/aiConnectionClient.test.js +57 -0
  44. package/test/assistantBoundedToolLoop.test.js +332 -0
  45. package/test/assistantScroll.browser.test.js +151 -0
  46. package/test/claudeConversationSession.test.js +1417 -3
  47. package/test/claudeProcess.test.js +175 -0
  48. package/test/claudeTurn.test.js +466 -0
  49. package/test/codexConversationSession.test.js +1257 -12
  50. package/test/codexHistoryAdapter.test.js +74 -1
  51. package/test/codexTurn.test.js +282 -1
  52. package/test/conversation.test.js +83 -0
  53. package/test/conversationAgentRun.test.js +181 -0
  54. package/test/conversationRuntime.test.js +95 -0
  55. package/test/conversationRuntimeStateUpgrade.test.js +89 -0
  56. package/test/conversationStreaming.test.js +85 -0
  57. package/test/conversationTools.test.js +165 -1
  58. package/test/openCodeClient.test.js +186 -0
  59. package/test/openCodeRuntime.test.js +372 -0
  60. 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.212",
12
- "@jskit-ai/http-runtime": "0.1.232",
13
- "@jskit-ai/kernel": "0.1.234",
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.51",
18
- "@jskit-ai/connectors-catalog": "0.1.51"
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, scrollKey: "fixture", welcomeMessage: "Composer responsiveness fixture"
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.212",
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.176",
51
- "@jskit-ai/resource-crud-core": "0.1.176",
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.232",
61
- "@jskit-ai/kernel": "0.1.234",
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: auto minmax(0, 1fr);
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 nativeOrigin(transaction, identity) {
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") return "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)) return 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 written = await transcript[name](sessionId, input);
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 = await nativeOrigin(transaction, input.nativeIdentity);
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 => streams.update(sessionId, {
216
- ...input, origin: await nativeOrigin(transaction, input.nativeIdentity)
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
- if (!file || file.size === 0) return { exists: false, messages: [], userIds: [], text: "" };
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
- // only this native-history read allows that unfinished tail.
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 initialization = await client.initialize(commandWrapper ? { hooks: {
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
- const proof = await stop();
322
- error.stopProof = proof;
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
- const value = await turn.tools.execute({ id, name: params.name, arguments: JSON.stringify(params.arguments || {}) }, { signal, messageId: native.messageId });
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
  }