mixdog 0.9.151 → 0.9.152
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 +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { isContextOverflowError } from '../providers/retry-classifier.mjs';
|
|
18
18
|
import { tryReadCached, setReadCached, invalidatePathForSession, clearReadDedupSession, extractTouchedPathsFromPatch, tryScopedToolCached, setScopedToolCached, clearScopedToolsForSession, clearScopedToolsForSessionPaths, invalidatePrefetchCache } from './read-dedup.mjs';
|
|
19
19
|
import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
|
|
20
|
+
import { cloneProviderReplay } from '../providers/lib/provider-replay.mjs';
|
|
20
21
|
|
|
21
22
|
import {
|
|
22
23
|
_stripMcpPrefix,
|
|
@@ -310,6 +311,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
310
311
|
const reasoningItems = Array.isArray(resp.reasoningItems) && resp.reasoningItems.length
|
|
311
312
|
? resp.reasoningItems
|
|
312
313
|
: null;
|
|
314
|
+
const providerReplay = cloneProviderReplay(resp.providerReplay);
|
|
313
315
|
const thinkingBlocks = Array.isArray(resp.thinkingBlocks) && resp.thinkingBlocks.length
|
|
314
316
|
? resp.thinkingBlocks
|
|
315
317
|
: null;
|
|
@@ -335,18 +337,19 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
335
337
|
: null;
|
|
336
338
|
// A native-only turn (server tool blocks, no flattened text) is real
|
|
337
339
|
// assistant output and must stay committable — not treated as empty.
|
|
338
|
-
if (!content && !reasoningContent && !reasoningItems && !thinkingBlocks && !assistantBlocks) return false;
|
|
340
|
+
if (!content && !reasoningContent && !reasoningItems && !thinkingBlocks && !assistantBlocks && !providerReplay) return false;
|
|
339
341
|
const message = attachAssistantTranscriptMetadata({
|
|
340
342
|
role: 'assistant',
|
|
341
343
|
content,
|
|
344
|
+
...(providerReplay ? { providerReplay } : {}),
|
|
342
345
|
// assistantBlocks already contains the thinking blocks verbatim in
|
|
343
346
|
// stream order, so thinkingBlocks is redundant (and would be
|
|
344
347
|
// double-counted by the context estimator) when it is present.
|
|
345
|
-
...(assistantBlocks ? { assistantBlocks } : {}),
|
|
348
|
+
...(assistantBlocks && !providerReplay ? { assistantBlocks } : {}),
|
|
346
349
|
// Anthropic adaptive-thinking signatures must be replayed verbatim
|
|
347
350
|
// before the continuation turn, just like tool-call trajectories.
|
|
348
|
-
...(thinkingBlocks && !assistantBlocks ? { thinkingBlocks } : {}),
|
|
349
|
-
...(reasoningItems ? { reasoningItems } : {}),
|
|
351
|
+
...(thinkingBlocks && !assistantBlocks && !providerReplay ? { thinkingBlocks } : {}),
|
|
352
|
+
...(reasoningItems && !providerReplay ? { reasoningItems } : {}),
|
|
350
353
|
...(reasoningContent ? { reasoningContent } : {}),
|
|
351
354
|
...(providerMetadata ? { providerMetadata } : {}),
|
|
352
355
|
...(stopReason ? { stopReason } : {}),
|
|
@@ -1011,6 +1014,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1011
1014
|
// every assistant message body.
|
|
1012
1015
|
recordToolBatch(sessionId, calls.length);
|
|
1013
1016
|
await Promise.resolve(onToolCall?.(iterations, calls));
|
|
1017
|
+
const _providerReplay = cloneProviderReplay(response.providerReplay);
|
|
1014
1018
|
// Append assistant message with tool calls. reasoningItems is the
|
|
1015
1019
|
// OpenAI Responses API replay payload (encrypted_content blobs);
|
|
1016
1020
|
// providers that ignore it just see an extra field and drop it,
|
|
@@ -1030,6 +1034,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1030
1034
|
// the whole request uncached — so a model that patches twice in a
|
|
1031
1035
|
// row also never sees a marker where its own last patch should be.
|
|
1032
1036
|
toolCalls: compactToolCallsForHistory(calls, { deferBodies: true }),
|
|
1037
|
+
...(_providerReplay
|
|
1038
|
+
? { providerReplay: _providerReplay }
|
|
1039
|
+
: {}),
|
|
1033
1040
|
// MIXED Anthropic turn (native server tools + client tool_use):
|
|
1034
1041
|
// the ordered `server_tool_use` / `*_tool_result` blocks exist ONLY
|
|
1035
1042
|
// in this verbatim list and are order-bound (a result block is
|
|
@@ -1040,6 +1047,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1040
1047
|
// text/thinking/tool_use blocks), so nothing is emitted twice and
|
|
1041
1048
|
// non-Anthropic providers simply ignore the field.
|
|
1042
1049
|
...(Array.isArray(response.assistantBlocks) && response.assistantBlocks.length
|
|
1050
|
+
&& !_providerReplay
|
|
1043
1051
|
? { assistantBlocks: response.assistantBlocks }
|
|
1044
1052
|
: {}),
|
|
1045
1053
|
// Anthropic adaptive thinking: prior-turn thinking blocks must be
|
|
@@ -1051,9 +1059,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1051
1059
|
// second copy would double-count in the context estimator.
|
|
1052
1060
|
...(Array.isArray(response.thinkingBlocks) && response.thinkingBlocks.length
|
|
1053
1061
|
&& !(Array.isArray(response.assistantBlocks) && response.assistantBlocks.length)
|
|
1062
|
+
&& !_providerReplay
|
|
1054
1063
|
? { thinkingBlocks: response.thinkingBlocks }
|
|
1055
1064
|
: {}),
|
|
1056
1065
|
...(Array.isArray(response.reasoningItems) && response.reasoningItems.length
|
|
1066
|
+
&& !_providerReplay
|
|
1057
1067
|
? { reasoningItems: response.reasoningItems }
|
|
1058
1068
|
: {}),
|
|
1059
1069
|
...(typeof response.reasoningContent === 'string' && response.reasoningContent
|
|
@@ -260,6 +260,19 @@ export function clearReadDedupSession(sessionId) {
|
|
|
260
260
|
clearScopedCounters(sessionId);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Start a fresh read epoch after compaction removes prior tool results from
|
|
265
|
+
* the active context. Both layers must reset together: retaining the result
|
|
266
|
+
* cache can bypass snapshot recreation, while retaining the persisted
|
|
267
|
+
* snapshot can make a fresh read answer only "[file unchanged]".
|
|
268
|
+
*/
|
|
269
|
+
export function resetReadStateAfterCompaction(sessionId) {
|
|
270
|
+
if (!sessionId) return;
|
|
271
|
+
_bySession.delete(sessionId);
|
|
272
|
+
_reverseIdx.delete(sessionId);
|
|
273
|
+
releaseReadSnapshotScope(sessionId, { deletePersisted: true, persist: false });
|
|
274
|
+
}
|
|
275
|
+
|
|
263
276
|
registerSessionPurgeHook((sessionId) => {
|
|
264
277
|
clearReadDedupSession(sessionId);
|
|
265
278
|
releaseReadSnapshotScope(sessionId, { deletePersisted: true, persist: false });
|
|
@@ -589,11 +589,34 @@ function withoutRecallReplayMetadata(message) {
|
|
|
589
589
|
return copy;
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
function hasAtomicReplayState(message) {
|
|
593
|
+
if (!message || message.role !== 'assistant') return false;
|
|
594
|
+
if (Array.isArray(message.providerReplay?.items) && message.providerReplay.items.length) return true;
|
|
595
|
+
if (Array.isArray(message.assistantBlocks) && message.assistantBlocks.length) return true;
|
|
596
|
+
if (Array.isArray(message.thinkingBlocks) && message.thinkingBlocks.length) return true;
|
|
597
|
+
if (Array.isArray(message.reasoningItems) && message.reasoningItems.length) return true;
|
|
598
|
+
if (Array.isArray(message.providerMetadata?.gemini?.thoughtParts)
|
|
599
|
+
&& message.providerMetadata.gemini.thoughtParts.length) return true;
|
|
600
|
+
if (Array.isArray(message.providerMetadata?.gemini?.textParts)
|
|
601
|
+
&& message.providerMetadata.gemini.textParts.length) return true;
|
|
602
|
+
if (Array.isArray(message.toolCalls)
|
|
603
|
+
&& message.toolCalls.some((call) => typeof call?.thoughtSignature === 'string' && call.thoughtSignature)) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
if (typeof message.reasoningContent === 'string' && message.reasoningContent) return true;
|
|
607
|
+
const openRouter = message.providerMetadata?.openrouter;
|
|
608
|
+
const reasoningDetails = openRouter?.reasoning_details ?? openRouter?.reasoningDetails;
|
|
609
|
+
return Array.isArray(reasoningDetails) && reasoningDetails.length > 0;
|
|
610
|
+
}
|
|
611
|
+
|
|
592
612
|
function fitRecallMessageToCap(message, cap, rawText = null) {
|
|
593
613
|
const text = rawText == null
|
|
594
614
|
? (typeof message?.content === 'string' ? message.content : extractText(message))
|
|
595
615
|
: String(rawText);
|
|
596
|
-
|
|
616
|
+
const bases = hasAtomicReplayState(message)
|
|
617
|
+
? [message]
|
|
618
|
+
: [message, withoutRecallReplayMetadata(message)];
|
|
619
|
+
for (const base of bases) {
|
|
597
620
|
if (!base || typeof base !== 'object') continue;
|
|
598
621
|
if (estimateMessagesTokens([{ ...base, content: text }]) <= cap) {
|
|
599
622
|
return { ...base, content: text };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { executeBuiltinTool } from '../tools/builtin.mjs';
|
|
8
|
+
import { COMPACT_TYPE_SEMANTIC } from './compact.mjs';
|
|
9
|
+
import { runSessionCompaction } from './manager/compaction-runner.mjs';
|
|
10
|
+
import { runPreSendCompactPass } from './pre-send-compact.mjs';
|
|
11
|
+
import { resetReadStateAfterCompaction } from './read-dedup.mjs';
|
|
12
|
+
|
|
13
|
+
const SUMMARY = [
|
|
14
|
+
'## Goal',
|
|
15
|
+
'- continue after compaction',
|
|
16
|
+
'',
|
|
17
|
+
'## Constraints & Preferences',
|
|
18
|
+
'- (none)',
|
|
19
|
+
'',
|
|
20
|
+
'## Progress',
|
|
21
|
+
'### Done',
|
|
22
|
+
'- old context summarized',
|
|
23
|
+
'',
|
|
24
|
+
'### In Progress',
|
|
25
|
+
'- (none)',
|
|
26
|
+
'',
|
|
27
|
+
'### Blocked',
|
|
28
|
+
'- (none)',
|
|
29
|
+
'',
|
|
30
|
+
'## Key Decisions',
|
|
31
|
+
'- (none)',
|
|
32
|
+
'',
|
|
33
|
+
'## Next Steps',
|
|
34
|
+
'- reread the working file',
|
|
35
|
+
'',
|
|
36
|
+
'## Critical Context',
|
|
37
|
+
'- active file must be readable again',
|
|
38
|
+
'',
|
|
39
|
+
'## Relevant Files',
|
|
40
|
+
'- working.txt',
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
function fixture(t, tag) {
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), `mixdog-compact-read-${tag}-`));
|
|
45
|
+
const file = join(dir, 'working.txt');
|
|
46
|
+
const sessionId = `compact-read-${tag}-${process.pid}-${Date.now()}`;
|
|
47
|
+
writeFileSync(file, 'alpha\nbeta\n', 'utf8');
|
|
48
|
+
t.after(() => {
|
|
49
|
+
resetReadStateAfterCompaction(sessionId);
|
|
50
|
+
rmSync(dir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
return { dir, file, sessionId };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function establishUnchangedSnapshot({ dir, file, sessionId }) {
|
|
56
|
+
const first = String(await executeBuiltinTool('read', { path: file }, dir, { sessionId }));
|
|
57
|
+
assert.match(first, /alpha/);
|
|
58
|
+
const duplicate = await executeBuiltinTool('read', { path: file }, dir, { sessionId });
|
|
59
|
+
assert.equal(duplicate, `[file unchanged: ${file.replaceAll('\\', '/')}]`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function semanticProvider(name) {
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
async send() {
|
|
66
|
+
return { content: SUMMARY };
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test('manual compaction starts a fresh read epoch', async (t) => {
|
|
72
|
+
const fx = fixture(t, 'manual');
|
|
73
|
+
await establishUnchangedSnapshot(fx);
|
|
74
|
+
|
|
75
|
+
const session = {
|
|
76
|
+
id: fx.sessionId,
|
|
77
|
+
provider: 'compact-read-manual',
|
|
78
|
+
model: 'fake-model',
|
|
79
|
+
contextWindow: 100_000,
|
|
80
|
+
compactBoundaryTokens: 100_000,
|
|
81
|
+
messages: [
|
|
82
|
+
{ role: 'system', content: 'system rules stay exact' },
|
|
83
|
+
{ role: 'user', content: 'older request about working.txt' },
|
|
84
|
+
{ role: 'assistant', content: 'older answer with file details' },
|
|
85
|
+
{ role: 'user', content: 'current request remains verbatim' },
|
|
86
|
+
],
|
|
87
|
+
tools: [],
|
|
88
|
+
compaction: {
|
|
89
|
+
type: COMPACT_TYPE_SEMANTIC,
|
|
90
|
+
compactType: COMPACT_TYPE_SEMANTIC,
|
|
91
|
+
tailTurns: 1,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const result = await runSessionCompaction(session, {
|
|
95
|
+
mode: 'manual',
|
|
96
|
+
force: true,
|
|
97
|
+
compactType: COMPACT_TYPE_SEMANTIC,
|
|
98
|
+
provider: semanticProvider('compact-read-manual'),
|
|
99
|
+
model: 'fake-model',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.equal(result.changed, true);
|
|
103
|
+
const reread = String(await executeBuiltinTool('read', { path: fx.file }, fx.dir, { sessionId: fx.sessionId }));
|
|
104
|
+
assert.doesNotMatch(reread, /file unchanged/);
|
|
105
|
+
assert.match(reread, /alpha/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('automatic pre-send compaction resets reads only after transcript change', async (t) => {
|
|
109
|
+
const fx = fixture(t, 'auto');
|
|
110
|
+
await establishUnchangedSnapshot(fx);
|
|
111
|
+
|
|
112
|
+
const session = {
|
|
113
|
+
id: fx.sessionId,
|
|
114
|
+
owner: 'agent',
|
|
115
|
+
provider: 'compact-read-auto',
|
|
116
|
+
model: 'fake-model',
|
|
117
|
+
cwd: fx.dir,
|
|
118
|
+
contextWindow: 20_000,
|
|
119
|
+
compactBoundaryTokens: 20_000,
|
|
120
|
+
messages: [
|
|
121
|
+
{ role: 'user', content: 'small request' },
|
|
122
|
+
{ role: 'assistant', content: 'small answer' },
|
|
123
|
+
],
|
|
124
|
+
tools: [],
|
|
125
|
+
compaction: { auto: true, tailTurns: 1 },
|
|
126
|
+
};
|
|
127
|
+
const state = {
|
|
128
|
+
provider: semanticProvider('compact-read-auto'),
|
|
129
|
+
messages: session.messages,
|
|
130
|
+
model: session.model,
|
|
131
|
+
requestTools: [],
|
|
132
|
+
sessionRef: session,
|
|
133
|
+
sessionId: fx.sessionId,
|
|
134
|
+
cwd: fx.dir,
|
|
135
|
+
opts: {},
|
|
136
|
+
signal: null,
|
|
137
|
+
iterations: 1,
|
|
138
|
+
lastUsage: null,
|
|
139
|
+
firstTurnUsage: null,
|
|
140
|
+
providerState: null,
|
|
141
|
+
reactiveOverflowRetryPending: false,
|
|
142
|
+
loopUsageMetricsTurnId: () => 'test-turn',
|
|
143
|
+
loopUsageMetricsEpoch: () => 0,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const noChange = await runPreSendCompactPass(state);
|
|
147
|
+
assert.equal(noChange.compactChanged, false);
|
|
148
|
+
const stillUnchanged = await executeBuiltinTool('read', { path: fx.file }, fx.dir, { sessionId: fx.sessionId });
|
|
149
|
+
assert.equal(stillUnchanged, `[file unchanged: ${fx.file.replaceAll('\\', '/')}]`);
|
|
150
|
+
|
|
151
|
+
session.messages.unshift(
|
|
152
|
+
{ role: 'user', content: `large old request ${'context '.repeat(12_000)}` },
|
|
153
|
+
{ role: 'assistant', content: `large old answer ${'detail '.repeat(12_000)}` },
|
|
154
|
+
);
|
|
155
|
+
const changed = await runPreSendCompactPass({ ...state, messages: session.messages });
|
|
156
|
+
assert.equal(changed.compactChanged, true);
|
|
157
|
+
|
|
158
|
+
const reread = String(await executeBuiltinTool('read', { path: fx.file }, fx.dir, { sessionId: fx.sessionId }));
|
|
159
|
+
assert.doesNotMatch(reread, /file unchanged/);
|
|
160
|
+
assert.match(reread, /alpha/);
|
|
161
|
+
});
|
|
@@ -336,20 +336,26 @@ function messageEstimateText(m) {
|
|
|
336
336
|
try { text += `\n${JSON.stringify(m.toolCalls)}`; }
|
|
337
337
|
catch { text += `\n[${m.toolCalls.length} tool calls]`; }
|
|
338
338
|
}
|
|
339
|
+
const providerReplayItems = m.role === 'assistant' && Array.isArray(m.providerReplay?.items)
|
|
340
|
+
? m.providerReplay.items
|
|
341
|
+
: null;
|
|
342
|
+
if (providerReplayItems?.length) {
|
|
343
|
+
text += `\n${nativeBlocksEstimateText(providerReplayItems)}`;
|
|
344
|
+
}
|
|
339
345
|
// Anthropic adaptive-thinking blocks round-trip verbatim (thinking text +
|
|
340
346
|
// signature / redacted data) and are re-sent on tool-continuation turns, so
|
|
341
347
|
// they consume real input tokens. Count them or trim/compact undercounts.
|
|
342
|
-
if (m.role === 'assistant' && Array.isArray(m.thinkingBlocks) && m.thinkingBlocks.length) {
|
|
348
|
+
if (!providerReplayItems && m.role === 'assistant' && Array.isArray(m.thinkingBlocks) && m.thinkingBlocks.length) {
|
|
343
349
|
text += `\n${nativeBlocksEstimateText(m.thinkingBlocks)}`;
|
|
344
350
|
}
|
|
345
351
|
// Some provider adapters replay their native assistant representation
|
|
346
352
|
// instead of content/toolCalls. Project it through the media normalizer so
|
|
347
353
|
// text/tool metadata and opaque reasoning are counted without base64 image
|
|
348
354
|
// bytes dominating the estimate.
|
|
349
|
-
if (m.role === 'assistant' && Array.isArray(m.assistantBlocks) && m.assistantBlocks.length) {
|
|
355
|
+
if (!providerReplayItems && m.role === 'assistant' && Array.isArray(m.assistantBlocks) && m.assistantBlocks.length) {
|
|
350
356
|
text += `\n${nativeBlocksEstimateText(m.assistantBlocks)}`;
|
|
351
357
|
}
|
|
352
|
-
if (m.role === 'assistant' && Array.isArray(m.reasoningItems) && m.reasoningItems.length) {
|
|
358
|
+
if (!providerReplayItems && m.role === 'assistant' && Array.isArray(m.reasoningItems) && m.reasoningItems.length) {
|
|
353
359
|
text += `\n${nativeBlocksEstimateText(m.reasoningItems)}`;
|
|
354
360
|
}
|
|
355
361
|
// Provider-scoped replay metadata is never sent to a different provider,
|
|
@@ -357,7 +363,7 @@ function messageEstimateText(m) {
|
|
|
357
363
|
const geminiThoughtParts = m.role === 'assistant'
|
|
358
364
|
? m.providerMetadata?.gemini?.thoughtParts
|
|
359
365
|
: null;
|
|
360
|
-
if (Array.isArray(geminiThoughtParts) && geminiThoughtParts.length) {
|
|
366
|
+
if (!providerReplayItems && Array.isArray(geminiThoughtParts) && geminiThoughtParts.length) {
|
|
361
367
|
text += `\n${nativeBlocksEstimateText(geminiThoughtParts)}`;
|
|
362
368
|
}
|
|
363
369
|
if (m.role === 'tool' && m.toolCallId) text += `\n${m.toolCallId}`;
|
|
@@ -384,6 +390,7 @@ function messageImageDescriptors(m) {
|
|
|
384
390
|
return [
|
|
385
391
|
...contentImageDescriptors(m.content),
|
|
386
392
|
...(m.role === 'assistant' ? contentImageDescriptors(m.assistantBlocks) : []),
|
|
393
|
+
...(m.role === 'assistant' ? contentImageDescriptors(m.providerReplay?.items) : []),
|
|
387
394
|
];
|
|
388
395
|
}
|
|
389
396
|
function messageImageAllowance(m) {
|
|
@@ -441,6 +448,7 @@ function contextMessageFingerprint(message) {
|
|
|
441
448
|
assistantBlocks: contextValueFingerprint(null),
|
|
442
449
|
reasoningItems: contextValueFingerprint(null),
|
|
443
450
|
providerMetadata: contextValueFingerprint(null),
|
|
451
|
+
providerReplay: contextValueFingerprint(null),
|
|
444
452
|
toolCallId: null,
|
|
445
453
|
};
|
|
446
454
|
}
|
|
@@ -452,6 +460,7 @@ function contextMessageFingerprint(message) {
|
|
|
452
460
|
assistantBlocks: contextValueFingerprint(message.assistantBlocks),
|
|
453
461
|
reasoningItems: contextValueFingerprint(message.reasoningItems),
|
|
454
462
|
providerMetadata: contextValueFingerprint(message.providerMetadata),
|
|
463
|
+
providerReplay: contextValueFingerprint(message.providerReplay),
|
|
455
464
|
toolCallId: message?.toolCallId || null,
|
|
456
465
|
};
|
|
457
466
|
}
|
|
@@ -468,6 +477,7 @@ function sameContextMessageFingerprint(a, b) {
|
|
|
468
477
|
&& sameContextValueFingerprint(a.assistantBlocks, b.assistantBlocks)
|
|
469
478
|
&& sameContextValueFingerprint(a.reasoningItems, b.reasoningItems)
|
|
470
479
|
&& sameContextValueFingerprint(a.providerMetadata, b.providerMetadata)
|
|
480
|
+
&& sameContextValueFingerprint(a.providerReplay, b.providerReplay)
|
|
471
481
|
&& a.toolCallId === b.toolCallId;
|
|
472
482
|
}
|
|
473
483
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Recall-fasttrack compaction pipeline (digest injection).
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Reads the session transcript already persisted by the memory watcher, then
|
|
3
|
+
// injects a small newest-first digest + recall pointer into the
|
|
4
4
|
// compacted messages. The former full-dump path (dump_session_roots +
|
|
5
5
|
// synchronous cycle1 drain) was removed 2026-07: the drain ran memory-
|
|
6
6
|
// pipeline LLM chunking calls inside the compaction (11.9s of a measured
|
|
@@ -117,6 +117,10 @@ async function runReadOnlyIoWithDeadline(name, parentSignal, run) {
|
|
|
117
117
|
|
|
118
118
|
export { runReadOnlyIoWithDeadline as _runReadOnlyIoWithDeadlineForTest };
|
|
119
119
|
|
|
120
|
+
export function resolveToolCompletionSessionId({ callerSessionId } = {}) {
|
|
121
|
+
return String(callerSessionId || '').trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
120
124
|
let codeGraphRuntimePromise = null;
|
|
121
125
|
async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
|
|
122
126
|
codeGraphRuntimePromise ??= import('../../tools/code-graph.mjs');
|
|
@@ -170,7 +174,14 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
|
|
|
170
174
|
const toolOpts = scopedCacheOutcome
|
|
171
175
|
? { ...executeOpts, scopedCacheOutcome }
|
|
172
176
|
: executeOpts;
|
|
173
|
-
|
|
177
|
+
// A background tool belongs to the session that invoked it. Subagent
|
|
178
|
+
// sessions carry the top-level UI session in ownerSessionId, but routing a
|
|
179
|
+
// shell completion there leaks the child task into the lead transcript.
|
|
180
|
+
const notificationSessionId = resolveToolCompletionSessionId({
|
|
181
|
+
callerSessionId,
|
|
182
|
+
ownerSessionId: sessionRef?.ownerSessionId,
|
|
183
|
+
requestedNotificationSessionId: executeOpts.notifySessionId,
|
|
184
|
+
});
|
|
174
185
|
const notifyFn = typeof executeOpts.notifyFn === 'function'
|
|
175
186
|
? executeOpts.notifyFn
|
|
176
187
|
: (text, meta = {}) => {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { createHash, randomUUID } from 'crypto';
|
|
7
7
|
import { getProvider } from '../../providers/registry.mjs';
|
|
8
8
|
import { readStreamOutcome } from '../../providers/lib/stream-outcome.mjs';
|
|
9
|
+
import { cloneProviderReplay } from '../../providers/lib/provider-replay.mjs';
|
|
9
10
|
import { classifyError } from '../../providers/retry-classifier.mjs';
|
|
10
11
|
import { normalizeCompactType, DEFAULT_COMPACT_TYPE } from '../compact.mjs';
|
|
11
12
|
import { loadSession, saveSession, saveSessionAsync, saveSessionAsyncDeferred, readSessionLifecycleFromDisk } from '../store.mjs';
|
|
@@ -60,11 +61,9 @@ import {
|
|
|
60
61
|
} from './message-sanitize.mjs';
|
|
61
62
|
import { createTurnInterruptionTracker } from './turn-interruption.mjs';
|
|
62
63
|
import {
|
|
63
|
-
cancelPendingTurnCheckpoint,
|
|
64
64
|
clearTurnCheckpoint,
|
|
65
|
+
createTurnCheckpointRecorder,
|
|
65
66
|
recoverTurnCheckpoint,
|
|
66
|
-
turnMessagesForCheckpoint,
|
|
67
|
-
writeTurnCheckpoint,
|
|
68
67
|
} from './turn-checkpoint.mjs';
|
|
69
68
|
import { _getAgentLoop } from './runtime-loaders.mjs';
|
|
70
69
|
import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
|
|
@@ -344,29 +343,30 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
344
343
|
const _turnCheckpointToken = randomUUID();
|
|
345
344
|
const _turnCheckpointStartedAt = Date.now();
|
|
346
345
|
const _codexTurnId = _codexWireSessionId ? mintUuidV7(_turnCheckpointStartedAt) : null;
|
|
346
|
+
// Coalescing window for streaming-delta flushes. It is no longer a
|
|
347
|
+
// throttle on a full-snapshot rewrite: each flush now journals only the
|
|
348
|
+
// delta since the previous one, so the window just bounds syscalls.
|
|
347
349
|
const _TURN_CHECKPOINT_THROTTLE_MS = 150;
|
|
348
350
|
let _turnCheckpointTimer = null;
|
|
349
351
|
let _turnCheckpointStopped = false;
|
|
350
352
|
let _turnCheckpointLastAt = 0;
|
|
351
353
|
let _turnCheckpointWarned = false;
|
|
354
|
+
// The recorder writes this turn's header synchronously on its first
|
|
355
|
+
// flush (the crash-durability anchor for the prompt, landing before the
|
|
356
|
+
// provider runs) and appends bounded deltas afterwards.
|
|
357
|
+
const _turnCheckpointRecorder = createTurnCheckpointRecorder({
|
|
358
|
+
sessionId,
|
|
359
|
+
generation: askGeneration,
|
|
360
|
+
turnToken: _turnCheckpointToken,
|
|
361
|
+
startedAt: _turnCheckpointStartedAt,
|
|
362
|
+
});
|
|
352
363
|
const _flushTurnCheckpoint = () => {
|
|
353
364
|
if (_turnCheckpointStopped || !_turnOutgoing || !cancelledUserTurnContent) return false;
|
|
354
365
|
try {
|
|
355
|
-
const written =
|
|
356
|
-
sessionId,
|
|
357
|
-
generation: askGeneration,
|
|
358
|
-
turnToken: _turnCheckpointToken,
|
|
359
|
-
startedAt: _turnCheckpointStartedAt,
|
|
366
|
+
const written = _turnCheckpointRecorder.record({
|
|
360
367
|
currentUserContent: cancelledUserTurnContent,
|
|
361
|
-
|
|
362
|
-
interruption: _turnInterruption
|
|
363
|
-
}, {
|
|
364
|
-
// The turn's first checkpoint is the crash-durability
|
|
365
|
-
// anchor for the prompt and must land before the provider
|
|
366
|
-
// runs; every later streaming/tool-boundary flush moves to
|
|
367
|
-
// the async latest-wins lane so serialization+disk never
|
|
368
|
-
// stall the engine thread mid-stream.
|
|
369
|
-
sync: _turnCheckpointLastAt === 0,
|
|
368
|
+
turnOutgoing: _turnOutgoing,
|
|
369
|
+
interruption: _turnInterruption,
|
|
370
370
|
});
|
|
371
371
|
if (written) _turnCheckpointLastAt = Date.now();
|
|
372
372
|
return written;
|
|
@@ -398,7 +398,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
398
398
|
_turnCheckpointStopped = true;
|
|
399
399
|
if (_turnCheckpointTimer) clearTimeout(_turnCheckpointTimer);
|
|
400
400
|
_turnCheckpointTimer = null;
|
|
401
|
-
|
|
401
|
+
_turnCheckpointRecorder.stop();
|
|
402
402
|
};
|
|
403
403
|
let _interruptionSnapshot = null;
|
|
404
404
|
const _prepareCloseSnapshot = (abortReason) => {
|
|
@@ -406,6 +406,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
406
406
|
if (!activeSession) return null;
|
|
407
407
|
_stopTurnCheckpoint();
|
|
408
408
|
activeSession.liveTurnMessages = null;
|
|
409
|
+
// Interruption finalize below rewrites the transcript; the
|
|
410
|
+
// mid-turn provider prefix snapshot must not survive it.
|
|
411
|
+
delete activeSession._providerPrefixGuardState;
|
|
409
412
|
_turnInterruption.restoreTombstonedText();
|
|
410
413
|
const finalized = _turnInterruption.finalize({
|
|
411
414
|
turnOutgoing: _turnOutgoing || activeSession.messages,
|
|
@@ -848,6 +851,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
848
851
|
? result.historyContent
|
|
849
852
|
: (result.content || '');
|
|
850
853
|
const _terminalStop = result?.stopReason ?? result?.stop_reason ?? null;
|
|
854
|
+
const _providerReplay = cloneProviderReplay(result.providerReplay);
|
|
851
855
|
session.messages.push({
|
|
852
856
|
role: 'assistant',
|
|
853
857
|
// Keep content as-is in memory (model-visible). Image bytes,
|
|
@@ -855,6 +859,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
855
859
|
// time inside the session store (store.mjs _sessionForDisk).
|
|
856
860
|
content: persistedAssistantContent,
|
|
857
861
|
...(_assistantTranscriptMeta ? { meta: { transcript: _assistantTranscriptMeta } } : {}),
|
|
862
|
+
...(_providerReplay
|
|
863
|
+
? { providerReplay: _providerReplay }
|
|
864
|
+
: {}),
|
|
858
865
|
...(typeof result.reasoningContent === 'string' && result.reasoningContent
|
|
859
866
|
? { reasoningContent: result.reasoningContent }
|
|
860
867
|
: {}),
|
|
@@ -1088,6 +1095,12 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1088
1095
|
if (activeSession) {
|
|
1089
1096
|
activeSession.liveTurnMessages = null;
|
|
1090
1097
|
delete activeSession.activeTurnCheckpoint;
|
|
1098
|
+
// The provider prefix snapshot commits per successful send
|
|
1099
|
+
// INSIDE the turn, while the transcript unwinds to its
|
|
1100
|
+
// pre-turn/finalized shape below. A surviving mid-turn
|
|
1101
|
+
// snapshot flags every retry as history_shrink ("Session
|
|
1102
|
+
// state changed unexpectedly."), so it unwinds with the turn.
|
|
1103
|
+
delete activeSession._providerPrefixGuardState;
|
|
1091
1104
|
}
|
|
1092
1105
|
// Restore before ANY finalization path. In particular, cancellation
|
|
1093
1106
|
// can race the acknowledged non-streaming restart and surface as a
|
|
@@ -29,6 +29,7 @@ import { uncachedInputTokensForProvider } from './usage-metrics.mjs';
|
|
|
29
29
|
import { pruneOffloadSession } from '../tool-result-offload.mjs';
|
|
30
30
|
import { _getPendingMessagesForSession } from './pending-messages.mjs';
|
|
31
31
|
import { isSessionCompactionBlocked } from './runtime-liveness.mjs';
|
|
32
|
+
import { resetReadStateAfterCompaction } from '../read-dedup.mjs';
|
|
32
33
|
import {
|
|
33
34
|
compactTargetBudget as compactTargetBudgetForPolicy,
|
|
34
35
|
currentContextEstimateTokens,
|
|
@@ -449,6 +450,7 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
449
450
|
const unchangedReason = changed ? null : (force ? 'nothing to compact' : 'below threshold');
|
|
450
451
|
const now = Date.now();
|
|
451
452
|
session.messages = compacted;
|
|
453
|
+
if (changed) resetReadStateAfterCompaction(resolvedSessionId);
|
|
452
454
|
// Best-effort GC only: the 10-minute mtime gate plus this idle-only guard
|
|
453
455
|
// lets an in-flight turn's sidecars survive until a later compaction/close.
|
|
454
456
|
const pruneSessionId = resolvedSessionId;
|
|
@@ -81,6 +81,9 @@ function applyCompactFailurePersistToSession(activeSession, {
|
|
|
81
81
|
if (!messagesAdvanced) {
|
|
82
82
|
activeSession.messages = sanitized;
|
|
83
83
|
activeSession.providerState = undefined;
|
|
84
|
+
// Sanitized replacement can rewrite/shrink the transcript; the
|
|
85
|
+
// provider prefix snapshot must re-baseline with it.
|
|
86
|
+
delete activeSession._providerPrefixGuardState;
|
|
84
87
|
}
|
|
85
88
|
activeSession.updatedAt = Date.now();
|
|
86
89
|
activeSession.lastUsedAt = Date.now();
|
|
@@ -229,6 +229,9 @@ export async function clearSessionMessages(sessionId, options = {}) {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
session.messages = keep;
|
|
232
|
+
// Clear truncates the transcript wholesale; drop the provider prefix
|
|
233
|
+
// snapshot so the next send re-baselines instead of history_shrink.
|
|
234
|
+
delete session._providerPrefixGuardState;
|
|
232
235
|
resetSessionBp3Environment(session);
|
|
233
236
|
session.totalInputTokens = 0;
|
|
234
237
|
session.totalOutputTokens = 0;
|
|
@@ -340,8 +343,10 @@ export async function rewindSessionMessagesTo(sessionId, options = {}) {
|
|
|
340
343
|
if (start < 0) return null;
|
|
341
344
|
const removed = messages.length - start;
|
|
342
345
|
session.messages = messages.slice(0, start);
|
|
343
|
-
// The provider cache is keyed on the exact prefix we just truncated
|
|
346
|
+
// The provider cache is keyed on the exact prefix we just truncated;
|
|
347
|
+
// the prefix-guard snapshot describes it too and must go with it.
|
|
344
348
|
session.providerState = undefined;
|
|
349
|
+
delete session._providerPrefixGuardState;
|
|
345
350
|
session.updatedAt = Date.now();
|
|
346
351
|
session.lastUsedAt = Date.now();
|
|
347
352
|
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
@@ -316,9 +316,7 @@ export function createSession(opts) {
|
|
|
316
316
|
sessionCwdLine
|
|
317
317
|
? `- Cwd: ${sessionCwdLine} — the active Project root; relative paths and shell commands resolve here.`
|
|
318
318
|
: '',
|
|
319
|
-
|
|
320
|
-
? `- Shell: ${process.platform === 'win32' ? 'PowerShell' : 'Bash'}. Use ${process.platform === 'win32' ? 'PowerShell' : 'Bash'} syntax unless the user specifies otherwise.`
|
|
321
|
-
: '',
|
|
319
|
+
`- Shell: ${process.platform === 'win32' ? 'PowerShell' : 'Bash'}. Use ${process.platform === 'win32' ? 'PowerShell' : 'Bash'} syntax unless the user specifies otherwise.`,
|
|
322
320
|
// A startup inventory of PATH binaries used to sit here; see
|
|
323
321
|
// runtime-capabilities.mjs for why it cannot be stated truthfully
|
|
324
322
|
// before a command has run. Whether the cwd is inside a repository is
|