mixdog 0.9.149 → 0.9.151
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 +28 -12
- package/package.json +1 -1
- package/src/headless-exec.mjs +40 -4
- package/src/headless-exec.test.mjs +116 -1
- package/src/rules/lead/01-general.md +3 -2
- package/src/rules/shared/00-general.md +1 -0
- package/src/rules/shared/10-tool-workflow.md +11 -12
- package/src/rules/shared/30-exploration.md +4 -3
- package/src/rules/shared/60-verification.md +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +1 -4
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +0 -18
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +4 -72
- package/src/runtime/agent/orchestrator/session/approval-mode.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +18 -33
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +21 -42
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +20 -125
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +27 -4
- package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +78 -0
- package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +63 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +29 -10
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +50 -23
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +50 -1
- package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +31 -0
- package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +106 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/absolute-glob-expand.mjs +129 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +218 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +50 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +69 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +33 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +71 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +54 -8
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +90 -51
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +25 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +30 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +60 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +54 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +49 -24
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -3
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +33 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +44 -10
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +57 -5
- package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +141 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +57 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +42 -23
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/memory/index.mjs +2 -4
- package/src/runtime/memory/lib/pg/process.mjs +29 -2
- package/src/runtime/memory/lib/pg/process.test.mjs +36 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +5 -1
- package/src/runtime/memory/lib/query-handlers.mjs +13 -19
- package/src/runtime/memory/lib/recall-format.mjs +13 -31
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/shared/memory-snapshot.mjs +57 -4
- package/src/runtime/shared/pristine-execution-contract.json +3 -0
- package/src/runtime/shared/pristine-execution.mjs +6 -3
- package/src/runtime/shared/tool-surface.mjs +6 -0
- package/src/runtime/shared/tool-surface.test.mjs +8 -0
- package/src/session-runtime/lifecycle-api.mjs +1 -0
- package/src/session-runtime/session-turn-api.mjs +5 -0
- package/src/standalone/agent-dispatch-broker.mjs +25 -80
- package/src/standalone/channel-worker-heartbeat.test.mjs +30 -0
- package/src/standalone/channel-worker.mjs +31 -1
- package/src/standalone/daemon.mjs +9 -9
- package/src/standalone/memory-runtime-proxy.mjs +5 -2
- package/src/standalone/session-client.mjs +23 -3
- package/src/standalone/session-runtime-host-health.test.mjs +36 -0
- package/src/standalone/session-runtime-host.mjs +26 -76
- package/src/standalone/session-runtime-worker.mjs +97 -0
- package/src/tui/App.jsx +3 -2
- package/src/tui/app/use-global-key-input.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +77 -14
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/dist/index.mjs +83 -23
- package/src/tui/lib/voice-setup.mjs +3 -2
- package/src/tui/session/completion-card-restore.test.mjs +18 -0
- package/src/tui/session/session-api-ext.mjs +2 -1
- package/src/tui/session/session-api.mjs +8 -1
- package/src/tui/session/task-wait-submit.test.mjs +80 -0
- package/src/tui/session/turn.mjs +58 -6
- package/src/tui/spinner-verbs.mjs +1 -0
- package/src/workflows/default/WORKFLOW.md +12 -19
- package/src/workflows/headless/WORKFLOW.md +11 -0
- package/src/workflows/solo/WORKFLOW.md +6 -11
|
@@ -45,7 +45,6 @@ import {
|
|
|
45
45
|
enforceSemanticSummarySchema,
|
|
46
46
|
fitSemanticSummaryMessage,
|
|
47
47
|
fitRecallFastTrackSummaryMessage,
|
|
48
|
-
stripNestedSummaryHeaderLines,
|
|
49
48
|
RECALL_TAIL_TRUNCATION_MARKER,
|
|
50
49
|
RECALL_TAIL_SHORT_TRUNCATION_MARKER,
|
|
51
50
|
} from './summary.mjs';
|
|
@@ -57,7 +56,6 @@ import {
|
|
|
57
56
|
fitRecallHandoffText,
|
|
58
57
|
conversationLinesFromMemoryText,
|
|
59
58
|
excludeTailFromConversation,
|
|
60
|
-
stripWorkingFileSections,
|
|
61
59
|
} from './handoff.mjs';
|
|
62
60
|
|
|
63
61
|
// Post-compact file re-attachment: re-inject fresh reads
|
|
@@ -555,12 +553,12 @@ export function recallFastTrackCompactMessages(messages, budgetTokens, opts = {}
|
|
|
555
553
|
//
|
|
556
554
|
// Turns are anchored on user-role boundaries: each turn = a user message plus
|
|
557
555
|
// the assistant/tool/system/developer messages that follow it. Recall-fasttrack
|
|
558
|
-
// keeps the newest live
|
|
556
|
+
// keeps the newest five live turns verbatim. Older conversation comes from memory.
|
|
559
557
|
//
|
|
560
558
|
// Partial tool_call/tool_result pairs that truncation might leave behind are
|
|
561
559
|
// repaired by sanitizeToolPairs/reconcileDedupStubs in the caller, so pairing
|
|
562
560
|
// stays valid even after trimming.
|
|
563
|
-
const RECALL_TAIL_USER_MAX =
|
|
561
|
+
const RECALL_TAIL_USER_MAX = 5;
|
|
564
562
|
const RECALL_TAIL_TOKEN_CAP = DEFAULT_COMPACTION_KEEP_TOKENS; // 8k
|
|
565
563
|
// A caller may request a cap smaller than one structurally valid message. Keep
|
|
566
564
|
// the cap strict above this unavoidable floor while retaining a real anchor.
|
|
@@ -695,10 +693,10 @@ function truncateTailToCap(messages, cap) {
|
|
|
695
693
|
return estimateMessagesTokens(sanitized) <= cap ? sanitized : [];
|
|
696
694
|
}
|
|
697
695
|
|
|
698
|
-
function splitRecallFitInputs(recallText
|
|
696
|
+
function splitRecallFitInputs(recallText) {
|
|
699
697
|
return {
|
|
700
698
|
recall: String(recallText || '').trim(),
|
|
701
|
-
prior:
|
|
699
|
+
prior: '',
|
|
702
700
|
};
|
|
703
701
|
}
|
|
704
702
|
|
|
@@ -799,7 +797,7 @@ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
|
|
|
799
797
|
}
|
|
800
798
|
const sanitized = redactToolCallSecretsInMessages(baseSanitized);
|
|
801
799
|
|
|
802
|
-
const { system: safeSystem, live
|
|
800
|
+
const { system: safeSystem, live } = splitLiveCompactionContext(sanitized);
|
|
803
801
|
const recallTailOpts = {
|
|
804
802
|
maxUsers: opts.recallTailMaxUsers ?? opts.tailTurns ?? RECALL_TAIL_USER_MAX,
|
|
805
803
|
tokenCap: (() => {
|
|
@@ -815,8 +813,8 @@ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
|
|
|
815
813
|
head: recallHead,
|
|
816
814
|
lastTurnStartIdx = recallHead.length,
|
|
817
815
|
} = selectRecallPreservedTail(live, recallTailOpts);
|
|
818
|
-
const recallFit = splitRecallFitInputs(opts.recallText
|
|
819
|
-
if (recallHead.length === 0
|
|
816
|
+
const recallFit = splitRecallFitInputs(opts.recallText);
|
|
817
|
+
if (recallHead.length === 0
|
|
820
818
|
&& !(recallFit.recall || recallFit.prior || opts.allowEmptyRecall === true)) {
|
|
821
819
|
throw new Error('recallFastTrackCompactMessages: no compactable prior history before preserved tail');
|
|
822
820
|
}
|
|
@@ -848,44 +846,31 @@ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
|
|
|
848
846
|
const recallRoom = (Number.isFinite(recallTokenCap) && recallTokenCap > 0)
|
|
849
847
|
? Math.min(recallRoomUncapped, Math.max(512, recallTokenCap - tailTokens))
|
|
850
848
|
: recallRoomUncapped;
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
.split('\n')
|
|
855
|
-
.map((line) => line.trim())
|
|
856
|
-
.filter(Boolean),
|
|
857
|
-
);
|
|
858
|
-
const toolLines = collectToolOutcomeLines(live)
|
|
859
|
-
.filter((line) => !priorLines.has(String(line || '').trim()));
|
|
860
|
-
const workingFiles = collectWorkingFileGroups(live, undefined, {
|
|
849
|
+
const toolLines = collectToolOutcomeLines(recallHead)
|
|
850
|
+
.filter((line) => !recallFit.recall.includes(String(line || '').trim()));
|
|
851
|
+
const workingFiles = collectWorkingFileGroups(recallHead, undefined, {
|
|
861
852
|
cwd: opts.cwd,
|
|
862
|
-
previousSummary:
|
|
853
|
+
previousSummary: '',
|
|
863
854
|
now: Date.now(),
|
|
864
855
|
});
|
|
865
856
|
const conversationLines = excludeTailFromConversation(
|
|
866
857
|
conversationLinesFromMemoryText(recallFit.recall),
|
|
867
858
|
recallTail,
|
|
868
|
-
)
|
|
859
|
+
);
|
|
869
860
|
const composedRecall = composeRecallHandoff({
|
|
870
861
|
sessionId: opts.sessionId || '',
|
|
871
862
|
conversationLines,
|
|
872
863
|
toolLines,
|
|
873
864
|
workingFiles,
|
|
874
865
|
});
|
|
875
|
-
const
|
|
876
|
-
? Math.max(256, Math.floor((recallRoom - 200) * 0.55))
|
|
877
|
-
: 0;
|
|
878
|
-
const currentRoom = Math.max(256, recallRoom - priorRoom - 200);
|
|
866
|
+
const currentRoom = Math.max(256, recallRoom - 200);
|
|
879
867
|
const fittedRecall = fitRecallHandoffText(composedRecall, currentRoom);
|
|
880
|
-
const priorPart = priorWithoutWorkingFiles
|
|
881
|
-
? fitRecallHandoffText(priorWithoutWorkingFiles, priorRoom)
|
|
882
|
-
: '';
|
|
883
868
|
const summaryMessage = fitRecallFastTrackSummaryMessage(
|
|
884
869
|
oldHistory,
|
|
885
870
|
fittedRecall,
|
|
886
871
|
recallRoom,
|
|
887
872
|
recallMeta,
|
|
888
|
-
|
|
873
|
+
'',
|
|
889
874
|
);
|
|
890
875
|
if (!summaryMessage) {
|
|
891
876
|
throw new Error(`recallFastTrackCompactMessages: summary cannot fit remaining budget=${recallRoom}`);
|
|
@@ -929,18 +914,18 @@ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
|
|
|
929
914
|
recallRoom,
|
|
930
915
|
recallChars: recallFit.recall.length,
|
|
931
916
|
recallBytes: textByteLength(recallFit.recall),
|
|
932
|
-
priorChars:
|
|
933
|
-
priorBytes:
|
|
917
|
+
priorChars: 0,
|
|
918
|
+
priorBytes: 0,
|
|
934
919
|
summaryMessageChars: summaryContent.length,
|
|
935
920
|
summaryMessageBytes: textByteLength(summaryContent),
|
|
936
921
|
recallEmpty: !recallFit.recall,
|
|
937
922
|
priorEmpty: !recallFit.prior,
|
|
938
923
|
recallTruncatedInSummary: !!recallFit.recall && !summaryContent.includes(recallFit.recall),
|
|
939
|
-
priorTruncatedInSummary:
|
|
924
|
+
priorTruncatedInSummary: false,
|
|
940
925
|
tailTruncated: recallTail.some((m) => messageContentHasMarker(m, RECALL_TAIL_TRUNCATION_MARKER) || messageContentHasMarker(m, RECALL_TAIL_SHORT_TRUNCATION_MARKER)),
|
|
941
926
|
fileReattached: reattach.reattached,
|
|
942
927
|
tailOptions: recallTailOpts,
|
|
943
|
-
previousSummary:
|
|
928
|
+
previousSummary: false,
|
|
944
929
|
durationMs: Date.now() - startedAt,
|
|
945
930
|
};
|
|
946
931
|
compactDebugLog('recall-fasttrack result', diagnostics);
|
|
@@ -19,43 +19,11 @@ import {
|
|
|
19
19
|
compactDebugLog,
|
|
20
20
|
} from './compact-debug.mjs';
|
|
21
21
|
import { positiveTokenInt } from './env.mjs';
|
|
22
|
-
import { TOOL_OUTPUT_MAX_BYTES } from '../../tools/builtin/tool-output-limit.mjs';
|
|
23
22
|
|
|
24
23
|
// ── Digest injection ────────────────────────────────────────────────────────
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
// synchronously at ingest, so recall serves everything the old full-dump
|
|
29
|
-
// injection used to carry.
|
|
30
|
-
// Default digest cap = the SHARED tool-output limit (TOOL_OUTPUT_MAX_BYTES,
|
|
31
|
-
// 50KB default, env MIXDOG_TOOL_OUTPUT_MAX_BYTES) — the digest injection is
|
|
32
|
-
// budgeted like any other tool result, not a special context share.
|
|
33
|
-
// compaction.recallDigestMaxKb still overrides per-session.
|
|
34
|
-
export const DIGEST_DEFAULT_MAX_KB = Math.max(1, Math.floor(TOOL_OUTPUT_MAX_BYTES / 1024));
|
|
35
|
-
|
|
36
|
-
// Byte-capped line-boundary truncation. Digest source is newest-first, so
|
|
37
|
-
// keeping the HEAD keeps the newest turns.
|
|
38
|
-
// Exported for manager/compaction-runner.mjs (manual//clear digest path) so
|
|
39
|
-
// both digest producers share one cap implementation.
|
|
40
|
-
export function truncateToKb(text, maxKb) {
|
|
41
|
-
const maxBytes = Math.max(1, maxKb) * 1024;
|
|
42
|
-
const s = String(text || '');
|
|
43
|
-
if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;
|
|
44
|
-
const lines = s.split('\n');
|
|
45
|
-
const marker = '[digest truncated at ' + maxKb + 'KB]';
|
|
46
|
-
const contentBudget = maxBytes - Buffer.byteLength(marker, 'utf8');
|
|
47
|
-
const out = [];
|
|
48
|
-
let used = 0;
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
const cost = Buffer.byteLength(line, 'utf8') + 1;
|
|
51
|
-
if (used + cost > contentBudget) break;
|
|
52
|
-
out.push(line);
|
|
53
|
-
used += cost;
|
|
54
|
-
}
|
|
55
|
-
return out.length ? out.join('\n') + '\n' + marker : marker;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function buildRecallDigestText(sessionId, digestBody, maxKb) {
|
|
24
|
+
// Memory provides every available summary/raw row. Only the final compaction
|
|
25
|
+
// budget is allowed to reduce the handoff.
|
|
26
|
+
function buildRecallDigestText(sessionId, digestBody) {
|
|
59
27
|
// No recall-usage instruction block here: the recall tool description
|
|
60
28
|
// already carries the usage-pattern cheatsheet (tool-defs.mjs), so
|
|
61
29
|
// repeating it per-compaction would be redundant injected tokens. The
|
|
@@ -64,7 +32,7 @@ function buildRecallDigestText(sessionId, digestBody, maxKb) {
|
|
|
64
32
|
return [
|
|
65
33
|
`[context compacted — session ${sessionId}]`,
|
|
66
34
|
`Recent digest (newest first):`,
|
|
67
|
-
|
|
35
|
+
String(digestBody || '').trim(),
|
|
68
36
|
].join('\n');
|
|
69
37
|
}
|
|
70
38
|
|
|
@@ -88,7 +56,17 @@ export function isUsableRecallDigestText(value) {
|
|
|
88
56
|
return !!trimmed && !/^\((?:no results|no current session)\)$/i.test(trimmed);
|
|
89
57
|
}
|
|
90
58
|
|
|
91
|
-
export
|
|
59
|
+
export const RECALL_FAST_TRACK_TAIL_TURNS = 5;
|
|
60
|
+
|
|
61
|
+
export async function runRecallFastTrackCompact({
|
|
62
|
+
sessionRef,
|
|
63
|
+
messages,
|
|
64
|
+
compactBudgetTokens,
|
|
65
|
+
compactPolicy,
|
|
66
|
+
sessionId,
|
|
67
|
+
signal,
|
|
68
|
+
executeMemorySearch,
|
|
69
|
+
}) {
|
|
92
70
|
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
93
71
|
const startedAt = Date.now();
|
|
94
72
|
const diagnostics = {
|
|
@@ -126,14 +104,15 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
126
104
|
// watcher already persisted. Do not retransmit the live transcript through
|
|
127
105
|
// the memory RPC: that duplicates canonical history and can exceed the
|
|
128
106
|
// bounded HTTP request body before compaction gets a chance to run.
|
|
129
|
-
const digestMaxKb = positiveTokenInt(sessionRef?.compaction?.recallDigestMaxKb) || DIGEST_DEFAULT_MAX_KB;
|
|
130
107
|
let digestBody = '';
|
|
131
108
|
t0 = Date.now();
|
|
132
109
|
try {
|
|
133
|
-
const
|
|
110
|
+
const searchMemory = typeof executeMemorySearch === 'function'
|
|
111
|
+
? executeMemorySearch
|
|
112
|
+
: (args, ctx) => executeInternalTool('memory', args, ctx);
|
|
113
|
+
const browsed = await searchMemory({
|
|
134
114
|
action: 'search',
|
|
135
115
|
sessionId,
|
|
136
|
-
limit: positiveTokenInt(sessionRef?.compaction?.recallDigestLimit) || 100,
|
|
137
116
|
includeMembers: true,
|
|
138
117
|
includeRaw: true,
|
|
139
118
|
compactHandoff: true,
|
|
@@ -171,7 +150,7 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
171
150
|
try { process.stderr.write(`[loop] recall-fasttrack fail-safe abort (sess=${sessionId || 'unknown'}): stored session unavailable — keeping full history, no recall notice injected\n`); } catch {}
|
|
172
151
|
throw new Error(`recall-fasttrack aborted: stored session memory unavailable; head preserved`);
|
|
173
152
|
}
|
|
174
|
-
const digestText = buildRecallDigestText(sessionId, digestBody
|
|
153
|
+
const digestText = buildRecallDigestText(sessionId, digestBody);
|
|
175
154
|
diagnostics.finalRecallChars = digestText.length;
|
|
176
155
|
diagnostics.finalRecallBytes = compactByteLength(digestText);
|
|
177
156
|
const contextWindow = positiveTokenInt(compactPolicy?.contextWindow)
|
|
@@ -192,7 +171,7 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
192
171
|
// Empty/sentinel browse output was rejected above, so the handoff always
|
|
193
172
|
// contains real stored session context.
|
|
194
173
|
allowEmptyRecall: false,
|
|
195
|
-
tailTurns:
|
|
174
|
+
tailTurns: RECALL_FAST_TRACK_TAIL_TURNS,
|
|
196
175
|
keepTokens: compactPolicy.keepTokens,
|
|
197
176
|
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
198
177
|
recallTokenCap,
|
|
@@ -6,22 +6,17 @@
|
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { getProvider } from '../../providers/registry.mjs';
|
|
8
8
|
import {
|
|
9
|
-
recallFastTrackCompactMessages,
|
|
10
9
|
semanticCompactMessages,
|
|
11
10
|
pruneToolOutputsUnanchored,
|
|
12
11
|
effectiveBudget as compactEffectiveBudget,
|
|
13
12
|
compactTypeIsRecallFastTrack,
|
|
14
13
|
compactTypeIsSemantic,
|
|
15
14
|
normalizeCompactType,
|
|
16
|
-
CONTEXT_SHARE_RATIO,
|
|
17
|
-
RECALL_TOKEN_CAP_FLOOR_TOKENS,
|
|
18
15
|
} from '../compact.mjs';
|
|
19
16
|
import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage, resolveCompactBufferRatio } from '../context-utils.mjs';
|
|
20
17
|
import { executeInternalTool } from '../../internal-tools.mjs';
|
|
21
18
|
import {
|
|
22
|
-
|
|
23
|
-
DIGEST_DEFAULT_MAX_KB,
|
|
24
|
-
isUsableRecallDigestText,
|
|
19
|
+
runRecallFastTrackCompact,
|
|
25
20
|
} from '../loop/recall-fasttrack.mjs';
|
|
26
21
|
import {
|
|
27
22
|
positiveContextWindow,
|
|
@@ -159,58 +154,6 @@ async function callMemoryBounded(args, callerCtx, timeoutMs, executeMemory = exe
|
|
|
159
154
|
try { outer?.removeEventListener?.('abort', onOuterAbort); } catch {}
|
|
160
155
|
}
|
|
161
156
|
}
|
|
162
|
-
async function runRecallFastTrackForSession(session, sessionId, opts = {}) {
|
|
163
|
-
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
164
|
-
const query = `session:${sessionId}:all-chunks`;
|
|
165
|
-
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
166
|
-
const callerCtx = {
|
|
167
|
-
callerSessionId: sessionId,
|
|
168
|
-
callerCwd: session?.cwd || undefined,
|
|
169
|
-
routingSessionId: sessionId,
|
|
170
|
-
clientHostPid: session?.clientHostPid,
|
|
171
|
-
signal: opts.signal || null,
|
|
172
|
-
};
|
|
173
|
-
const memoryTimeoutMs = recallMemoryTimeoutMs(session);
|
|
174
|
-
const executeMemory = typeof opts.executeInternalToolFn === 'function'
|
|
175
|
-
? opts.executeInternalToolFn
|
|
176
|
-
: executeInternalTool;
|
|
177
|
-
// The transcript watcher already persists this session incrementally.
|
|
178
|
-
// Manual/clear compaction reads those rows directly instead of sending the
|
|
179
|
-
// entire live message array through the memory RPC again.
|
|
180
|
-
let recallText = '';
|
|
181
|
-
try {
|
|
182
|
-
const browsed = await callMemoryColdStart({
|
|
183
|
-
action: 'search',
|
|
184
|
-
sessionId,
|
|
185
|
-
limit: positiveContextWindow(session?.compaction?.recallDigestLimit) || 100,
|
|
186
|
-
includeMembers: true,
|
|
187
|
-
includeRaw: true,
|
|
188
|
-
compactHandoff: true,
|
|
189
|
-
}, callerCtx, memoryTimeoutMs, executeMemory);
|
|
190
|
-
recallText = typeof browsed === 'string' ? browsed : String(browsed?.text ?? browsed ?? '');
|
|
191
|
-
if (!isUsableRecallDigestText(recallText)) {
|
|
192
|
-
throw new Error('memory has no stored history for this session');
|
|
193
|
-
}
|
|
194
|
-
} catch (err) {
|
|
195
|
-
// Without real stored context we cannot safely replace the live head.
|
|
196
|
-
// Bail to the semantic fallback rather than injecting a false recall
|
|
197
|
-
// handoff.
|
|
198
|
-
try { process.stderr.write(`[session] recall-digest browse failed — bailing (sess=${sessionId}): ${err?.message || err}\n`); } catch {}
|
|
199
|
-
throw new Error(`recall-fasttrack search failed: ${err?.message || err}`);
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
query,
|
|
203
|
-
querySha,
|
|
204
|
-
recallText: [
|
|
205
|
-
`session_id=${sessionId}`,
|
|
206
|
-
// Same byte cap as the loop digest path (recallDigestMaxKb,
|
|
207
|
-
// default = shared tool-output limit) — without it the memory
|
|
208
|
-
// renderer bounds the browse at ~200 rows × 1000 chars, letting a
|
|
209
|
-
// manual//clear compact process a far larger digest than loop's.
|
|
210
|
-
truncateToKb(recallText, positiveContextWindow(session?.compaction?.recallDigestMaxKb) || DIGEST_DEFAULT_MAX_KB),
|
|
211
|
-
].map(v => String(v || '').trim()).filter(Boolean).join('\n\n'),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
157
|
// Element-identity change detection (same approach as loop.mjs messagesArrayChanged): two
|
|
215
158
|
// arrays are "unchanged" only when same length AND every slot is the same object
|
|
216
159
|
// reference. Used to reject a no-op prune (which returns a fresh array whose
|
|
@@ -307,27 +250,27 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
307
250
|
let recallFastTrackError = null;
|
|
308
251
|
if (compactTypeIsRecallFastTrack(compactType)) {
|
|
309
252
|
try {
|
|
310
|
-
const recallPayload = await runRecallFastTrackForSession(session, resolvedSessionId, opts);
|
|
311
253
|
const contextWindow = positiveContextWindow(session.contextWindow) || boundary;
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
recallFastTrackResult =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
328
|
-
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
329
|
-
recallTokenCap,
|
|
254
|
+
const memoryTimeoutMs = recallMemoryTimeoutMs(session);
|
|
255
|
+
const executeMemory = typeof opts.executeInternalToolFn === 'function'
|
|
256
|
+
? opts.executeInternalToolFn
|
|
257
|
+
: executeInternalTool;
|
|
258
|
+
recallFastTrackResult = await runRecallFastTrackCompact({
|
|
259
|
+
sessionRef: session,
|
|
260
|
+
messages,
|
|
261
|
+
compactBudgetTokens: budget,
|
|
262
|
+
compactPolicy: {
|
|
263
|
+
reserveTokens,
|
|
264
|
+
contextWindow,
|
|
265
|
+
boundaryTokens: boundary,
|
|
266
|
+
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
267
|
+
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
268
|
+
},
|
|
330
269
|
sessionId: resolvedSessionId,
|
|
270
|
+
signal: opts.signal || null,
|
|
271
|
+
executeMemorySearch: (args, callerCtx) => (
|
|
272
|
+
callMemoryColdStart(args, callerCtx, memoryTimeoutMs, executeMemory)
|
|
273
|
+
),
|
|
331
274
|
});
|
|
332
275
|
if (Array.isArray(recallFastTrackResult?.messages)) {
|
|
333
276
|
compacted = recallFastTrackResult.messages;
|
|
@@ -338,54 +281,6 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
338
281
|
try {
|
|
339
282
|
process.stderr.write(`[session] recall-fasttrack ${mode} compact failed (sess=${session.id || 'unknown'}): ${err?.message || err}\n`);
|
|
340
283
|
} catch { /* best-effort */ }
|
|
341
|
-
// Degraded-compact fallback: recall-fasttrack failed (empty recall,
|
|
342
|
-
// ingest error, fit failure). Before recording a hard failure, try
|
|
343
|
-
// the semantic path once so auto-clear/manual compaction still makes
|
|
344
|
-
// progress WITHOUT shipping an empty-recall summary. History is only
|
|
345
|
-
// replaced when the semantic summary actually succeeds.
|
|
346
|
-
if (semanticCompactionEnabledForSession(session)
|
|
347
|
-
&& provider && typeof provider.send === 'function') {
|
|
348
|
-
try {
|
|
349
|
-
semanticCompactResult = await semanticCompactMessages(
|
|
350
|
-
provider,
|
|
351
|
-
messages,
|
|
352
|
-
opts.model || resolveSemanticSummaryModel(session, { budgetTokens: budget }) || session.model,
|
|
353
|
-
budget,
|
|
354
|
-
{
|
|
355
|
-
reserveTokens,
|
|
356
|
-
providerName: session.provider || provider?.name || null,
|
|
357
|
-
sessionId: resolvedSessionId,
|
|
358
|
-
cwd: session.cwd,
|
|
359
|
-
signal: opts.signal || null,
|
|
360
|
-
// Carries the session's wire identity so the
|
|
361
|
-
// summary request stays on the session's own
|
|
362
|
-
// prefix-cache slot instead of opening a second one.
|
|
363
|
-
sendOpts: { session },
|
|
364
|
-
promptCacheKey: session.promptCacheKey || null,
|
|
365
|
-
providerCacheKey: session.promptCacheKey || null,
|
|
366
|
-
timeoutMs: semanticCompactTimeoutMs(session, beforeMessageTokens),
|
|
367
|
-
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
368
|
-
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
369
|
-
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
370
|
-
filterOldHistoryForIngest: opts.filterOldHistoryForIngest === true,
|
|
371
|
-
force: true,
|
|
372
|
-
},
|
|
373
|
-
);
|
|
374
|
-
if (Array.isArray(semanticCompactResult?.messages)) {
|
|
375
|
-
compacted = semanticCompactResult.messages;
|
|
376
|
-
compactError = null;
|
|
377
|
-
addCompactUsageToSession(session, semanticCompactResult.usage);
|
|
378
|
-
try {
|
|
379
|
-
process.stderr.write(`[session] degraded compact: recall-fasttrack failed, semantic fallback succeeded (sess=${session.id || 'unknown'}, mode=${mode})\n`);
|
|
380
|
-
} catch { /* best-effort */ }
|
|
381
|
-
}
|
|
382
|
-
} catch (fallbackErr) {
|
|
383
|
-
semanticCompactError = fallbackErr;
|
|
384
|
-
try {
|
|
385
|
-
process.stderr.write(`[session] degraded compact: semantic fallback also failed (sess=${session.id || 'unknown'}): ${fallbackErr?.message || fallbackErr}\n`);
|
|
386
|
-
} catch { /* best-effort */ }
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
284
|
}
|
|
390
285
|
} else if (compactTypeIsSemantic(compactType)) {
|
|
391
286
|
try {
|
|
@@ -40,11 +40,8 @@ import { getAgentRuntimeSync, warnAgentRuntimeResolveFailureOnce } from './agent
|
|
|
40
40
|
import { ensureCodexWireSessionId, mintSessionId, mintUuidV7 } from './session-id.mjs';
|
|
41
41
|
import { providerCacheKey } from './provider-cache-key.mjs';
|
|
42
42
|
import { clearTurnCheckpoint, recoverTurnCheckpoint } from './turn-checkpoint.mjs';
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
workflowContextForApprovalMode,
|
|
46
|
-
} from '../approval-mode.mjs';
|
|
47
|
-
import { describeShellStartupPolicy } from '../../tools/builtin/runtime-capabilities.mjs';
|
|
43
|
+
import { IMPLICIT_APPROVAL_MODE } from '../approval-mode.mjs';
|
|
44
|
+
import { describeGitStartupState } from '../../tools/builtin/runtime-capabilities.mjs';
|
|
48
45
|
import { captureOriginalUserCwd } from '../../../../shared/user-cwd.mjs';
|
|
49
46
|
import { refreshSessionBp3Environment } from './prompt-utils.mjs';
|
|
50
47
|
|
|
@@ -314,6 +311,7 @@ export function createSession(opts) {
|
|
|
314
311
|
// resolves explicit session signals first and never leaks the daemon's
|
|
315
312
|
// install root (user-cwd.mjs safe fallback chain).
|
|
316
313
|
const sessionCwdLine = opts.cwd || captureOriginalUserCwd();
|
|
314
|
+
const wantsGitStartupLine = toolsForRouting.some((tool) => tool?.name === 'git');
|
|
317
315
|
const shellEnvironmentContext = [
|
|
318
316
|
sessionCwdLine
|
|
319
317
|
? `- Cwd: ${sessionCwdLine} — the active Project root; relative paths and shell commands resolve here.`
|
|
@@ -321,8 +319,15 @@ export function createSession(opts) {
|
|
|
321
319
|
!ownerIsAgent
|
|
322
320
|
? `- Shell: ${process.platform === 'win32' ? 'PowerShell' : 'Bash'}. Use ${process.platform === 'win32' ? 'PowerShell' : 'Bash'} syntax unless the user specifies otherwise.`
|
|
323
321
|
: '',
|
|
324
|
-
|
|
325
|
-
|
|
322
|
+
// A startup inventory of PATH binaries used to sit here; see
|
|
323
|
+
// runtime-capabilities.mjs for why it cannot be stated truthfully
|
|
324
|
+
// before a command has run. Whether the cwd is inside a repository is
|
|
325
|
+
// different: it is a property of this directory, true at startup and
|
|
326
|
+
// observable without spawning anything, and the git tool cannot infer
|
|
327
|
+
// it. Without it a session spends a call discovering `exited 128`, and
|
|
328
|
+
// repeats it per candidate path.
|
|
329
|
+
wantsGitStartupLine
|
|
330
|
+
? describeGitStartupState(sessionCwdLine ? { cwd: sessionCwdLine } : {})
|
|
326
331
|
: '',
|
|
327
332
|
].filter(Boolean).join('\n');
|
|
328
333
|
const { baseRules, stableSystemContext, sessionMarkerCore, sessionEnvironment } = composeSystemPrompt({
|
|
@@ -333,7 +338,7 @@ export function createSession(opts) {
|
|
|
333
338
|
skipRoleCatalog: !ownerIsAgent,
|
|
334
339
|
profile: profile || undefined,
|
|
335
340
|
agent: resolvedAgent,
|
|
336
|
-
workflowContext:
|
|
341
|
+
workflowContext: opts.workflowContext || null,
|
|
337
342
|
coreMemoryContext: opts.coreMemoryContext || null,
|
|
338
343
|
skillManifest: buildSkillManifest(skills),
|
|
339
344
|
environmentContext: shellEnvironmentContext,
|
|
@@ -493,6 +498,36 @@ export function contextSeedForRouteUpdate(session, routeChanged, selectedContext
|
|
|
493
498
|
: {};
|
|
494
499
|
}
|
|
495
500
|
|
|
501
|
+
// The shared-rules block (BP1) renders tool-conditional variants against the
|
|
502
|
+
// edit dialect the model actually receives (edit vs apply_patch). An
|
|
503
|
+
// empty-session route change swaps the tool surface, so this block must
|
|
504
|
+
// re-render too — otherwise a session created on a GPT default route and
|
|
505
|
+
// switched to Claude keeps apply_patch placement guidance for a tool it can
|
|
506
|
+
// no longer call (and vice versa). The block is identified by EXACT previous
|
|
507
|
+
// content: the old variant is rebuilt from the same inputs and matched, so
|
|
508
|
+
// only the true BP1 block is ever replaced; custom-prompt or agent layouts
|
|
509
|
+
// without that block are left untouched, and a same-dialect switch is a no-op.
|
|
510
|
+
export function _refreshSessionRuleVariantsForModel(session, previousModel) {
|
|
511
|
+
const deny = [
|
|
512
|
+
...(Array.isArray(session?.disallowedTools) ? session.disallowedTools : []),
|
|
513
|
+
...(getHiddenAgent(session?.agent || null) ? ['Skill'] : []),
|
|
514
|
+
...(!isAgentOwner(session) && workflowDisallowsAgentTool(session?.workflow) ? ['agent'] : []),
|
|
515
|
+
];
|
|
516
|
+
const previousRules = _buildSharedRules({ omitTools: [...deny, unusedModelEditToolName(previousModel)] });
|
|
517
|
+
const nextRules = _buildSharedRules({ omitTools: [...deny, unusedModelEditToolName(session?.model)] });
|
|
518
|
+
if (!previousRules || previousRules === nextRules) return false;
|
|
519
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
520
|
+
const index = messages.findIndex((message) => (
|
|
521
|
+
message?.role === 'system' && !message.cacheTier && message.content === previousRules
|
|
522
|
+
));
|
|
523
|
+
if (index < 0) return false;
|
|
524
|
+
// Replace, never mutate: session-store delta saves treat stable message
|
|
525
|
+
// references as an append-only prefix, so a fresh object forces the full
|
|
526
|
+
// snapshot that keeps the persisted transcript in sync.
|
|
527
|
+
messages[index] = { ...messages[index], content: nextRules };
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
|
|
496
531
|
export function updateSessionRoute(id, route = {}) {
|
|
497
532
|
if (!id) return null;
|
|
498
533
|
const session = loadSession(id);
|
|
@@ -576,8 +611,15 @@ export function updateSessionRoute(id, route = {}) {
|
|
|
576
611
|
for (const key of ['deferredSelectedTools', 'deferredCallableTools', 'deferredDefaultTools', 'deferredDiscoveredTools']) {
|
|
577
612
|
if (Array.isArray(session[key])) session[key] = filterModelEditToolNames(session[key], session.model);
|
|
578
613
|
}
|
|
614
|
+
_refreshSessionRuleVariantsForModel(session, previousModel);
|
|
579
615
|
_preparedResumes.delete(id);
|
|
580
616
|
}
|
|
617
|
+
// Route fields feed the `# Session` prompt block (Model: … · EFFORT · FAST).
|
|
618
|
+
// Rebuild it here: createSession stamped the block with the creation-time
|
|
619
|
+
// route and set sessionStartMetaInjected, so the ask-time refresh guard
|
|
620
|
+
// skips it and an empty-session route change would otherwise keep the old
|
|
621
|
+
// model line in the system prompt (model self-identity confusion).
|
|
622
|
+
refreshSessionBp3Environment(session, session.cwd);
|
|
581
623
|
session.updatedAt = Date.now();
|
|
582
624
|
setLiveSession(session);
|
|
583
625
|
void saveSessionAsync(session, { expectedGeneration: session.generation })
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { test } from 'node:test';
|
|
3
3
|
import { resolveSessionContextMeta } from './context-meta.mjs';
|
|
4
|
-
import { contextSeedForRouteUpdate } from './session-lifecycle.mjs';
|
|
4
|
+
import { contextSeedForRouteUpdate, _refreshSessionRuleVariantsForModel } from './session-lifecycle.mjs';
|
|
5
|
+
import { _buildSharedRules } from './rules-cache.mjs';
|
|
5
6
|
|
|
6
7
|
test('a cold Cursor route uses 200k instead of inheriting another model window', () => {
|
|
7
8
|
const seed = contextSeedForRouteUpdate({
|
|
@@ -38,3 +39,32 @@ test('an explicitly selected context window survives a route update', () => {
|
|
|
38
39
|
selectedContextWindow: 300_000,
|
|
39
40
|
});
|
|
40
41
|
});
|
|
42
|
+
|
|
43
|
+
test('an empty-session route change re-renders the edit-dialect rule variants', () => {
|
|
44
|
+
const gptRules = _buildSharedRules({ omitTools: ['edit'] });
|
|
45
|
+
const claudeRules = _buildSharedRules({ omitTools: ['apply_patch'] });
|
|
46
|
+
assert.notEqual(gptRules, claudeRules);
|
|
47
|
+
const session = {
|
|
48
|
+
model: 'claude-fable-5',
|
|
49
|
+
messages: [
|
|
50
|
+
{ role: 'system', content: gptRules },
|
|
51
|
+
{ role: 'system', content: 'profile block' },
|
|
52
|
+
{ role: 'system', content: 'core block', cacheTier: 'tier3' },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
// GPT-created session switched to Claude: BP1 flips to the edit variant.
|
|
56
|
+
assert.equal(_refreshSessionRuleVariantsForModel(session, 'gpt-5.6-sol'), true);
|
|
57
|
+
assert.equal(session.messages[0].content, claudeRules);
|
|
58
|
+
assert.match(session.messages[0].content, /`edit`/);
|
|
59
|
+
assert.doesNotMatch(session.messages[0].content, /apply_patch/);
|
|
60
|
+
// Untouched blocks keep their identity and content.
|
|
61
|
+
assert.equal(session.messages[1].content, 'profile block');
|
|
62
|
+
assert.equal(session.messages[2].cacheTier, 'tier3');
|
|
63
|
+
// Same edit dialect on both sides is a no-op.
|
|
64
|
+
assert.equal(_refreshSessionRuleVariantsForModel(session, 'claude-opus-5'), false);
|
|
65
|
+
// A layout without the expected BP1 content is left untouched.
|
|
66
|
+
assert.equal(_refreshSessionRuleVariantsForModel({
|
|
67
|
+
model: 'claude-fable-5',
|
|
68
|
+
messages: [{ role: 'system', content: 'custom prompt' }],
|
|
69
|
+
}, 'gpt-5.6-sol'), false);
|
|
70
|
+
});
|
|
@@ -30,22 +30,41 @@ export function _sessionForDisk(session) {
|
|
|
30
30
|
// persist a non-canonical message array).
|
|
31
31
|
// - toolApprovalHook: the askOpts.onToolApproval callback wired for the
|
|
32
32
|
// turn — a function that must never be serialized.
|
|
33
|
+
// - _providerPrefixGuardState: hashes of the live provider projection.
|
|
34
|
+
// Stored history intentionally omits inline media, so these hashes are
|
|
35
|
+
// valid only for the current runtime and must not survive a reload.
|
|
33
36
|
const hasTransient = session && typeof session === 'object'
|
|
34
37
|
&& (Object.prototype.hasOwnProperty.call(session, 'liveTurnMessages')
|
|
35
|
-
|| Object.prototype.hasOwnProperty.call(session, 'toolApprovalHook')
|
|
38
|
+
|| Object.prototype.hasOwnProperty.call(session, 'toolApprovalHook')
|
|
39
|
+
|| Object.prototype.hasOwnProperty.call(session, '_providerPrefixGuardState'));
|
|
36
40
|
const messages = Array.isArray(session?.messages) ? session.messages : null;
|
|
37
41
|
if (!messages || messages.length === 0) {
|
|
38
42
|
if (!hasTransient) return session;
|
|
39
|
-
const {
|
|
43
|
+
const {
|
|
44
|
+
liveTurnMessages: _dropLTM,
|
|
45
|
+
toolApprovalHook: _dropTAH,
|
|
46
|
+
_providerPrefixGuardState: _dropPPGS,
|
|
47
|
+
...rest
|
|
48
|
+
} = session;
|
|
40
49
|
return rest;
|
|
41
50
|
}
|
|
42
51
|
const out = _messagesForDisk(messages);
|
|
43
52
|
if (out === messages) {
|
|
44
53
|
if (!hasTransient) return session;
|
|
45
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
liveTurnMessages: _dropLTM,
|
|
56
|
+
toolApprovalHook: _dropTAH,
|
|
57
|
+
_providerPrefixGuardState: _dropPPGS,
|
|
58
|
+
...rest
|
|
59
|
+
} = session;
|
|
46
60
|
return rest;
|
|
47
61
|
}
|
|
48
|
-
const {
|
|
62
|
+
const {
|
|
63
|
+
liveTurnMessages: _dropLTM,
|
|
64
|
+
toolApprovalHook: _dropTAH,
|
|
65
|
+
_providerPrefixGuardState: _dropPPGS,
|
|
66
|
+
...rest
|
|
67
|
+
} = session;
|
|
49
68
|
return { ...rest, messages: out };
|
|
50
69
|
}
|
|
51
70
|
|
|
@@ -95,6 +114,10 @@ export function _storedSessionFromFile(dir, filename, ensureLifecycle = true) {
|
|
|
95
114
|
const record = readTopLevelLifecycleRecord(text);
|
|
96
115
|
if (isLifecycleUnreadable(record) || record.id !== storageId) return null;
|
|
97
116
|
const session = record.doc;
|
|
117
|
+
// Older builds persisted this runtime-local snapshot. Discard it at
|
|
118
|
+
// the reload boundary before sanitized history establishes a new
|
|
119
|
+
// provider prefix baseline.
|
|
120
|
+
delete session._providerPrefixGuardState;
|
|
98
121
|
return ensureLifecycle ? _ensureLifecycleFields(session) : session;
|
|
99
122
|
} catch {
|
|
100
123
|
return null;
|