mixdog 0.9.55 → 0.9.59
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 +3 -1
- package/scripts/_smoke_wd.mjs +7 -0
- package/scripts/agent-terminal-reap-test.mjs +127 -2
- package/scripts/compact-file-reattach-test.mjs +88 -0
- package/scripts/compact-pressure-test.mjs +45 -21
- package/scripts/compact-recall-digest-test.mjs +57 -0
- package/scripts/compact-smoke.mjs +35 -39
- package/scripts/desktop-session-bridge-test.mjs +271 -9
- package/scripts/generate-oc-icons.mjs +11 -0
- package/scripts/live-share-test.mjs +148 -0
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/max-output-recovery-test.mjs +12 -1
- package/scripts/mouse-tracking-restore-smoke.mjs +107 -0
- package/scripts/provider-admission-scheduler-test.mjs +100 -0
- package/scripts/provider-contract-test.mjs +49 -0
- package/scripts/resource-admission-test.mjs +5 -2
- package/scripts/session-ingest-compaction-smoke.mjs +38 -0
- package/scripts/steering-drain-buckets-test.mjs +15 -0
- package/scripts/submit-commandbusy-race-test.mjs +54 -3
- package/scripts/tool-smoke.mjs +2 -3
- package/scripts/tui-ambiguous-width-test.mjs +113 -0
- package/scripts/tui-runtime-load-bench.mjs +5 -0
- package/scripts/tui-transcript-perf-test.mjs +167 -0
- package/src/app.mjs +23 -0
- package/src/cli.mjs +6 -0
- package/src/lib/keychain-cjs.cjs +70 -20
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +18 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +111 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +61 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +13 -4
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +43 -2
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +112 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +203 -49
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -15
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +109 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -7
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +39 -39
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +108 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -11
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +132 -24
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +91 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +14 -33
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +169 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +474 -42
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +17 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +36 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +249 -11
- package/src/runtime/channels/data/voice-runtime-manifest.json +21 -5
- package/src/runtime/channels/lib/scheduler.mjs +8 -6
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +71 -8
- package/src/runtime/channels/lib/voice-transcription.mjs +2 -2
- package/src/runtime/channels/lib/whisper-language.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +7 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +20 -0
- package/src/runtime/memory/lib/query-handlers.mjs +10 -4
- package/src/runtime/memory/lib/recall-format.mjs +43 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +90 -64
- package/src/runtime/shared/err-text.mjs +4 -1
- package/src/runtime/shared/resource-admission.mjs +11 -0
- package/src/runtime/shared/schedule-model-ref.mjs +28 -0
- package/src/runtime/shared/schedule-session-run.mjs +63 -0
- package/src/runtime/shared/schedules-db.mjs +8 -3
- package/src/runtime/shared/tool-result-summary.mjs +4 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/transcript-writer.mjs +17 -0
- package/src/session-runtime/channel-config-api.mjs +11 -0
- package/src/session-runtime/config-helpers.mjs +9 -6
- package/src/session-runtime/context-status.mjs +80 -3
- package/src/session-runtime/lifecycle-api.mjs +154 -40
- package/src/session-runtime/provider-auth-api.mjs +21 -2
- package/src/session-runtime/provider-usage.mjs +4 -1
- package/src/session-runtime/resource-api.mjs +12 -11
- package/src/session-runtime/runtime-core.mjs +45 -11
- package/src/session-runtime/session-text.mjs +56 -6
- package/src/session-runtime/session-turn-api.mjs +70 -2
- package/src/session-runtime/tool-catalog.mjs +2 -1
- package/src/session-runtime/workflow-agents-api.mjs +2 -2
- package/src/standalone/agent-tool/render.mjs +5 -1
- package/src/standalone/agent-tool.mjs +63 -3
- package/src/standalone/channel-admin.mjs +19 -3
- package/src/standalone/channel-daemon.mjs +40 -0
- package/src/tui/app/resume-picker.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +19 -0
- package/src/tui/app/transcript-window.mjs +45 -8
- package/src/tui/app/use-mouse-input.mjs +101 -31
- package/src/tui/app/use-transcript-window.mjs +53 -9
- package/src/tui/components/ToolExecution.jsx +3 -4
- package/src/tui/components/TranscriptItem.jsx +3 -0
- package/src/tui/dist/index.mjs +17391 -14920
- package/src/tui/engine/context-state.mjs +10 -1
- package/src/tui/engine/live-share.mjs +390 -0
- package/src/tui/engine/queue-helpers.mjs +23 -0
- package/src/tui/engine/session-api-ext.mjs +439 -40
- package/src/tui/engine/session-api.mjs +7 -1
- package/src/tui/engine/session-flow.mjs +21 -7
- package/src/tui/engine/tool-card-results.mjs +3 -1
- package/src/tui/engine/turn.mjs +57 -4
- package/src/tui/engine.mjs +211 -7
- package/src/tui/index.jsx +2 -0
- package/src/tui/lib/voice-setup.mjs +10 -4
- package/src/tui/paste-attachments.mjs +4 -1
- package/src/vendor/statusline/src/gateway/session-routes.mjs +24 -1
- package/src/workflows/default/WORKFLOW.md +3 -0
- package/vendor/ink/build/display-width.js +14 -0
- package/vendor/ink/build/output.js +24 -3
- package/vendor/ink/build/wrap-text.d.ts +2 -0
- package/vendor/ink/build/wrap-text.js +45 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isOffloadedToolResultText } from './tool-result-offload.mjs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import { providerNativeToolPrefixCount } from '../../../../session-runtime/provider-request-tools.mjs';
|
|
4
|
-
import { contentImageDescriptors, contentToText } from '../providers/media-normalization.mjs';
|
|
5
|
+
import { contentFileDescriptors, contentImageDescriptors, contentToText } from '../providers/media-normalization.mjs';
|
|
5
6
|
export {
|
|
6
7
|
DEFAULT_COMPACTION_BUFFER_TOKENS,
|
|
7
8
|
DEFAULT_COMPACTION_BUFFER_RATIO,
|
|
@@ -24,31 +25,137 @@ export {
|
|
|
24
25
|
} from './context-compaction-policy.mjs';
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
|
-
//
|
|
28
|
+
// Token estimation: real o200k_base BPE (tiktoken) + per-provider calibration.
|
|
28
29
|
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
30
|
+
// estimateTokens() encodes the provider-visible text projection with the real
|
|
31
|
+
// o200k_base BPE (tiktoken, same approach as Roo Code / goose / chatbox), so
|
|
32
|
+
// estimates track actual billing instead of a weighted-chars heuristic that
|
|
33
|
+
// diverged -25%..+60% depending on script and provider. The residual
|
|
34
|
+
// provider-specific gap between an o200k count and billed prompt tokens is
|
|
35
|
+
// reconciled by providerTokenCalibration(): measured on prefix-verified live
|
|
36
|
+
// sessions, OpenAI billing matches o200k directly (median ratio 1.00) while
|
|
37
|
+
// Anthropic bills the identical projection at ~1.7x o200k (Claude tokenizer +
|
|
38
|
+
// wire framing, median 1.71). Calibration is applied at the provider-aware
|
|
39
|
+
// aggregation boundary (compaction pressure / context gauge), never inside
|
|
40
|
+
// the provider-agnostic per-message memo.
|
|
36
41
|
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// Weights deliberately lean high (overcount) for CJK/Hangul/emoji.
|
|
42
|
+
// The previous conservative Unicode-weight heuristic is retained verbatim as
|
|
43
|
+
// legacyEstimateTokens(), used only when the tiktoken WASM cannot be loaded
|
|
44
|
+
// (stripped installs / exotic platforms) so estimation degrades safely.
|
|
41
45
|
//
|
|
42
|
-
// MIXDOG_TOKEN_ESTIMATE_SAFETY_MULTIPLIER (default 1.
|
|
43
|
-
// operators dial extra headroom without code changes
|
|
46
|
+
// MIXDOG_TOKEN_ESTIMATE_SAFETY_MULTIPLIER (default 1.0, clamped 1.0..2.0)
|
|
47
|
+
// still lets operators dial extra headroom without code changes; the
|
|
48
|
+
// provider-usage baseline remains the primary pressure source when aligned.
|
|
44
49
|
function readSafetyMultiplier() {
|
|
45
50
|
const raw = Number(process.env.MIXDOG_TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
|
|
46
51
|
if (Number.isFinite(raw)) return Math.min(2.0, Math.max(1.0, raw));
|
|
47
|
-
return 1.
|
|
52
|
+
return 1.0;
|
|
48
53
|
}
|
|
49
54
|
const TOKEN_ESTIMATE_SAFETY_MULTIPLIER = readSafetyMultiplier();
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
|
|
56
|
+
// Lazy singleton o200k_base encoder. createRequire keeps the load dynamic for
|
|
57
|
+
// bundlers (TUI esbuild) and lets a missing/broken WASM fall back cleanly.
|
|
58
|
+
const _requireForTokenizer = createRequire(import.meta.url);
|
|
59
|
+
let _bpeEncoder; // undefined = not attempted, null = unavailable
|
|
60
|
+
function bpeEncoder() {
|
|
61
|
+
if (_bpeEncoder !== undefined) return _bpeEncoder;
|
|
62
|
+
try {
|
|
63
|
+
const { Tiktoken } = _requireForTokenizer('tiktoken/lite');
|
|
64
|
+
const o200k = _requireForTokenizer('tiktoken/encoders/o200k_base.json');
|
|
65
|
+
_bpeEncoder = new Tiktoken(o200k.bpe_ranks, o200k.special_tokens, o200k.pat_str);
|
|
66
|
+
} catch {
|
|
67
|
+
_bpeEncoder = null;
|
|
68
|
+
}
|
|
69
|
+
return _bpeEncoder;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Compaction budget loops re-estimate overlapping transcript slices; identical
|
|
73
|
+
// large strings (tool results, system blocks) would re-encode every pass.
|
|
74
|
+
// Hash-keyed LRU (goose token_counter.rs pattern) absorbs the repeats.
|
|
75
|
+
const TOKEN_COUNT_CACHE_MIN_CHARS = 512;
|
|
76
|
+
const TOKEN_COUNT_CACHE_MAX_ENTRIES = 1_024;
|
|
77
|
+
const tokenCountCache = new Map();
|
|
78
|
+
// BPE merge cost is quadratic in WORD length, not text length: a degenerate
|
|
79
|
+
// single-word run ('p'.repeat(250k), base64/minified blobs) forms one giant
|
|
80
|
+
// pat_str word and encode() spins for minutes while the event loop is blocked
|
|
81
|
+
// (observed live via inspector: compact-smoke hung inside tiktoken encode).
|
|
82
|
+
// Encoding fixed-size slices caps the worst-case word at the chunk size, so
|
|
83
|
+
// cost stays linear. Chunk cuts snap back to the nearest whitespace so the
|
|
84
|
+
// next slice starts at a natural ` word` boundary — o200k tokenizes that
|
|
85
|
+
// identically to the unsliced text, keeping estimates EXACT for prose
|
|
86
|
+
// (compact-smoke asserts est === real o200k count). Only a whitespace-free
|
|
87
|
+
// degenerate run falls back to a hard cut (±1 token per boundary, safely
|
|
88
|
+
// inside the estimate's safety multiplier). Hard cuts avoid splitting a
|
|
89
|
+
// surrogate pair so sliced emoji/CJK-ext code points still encode cleanly.
|
|
90
|
+
const BPE_ENCODE_CHUNK_CHARS = 4_096;
|
|
91
|
+
const BPE_CHUNK_BOUNDARY_SCAN = 512;
|
|
92
|
+
function bpeEncodeCount(enc, s) {
|
|
93
|
+
if (s.length <= BPE_ENCODE_CHUNK_CHARS) return enc.encode(s, undefined, []).length;
|
|
94
|
+
let total = 0;
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < s.length) {
|
|
97
|
+
let end = Math.min(s.length, i + BPE_ENCODE_CHUNK_CHARS);
|
|
98
|
+
if (end < s.length) {
|
|
99
|
+
// Prefer cutting BEFORE a whitespace run: the next chunk then
|
|
100
|
+
// starts with ` word`, which o200k merges exactly as it would
|
|
101
|
+
// mid-text. Scan a bounded window so degenerate inputs stay O(1).
|
|
102
|
+
let ws = -1;
|
|
103
|
+
const scanFloor = Math.max(i + 1, end - BPE_CHUNK_BOUNDARY_SCAN);
|
|
104
|
+
for (let j = end - 1; j >= scanFloor; j -= 1) {
|
|
105
|
+
const c = s.charCodeAt(j);
|
|
106
|
+
if (c === 0x20 || c === 0x0A || c === 0x0D || c === 0x09) { ws = j; break; }
|
|
107
|
+
}
|
|
108
|
+
if (ws > i) {
|
|
109
|
+
end = ws; // next chunk starts at the whitespace
|
|
110
|
+
} else {
|
|
111
|
+
const last = s.charCodeAt(end - 1);
|
|
112
|
+
if (last >= 0xD800 && last <= 0xDBFF) end += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
total += enc.encode(s.slice(i, end), undefined, []).length;
|
|
116
|
+
i = end;
|
|
117
|
+
}
|
|
118
|
+
return total;
|
|
119
|
+
}
|
|
120
|
+
function bpeTokenCount(enc, s) {
|
|
121
|
+
if (s.length < TOKEN_COUNT_CACHE_MIN_CHARS) return bpeEncodeCount(enc, s);
|
|
122
|
+
const key = `${s.length}:${createHash('sha1').update(s).digest('base64')}`;
|
|
123
|
+
const hit = tokenCountCache.get(key);
|
|
124
|
+
if (hit !== undefined) {
|
|
125
|
+
tokenCountCache.delete(key);
|
|
126
|
+
tokenCountCache.set(key, hit);
|
|
127
|
+
return hit;
|
|
128
|
+
}
|
|
129
|
+
const count = bpeEncodeCount(enc, s);
|
|
130
|
+
if (tokenCountCache.size >= TOKEN_COUNT_CACHE_MAX_ENTRIES) {
|
|
131
|
+
tokenCountCache.delete(tokenCountCache.keys().next().value);
|
|
132
|
+
}
|
|
133
|
+
tokenCountCache.set(key, count);
|
|
134
|
+
return count;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Billed-prompt / o200k-estimate ratio per provider family, measured against
|
|
138
|
+
// prefix-signature-verified provider baselines from real sessions (opaque
|
|
139
|
+
// signature/encrypted payloads excluded from the projection — see
|
|
140
|
+
// stripOpaquePayloads). Env overrides let a deployment recalibrate without a
|
|
141
|
+
// code change; values are clamped to a plausible band.
|
|
142
|
+
function calibrationEnv(name) {
|
|
143
|
+
const n = Number(process.env[name]);
|
|
144
|
+
return Number.isFinite(n) && n > 0 ? Math.min(3, Math.max(0.25, n)) : null;
|
|
145
|
+
}
|
|
146
|
+
export function providerTokenCalibration(provider) {
|
|
147
|
+
const p = String(provider || '').toLowerCase();
|
|
148
|
+
if (p.startsWith('anthropic')) return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_ANTHROPIC') ?? 1.7;
|
|
149
|
+
if (p.startsWith('gemini') || p.startsWith('google')) return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_GEMINI') ?? 1.15;
|
|
150
|
+
return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_DEFAULT') ?? 1.0;
|
|
151
|
+
}
|
|
152
|
+
// Claude Code parity (services/tokenEstimation.ts): images/documents count a
|
|
153
|
+
// flat 2000 tokens when dimensions are unknown — the conservative constant CC
|
|
154
|
+
// shares with microCompact's IMAGE_MAX_TOKEN_SIZE. Known dimensions may only
|
|
155
|
+
// RAISE the allowance via Anthropic's real vision formula (w*h/750), capped at
|
|
156
|
+
// the 2000x2000 resize ceiling (5333 tokens).
|
|
157
|
+
export const IMAGE_VISUAL_TOKEN_ALLOWANCE = 2_000;
|
|
158
|
+
const IMAGE_MAX_TOKEN_ALLOWANCE = 5_333;
|
|
52
159
|
|
|
53
160
|
// Per-code-point token-cost weight. Tuned to overcount, not match exactly.
|
|
54
161
|
function codePointTokenWeight(cp) {
|
|
@@ -86,13 +193,23 @@ function codePointTokenWeight(cp) {
|
|
|
86
193
|
return 0.8;
|
|
87
194
|
}
|
|
88
195
|
|
|
89
|
-
//
|
|
90
|
-
// surrogate-pair emoji are scored once, at the high emoji weight), takes the
|
|
91
|
-
// max of the weighted sum and the chars/4 ASCII floor, then applies the safety
|
|
92
|
-
// multiplier. Always overcounts relative to chars/4 for non-ASCII text.
|
|
196
|
+
// Real-BPE token estimate with heuristic fallback. See module header.
|
|
93
197
|
export function estimateTokens(text) {
|
|
94
198
|
const s = String(text ?? '');
|
|
95
199
|
if (s.length === 0) return 0;
|
|
200
|
+
const enc = bpeEncoder();
|
|
201
|
+
if (enc) {
|
|
202
|
+
try {
|
|
203
|
+
return Math.ceil(bpeTokenCount(enc, s) * TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
|
|
204
|
+
} catch { /* corrupt input — degrade to the heuristic below */ }
|
|
205
|
+
}
|
|
206
|
+
return legacyEstimateTokens(s);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Legacy conservative Unicode-aware estimate (fallback only). Iterates by
|
|
210
|
+
// code point, takes the max of the weighted sum and the chars/4 ASCII floor,
|
|
211
|
+
// then applies the safety multiplier.
|
|
212
|
+
function legacyEstimateTokens(s) {
|
|
96
213
|
let weighted = 0;
|
|
97
214
|
for (const ch of s) weighted += codePointTokenWeight(ch.codePointAt(0));
|
|
98
215
|
// Encoded blobs, minified JSON and generated identifiers do not get the
|
|
@@ -101,7 +218,9 @@ export function estimateTokens(text) {
|
|
|
101
218
|
// for those runs without penalizing ordinary spaced prose.
|
|
102
219
|
let denseAsciiFloor = 0;
|
|
103
220
|
for (const match of s.matchAll(/[\x21-\x7e]{16,}/g)) {
|
|
104
|
-
|
|
221
|
+
// Claude Code prices dense JSON/JSONL at chars/2 (bytesPerTokenForFileType);
|
|
222
|
+
// 0.5/char keeps that parity for long unmerged printable runs.
|
|
223
|
+
denseAsciiFloor += match[0].length * 0.5;
|
|
105
224
|
}
|
|
106
225
|
const encodedWords = s.match(/\b(?=[A-Za-z0-9]{8,}\b)(?=[A-Za-z0-9]*[A-Za-z])(?=[A-Za-z0-9]*\d)[A-Za-z0-9]+\b/g) || [];
|
|
107
226
|
if (encodedWords.length >= 3) {
|
|
@@ -111,7 +230,7 @@ export function estimateTokens(text) {
|
|
|
111
230
|
const encodedChars = encodedWords.reduce((sum, word) => sum + word.length, 0);
|
|
112
231
|
denseAsciiFloor = Math.max(
|
|
113
232
|
denseAsciiFloor,
|
|
114
|
-
(encodedChars * 0.
|
|
233
|
+
(encodedChars * 0.5) + ((s.length - encodedChars) * 0.25),
|
|
115
234
|
);
|
|
116
235
|
}
|
|
117
236
|
const lines = s.split(/\r?\n/).filter(line => line.trim());
|
|
@@ -122,12 +241,35 @@ export function estimateTokens(text) {
|
|
|
122
241
|
&& (jsonLikeLines >= Math.ceil(lines.length / 2) || structural / nonWhitespace >= 0.12)) {
|
|
123
242
|
// JSONL, compact tables and generated line protocols can consist
|
|
124
243
|
// entirely of short runs while still tokenizing like minified data.
|
|
125
|
-
|
|
244
|
+
// chars/2 on the dense payload chars (Claude Code JSON parity).
|
|
245
|
+
denseAsciiFloor = Math.max(denseAsciiFloor, (nonWhitespace * 0.5) + ((s.length - nonWhitespace) * 0.25));
|
|
126
246
|
}
|
|
127
247
|
const asciiFloor = s.length / 4; // never below the legacy chars/4 lower bound
|
|
128
248
|
return Math.ceil(Math.max(weighted, asciiFloor, denseAsciiFloor) * TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
|
|
129
249
|
}
|
|
130
250
|
|
|
251
|
+
// Opaque replay payloads (Anthropic thinking signatures, OpenAI encrypted
|
|
252
|
+
// reasoning blobs, redacted data) are long base64-ish strings that are NOT
|
|
253
|
+
// billed proportionally to their serialized length — including them made
|
|
254
|
+
// estimates swing wildly per turn. Replace them with a fixed marker before
|
|
255
|
+
// token counting; calibration factors were measured against this projection.
|
|
256
|
+
// Stripping is KEY-SCOPED: only fields that structurally carry opaque replay
|
|
257
|
+
// material (signatures / encrypted / redacted / raw data blobs) are eligible,
|
|
258
|
+
// so genuine long model text (e.g. Gemini thought text) keeps its real cost.
|
|
259
|
+
const OPAQUE_PAYLOAD_KEY_RE = /signature|encrypted|redacted|^data$|^blob$/i;
|
|
260
|
+
const OPAQUE_PAYLOAD_RE = /^[A-Za-z0-9+/_=-]{64,}$/;
|
|
261
|
+
function stripOpaquePayloads(value, depth = 0, keyHint = '') {
|
|
262
|
+
if (typeof value === 'string') {
|
|
263
|
+
return OPAQUE_PAYLOAD_KEY_RE.test(keyHint) && value.length >= 64 && OPAQUE_PAYLOAD_RE.test(value)
|
|
264
|
+
? '[opaque]'
|
|
265
|
+
: value;
|
|
266
|
+
}
|
|
267
|
+
if (depth >= 8 || !value || typeof value !== 'object') return value;
|
|
268
|
+
if (Array.isArray(value)) return value.map((v) => stripOpaquePayloads(v, depth + 1, keyHint));
|
|
269
|
+
const out = {};
|
|
270
|
+
for (const [k, v] of Object.entries(value)) out[k] = stripOpaquePayloads(v, depth + 1, k);
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
131
273
|
function nativeBlocksEstimateText(value) {
|
|
132
274
|
const list = Array.isArray(value) ? value : [value];
|
|
133
275
|
return list.map((block) => {
|
|
@@ -137,7 +279,7 @@ function nativeBlocksEstimateText(value) {
|
|
|
137
279
|
type: 'image', width, height, detail,
|
|
138
280
|
})));
|
|
139
281
|
}
|
|
140
|
-
try { return JSON.stringify(block); }
|
|
282
|
+
try { return JSON.stringify(stripOpaquePayloads(block)); }
|
|
141
283
|
catch { return String(block ?? ''); }
|
|
142
284
|
}).join('\n');
|
|
143
285
|
}
|
|
@@ -181,15 +323,18 @@ export function messageEstimateText(m) {
|
|
|
181
323
|
}
|
|
182
324
|
function imageDescriptorAllowance(descriptor) {
|
|
183
325
|
if (descriptor.width && descriptor.height) {
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
326
|
+
// Anthropic vision cost: tokens = (width * height) / 750, with images
|
|
327
|
+
// resized down to at most 2000x2000 (5333 tokens) — Claude Code uses
|
|
328
|
+
// the same formula/cap. Caller-supplied dimensions may RAISE the
|
|
329
|
+
// allowance above the unknown-image floor but never lower it (the
|
|
330
|
+
// provider normalizer may not preserve caller metadata).
|
|
331
|
+
const formula = Math.ceil((descriptor.width * descriptor.height) / 750);
|
|
332
|
+
return Math.min(
|
|
333
|
+
IMAGE_MAX_TOKEN_ALLOWANCE,
|
|
334
|
+
Math.max(IMAGE_VISUAL_TOKEN_ALLOWANCE, formula),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
// Unknown-size images: flat conservative allowance (Claude Code parity).
|
|
193
338
|
return IMAGE_VISUAL_TOKEN_ALLOWANCE;
|
|
194
339
|
}
|
|
195
340
|
function messageImageDescriptors(m) {
|
|
@@ -203,8 +348,21 @@ function messageImageAllowance(m) {
|
|
|
203
348
|
if (!m || typeof m !== 'object') return 0;
|
|
204
349
|
return messageImageDescriptors(m).reduce((sum, descriptor) => sum + imageDescriptorAllowance(descriptor), 0);
|
|
205
350
|
}
|
|
351
|
+
// Inline document (PDF) allowance: Anthropic bills ~1,500-3,000 tokens per
|
|
352
|
+
// page and a typical PDF page is ~50-100KB, so ~bytes/16 with a floor and a
|
|
353
|
+
// cap. The base64 payload itself is excluded from text estimates (see
|
|
354
|
+
// jsonFallbackFromPart), so this allowance is the document's entire cost.
|
|
355
|
+
const FILE_TOKEN_ALLOWANCE_FLOOR = 1_500;
|
|
356
|
+
const FILE_MAX_TOKEN_ALLOWANCE = 300_000;
|
|
357
|
+
function messageFileAllowance(m) {
|
|
358
|
+
if (!m || typeof m !== 'object') return 0;
|
|
359
|
+
return contentFileDescriptors(m.content).reduce((sum, descriptor) => sum + Math.min(
|
|
360
|
+
FILE_MAX_TOKEN_ALLOWANCE,
|
|
361
|
+
Math.max(FILE_TOKEN_ALLOWANCE_FLOOR, Math.ceil((descriptor.sizeBytes || 0) / 16)),
|
|
362
|
+
), 0);
|
|
363
|
+
}
|
|
206
364
|
export function estimateMessageTokens(m) {
|
|
207
|
-
return estimateTokens(messageEstimateText(m)) + messageImageAllowance(m) + 4;
|
|
365
|
+
return estimateTokens(messageEstimateText(m)) + messageImageAllowance(m) + messageFileAllowance(m) + 4;
|
|
208
366
|
}
|
|
209
367
|
export function estimateMessagesTokens(messages) {
|
|
210
368
|
return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
|
|
@@ -479,11 +637,6 @@ export function contextMessagesSignature(messages, count = messages?.length) {
|
|
|
479
637
|
return hash.digest('hex');
|
|
480
638
|
}
|
|
481
639
|
|
|
482
|
-
// Per-request overhead the provider injects that never appears in the
|
|
483
|
-
// `messages` array: function-calling preamble + system-prompt framing the
|
|
484
|
-
// provider wraps around the request. The chars/4 message estimate misses all
|
|
485
|
-
// of it, so a "fits" verdict computed from messages alone is optimistic.
|
|
486
|
-
const REQUEST_OVERHEAD_TOKENS = 512;
|
|
487
640
|
const toolSchemaTokenMemo = new WeakMap();
|
|
488
641
|
const requestReserveTokenMemo = new WeakMap();
|
|
489
642
|
|
|
@@ -531,17 +684,16 @@ export function estimateToolSchemaTokens(tools) {
|
|
|
531
684
|
}
|
|
532
685
|
|
|
533
686
|
/**
|
|
534
|
-
* Total
|
|
535
|
-
* compaction
|
|
536
|
-
*
|
|
537
|
-
* request-side bytes the message estimate cannot see.
|
|
687
|
+
* Total request-side bytes the caller should reserve out of the context window
|
|
688
|
+
* before compaction. Only serialized tool schemas are counted; providers do
|
|
689
|
+
* not expose a stable framing cost, so no synthetic fixed allowance is added.
|
|
538
690
|
*/
|
|
539
691
|
export function estimateRequestReserveTokens(tools) {
|
|
540
|
-
if (!Array.isArray(tools)) return estimateToolSchemaTokens(tools)
|
|
692
|
+
if (!Array.isArray(tools)) return estimateToolSchemaTokens(tools);
|
|
541
693
|
const signature = toolSchemaSignature(tools);
|
|
542
694
|
const cached = requestReserveTokenMemo.get(tools);
|
|
543
695
|
if (cached?.signature === signature) return cached.value;
|
|
544
|
-
const reserve = estimateToolSchemaTokens(tools)
|
|
696
|
+
const reserve = estimateToolSchemaTokens(tools);
|
|
545
697
|
requestReserveTokenMemo.set(tools, { signature, value: reserve });
|
|
546
698
|
return reserve;
|
|
547
699
|
}
|
|
@@ -554,7 +706,7 @@ export function estimateRequestReserveTokens(tools) {
|
|
|
554
706
|
*
|
|
555
707
|
* @param {unknown[]} messages
|
|
556
708
|
* @param {unknown[]|number} toolsOrReserve tool list or precomputed reserve tokens
|
|
557
|
-
* @param {{ messageCount?: number, estimatedMessageTokens?: number }} [opts]
|
|
709
|
+
* @param {{ messageCount?: number, estimatedMessageTokens?: number, provider?: string }} [opts]
|
|
558
710
|
*/
|
|
559
711
|
export function estimateTranscriptContextUsage(messages, toolsOrReserve, opts = {}) {
|
|
560
712
|
const list = Array.isArray(messages) ? messages : [];
|
|
@@ -566,7 +718,9 @@ export function estimateTranscriptContextUsage(messages, toolsOrReserve, opts =
|
|
|
566
718
|
const reserve = typeof toolsOrReserve === 'number' && Number.isFinite(toolsOrReserve)
|
|
567
719
|
? Math.max(0, toolsOrReserve)
|
|
568
720
|
: estimateRequestReserveTokens(toolsOrReserve);
|
|
569
|
-
|
|
721
|
+
// Provider-aware calibration reconciles the o200k estimate with actual
|
|
722
|
+
// billing (see providerTokenCalibration). No provider → neutral 1.0.
|
|
723
|
+
return Math.round((messageTokens + reserve) * providerTokenCalibration(opts.provider));
|
|
570
724
|
}
|
|
571
725
|
|
|
572
726
|
const TOOL_MISSING_STUB = '[Older tool result unavailable after context compaction]';
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// Compaction debug/diagnostic helpers extracted from loop.mjs.
|
|
2
|
-
import {
|
|
2
|
+
import { summarizeContextMessages } from '../context-utils.mjs';
|
|
3
3
|
|
|
4
4
|
export function estimateMessagesTokensSafe(messages) {
|
|
5
|
-
|
|
5
|
+
// summarizeContextMessages is the fingerprint-validated cached form of
|
|
6
|
+
// estimateMessagesTokens (same per-message estimator, accumulated on the
|
|
7
|
+
// live array). The uncached sum cost ~30ms per call on a long session and
|
|
8
|
+
// ran on EVERY pre-send iteration; the cached path is ~1ms warm.
|
|
9
|
+
try { return summarizeContextMessages(messages).estimatedTokens; }
|
|
6
10
|
catch { return null; }
|
|
7
11
|
}
|
|
8
12
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
contextMessagesSignature,
|
|
7
7
|
estimateMessagesTokens,
|
|
8
8
|
estimateRequestReserveTokens,
|
|
9
|
+
providerTokenCalibration,
|
|
9
10
|
resolveSessionCompactPolicy,
|
|
10
11
|
toolSchemaSignature,
|
|
11
12
|
} from '../context-utils.mjs';
|
|
@@ -170,13 +171,32 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
170
171
|
reserveTokens,
|
|
171
172
|
requestReserveTokens: requestReserve,
|
|
172
173
|
configuredReserveTokens: configuredReserve,
|
|
174
|
+
provider: sessionRef.provider || null,
|
|
175
|
+
tokenCalibration: providerTokenCalibration(sessionRef.provider),
|
|
173
176
|
toolSchemaSignature: toolSchemaSignature(tools),
|
|
174
177
|
};
|
|
175
178
|
}
|
|
176
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Transcript + request reserve fallback used until an aligned provider
|
|
181
|
+
* baseline exists. The transcript estimate and the serialized-tool-schema
|
|
182
|
+
* reserve are both text the provider tokenizes, so the per-provider billing
|
|
183
|
+
* calibration applies to them; a configured operator reserve is a raw token
|
|
184
|
+
* allowance and stays uncalibrated. With no calibration on the policy the
|
|
185
|
+
* result is the exact legacy sum (estimate + reserveTokens).
|
|
186
|
+
*/
|
|
177
187
|
function compactPressureTokens(messageTokensEst, policy) {
|
|
178
188
|
if (messageTokensEst === null) return 0;
|
|
179
|
-
|
|
189
|
+
const calibration = Number(policy?.tokenCalibration) > 0 ? Number(policy.tokenCalibration) : 1;
|
|
190
|
+
const configured = Math.max(0, Number(policy?.configuredReserveTokens) || 0);
|
|
191
|
+
const totalReserve = Math.max(0, Number(policy?.reserveTokens) || 0);
|
|
192
|
+
// reserveTokens is the caller-facing override (tests/policies may zero it);
|
|
193
|
+
// the request-schema share can never exceed it.
|
|
194
|
+
const requestReserve = Math.min(
|
|
195
|
+
totalReserve,
|
|
196
|
+
Math.max(0, Number(policy?.requestReserveTokens ?? (totalReserve - configured)) || 0),
|
|
197
|
+
);
|
|
198
|
+
const otherReserve = Math.max(0, totalReserve - requestReserve);
|
|
199
|
+
return Math.max(0, Math.round((messageTokensEst + requestReserve) * calibration) + otherReserve);
|
|
180
200
|
}
|
|
181
201
|
|
|
182
202
|
function providerPressureTokens(sessionRef, usage) {
|
|
@@ -241,6 +261,13 @@ export function invalidateProviderContextBaseline(sessionRef) {
|
|
|
241
261
|
sessionRef.lastContextTokensStaleAfterCompact = true;
|
|
242
262
|
}
|
|
243
263
|
|
|
264
|
+
// A baseline is refreshed on every provider_send/turn-end. When the transcript
|
|
265
|
+
// has grown but no fresh recording landed for this long, recording is failing
|
|
266
|
+
// (zero/absent usage keeps the OLD baseline because record() only returns
|
|
267
|
+
// false) — distrust it and fall back to the estimate. An idle session whose
|
|
268
|
+
// transcript did NOT grow keeps its baseline regardless of age.
|
|
269
|
+
const BASELINE_MAX_STALE_GROWTH_MS = 30 * 60 * 1000;
|
|
270
|
+
|
|
244
271
|
function providerBaselinePressureTokens(messages, sessionRef, policy) {
|
|
245
272
|
if (!Array.isArray(messages) || !sessionRef
|
|
246
273
|
|| sessionRef.lastContextTokensStaleAfterCompact === true) return null;
|
|
@@ -255,6 +282,8 @@ function providerBaselinePressureTokens(messages, sessionRef, policy) {
|
|
|
255
282
|
|| sessionRef.contextPressureBaselineModel !== (sessionRef.model || null)
|
|
256
283
|
|| sessionRef.contextPressureBaselineToolSignature !== policy?.toolSchemaSignature
|
|
257
284
|
|| sessionRef.contextPressureBaselinePrefixSignature !== contextMessagesSignature(messages, count)) return null;
|
|
285
|
+
if (messages.length > count && baselineAt > 0
|
|
286
|
+
&& (Date.now() - baselineAt) > BASELINE_MAX_STALE_GROWTH_MS) return null;
|
|
258
287
|
if (sessionRef.contextPressureBaselineBoundary === 'request') {
|
|
259
288
|
const assistantOffset = messages.slice(count).findIndex(message => message?.role === 'assistant');
|
|
260
289
|
if (assistantOffset >= 0) {
|
|
@@ -268,8 +297,11 @@ function providerBaselinePressureTokens(messages, sessionRef, policy) {
|
|
|
268
297
|
}
|
|
269
298
|
}
|
|
270
299
|
try {
|
|
300
|
+
// Baseline tokens are authoritative provider billing; only the growth
|
|
301
|
+
// after the baseline is a local estimate and needs billing calibration.
|
|
302
|
+
const calibration = Number(policy?.tokenCalibration) > 0 ? Number(policy.tokenCalibration) : 1;
|
|
271
303
|
const growth = count < messages.length
|
|
272
|
-
? estimateMessagesTokens(messages.slice(count))
|
|
304
|
+
? Math.round(estimateMessagesTokens(messages.slice(count)) * calibration)
|
|
273
305
|
: 0;
|
|
274
306
|
return Math.max(0, tokens + growth + Math.max(0, Number(policy?.configuredReserveTokens) || 0));
|
|
275
307
|
} catch {
|
|
@@ -278,8 +310,20 @@ function providerBaselinePressureTokens(messages, sessionRef, policy) {
|
|
|
278
310
|
}
|
|
279
311
|
|
|
280
312
|
export function resolveCompactionPressureTokens(messageTokensEst, policy, { messages, sessionRef } = {}) {
|
|
281
|
-
|
|
282
|
-
|
|
313
|
+
const baseline = providerBaselinePressureTokens(messages, sessionRef, policy);
|
|
314
|
+
const estimate = compactPressureTokens(messageTokensEst, policy);
|
|
315
|
+
if (baseline == null) return estimate;
|
|
316
|
+
// Sanity band: the baseline exists to correct OVER-counting estimates
|
|
317
|
+
// (dense-data floors can inflate the estimate up to ~2x real usage), so a
|
|
318
|
+
// lower baseline is normally preferred. But a corrupt/stale baseline below
|
|
319
|
+
// HALF the estimate is no longer plausible as an overcount correction —
|
|
320
|
+
// a live session showed ~70% on the gauge while the provider was billing
|
|
321
|
+
// 111% of a 1M window. Beyond that band, trust the transcript estimate for
|
|
322
|
+
// both the gauge and the compaction decision. Erring toward the estimate
|
|
323
|
+
// may compact somewhat early; erring toward a rotten baseline blows past
|
|
324
|
+
// the context window at full token cost.
|
|
325
|
+
if (Number.isFinite(estimate) && estimate > 0 && baseline * 2 < estimate) return estimate;
|
|
326
|
+
return baseline;
|
|
283
327
|
}
|
|
284
328
|
|
|
285
329
|
/** Telemetry pressure when a reactive overflow retry forces the next compact. */
|
|
@@ -331,7 +375,16 @@ export function shouldCompactForSession(messageTokensEst, policy, {
|
|
|
331
375
|
const pressure = Number.isFinite(Number(pressureTokens))
|
|
332
376
|
? Number(pressureTokens)
|
|
333
377
|
: resolveCompactionPressureTokens(messageTokensEst, policy, { messages, sessionRef });
|
|
334
|
-
|
|
378
|
+
const trigger = policy.triggerTokens || policy.boundaryTokens;
|
|
379
|
+
if (pressure >= trigger) return true;
|
|
380
|
+
// Safety net: the provider-usage baseline exists to correct OVER-counting
|
|
381
|
+
// transcript estimates, so a lower baseline-derived pressure is normally
|
|
382
|
+
// preferred. But a stale/wrong baseline must never SUPPRESS compaction
|
|
383
|
+
// once the raw transcript estimate itself has crossed the trigger — that
|
|
384
|
+
// failure mode let a live session sail past a 950k trigger to 1.1M+ real
|
|
385
|
+
// tokens without a single auto compact. The trigger decision (not the
|
|
386
|
+
// gauge) therefore takes the max of both pressure sources.
|
|
387
|
+
return compactPressureTokens(messageTokensEst, policy) >= trigger;
|
|
335
388
|
}
|
|
336
389
|
export function countPrunedToolOutputs(before, after) {
|
|
337
390
|
if (!Array.isArray(before) || !Array.isArray(after)) return 0;
|
|
@@ -349,6 +402,11 @@ export function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
349
402
|
? sessionRef.compaction
|
|
350
403
|
: {};
|
|
351
404
|
const changed = meta.compactChanged === true || meta.pruneCount > 0;
|
|
405
|
+
// Both are successful terminal pre-send states. In particular,
|
|
406
|
+
// pre_send_check is the no-op path after a prior recovered/failing compact;
|
|
407
|
+
// retaining its old component error makes status report a failure although
|
|
408
|
+
// this send's compaction stage completed successfully.
|
|
409
|
+
const terminalSuccess = meta.stage === 'pre_send' || meta.stage === 'pre_send_check';
|
|
352
410
|
sessionRef.compaction = {
|
|
353
411
|
...prev,
|
|
354
412
|
auto: policy.auto !== false,
|
|
@@ -383,16 +441,22 @@ export function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
383
441
|
lastChanged: changed,
|
|
384
442
|
lastTrigger: meta.trigger || prev.lastTrigger || null,
|
|
385
443
|
lastSemantic: meta.semanticCompact === true,
|
|
386
|
-
lastSemanticError:
|
|
387
|
-
?
|
|
388
|
-
: (
|
|
444
|
+
lastSemanticError: terminalSuccess
|
|
445
|
+
? null
|
|
446
|
+
: Object.hasOwn(meta, 'semanticError')
|
|
447
|
+
? (meta.semanticError ?? null)
|
|
448
|
+
: (prev.lastSemanticError ?? null),
|
|
389
449
|
lastRecallFastTrack: meta.recallFastTrack === true,
|
|
390
|
-
lastRecallFastTrackError:
|
|
391
|
-
?
|
|
392
|
-
: (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
450
|
+
lastRecallFastTrackError: terminalSuccess
|
|
451
|
+
? null
|
|
452
|
+
: Object.hasOwn(meta, 'recallFastTrackError')
|
|
453
|
+
? (meta.recallFastTrackError ?? null)
|
|
454
|
+
: (prev.lastRecallFastTrackError ?? null),
|
|
455
|
+
lastError: terminalSuccess
|
|
456
|
+
? null
|
|
457
|
+
: Object.hasOwn(meta, 'compactError') || Object.hasOwn(meta, 'lastError')
|
|
458
|
+
? (meta.compactError ?? meta.lastError ?? null)
|
|
459
|
+
: (prev.lastError ?? null),
|
|
396
460
|
lastPruneCount: meta.pruneCount || 0,
|
|
397
461
|
lastDurationMs: meta.durationMs != null && Number.isFinite(Number(meta.durationMs))
|
|
398
462
|
? Math.max(0, Math.round(Number(meta.durationMs)))
|
|
@@ -402,6 +466,23 @@ export function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
|
402
466
|
? true
|
|
403
467
|
: prev.singleShotConsumed === true,
|
|
404
468
|
};
|
|
469
|
+
// Postmortem ring buffer: the per-check telemetry above is overwritten on
|
|
470
|
+
// every stage change, which erased all pre-compact evidence when a session
|
|
471
|
+
// blew past its trigger without compacting. Keep the last few decisions
|
|
472
|
+
// (pressure vs estimate vs trigger plus the live baseline) on the session
|
|
473
|
+
// so a missed-trigger incident is diagnosable after the fact.
|
|
474
|
+
{
|
|
475
|
+
const prior = Array.isArray(prev.recentChecks) ? prev.recentChecks : [];
|
|
476
|
+
sessionRef.compaction.recentChecks = [...prior, {
|
|
477
|
+
at: Date.now(),
|
|
478
|
+
stage: meta.stage || null,
|
|
479
|
+
pressure: meta.pressureTokens ?? null,
|
|
480
|
+
est: meta.beforeTokens ?? null,
|
|
481
|
+
trigger: policy.triggerTokens || policy.boundaryTokens || null,
|
|
482
|
+
baseline: positiveTokenInt(sessionRef.contextPressureBaselineTokens) || null,
|
|
483
|
+
baselineAt: Number(sessionRef.contextPressureBaselineUpdatedAt) || null,
|
|
484
|
+
}].slice(-8);
|
|
485
|
+
}
|
|
405
486
|
if (changed) {
|
|
406
487
|
const changedAt = Date.now();
|
|
407
488
|
sessionRef.compaction.lastChangedAt = changedAt;
|
|
@@ -435,3 +516,17 @@ export function emitCompactEvent(opts, event = {}) {
|
|
|
435
516
|
export function compactEventType(policy, fallback = DEFAULT_COMPACT_TYPE) {
|
|
436
517
|
return policy?.compactType || policy?.type || fallback;
|
|
437
518
|
}
|
|
519
|
+
|
|
520
|
+
// Semantic-summary model override. NO automatic downshift: the runtime cannot
|
|
521
|
+
// know which models a given account/gateway can actually serve (an OAuth plan
|
|
522
|
+
// or relay may not expose Haiku at all), so guessing a cheaper model risks a
|
|
523
|
+
// hard compact failure. The summary runs on the session's own model unless an
|
|
524
|
+
// operator explicitly configures compaction.semanticModel (or the
|
|
525
|
+
// MIXDOG_AGENT_COMPACT_SEMANTIC_MODEL env) — that opt-in is the only override.
|
|
526
|
+
export function resolveSemanticSummaryModel(sessionRef, _opts = {}) {
|
|
527
|
+
const cfg = sessionRef?.compaction && typeof sessionRef.compaction === 'object' ? sessionRef.compaction : {};
|
|
528
|
+
const explicit = String(cfg.semanticModel || '').trim()
|
|
529
|
+
|| String(process.env.MIXDOG_AGENT_COMPACT_SEMANTIC_MODEL || '').trim();
|
|
530
|
+
if (explicit) return explicit;
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
@@ -161,6 +161,7 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
161
161
|
limit: positiveTokenInt(sessionRef?.compaction?.recallDigestLimit) || 30,
|
|
162
162
|
includeMembers: true,
|
|
163
163
|
includeRaw: true,
|
|
164
|
+
compactDigest: true,
|
|
164
165
|
}, callerCtx);
|
|
165
166
|
digestBody = typeof browsed === 'string' ? browsed : String(browsed?.text ?? browsed ?? '');
|
|
166
167
|
} catch (err) {
|
|
@@ -213,6 +214,7 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
213
214
|
recallText: digestText,
|
|
214
215
|
query,
|
|
215
216
|
querySha,
|
|
217
|
+
cwd: sessionRef?.cwd,
|
|
216
218
|
// Ingest + search both succeeded above, so the memory DB genuinely holds
|
|
217
219
|
// this transcript and the recall notice is truthful even when the digest
|
|
218
220
|
// body is small. A failure would have aborted before reaching here.
|
|
@@ -9,7 +9,7 @@ import { executeBashSessionTool } from '../../tools/bash-session.mjs';
|
|
|
9
9
|
import { executePatchTool } from '../../tools/patch.mjs';
|
|
10
10
|
import { executeInternalTool, isInternalTool } from '../../internal-tools.mjs';
|
|
11
11
|
import { normalizeToolEnvelope, makeToolEnvelope } from '../tool-envelope.mjs';
|
|
12
|
-
import { getSessionAbortSignal, enqueuePendingMessage, markCompletionEntry } from '../manager.mjs';
|
|
12
|
+
import { getSessionAbortSignal, enqueuePendingMessage, markCompletionEntry, markSessionToolOutputTail } from '../manager.mjs';
|
|
13
13
|
import { createScopedCacheOutcome } from '../cache/scoped-cache-outcome.mjs';
|
|
14
14
|
import { modelVisibleToolCompletionMessage } from '../../../../shared/tool-execution-contract.mjs';
|
|
15
15
|
import { _isScopedCacheableTool } from './tool-classify.mjs';
|
|
@@ -81,6 +81,12 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
|
|
|
81
81
|
routingSessionId: callerSessionId,
|
|
82
82
|
clientHostPid: sessionRef?.clientHostPid,
|
|
83
83
|
notifyFn,
|
|
84
|
+
// Live shell-output tail → session liveness (~1 s cadence from the
|
|
85
|
+
// shell tool's tail timer), surfaced to transcript consumers (desktop
|
|
86
|
+
// running tool cards) via getSessionProgressSnapshot.
|
|
87
|
+
onOutputTail: (tail) => {
|
|
88
|
+
try { markSessionToolOutputTail(callerSessionId, tail); } catch { /* best effort */ }
|
|
89
|
+
},
|
|
84
90
|
};
|
|
85
91
|
const beforeToolHook = typeof executeOpts.beforeToolHook === 'function'
|
|
86
92
|
? executeOpts.beforeToolHook
|