mixdog 0.9.38 → 0.9.40
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/package.json +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +176 -0
- package/scripts/explore-bench.mjs +38 -2
- package/scripts/explore-prompt-policy-test.mjs +152 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/steering-drain-buckets-test.mjs +316 -2
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/scripts/tui-transcript-perf-test.mjs +279 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +50 -60
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -45
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +67 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/buffered-appender.mjs +13 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/lifecycle-api.mjs +4 -0
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/runtime-core.mjs +36 -2
- package/src/session-runtime/session-turn-api.mjs +12 -0
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +53 -70
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +33 -1
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +10 -10
- package/src/tui/app/use-transcript-window.mjs +38 -56
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +866 -300
- package/src/tui/engine/agent-job-feed.mjs +216 -19
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/session-api.mjs +14 -2
- package/src/tui/engine/session-flow.mjs +22 -2
- package/src/tui/engine/tool-card-results.mjs +64 -37
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +172 -77
- package/src/tui/engine.mjs +199 -39
- package/src/tui/index.jsx +2 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- package/scripts/_jp2.mjs +0 -45614
|
@@ -26,6 +26,7 @@ let _localTracePath = null;
|
|
|
26
26
|
let _localTraceBuffer = [];
|
|
27
27
|
let _localTraceTimer = null;
|
|
28
28
|
let _localTraceFlushInFlight = false;
|
|
29
|
+
let _localTraceFlushPromise = null;
|
|
29
30
|
let _toolFailurePath = null;
|
|
30
31
|
let _toolFailureBuffer = [];
|
|
31
32
|
let _toolFailureTimer = null;
|
|
@@ -132,12 +133,10 @@ function _flushLocalTrace() {
|
|
|
132
133
|
clearTimeout(_localTraceTimer);
|
|
133
134
|
_localTraceTimer = null;
|
|
134
135
|
}
|
|
135
|
-
if (_localTraceBuffer.length === 0) return;
|
|
136
136
|
if (_localTraceFlushInFlight) {
|
|
137
|
-
|
|
138
|
-
_localTraceTimer.unref?.();
|
|
139
|
-
return;
|
|
137
|
+
return _localTraceFlushPromise;
|
|
140
138
|
}
|
|
139
|
+
if (_localTraceBuffer.length === 0) return null;
|
|
141
140
|
const path = _resolveLocalTracePath();
|
|
142
141
|
if (!path) return;
|
|
143
142
|
const chunk = _localTraceBuffer.join('');
|
|
@@ -157,17 +156,20 @@ function _flushLocalTrace() {
|
|
|
157
156
|
// mode only applies on file creation; existing files keep their mode.
|
|
158
157
|
// Windows ignores POSIX bits — ACL governs there.
|
|
159
158
|
_localTraceFlushInFlight = true;
|
|
160
|
-
appendFile(path, chunk, { encoding: 'utf8', mode: 0o600 })
|
|
159
|
+
const pending = appendFile(path, chunk, { encoding: 'utf8', mode: 0o600 })
|
|
161
160
|
.catch((err) => {
|
|
162
161
|
warnAgentOnce('agent-trace:local-spool', `[agent-trace] local spool failed (${err?.message})`);
|
|
163
162
|
})
|
|
164
163
|
.finally(() => {
|
|
165
164
|
_localTraceFlushInFlight = false;
|
|
165
|
+
_localTraceFlushPromise = null;
|
|
166
166
|
if (_localTraceBuffer.length > 0) {
|
|
167
167
|
_localTraceTimer = setTimeout(_flushLocalTrace, 0);
|
|
168
168
|
_localTraceTimer.unref?.();
|
|
169
169
|
}
|
|
170
170
|
});
|
|
171
|
+
_localTraceFlushPromise = pending;
|
|
172
|
+
return pending;
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
function _flushLocalTraceSync() {
|
|
@@ -294,7 +296,20 @@ function _scheduleFlush(immediate = false) {
|
|
|
294
296
|
}
|
|
295
297
|
}
|
|
296
298
|
|
|
299
|
+
async function _drainLocalTrace() {
|
|
300
|
+
if (!_resolveLocalTracePath()) return;
|
|
301
|
+
if (_localTraceTimer) {
|
|
302
|
+
clearTimeout(_localTraceTimer);
|
|
303
|
+
_localTraceTimer = null;
|
|
304
|
+
}
|
|
305
|
+
while (_localTraceBuffer.length > 0 || _localTraceFlushInFlight) {
|
|
306
|
+
const pending = _flushLocalTrace();
|
|
307
|
+
if (pending) await pending;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
297
311
|
async function drainAgentTrace() {
|
|
312
|
+
await _drainLocalTrace();
|
|
298
313
|
if (!_resolveServiceUrl()) return;
|
|
299
314
|
if (_flushTimer) { clearTimeout(_flushTimer); _flushTimer = null; }
|
|
300
315
|
for (let i = 0; i < 10 && _buffer.length > 0; i++) {
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
import os from 'node:os';
|
|
17
17
|
|
|
18
18
|
// Offline fallback only; live value refreshes from npm (24h TTL, in-process).
|
|
19
|
-
|
|
19
|
+
// The backend gates model exposure AND per-request model access on the client
|
|
20
|
+
// version (gpt-5.6-* require >= 0.144.0 per codex models-manager/models.json,
|
|
21
|
+
// verified 2026-07-09), so keep this at the current release when bumping.
|
|
22
|
+
export const CODEX_CLIENT_VERSION_FLOOR = '0.144.1';
|
|
20
23
|
const VERSION_TTL_MS = 24 * 60 * 60_000;
|
|
21
24
|
let _cache = { value: null, fetchedAt: 0 };
|
|
22
25
|
let _refreshInFlight = null;
|
|
@@ -52,6 +55,23 @@ export function codexClientVersionSync() {
|
|
|
52
55
|
return _cache.value || CODEX_CLIENT_VERSION_FLOOR;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Awaitable warmup for cold-start paths: resolves the live npm version (or
|
|
60
|
+
* floor on failure) and fills the shared cache so codexClientVersionSync()
|
|
61
|
+
* and codexVersionHeader() stop reporting the floor. Never rejects; dedupes
|
|
62
|
+
* with the in-flight background refresh. First turns await this so the
|
|
63
|
+
* backend's minimal_client_version gate never sees a stale floor.
|
|
64
|
+
*/
|
|
65
|
+
export function warmCodexClientVersion() {
|
|
66
|
+
if (_cache.value && Date.now() - _cache.fetchedAt < VERSION_TTL_MS) {
|
|
67
|
+
return Promise.resolve(_cache.value);
|
|
68
|
+
}
|
|
69
|
+
if (!_refreshInFlight) {
|
|
70
|
+
_refreshInFlight = _refresh().finally(() => { _refreshInFlight = null; });
|
|
71
|
+
}
|
|
72
|
+
return _refreshInFlight;
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
function _osType() {
|
|
56
76
|
// codex os_info reports "Windows"/"Mac OS"/"Linux"; node os.type() gives
|
|
57
77
|
// Windows_NT/Darwin/Linux. Map to codex's vocabulary.
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
_shouldUseOpenAIHttpFallback,
|
|
60
60
|
} from './openai-oauth-http-sse.mjs';
|
|
61
61
|
import { createOpenAIOAuthLogin } from './openai-oauth-login.mjs';
|
|
62
|
+
import { warmCodexClientVersion } from './codex-client-meta.mjs';
|
|
62
63
|
import {
|
|
63
64
|
_displayCodexModel,
|
|
64
65
|
_codexFamily,
|
|
@@ -77,42 +78,13 @@ const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
|
77
78
|
export const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
78
79
|
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
79
80
|
export const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// gpt-5.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// Offline fallback only — _resolveCodexClientVersion() fetches the live
|
|
88
|
-
// @openai/codex latest from npm first. Bumped to the current release
|
|
89
|
-
// (0.142.5, verified 2026-07-03) so the offline path stays close to what the
|
|
90
|
-
// backend expects for client-version gating.
|
|
91
|
-
const CODEX_CLIENT_VERSION_FLOOR = '0.142.5';
|
|
92
|
-
const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
93
|
-
let _codexVersionCache = { value: null, fetchedAt: 0 };
|
|
94
|
-
|
|
95
|
-
async function _resolveCodexClientVersion() {
|
|
96
|
-
const now = Date.now();
|
|
97
|
-
if (_codexVersionCache.value && now - _codexVersionCache.fetchedAt < CODEX_VERSION_CACHE_TTL_MS) {
|
|
98
|
-
return _codexVersionCache.value;
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
const res = await fetch('https://registry.npmjs.org/@openai/codex/latest', {
|
|
102
|
-
signal: AbortSignal.timeout(5_000),
|
|
103
|
-
});
|
|
104
|
-
if (res.ok) {
|
|
105
|
-
const j = await res.json();
|
|
106
|
-
const v = String(j?.version || '').trim();
|
|
107
|
-
if (/^\d+\.\d+\.\d+/.test(v)) {
|
|
108
|
-
_codexVersionCache = { value: v, fetchedAt: now };
|
|
109
|
-
return v;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch { /* network down / npm rejects — use floor */ }
|
|
113
|
-
_codexVersionCache = { value: CODEX_CLIENT_VERSION_FLOOR, fetchedAt: now };
|
|
114
|
-
return CODEX_CLIENT_VERSION_FLOOR;
|
|
115
|
-
}
|
|
81
|
+
// Client version for the models endpoint query and the `version`/User-Agent
|
|
82
|
+
// request headers — the OAuth backend rejects requests without it, gates new
|
|
83
|
+
// model exposures on it (gpt-5.6-* require >= 0.144.0), and rejects turns on
|
|
84
|
+
// gated models when the reported version is below the model's
|
|
85
|
+
// minimal_client_version. Resolution is unified in codex-client-meta.mjs
|
|
86
|
+
// (live npm @openai/codex latest, 24h in-process cache, offline floor) so the
|
|
87
|
+
// catalog query and the transport headers can never disagree.
|
|
116
88
|
const CODEX_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
117
89
|
const CODEX_MODEL_CACHE_SCHEMA_VERSION = 3;
|
|
118
90
|
const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
@@ -784,7 +756,14 @@ export class OpenAIOAuthProvider {
|
|
|
784
756
|
? Promise.resolve(opts._prebuiltBody)
|
|
785
757
|
: Promise.resolve().then(() => buildRequestBody(messages, useModel, tools, bodyOpts));
|
|
786
758
|
const _authP = this.ensureAuth();
|
|
759
|
+
// Cold-start guard: the WS/SSE transports read the client version via
|
|
760
|
+
// the SYNC accessor for the `version` header + User-Agent. Await the
|
|
761
|
+
// shared resolver (parallel with auth; never rejects; no-op once
|
|
762
|
+
// cached) so the first turn after boot doesn't report the offline
|
|
763
|
+
// floor and trip the backend's minimal_client_version gate.
|
|
764
|
+
const _verP = warmCodexClientVersion();
|
|
787
765
|
let auth = await _authP;
|
|
766
|
+
await _verP;
|
|
788
767
|
const body = await _bodyP;
|
|
789
768
|
// poolKey != cacheKey by design (see openai-oauth-ws.mjs header note).
|
|
790
769
|
// poolKey is per-session so parallel reviewer/worker callers each get
|
|
@@ -1038,7 +1017,7 @@ export class OpenAIOAuthProvider {
|
|
|
1038
1017
|
}
|
|
1039
1018
|
try {
|
|
1040
1019
|
const auth = await this.ensureAuth();
|
|
1041
|
-
const clientVersion = await
|
|
1020
|
+
const clientVersion = await warmCodexClientVersion();
|
|
1042
1021
|
const url = `https://chatgpt.com/backend-api/codex/models?client_version=${clientVersion}`;
|
|
1043
1022
|
const res = await fetch(url, {
|
|
1044
1023
|
signal: AbortSignal.timeout(10_000),
|
|
@@ -1076,7 +1055,7 @@ export class OpenAIOAuthProvider {
|
|
|
1076
1055
|
_codexRefreshInFlight = (async () => {
|
|
1077
1056
|
try {
|
|
1078
1057
|
const auth = await this.ensureAuth();
|
|
1079
|
-
const clientVersion = await
|
|
1058
|
+
const clientVersion = await warmCodexClientVersion();
|
|
1080
1059
|
const url = `https://chatgpt.com/backend-api/codex/models?client_version=${clientVersion}`;
|
|
1081
1060
|
const res = await fetch(url, {
|
|
1082
1061
|
signal: AbortSignal.timeout(10_000),
|
|
@@ -305,11 +305,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
305
305
|
// cut off early. Other runaway protection is behavior-based (steering
|
|
306
306
|
// ladder hints, REPEAT_FAIL_LIMIT), never a lower iteration count.
|
|
307
307
|
let _iterWarnStage = 0;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
// Tiny-cap loops (e.g. explorer cap=3) can't afford staged 50/75/90%
|
|
309
|
+
// steers — the 50% stage lands on iteration 1 in every session, spamming
|
|
310
|
+
// the normal batch→answer path. For caps < 10 emit ONE wrap-up warning at
|
|
311
|
+
// the penultimate iteration instead; caps >= 10 keep staged behavior.
|
|
312
|
+
const _singleWarn = maxLoopIterations < 10;
|
|
313
|
+
const _iterWarnAt = _singleWarn
|
|
314
|
+
? [Math.max(1, maxLoopIterations - 1)]
|
|
315
|
+
: [
|
|
316
|
+
Math.floor(maxLoopIterations * 0.5),
|
|
317
|
+
Math.floor(maxLoopIterations * 0.75),
|
|
318
|
+
Math.floor(maxLoopIterations * 0.9),
|
|
319
|
+
];
|
|
313
320
|
while (true) {
|
|
314
321
|
throwIfAborted();
|
|
315
322
|
if (iterations >= maxLoopIterations) {
|
|
@@ -341,9 +348,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
341
348
|
if (_iterWarnStage < _iterWarnAt.length && iterations >= _iterWarnAt[_iterWarnStage]) {
|
|
342
349
|
_iterWarnStage += 1;
|
|
343
350
|
const warnAt = _iterWarnAt[_iterWarnStage - 1];
|
|
344
|
-
const stageMsg =
|
|
345
|
-
? `Iteration budget
|
|
346
|
-
:
|
|
351
|
+
const stageMsg = _singleWarn
|
|
352
|
+
? `Iteration budget nearly spent: ${warnAt} of ${maxLoopIterations} iterations used — answer NOW with the best anchors you already hold.`
|
|
353
|
+
: _iterWarnStage === 1
|
|
354
|
+
? `Iteration budget notice: ${warnAt} of ${maxLoopIterations} iterations used. Converge on a conclusion: prefer finishing the current objective over opening new exploration.`
|
|
355
|
+
: `Iteration budget warning (stage ${_iterWarnStage}): ${warnAt} of ${maxLoopIterations} iterations used — the loop hard-stops at ${maxLoopIterations}. Wrap up now: summarize progress, state what remains, and finish with your best current result.`;
|
|
347
356
|
messages.push({ role: 'user', content: `<system-reminder>\n${stageMsg}\n</system-reminder>`, meta: 'hook' });
|
|
348
357
|
process.stderr.write(`[loop] iteration warning stage ${_iterWarnStage} at ${iterations} (sess=${sessionId || 'unknown'}); continuing with steer.\n`);
|
|
349
358
|
try {
|
|
@@ -99,6 +99,212 @@ export function estimateMessagesTokens(messages) {
|
|
|
99
99
|
return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Context status is polled while the agent loop mutates and replaces message
|
|
103
|
+
// arrays. Keep the accumulated summary on that array, but cheaply validate
|
|
104
|
+
// every entry before reusing its contribution. The fingerprint deliberately
|
|
105
|
+
// avoids serializing content/blocks on the warm path; it compares the
|
|
106
|
+
// references of every estimator-visible string instead.
|
|
107
|
+
// Producer invariant: compaction copies message/call objects, transcript repair
|
|
108
|
+
// replaces array entries, stored-tool-args replaces `arguments`, and MCP reload
|
|
109
|
+
// replaces tool descriptors; settled nested non-string payloads are not mutated
|
|
110
|
+
// in place without replacing their containing reference.
|
|
111
|
+
const contextMessageMemo = new WeakMap();
|
|
112
|
+
const contextTranscriptMemo = new WeakMap();
|
|
113
|
+
|
|
114
|
+
function contextValueFingerprint(value) {
|
|
115
|
+
if (typeof value === 'string') return { value, entries: null };
|
|
116
|
+
if (Array.isArray(value)) return {
|
|
117
|
+
value,
|
|
118
|
+
entries: value.map(contextBlockFingerprint),
|
|
119
|
+
};
|
|
120
|
+
return value && typeof value === 'object'
|
|
121
|
+
? { value, entries: [contextBlockFingerprint(value)] }
|
|
122
|
+
: { value, entries: null };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function contextBlockFingerprint(block) {
|
|
126
|
+
if (!block || typeof block !== 'object') return { value: block };
|
|
127
|
+
const fn = block.function;
|
|
128
|
+
return {
|
|
129
|
+
value: block,
|
|
130
|
+
text: typeof block.text === 'string' ? block.text : null,
|
|
131
|
+
content: typeof block.content === 'string' ? block.content : null,
|
|
132
|
+
args: typeof block.args === 'string' ? block.args : null,
|
|
133
|
+
arguments: block.arguments,
|
|
134
|
+
input: typeof block.input === 'string' ? block.input : null,
|
|
135
|
+
function: fn,
|
|
136
|
+
functionArguments: typeof fn?.arguments === 'string' ? fn.arguments : null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function contextMessageFingerprint(message) {
|
|
141
|
+
if (!message || typeof message !== 'object') {
|
|
142
|
+
return {
|
|
143
|
+
role: undefined,
|
|
144
|
+
content: contextValueFingerprint(''),
|
|
145
|
+
toolCalls: contextValueFingerprint(null),
|
|
146
|
+
thinkingBlocks: contextValueFingerprint(null),
|
|
147
|
+
assistantBlocks: contextValueFingerprint(null),
|
|
148
|
+
toolCallId: null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
role: message.role,
|
|
153
|
+
content: contextValueFingerprint(message.content),
|
|
154
|
+
toolCalls: contextValueFingerprint(message.toolCalls),
|
|
155
|
+
thinkingBlocks: contextValueFingerprint(message.thinkingBlocks),
|
|
156
|
+
assistantBlocks: contextValueFingerprint(message.assistantBlocks),
|
|
157
|
+
toolCallId: message?.toolCallId || null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sameContextValueFingerprint(a, b) {
|
|
162
|
+
if (!a || !b || a.value !== b.value) return false;
|
|
163
|
+
if (a.entries === null || b.entries === null) return a.entries === b.entries;
|
|
164
|
+
if (a.entries.length !== b.entries.length) return false;
|
|
165
|
+
for (let index = 0; index < a.entries.length; index += 1) {
|
|
166
|
+
if (!sameContextBlockFingerprint(a.entries[index], b.entries[index])) return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sameContextBlockFingerprint(a, b) {
|
|
172
|
+
return !!a && !!b
|
|
173
|
+
&& a.value === b.value
|
|
174
|
+
&& a.text === b.text
|
|
175
|
+
&& a.content === b.content
|
|
176
|
+
&& a.args === b.args
|
|
177
|
+
&& Object.is(a.arguments, b.arguments)
|
|
178
|
+
&& a.input === b.input
|
|
179
|
+
&& a.function === b.function
|
|
180
|
+
&& a.functionArguments === b.functionArguments;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function sameContextMessageFingerprint(a, b) {
|
|
184
|
+
return !!a && a.role === b.role
|
|
185
|
+
&& sameContextValueFingerprint(a.content, b.content)
|
|
186
|
+
&& sameContextValueFingerprint(a.toolCalls, b.toolCalls)
|
|
187
|
+
&& sameContextValueFingerprint(a.thinkingBlocks, b.thinkingBlocks)
|
|
188
|
+
&& sameContextValueFingerprint(a.assistantBlocks, b.assistantBlocks)
|
|
189
|
+
&& a.toolCallId === b.toolCallId;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function contextMessageContribution(message) {
|
|
193
|
+
const fingerprint = contextMessageFingerprint(message);
|
|
194
|
+
if (message && typeof message === 'object') {
|
|
195
|
+
const cached = contextMessageMemo.get(message);
|
|
196
|
+
if (cached && sameContextMessageFingerprint(cached.fingerprint, fingerprint)) return cached.contribution;
|
|
197
|
+
}
|
|
198
|
+
const role = ['system', 'user', 'assistant', 'tool'].includes(fingerprint.role) ? fingerprint.role : 'other';
|
|
199
|
+
const text = messageEstimateText(message);
|
|
200
|
+
const tokens = estimateTokens(text) + 4;
|
|
201
|
+
const contribution = {
|
|
202
|
+
role,
|
|
203
|
+
tokens,
|
|
204
|
+
reminderBuckets: null,
|
|
205
|
+
toolCallCount: 0,
|
|
206
|
+
toolCallTokens: 0,
|
|
207
|
+
toolResultCount: role === 'tool' ? 1 : 0,
|
|
208
|
+
toolResultTokens: role === 'tool' ? tokens : 0,
|
|
209
|
+
};
|
|
210
|
+
if (role === 'user' && String(text || '').trim().startsWith('<system-reminder>')) {
|
|
211
|
+
const buckets = { tokens: contribution.tokens, otherTokens: contribution.tokens };
|
|
212
|
+
let sectionTokens = 0;
|
|
213
|
+
for (const section of splitMarkdownSections(stripSystemReminder(text))) {
|
|
214
|
+
const bucket = reminderSectionBucket(section);
|
|
215
|
+
const sectionTokenCount = estimateTokens(section);
|
|
216
|
+
buckets[bucket] = (buckets[bucket] || 0) + sectionTokenCount;
|
|
217
|
+
sectionTokens += sectionTokenCount;
|
|
218
|
+
}
|
|
219
|
+
buckets.otherTokens = Math.max(0, contribution.tokens - sectionTokens);
|
|
220
|
+
contribution.reminderBuckets = buckets;
|
|
221
|
+
}
|
|
222
|
+
if (fingerprint.role === 'assistant' && Array.isArray(message?.toolCalls) && message.toolCalls.length) {
|
|
223
|
+
contribution.toolCallCount = message.toolCalls.length;
|
|
224
|
+
try { contribution.toolCallTokens = estimateTokens(JSON.stringify(message.toolCalls)); }
|
|
225
|
+
catch { contribution.toolCallTokens = estimateTokens(`[${message.toolCalls.length} tool calls]`); }
|
|
226
|
+
}
|
|
227
|
+
if (message && typeof message === 'object') {
|
|
228
|
+
contextMessageMemo.set(message, { fingerprint, contribution });
|
|
229
|
+
}
|
|
230
|
+
return contribution;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function emptyContextSummaryState() {
|
|
234
|
+
return {
|
|
235
|
+
rows: {
|
|
236
|
+
system: { count: 0, tokens: 0 },
|
|
237
|
+
user: { count: 0, tokens: 0 },
|
|
238
|
+
assistant: { count: 0, tokens: 0 },
|
|
239
|
+
tool: { count: 0, tokens: 0 },
|
|
240
|
+
other: { count: 0, tokens: 0 },
|
|
241
|
+
},
|
|
242
|
+
semantic: {
|
|
243
|
+
system: { count: 0, tokens: 0 },
|
|
244
|
+
chat: { count: 0, tokens: 0 },
|
|
245
|
+
assistant: { count: 0, tokens: 0 },
|
|
246
|
+
toolResults: { count: 0, tokens: 0 },
|
|
247
|
+
reminders: { count: 0, tokens: 0, otherTokens: 0 },
|
|
248
|
+
workflow: { tokens: 0 },
|
|
249
|
+
memory: { tokens: 0 },
|
|
250
|
+
workspace: { tokens: 0 },
|
|
251
|
+
environment: { tokens: 0 },
|
|
252
|
+
other: { tokens: 0 },
|
|
253
|
+
},
|
|
254
|
+
estimatedTokens: 0,
|
|
255
|
+
toolCallCount: 0,
|
|
256
|
+
toolCallTokens: 0,
|
|
257
|
+
toolResultCount: 0,
|
|
258
|
+
toolResultTokens: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function applyContextMessageContribution(state, contribution, direction) {
|
|
263
|
+
const { role, tokens } = contribution;
|
|
264
|
+
state.estimatedTokens += direction * tokens;
|
|
265
|
+
state.rows[role].count += direction;
|
|
266
|
+
state.rows[role].tokens += direction * tokens;
|
|
267
|
+
if (role === 'system') {
|
|
268
|
+
state.semantic.system.count += direction;
|
|
269
|
+
state.semantic.system.tokens += direction * tokens;
|
|
270
|
+
} else if (role === 'user') {
|
|
271
|
+
if (contribution.reminderBuckets) {
|
|
272
|
+
state.semantic.reminders.count += direction;
|
|
273
|
+
state.semantic.reminders.tokens += direction * contribution.reminderBuckets.tokens;
|
|
274
|
+
state.semantic.reminders.otherTokens += direction * contribution.reminderBuckets.otherTokens;
|
|
275
|
+
for (const bucket of ['workflow', 'memory', 'workspace', 'environment', 'other']) {
|
|
276
|
+
state.semantic[bucket].tokens += direction * (contribution.reminderBuckets[bucket] || 0);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
state.semantic.chat.count += direction;
|
|
280
|
+
state.semantic.chat.tokens += direction * tokens;
|
|
281
|
+
}
|
|
282
|
+
} else if (role === 'assistant') {
|
|
283
|
+
state.semantic.assistant.count += direction;
|
|
284
|
+
state.semantic.assistant.tokens += direction * tokens;
|
|
285
|
+
} else if (role === 'tool') {
|
|
286
|
+
state.semantic.toolResults.count += direction;
|
|
287
|
+
state.semantic.toolResults.tokens += direction * tokens;
|
|
288
|
+
}
|
|
289
|
+
state.toolCallCount += direction * contribution.toolCallCount;
|
|
290
|
+
state.toolCallTokens += direction * contribution.toolCallTokens;
|
|
291
|
+
state.toolResultCount += direction * contribution.toolResultCount;
|
|
292
|
+
state.toolResultTokens += direction * contribution.toolResultTokens;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function contextSummaryResult(state, count) {
|
|
296
|
+
return {
|
|
297
|
+
count,
|
|
298
|
+
estimatedTokens: state.estimatedTokens,
|
|
299
|
+
roles: Object.fromEntries(Object.entries(state.rows).map(([role, row]) => [role, { ...row }])),
|
|
300
|
+
semantic: Object.fromEntries(Object.entries(state.semantic).map(([name, row]) => [name, { ...row }])),
|
|
301
|
+
toolCallCount: state.toolCallCount,
|
|
302
|
+
toolCallTokens: state.toolCallTokens,
|
|
303
|
+
toolResultCount: state.toolResultCount,
|
|
304
|
+
toolResultTokens: state.toolResultTokens,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
102
308
|
export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
|
|
103
309
|
export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
|
|
104
310
|
export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
|
|
@@ -301,81 +507,23 @@ function reminderSectionBucket(section) {
|
|
|
301
507
|
}
|
|
302
508
|
|
|
303
509
|
export function summarizeContextMessages(messages) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
other: { count: 0, tokens: 0 },
|
|
310
|
-
};
|
|
311
|
-
const semantic = {
|
|
312
|
-
system: { count: 0, tokens: 0 },
|
|
313
|
-
chat: { count: 0, tokens: 0 },
|
|
314
|
-
assistant: { count: 0, tokens: 0 },
|
|
315
|
-
toolResults: { count: 0, tokens: 0 },
|
|
316
|
-
reminders: { count: 0, tokens: 0, otherTokens: 0 },
|
|
317
|
-
workflow: { tokens: 0 },
|
|
318
|
-
memory: { tokens: 0 },
|
|
319
|
-
workspace: { tokens: 0 },
|
|
320
|
-
environment: { tokens: 0 },
|
|
321
|
-
other: { tokens: 0 },
|
|
322
|
-
};
|
|
323
|
-
let toolCallCount = 0;
|
|
324
|
-
let toolCallTokens = 0;
|
|
325
|
-
let toolResultCount = 0;
|
|
326
|
-
let toolResultTokens = 0;
|
|
327
|
-
for (const message of messages || []) {
|
|
328
|
-
const role = rows[message?.role] ? message.role : 'other';
|
|
329
|
-
const text = messageEstimateText(message);
|
|
330
|
-
const tokens = estimateMessageTokens(message);
|
|
331
|
-
rows[role].count += 1;
|
|
332
|
-
rows[role].tokens += tokens;
|
|
333
|
-
if (role === 'system') {
|
|
334
|
-
semantic.system.count += 1;
|
|
335
|
-
semantic.system.tokens += tokens;
|
|
336
|
-
} else if (role === 'user') {
|
|
337
|
-
if (String(text || '').trim().startsWith('<system-reminder>')) {
|
|
338
|
-
semantic.reminders.count += 1;
|
|
339
|
-
semantic.reminders.tokens += tokens;
|
|
340
|
-
let sectionTokens = 0;
|
|
341
|
-
for (const section of splitMarkdownSections(stripSystemReminder(text))) {
|
|
342
|
-
const bucket = reminderSectionBucket(section);
|
|
343
|
-
const sectionTokenCount = estimateTokens(section);
|
|
344
|
-
semantic[bucket].tokens += sectionTokenCount;
|
|
345
|
-
sectionTokens += sectionTokenCount;
|
|
346
|
-
}
|
|
347
|
-
semantic.reminders.otherTokens += Math.max(0, tokens - sectionTokens);
|
|
348
|
-
} else {
|
|
349
|
-
semantic.chat.count += 1;
|
|
350
|
-
semantic.chat.tokens += tokens;
|
|
351
|
-
}
|
|
352
|
-
} else if (role === 'assistant') {
|
|
353
|
-
semantic.assistant.count += 1;
|
|
354
|
-
semantic.assistant.tokens += tokens;
|
|
355
|
-
} else if (role === 'tool') {
|
|
356
|
-
semantic.toolResults.count += 1;
|
|
357
|
-
semantic.toolResults.tokens += tokens;
|
|
358
|
-
}
|
|
359
|
-
if (message?.role === 'assistant' && Array.isArray(message.toolCalls) && message.toolCalls.length) {
|
|
360
|
-
toolCallCount += message.toolCalls.length;
|
|
361
|
-
try { toolCallTokens += estimateTokens(JSON.stringify(message.toolCalls)); }
|
|
362
|
-
catch { toolCallTokens += estimateTokens(`[${message.toolCalls.length} tool calls]`); }
|
|
363
|
-
}
|
|
364
|
-
if (message?.role === 'tool') {
|
|
365
|
-
toolResultCount += 1;
|
|
366
|
-
toolResultTokens += tokens;
|
|
367
|
-
}
|
|
510
|
+
if (!Array.isArray(messages)) return contextSummaryResult(emptyContextSummaryState(), 0);
|
|
511
|
+
let cached = contextTranscriptMemo.get(messages);
|
|
512
|
+
if (!cached || messages.length < cached.count) {
|
|
513
|
+
cached = { count: 0, contributions: [], state: emptyContextSummaryState() };
|
|
514
|
+
contextTranscriptMemo.set(messages, cached);
|
|
368
515
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
516
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
517
|
+
const previous = cached.contributions[index];
|
|
518
|
+
const contribution = contextMessageContribution(messages[index]);
|
|
519
|
+
if (previous === contribution) continue;
|
|
520
|
+
if (previous) applyContextMessageContribution(cached.state, previous, -1);
|
|
521
|
+
cached.contributions[index] = contribution;
|
|
522
|
+
applyContextMessageContribution(cached.state, contribution, 1);
|
|
523
|
+
}
|
|
524
|
+
cached.contributions.length = messages.length;
|
|
525
|
+
cached.count = messages.length;
|
|
526
|
+
return contextSummaryResult(cached.state, messages.length);
|
|
379
527
|
}
|
|
380
528
|
|
|
381
529
|
// Per-request overhead the provider injects that never appears in the
|
|
@@ -383,6 +531,39 @@ export function summarizeContextMessages(messages) {
|
|
|
383
531
|
// provider wraps around the request. The chars/4 message estimate misses all
|
|
384
532
|
// of it, so a "fits" verdict computed from messages alone is optimistic.
|
|
385
533
|
const REQUEST_OVERHEAD_TOKENS = 512;
|
|
534
|
+
const toolSchemaTokenMemo = new WeakMap();
|
|
535
|
+
const requestReserveTokenMemo = new WeakMap();
|
|
536
|
+
|
|
537
|
+
function sameToolArrayEntries(cached, tools) {
|
|
538
|
+
if (!cached || cached.entries.length !== tools.length) return false;
|
|
539
|
+
for (let index = 0; index < tools.length; index += 1) {
|
|
540
|
+
const entry = cached.entries[index];
|
|
541
|
+
const tool = tools[index];
|
|
542
|
+
if (entry.tool !== tool
|
|
543
|
+
|| entry.name !== tool?.name
|
|
544
|
+
|| entry.description !== tool?.description
|
|
545
|
+
|| entry.inputSchema !== tool?.inputSchema
|
|
546
|
+
|| entry.input_schema !== tool?.input_schema
|
|
547
|
+
|| entry.parameters !== tool?.parameters
|
|
548
|
+
|| entry.schema !== tool?.schema) return false;
|
|
549
|
+
}
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function cacheToolArrayValue(tools, value) {
|
|
554
|
+
return {
|
|
555
|
+
entries: tools.map((tool) => ({
|
|
556
|
+
tool,
|
|
557
|
+
name: tool?.name,
|
|
558
|
+
description: tool?.description,
|
|
559
|
+
inputSchema: tool?.inputSchema,
|
|
560
|
+
input_schema: tool?.input_schema,
|
|
561
|
+
parameters: tool?.parameters,
|
|
562
|
+
schema: tool?.schema,
|
|
563
|
+
})),
|
|
564
|
+
value,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
386
567
|
|
|
387
568
|
/**
|
|
388
569
|
* Estimate the token cost of the tool/function schemas a provider appends to
|
|
@@ -394,10 +575,14 @@ const REQUEST_OVERHEAD_TOKENS = 512;
|
|
|
394
575
|
*/
|
|
395
576
|
export function estimateToolSchemaTokens(tools) {
|
|
396
577
|
if (!Array.isArray(tools) || tools.length === 0) return 0;
|
|
578
|
+
const cached = toolSchemaTokenMemo.get(tools);
|
|
579
|
+
if (sameToolArrayEntries(cached, tools)) return cached.value;
|
|
397
580
|
let text = '';
|
|
398
581
|
try { text = JSON.stringify(tools); }
|
|
399
582
|
catch { text = tools.map(t => String(t?.name ?? '')).join(''); }
|
|
400
|
-
|
|
583
|
+
const tokens = estimateTokens(text);
|
|
584
|
+
toolSchemaTokenMemo.set(tools, cacheToolArrayValue(tools, tokens));
|
|
585
|
+
return tokens;
|
|
401
586
|
}
|
|
402
587
|
|
|
403
588
|
/**
|
|
@@ -407,7 +592,12 @@ export function estimateToolSchemaTokens(tools) {
|
|
|
407
592
|
* request-side bytes the message estimate cannot see.
|
|
408
593
|
*/
|
|
409
594
|
export function estimateRequestReserveTokens(tools) {
|
|
410
|
-
return estimateToolSchemaTokens(tools) + REQUEST_OVERHEAD_TOKENS;
|
|
595
|
+
if (!Array.isArray(tools)) return estimateToolSchemaTokens(tools) + REQUEST_OVERHEAD_TOKENS;
|
|
596
|
+
const cached = requestReserveTokenMemo.get(tools);
|
|
597
|
+
if (sameToolArrayEntries(cached, tools)) return cached.value;
|
|
598
|
+
const reserve = estimateToolSchemaTokens(tools) + REQUEST_OVERHEAD_TOKENS;
|
|
599
|
+
requestReserveTokenMemo.set(tools, cacheToolArrayValue(tools, reserve));
|
|
600
|
+
return reserve;
|
|
411
601
|
}
|
|
412
602
|
|
|
413
603
|
/**
|
|
@@ -418,13 +608,15 @@ export function estimateRequestReserveTokens(tools) {
|
|
|
418
608
|
*
|
|
419
609
|
* @param {unknown[]} messages
|
|
420
610
|
* @param {unknown[]|number} toolsOrReserve tool list or precomputed reserve tokens
|
|
421
|
-
* @param {{ messageCount?: number }} [opts]
|
|
611
|
+
* @param {{ messageCount?: number, estimatedMessageTokens?: number }} [opts]
|
|
422
612
|
*/
|
|
423
613
|
export function estimateTranscriptContextUsage(messages, toolsOrReserve, opts = {}) {
|
|
424
614
|
const list = Array.isArray(messages) ? messages : [];
|
|
425
615
|
const count = Number.isFinite(Number(opts.messageCount)) ? Number(opts.messageCount) : list.length;
|
|
426
616
|
if (count <= 0 || list.length === 0) return 0;
|
|
427
|
-
const messageTokens =
|
|
617
|
+
const messageTokens = Number.isFinite(Number(opts.estimatedMessageTokens))
|
|
618
|
+
? Number(opts.estimatedMessageTokens)
|
|
619
|
+
: summarizeContextMessages(list).estimatedTokens;
|
|
428
620
|
const reserve = typeof toolsOrReserve === 'number' && Number.isFinite(toolsOrReserve)
|
|
429
621
|
? Math.max(0, toolsOrReserve)
|
|
430
622
|
: estimateRequestReserveTokens(toolsOrReserve);
|
|
@@ -15,7 +15,8 @@ import { isAgentOwner } from '../../agent-owner.mjs';
|
|
|
15
15
|
|
|
16
16
|
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
17
17
|
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
18
|
-
// stream.
|
|
18
|
+
// stream. MCP follows that annotation; writes, bash, and skills stay serial
|
|
19
|
+
// after send() returns.
|
|
19
20
|
// Memoized: the read-only name Set is built once per distinct `tools` array
|
|
20
21
|
// (keyed by identity via a module-level WeakMap) so repeated per-call lookups
|
|
21
22
|
// are O(1) instead of O(N) tools.find scans.
|
|
@@ -26,6 +26,9 @@ import {
|
|
|
26
26
|
compactTypeForSession,
|
|
27
27
|
} from './context-meta.mjs';
|
|
28
28
|
import { uncachedInputTokensForProvider } from './usage-metrics.mjs';
|
|
29
|
+
import { pruneOffloadSession } from '../tool-result-offload.mjs';
|
|
30
|
+
import { _getPendingMessagesForSession } from './pending-messages.mjs';
|
|
31
|
+
import { isSessionCompactionBlocked } from './runtime-liveness.mjs';
|
|
29
32
|
|
|
30
33
|
// 'compacting' is a transient in-flight stage written just before semantic /
|
|
31
34
|
// recall-fasttrack compaction runs. If the process crashes or only partially
|
|
@@ -489,6 +492,18 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
489
492
|
const unchangedReason = changed ? null : (force ? 'nothing to compact' : 'below threshold');
|
|
490
493
|
const now = Date.now();
|
|
491
494
|
session.messages = compacted;
|
|
495
|
+
// Best-effort GC only: the 10-minute mtime gate plus this idle-only guard
|
|
496
|
+
// lets an in-flight turn's sidecars survive until a later compaction/close.
|
|
497
|
+
const pruneSessionId = opts.sessionId || session.id;
|
|
498
|
+
if (!isSessionCompactionBlocked(pruneSessionId)) {
|
|
499
|
+
try {
|
|
500
|
+
await pruneOffloadSession(pruneSessionId, () => [
|
|
501
|
+
session.messages,
|
|
502
|
+
session.liveTurnMessages,
|
|
503
|
+
_getPendingMessagesForSession(pruneSessionId),
|
|
504
|
+
]);
|
|
505
|
+
} catch { /* best-effort */ }
|
|
506
|
+
}
|
|
492
507
|
session.providerState = undefined;
|
|
493
508
|
session.compaction = {
|
|
494
509
|
...(session.compaction || {}),
|