@jskit-ai/assistant-core 0.1.212 → 0.1.214
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
- package/package.json +5 -5
- package/src/client/conversation/AssistantConversationElement.vue +5 -1
- package/src/client/conversation/AssistantTranscript.vue +10 -1
- package/src/server/conversation/agentRun.js +38 -11
- package/src/server/conversation/claudeHistory.js +40 -5
- package/src/server/conversation/claudeProcess.js +28 -4
- package/src/server/conversation/claudeTools.js +11 -2
- package/src/server/conversation/claudeTurn.js +213 -23
- package/src/server/conversation/codexConfiguration.js +1 -1
- package/src/server/conversation/codexHistoryAdapter.js +11 -4
- package/src/server/conversation/codexNotificationQueue.js +11 -2
- package/src/server/conversation/codexObservationControl.js +8 -8
- package/src/server/conversation/codexOutput.js +20 -15
- package/src/server/conversation/codexProvider.js +40 -3
- package/src/server/conversation/codexProviderOwner.js +110 -1
- package/src/server/conversation/codexTurn.js +3 -0
- package/src/server/conversation/continuity.js +24 -6
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/nativeHistoryExport.js +97 -0
- package/src/server/conversation/openCodeClient.js +47 -1
- package/src/server/conversation/openCodeRuntime.js +183 -28
- package/src/server/conversation/openCodeTurn.js +65 -24
- package/src/server/conversation/providers/api.js +7 -0
- package/src/server/conversation/providers/claudeDriver.js +52 -7
- package/src/server/conversation/providers/codex.js +19 -2
- package/src/server/conversation/providers/codexDriver.js +92 -14
- package/src/server/conversation/providers/opencodeDriver.js +31 -9
- package/src/server/conversation/runtime.js +424 -65
- package/src/server/conversation/runtimeStateUpgrade.js +77 -2
- package/src/server/conversation/streams.js +25 -5
- package/src/server/conversation/tools.js +2 -2
- package/src/server/conversation/transcript.js +23 -8
- package/src/server/hosts/claudeHistory.js +1 -0
- package/src/server/hosts/claudeProcess.js +1 -0
- package/src/server/hosts/codexProvider.js +3 -0
- package/src/server/hosts/nativeHistory.js +1 -0
- package/src/server/hosts/openCodeClient.js +1 -0
- package/src/server/hosts/openCodeTurn.js +1 -0
- package/src/server/index.js +1 -1
- package/src/server/lib/assistantToolLoop.js +99 -2
- package/test/aiConnectionClient.test.js +57 -0
- package/test/assistantBoundedToolLoop.test.js +332 -0
- package/test/assistantScroll.browser.test.js +151 -0
- package/test/claudeConversationSession.test.js +1417 -3
- package/test/claudeProcess.test.js +175 -0
- package/test/claudeTurn.test.js +466 -0
- package/test/codexConversationSession.test.js +1257 -12
- package/test/codexHistoryAdapter.test.js +74 -1
- package/test/codexTurn.test.js +282 -1
- package/test/conversation.test.js +83 -0
- package/test/conversationAgentRun.test.js +181 -0
- package/test/conversationRuntime.test.js +95 -0
- package/test/conversationRuntimeStateUpgrade.test.js +89 -0
- package/test/conversationStreaming.test.js +85 -0
- package/test/conversationTools.test.js +165 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +372 -0
- package/test/opencodeConversationSession.test.js +637 -5
|
@@ -149,8 +149,20 @@ export function createClaudeConversationOwner({
|
|
|
149
149
|
phase: entry.turn.active && !entry.observationError ? text(entry.turn.phase) : "" } : null;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function currentApplicationCommand(entry, command, owner = command?.nativeOwner) {
|
|
153
|
+
const native = entry?.nativeTurn.read();
|
|
154
|
+
return Boolean(owner && entry && !closing && !closingSessions.has(entry.context.key) &&
|
|
155
|
+
entries.get(entry.key) === entry && entry.command === command && command.entry === entry &&
|
|
156
|
+
command.admitted && !command.signal.aborted && !command.toolFailure &&
|
|
157
|
+
!entry.disposed && !entry.stopping && !entry.stopPending && !entry.observationError &&
|
|
158
|
+
entry.process === owner.process && entry.executionId === owner.executionId &&
|
|
159
|
+
entry.accountIdentity === owner.accountIdentity && entry.id === owner.threadId &&
|
|
160
|
+
entry.turn?.id === owner.turnId && entry.turn.active && native.turnId === owner.turnId && native.active);
|
|
161
|
+
}
|
|
162
|
+
|
|
152
163
|
function readRetainedTurn(contextKey, conversationId, state) {
|
|
153
164
|
const entry = entries.get(`${contextKey}\0${conversationId}`);
|
|
165
|
+
if (state.current) return currentApplicationCommand(entry, entry?.command) ? snapshot(entry) : null;
|
|
154
166
|
const saved = state.saved;
|
|
155
167
|
return state.nativeResult?.turn || (entry ? snapshot(entry) : saved?.turnId ? {
|
|
156
168
|
id: saved.turnId, threadId: conversationId, state: saved.state,
|
|
@@ -158,6 +170,15 @@ export function createClaudeConversationOwner({
|
|
|
158
170
|
} : null);
|
|
159
171
|
}
|
|
160
172
|
|
|
173
|
+
// This lookup uses only the existing current entry and native receipt owner.
|
|
174
|
+
function readFinalAssistantResult(contextKey, conversationId = "", turnId = "") {
|
|
175
|
+
if (!text(contextKey) || !text(conversationId) || !text(turnId) || closing || closingSessions.has(contextKey)) return null;
|
|
176
|
+
const entry = entries.get(`${contextKey}\0${conversationId}`);
|
|
177
|
+
if (!entry || entry.disposed || entry.stopping || entry.observationError ||
|
|
178
|
+
entry.id !== conversationId || entry.context.key !== contextKey || entry.turn?.id !== turnId) return null;
|
|
179
|
+
return entry.nativeTurn.readFinalAssistantResult(conversationId, turnId);
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
function readHistory(entry) {
|
|
162
183
|
return readClaudeHistory({ configRoot, workdir: entry.nativeWorkdir, conversationId: entry.id });
|
|
163
184
|
}
|
|
@@ -182,7 +203,7 @@ export function createClaudeConversationOwner({
|
|
|
182
203
|
if (saved && typeof saved.then === "function") saved = await saved;
|
|
183
204
|
if (!main && !saved && !create && !recoveringCleanup) throw createError("This Claude conversation is unavailable.");
|
|
184
205
|
const state = saved ? JSON.parse(saved) : {};
|
|
185
|
-
const entry = { id, key, main, persistent: state.persistent === true, context, process: null,
|
|
206
|
+
const entry = { id, key, main, persistent: state.persistent === true, context, process: null, startupCleanup: null,
|
|
186
207
|
executionId: state.executionId || (recoveringCleanup ? cleanupExecutionId : ""),
|
|
187
208
|
accountIdentities: state.accountIdentities || (state.accountIdentity ? { anthropic: state.accountIdentity } : {}),
|
|
188
209
|
accountIdentity: state.accountIdentity || "", sent: state.sent === true, nativeWorkdir: state.nativeWorkdir || nativeWorkdir,
|
|
@@ -231,6 +252,7 @@ export function createClaudeConversationOwner({
|
|
|
231
252
|
const account = preparation.account(entry);
|
|
232
253
|
const identity = await account.identity;
|
|
233
254
|
const providerId = account.providerId;
|
|
255
|
+
if (entry.accountIdentity !== identity) entry.nativeTurn.invalidateFinalAssistantResult();
|
|
234
256
|
try {
|
|
235
257
|
await bindClaudeConversationAccount(entry, { identity, providerId,
|
|
236
258
|
stop: (entry, reason) => entry.nativeTurn.stopProcess(reason), save: saveAccount });
|
|
@@ -290,7 +312,9 @@ export function createClaudeConversationOwner({
|
|
|
290
312
|
if (options && typeof options.then === "function") options = await options;
|
|
291
313
|
try { entry.process = await createProcess({ ...options,
|
|
292
314
|
workdir: entry.nativeWorkdir, sessionId: entry.id, resume: entry.sent,
|
|
293
|
-
onStarted: async executionId => {
|
|
315
|
+
onStarted: async (executionId, stop) => {
|
|
316
|
+
if (typeof stop === "function") entry.startupCleanup = { executionId, stop };
|
|
317
|
+
if (entry.executionId === executionId) return;
|
|
294
318
|
entry.executionId = executionId;
|
|
295
319
|
await store.save(entry, { execution: true });
|
|
296
320
|
await onEvent(entry, { type: "execution", context, executionId });
|
|
@@ -310,10 +334,27 @@ export function createClaudeConversationOwner({
|
|
|
310
334
|
}
|
|
311
335
|
await entry.nativeTurn.receive(frame);
|
|
312
336
|
},
|
|
313
|
-
...(store.releaseExecution ? {
|
|
337
|
+
...(store.releaseExecution || entry.command?.tools ? {
|
|
314
338
|
async onControlRequest(request, { signal }) {
|
|
315
339
|
const active = entry.command;
|
|
316
|
-
const
|
|
340
|
+
const suppliedTools = !store.releaseExecution && active?.tools;
|
|
341
|
+
const owner = active?.nativeOwner;
|
|
342
|
+
const work = claudeApplicationToolResponse(request, { schemas: entry.toolSchemas || [], turn: active, signal,
|
|
343
|
+
...(suppliedTools ? {
|
|
344
|
+
assertCurrent(native) {
|
|
345
|
+
if (!currentApplicationCommand(entry, active, owner) || active.nativeOwner !== owner ||
|
|
346
|
+
native.messageId !== active.messageId) throw new Error("Claude's application call belongs to a retired input.");
|
|
347
|
+
},
|
|
348
|
+
onExecutionFailure(error, native) {
|
|
349
|
+
// Already-invoked old work retains its original result. Only
|
|
350
|
+
// this still-current authored input can fail this native run.
|
|
351
|
+
if (currentApplicationCommand(entry, active, owner) && active.nativeOwner === owner &&
|
|
352
|
+
native.messageId === active.messageId) {
|
|
353
|
+
active.toolFailure = error;
|
|
354
|
+
active.failure.reject(error);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} : {}) });
|
|
317
358
|
active?.toolWork.add(work);
|
|
318
359
|
try { return await work; }
|
|
319
360
|
finally { active?.toolWork.delete(work); }
|
|
@@ -331,13 +372,27 @@ export function createClaudeConversationOwner({
|
|
|
331
372
|
await entry.nativeTurn.stopProcess(error.message);
|
|
332
373
|
}
|
|
333
374
|
}); } catch (error) {
|
|
334
|
-
if (
|
|
375
|
+
if (error.executionId && error.stopProof?.scopeEmpty !== true && !entry.executionId) {
|
|
376
|
+
// Streaming startup can fail after host admission, before onStarted.
|
|
377
|
+
// Retain that custody through the same binding owner for Stop recovery.
|
|
378
|
+
error.cleanupFailed = true;
|
|
379
|
+
entry.executionId = error.executionId;
|
|
380
|
+
try {
|
|
381
|
+
await store.save(entry, { execution: true });
|
|
382
|
+
await onEvent(entry, { type: "execution", context, executionId: entry.executionId });
|
|
383
|
+
} catch (bindingError) { error.bindingError = bindingError; }
|
|
384
|
+
}
|
|
385
|
+
if (store.releaseExecution && error.stopProof?.scopeEmpty) {
|
|
386
|
+
await store.releaseExecution(entry);
|
|
387
|
+
if (entry.startupCleanup?.executionId === error.executionId) entry.startupCleanup = null;
|
|
388
|
+
}
|
|
335
389
|
throw error;
|
|
336
390
|
}
|
|
391
|
+
if (entry.startupCleanup?.executionId === entry.executionId) entry.startupCleanup = null;
|
|
337
392
|
return entry.process;
|
|
338
393
|
}
|
|
339
394
|
|
|
340
|
-
async function send(entry, input = {}, { renewal = false, command } = {}) {
|
|
395
|
+
async function send(entry, input = {}, { renewal = false, command, applicationCommand } = {}) {
|
|
341
396
|
const message = command ? input.text : text(input.message || input.prompt);
|
|
342
397
|
if (!command && !message) throw createError("Enter a message for Claude.");
|
|
343
398
|
const context = entry.context;
|
|
@@ -386,8 +441,17 @@ export function createClaudeConversationOwner({
|
|
|
386
441
|
entry.onEvent = context.onEvent;
|
|
387
442
|
entry.lastMessageId = uuid;
|
|
388
443
|
const actorMetadata = command ? undefined : await onEvent(entry, { type: "message-metadata", input, context });
|
|
444
|
+
const nativeOwner = applicationCommand ? { process: native, executionId: entry.executionId,
|
|
445
|
+
accountIdentity: entry.accountIdentity, threadId: entry.id, turnId: entry.turn.id } : null;
|
|
389
446
|
let conversationTurn;
|
|
447
|
+
function deliveryResult() {
|
|
448
|
+
return { ok: true, delivered: true, deliveryMode: steering ? "steer" : "new_turn",
|
|
449
|
+
thread: { id: entry.id }, turn: snapshot(entry), workdir: context.workdir, conversationTurn };
|
|
450
|
+
}
|
|
390
451
|
async function accept() {
|
|
452
|
+
// The new native ACK retires old effect custody while its canonical
|
|
453
|
+
// admission/commit is awaited; it cannot retag a delayed old failure.
|
|
454
|
+
if (applicationCommand) applicationCommand.active.nativeOwner = null;
|
|
391
455
|
if (command) {
|
|
392
456
|
if (!steering) await onEvent(entry, { type: "binding-admitted" });
|
|
393
457
|
await command.accept();
|
|
@@ -401,6 +465,22 @@ export function createClaudeConversationOwner({
|
|
|
401
465
|
if (admission) conversationTurn = await admission;
|
|
402
466
|
}
|
|
403
467
|
if (!command) await entry.nativeTurn.updateState("active");
|
|
468
|
+
if (applicationCommand && conversationTurn) {
|
|
469
|
+
const value = deliveryResult();
|
|
470
|
+
applicationCommand.active.delivery = { value, completion: entry.completion?.promise };
|
|
471
|
+
// The original canonical admission and publication precede Core's same
|
|
472
|
+
// accepted-request gate; native tools cannot race an unaccepted user.
|
|
473
|
+
await applicationCommand.accept({ nativeResult: { value }, conversationTurn, nativeTurnId: entry.turn?.id });
|
|
474
|
+
if (entry.process !== nativeOwner.process || entry.executionId !== nativeOwner.executionId ||
|
|
475
|
+
entry.accountIdentity !== nativeOwner.accountIdentity || entry.id !== nativeOwner.threadId ||
|
|
476
|
+
entry.turn?.id !== nativeOwner.turnId || entry.disposed || entry.stopping || entry.stopPending) {
|
|
477
|
+
throw new Error("Claude's admitted tools no longer belong to this native process.");
|
|
478
|
+
}
|
|
479
|
+
applicationCommand.active.nativeOwner = nativeOwner;
|
|
480
|
+
applicationCommand.active.messageId = messageId;
|
|
481
|
+
if (steering) applicationCommand.active.nativeToolCount = 0;
|
|
482
|
+
applicationCommand.active.admitted = true;
|
|
483
|
+
}
|
|
404
484
|
}
|
|
405
485
|
try {
|
|
406
486
|
if (command) command.attempted = true;
|
|
@@ -411,8 +491,7 @@ export function createClaudeConversationOwner({
|
|
|
411
491
|
if (!command) await entry.nativeTurn.stopProcess(error.message);
|
|
412
492
|
throw error;
|
|
413
493
|
}
|
|
414
|
-
return
|
|
415
|
-
thread: { id: entry.id }, turn: snapshot(entry), workdir: context.workdir, conversationTurn };
|
|
494
|
+
return deliveryResult();
|
|
416
495
|
}
|
|
417
496
|
|
|
418
497
|
async function startTurn(entry, input = {}) {
|
|
@@ -641,10 +720,19 @@ export function createClaudeConversationOwner({
|
|
|
641
720
|
}
|
|
642
721
|
command.signal.throwIfAborted();
|
|
643
722
|
const entry = await acquire(command.context);
|
|
644
|
-
const
|
|
723
|
+
const active = control.current;
|
|
724
|
+
const applicationCommand = active?.tools ? { active, accept: command.accept } : null;
|
|
725
|
+
if (applicationCommand) {
|
|
726
|
+
if (entry.disposed || entry.command && entry.command !== active) throw new Error("This Claude conversation is closed or already working.");
|
|
727
|
+
if (active.entry && active.entry !== entry) throw new Error("Claude's application tools belong to another native conversation.");
|
|
728
|
+
active.entry = entry;
|
|
729
|
+
entry.command = active;
|
|
730
|
+
entry.toolSchemas = active.tools.schemas;
|
|
731
|
+
}
|
|
732
|
+
const value = await send(entry, { ...command.input.nativeMessage, onPromptSending: command.beforeDispatch }, { applicationCommand });
|
|
645
733
|
const completion = value.conversationTurn ? entry.completion?.promise : null;
|
|
646
734
|
const delivered = { value, completion };
|
|
647
|
-
if (value.delivered === true && value.conversationTurn) {
|
|
735
|
+
if (!applicationCommand && value.delivered === true && value.conversationTurn) {
|
|
648
736
|
if (control.current) control.current.delivery = delivered;
|
|
649
737
|
await command.accept({ nativeResult: { value }, conversationTurn: value.conversationTurn, nativeTurnId: value.turn?.id });
|
|
650
738
|
}
|
|
@@ -660,10 +748,10 @@ export function createClaudeConversationOwner({
|
|
|
660
748
|
async function run(control, command, { acquire, reportFailure, entry: selectedEntry, maximumOutput, maximumToolCalls = 32, timeoutMs } = {}) {
|
|
661
749
|
if (control.disposed || control.current) throw new Error("This Claude conversation is closed or already working.");
|
|
662
750
|
const binding = Boolean(store.releaseExecution);
|
|
663
|
-
const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent } = command;
|
|
751
|
+
const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent, onNativeToolUse } = command;
|
|
664
752
|
const completion = Promise.withResolvers();
|
|
665
753
|
completion.promise.catch(() => {});
|
|
666
|
-
const active = { completion, onMessage, onEvent, signal, tools, admitted: false, nativeTools: new Map(),
|
|
754
|
+
const active = { completion, onMessage, onEvent, onNativeToolUse, signal, tools, admitted: false, nativeTools: new Map(),
|
|
667
755
|
messageId: input.messageId, goal: input.goal, nativeToolCount: 0, toolWork: new Set(), maximumToolCalls,
|
|
668
756
|
context, failure: Promise.withResolvers() };
|
|
669
757
|
let failure;
|
|
@@ -734,12 +822,29 @@ export function createClaudeConversationOwner({
|
|
|
734
822
|
if (binding) {
|
|
735
823
|
try {
|
|
736
824
|
if (problem || signal.aborted) await (cancellation || entry.nativeTurn.stopProcess(problem?.message || "Work stopped."));
|
|
737
|
-
} catch (error) {
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (problem && error !== problem) error.cause ||= problem;
|
|
827
|
+
problem = error;
|
|
828
|
+
}
|
|
738
829
|
finally {
|
|
739
830
|
await Promise.allSettled([...active.toolWork]);
|
|
740
831
|
if (entry?.command === active) entry.command = null;
|
|
741
832
|
}
|
|
742
|
-
} else
|
|
833
|
+
} else {
|
|
834
|
+
await cancellation?.catch(() => {});
|
|
835
|
+
if (tools && active.entry) {
|
|
836
|
+
try {
|
|
837
|
+
if (problem || signal.aborted) await active.entry.nativeTurn.stopProcess(problem?.message || "Work stopped.");
|
|
838
|
+
} catch (error) {
|
|
839
|
+
if (problem && error !== problem) error.cause ||= problem;
|
|
840
|
+
problem = error;
|
|
841
|
+
}
|
|
842
|
+
finally {
|
|
843
|
+
await Promise.allSettled([...active.toolWork]);
|
|
844
|
+
if (active.entry.command === active) active.entry.command = null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
743
848
|
if (control.current === active) control.current = null;
|
|
744
849
|
if (problem) {
|
|
745
850
|
if (binding && nativeDispatch.attempted && !nativeDispatch.admitted) problem.delivery = "uncertain";
|
|
@@ -748,7 +853,7 @@ export function createClaudeConversationOwner({
|
|
|
748
853
|
if (!binding) return { value: delivered.value };
|
|
749
854
|
}
|
|
750
855
|
|
|
751
|
-
return Object.freeze({ entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readHistory, ensureReady, ensureProcess, stopForTerminal, prepareTerminal, send, startTurn,
|
|
856
|
+
return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, stopForTerminal, prepareTerminal, send, startTurn,
|
|
752
857
|
interrupt, read, inspectAdmission, hasActiveTemporaryConversation, hasActiveConversation, retireConversationHistory, reconcileSessions, restoreSessionEntries, closeSession, closeProject, invalidateRuntimes, drain, forget, close, deleteConversation, recover, stopSelected, wait, stopConversation,
|
|
753
858
|
readGoal, updateGoal, renewal, run, steer, cancel });
|
|
754
859
|
}
|
|
@@ -773,6 +878,36 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
773
878
|
let cancelling = false;
|
|
774
879
|
let steering;
|
|
775
880
|
let maximumOutput = 4 * 1024 * 1024;
|
|
881
|
+
let inputOwner = null;
|
|
882
|
+
let acceptedInput = null;
|
|
883
|
+
let publishedAssistant = null;
|
|
884
|
+
let finalAssistantResult = null;
|
|
885
|
+
|
|
886
|
+
function clearFinalAssistantResult() {
|
|
887
|
+
publishedAssistant = null;
|
|
888
|
+
finalAssistantResult = null;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function invalidateFinalAssistantResult() {
|
|
892
|
+
inputOwner = null;
|
|
893
|
+
acceptedInput = null;
|
|
894
|
+
clearFinalAssistantResult();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function currentReceiptOwner(owner) {
|
|
898
|
+
return Boolean(entry && owner && inputOwner === owner && acceptedInput === owner &&
|
|
899
|
+
owner.commandId === currentCommandId && entry.process && entry.process === owner.process &&
|
|
900
|
+
entry.executionId === owner.executionId && entry.accountIdentity === owner.accountIdentity &&
|
|
901
|
+
entry.turn?.id === turnId && !entry.stopping && !entry.stopPending && !entry.disposed &&
|
|
902
|
+
!entry.observationError && !entry.command?.toolWork.size && !entry.command?.toolFailure);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function readFinalAssistantResult(threadId, expectedTurnId) {
|
|
906
|
+
if (threadId !== conversationId || expectedTurnId !== turnId || !finalAssistantResult ||
|
|
907
|
+
active || stopped || cancelling || outcome?.status !== "completed" || outcome.error ||
|
|
908
|
+
inFlight.size || tasks.size || admissions.size || !currentReceiptOwner(acceptedInput)) return null;
|
|
909
|
+
return structuredClone(finalAssistantResult);
|
|
910
|
+
}
|
|
776
911
|
|
|
777
912
|
function read() {
|
|
778
913
|
return { turnId, active, text: result, phase, status: active ? "inProgress" : outcome?.status || "ready",
|
|
@@ -810,6 +945,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
810
945
|
}
|
|
811
946
|
|
|
812
947
|
async function stopProcess(reason = "") {
|
|
948
|
+
invalidateFinalAssistantResult();
|
|
813
949
|
// Runtime bindings commit execution release before dropping the handle.
|
|
814
950
|
// Retained host entries keep their original post-proof checkpoint order.
|
|
815
951
|
const binding = Boolean(process.releaseExecution);
|
|
@@ -817,10 +953,11 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
817
953
|
const active = read().active;
|
|
818
954
|
if (!binding) entry.stopping = true;
|
|
819
955
|
const operation = async () => {
|
|
956
|
+
const startup = entry.startupCleanup?.executionId === entry.executionId ? entry.startupCleanup : null;
|
|
820
957
|
let proof;
|
|
821
958
|
try {
|
|
822
959
|
if (!binding || entry.process || entry.executionId) {
|
|
823
|
-
proof = await stopClaudeCodeProcess({ process: entry.process, executionId: entry.executionId,
|
|
960
|
+
proof = await stopClaudeCodeProcess({ process: entry.process || startup, executionId: entry.executionId,
|
|
824
961
|
stopExecution: process.stopExecution });
|
|
825
962
|
}
|
|
826
963
|
} catch (error) {
|
|
@@ -836,12 +973,25 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
836
973
|
entry.exitProof = proof;
|
|
837
974
|
stop(binding ? reason : reason || "Claude stopped before acknowledging the prompt.");
|
|
838
975
|
if (binding) {
|
|
976
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
839
977
|
if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
|
|
840
978
|
return;
|
|
841
979
|
}
|
|
842
|
-
if (entry.turn?.active)
|
|
843
|
-
|
|
980
|
+
if (entry.turn?.active) {
|
|
981
|
+
try { await updateState("interrupted", reason); }
|
|
982
|
+
catch (error) {
|
|
983
|
+
// Native cleanup is verified even when the host refuses its checkpoint.
|
|
984
|
+
// Retain that refusal without publishing a completed answer or active work.
|
|
985
|
+
setState("interrupted", error.message);
|
|
986
|
+
await onEvent({ type: "save" });
|
|
987
|
+
await onEvent({ type: "state", state: "interrupted", active: false, message: error.message });
|
|
988
|
+
entry.completion?.resolve(readResult());
|
|
989
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
} else await onEvent({ type: "save" });
|
|
844
993
|
entry.completion?.resolve(readResult());
|
|
994
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
845
995
|
return entry.exitProof;
|
|
846
996
|
};
|
|
847
997
|
if (!binding) return operation();
|
|
@@ -864,7 +1014,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
864
1014
|
if (binding) setState(state, event.error);
|
|
865
1015
|
else await updateState(state, event.error);
|
|
866
1016
|
}
|
|
867
|
-
await onEvent(event);
|
|
1017
|
+
const published = await onEvent(event);
|
|
868
1018
|
if (event.type === "provider-event") {
|
|
869
1019
|
const frame = event.event;
|
|
870
1020
|
const change = frame.type === "active_goal" ? "goal" : frame.type === "rate_limit_event" ? "usage" : "";
|
|
@@ -874,6 +1024,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
874
1024
|
if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
|
|
875
1025
|
entry.completion?.resolve(binding ? event : readResult());
|
|
876
1026
|
}
|
|
1027
|
+
return published;
|
|
877
1028
|
}
|
|
878
1029
|
|
|
879
1030
|
function checkOutput(value, role = "assistant") {
|
|
@@ -884,7 +1035,16 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
884
1035
|
|
|
885
1036
|
async function publishMessage(message) {
|
|
886
1037
|
messages.set(message.id, message);
|
|
887
|
-
|
|
1038
|
+
const owner = acceptedInput;
|
|
1039
|
+
const completeAssistant = message.complete && message.role === "assistant";
|
|
1040
|
+
if (completeAssistant) clearFinalAssistantResult();
|
|
1041
|
+
const conversationTurn = await publish({ type: "message", message });
|
|
1042
|
+
if (completeAssistant && message.text && currentReceiptOwner(owner) && !stopped && !cancelling &&
|
|
1043
|
+
conversationTurn && typeof conversationTurn === "object" && !Array.isArray(conversationTurn)) {
|
|
1044
|
+
publishedAssistant = { owner, itemId: message.id, outputId: message.outputId || message.id,
|
|
1045
|
+
text: message.text, conversationTurn: structuredClone(conversationTurn) };
|
|
1046
|
+
}
|
|
1047
|
+
return conversationTurn;
|
|
888
1048
|
}
|
|
889
1049
|
|
|
890
1050
|
async function receive(frame) {
|
|
@@ -908,9 +1068,12 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
908
1068
|
clearTimeout(pending.timer);
|
|
909
1069
|
try {
|
|
910
1070
|
await pending.accept();
|
|
1071
|
+
if (inputOwner === pending.receiptOwner && !stopped && !cancelling) acceptedInput = pending.receiptOwner;
|
|
911
1072
|
admissions.delete(uuid);
|
|
912
1073
|
pending.resolve();
|
|
913
1074
|
} catch (error) {
|
|
1075
|
+
acceptedInput = null;
|
|
1076
|
+
clearFinalAssistantResult();
|
|
914
1077
|
pending.reject(error);
|
|
915
1078
|
throw error;
|
|
916
1079
|
}
|
|
@@ -988,12 +1151,22 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
988
1151
|
outcome && inFlight.size === 0 && tasks.size === 0) {
|
|
989
1152
|
active = false;
|
|
990
1153
|
phase = "";
|
|
991
|
-
|
|
1154
|
+
const candidate = publishedAssistant;
|
|
1155
|
+
finalAssistantResult = outcome.status === "completed" && !outcome.error && !stopped && !cancelling &&
|
|
1156
|
+
admissions.size === 0 && candidate && currentReceiptOwner(candidate.owner) && candidate.text === result
|
|
1157
|
+
? { threadId: conversationId, turnId, inputMessageId: candidate.owner.commandId,
|
|
1158
|
+
itemId: candidate.itemId, outputId: candidate.outputId, text: candidate.text,
|
|
1159
|
+
conversationTurn: candidate.conversationTurn } : null;
|
|
1160
|
+
try { await publish({ type: "settled", ...read(), source: frame.type }); }
|
|
1161
|
+
catch (error) { clearFinalAssistantResult(); throw error; }
|
|
992
1162
|
}
|
|
993
1163
|
}
|
|
994
1164
|
|
|
995
1165
|
function stop(reason = "Claude stopped before acknowledging the prompt.") {
|
|
996
1166
|
stopped = true;
|
|
1167
|
+
inputOwner = null;
|
|
1168
|
+
acceptedInput = null;
|
|
1169
|
+
clearFinalAssistantResult();
|
|
997
1170
|
for (const pending of admissions.values()) {
|
|
998
1171
|
pending.reject(failure(reason));
|
|
999
1172
|
}
|
|
@@ -1008,12 +1181,13 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1008
1181
|
|
|
1009
1182
|
async function interrupt(client) {
|
|
1010
1183
|
cancelling = true;
|
|
1184
|
+
clearFinalAssistantResult();
|
|
1011
1185
|
if (client) await client.interrupt();
|
|
1012
1186
|
// The process host still must drain its owned scope and call stop().
|
|
1013
1187
|
}
|
|
1014
1188
|
|
|
1015
1189
|
return Object.freeze({
|
|
1016
|
-
receive, read, stop, interrupt,
|
|
1190
|
+
receive, read, stop, interrupt, readFinalAssistantResult, invalidateFinalAssistantResult,
|
|
1017
1191
|
...(entry ? { readResult, updateState, stopProcess } : {}),
|
|
1018
1192
|
...(instructions ? {
|
|
1019
1193
|
prepare: instructions.prepare,
|
|
@@ -1024,6 +1198,9 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1024
1198
|
begin({ id, maxOutputCharacters = 4 * 1024 * 1024 } = {}) {
|
|
1025
1199
|
if (active) throw failure("This Claude turn is still working.");
|
|
1026
1200
|
if (!id || !Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 1) throw new TypeError("Invalid Claude turn configuration.");
|
|
1201
|
+
inputOwner = null;
|
|
1202
|
+
acceptedInput = null;
|
|
1203
|
+
clearFinalAssistantResult();
|
|
1027
1204
|
turnId = id;
|
|
1028
1205
|
maximumOutput = maxOutputCharacters;
|
|
1029
1206
|
messages.clear();
|
|
@@ -1041,10 +1218,18 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1041
1218
|
messages.clear();
|
|
1042
1219
|
result = "";
|
|
1043
1220
|
outcome = null;
|
|
1221
|
+
inputOwner = null;
|
|
1222
|
+
acceptedInput = null;
|
|
1223
|
+
clearFinalAssistantResult();
|
|
1044
1224
|
},
|
|
1045
1225
|
async send(client, message, { messageId: uuid, accept, timeoutMs = 30_000 } = {}) {
|
|
1046
1226
|
if (!active || stopped || cancelling) throw failure("Claude has no active turn for this message.");
|
|
1047
1227
|
if (!uuid || typeof accept !== "function" || admissions.has(uuid)) throw new TypeError("A distinct message id and admission writer are required.");
|
|
1228
|
+
clearFinalAssistantResult();
|
|
1229
|
+
acceptedInput = null;
|
|
1230
|
+
inputOwner = entry ? { commandId: uuid, process: entry.process, executionId: entry.executionId,
|
|
1231
|
+
accountIdentity: entry.accountIdentity } : null;
|
|
1232
|
+
const receiptOwner = inputOwner;
|
|
1048
1233
|
const admitted = Promise.withResolvers();
|
|
1049
1234
|
void admitted.promise.catch(() => {});
|
|
1050
1235
|
inFlight.add(uuid);
|
|
@@ -1052,9 +1237,12 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1052
1237
|
failure("Claude has not acknowledged this prompt. Its delivery is uncertain.", "assistant_claude_admission_unknown"),
|
|
1053
1238
|
{ delivery: "uncertain" }
|
|
1054
1239
|
)), timeoutMs);
|
|
1055
|
-
admissions.set(uuid, { ...admitted, accept, timer });
|
|
1240
|
+
admissions.set(uuid, { ...admitted, accept, timer, receiptOwner });
|
|
1056
1241
|
try {
|
|
1057
1242
|
await Promise.all([client.send(message, { messageId: uuid, sessionId: conversationId }), admitted.promise]);
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
if (inputOwner === receiptOwner) invalidateFinalAssistantResult();
|
|
1245
|
+
throw error;
|
|
1058
1246
|
} finally {
|
|
1059
1247
|
clearTimeout(timer);
|
|
1060
1248
|
}
|
|
@@ -1064,6 +1252,8 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1064
1252
|
// Streaming user input otherwise queues behind the entire running turn.
|
|
1065
1253
|
// Claude's supported steering path interrupts generation, then continues
|
|
1066
1254
|
// the same native history with the new instruction.
|
|
1255
|
+
acceptedInput = null;
|
|
1256
|
+
clearFinalAssistantResult();
|
|
1067
1257
|
steering = Promise.withResolvers();
|
|
1068
1258
|
let timer;
|
|
1069
1259
|
try {
|
|
@@ -943,7 +943,7 @@ export function codexProviderModelCatalog({ models }) {
|
|
|
943
943
|
supports_reasoning_summaries: false,
|
|
944
944
|
default_reasoning_summary: "none",
|
|
945
945
|
support_verbosity: false,
|
|
946
|
-
truncation_policy: { mode: "
|
|
946
|
+
truncation_policy: { mode: "tokens", limit: 10000 },
|
|
947
947
|
context_window: model.contextWindow,
|
|
948
948
|
max_context_window: model.contextWindow,
|
|
949
949
|
effective_context_window_percent: 95,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { open, realpath } from "node:fs/promises";
|
|
2
|
+
import { isUtf8 } from "node:buffer";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { Readable } from "node:stream";
|
|
@@ -120,8 +121,9 @@ function retainedUserRecord(item) {
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
// Scan a fixed snapshot without retaining the lifetime transcript. A single
|
|
123
|
-
// record still has a transport-sized bound; incomplete
|
|
124
|
-
|
|
124
|
+
// record still has a transport-sized bound; recovery ignores incomplete appends.
|
|
125
|
+
// Stopped offline inspection can require complete rows without changing recovery.
|
|
126
|
+
async function* readCodexHistoryRows(file, start, end, signal, { strictComplete = false } = {}) {
|
|
125
127
|
const chunk = Buffer.alloc(64 * 1024);
|
|
126
128
|
let pending = [];
|
|
127
129
|
let size = 0;
|
|
@@ -141,7 +143,11 @@ async function* readCodexHistoryRows(file, start, end, signal) {
|
|
|
141
143
|
pending.push(Buffer.from(chunk.subarray(from, until)));
|
|
142
144
|
if (complete) {
|
|
143
145
|
signal.throwIfAborted();
|
|
144
|
-
if (size)
|
|
146
|
+
if (size) {
|
|
147
|
+
const bytes = Buffer.concat(pending, size);
|
|
148
|
+
if (strictComplete && !isUtf8(bytes)) throw compactionHistoryError("the saved history contains invalid UTF-8.");
|
|
149
|
+
yield { row: JSON.parse(bytes.toString("utf8")), offset: lineStart };
|
|
150
|
+
}
|
|
145
151
|
pending = [];
|
|
146
152
|
size = 0;
|
|
147
153
|
lineStart = offset + newline + 1;
|
|
@@ -150,6 +156,7 @@ async function* readCodexHistoryRows(file, start, end, signal) {
|
|
|
150
156
|
}
|
|
151
157
|
offset += bytesRead;
|
|
152
158
|
}
|
|
159
|
+
if (strictComplete && size) throw compactionHistoryError("the saved history has an incomplete final record.");
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
// Native compaction keeps the original rollout on disk. Foreign providers
|
|
@@ -456,4 +463,4 @@ async function prepareCodexHistory(params, client, { baseUrl, modelProviderId, .
|
|
|
456
463
|
} };
|
|
457
464
|
}
|
|
458
465
|
|
|
459
|
-
export { startCodexHistoryAdapter, translateCodexHistory, prepareCodexHistory };
|
|
466
|
+
export { startCodexHistoryAdapter, translateCodexHistory, prepareCodexHistory, readCodexHistoryRows };
|
|
@@ -40,8 +40,7 @@ export function createCodexAppServerNotificationQueue({
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
async function
|
|
44
|
-
const taskSessionKey = namespace(sessionId);
|
|
43
|
+
async function drainCodexAppServerNotificationTasksForKey(taskSessionKey) {
|
|
45
44
|
while (true) {
|
|
46
45
|
const task = codexAppServerNotificationTasks.get(taskSessionKey);
|
|
47
46
|
if (!task) {
|
|
@@ -51,6 +50,15 @@ export function createCodexAppServerNotificationQueue({
|
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
|
|
53
|
+
async function drainCodexAppServerNotificationTasks(sessionId = "") {
|
|
54
|
+
return drainCodexAppServerNotificationTasksForKey(namespace(sessionId));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function captureCodexAppServerNotificationDrain(sessionId = "") {
|
|
58
|
+
const taskSessionKey = namespace(sessionId);
|
|
59
|
+
return () => drainCodexAppServerNotificationTasksForKey(taskSessionKey);
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
function queueCodexAppServerStream(context, classification, deliver) {
|
|
55
63
|
if (isClosing() || !classification.itemId || !classification.turnId) return;
|
|
56
64
|
const pending = codexAppServerPendingStreams.get(context.sessionKey);
|
|
@@ -97,6 +105,7 @@ export function createCodexAppServerNotificationQueue({
|
|
|
97
105
|
pendingReasoning: codexAppServerPendingReasoning,
|
|
98
106
|
run: runCodexAppServerNotificationTask,
|
|
99
107
|
drain: drainCodexAppServerNotificationTasks,
|
|
108
|
+
captureDrain: captureCodexAppServerNotificationDrain,
|
|
100
109
|
stream: queueCodexAppServerStream,
|
|
101
110
|
reasoning: queueCodexAppServerReasoning
|
|
102
111
|
};
|
|
@@ -39,19 +39,19 @@ export function createCodexObservationControl({
|
|
|
39
39
|
function createCodexAppServerObservation(sessionId, context = {}) {
|
|
40
40
|
const { projectContext, target, retireProvider } = context;
|
|
41
41
|
return {
|
|
42
|
-
prepare({ provider, error }) {
|
|
42
|
+
prepare({ provider, error, threadIds }) {
|
|
43
43
|
return runInContext(projectContext, async () => {
|
|
44
44
|
const managed = target(error);
|
|
45
45
|
const threads = new Map();
|
|
46
46
|
for (const entry of codexAppServerConversations.get(namespace(sessionId))?.values() || []) {
|
|
47
|
-
if (entry.provider !== provider) continue;
|
|
47
|
+
if (entry.provider !== provider || threadIds && !threadIds.includes(entry.conversationId)) continue;
|
|
48
48
|
threads.set(entry.conversationId, entry.runId);
|
|
49
49
|
if (codexAppServerConversationTurnIsActive(entry.status) || entry.goal?.status === "active") {
|
|
50
50
|
entry.error = managed.pendingMessage;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
// A helper owns only its own threads. Main chat belongs to its managed provider.
|
|
54
|
-
if (managed.threadId) {
|
|
54
|
+
if (managed.threadId && (!threadIds || threadIds.includes(managed.threadId))) {
|
|
55
55
|
const runtime = await createRuntime();
|
|
56
56
|
const turn = turnState(await runtime.getSession(sessionId));
|
|
57
57
|
threads.set(managed.threadId, turn.turnId);
|
|
@@ -66,10 +66,10 @@ export function createCodexObservationControl({
|
|
|
66
66
|
return threads;
|
|
67
67
|
});
|
|
68
68
|
},
|
|
69
|
-
barrier({ error }) {
|
|
69
|
+
barrier({ error, threadIds }) {
|
|
70
70
|
return runInContext(projectContext, async () => {
|
|
71
71
|
const managed = target(error);
|
|
72
|
-
if (!managed.threadId) return;
|
|
72
|
+
if (!managed.threadId || threadIds && !threadIds.includes(managed.threadId)) return;
|
|
73
73
|
const runtime = await createRuntime();
|
|
74
74
|
const turn = turnState(await runtime.getSession(sessionId));
|
|
75
75
|
if (!turn.active && !codexAppServerTurnOwnsActiveGoal(turn, managed.threadId)) return;
|
|
@@ -79,12 +79,12 @@ export function createCodexObservationControl({
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
},
|
|
82
|
-
complete({ provider, error, sharedStop }) {
|
|
82
|
+
complete({ provider, error, sharedStop, threadIds }) {
|
|
83
83
|
return runInContext(projectContext, async () => {
|
|
84
84
|
const managed = target(error);
|
|
85
85
|
let stoppedRun = null;
|
|
86
86
|
let conversationStream = null;
|
|
87
|
-
if (managed.threadId) {
|
|
87
|
+
if (managed.threadId && (!threadIds || threadIds.includes(managed.threadId))) {
|
|
88
88
|
const runtime = await createRuntime();
|
|
89
89
|
stoppedRun = await runtime.store.mutateSession(sessionId, async () => {
|
|
90
90
|
const run = await readCodexAppServerAgentRunForSession(runtime.store, sessionId);
|
|
@@ -105,7 +105,7 @@ export function createCodexObservationControl({
|
|
|
105
105
|
if (sharedStop) await retireProvider(provider);
|
|
106
106
|
else provider.observationFailure = null;
|
|
107
107
|
for (const entry of codexAppServerConversations.get(namespace(sessionId))?.values() || []) {
|
|
108
|
-
if (entry.provider === provider && (
|
|
108
|
+
if (entry.provider === provider && (!threadIds || threadIds.includes(entry.conversationId)) && (
|
|
109
109
|
codexAppServerConversationTurnIsActive(entry.status) || entry.goal?.status === "active"
|
|
110
110
|
)) {
|
|
111
111
|
entry.status = "interrupted";
|