@jskit-ai/assistant-core 0.2.7 → 0.2.9
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 +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -13,15 +13,31 @@ const statusReads = new Map();
|
|
|
13
13
|
const STATUS_RETRY_MS = 30_000;
|
|
14
14
|
const CLAUDE_EXIT_WAIT_MS = 5_000;
|
|
15
15
|
|
|
16
|
+
/** A Claude process whose caller records no execution identity. */
|
|
17
|
+
function ignoreProcessStart() {}
|
|
18
|
+
|
|
19
|
+
/** A Claude process whose caller keeps no state after its connection fails. */
|
|
20
|
+
function ignoreProcessFailure() {}
|
|
21
|
+
|
|
22
|
+
/** Account queries outside a conversation owner borrow no running process. */
|
|
23
|
+
function noOwnedProcesses() {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Account queries outside a conversation owner never close for reconnection. */
|
|
28
|
+
function neverClosing() {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
/** Person-facing cause for a Claude process that ended without being stopped. */
|
|
17
33
|
export function claudeUnexpectedExitMessage(processExit = {}) {
|
|
18
34
|
const resume = "The conversation so far is saved; send a message to continue.";
|
|
19
|
-
if (Number(processExit
|
|
35
|
+
if (Number(processExit.taskLimit?.count) > 0) {
|
|
20
36
|
return "Claude stopped unexpectedly: the server refused to create more processes or threads for it " +
|
|
21
37
|
`(process/thread limit reached). ${resume}`;
|
|
22
38
|
}
|
|
23
|
-
if (processExit
|
|
24
|
-
if (Number.isInteger(processExit
|
|
39
|
+
if (processExit.signal) return `Claude stopped unexpectedly: it was terminated by ${processExit.signal}. ${resume}`;
|
|
40
|
+
if (Number.isInteger(processExit.code)) {
|
|
25
41
|
return `Claude stopped unexpectedly with exit code ${processExit.code}. ${resume}`;
|
|
26
42
|
}
|
|
27
43
|
return `Claude stopped unexpectedly. ${resume}`;
|
|
@@ -38,12 +54,22 @@ async function fileRevision(file) {
|
|
|
38
54
|
}
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The CLI's own account status, cached until its credentials change.
|
|
59
|
+
* @param {object} options
|
|
60
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeEnv} options.env
|
|
61
|
+
* @param {{ home: string, ok?: boolean, error?: string }} [options.credentialHome] The account's home; the environment's HOME when not given.
|
|
62
|
+
* @param {string} [options.command]
|
|
63
|
+
* @param {(request: Record<string, any>) => Promise<Record<string, any>>} options.commandRunner
|
|
64
|
+
* @param {AbortSignal} [options.signal]
|
|
65
|
+
*/
|
|
66
|
+
export async function readClaudeCodeAuthStatus({ env, credentialHome, command = "claude", commandRunner, signal }) {
|
|
67
|
+
if (!env) throw new TypeError("readClaudeCodeAuthStatus requires the application's environment, such as process.env.");
|
|
68
|
+
const accountHome = credentialHome ?? { home: env.HOME || homedir() };
|
|
69
|
+
if (accountHome.ok === false) return { loggedIn: false, error: accountHome.error };
|
|
70
|
+
// readable-check: allow method-probe - input validation: the capture runner a host hands the account status read
|
|
45
71
|
if (typeof commandRunner !== "function") throw new TypeError("Claude account status requires its host capture runner.");
|
|
46
|
-
const configRoot = env.CLAUDE_CONFIG_DIR || path.join(
|
|
72
|
+
const configRoot = env.CLAUDE_CONFIG_DIR || path.join(accountHome.home, ".claude");
|
|
47
73
|
// Inspect file metadata only. Claude still owns reading and validating credentials.
|
|
48
74
|
// Sign-in writes .credentials.json and sign-out removes it. Claude Code also
|
|
49
75
|
// rewrites .claude.json during turns and after every status read, so that
|
|
@@ -51,13 +77,13 @@ export async function readClaudeCodeAuthStatus({
|
|
|
51
77
|
const revision = await fileRevision(path.join(configRoot, ".credentials.json"));
|
|
52
78
|
// One entry per account, whichever command path or host runner reads it: only
|
|
53
79
|
// the account's location and its identity variables count.
|
|
54
|
-
const key = jsonHash([configRoot,
|
|
80
|
+
const key = jsonHash([configRoot, accountHome.home, Object.entries(env).filter(([name, value]) =>
|
|
55
81
|
value && (name === "CLAUDE_CODE_OAUTH_TOKEN" || name.startsWith("ANTHROPIC_"))).sort()]);
|
|
56
82
|
const previous = statusReads.get(key);
|
|
57
83
|
if (previous?.revision === revision && (previous.pending || previous.expiresAt > Date.now())) return previous.value;
|
|
58
84
|
const entry = { revision, pending: true, expiresAt: 0 };
|
|
59
85
|
statusReads.set(key, entry);
|
|
60
|
-
entry.value = readNativeClaudeCodeAuthStatus({ env, credentialHome, command, commandRunner, signal }).then(({ status, unfinished }) => {
|
|
86
|
+
entry.value = readNativeClaudeCodeAuthStatus({ env, credentialHome: accountHome, command, commandRunner, signal }).then(({ status, unfinished }) => {
|
|
61
87
|
// Keychain changes cannot be observed with file metadata on macOS.
|
|
62
88
|
if (process.platform !== "darwin") {
|
|
63
89
|
// Claude's answer holds until its credentials change. A read the host timed
|
|
@@ -99,7 +125,7 @@ function claudeCodeAuthStatus(result) {
|
|
|
99
125
|
}
|
|
100
126
|
}
|
|
101
127
|
|
|
102
|
-
/**
|
|
128
|
+
/** A native connection's exact pin, shared with stopped metadata conversion. */
|
|
103
129
|
export function claudeConnectionIdentity(configRoot, providerId, baseUrl, apiKey) {
|
|
104
130
|
return jsonHash([configRoot, providerId, baseUrl, apiKey]);
|
|
105
131
|
}
|
|
@@ -142,6 +168,7 @@ export async function verifyClaudeProviderKey(provider, apiKey, fetchImpl) {
|
|
|
142
168
|
throw new Error(`${provider.label} could not be reached through Claude Code. Your previous connection is unchanged; try again.`);
|
|
143
169
|
}
|
|
144
170
|
if (!response.ok) {
|
|
171
|
+
// readable-check: allow optional-call - platform object: another fetch implementation's Response may lack a readable body
|
|
145
172
|
await response.body?.cancel?.();
|
|
146
173
|
throw new Error(response.status === 401 || response.status === 403
|
|
147
174
|
? `${provider.label} rejected this key. Check the key and the required account plan.`
|
|
@@ -287,8 +314,20 @@ export function claudeCatalogueModels(initialization) {
|
|
|
287
314
|
* usage is shared by `accountIdentity` with every other owner in the process, so
|
|
288
315
|
* that identity names exactly one Claude account. `configRoot` is the account's
|
|
289
316
|
* Claude configuration directory, whose credentials the model list follows.
|
|
317
|
+
* @param {object} options
|
|
318
|
+
* @param {(request: { signal: AbortSignal, purpose: string }) => Promise<ClaudeCodeProcess>} options.createProcess
|
|
319
|
+
* @param {(context: Record<string, any>) => Promise<string>} options.accountIdentity
|
|
320
|
+
* @param {string} options.configRoot
|
|
321
|
+
* @param {() => Iterable<{ process: ClaudeCodeProcess | null, stopping?: unknown, accountIdentity: string }>} [options.processes]
|
|
322
|
+
* @param {() => boolean} [options.isClosing]
|
|
290
323
|
*/
|
|
291
|
-
export function createClaudeAccountQueries({
|
|
324
|
+
export function createClaudeAccountQueries({
|
|
325
|
+
createProcess,
|
|
326
|
+
accountIdentity,
|
|
327
|
+
configRoot,
|
|
328
|
+
processes = noOwnedProcesses,
|
|
329
|
+
isClosing = neverClosing
|
|
330
|
+
}) {
|
|
292
331
|
const error = message => Object.assign(new Error(message), { code: "claude_account_query_failed" });
|
|
293
332
|
const accountProcessStops = new Set();
|
|
294
333
|
const accountReads = new Set();
|
|
@@ -349,112 +388,141 @@ export function createClaudeAccountQueries({ createProcess, accountIdentity, con
|
|
|
349
388
|
fileRevision(command)]));
|
|
350
389
|
}
|
|
351
390
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (kept && kept.revision === await catalogueRevision(kept.command)) {
|
|
362
|
-
assertCurrent(expectedRevision);
|
|
363
|
-
return kept.value;
|
|
364
|
-
}
|
|
365
|
-
if (!catalogStarts.has(identity)) catalogStarts.set(identity, (async () => {
|
|
366
|
-
// A conversation's process answers with what it knew when it started, so
|
|
367
|
-
// the list comes from a Claude started for this read.
|
|
368
|
-
const { value, command } = await readAccountProcess(identity, expectedRevision, "catalogue",
|
|
369
|
-
async (native) => ({ value: await native.initialization, command: native.command }));
|
|
370
|
-
const answered = await catalogueRevision(command);
|
|
371
|
-
if (identity !== await accountIdentity(context)) throw error("The signed-in Claude account changed. Refresh the model list.");
|
|
372
|
-
assertCurrent(expectedRevision);
|
|
373
|
-
catalog = { identity, command, revision: answered, value };
|
|
374
|
-
return value;
|
|
375
|
-
})().finally(() => { catalogStarts.delete(identity); }));
|
|
376
|
-
return catalogStarts.get(identity);
|
|
377
|
-
},
|
|
378
|
-
async readPlanUsage(context) {
|
|
379
|
-
const expectedRevision = revision;
|
|
391
|
+
async function readCatalogue(context) {
|
|
392
|
+
const expectedRevision = revision;
|
|
393
|
+
assertCurrent(expectedRevision);
|
|
394
|
+
const identity = await accountIdentity(context);
|
|
395
|
+
assertCurrent(expectedRevision);
|
|
396
|
+
// An answer is kept until its account, credentials or executable change, so
|
|
397
|
+
// reading it again starts no Claude.
|
|
398
|
+
const kept = catalog?.identity === identity ? catalog : null;
|
|
399
|
+
if (kept && kept.revision === await catalogueRevision(kept.command)) {
|
|
380
400
|
assertCurrent(expectedRevision);
|
|
381
|
-
|
|
401
|
+
return kept.value;
|
|
402
|
+
}
|
|
403
|
+
if (!catalogStarts.has(identity)) catalogStarts.set(identity, (async () => {
|
|
404
|
+
// A conversation's process answers with what it knew when it started, so
|
|
405
|
+
// the list comes from a Claude started for this read.
|
|
406
|
+
const { value, command } = await readAccountProcess(identity, expectedRevision, "catalogue",
|
|
407
|
+
async (native) => ({ value: await native.initialization, command: native.command }));
|
|
408
|
+
const answered = await catalogueRevision(command);
|
|
409
|
+
if (identity !== await accountIdentity(context)) throw error("The signed-in Claude account changed. Refresh the model list.");
|
|
382
410
|
assertCurrent(expectedRevision);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
catalog = { identity, command, revision: answered, value };
|
|
412
|
+
return value;
|
|
413
|
+
})().finally(() => { catalogStarts.delete(identity); }));
|
|
414
|
+
return catalogStarts.get(identity);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function readPlanUsage(context) {
|
|
418
|
+
const expectedRevision = revision;
|
|
419
|
+
assertCurrent(expectedRevision);
|
|
420
|
+
const identity = await accountIdentity(context);
|
|
421
|
+
assertCurrent(expectedRevision);
|
|
422
|
+
// One reading per account, whichever owner asks; a started process is stopped after its read.
|
|
423
|
+
let usage = planUsageReads.get(identity);
|
|
424
|
+
if (!usage) {
|
|
425
|
+
usage = { liveReadAt: -Infinity, temporaryReadAt: -Infinity, reading: null, pending: null };
|
|
426
|
+
planUsageReads.set(identity, usage);
|
|
427
|
+
}
|
|
428
|
+
usageAccounts.add(identity);
|
|
429
|
+
if (usage.pending) return usage.pending;
|
|
430
|
+
const live = Boolean(ownedProcess(identity));
|
|
431
|
+
const liveDue = live && Date.now() - usage.liveReadAt >= PLAN_USAGE_LIVE_READ_MS;
|
|
432
|
+
// A one-shot Claude starts only when neither a verified reading nor a one-shot
|
|
433
|
+
// read is from the last idle interval.
|
|
434
|
+
const temporaryDue = () => Date.now() - Math.max(usage.temporaryReadAt, usage.reading?.checkedAt ?? -Infinity) >=
|
|
435
|
+
PLAN_USAGE_IDLE_READ_MS;
|
|
436
|
+
if (!liveDue && (live || !temporaryDue())) return presentPlanUsage(usage.reading);
|
|
437
|
+
const pending = (async () => {
|
|
438
|
+
// A live process's held endpoint read is its own, so a fresh one-shot Claude
|
|
439
|
+
// may still reach the endpoint when the live answer cannot prove it is current.
|
|
440
|
+
for (const borrow of liveDue ? [true, false] : [false]) {
|
|
441
|
+
if (!borrow && !temporaryDue()) break;
|
|
442
|
+
usage[borrow ? "liveReadAt" : "temporaryReadAt"] = Date.now();
|
|
443
|
+
let value;
|
|
444
|
+
try {
|
|
445
|
+
value = await readAccountProcess(identity, expectedRevision, "usage", (native) => native.client.request(
|
|
446
|
+
{ subtype: "get_usage", skip_behaviors: true }), { borrow });
|
|
447
|
+
} catch (cause) {
|
|
448
|
+
if (borrow || !liveDue) throw cause;
|
|
414
449
|
assertCurrent(expectedRevision);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
if (identity !== await accountIdentity(context)) return { status: "unavailable", windows: [] };
|
|
453
|
+
assertCurrent(expectedRevision);
|
|
454
|
+
// An answer that cannot prove it is current keeps the last verified reading.
|
|
455
|
+
if (claudePlanUsageVerified(value)) {
|
|
456
|
+
usage.reading = { ...claudePlanUsage(value), identity };
|
|
457
|
+
break;
|
|
420
458
|
}
|
|
421
|
-
return presentPlanUsage(usage.reading);
|
|
422
|
-
})().finally(() => {
|
|
423
|
-
usageReads.delete(pending);
|
|
424
|
-
if (usage.pending === pending) usage.pending = null;
|
|
425
|
-
});
|
|
426
|
-
// Requests that arrive while the account is read share that read.
|
|
427
|
-
usage.pending = pending;
|
|
428
|
-
usageReads.add(pending);
|
|
429
|
-
return pending;
|
|
430
|
-
},
|
|
431
|
-
// A native usage change lets the next request read again without hiding the last reading.
|
|
432
|
-
invalidateUsage() {
|
|
433
|
-
for (const identity of usageAccounts) {
|
|
434
|
-
const usage = planUsageReads.get(identity);
|
|
435
|
-
if (usage) usage.liveReadAt = -Infinity;
|
|
436
459
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
460
|
+
return presentPlanUsage(usage.reading);
|
|
461
|
+
})().finally(() => {
|
|
462
|
+
usageReads.delete(pending);
|
|
463
|
+
if (usage.pending === pending) usage.pending = null;
|
|
464
|
+
});
|
|
465
|
+
// Requests that arrive while the account is read share that read.
|
|
466
|
+
usage.pending = pending;
|
|
467
|
+
usageReads.add(pending);
|
|
468
|
+
return pending;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** A native usage change lets the next request read again without hiding the last reading. */
|
|
472
|
+
function invalidateUsage() {
|
|
473
|
+
for (const identity of usageAccounts) {
|
|
474
|
+
const usage = planUsageReads.get(identity);
|
|
475
|
+
if (usage) usage.liveReadAt = -Infinity;
|
|
448
476
|
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function invalidate() {
|
|
480
|
+
revision++;
|
|
481
|
+
// Dropped at once: a read started before this cannot store its answer.
|
|
482
|
+
catalog = null;
|
|
483
|
+
for (const controller of accountReads) controller.abort(error("Claude account query was cancelled for reconnection."));
|
|
484
|
+
await Promise.allSettled([...catalogStarts.values(), ...usageReads]);
|
|
485
|
+
// The account changed: the next request for an account this owner used reads again.
|
|
486
|
+
for (const identity of usageAccounts) planUsageReads.delete(identity);
|
|
487
|
+
usageAccounts.clear();
|
|
488
|
+
for (const native of accountProcessStops) await stopAccountProcess(native);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
readCatalogue,
|
|
493
|
+
readPlanUsage,
|
|
494
|
+
invalidateUsage,
|
|
495
|
+
invalidate
|
|
449
496
|
});
|
|
450
497
|
}
|
|
451
498
|
|
|
452
|
-
/**
|
|
499
|
+
/**
|
|
500
|
+
* @typedef {Awaited<ReturnType<typeof createClaudeCodeProcess>>} ClaudeCodeProcess
|
|
501
|
+
*/
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Native startup and handshake; an execution host owns processes and their identity.
|
|
505
|
+
* @param {object} options
|
|
506
|
+
* @param {string} [options.command]
|
|
507
|
+
* @param {import("./localExecution.js").ConversationExecution} [options.execution]
|
|
508
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeEnv} options.env
|
|
509
|
+
* @param {string} [options.workdir]
|
|
510
|
+
* @param {(frame: Record<string, any>) => Promise<unknown>} [options.onEvent]
|
|
511
|
+
* @param {(request: Record<string, any>, context: { signal: AbortSignal }) => Promise<unknown>} [options.onControlRequest]
|
|
512
|
+
* @param {(error: Error) => unknown} [options.onFailure]
|
|
513
|
+
* @param {(executionId: string, stop: () => Promise<Record<string, any>>) => unknown} [options.onStarted]
|
|
514
|
+
* @param {object} [options.executionLimits]
|
|
515
|
+
* @param {AbortSignal} [options.signal]
|
|
516
|
+
* @param {Record<string, any>} [options.commandWrapper]
|
|
517
|
+
* @param {number} [options.initializationTimeoutMs]
|
|
518
|
+
* @param {(stage: string) => void} [options.onInitializationProgress]
|
|
519
|
+
*/
|
|
453
520
|
export async function createClaudeCodeProcess({
|
|
454
|
-
command = "claude", execution = createLocalConversationExecution(), env
|
|
455
|
-
workdir, onEvent, onControlRequest, onFailure, onStarted, executionLimits, signal,
|
|
456
|
-
initializationTimeoutMs = 30_000, onInitializationProgress, ...options
|
|
457
|
-
}
|
|
521
|
+
command = "claude", execution = createLocalConversationExecution(), env,
|
|
522
|
+
workdir, onEvent, onControlRequest, onFailure = ignoreProcessFailure, onStarted = ignoreProcessStart, executionLimits, signal,
|
|
523
|
+
commandWrapper, initializationTimeoutMs = 30_000, onInitializationProgress, ...options
|
|
524
|
+
}) {
|
|
525
|
+
if (!env) throw new TypeError("createClaudeCodeProcess requires the application's environment, such as process.env.");
|
|
458
526
|
if (commandWrapper !== undefined) validateCommandWrapper(commandWrapper);
|
|
459
527
|
const environment = { ...env, DISABLE_AUTOUPDATER: "1" };
|
|
460
528
|
for (const name of ["DBUS_SESSION_BUS_ADDRESS", "DBUS_STARTER_ADDRESS", "DBUS_STARTER_BUS_TYPE",
|
|
@@ -479,7 +547,7 @@ export async function createClaudeCodeProcess({
|
|
|
479
547
|
signal?.throwIfAborted();
|
|
480
548
|
native = await execution.start({ command, args: claudeCodeArguments(options), cwd: workdir,
|
|
481
549
|
env: environment, stream: true, limits: executionLimits });
|
|
482
|
-
await onStarted
|
|
550
|
+
await onStarted(native.id, stop);
|
|
483
551
|
signal?.throwIfAborted();
|
|
484
552
|
const stream = Duplex.from({ readable: native.stdout, writable: native.stdin });
|
|
485
553
|
// A lost stream usually means the process ended. Its exit details arrive
|
|
@@ -489,11 +557,11 @@ export async function createClaudeCodeProcess({
|
|
|
489
557
|
if (failure?.code === "assistant_claude_observation_lost" && exited && !stopped && !stopping) {
|
|
490
558
|
const processExit = failure.processExit || await Promise.race([
|
|
491
559
|
exited.catch(() => null),
|
|
492
|
-
new Promise((resolve) => setTimeout(resolve, CLAUDE_EXIT_WAIT_MS, null).unref
|
|
560
|
+
new Promise((resolve) => setTimeout(resolve, CLAUDE_EXIT_WAIT_MS, null).unref())
|
|
493
561
|
]);
|
|
494
562
|
if (processExit) Object.assign(failure, { processExit, message: claudeUnexpectedExitMessage(processExit) });
|
|
495
563
|
}
|
|
496
|
-
return onFailure
|
|
564
|
+
return onFailure(failure);
|
|
497
565
|
};
|
|
498
566
|
client = createClaudeJsonClient({ stream, onEvent, onFailure: describeFailure,
|
|
499
567
|
onControlRequest(request, context) {
|
|
@@ -525,7 +593,13 @@ export async function createClaudeCodeProcess({
|
|
|
525
593
|
}, error => {
|
|
526
594
|
if (!stopped && !stopping) stream.destroy(error);
|
|
527
595
|
});
|
|
528
|
-
return {
|
|
596
|
+
return Object.freeze({
|
|
597
|
+
client,
|
|
598
|
+
initialization,
|
|
599
|
+
executionId: native.id,
|
|
600
|
+
command,
|
|
601
|
+
stop
|
|
602
|
+
});
|
|
529
603
|
} catch (error) {
|
|
530
604
|
// A rejected start may already own an execution and its cleanup proof.
|
|
531
605
|
// Only a returned native handle transfers that cleanup to this owner.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createClaudeCodeProcess } from "./claudeProcess.js";
|
|
3
|
+
import { claudeApplicationToolResponse } from "./claudeTools.js";
|
|
4
|
+
|
|
5
|
+
export function hasPendingClaudeStartup(entry) {
|
|
6
|
+
return entry.startupCleanup?.pending === true && entry.startupCleanup.executionId === entry.executionId;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Starts a retained entry's Claude process: it fences preparation, routes the
|
|
11
|
+
* process's frames and application tool calls, keeps startup custody, and
|
|
12
|
+
* suspends a retained goal before the first prompt. Local commands (goal clear,
|
|
13
|
+
* compaction) run on that process outside the turn view.
|
|
14
|
+
* @param {{
|
|
15
|
+
* store: import("./claudeTurn.js").ClaudeConversationStore,
|
|
16
|
+
* process: import("./claudeTurn.js").ClaudeConversationProcessHost,
|
|
17
|
+
* createError: import("./claudeTurn.js").ClaudeConversationErrorFactory,
|
|
18
|
+
* onEvent: import("./claudeTurn.js").ClaudeConversationEventHandler,
|
|
19
|
+
* bound: boolean,
|
|
20
|
+
* readHistory: (entry: import("./claudeTurn.js").ClaudeConversationEntry) => Promise<Record<string, any>>,
|
|
21
|
+
* isClosing: (contextKey: string) => boolean,
|
|
22
|
+
* applicationCall: import("node:async_hooks").AsyncLocalStorage<Record<string, any>>,
|
|
23
|
+
* currentApplicationCommand: (entry: import("./claudeTurn.js").ClaudeConversationEntry, command: Record<string, any>, owner?: Record<string, any>) => boolean
|
|
24
|
+
* }} options
|
|
25
|
+
*/
|
|
26
|
+
export function createClaudeProcessStart({
|
|
27
|
+
store,
|
|
28
|
+
process,
|
|
29
|
+
createError,
|
|
30
|
+
onEvent,
|
|
31
|
+
bound,
|
|
32
|
+
readHistory,
|
|
33
|
+
isClosing,
|
|
34
|
+
applicationCall,
|
|
35
|
+
currentApplicationCommand
|
|
36
|
+
}) {
|
|
37
|
+
const createProcess = process.create || createClaudeCodeProcess;
|
|
38
|
+
|
|
39
|
+
async function startProcess(entry, configuration) {
|
|
40
|
+
const context = entry.context;
|
|
41
|
+
const revision = entry.preparationRevision;
|
|
42
|
+
const assertPreparing = () => {
|
|
43
|
+
if (revision !== entry.preparationRevision || entry.disposed || isClosing(context.key)) {
|
|
44
|
+
throw createError("Claude preparation was stopped while this session was closing or changing.", { code: "conversation_claude_preparation_stopped" });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
assertPreparing();
|
|
48
|
+
let prepared = process.prepare ? process.prepare(entry, configuration, context) : { workdir: entry.nativeWorkdir };
|
|
49
|
+
// readable-check: allow method-probe - data: a host prepares the workdir synchronously or with a promise; a synchronous one starts without yielding
|
|
50
|
+
if (prepared && typeof prepared.then === "function") prepared = await prepared;
|
|
51
|
+
entry.nativeWorkdir = prepared.workdir;
|
|
52
|
+
if (!entry.sent) entry.sent = (await readHistory(entry)).exists;
|
|
53
|
+
entry.stopping = false;
|
|
54
|
+
entry.profile = configuration.profile;
|
|
55
|
+
let options = process.configure(entry, configuration, { context, prepared });
|
|
56
|
+
// readable-check: allow method-probe - data: a host configures the process synchronously or with a promise; a synchronous one starts without yielding
|
|
57
|
+
if (options && typeof options.then === "function") options = await options;
|
|
58
|
+
assertPreparing();
|
|
59
|
+
let startupCleanup;
|
|
60
|
+
let startedExecutionId;
|
|
61
|
+
const processToken = {};
|
|
62
|
+
try { entry.process = await createProcess({ ...options,
|
|
63
|
+
workdir: entry.nativeWorkdir, sessionId: entry.id, resume: entry.sent,
|
|
64
|
+
onStarted: async (executionId, stop) => {
|
|
65
|
+
startedExecutionId = executionId;
|
|
66
|
+
if (typeof stop === "function") {
|
|
67
|
+
startupCleanup = { executionId, stop, pending: true };
|
|
68
|
+
entry.startupCleanup = startupCleanup;
|
|
69
|
+
}
|
|
70
|
+
if (entry.executionId !== executionId) {
|
|
71
|
+
entry.executionId = executionId;
|
|
72
|
+
await store.save(entry, { execution: true });
|
|
73
|
+
await onEvent(entry, { type: "execution", context, executionId });
|
|
74
|
+
}
|
|
75
|
+
assertPreparing();
|
|
76
|
+
},
|
|
77
|
+
async onEvent(frame) {
|
|
78
|
+
const pending = entry.goalToolStop;
|
|
79
|
+
if (pending && pending.executionId === startedExecutionId && entry.process === pending.process &&
|
|
80
|
+
frame.type === "user" && !frame.parent_tool_use_id && Array.isArray(frame.message?.content) &&
|
|
81
|
+
frame.message.content.some(block => block.type === "tool_result" && block.tool_use_id === pending.toolUseId)) {
|
|
82
|
+
// The CLI has consumed the tool response. Do not join process cleanup
|
|
83
|
+
// on its event reader: interrupt/exit also needs that reader to drain.
|
|
84
|
+
pending.stop();
|
|
85
|
+
}
|
|
86
|
+
// Local commands (retained goal clear, compaction) are not AI turns.
|
|
87
|
+
// Their frames stay out of the turn view until their own result.
|
|
88
|
+
const local = entry.localCommand;
|
|
89
|
+
if (local?.processToken === processToken) {
|
|
90
|
+
if (frame.type === "result" && frame.user_message_uuid === local.id) {
|
|
91
|
+
if (frame.local_command === local.name && frame.is_error !== true && frame.subtype === "success") local.resolve();
|
|
92
|
+
else local.reject(createError(local.failureMessage, { code: local.failureCode }));
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const current = entry.command;
|
|
97
|
+
if (current && frame.type === "assistant" && !frame.parent_tool_use_id) {
|
|
98
|
+
for (const tool of frame.message?.content || []) if (tool.type === "tool_use" && tool.id && tool.name?.startsWith("mcp__application__")) {
|
|
99
|
+
if (current.nativeToolCount >= current.maximumToolCalls && !current.nativeTools.has(tool.id)) {
|
|
100
|
+
throw new Error("Claude exceeded the application tool-call limit.");
|
|
101
|
+
}
|
|
102
|
+
if (!current.nativeTools.has(tool.id)) {
|
|
103
|
+
current.nativeToolCount++;
|
|
104
|
+
current.nativeTools.set(tool.id, { name: tool.name, input: tool.input, messageId: current.messageId });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await entry.nativeTurn.receive(frame);
|
|
109
|
+
},
|
|
110
|
+
...(bound || options.applicationTools || entry.command?.tools ? {
|
|
111
|
+
async onControlRequest(request, { signal }) {
|
|
112
|
+
const active = entry.command;
|
|
113
|
+
const suppliedTools = !bound && active?.tools;
|
|
114
|
+
const owner = active?.nativeOwner;
|
|
115
|
+
const work = applicationCall.run({ entry, command: active, owner,
|
|
116
|
+
toolUseId: request.message?.params?._meta?.["claudecode/toolUseId"] },
|
|
117
|
+
() => claudeApplicationToolResponse(request, { schemas: entry.toolSchemas || [], turn: active, signal,
|
|
118
|
+
...(suppliedTools ? {
|
|
119
|
+
assertCurrent(native) {
|
|
120
|
+
if (!currentApplicationCommand(entry, active, owner) || active.nativeOwner !== owner ||
|
|
121
|
+
native.messageId !== active.messageId) throw new Error("Claude's application call belongs to a retired input.");
|
|
122
|
+
},
|
|
123
|
+
onExecutionFailure(error, native) {
|
|
124
|
+
// Already-invoked old work retains its original result. Only
|
|
125
|
+
// this still-current authored input can fail this native run.
|
|
126
|
+
if (currentApplicationCommand(entry, active, owner) && active.nativeOwner === owner &&
|
|
127
|
+
native.messageId === active.messageId) {
|
|
128
|
+
active.toolFailure = error;
|
|
129
|
+
active.failure.reject(error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} : {}) }));
|
|
133
|
+
active?.toolWork.add(work);
|
|
134
|
+
try { return await work; }
|
|
135
|
+
finally { active?.toolWork.delete(work); }
|
|
136
|
+
}
|
|
137
|
+
} : {}),
|
|
138
|
+
onFailure: async error => {
|
|
139
|
+
if (entry.localCommand?.processToken === processToken) entry.localCommand.reject(error);
|
|
140
|
+
if (bound) {
|
|
141
|
+
entry.nativeTurn.invalidateInstructions();
|
|
142
|
+
try { await entry.nativeTurn.stopProcess(error.message); }
|
|
143
|
+
catch (cleanupError) { error = cleanupError; await context.reportFailure(cleanupError); }
|
|
144
|
+
entry.command?.completion.reject(error);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
entry.observationError = error.message;
|
|
148
|
+
await entry.nativeTurn.stopProcess(error.message);
|
|
149
|
+
}
|
|
150
|
+
}); } catch (error) {
|
|
151
|
+
if (error.executionId && error.stopProof?.scopeEmpty !== true && !entry.executionId) {
|
|
152
|
+
// Streaming startup can fail after host admission, before onStarted.
|
|
153
|
+
// Retain that custody through the same binding owner for Stop recovery.
|
|
154
|
+
error.cleanupFailed = true;
|
|
155
|
+
entry.executionId = error.executionId;
|
|
156
|
+
try {
|
|
157
|
+
await store.save(entry, { execution: true });
|
|
158
|
+
await onEvent(entry, { type: "execution", context, executionId: entry.executionId });
|
|
159
|
+
} catch (bindingError) { error.bindingError = bindingError; }
|
|
160
|
+
}
|
|
161
|
+
if (bound && error.stopProof?.scopeEmpty) {
|
|
162
|
+
await store.releaseExecution(entry);
|
|
163
|
+
if (entry.startupCleanup?.executionId === error.executionId) entry.startupCleanup = null;
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
} finally {
|
|
167
|
+
if (startupCleanup) startupCleanup.pending = false;
|
|
168
|
+
}
|
|
169
|
+
try { assertPreparing(); }
|
|
170
|
+
catch (error) {
|
|
171
|
+
await entry.nativeTurn.stopProcess(error.message);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if (entry.startupCleanup?.executionId === entry.executionId) entry.startupCleanup = null;
|
|
175
|
+
entry.processToken = processToken;
|
|
176
|
+
if (entry.pausedGoal || entry.blockedGoal) {
|
|
177
|
+
try {
|
|
178
|
+
const id = await runLocalCommand(entry, "/goal clear", { name: "goal", timeoutMs: 30_000,
|
|
179
|
+
failureMessage: "Claude could not suspend its retained goal checker.", failureCode: "conversation_claude_goal_suspend_failed",
|
|
180
|
+
timeoutMessage: "Claude timed out suspending its retained goal checker.", timeoutCode: "conversation_claude_goal_suspend_timeout" });
|
|
181
|
+
const cleared = await readHistory(entry);
|
|
182
|
+
if (cleared.goal || cleared.goalCommandId !== id) {
|
|
183
|
+
throw createError("Claude has not saved the retained goal's native clear receipt.", { code: "conversation_claude_goal_clear_unconfirmed" });
|
|
184
|
+
}
|
|
185
|
+
assertPreparing();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
await entry.nativeTurn.stopProcess(error.message);
|
|
188
|
+
throw Object.assign(error, { delivery: "not-sent" });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return entry.process;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function runLocalCommand(entry, command, { name, timeoutMs, failureMessage, failureCode, timeoutMessage, timeoutCode }) {
|
|
195
|
+
const completion = Promise.withResolvers();
|
|
196
|
+
completion.promise.catch(() => {});
|
|
197
|
+
const local = { ...completion, id: randomUUID(), name, failureMessage, failureCode, processToken: entry.processToken };
|
|
198
|
+
entry.localCommand = local;
|
|
199
|
+
const timer = setTimeout(() => completion.reject(createError(timeoutMessage, { code: timeoutCode })), timeoutMs);
|
|
200
|
+
try {
|
|
201
|
+
await entry.process.client.send(command, { messageId: local.id, sessionId: entry.id });
|
|
202
|
+
await completion.promise;
|
|
203
|
+
return local.id;
|
|
204
|
+
} finally {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
if (entry.localCommand === local) entry.localCommand = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
startProcess,
|
|
212
|
+
runLocalCommand
|
|
213
|
+
});
|
|
214
|
+
}
|