@jskit-ai/assistant-core 0.1.239 → 0.1.241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- package/src/client/conversation/AssistantTranscript.vue +26 -29
- package/src/client/conversation/LongTextPreviewBlocks.vue +11 -0
- package/src/client/conversation/messageDelivery.js +8 -2
- package/src/client/conversation/useAssistantAttachments.js +1 -1
- package/src/server/conversation/agentRun.js +12 -198
- package/src/server/conversation/attachments.js +6 -5
- package/src/server/conversation/boundedToolLoop.js +160 -0
- package/src/server/conversation/claudeHistory.js +20 -16
- package/src/server/conversation/claudeProcess.js +2 -2
- package/src/server/conversation/claudeTurn.js +51 -40
- package/src/server/conversation/codexAuthPreflight.js +104 -0
- package/src/server/conversation/codexClient.js +11 -16
- package/src/server/conversation/codexConfiguration.js +112 -98
- package/src/server/conversation/codexConversationStore.js +189 -0
- package/src/server/conversation/codexConversations.js +47 -44
- package/src/server/conversation/codexDetachedTurn.js +12 -11
- package/src/server/conversation/codexEvents.js +95 -99
- package/src/server/conversation/codexGoalCommands.js +1 -1
- package/src/server/conversation/codexHelperExecution.js +3 -13
- package/src/server/conversation/codexHelperThreadLedger.js +7 -13
- package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
- package/src/server/conversation/codexHelperThreads.js +5 -6
- package/src/server/conversation/codexHistoryAdapter.js +32 -26
- package/src/server/conversation/codexMessageCommands.js +22 -22
- package/src/server/conversation/codexMessageDelivery.js +12 -7
- package/src/server/conversation/codexModelCatalog.js +97 -0
- package/src/server/conversation/codexNotificationObserver.js +24 -40
- package/src/server/conversation/codexNotificationQueue.js +2 -3
- package/src/server/conversation/codexOutput.js +20 -27
- package/src/server/conversation/codexProcess.js +123 -127
- package/src/server/conversation/codexProcessLeader.js +3 -3
- package/src/server/conversation/codexProvider.js +31 -614
- package/src/server/conversation/codexProviderOwner.js +2 -2
- package/src/server/conversation/codexProviderSelection.js +38 -40
- package/src/server/conversation/codexRenewalCommands.js +13 -10
- package/src/server/conversation/codexRenewalThreads.js +412 -0
- package/src/server/conversation/codexRunJournal.js +24 -42
- package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
- package/src/server/conversation/codexSessionCleanup.js +11 -10
- package/src/server/conversation/codexSessionReconciliation.js +6 -6
- package/src/server/conversation/codexThreadReadiness.js +19 -33
- package/src/server/conversation/codexThreadStatus.js +3 -3
- package/src/server/conversation/codexTurn.js +124 -297
- package/src/server/conversation/codexTurnRecovery.js +49 -28
- package/src/server/conversation/codexTurnState.js +34 -13
- package/src/server/conversation/commandWrapper.js +3 -0
- package/src/server/conversation/deliveryTiming.js +14 -5
- package/src/server/conversation/index.js +1 -0
- package/src/server/conversation/localExecution.js +13 -17
- package/src/server/conversation/normalize.js +8 -3
- package/src/server/conversation/openCodeClient.js +9 -10
- package/src/server/conversation/openCodeProcess.js +9 -5
- package/src/server/conversation/openCodeRuntime.js +10 -11
- package/src/server/conversation/openCodeTurn.js +43 -29
- package/src/server/conversation/providers/claudeDriver.js +10 -16
- package/src/server/conversation/providers/codexDriver.js +27 -43
- package/src/server/conversation/providers/opencodeDriver.js +17 -42
- package/src/server/conversation/providers/scoped.js +48 -4
- package/src/server/conversation/runtime.js +50 -49
- package/src/server/conversation/runtimeStateUpgrade.js +34 -22
- package/src/server/conversation/transcript.js +10 -10
- package/src/server/hosts/codexConfiguration.js +1 -0
- package/src/server/hosts/codexProvider.js +8 -6
- package/src/server/index.js +2 -1
- package/src/server/lib/assistantToolLoop.js +1 -153
- package/src/server/lib/serviceToolCatalog.js +17 -42
- package/src/shared/assistantResource.js +4 -2
- package/src/shared/conversation/attachments.js +4 -0
- package/src/shared/conversation/longTextBlocks.js +54 -19
- package/src/shared/conversation/numberedQuestionSugar.js +17 -10
- package/src/shared/conversation/pagination.js +4 -6
- package/src/shared/conversation/patches.js +1 -4
- package/src/shared/conversation/streaming.js +3 -11
- package/src/shared/conversation/turns.js +11 -0
- package/src/shared/index.js +1 -0
- package/test/assistantBoundedToolLoop.test.js +2 -2
- package/test/assistantResource.test.js +4 -4
- package/test/assistantScroll.browser.test.js +29 -6
- package/test/assistantSettingsResource.test.js +2 -2
- package/test/claudeConversationSession.test.js +20 -3
- package/test/claudeTurn.test.js +27 -7
- package/test/codexClient.test.js +20 -0
- package/test/codexConversationSession.test.js +5 -3
- package/test/codexConversationsCheckpoint.test.js +51 -0
- package/test/codexModelCatalog.test.js +1 -1
- package/test/codexProviderHome.test.js +51 -0
- package/test/codexProviderSelection.test.js +71 -0
- package/test/codexTurn.test.js +222 -6
- package/test/conversationAgentRun.test.js +2 -1
- package/test/conversationAttachments.test.js +27 -2
- package/test/conversationDeliveryTiming.test.js +21 -0
- package/test/conversationNormalize.test.js +11 -0
- package/test/fixtures/nestedListQuestionMessage.md +37 -0
- package/test/frameworkActions.test.js +2 -2
- package/test/longTextBlocks.test.js +102 -0
- package/test/messageDelivery.test.js +7 -1
- package/test/openCodeClient.test.js +20 -0
- package/test/openCodeRuntime.test.js +1 -1
- package/test/openCodeTurn.test.js +90 -0
- package/test/opencodeConversationSession.test.js +1 -1
- package/test/serviceToolCatalog.test.js +27 -0
|
@@ -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) => {
|
|
@@ -283,10 +294,7 @@ async function readClaudeHistory(options) {
|
|
|
283
294
|
userIds.push(frame.uuid);
|
|
284
295
|
lastUserId = frame.uuid;
|
|
285
296
|
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 });
|
|
297
|
+
inspected.userMessages.push({ id: frame.uuid, text: claudeUserText(frame.message.content) });
|
|
290
298
|
}
|
|
291
299
|
}
|
|
292
300
|
if (options.includeFinalCarriers && !frame.parent_tool_use_id && !frame.isMeta) {
|
|
@@ -333,8 +341,6 @@ export async function deleteClaudeConversationHistory({ configRoot, workdir, con
|
|
|
333
341
|
|
|
334
342
|
export { claudeHistoryPath, claudeMessageBlocks, readClaudeHistory, requireClaudeSessionId, archivedClaudeProjectDirectory, matchingClaudeProjectDirectories };
|
|
335
343
|
|
|
336
|
-
const TRANSCRIPT_PATH_PATTERN = /\.jsonl(?:\.(?:superseded|orphaned)-.*)?$/u;
|
|
337
|
-
|
|
338
344
|
export async function retireClaudeConversationHistory({ configRoot, binding, beforeDelete, requireIdle, signal }) {
|
|
339
345
|
const id = requireClaudeSessionId(binding.conversationId);
|
|
340
346
|
if (!path.isAbsolute(configRoot)) throw new TypeError("Claude retirement requires an absolute configuration root.");
|
|
@@ -411,9 +417,7 @@ export async function retireClaudeConversationHistory({ configRoot, binding, bef
|
|
|
411
417
|
.filter((message) => message.role !== "thinking")
|
|
412
418
|
.map((message) => ({ ...provenance, role: "assistant", text: message.text }));
|
|
413
419
|
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");
|
|
420
|
+
const user = claudeUserText(frame.message?.content);
|
|
417
421
|
if (user) text.push({ ...provenance, role: "user", text: user });
|
|
418
422
|
}
|
|
419
423
|
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
|
}
|
|
@@ -893,6 +899,14 @@ export function createClaudeConversationOwner({
|
|
|
893
899
|
return { status: "available", threadId: entry.id, goal: await entry.goals.read(history), goalCommandId: history.goalCommandId || null };
|
|
894
900
|
}
|
|
895
901
|
|
|
902
|
+
// A native Set, Resume or Cancel supersedes a retained Pause or blocker.
|
|
903
|
+
async function releaseRetainedGoal(entry) {
|
|
904
|
+
if (!entry.pausedGoal && !entry.blockedGoal) return;
|
|
905
|
+
entry.pausedGoal = null;
|
|
906
|
+
entry.blockedGoal = null;
|
|
907
|
+
await store.save(entry);
|
|
908
|
+
}
|
|
909
|
+
|
|
896
910
|
async function updateGoal(entry, input, operations) {
|
|
897
911
|
await bindAccount(entry);
|
|
898
912
|
const call = applicationCall.getStore();
|
|
@@ -928,7 +942,7 @@ export function createClaudeConversationOwner({
|
|
|
928
942
|
for (;;) {
|
|
929
943
|
const history = await readHistory(entry);
|
|
930
944
|
if (history.goalCommandId === claudeNativeMessageId(messageId)) {
|
|
931
|
-
|
|
945
|
+
await releaseRetainedGoal(entry);
|
|
932
946
|
result = { status: "available", threadId: entry.id, goal: await entry.goals.read(history), goalCommandId: history.goalCommandId };
|
|
933
947
|
break;
|
|
934
948
|
}
|
|
@@ -952,7 +966,7 @@ export function createClaudeConversationOwner({
|
|
|
952
966
|
}
|
|
953
967
|
|
|
954
968
|
async function cancel(control, { context, acquire, reportFailure, entry }) {
|
|
955
|
-
if (
|
|
969
|
+
if (bound) {
|
|
956
970
|
await entry.nativeTurn.stopProcess("Work stopped.");
|
|
957
971
|
return;
|
|
958
972
|
}
|
|
@@ -973,7 +987,7 @@ export function createClaudeConversationOwner({
|
|
|
973
987
|
}
|
|
974
988
|
|
|
975
989
|
async function dispatch(control, command, { acquire, entry: selectedEntry, dispatch: nativeDispatch }) {
|
|
976
|
-
if (
|
|
990
|
+
if (bound) {
|
|
977
991
|
await send(selectedEntry, command.input, { command: nativeDispatch });
|
|
978
992
|
return;
|
|
979
993
|
}
|
|
@@ -1001,14 +1015,13 @@ export function createClaudeConversationOwner({
|
|
|
1001
1015
|
}
|
|
1002
1016
|
|
|
1003
1017
|
function steer(control, command, options) {
|
|
1004
|
-
if (!
|
|
1018
|
+
if (!bound) return dispatch(control, command, options).then(delivered => ({ value: delivered.value }));
|
|
1005
1019
|
if (!control.current?.steer || !control.current.entry.nativeTurn.read().active) throw new Error("Claude has no active turn to steer.");
|
|
1006
1020
|
return control.current.steer(command);
|
|
1007
1021
|
}
|
|
1008
1022
|
|
|
1009
1023
|
async function run(control, command, { acquire, reportFailure, entry: selectedEntry, maximumOutput, maximumToolCalls = 32, timeoutMs } = {}) {
|
|
1010
1024
|
if (control.disposed || control.current) throw new Error("This Claude conversation is closed or already working.");
|
|
1011
|
-
const binding = Boolean(store.releaseExecution);
|
|
1012
1025
|
const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent, onNativeToolUse } = command;
|
|
1013
1026
|
const completion = Promise.withResolvers();
|
|
1014
1027
|
completion.promise.catch(() => {});
|
|
@@ -1039,7 +1052,7 @@ export function createClaudeConversationOwner({
|
|
|
1039
1052
|
let delivered;
|
|
1040
1053
|
function abort() {
|
|
1041
1054
|
if (cancellation) return;
|
|
1042
|
-
if (
|
|
1055
|
+
if (bound) {
|
|
1043
1056
|
cancellation = entry.nativeTurn.cancel(entry.process?.client, () => entry.nativeTurn.stopProcess("Work stopped."));
|
|
1044
1057
|
cancellation.then(() => completion.reject(signal.reason), error => completion.reject(error));
|
|
1045
1058
|
} else {
|
|
@@ -1048,7 +1061,7 @@ export function createClaudeConversationOwner({
|
|
|
1048
1061
|
}
|
|
1049
1062
|
}
|
|
1050
1063
|
try {
|
|
1051
|
-
if (
|
|
1064
|
+
if (bound) {
|
|
1052
1065
|
entry = selectedEntry;
|
|
1053
1066
|
if (entry.disposed || entry.command) throw new Error("This Claude conversation is closed or already working.");
|
|
1054
1067
|
active.entry = entry;
|
|
@@ -1062,7 +1075,7 @@ export function createClaudeConversationOwner({
|
|
|
1062
1075
|
let dispatchFailure;
|
|
1063
1076
|
try {
|
|
1064
1077
|
delivered = await dispatch(control, command, { acquire, entry, dispatch: nativeDispatch });
|
|
1065
|
-
if (
|
|
1078
|
+
if (bound) ready.resolve();
|
|
1066
1079
|
} catch (error) { dispatchFailure = error; }
|
|
1067
1080
|
for (;;) {
|
|
1068
1081
|
const settling = active.delivery;
|
|
@@ -1078,15 +1091,15 @@ export function createClaudeConversationOwner({
|
|
|
1078
1091
|
catch (error) { problem = error; }
|
|
1079
1092
|
}
|
|
1080
1093
|
problem = dispatchFailure || failure || problem;
|
|
1081
|
-
if (!problem && settling?.completion && (
|
|
1082
|
-
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 });
|
|
1083
1096
|
}
|
|
1084
1097
|
break;
|
|
1085
1098
|
}
|
|
1086
1099
|
} catch (error) { problem = error; }
|
|
1087
1100
|
ready.reject(problem || new Error("Claude work ended."));
|
|
1088
1101
|
signal.removeEventListener("abort", abort);
|
|
1089
|
-
if (
|
|
1102
|
+
if (bound) {
|
|
1090
1103
|
try {
|
|
1091
1104
|
if (problem || signal.aborted) await (cancellation || entry.nativeTurn.stopProcess(problem?.message || "Work stopped."));
|
|
1092
1105
|
} catch (error) {
|
|
@@ -1118,19 +1131,19 @@ export function createClaudeConversationOwner({
|
|
|
1118
1131
|
}
|
|
1119
1132
|
if (control.current === active) control.current = null;
|
|
1120
1133
|
if (problem) {
|
|
1121
|
-
if (
|
|
1134
|
+
if (bound && nativeDispatch.attempted && !nativeDispatch.admitted && problem.delivery !== "not-sent") problem.delivery = "uncertain";
|
|
1122
1135
|
throw problem;
|
|
1123
1136
|
}
|
|
1124
|
-
if (!
|
|
1137
|
+
if (!bound) return { value: delivered.value };
|
|
1125
1138
|
}
|
|
1126
1139
|
|
|
1127
|
-
return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, readContextUsage, compactContext, stopForTerminal, prepareTerminal, send, startTurn,
|
|
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,
|
|
1128
1141
|
interrupt, read, inspectAdmission, hasActiveTemporaryConversation, hasActiveConversation, retireConversationHistory, reconcileSessions, restoreSessionEntries, closeSession, closeProject, invalidateRuntimes, drain, forget, close, deleteConversation, recover, stopSelected, wait, stopConversation,
|
|
1129
1142
|
readGoal, updateGoal, renewal, run, steer, cancel });
|
|
1130
1143
|
}
|
|
1131
1144
|
|
|
1132
1145
|
/** Native turn handling moved from the production session provider; hosts persist its events. */
|
|
1133
|
-
export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry } = {}) {
|
|
1146
|
+
export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry, bound = false } = {}) {
|
|
1134
1147
|
if (!conversationId) throw new TypeError("A Claude conversation id is required.");
|
|
1135
1148
|
const instructions = process ? createClaudeConversationAdapter(process) : null;
|
|
1136
1149
|
const publish = entry ? publishEntryEvent : onEvent;
|
|
@@ -1241,31 +1254,30 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1241
1254
|
invalidateFinalAssistantResult();
|
|
1242
1255
|
// Runtime bindings commit execution release before dropping the handle.
|
|
1243
1256
|
// Retained host entries keep their original post-proof checkpoint order.
|
|
1244
|
-
const binding = Boolean(process.releaseExecution);
|
|
1245
1257
|
if (entry.stopPending) return entry.stopPending;
|
|
1246
1258
|
const active = read().active;
|
|
1247
|
-
if (!
|
|
1259
|
+
if (!bound) entry.stopping = true;
|
|
1248
1260
|
const operation = async () => {
|
|
1249
1261
|
const startup = entry.startupCleanup?.executionId === entry.executionId ? entry.startupCleanup : null;
|
|
1250
1262
|
let proof;
|
|
1251
1263
|
try {
|
|
1252
|
-
if (!
|
|
1264
|
+
if (!bound || entry.process || entry.executionId) {
|
|
1253
1265
|
proof = await stopClaudeCodeProcess({ process: entry.process || startup, executionId: entry.executionId,
|
|
1254
1266
|
stopExecution: process.stopExecution });
|
|
1255
1267
|
}
|
|
1256
1268
|
} catch (error) {
|
|
1257
|
-
if (
|
|
1269
|
+
if (bound || error.code !== "claude_stop_unconfirmed") throw error;
|
|
1258
1270
|
entry.observationError = reason || "Claude process exit has not been confirmed. Try Stop again.";
|
|
1259
1271
|
await onEvent({ type: "save" });
|
|
1260
1272
|
await onEvent({ type: "stop-unconfirmed" });
|
|
1261
1273
|
throw error;
|
|
1262
1274
|
}
|
|
1263
|
-
if (
|
|
1275
|
+
if (bound && (entry.process || entry.executionId)) await process.releaseExecution();
|
|
1264
1276
|
entry.process = null;
|
|
1265
1277
|
entry.executionId = "";
|
|
1266
1278
|
entry.exitProof = proof;
|
|
1267
|
-
stop(
|
|
1268
|
-
if (
|
|
1279
|
+
stop(bound ? reason : reason || "Claude stopped before acknowledging the prompt.");
|
|
1280
|
+
if (bound) {
|
|
1269
1281
|
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
1270
1282
|
if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
|
|
1271
1283
|
return;
|
|
@@ -1287,7 +1299,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1287
1299
|
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
1288
1300
|
return entry.exitProof;
|
|
1289
1301
|
};
|
|
1290
|
-
entry.stopPending = operation().catch(error => { if (
|
|
1302
|
+
entry.stopPending = operation().catch(error => { if (bound) error.cleanupFailed = true; throw error; })
|
|
1291
1303
|
.finally(() => { entry.stopPending = null; });
|
|
1292
1304
|
return entry.stopPending;
|
|
1293
1305
|
}
|
|
@@ -1295,9 +1307,8 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1295
1307
|
async function publishEntryEvent(event, deferred = false) {
|
|
1296
1308
|
if (entry.stopping) return;
|
|
1297
1309
|
if (event.type === "settled" && event.generation !== generation) return;
|
|
1298
|
-
const binding = Boolean(process.releaseExecution);
|
|
1299
1310
|
const command = entry.command;
|
|
1300
|
-
if (event.type === "settled" && !
|
|
1311
|
+
if (event.type === "settled" && !bound && !deferred && command?.finishIfCurrent && command !== checkpointedCommand) {
|
|
1301
1312
|
const delivery = command.delivery;
|
|
1302
1313
|
const completion = entry.completion;
|
|
1303
1314
|
const owner = acceptedInput;
|
|
@@ -1328,17 +1339,17 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1328
1339
|
}
|
|
1329
1340
|
if (event.type === "continued") {
|
|
1330
1341
|
if (entry.completion === resolvedCompletion) entry.completion = Promise.withResolvers();
|
|
1331
|
-
if (
|
|
1342
|
+
if (bound) setState("active");
|
|
1332
1343
|
else await updateState("active");
|
|
1333
1344
|
} else if (event.type === "admitted") {
|
|
1334
1345
|
if (event.userEcho) entry.sent = true;
|
|
1335
|
-
if (!
|
|
1346
|
+
if (!bound) await onEvent({ type: "save" });
|
|
1336
1347
|
} else if (event.type === "phase") {
|
|
1337
1348
|
entry.turn.phase = event.phase;
|
|
1338
|
-
if (!
|
|
1349
|
+
if (!bound) await updateState(entry.turn.state);
|
|
1339
1350
|
} else if (event.type === "settled") {
|
|
1340
1351
|
const state = event.status === "interrupted" ? "interrupted" : event.status === "failed" ? "failed" : "completed";
|
|
1341
|
-
if (
|
|
1352
|
+
if (bound) setState(state, event.error);
|
|
1342
1353
|
else if (!await updateState(state, event.error, event.generation)) return;
|
|
1343
1354
|
}
|
|
1344
1355
|
const published = await onEvent(event);
|
|
@@ -1350,7 +1361,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1350
1361
|
if (event.type === "settled") {
|
|
1351
1362
|
if (event.generation !== generation) return;
|
|
1352
1363
|
if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
|
|
1353
|
-
resolveCompletion(
|
|
1364
|
+
resolveCompletion(bound ? event : readResult());
|
|
1354
1365
|
}
|
|
1355
1366
|
return published;
|
|
1356
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);
|