@jskit-ai/assistant-core 0.1.240 → 0.1.242
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- package/src/client/conversation/AssistantTranscript.vue +26 -29
- package/src/client/conversation/LongTextPreviewBlocks.vue +11 -0
- package/src/client/conversation/messageDelivery.js +8 -2
- package/src/client/conversation/useAssistantAttachments.js +1 -1
- package/src/server/conversation/agentRun.js +12 -198
- package/src/server/conversation/attachments.js +6 -5
- package/src/server/conversation/boundedToolLoop.js +160 -0
- package/src/server/conversation/claudeHistory.js +37 -23
- package/src/server/conversation/claudeProcess.js +2 -2
- package/src/server/conversation/claudeTurn.js +41 -38
- package/src/server/conversation/codexAuthPreflight.js +104 -0
- package/src/server/conversation/codexClient.js +11 -16
- package/src/server/conversation/codexConfiguration.js +112 -98
- package/src/server/conversation/codexConversationStore.js +189 -0
- package/src/server/conversation/codexConversations.js +47 -44
- package/src/server/conversation/codexDetachedTurn.js +12 -11
- package/src/server/conversation/codexEvents.js +95 -99
- package/src/server/conversation/codexHelperExecution.js +3 -13
- package/src/server/conversation/codexHelperThreadLedger.js +7 -13
- package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
- package/src/server/conversation/codexHelperThreads.js +5 -6
- package/src/server/conversation/codexHistoryAdapter.js +32 -26
- package/src/server/conversation/codexMessageCommands.js +22 -22
- package/src/server/conversation/codexMessageDelivery.js +12 -7
- package/src/server/conversation/codexModelCatalog.js +97 -0
- package/src/server/conversation/codexNotificationObserver.js +24 -40
- package/src/server/conversation/codexNotificationQueue.js +2 -3
- package/src/server/conversation/codexOutput.js +20 -27
- package/src/server/conversation/codexProcess.js +123 -127
- package/src/server/conversation/codexProcessLeader.js +3 -3
- package/src/server/conversation/codexProvider.js +31 -614
- package/src/server/conversation/codexProviderOwner.js +2 -2
- package/src/server/conversation/codexProviderSelection.js +38 -40
- package/src/server/conversation/codexRenewalCommands.js +13 -10
- package/src/server/conversation/codexRenewalThreads.js +412 -0
- package/src/server/conversation/codexRunJournal.js +24 -42
- package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
- package/src/server/conversation/codexSessionCleanup.js +11 -10
- package/src/server/conversation/codexSessionReconciliation.js +6 -6
- package/src/server/conversation/codexThreadReadiness.js +19 -33
- package/src/server/conversation/codexThreadStatus.js +3 -3
- package/src/server/conversation/codexTurn.js +124 -297
- package/src/server/conversation/codexTurnRecovery.js +49 -28
- package/src/server/conversation/codexTurnState.js +34 -13
- package/src/server/conversation/commandWrapper.js +3 -0
- package/src/server/conversation/deliveryTiming.js +14 -5
- package/src/server/conversation/index.js +1 -0
- package/src/server/conversation/localExecution.js +13 -17
- package/src/server/conversation/normalize.js +8 -3
- package/src/server/conversation/openCodeClient.js +9 -10
- package/src/server/conversation/openCodeProcess.js +9 -5
- package/src/server/conversation/openCodeRuntime.js +10 -11
- package/src/server/conversation/openCodeTurn.js +43 -29
- package/src/server/conversation/providers/claudeDriver.js +8 -16
- package/src/server/conversation/providers/codexDriver.js +25 -41
- package/src/server/conversation/providers/opencodeDriver.js +17 -42
- package/src/server/conversation/providers/scoped.js +48 -4
- package/src/server/conversation/runtime.js +50 -49
- package/src/server/conversation/runtimeStateUpgrade.js +34 -22
- package/src/server/conversation/transcript.js +10 -10
- package/src/server/hosts/codexConfiguration.js +1 -0
- package/src/server/hosts/codexProvider.js +8 -6
- package/src/server/index.js +2 -1
- package/src/server/lib/assistantToolLoop.js +1 -153
- package/src/server/lib/serviceToolCatalog.js +17 -42
- package/src/shared/assistantResource.js +4 -2
- package/src/shared/conversation/attachments.js +4 -0
- package/src/shared/conversation/longTextBlocks.js +54 -19
- package/src/shared/conversation/numberedQuestionSugar.js +17 -10
- package/src/shared/conversation/pagination.js +4 -6
- package/src/shared/conversation/patches.js +1 -4
- package/src/shared/conversation/streaming.js +3 -11
- package/src/shared/conversation/turns.js +11 -0
- package/src/shared/index.js +1 -0
- package/test/assistantBoundedToolLoop.test.js +2 -2
- package/test/assistantResource.test.js +4 -4
- package/test/assistantSettingsResource.test.js +2 -2
- package/test/claudeConversationSession.test.js +11 -1
- package/test/claudeTurn.test.js +35 -7
- package/test/codexClient.test.js +20 -0
- package/test/codexConversationSession.test.js +2 -2
- package/test/codexConversationsCheckpoint.test.js +51 -0
- package/test/codexModelCatalog.test.js +1 -1
- package/test/codexProviderHome.test.js +51 -0
- package/test/codexProviderSelection.test.js +71 -0
- package/test/codexTurn.test.js +222 -6
- package/test/conversationAgentRun.test.js +2 -1
- package/test/conversationAttachments.test.js +27 -2
- package/test/conversationDeliveryTiming.test.js +21 -0
- package/test/conversationNormalize.test.js +11 -0
- package/test/fixtures/nestedListQuestionMessage.md +37 -0
- package/test/frameworkActions.test.js +2 -2
- package/test/longTextBlocks.test.js +102 -0
- package/test/messageDelivery.test.js +7 -1
- package/test/openCodeClient.test.js +20 -0
- package/test/openCodeRuntime.test.js +1 -1
- package/test/openCodeTurn.test.js +90 -0
- package/test/opencodeConversationSession.test.js +1 -1
- package/test/serviceToolCatalog.test.js +27 -0
|
@@ -5,18 +5,30 @@ import { readClaudeJsonFrames } from "./claudeClient.js";
|
|
|
5
5
|
import { createNativeHistoryExport, retireNativeConversation } from "./nativeHistoryExport.js";
|
|
6
6
|
|
|
7
7
|
const UUID_PATTERN = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iu;
|
|
8
|
+
const TRANSCRIPT_PATH_PATTERN = /\.jsonl(?:\.(?:superseded|orphaned)-.*)?$/u;
|
|
8
9
|
|
|
9
10
|
function requireClaudeSessionId(id) {
|
|
10
11
|
if (!UUID_PATTERN.test(id)) throw new TypeError("Invalid Claude conversation id.");
|
|
11
12
|
return id;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// Claude's project directory name for a canonical working directory.
|
|
16
|
+
function claudeProjectDirectoryName(canonical) {
|
|
17
|
+
return canonical.normalize("NFC").replace(/[^a-zA-Z0-9]/gu, "-");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Text the person authored in a user frame: plain content or its text blocks.
|
|
21
|
+
function claudeUserText(content) {
|
|
22
|
+
return typeof content === "string" ? content : (Array.isArray(content) ? content : [])
|
|
23
|
+
.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
async function archivedClaudeProjectDirectory(workdir) {
|
|
15
27
|
const canonical = await realpath(workdir).catch((error) => {
|
|
16
28
|
if (error.code === "ENOENT") return workdir;
|
|
17
29
|
throw error;
|
|
18
30
|
});
|
|
19
|
-
return canonical
|
|
31
|
+
return claudeProjectDirectoryName(canonical);
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
function matchingClaudeProjectDirectories(names, directory) {
|
|
@@ -37,7 +49,7 @@ export async function listClaudeConversationStorage({ configRoot, binding }) {
|
|
|
37
49
|
const entries = await readdir(path.join(projects, name), { withFileTypes: true });
|
|
38
50
|
if (entries.length > 10000) throw new Error("Claude conversation inventory exceeds its limit.");
|
|
39
51
|
for (const entry of entries) {
|
|
40
|
-
const id = entry.name.replace(
|
|
52
|
+
const id = entry.name.replace(TRANSCRIPT_PATH_PATTERN, "");
|
|
41
53
|
if (UUID_PATTERN.test(id) && entry.isFile()) ids.add(id);
|
|
42
54
|
}
|
|
43
55
|
}
|
|
@@ -49,14 +61,13 @@ export async function listClaudeConversationStorage({ configRoot, binding }) {
|
|
|
49
61
|
// lookup because its native and Node builds use different hash suffixes.
|
|
50
62
|
async function claudeHistoryPath({ configRoot, workdir, conversationId }) {
|
|
51
63
|
requireClaudeSessionId(conversationId);
|
|
52
|
-
const
|
|
53
|
-
const directory = canonical.replace(/[^a-zA-Z0-9]/gu, "-");
|
|
64
|
+
const directory = claudeProjectDirectoryName(await realpath(workdir));
|
|
54
65
|
const projects = path.join(configRoot, "projects");
|
|
55
|
-
const candidates = directory.length <= 200 ? [directory] :
|
|
56
|
-
|
|
66
|
+
const candidates = directory.length <= 200 ? [directory] : matchingClaudeProjectDirectories(
|
|
67
|
+
await readdir(projects).catch((error) => {
|
|
57
68
|
if (error.code === "ENOENT") return [];
|
|
58
69
|
throw error;
|
|
59
|
-
})
|
|
70
|
+
}), directory);
|
|
60
71
|
for (const name of candidates) {
|
|
61
72
|
const candidate = path.join(projects, name, `${conversationId}.jsonl`);
|
|
62
73
|
const info = await stat(candidate).catch((error) => {
|
|
@@ -132,6 +143,19 @@ function claudeHistoryFrames(file) {
|
|
|
132
143
|
return reading;
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
// The offset just after the last newline between start and size.
|
|
147
|
+
async function completeHistoryOffset(handle, start, size) {
|
|
148
|
+
const block = Buffer.alloc(64 * 1024);
|
|
149
|
+
for (let end = size; end > start;) {
|
|
150
|
+
const from = Math.max(start, end - block.length);
|
|
151
|
+
await handle.read(block, 0, end - from, from);
|
|
152
|
+
const newline = block.subarray(0, end - from).lastIndexOf(10);
|
|
153
|
+
if (newline !== -1) return from + newline + 1;
|
|
154
|
+
end = from;
|
|
155
|
+
}
|
|
156
|
+
return start;
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
async function readClaudeHistoryFrames(file) {
|
|
136
160
|
const cached = historyCache.get(file.path)?.value;
|
|
137
161
|
const handle = await open(file.path, "r");
|
|
@@ -143,18 +167,16 @@ async function readClaudeHistoryFrames(file) {
|
|
|
143
167
|
reusable = check.equals(cached.check);
|
|
144
168
|
}
|
|
145
169
|
const start = reusable ? cached.offset : 0;
|
|
146
|
-
const appended = Buffer.alloc(file.size - start);
|
|
147
|
-
if (appended.length) await handle.read(appended, 0, appended.length, start);
|
|
148
170
|
// Only newline-terminated frames are complete; a writer may still be
|
|
149
171
|
// writing the unfinished tail, which the next read parses.
|
|
150
|
-
const
|
|
172
|
+
const offset = await completeHistoryOffset(handle, start, file.size);
|
|
151
173
|
const added = [];
|
|
152
|
-
if (
|
|
153
|
-
|
|
174
|
+
if (offset > start) {
|
|
175
|
+
// Streamed, so a long conversation's history is never held whole in memory.
|
|
176
|
+
for await (const frame of readClaudeJsonFrames(handle.createReadStream({ start, end: offset - 1, autoClose: false }))) {
|
|
154
177
|
if (!frame.isSidechain) added.push(retainedClaudeFrame(frame));
|
|
155
178
|
}
|
|
156
179
|
}
|
|
157
|
-
const offset = start + complete;
|
|
158
180
|
const check = Buffer.alloc(Math.min(HISTORY_CHECK_BYTES, offset));
|
|
159
181
|
await handle.read(check, 0, check.length, offset - check.length);
|
|
160
182
|
const value = { dev: file.dev, ino: file.ino, offset, check,
|
|
@@ -179,7 +201,6 @@ async function readClaudeHistory(options) {
|
|
|
179
201
|
...(options.includeFinalCarriers ? { completedResults: [], failedResults: [], structuredOutputs: [] } : {})
|
|
180
202
|
};
|
|
181
203
|
if (!file || file.size === 0) return { exists: false, messages: [], userIds: [], text: "", ...inspected };
|
|
182
|
-
if (file.size > 256 * 1024 * 1024) throw new Error("Claude history exceeds the supported read limit.");
|
|
183
204
|
const messages = new Map();
|
|
184
205
|
const userIds = [];
|
|
185
206
|
let lastUserId = "";
|
|
@@ -283,10 +304,7 @@ async function readClaudeHistory(options) {
|
|
|
283
304
|
userIds.push(frame.uuid);
|
|
284
305
|
lastUserId = frame.uuid;
|
|
285
306
|
if (options.includeUserMessages && !frame.parent_tool_use_id) {
|
|
286
|
-
|
|
287
|
-
const text = typeof content === "string" ? content : content
|
|
288
|
-
.filter(block => block.type === "text" && typeof block.text === "string").map(block => block.text).join("\n\n");
|
|
289
|
-
inspected.userMessages.push({ id: frame.uuid, text });
|
|
307
|
+
inspected.userMessages.push({ id: frame.uuid, text: claudeUserText(frame.message.content) });
|
|
290
308
|
}
|
|
291
309
|
}
|
|
292
310
|
if (options.includeFinalCarriers && !frame.parent_tool_use_id && !frame.isMeta) {
|
|
@@ -333,8 +351,6 @@ export async function deleteClaudeConversationHistory({ configRoot, workdir, con
|
|
|
333
351
|
|
|
334
352
|
export { claudeHistoryPath, claudeMessageBlocks, readClaudeHistory, requireClaudeSessionId, archivedClaudeProjectDirectory, matchingClaudeProjectDirectories };
|
|
335
353
|
|
|
336
|
-
const TRANSCRIPT_PATH_PATTERN = /\.jsonl(?:\.(?:superseded|orphaned)-.*)?$/u;
|
|
337
|
-
|
|
338
354
|
export async function retireClaudeConversationHistory({ configRoot, binding, beforeDelete, requireIdle, signal }) {
|
|
339
355
|
const id = requireClaudeSessionId(binding.conversationId);
|
|
340
356
|
if (!path.isAbsolute(configRoot)) throw new TypeError("Claude retirement requires an absolute configuration root.");
|
|
@@ -411,9 +427,7 @@ export async function retireClaudeConversationHistory({ configRoot, binding, bef
|
|
|
411
427
|
.filter((message) => message.role !== "thinking")
|
|
412
428
|
.map((message) => ({ ...provenance, role: "assistant", text: message.text }));
|
|
413
429
|
if (frame.type === "user") {
|
|
414
|
-
const
|
|
415
|
-
const user = typeof content === "string" ? content : (Array.isArray(content) ? content : [])
|
|
416
|
-
.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n");
|
|
430
|
+
const user = claudeUserText(frame.message?.content);
|
|
417
431
|
if (user) text.push({ ...provenance, role: "user", text: user });
|
|
418
432
|
}
|
|
419
433
|
if (frame.attachment?.type === "goal_status" && typeof frame.attachment.condition === "string") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsonHash } from "./normalize.js";
|
|
2
2
|
import { nativeAiProvider } from "../../shared/nativeProviders.js";
|
|
3
3
|
import { Duplex } from "node:stream";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
@@ -83,7 +83,7 @@ async function readNativeClaudeCodeAuthStatus({ env, credentialHome, command, co
|
|
|
83
83
|
|
|
84
84
|
/** Exact existing native connection pin, shared with stopped metadata conversion. */
|
|
85
85
|
export function claudeConnectionIdentity(configRoot, providerId, baseUrl, apiKey) {
|
|
86
|
-
return
|
|
86
|
+
return jsonHash([configRoot, providerId, baseUrl, apiKey]);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** Native provider routing; the application supplies only its authorized selection. */
|
|
@@ -5,13 +5,14 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
5
5
|
import { claudeMessageBlocks, deleteClaudeConversationHistory, readClaudeHistory, requireClaudeSessionId, retireClaudeConversationHistory } from "./claudeHistory.js";
|
|
6
6
|
import { bindClaudeConversationAccount, claudeCodeArguments, createClaudeAccountQueries, createClaudeCodeProcess, stopClaudeCodeProcess } from "./claudeProcess.js";
|
|
7
7
|
import { claudeApplicationToolResponse } from "./claudeTools.js";
|
|
8
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
8
9
|
import { createClaudeConversationAdapter } from "./providers/claude.js";
|
|
9
10
|
|
|
10
11
|
function failure(message, code = "assistant_claude_turn_failed") {
|
|
11
12
|
return Object.assign(new Error(message), { code });
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
const text =
|
|
15
|
+
const text = normalizeNullableText;
|
|
15
16
|
const now = () => new Date().toISOString();
|
|
16
17
|
|
|
17
18
|
function hasPendingClaudeStartup(entry) {
|
|
@@ -166,6 +167,11 @@ export function createClaudeConversationOwner({
|
|
|
166
167
|
let closing = false;
|
|
167
168
|
const createProcess = process.create || createClaudeCodeProcess;
|
|
168
169
|
const saveAccount = store.save;
|
|
170
|
+
// Two hosts share this owner. A bound store (one with releaseExecution) is a
|
|
171
|
+
// runtime binding: the native turn owns activity and execution release.
|
|
172
|
+
// Otherwise the host retains entries and their original checkpoint order.
|
|
173
|
+
const bound = Boolean(store.releaseExecution);
|
|
174
|
+
const isTurnActive = entry => bound ? entry.nativeTurn.read().active : Boolean(entry.turn?.active);
|
|
169
175
|
|
|
170
176
|
function createAccountQueries({ accountIdentity, createProcess }) {
|
|
171
177
|
return createClaudeAccountQueries({ accountIdentity,
|
|
@@ -249,13 +255,13 @@ export function createClaudeConversationOwner({
|
|
|
249
255
|
}
|
|
250
256
|
}
|
|
251
257
|
if (entry.pausedGoal && entry.blockedGoal) throw createError("The saved Claude goal cannot be both paused and blocked.");
|
|
252
|
-
entry.nativeTurn = createClaudeConversationTurn({ conversationId: id, entry,
|
|
258
|
+
entry.nativeTurn = createClaudeConversationTurn({ conversationId: id, entry, bound,
|
|
253
259
|
onEvent: event => onEvent(entry, event),
|
|
254
260
|
process: {
|
|
255
261
|
stopExecution: process.stopExecution,
|
|
256
|
-
...(
|
|
262
|
+
...(bound ? { releaseExecution: () => store.releaseExecution(entry) } : {}),
|
|
257
263
|
getProcess: () => entry.stopping ? null : entry.process,
|
|
258
|
-
isActive: () =>
|
|
264
|
+
isActive: () => isTurnActive(entry),
|
|
259
265
|
startProcess: input => startProcess(entry, input),
|
|
260
266
|
stopProcess: reason => entry.process || entry.executionId
|
|
261
267
|
? entry.nativeTurn.stopProcess(reason, { preparing: true }) : Promise.resolve()
|
|
@@ -271,7 +277,7 @@ export function createClaudeConversationOwner({
|
|
|
271
277
|
try { await store.save(entry); }
|
|
272
278
|
catch (error) { Object.assign(entry, previous); throw error; }
|
|
273
279
|
},
|
|
274
|
-
readState: () => ({ running: Boolean(entry.process), active:
|
|
280
|
+
readState: () => ({ running: Boolean(entry.process), active: isTurnActive(entry) }),
|
|
275
281
|
interrupt: () => interrupt(entry), send: input => send(entry, input) });
|
|
276
282
|
entries.set(key, entry);
|
|
277
283
|
await store.save(entry);
|
|
@@ -312,7 +318,7 @@ export function createClaudeConversationOwner({
|
|
|
312
318
|
async function ensureReady(entry) {
|
|
313
319
|
if (entry.process) {
|
|
314
320
|
// Status reads retain active instructions; apply changes at idle admission.
|
|
315
|
-
if (
|
|
321
|
+
if (isTurnActive(entry)) await bindAccount(entry);
|
|
316
322
|
else await ensureProcess(entry);
|
|
317
323
|
} else {
|
|
318
324
|
await bindAccount(entry);
|
|
@@ -441,10 +447,10 @@ export function createClaudeConversationOwner({
|
|
|
441
447
|
}
|
|
442
448
|
await entry.nativeTurn.receive(frame);
|
|
443
449
|
},
|
|
444
|
-
...(
|
|
450
|
+
...(bound || options.applicationTools || entry.command?.tools ? {
|
|
445
451
|
async onControlRequest(request, { signal }) {
|
|
446
452
|
const active = entry.command;
|
|
447
|
-
const suppliedTools = !
|
|
453
|
+
const suppliedTools = !bound && active?.tools;
|
|
448
454
|
const owner = active?.nativeOwner;
|
|
449
455
|
const work = applicationCall.run({ entry, command: active, owner,
|
|
450
456
|
toolUseId: request.message?.params?._meta?.["claudecode/toolUseId"] },
|
|
@@ -471,7 +477,7 @@ export function createClaudeConversationOwner({
|
|
|
471
477
|
} : {}),
|
|
472
478
|
onFailure: async error => {
|
|
473
479
|
if (entry.localCommand?.processToken === processToken) entry.localCommand.reject(error);
|
|
474
|
-
if (
|
|
480
|
+
if (bound) {
|
|
475
481
|
entry.nativeTurn.invalidateInstructions();
|
|
476
482
|
try { await entry.nativeTurn.stopProcess(error.message); }
|
|
477
483
|
catch (cleanupError) { error = cleanupError; await context.reportFailure(cleanupError); }
|
|
@@ -492,7 +498,7 @@ export function createClaudeConversationOwner({
|
|
|
492
498
|
await onEvent(entry, { type: "execution", context, executionId: entry.executionId });
|
|
493
499
|
} catch (bindingError) { error.bindingError = bindingError; }
|
|
494
500
|
}
|
|
495
|
-
if (
|
|
501
|
+
if (bound && error.stopProof?.scopeEmpty) {
|
|
496
502
|
await store.releaseExecution(entry);
|
|
497
503
|
if (entry.startupCleanup?.executionId === error.executionId) entry.startupCleanup = null;
|
|
498
504
|
}
|
|
@@ -960,7 +966,7 @@ export function createClaudeConversationOwner({
|
|
|
960
966
|
}
|
|
961
967
|
|
|
962
968
|
async function cancel(control, { context, acquire, reportFailure, entry }) {
|
|
963
|
-
if (
|
|
969
|
+
if (bound) {
|
|
964
970
|
await entry.nativeTurn.stopProcess("Work stopped.");
|
|
965
971
|
return;
|
|
966
972
|
}
|
|
@@ -981,7 +987,7 @@ export function createClaudeConversationOwner({
|
|
|
981
987
|
}
|
|
982
988
|
|
|
983
989
|
async function dispatch(control, command, { acquire, entry: selectedEntry, dispatch: nativeDispatch }) {
|
|
984
|
-
if (
|
|
990
|
+
if (bound) {
|
|
985
991
|
await send(selectedEntry, command.input, { command: nativeDispatch });
|
|
986
992
|
return;
|
|
987
993
|
}
|
|
@@ -1009,14 +1015,13 @@ export function createClaudeConversationOwner({
|
|
|
1009
1015
|
}
|
|
1010
1016
|
|
|
1011
1017
|
function steer(control, command, options) {
|
|
1012
|
-
if (!
|
|
1018
|
+
if (!bound) return dispatch(control, command, options).then(delivered => ({ value: delivered.value }));
|
|
1013
1019
|
if (!control.current?.steer || !control.current.entry.nativeTurn.read().active) throw new Error("Claude has no active turn to steer.");
|
|
1014
1020
|
return control.current.steer(command);
|
|
1015
1021
|
}
|
|
1016
1022
|
|
|
1017
1023
|
async function run(control, command, { acquire, reportFailure, entry: selectedEntry, maximumOutput, maximumToolCalls = 32, timeoutMs } = {}) {
|
|
1018
1024
|
if (control.disposed || control.current) throw new Error("This Claude conversation is closed or already working.");
|
|
1019
|
-
const binding = Boolean(store.releaseExecution);
|
|
1020
1025
|
const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent, onNativeToolUse } = command;
|
|
1021
1026
|
const completion = Promise.withResolvers();
|
|
1022
1027
|
completion.promise.catch(() => {});
|
|
@@ -1047,7 +1052,7 @@ export function createClaudeConversationOwner({
|
|
|
1047
1052
|
let delivered;
|
|
1048
1053
|
function abort() {
|
|
1049
1054
|
if (cancellation) return;
|
|
1050
|
-
if (
|
|
1055
|
+
if (bound) {
|
|
1051
1056
|
cancellation = entry.nativeTurn.cancel(entry.process?.client, () => entry.nativeTurn.stopProcess("Work stopped."));
|
|
1052
1057
|
cancellation.then(() => completion.reject(signal.reason), error => completion.reject(error));
|
|
1053
1058
|
} else {
|
|
@@ -1056,7 +1061,7 @@ export function createClaudeConversationOwner({
|
|
|
1056
1061
|
}
|
|
1057
1062
|
}
|
|
1058
1063
|
try {
|
|
1059
|
-
if (
|
|
1064
|
+
if (bound) {
|
|
1060
1065
|
entry = selectedEntry;
|
|
1061
1066
|
if (entry.disposed || entry.command) throw new Error("This Claude conversation is closed or already working.");
|
|
1062
1067
|
active.entry = entry;
|
|
@@ -1070,7 +1075,7 @@ export function createClaudeConversationOwner({
|
|
|
1070
1075
|
let dispatchFailure;
|
|
1071
1076
|
try {
|
|
1072
1077
|
delivered = await dispatch(control, command, { acquire, entry, dispatch: nativeDispatch });
|
|
1073
|
-
if (
|
|
1078
|
+
if (bound) ready.resolve();
|
|
1074
1079
|
} catch (error) { dispatchFailure = error; }
|
|
1075
1080
|
for (;;) {
|
|
1076
1081
|
const settling = active.delivery;
|
|
@@ -1086,15 +1091,15 @@ export function createClaudeConversationOwner({
|
|
|
1086
1091
|
catch (error) { problem = error; }
|
|
1087
1092
|
}
|
|
1088
1093
|
problem = dispatchFailure || failure || problem;
|
|
1089
|
-
if (!problem && settling?.completion && (
|
|
1090
|
-
problem = Object.assign(new Error(result.error || (
|
|
1094
|
+
if (!problem && settling?.completion && (bound ? result.status !== "completed" : !result.ok)) {
|
|
1095
|
+
problem = Object.assign(new Error(result.error || (bound ? "Claude interrupted this answer." : `Claude turn ${result.status}.`)), { status: result.status });
|
|
1091
1096
|
}
|
|
1092
1097
|
break;
|
|
1093
1098
|
}
|
|
1094
1099
|
} catch (error) { problem = error; }
|
|
1095
1100
|
ready.reject(problem || new Error("Claude work ended."));
|
|
1096
1101
|
signal.removeEventListener("abort", abort);
|
|
1097
|
-
if (
|
|
1102
|
+
if (bound) {
|
|
1098
1103
|
try {
|
|
1099
1104
|
if (problem || signal.aborted) await (cancellation || entry.nativeTurn.stopProcess(problem?.message || "Work stopped."));
|
|
1100
1105
|
} catch (error) {
|
|
@@ -1126,10 +1131,10 @@ export function createClaudeConversationOwner({
|
|
|
1126
1131
|
}
|
|
1127
1132
|
if (control.current === active) control.current = null;
|
|
1128
1133
|
if (problem) {
|
|
1129
|
-
if (
|
|
1134
|
+
if (bound && nativeDispatch.attempted && !nativeDispatch.admitted && problem.delivery !== "not-sent") problem.delivery = "uncertain";
|
|
1130
1135
|
throw problem;
|
|
1131
1136
|
}
|
|
1132
|
-
if (!
|
|
1137
|
+
if (!bound) return { value: delivered.value };
|
|
1133
1138
|
}
|
|
1134
1139
|
|
|
1135
1140
|
return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, readContextUsage, compactContext, releaseRetainedGoal, stopForTerminal, prepareTerminal, send, startTurn,
|
|
@@ -1138,7 +1143,7 @@ export function createClaudeConversationOwner({
|
|
|
1138
1143
|
}
|
|
1139
1144
|
|
|
1140
1145
|
/** Native turn handling moved from the production session provider; hosts persist its events. */
|
|
1141
|
-
export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry } = {}) {
|
|
1146
|
+
export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry, bound = false } = {}) {
|
|
1142
1147
|
if (!conversationId) throw new TypeError("A Claude conversation id is required.");
|
|
1143
1148
|
const instructions = process ? createClaudeConversationAdapter(process) : null;
|
|
1144
1149
|
const publish = entry ? publishEntryEvent : onEvent;
|
|
@@ -1249,31 +1254,30 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1249
1254
|
invalidateFinalAssistantResult();
|
|
1250
1255
|
// Runtime bindings commit execution release before dropping the handle.
|
|
1251
1256
|
// Retained host entries keep their original post-proof checkpoint order.
|
|
1252
|
-
const binding = Boolean(process.releaseExecution);
|
|
1253
1257
|
if (entry.stopPending) return entry.stopPending;
|
|
1254
1258
|
const active = read().active;
|
|
1255
|
-
if (!
|
|
1259
|
+
if (!bound) entry.stopping = true;
|
|
1256
1260
|
const operation = async () => {
|
|
1257
1261
|
const startup = entry.startupCleanup?.executionId === entry.executionId ? entry.startupCleanup : null;
|
|
1258
1262
|
let proof;
|
|
1259
1263
|
try {
|
|
1260
|
-
if (!
|
|
1264
|
+
if (!bound || entry.process || entry.executionId) {
|
|
1261
1265
|
proof = await stopClaudeCodeProcess({ process: entry.process || startup, executionId: entry.executionId,
|
|
1262
1266
|
stopExecution: process.stopExecution });
|
|
1263
1267
|
}
|
|
1264
1268
|
} catch (error) {
|
|
1265
|
-
if (
|
|
1269
|
+
if (bound || error.code !== "claude_stop_unconfirmed") throw error;
|
|
1266
1270
|
entry.observationError = reason || "Claude process exit has not been confirmed. Try Stop again.";
|
|
1267
1271
|
await onEvent({ type: "save" });
|
|
1268
1272
|
await onEvent({ type: "stop-unconfirmed" });
|
|
1269
1273
|
throw error;
|
|
1270
1274
|
}
|
|
1271
|
-
if (
|
|
1275
|
+
if (bound && (entry.process || entry.executionId)) await process.releaseExecution();
|
|
1272
1276
|
entry.process = null;
|
|
1273
1277
|
entry.executionId = "";
|
|
1274
1278
|
entry.exitProof = proof;
|
|
1275
|
-
stop(
|
|
1276
|
-
if (
|
|
1279
|
+
stop(bound ? reason : reason || "Claude stopped before acknowledging the prompt.");
|
|
1280
|
+
if (bound) {
|
|
1277
1281
|
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
1278
1282
|
if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
|
|
1279
1283
|
return;
|
|
@@ -1295,7 +1299,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1295
1299
|
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
1296
1300
|
return entry.exitProof;
|
|
1297
1301
|
};
|
|
1298
|
-
entry.stopPending = operation().catch(error => { if (
|
|
1302
|
+
entry.stopPending = operation().catch(error => { if (bound) error.cleanupFailed = true; throw error; })
|
|
1299
1303
|
.finally(() => { entry.stopPending = null; });
|
|
1300
1304
|
return entry.stopPending;
|
|
1301
1305
|
}
|
|
@@ -1303,9 +1307,8 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1303
1307
|
async function publishEntryEvent(event, deferred = false) {
|
|
1304
1308
|
if (entry.stopping) return;
|
|
1305
1309
|
if (event.type === "settled" && event.generation !== generation) return;
|
|
1306
|
-
const binding = Boolean(process.releaseExecution);
|
|
1307
1310
|
const command = entry.command;
|
|
1308
|
-
if (event.type === "settled" && !
|
|
1311
|
+
if (event.type === "settled" && !bound && !deferred && command?.finishIfCurrent && command !== checkpointedCommand) {
|
|
1309
1312
|
const delivery = command.delivery;
|
|
1310
1313
|
const completion = entry.completion;
|
|
1311
1314
|
const owner = acceptedInput;
|
|
@@ -1336,17 +1339,17 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1336
1339
|
}
|
|
1337
1340
|
if (event.type === "continued") {
|
|
1338
1341
|
if (entry.completion === resolvedCompletion) entry.completion = Promise.withResolvers();
|
|
1339
|
-
if (
|
|
1342
|
+
if (bound) setState("active");
|
|
1340
1343
|
else await updateState("active");
|
|
1341
1344
|
} else if (event.type === "admitted") {
|
|
1342
1345
|
if (event.userEcho) entry.sent = true;
|
|
1343
|
-
if (!
|
|
1346
|
+
if (!bound) await onEvent({ type: "save" });
|
|
1344
1347
|
} else if (event.type === "phase") {
|
|
1345
1348
|
entry.turn.phase = event.phase;
|
|
1346
|
-
if (!
|
|
1349
|
+
if (!bound) await updateState(entry.turn.state);
|
|
1347
1350
|
} else if (event.type === "settled") {
|
|
1348
1351
|
const state = event.status === "interrupted" ? "interrupted" : event.status === "failed" ? "failed" : "completed";
|
|
1349
|
-
if (
|
|
1352
|
+
if (bound) setState(state, event.error);
|
|
1350
1353
|
else if (!await updateState(state, event.error, event.generation)) return;
|
|
1351
1354
|
}
|
|
1352
1355
|
const published = await onEvent(event);
|
|
@@ -1358,7 +1361,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1358
1361
|
if (event.type === "settled") {
|
|
1359
1362
|
if (event.generation !== generation) return;
|
|
1360
1363
|
if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
|
|
1361
|
-
resolveCompletion(
|
|
1364
|
+
resolveCompletion(bound ? event : readResult());
|
|
1362
1365
|
}
|
|
1363
1366
|
return published;
|
|
1364
1367
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { codexAuthOutputRequiresReconnect } from "./codexConfiguration.js";
|
|
3
|
+
import {
|
|
4
|
+
assertExistingDirectory, codexAppServerCommandBaseEnv, codexAppServerCredentialHome,
|
|
5
|
+
codexAppServerProcessCwd, ensureWritablePrivateDirectory
|
|
6
|
+
} from "./codexProcess.js";
|
|
7
|
+
import { normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
|
|
8
|
+
import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
|
|
9
|
+
|
|
10
|
+
// Codex auth preflight: one bounded `codex debug models` run whose
|
|
11
|
+
// reconnect-required output marks the stored credentials invalid.
|
|
12
|
+
const CODEX_AUTH_PREFLIGHT_TIMEOUT_MS = 15000;
|
|
13
|
+
const CODEX_AUTH_PREFLIGHT_OUTPUT_TAIL_BYTES = 4096;
|
|
14
|
+
|
|
15
|
+
function tailAppend(text = "", chunk = "", maxBytes = CODEX_AUTH_PREFLIGHT_OUTPUT_TAIL_BYTES) {
|
|
16
|
+
const next = `${String(text || "")}${String(chunk || "")}`;
|
|
17
|
+
return next.length > maxBytes ? next.slice(-maxBytes) : next;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function codexAuthPreflightArgs() {
|
|
21
|
+
return [
|
|
22
|
+
"-c",
|
|
23
|
+
"check_for_update_on_startup=false",
|
|
24
|
+
"debug",
|
|
25
|
+
"models"
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function runCodexAuthPreflight({
|
|
30
|
+
codexCommand = "codex",
|
|
31
|
+
env = process.env,
|
|
32
|
+
executionRoot = "",
|
|
33
|
+
runtimeDir = "",
|
|
34
|
+
terminalEnv = {},
|
|
35
|
+
timeoutMs = CODEX_AUTH_PREFLIGHT_TIMEOUT_MS,
|
|
36
|
+
toolHomeSource = "",
|
|
37
|
+
workdir = ""
|
|
38
|
+
} = {}, execution) {
|
|
39
|
+
const normalizedToolHomeSource = normalizeAgentText(toolHomeSource);
|
|
40
|
+
if (normalizedToolHomeSource) {
|
|
41
|
+
await assertExistingDirectory(normalizedToolHomeSource, "Codex credential home");
|
|
42
|
+
}
|
|
43
|
+
const normalizedRuntimeDir = normalizeAgentText(runtimeDir);
|
|
44
|
+
if (normalizedRuntimeDir) {
|
|
45
|
+
await ensureWritablePrivateDirectory(path.resolve(normalizedRuntimeDir));
|
|
46
|
+
}
|
|
47
|
+
const baseEnv = codexAppServerCommandBaseEnv({
|
|
48
|
+
env,
|
|
49
|
+
terminalEnv
|
|
50
|
+
});
|
|
51
|
+
const processCwd = codexAppServerProcessCwd({
|
|
52
|
+
executionRoot,
|
|
53
|
+
runtimeDir,
|
|
54
|
+
workdir
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const result = await execution.run({
|
|
58
|
+
args: codexAuthPreflightArgs(),
|
|
59
|
+
baseEnv,
|
|
60
|
+
command: codexCommand,
|
|
61
|
+
credentialHome: codexAppServerCredentialHome(normalizedToolHomeSource, baseEnv),
|
|
62
|
+
cwd: processCwd,
|
|
63
|
+
inheritProcessEnv: false,
|
|
64
|
+
mode: "capture",
|
|
65
|
+
timeout: normalizePositiveInteger(timeoutMs, { fallback: CODEX_AUTH_PREFLIGHT_TIMEOUT_MS })
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
code: result.exitCode,
|
|
69
|
+
ok: result.ok === true,
|
|
70
|
+
output: tailAppend("", result.output || [
|
|
71
|
+
result.stderr,
|
|
72
|
+
result.stdout
|
|
73
|
+
].filter(Boolean).join("\n")),
|
|
74
|
+
signal: result.signal,
|
|
75
|
+
timedOut: result.timedOut === true
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
error,
|
|
80
|
+
ok: false,
|
|
81
|
+
output: normalizeAgentText(error?.message || error)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function assertCodexAuthPreflightReady(options = {}, {
|
|
87
|
+
reason = "codex-auth-preflight", execution, credentials
|
|
88
|
+
} = {}) {
|
|
89
|
+
const result = await runCodexAuthPreflight(options, execution);
|
|
90
|
+
if (result.ok && !codexAuthOutputRequiresReconnect(result.output)) {
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
const observed = normalizeAgentText(result.output || result.error?.message || "Codex auth preflight failed.");
|
|
94
|
+
if (codexAuthOutputRequiresReconnect(observed)) {
|
|
95
|
+
await credentials.markInvalid( {
|
|
96
|
+
reason
|
|
97
|
+
});
|
|
98
|
+
throw credentials.reconnectError({
|
|
99
|
+
modelProviderId: "",
|
|
100
|
+
observed
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
throw new Error(observed || "Codex auth preflight failed.");
|
|
104
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { createConnection } from "node:net";
|
|
2
2
|
import WebSocket from "ws";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
|
|
4
5
|
const CODEX_APP_SERVER_REQUEST_TIMEOUT_MS = 60_000;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
function messageTooLargeError() {
|
|
8
|
+
const error = new Error("Codex app-server message exceeded the configured transport limit.");
|
|
9
|
+
error.code = "assistant_codex_app_server_message_too_large";
|
|
10
|
+
return error;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
function socketPathFromCodexAppServerEndpoint(endpoint = "") {
|
|
@@ -56,10 +59,8 @@ class CodexAppServerJsonRpcClient {
|
|
|
56
59
|
WebSocketImpl = WebSocket
|
|
57
60
|
} = {}) {
|
|
58
61
|
this.endpoint = normalizeAgentText(endpoint);
|
|
59
|
-
this.maxMessageBytes =
|
|
60
|
-
|
|
61
|
-
: Number.POSITIVE_INFINITY;
|
|
62
|
-
this.requestTimeoutMs = normalizePositiveInteger(requestTimeoutMs, CODEX_APP_SERVER_REQUEST_TIMEOUT_MS);
|
|
62
|
+
this.maxMessageBytes = normalizePositiveInteger(maxMessageBytes, { fallback: Number.POSITIVE_INFINITY });
|
|
63
|
+
this.requestTimeoutMs = normalizePositiveInteger(requestTimeoutMs, { fallback: CODEX_APP_SERVER_REQUEST_TIMEOUT_MS });
|
|
63
64
|
this.WebSocketImpl = WebSocketImpl;
|
|
64
65
|
this.onDisconnect = onDisconnect;
|
|
65
66
|
this.nextRequestId = 1;
|
|
@@ -233,11 +234,7 @@ class CodexAppServerJsonRpcClient {
|
|
|
233
234
|
throw new Error("Codex app-server connection is not open.");
|
|
234
235
|
}
|
|
235
236
|
const serialized = JSON.stringify(payload);
|
|
236
|
-
if (Buffer.byteLength(serialized, "utf8") > this.maxMessageBytes)
|
|
237
|
-
const error = new Error("Codex app-server message exceeded the configured transport limit.");
|
|
238
|
-
error.code = "assistant_codex_app_server_message_too_large";
|
|
239
|
-
throw error;
|
|
240
|
-
}
|
|
237
|
+
if (Buffer.byteLength(serialized, "utf8") > this.maxMessageBytes) throw messageTooLargeError();
|
|
241
238
|
this.socket.send(serialized);
|
|
242
239
|
}
|
|
243
240
|
|
|
@@ -246,9 +243,7 @@ class CodexAppServerJsonRpcClient {
|
|
|
246
243
|
try {
|
|
247
244
|
const text = socketMessageText(event, this.maxMessageBytes);
|
|
248
245
|
if (text === null) {
|
|
249
|
-
|
|
250
|
-
error.code = "assistant_codex_app_server_message_too_large";
|
|
251
|
-
this.disconnect(error);
|
|
246
|
+
this.disconnect(messageTooLargeError());
|
|
252
247
|
return;
|
|
253
248
|
}
|
|
254
249
|
message = JSON.parse(text);
|