mixdog 0.9.85 → 0.9.87
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/README.md +7 -1
- package/package.json +4 -3
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +26 -0
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
- package/src/runtime/memory/tool-defs.mjs +19 -3
- package/src/runtime/shared/atomic-file.mjs +25 -14
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +49 -9
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/provider-auth-api.mjs +8 -0
- package/src/session-runtime/provider-usage.mjs +30 -5
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +286 -548
- package/src/session-runtime/session-lifecycle.mjs +378 -0
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +443 -0
- package/src/standalone/agent-tool/job-views.mjs +329 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
- package/src/standalone/agent-tool/tag-registry.mjs +340 -0
- package/src/standalone/agent-tool.mjs +112 -941
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +9 -10
- package/src/standalone/usage-dashboard.mjs +23 -1
- package/src/tui/App.jsx +362 -2569
- package/src/tui/app/app-view.jsx +504 -0
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/create-app-pickers.mjs +313 -0
- package/src/tui/app/prompt-submit.mjs +501 -0
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/shell-layout.mjs +563 -0
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +0 -45
- package/src/tui/app/usage-context-panels.mjs +288 -0
- package/src/tui/app/use-copy-selection.mjs +56 -0
- package/src/tui/app/use-global-key-input.mjs +151 -0
- package/src/tui/app/use-pasted-buffers.mjs +115 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
- package/src/tui/app/use-prompt-hint.mjs +58 -0
- package/src/tui/app/use-prompt-queue-history.mjs +100 -0
- package/src/tui/app/use-terminal-chrome.mjs +72 -0
- package/src/tui/app/use-transcript-activity.mjs +131 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +9857 -9159
- package/src/tui/engine/session-api-ext.mjs +47 -9
- package/src/tui/engine.mjs +7 -1
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- package/src/runtime/shared/workspace-router.mjs +0 -259
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// mutex-guarded turn loop (askSession) plus the abort-aware call wrapper
|
|
4
4
|
// (_api_call_with_interrupt). Behavior-preserving move — every runtime
|
|
5
5
|
// helper it used in manager.mjs is now imported from its split module.
|
|
6
|
-
import { createHash } from 'crypto';
|
|
6
|
+
import { createHash, randomUUID } from 'crypto';
|
|
7
7
|
import { getProvider } from '../../providers/registry.mjs';
|
|
8
8
|
import { normalizeCompactType, DEFAULT_COMPACT_TYPE } from '../compact.mjs';
|
|
9
|
-
import { loadSession, saveSessionAsync, saveSessionAsyncDeferred, readSessionLifecycleFromDisk } from '../store.mjs';
|
|
9
|
+
import { loadSession, saveSession, saveSessionAsync, saveSessionAsyncDeferred, readSessionLifecycleFromDisk } from '../store.mjs';
|
|
10
10
|
import { createAbortController } from '../../../../shared/abort-controller.mjs';
|
|
11
11
|
import { logLlmCall } from '../../../../shared/llm/usage-log.mjs';
|
|
12
12
|
import { appendAgentTrace } from '../../agent-trace.mjs';
|
|
@@ -56,6 +56,12 @@ import {
|
|
|
56
56
|
persistCompactedOutgoingAfterAskFailure,
|
|
57
57
|
} from './message-sanitize.mjs';
|
|
58
58
|
import { createTurnInterruptionTracker } from './turn-interruption.mjs';
|
|
59
|
+
import {
|
|
60
|
+
clearTurnCheckpoint,
|
|
61
|
+
recoverTurnCheckpoint,
|
|
62
|
+
turnMessagesForCheckpoint,
|
|
63
|
+
writeTurnCheckpoint,
|
|
64
|
+
} from './turn-checkpoint.mjs';
|
|
59
65
|
import { _getAgentLoop } from './runtime-loaders.mjs';
|
|
60
66
|
import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
|
|
61
67
|
import { recordProviderContextBaseline } from '../loop/compact-policy.mjs';
|
|
@@ -208,6 +214,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
208
214
|
// Hoisted so the outer finally (which runs once after the whole turn loop)
|
|
209
215
|
// can compare against the last turn's generation.
|
|
210
216
|
let askGeneration = 0;
|
|
217
|
+
let _crashRecoveryChecked = false;
|
|
211
218
|
try {
|
|
212
219
|
// Turn loop (pendingMessages pattern): run the current prompt, then drain
|
|
213
220
|
// any `agent type=send` messages that were queued while this turn was in
|
|
@@ -262,6 +269,17 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
262
269
|
if (preSession.closed === true) {
|
|
263
270
|
throw new SessionClosedError(sessionId, 'session already closed');
|
|
264
271
|
}
|
|
272
|
+
if (!_crashRecoveryChecked) {
|
|
273
|
+
_crashRecoveryChecked = true;
|
|
274
|
+
const recovery = recoverTurnCheckpoint(preSession);
|
|
275
|
+
if (recovery.changed) {
|
|
276
|
+
saveSession(preSession, {
|
|
277
|
+
sync: true,
|
|
278
|
+
expectedGeneration: preSession.generation,
|
|
279
|
+
});
|
|
280
|
+
if (recovery.turnToken) clearTurnCheckpoint(sessionId, recovery.turnToken);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
265
283
|
// A prior crash/partial-save during compaction may have pinned
|
|
266
284
|
// compaction.lastStage='compacting'. This ask is starting fresh, so
|
|
267
285
|
// recover the stale transient stage before the loop's pre-send compact
|
|
@@ -306,11 +324,62 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
306
324
|
let cancelledUserTurnContent = '';
|
|
307
325
|
let _turnOutgoing = null;
|
|
308
326
|
const _turnInterruption = createTurnInterruptionTracker();
|
|
327
|
+
const _turnCheckpointToken = randomUUID();
|
|
328
|
+
const _turnCheckpointStartedAt = Date.now();
|
|
329
|
+
const _TURN_CHECKPOINT_THROTTLE_MS = 150;
|
|
330
|
+
let _turnCheckpointTimer = null;
|
|
331
|
+
let _turnCheckpointStopped = false;
|
|
332
|
+
let _turnCheckpointLastAt = 0;
|
|
333
|
+
let _turnCheckpointWarned = false;
|
|
334
|
+
const _flushTurnCheckpoint = () => {
|
|
335
|
+
if (_turnCheckpointStopped || !_turnOutgoing || !cancelledUserTurnContent) return false;
|
|
336
|
+
try {
|
|
337
|
+
const written = writeTurnCheckpoint({
|
|
338
|
+
sessionId,
|
|
339
|
+
generation: askGeneration,
|
|
340
|
+
turnToken: _turnCheckpointToken,
|
|
341
|
+
startedAt: _turnCheckpointStartedAt,
|
|
342
|
+
currentUserContent: cancelledUserTurnContent,
|
|
343
|
+
turnMessages: turnMessagesForCheckpoint(_turnOutgoing, cancelledUserTurnContent),
|
|
344
|
+
interruption: _turnInterruption.snapshot(),
|
|
345
|
+
});
|
|
346
|
+
if (written) _turnCheckpointLastAt = Date.now();
|
|
347
|
+
return written;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (!_turnCheckpointWarned) {
|
|
350
|
+
_turnCheckpointWarned = true;
|
|
351
|
+
try { process.stderr.write(`[turn-checkpoint] write failed session=${sessionId}: ${error?.message || error}\n`); } catch {}
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const _scheduleTurnCheckpoint = (immediate = false) => {
|
|
357
|
+
if (_turnCheckpointStopped || !_turnOutgoing) return;
|
|
358
|
+
if (immediate || Date.now() - _turnCheckpointLastAt >= _TURN_CHECKPOINT_THROTTLE_MS) {
|
|
359
|
+
if (_turnCheckpointTimer) clearTimeout(_turnCheckpointTimer);
|
|
360
|
+
_turnCheckpointTimer = null;
|
|
361
|
+
_flushTurnCheckpoint();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (_turnCheckpointTimer) return;
|
|
365
|
+
const waitMs = Math.max(1, _TURN_CHECKPOINT_THROTTLE_MS - (Date.now() - _turnCheckpointLastAt));
|
|
366
|
+
_turnCheckpointTimer = setTimeout(() => {
|
|
367
|
+
_turnCheckpointTimer = null;
|
|
368
|
+
_flushTurnCheckpoint();
|
|
369
|
+
}, waitMs);
|
|
370
|
+
_turnCheckpointTimer.unref?.();
|
|
371
|
+
};
|
|
372
|
+
const _stopTurnCheckpoint = () => {
|
|
373
|
+
_turnCheckpointStopped = true;
|
|
374
|
+
if (_turnCheckpointTimer) clearTimeout(_turnCheckpointTimer);
|
|
375
|
+
_turnCheckpointTimer = null;
|
|
376
|
+
};
|
|
309
377
|
const _sessionStartMetaInjectedBeforeTurn = preSession.sessionStartMetaInjected === true;
|
|
310
378
|
let _interruptionSnapshot = null;
|
|
311
379
|
const _prepareCloseSnapshot = (abortReason) => {
|
|
312
380
|
if (_interruptionSnapshot) return _interruptionSnapshot;
|
|
313
381
|
if (!activeSession) return null;
|
|
382
|
+
_stopTurnCheckpoint();
|
|
314
383
|
activeSession.liveTurnMessages = null;
|
|
315
384
|
_turnInterruption.restoreTombstonedText();
|
|
316
385
|
const finalized = _turnInterruption.finalize({
|
|
@@ -319,6 +388,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
319
388
|
abortReason,
|
|
320
389
|
});
|
|
321
390
|
activeSession.messages = finalized.messages;
|
|
391
|
+
delete activeSession.activeTurnCheckpoint;
|
|
322
392
|
if (!finalized.responsePreserved) {
|
|
323
393
|
if (finalized.userTurnPreserved) {
|
|
324
394
|
// A non-user detach keeps the provisional prompt but makes
|
|
@@ -455,6 +525,15 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
455
525
|
// user turn is introduced.
|
|
456
526
|
session.messages = filterModelVisibleSessionMessages(outgoing);
|
|
457
527
|
session.liveTurnMessages = outgoing;
|
|
528
|
+
session.activeTurnCheckpoint = {
|
|
529
|
+
version: 1,
|
|
530
|
+
turnToken: _turnCheckpointToken,
|
|
531
|
+
startedAt: _turnCheckpointStartedAt,
|
|
532
|
+
};
|
|
533
|
+
// The sidecar lands synchronously before provider execution. Even
|
|
534
|
+
// if the async canonical-session preflight save has not reached its
|
|
535
|
+
// worker when the process is killed, recovery still has the prompt.
|
|
536
|
+
_scheduleTurnCheckpoint(true);
|
|
458
537
|
saveSessionAsync(session, { expectedGeneration: askGeneration }).catch((err) => {
|
|
459
538
|
try { process.stderr.write(`[session] preflight user-turn save failed: ${err?.message || err}\n`); } catch {}
|
|
460
539
|
});
|
|
@@ -488,6 +567,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
488
567
|
const agentLoop = await _getAgentLoop();
|
|
489
568
|
const _trackTextDelta = (chunk) => {
|
|
490
569
|
_turnInterruption.recordTextDelta(chunk);
|
|
570
|
+
_scheduleTurnCheckpoint();
|
|
491
571
|
if (typeof askOpts?.onTextDelta === 'function') askOpts.onTextDelta(chunk);
|
|
492
572
|
};
|
|
493
573
|
const _trackTextReset = async (detail) => {
|
|
@@ -504,23 +584,28 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
504
584
|
}
|
|
505
585
|
if (!acknowledged) return false;
|
|
506
586
|
_turnInterruption.tombstoneText(detail?.chars);
|
|
587
|
+
_scheduleTurnCheckpoint(true);
|
|
507
588
|
return true;
|
|
508
589
|
};
|
|
509
590
|
const _trackReasoningDelta = (chunk) => {
|
|
510
591
|
_turnInterruption.recordReasoningDelta(chunk);
|
|
592
|
+
_scheduleTurnCheckpoint();
|
|
511
593
|
if (typeof askOpts?.onReasoningDelta === 'function') askOpts.onReasoningDelta(chunk);
|
|
512
594
|
};
|
|
513
595
|
const _trackAssistantText = (text) => {
|
|
514
596
|
_turnInterruption.recordAssistantText(text);
|
|
597
|
+
_scheduleTurnCheckpoint(true);
|
|
515
598
|
if (typeof askOpts?.onAssistantText === 'function') askOpts.onAssistantText(text);
|
|
516
599
|
};
|
|
517
600
|
const _trackedOnToolCall = async (iteration, calls) => {
|
|
518
601
|
_turnInterruption.recordToolCalls(calls);
|
|
602
|
+
_scheduleTurnCheckpoint(true);
|
|
519
603
|
if (typeof onToolCall === 'function') return await onToolCall(iteration, calls);
|
|
520
604
|
return undefined;
|
|
521
605
|
};
|
|
522
606
|
const _trackToolResult = (message) => {
|
|
523
607
|
_turnInterruption.recordToolResult(message);
|
|
608
|
+
_scheduleTurnCheckpoint(true);
|
|
524
609
|
if (typeof askOpts?.onToolResult === 'function') askOpts.onToolResult(message);
|
|
525
610
|
};
|
|
526
611
|
const priorToolApprovalHook = session.toolApprovalHook;
|
|
@@ -539,10 +624,22 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
539
624
|
onReasoningDelta: _trackReasoningDelta,
|
|
540
625
|
onAssistantText: _trackAssistantText,
|
|
541
626
|
takeAssistantTranscriptMetadata: _takeAssistantTranscriptMetadata,
|
|
542
|
-
onAssistantMessageCommitted: () =>
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
627
|
+
onAssistantMessageCommitted: () => {
|
|
628
|
+
_turnInterruption.markAssistantMessageCommitted();
|
|
629
|
+
_scheduleTurnCheckpoint(true);
|
|
630
|
+
},
|
|
631
|
+
onAssistantToolCallObserved: (call, detail) => {
|
|
632
|
+
_turnInterruption.recordToolCalls([call], detail);
|
|
633
|
+
_scheduleTurnCheckpoint(true);
|
|
634
|
+
},
|
|
635
|
+
onProviderSendStarted: () => {
|
|
636
|
+
_turnInterruption.markProviderSendStarted();
|
|
637
|
+
_scheduleTurnCheckpoint(true);
|
|
638
|
+
},
|
|
639
|
+
onToolPhaseStarted: () => {
|
|
640
|
+
_turnInterruption.markToolPhaseStarted();
|
|
641
|
+
_scheduleTurnCheckpoint(true);
|
|
642
|
+
},
|
|
546
643
|
onUsageDelta: (d) => {
|
|
547
644
|
persistIterationMetrics(d).catch(() => {});
|
|
548
645
|
// provider_send usage arrives before agentLoop appends
|
|
@@ -565,7 +662,10 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
565
662
|
},
|
|
566
663
|
onToolResult: _trackToolResult,
|
|
567
664
|
onToolApproval: typeof askOpts?.onToolApproval === 'function' ? askOpts.onToolApproval : undefined,
|
|
568
|
-
onCompactEvent:
|
|
665
|
+
onCompactEvent: (event) => {
|
|
666
|
+
_scheduleTurnCheckpoint(true);
|
|
667
|
+
try { askOpts?.onCompactEvent?.(event); } catch {}
|
|
668
|
+
},
|
|
569
669
|
// Claude Code parity: mid-chain queued prompt/notification
|
|
570
670
|
// drain is owned by agentLoop at provider-continuation
|
|
571
671
|
// boundaries (after a tool batch, before the next send).
|
|
@@ -586,7 +686,10 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
586
686
|
// post-loop _pendingTail drain below.
|
|
587
687
|
return out;
|
|
588
688
|
},
|
|
589
|
-
onSteerMessage:
|
|
689
|
+
onSteerMessage: (text, detail) => {
|
|
690
|
+
_scheduleTurnCheckpoint(true);
|
|
691
|
+
try { askOpts?.onSteerMessage?.(text, detail); } catch {}
|
|
692
|
+
},
|
|
590
693
|
notifyFn: typeof askOpts?.notifyFn === 'function' ? askOpts.notifyFn : undefined,
|
|
591
694
|
promptCacheKey: session.promptCacheKey || sessionId,
|
|
592
695
|
// Provider-scoped cache key (mixdog-codex, mixdog-claude…).
|
|
@@ -639,6 +742,8 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
639
742
|
// Turn committed into session.messages; drop the live-turn alias so
|
|
640
743
|
// contextStatus() reverts to the authoritative committed transcript.
|
|
641
744
|
session.liveTurnMessages = null;
|
|
745
|
+
_stopTurnCheckpoint();
|
|
746
|
+
delete session.activeTurnCheckpoint;
|
|
642
747
|
const _assistantTranscriptMeta = persistedAssistantTranscriptMetadata(_rawTranscriptMeta);
|
|
643
748
|
if (result.content || result.reasoningContent) {
|
|
644
749
|
// Max-output recovery returns the complete concatenated text to
|
|
@@ -846,6 +951,10 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
846
951
|
? saveSessionAsyncDeferred
|
|
847
952
|
: saveSessionAsync;
|
|
848
953
|
const terminalSave = saveTerminalSession(session, { expectedGeneration: askGeneration });
|
|
954
|
+
terminalSave.then(
|
|
955
|
+
() => clearTurnCheckpoint(sessionId, _turnCheckpointToken),
|
|
956
|
+
() => {},
|
|
957
|
+
);
|
|
849
958
|
finalizePendingMessageDelivery(
|
|
850
959
|
session,
|
|
851
960
|
_turnPendingEntries,
|
|
@@ -867,10 +976,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
867
976
|
markSessionDone(sessionId, { empty: isEmptyFinal });
|
|
868
977
|
_result = terminalResultPreview;
|
|
869
978
|
} catch (err) {
|
|
979
|
+
_stopTurnCheckpoint();
|
|
870
980
|
// Cancellation/error paths bypass the commit point above; drop the
|
|
871
981
|
// live-turn alias so contextStatus() stops estimating from the
|
|
872
982
|
// stale in-flight array once the turn unwinds.
|
|
873
|
-
if (activeSession)
|
|
983
|
+
if (activeSession) {
|
|
984
|
+
activeSession.liveTurnMessages = null;
|
|
985
|
+
delete activeSession.activeTurnCheckpoint;
|
|
986
|
+
}
|
|
874
987
|
// Restore before ANY finalization path. In particular, cancellation
|
|
875
988
|
// can race the acknowledged non-streaming restart and surface as a
|
|
876
989
|
// SessionClosedError; its interruption snapshot must include the
|
|
@@ -899,6 +1012,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
899
1012
|
} else {
|
|
900
1013
|
await durableSave;
|
|
901
1014
|
}
|
|
1015
|
+
clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
902
1016
|
} catch { /* cancellation cleanup is best-effort */ }
|
|
903
1017
|
if (currentRuntime) currentRuntime.session = activeSession;
|
|
904
1018
|
} else releasePendingMessages(sessionId, _turnPendingEntries);
|
|
@@ -918,6 +1032,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
918
1032
|
const preserveProviderPartial = restoredResetText
|
|
919
1033
|
|| err?.liveTextEmitted === true
|
|
920
1034
|
|| err?.unsafeToRetry === true;
|
|
1035
|
+
let _errorStateDurable = false;
|
|
921
1036
|
if (preserveProviderPartial && activeSession && _turnInterruption.hasResponseStarted()) {
|
|
922
1037
|
const finalized = _prepareCloseSnapshot('provider-error');
|
|
923
1038
|
if (finalized.responsePreserved) recordPendingMessageDelivery(activeSession, _turnPendingEntries);
|
|
@@ -932,6 +1047,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
932
1047
|
} else {
|
|
933
1048
|
await durableSave;
|
|
934
1049
|
}
|
|
1050
|
+
_errorStateDurable = true;
|
|
935
1051
|
} catch { /* provider-failure history persistence is best-effort */ }
|
|
936
1052
|
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
937
1053
|
if (currentRuntime) currentRuntime.session = activeSession;
|
|
@@ -951,11 +1067,19 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
951
1067
|
activeSession, _turnPendingEntries, durableSave,
|
|
952
1068
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
953
1069
|
);
|
|
1070
|
+
_errorStateDurable = true;
|
|
954
1071
|
} catch {}
|
|
955
1072
|
} else {
|
|
956
1073
|
releasePendingMessages(sessionId, _turnPendingEntries);
|
|
957
1074
|
}
|
|
958
1075
|
}
|
|
1076
|
+
if (!_errorStateDurable && activeSession) {
|
|
1077
|
+
try {
|
|
1078
|
+
await saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1079
|
+
_errorStateDurable = true;
|
|
1080
|
+
} catch { /* retain checkpoint when canonical persistence fails */ }
|
|
1081
|
+
}
|
|
1082
|
+
if (_errorStateDurable) clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
959
1083
|
markSessionError(sessionId, err && err.message ? err.message : String(err));
|
|
960
1084
|
throw err;
|
|
961
1085
|
}
|
|
@@ -1000,6 +1124,8 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1000
1124
|
return _result;
|
|
1001
1125
|
}
|
|
1002
1126
|
} finally {
|
|
1127
|
+
// A thrown setup/provider path must never leave a delayed checkpoint
|
|
1128
|
+
// writer alive after the turn lock is released.
|
|
1003
1129
|
// Clear the controller only if it's still ours (closeSession may have
|
|
1004
1130
|
// swapped it). Leave the rest of the runtime entry intact so agent type=list
|
|
1005
1131
|
// can still surface the final stage (done/error/cancelling).
|
|
@@ -569,18 +569,30 @@ export function hydratePendingMessages(sessionId, options = {}) {
|
|
|
569
569
|
|
|
570
570
|
function clearPersistedPendingMessages(sessionId) {
|
|
571
571
|
if (!isValidPendingSessionId(sessionId)) return;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
572
|
+
// ASYNC, chained behind this session's persist tail. The previous SYNC
|
|
573
|
+
// variant self-deadlocked: closeSession ran it on the event loop while an
|
|
574
|
+
// in-flight async spool op (persist/ack/hydrate) in the SAME process held
|
|
575
|
+
// the lock — the sync wait starved the holder's continuations, so every
|
|
576
|
+
// such race burned the full 8s lock timeout (observed as an 8s shard-child
|
|
577
|
+
// stall after each one-shot session close). In-memory queues are already
|
|
578
|
+
// dropped synchronously by _dropPendingMessageState; only the durable
|
|
579
|
+
// spool row lags behind, exactly like the ack path.
|
|
580
|
+
const preceding = _pendingPersistTails.get(sessionId) || Promise.resolve();
|
|
581
|
+
const operation = preceding.catch(() => {}).then(() => updateJsonAtomic(pendingMessagesPath(), (raw) => {
|
|
582
|
+
const next = normalizePendingStore(raw);
|
|
583
|
+
if (!Object.prototype.hasOwnProperty.call(next.sessions, sessionId)) return undefined;
|
|
584
|
+
delete next.sessions[sessionId];
|
|
585
|
+
if (next.sessionTouchedAt) delete next.sessionTouchedAt[sessionId];
|
|
586
|
+
next.updatedAt = Date.now();
|
|
587
|
+
return next;
|
|
588
|
+
}, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false }))
|
|
589
|
+
.catch((err) => {
|
|
590
|
+
try { process.stderr.write(`[session] pending-message clear failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
|
|
591
|
+
});
|
|
592
|
+
_pendingPersistTails.set(sessionId, operation);
|
|
593
|
+
operation.finally(() => {
|
|
594
|
+
if (_pendingPersistTails.get(sessionId) === operation) _pendingPersistTails.delete(sessionId);
|
|
595
|
+
}).catch(() => {});
|
|
584
596
|
}
|
|
585
597
|
|
|
586
598
|
function shouldEvictPendingSession(sessionId, ttlMs, entryTouchedAt, now = Date.now()) {
|
|
@@ -913,5 +925,12 @@ export function _dropPendingMessageState(id, { clearPersisted = true } = {}) {
|
|
|
913
925
|
}
|
|
914
926
|
|
|
915
927
|
setImmediate(() => {
|
|
928
|
+
// Spool hygiene belongs to the lead/daemon process. Worker-mode children
|
|
929
|
+
// (channels worker, agent shard) share the same spool file; a boot-time
|
|
930
|
+
// SYNC sweep in every child contends on the cross-process lock against
|
|
931
|
+
// the lead's writes (observed in the first live shard run: "atomic lock
|
|
932
|
+
// timeout after 8000ms: session-pending-messages.json.lock") for zero
|
|
933
|
+
// benefit — the lead already sweeps.
|
|
934
|
+
if (process.env.MIXDOG_WORKER_MODE === '1') return;
|
|
916
935
|
try { sweepOrphanedPendingMessages(); } catch { /* ignore */ }
|
|
917
936
|
});
|
|
@@ -11,6 +11,7 @@ import { SessionClosedError } from './session-errors.mjs';
|
|
|
11
11
|
import { _dropPendingMessageState } from './pending-messages.mjs';
|
|
12
12
|
import { _stopToolActivityHeartbeat, _getRuntimeEntry, _clearSessionRuntime } from './runtime-liveness.mjs';
|
|
13
13
|
import { _closeBashSessionLazy } from './runtime-loaders.mjs';
|
|
14
|
+
import { clearTurnCheckpoint } from './turn-checkpoint.mjs';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Close a session. Plants a `closed=true` tombstone on disk with a bumped
|
|
@@ -69,6 +70,9 @@ export function closeSession(id, reason = 'manual', opts = {}) {
|
|
|
69
70
|
// write could silently overwrite the session after the user resumes
|
|
70
71
|
// it back (BL: burned-session late-save clobber).
|
|
71
72
|
const newGen = tombstone ? markSessionClosed(id, reason) : bumpSessionGeneration(id, reason);
|
|
73
|
+
// prepareCloseSnapshot was folded into the synchronous generation write
|
|
74
|
+
// above. Only now is it safe to remove the crash fallback.
|
|
75
|
+
if (typeof newGen === 'number') clearTurnCheckpoint(id);
|
|
72
76
|
// 2. Mark runtime as closed so post-await validation in askSession fires.
|
|
73
77
|
if (entry) {
|
|
74
78
|
entry.closed = true;
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { getAgentRuntimeSync, warnAgentRuntimeResolveFailureOnce } from './agent-runtime-singleton.mjs';
|
|
35
35
|
import { mintSessionId } from './session-id.mjs';
|
|
36
36
|
import { providerCacheKey } from './provider-cache-key.mjs';
|
|
37
|
+
import { clearTurnCheckpoint, recoverTurnCheckpoint } from './turn-checkpoint.mjs';
|
|
37
38
|
|
|
38
39
|
function buildSessionProviderCacheOpts(providerName, sessionId, agent = null) {
|
|
39
40
|
// Keep this in sync with createSession's provider-cache policy: only
|
|
@@ -657,6 +658,19 @@ export async function resumeSession(sessionId, preset, options = {}) {
|
|
|
657
658
|
}
|
|
658
659
|
return attached;
|
|
659
660
|
}
|
|
661
|
+
const crashRecovery = recoverTurnCheckpoint(session);
|
|
662
|
+
if (crashRecovery.changed) {
|
|
663
|
+
// Resume must not expose a recovered frame that exists only in memory.
|
|
664
|
+
// Persist synchronously, then remove the sidecar; a second crash at
|
|
665
|
+
// either boundary therefore leaves at least one complete copy.
|
|
666
|
+
saveSession(session, {
|
|
667
|
+
sync: true,
|
|
668
|
+
expectedGeneration: session.generation,
|
|
669
|
+
});
|
|
670
|
+
if (crashRecovery.turnToken) {
|
|
671
|
+
clearTurnCheckpoint(sessionId, crashRecovery.turnToken);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
660
674
|
const oldTools = session.tools || [];
|
|
661
675
|
const cached = _preparedResumes.get(sessionId);
|
|
662
676
|
_preparedResumes.delete(sessionId);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
unlinkSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from 'fs';
|
|
8
|
+
import { randomBytes } from 'crypto';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { getPluginData } from '../../config.mjs';
|
|
11
|
+
import { renameWithRetrySync } from '../../../../shared/atomic-file.mjs';
|
|
12
|
+
import { sanitizeContentForStoredHistory } from '../../providers/media-normalization.mjs';
|
|
13
|
+
import { promptContentText } from './prompt-utils.mjs';
|
|
14
|
+
import { finalizeTurnInterruptionSnapshot } from './turn-interruption.mjs';
|
|
15
|
+
|
|
16
|
+
const TURN_CHECKPOINT_VERSION = 1;
|
|
17
|
+
const SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
18
|
+
|
|
19
|
+
function checkpointDir() {
|
|
20
|
+
const dir = join(getPluginData(), 'turn-checkpoints');
|
|
21
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function turnCheckpointPath(sessionId) {
|
|
26
|
+
if (!SESSION_ID.test(String(sessionId || ''))) {
|
|
27
|
+
throw new Error(`[turn-checkpoint] invalid session id: ${JSON.stringify(sessionId)}`);
|
|
28
|
+
}
|
|
29
|
+
return join(checkpointDir(), `${sessionId}.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function checkpointMessage(message) {
|
|
33
|
+
if (!message || typeof message !== 'object') return message;
|
|
34
|
+
const content = sanitizeContentForStoredHistory(message.content);
|
|
35
|
+
return content === message.content ? message : { ...message, content };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function matchingUserMessage(message, currentUserContent) {
|
|
39
|
+
return message?.role === 'user'
|
|
40
|
+
&& (message.content === currentUserContent
|
|
41
|
+
|| promptContentText(message.content) === promptContentText(currentUserContent));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function turnMessagesForCheckpoint(messages, currentUserContent) {
|
|
45
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
46
|
+
let start = -1;
|
|
47
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
48
|
+
if (matchingUserMessage(source[index], currentUserContent)) {
|
|
49
|
+
start = index;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const turn = start >= 0 ? source.slice(start) : [];
|
|
54
|
+
return turn.map(checkpointMessage);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function writeTurnCheckpoint(checkpoint) {
|
|
58
|
+
if (!checkpoint?.sessionId || !checkpoint?.turnToken) return false;
|
|
59
|
+
const target = turnCheckpointPath(checkpoint.sessionId);
|
|
60
|
+
const temp = `${target}.${randomBytes(6).toString('hex')}.tmp`;
|
|
61
|
+
const payload = {
|
|
62
|
+
...checkpoint,
|
|
63
|
+
version: TURN_CHECKPOINT_VERSION,
|
|
64
|
+
currentUserContent: sanitizeContentForStoredHistory(checkpoint.currentUserContent),
|
|
65
|
+
turnMessages: (Array.isArray(checkpoint.turnMessages) ? checkpoint.turnMessages : [])
|
|
66
|
+
.map(checkpointMessage),
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
writeFileSync(temp, JSON.stringify(payload), 'utf8');
|
|
71
|
+
renameWithRetrySync(temp, target);
|
|
72
|
+
return true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
try { unlinkSync(temp); } catch {}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readTurnCheckpoint(sessionId) {
|
|
80
|
+
const target = turnCheckpointPath(sessionId);
|
|
81
|
+
try {
|
|
82
|
+
const value = JSON.parse(readFileSync(target, 'utf8'));
|
|
83
|
+
if (value?.version !== TURN_CHECKPOINT_VERSION
|
|
84
|
+
|| value?.sessionId !== sessionId
|
|
85
|
+
|| !value?.turnToken
|
|
86
|
+
|| !Array.isArray(value?.turnMessages)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function clearTurnCheckpoint(sessionId, turnToken = null) {
|
|
96
|
+
const target = turnCheckpointPath(sessionId);
|
|
97
|
+
if (!existsSync(target)) return true;
|
|
98
|
+
if (turnToken) {
|
|
99
|
+
const current = readTurnCheckpoint(sessionId);
|
|
100
|
+
// A token guard prevents an older turn's late terminal save from
|
|
101
|
+
// deleting the checkpoint already created by its queued follow-up.
|
|
102
|
+
if (current?.turnToken && current.turnToken !== turnToken) return false;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
unlinkSync(target);
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findPersistedTurnStart(messages, currentUserContent) {
|
|
113
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
114
|
+
if (matchingUserMessage(messages[index], currentUserContent)) return index;
|
|
115
|
+
}
|
|
116
|
+
return -1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Merge a force-killed turn checkpoint into a loaded session. The caller must
|
|
121
|
+
* durably save the mutated session before clearing the returned token.
|
|
122
|
+
*/
|
|
123
|
+
export function recoverTurnCheckpoint(session) {
|
|
124
|
+
if (!session?.id || session.closed === true) {
|
|
125
|
+
return { changed: false, recovered: false, turnToken: null };
|
|
126
|
+
}
|
|
127
|
+
const checkpoint = readTurnCheckpoint(session.id);
|
|
128
|
+
const marker = session.activeTurnCheckpoint;
|
|
129
|
+
if (!checkpoint) {
|
|
130
|
+
if (!marker) return { changed: false, recovered: false, turnToken: null };
|
|
131
|
+
delete session.activeTurnCheckpoint;
|
|
132
|
+
return { changed: true, recovered: false, turnToken: null };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sessionGeneration = Number(session.generation) || 0;
|
|
136
|
+
const checkpointGeneration = Number(checkpoint.generation) || 0;
|
|
137
|
+
const markerToken = typeof marker?.turnToken === 'string' ? marker.turnToken : null;
|
|
138
|
+
const markerMatches = markerToken === checkpoint.turnToken;
|
|
139
|
+
const checkpointIsNewer = Number(checkpoint.updatedAt) > Number(session.updatedAt || 0);
|
|
140
|
+
if (checkpointGeneration !== sessionGeneration
|
|
141
|
+
|| (markerToken && !markerMatches)
|
|
142
|
+
|| (!markerToken && !checkpointIsNewer)) {
|
|
143
|
+
clearTurnCheckpoint(session.id, checkpoint.turnToken);
|
|
144
|
+
if (markerToken === checkpoint.turnToken) delete session.activeTurnCheckpoint;
|
|
145
|
+
return { changed: false, recovered: false, turnToken: null };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const finalized = finalizeTurnInterruptionSnapshot({
|
|
149
|
+
turnOutgoing: checkpoint.turnMessages,
|
|
150
|
+
currentUserContent: checkpoint.currentUserContent,
|
|
151
|
+
snapshot: checkpoint.interruption,
|
|
152
|
+
abortReason: 'process-crash',
|
|
153
|
+
});
|
|
154
|
+
const current = Array.isArray(session.messages) ? session.messages : [];
|
|
155
|
+
const start = findPersistedTurnStart(current, checkpoint.currentUserContent);
|
|
156
|
+
session.messages = [
|
|
157
|
+
...(start >= 0 ? current.slice(0, start) : current),
|
|
158
|
+
...finalized.messages,
|
|
159
|
+
];
|
|
160
|
+
delete session.activeTurnCheckpoint;
|
|
161
|
+
delete session.providerState;
|
|
162
|
+
session.updatedAt = Date.now();
|
|
163
|
+
session.lastUsedAt = Date.now();
|
|
164
|
+
return {
|
|
165
|
+
changed: true,
|
|
166
|
+
recovered: true,
|
|
167
|
+
turnToken: checkpoint.turnToken,
|
|
168
|
+
responsePreserved: finalized.responsePreserved,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -8,6 +8,8 @@ import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
|
|
|
8
8
|
const INTERRUPT_MESSAGE = '[Request interrupted by user]';
|
|
9
9
|
const INTERRUPT_MESSAGE_FOR_TOOL_USE = '[Request interrupted by user for tool use]';
|
|
10
10
|
const STREAMING_INTERRUPTED_TOOL_RESULT = 'Interrupted by user';
|
|
11
|
+
const PROCESS_RESTART_INTERRUPT_MESSAGE = '[Request interrupted by process restart]';
|
|
12
|
+
const PROCESS_RESTART_TOOL_RESULT = 'Interrupted by process restart';
|
|
11
13
|
const TOOL_USE_REJECT_RESULT = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
12
14
|
|
|
13
15
|
// Abort reasons that represent an EXPLICIT user cancellation of this turn.
|
|
@@ -134,9 +136,11 @@ function finalizeInterruptedTurn({
|
|
|
134
136
|
const executionStarted = phase === 'tools' || observed?.eagerStarted === true;
|
|
135
137
|
messages.push({
|
|
136
138
|
role: 'tool',
|
|
137
|
-
content:
|
|
138
|
-
?
|
|
139
|
-
:
|
|
139
|
+
content: abortReason === 'process-crash'
|
|
140
|
+
? PROCESS_RESTART_TOOL_RESULT
|
|
141
|
+
: (executionStarted
|
|
142
|
+
? TOOL_USE_REJECT_RESULT
|
|
143
|
+
: STREAMING_INTERRUPTED_TOOL_RESULT),
|
|
140
144
|
toolCallId: callId,
|
|
141
145
|
toolKind: 'error',
|
|
142
146
|
});
|
|
@@ -149,9 +153,11 @@ function finalizeInterruptedTurn({
|
|
|
149
153
|
if (abortReason !== 'interrupt' && abortReason !== 'provider-error') {
|
|
150
154
|
pairedMessages.push({
|
|
151
155
|
role: 'user',
|
|
152
|
-
content:
|
|
153
|
-
?
|
|
154
|
-
:
|
|
156
|
+
content: abortReason === 'process-crash'
|
|
157
|
+
? PROCESS_RESTART_INTERRUPT_MESSAGE
|
|
158
|
+
: (phase === 'tools'
|
|
159
|
+
? INTERRUPT_MESSAGE_FOR_TOOL_USE
|
|
160
|
+
: INTERRUPT_MESSAGE),
|
|
155
161
|
});
|
|
156
162
|
}
|
|
157
163
|
return { messages: pairedMessages, responsePreserved: true, userTurnPreserved: true };
|
|
@@ -247,6 +253,19 @@ export function createTurnInterruptionTracker() {
|
|
|
247
253
|
phase = 'tools';
|
|
248
254
|
responseStarted = true;
|
|
249
255
|
},
|
|
256
|
+
snapshot() {
|
|
257
|
+
return {
|
|
258
|
+
responseStarted,
|
|
259
|
+
// A reset acknowledged by the UI temporarily tombstones text.
|
|
260
|
+
// A process crash cannot complete that replacement, so retain
|
|
261
|
+
// the same visible bytes the normal error path restores.
|
|
262
|
+
partialAssistantContent: partialAssistantContent + tombstonedAssistantContent,
|
|
263
|
+
partialReasoningContent,
|
|
264
|
+
phase,
|
|
265
|
+
observedToolCalls: [...observedToolCalls.entries()],
|
|
266
|
+
observedToolResults: [...observedToolResults.entries()],
|
|
267
|
+
};
|
|
268
|
+
},
|
|
250
269
|
finalize({ turnOutgoing, currentUserContent, abortReason = null }) {
|
|
251
270
|
return finalizeInterruptedTurn({
|
|
252
271
|
turnOutgoing,
|
|
@@ -262,3 +281,23 @@ export function createTurnInterruptionTracker() {
|
|
|
262
281
|
},
|
|
263
282
|
};
|
|
264
283
|
}
|
|
284
|
+
|
|
285
|
+
export function finalizeTurnInterruptionSnapshot({
|
|
286
|
+
turnOutgoing,
|
|
287
|
+
currentUserContent,
|
|
288
|
+
snapshot,
|
|
289
|
+
abortReason = 'process-crash',
|
|
290
|
+
}) {
|
|
291
|
+
const source = snapshot && typeof snapshot === 'object' ? snapshot : {};
|
|
292
|
+
return finalizeInterruptedTurn({
|
|
293
|
+
turnOutgoing,
|
|
294
|
+
currentUserContent,
|
|
295
|
+
responseStarted: source.responseStarted === true,
|
|
296
|
+
partialAssistantContent: String(source.partialAssistantContent || ''),
|
|
297
|
+
partialReasoningContent: String(source.partialReasoningContent || ''),
|
|
298
|
+
observedToolCalls: new Map(Array.isArray(source.observedToolCalls) ? source.observedToolCalls : []),
|
|
299
|
+
observedToolResults: new Map(Array.isArray(source.observedToolResults) ? source.observedToolResults : []),
|
|
300
|
+
phase: source.phase === 'tools' ? 'tools' : 'streaming',
|
|
301
|
+
abortReason,
|
|
302
|
+
});
|
|
303
|
+
}
|