mixdog 0.9.45 → 0.9.47
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 +5 -2
- package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
- package/scripts/agent-model-liveness-test.mjs +618 -0
- package/scripts/agent-trace-io-test.mjs +68 -4
- package/scripts/bench-run.mjs +30 -3
- package/scripts/compact-pressure-test.mjs +187 -4
- package/scripts/compact-prior-context-flatten-test.mjs +252 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -6
- package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
- package/scripts/context-mcp-metering-test.mjs +75 -0
- package/scripts/explore-prompt-policy-test.mjs +15 -16
- package/scripts/explore-timeout-cancel-test.mjs +345 -0
- package/scripts/headless-pristine-execution-test.mjs +614 -0
- package/scripts/internal-comms-smoke.mjs +15 -6
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/memory-core-input-test.mjs +137 -0
- package/scripts/memory-rule-contract-test.mjs +5 -5
- package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
- package/scripts/parent-abort-link-test.mjs +22 -0
- package/scripts/provider-toolcall-test.mjs +22 -0
- package/scripts/reactive-compact-persist-smoke.mjs +8 -4
- package/scripts/session-bench.mjs +3 -70
- package/scripts/task-bench.mjs +0 -2
- package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
- package/scripts/tool-smoke.mjs +21 -21
- package/scripts/tool-tui-presentation-test.mjs +68 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
- package/src/app.mjs +28 -103
- package/src/cli.mjs +17 -13
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/headless-command.mjs +139 -0
- package/src/headless-role.mjs +121 -10
- package/src/help.mjs +4 -1
- package/src/rules/agent/00-common.md +3 -3
- package/src/rules/agent/00-core.md +8 -9
- package/src/rules/agent/20-skip-protocol.md +2 -3
- package/src/rules/agent/30-explorer.md +50 -56
- package/src/rules/agent/40-cycle1-agent.md +10 -12
- package/src/rules/agent/41-cycle2-agent.md +12 -9
- package/src/rules/agent/42-cycle3-agent.md +4 -6
- package/src/rules/lead/01-general.md +5 -6
- package/src/rules/lead/02-channels.md +1 -1
- package/src/rules/lead/lead-brief.md +14 -17
- package/src/rules/lead/lead-tool.md +3 -3
- package/src/rules/shared/01-tool.md +41 -43
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
- package/src/runtime/agent/orchestrator/config.mjs +96 -30
- package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/runtime/memory/index.mjs +0 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
- package/src/runtime/memory/lib/http-router.mjs +0 -193
- package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +5 -6
- package/src/runtime/shared/config.mjs +11 -34
- package/src/runtime/shared/pristine-execution-contract.json +75 -0
- package/src/runtime/shared/pristine-execution.mjs +356 -0
- package/src/runtime/shared/provider-api-key.mjs +43 -0
- package/src/runtime/shared/provider-auth-binding.mjs +21 -0
- package/src/session-runtime/context-status.mjs +61 -13
- package/src/session-runtime/mcp-glue.mjs +29 -2
- package/src/session-runtime/plugin-mcp.mjs +7 -0
- package/src/session-runtime/resource-api.mjs +38 -5
- package/src/session-runtime/runtime-core.mjs +5 -1
- package/src/session-runtime/session-turn-api.mjs +14 -2
- package/src/session-runtime/settings-api.mjs +5 -0
- package/src/session-runtime/tool-catalog.mjs +13 -2
- package/src/session-runtime/tool-defs.mjs +1 -3
- package/src/standalone/agent-task-status.mjs +50 -11
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/explore-tool.mjs +257 -49
- package/src/standalone/seeds.mjs +1 -0
- package/src/tui/App.jsx +23 -10
- package/src/tui/app/use-transcript-scroll.mjs +4 -3
- package/src/tui/app/use-transcript-window.mjs +12 -21
- package/src/tui/components/ContextPanel.jsx +19 -25
- package/src/tui/dist/index.mjs +77 -65
- package/src/tui/engine/agent-envelope.mjs +16 -5
- package/src/tui/engine/labels.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +21 -51
- package/src/workflows/solo/WORKFLOW.md +12 -17
- package/src/workflows/solo-review/WORKFLOW.md +0 -47
|
@@ -8,83 +8,6 @@ import { appendAgentTrace } from '../../agent-trace.mjs';
|
|
|
8
8
|
import { level2SteerMessage } from './completion-guards.mjs';
|
|
9
9
|
import { isEagerDispatchable } from './tool-helpers.mjs';
|
|
10
10
|
|
|
11
|
-
// --- Steering detector helpers (pure) ---
|
|
12
|
-
const _WORD_RE = /[A-Za-z_$][\w$]*/g;
|
|
13
|
-
const _IDENT_RE = /^[A-Za-z_$][\w$]{2,}$/;
|
|
14
|
-
const _SRC_DIR_RE = /(^|[\\/])(src|lib|app|packages|components|pages)([\\/]|$)/i;
|
|
15
|
-
function _grepPatterns(c) {
|
|
16
|
-
const p = c?.arguments?.pattern;
|
|
17
|
-
if (typeof p === 'string') return [p];
|
|
18
|
-
if (Array.isArray(p)) return p.filter((x) => typeof x === 'string');
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
function _patternTokens(c) {
|
|
22
|
-
const out = new Set();
|
|
23
|
-
for (const pat of _grepPatterns(c)) {
|
|
24
|
-
for (const t of (String(pat).match(_WORD_RE) || [])) {
|
|
25
|
-
if (t.length >= 3) out.add(t.toLowerCase());
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
function _pathsOf(arg) {
|
|
31
|
-
const out = [];
|
|
32
|
-
const push = (v) => {
|
|
33
|
-
if (typeof v === 'string' && v) out.push(v);
|
|
34
|
-
else if (v && typeof v === 'object' && typeof v.path === 'string' && v.path) out.push(v.path);
|
|
35
|
-
};
|
|
36
|
-
if (Array.isArray(arg)) arg.forEach(push); else push(arg);
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
// Case-insensitive (win32 paths) forward-slash normalization for all path compares.
|
|
40
|
-
function _normPath(p) { return String(p).replace(/\\/g, '/').toLowerCase(); }
|
|
41
|
-
function _baseName(p) { const s = _normPath(p); const i = s.lastIndexOf('/'); return i >= 0 ? s.slice(i + 1) : s; }
|
|
42
|
-
function _dirName(p) { const s = _normPath(p); const i = s.lastIndexOf('/'); return i >= 0 ? s.slice(0, i) : ''; }
|
|
43
|
-
// A path targets a specific file (not a directory scope) when its basename
|
|
44
|
-
// carries a file extension — the signal that a grep was already file-scoped.
|
|
45
|
-
function _isFileScopedPath(p) { return /\.[A-Za-z0-9]{1,12}$/.test(_baseName(p)); }
|
|
46
|
-
// Grep context flag: -C/-A/-B count as context ONLY when > 0. An explicit
|
|
47
|
-
// -C:0 / -A:0 / -B:0 is NO context (bare match lines).
|
|
48
|
-
function _hasGrepContext(a) { return Number(a?.['-A']) > 0 || Number(a?.['-B']) > 0 || Number(a?.['-C']) > 0; }
|
|
49
|
-
// Canonical path segments: normalize slashes/case, drop '.' and resolve '..'
|
|
50
|
-
// so ./ and ../ variants collapse. Used by the abs/rel-tolerant path match.
|
|
51
|
-
function _canonSegs(p) {
|
|
52
|
-
const out = [];
|
|
53
|
-
for (const seg of _normPath(p).split('/')) {
|
|
54
|
-
if (seg === '' || seg === '.') continue;
|
|
55
|
-
if (seg === '..') { if (out.length && out[out.length - 1] !== '..') out.pop(); else out.push('..'); continue; }
|
|
56
|
-
out.push(seg);
|
|
57
|
-
}
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
function _canonPath(p) { return _canonSegs(p).join('/'); }
|
|
61
|
-
// abs/rel-tolerant same-file test: equal when one canonical path is a
|
|
62
|
-
// segment-aligned suffix of the other (so C:/proj/src/x.mjs, src/x.mjs and
|
|
63
|
-
// ./src/x.mjs all match). Basename equality is implied by the suffix compare.
|
|
64
|
-
function _pathsMatch(a, b) {
|
|
65
|
-
if (!a || !b) return false;
|
|
66
|
-
const A = _canonSegs(a); const B = _canonSegs(b);
|
|
67
|
-
if (!A.length || !B.length) return false;
|
|
68
|
-
const long = A.length >= B.length ? A : B;
|
|
69
|
-
const short = A.length >= B.length ? B : A;
|
|
70
|
-
for (let i = 0; i < short.length; i += 1) {
|
|
71
|
-
if (long[long.length - short.length + i] !== short[i]) return false;
|
|
72
|
-
}
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
// Exact (normalized) same-file test — used where "related" is too loose.
|
|
76
|
-
function _samePath(a, b) { return !!a && !!b && _normPath(a) === _normPath(b); }
|
|
77
|
-
// Loose sibling/variant relation — cluster detector pairs this WITH token overlap.
|
|
78
|
-
function _pathsRelated(a, b) {
|
|
79
|
-
if (!a || !b) return false;
|
|
80
|
-
const na = _normPath(a); const nb = _normPath(b);
|
|
81
|
-
if (na === nb) return true; // identical
|
|
82
|
-
if (na.includes(nb) || nb.includes(na)) return true; // normalized variant
|
|
83
|
-
if (_dirName(na) && _dirName(na) === _dirName(nb)) return true; // siblings
|
|
84
|
-
const ba = _baseName(na); const bb = _baseName(nb);
|
|
85
|
-
return !!ba && ba === bb; // same file name
|
|
86
|
-
}
|
|
87
|
-
function _setsOverlap(a, b) { for (const x of a) { if (b.has(x)) return true; } return false; }
|
|
88
11
|
// A single chained segment is read-only only if it STARTS with a read-only verb.
|
|
89
12
|
function _isReadOnlyShellSegment(seg) {
|
|
90
13
|
const s = seg.trim();
|
|
@@ -108,23 +31,6 @@ function _isReadOnlyShellCmd(cmd) {
|
|
|
108
31
|
}
|
|
109
32
|
return sawCmd;
|
|
110
33
|
}
|
|
111
|
-
// Extract (path, offset) windows from a read call: path may be a string, an
|
|
112
|
-
// array of strings, or an array of {path,offset,limit} regions; a top-level
|
|
113
|
-
// offset applies to string entries.
|
|
114
|
-
function _readWindows(c) {
|
|
115
|
-
const a = c?.arguments || {};
|
|
116
|
-
const out = [];
|
|
117
|
-
const push = (p, off) => { if (typeof p === 'string' && p) out.push({ path: p, offset: Number(off) }); };
|
|
118
|
-
const v = a.path;
|
|
119
|
-
if (Array.isArray(v)) {
|
|
120
|
-
for (const e of v) {
|
|
121
|
-
if (typeof e === 'string') push(e, a.offset);
|
|
122
|
-
else if (e && typeof e === 'object') push(e.path, e.offset ?? a.offset);
|
|
123
|
-
}
|
|
124
|
-
} else if (typeof v === 'string') push(v, a.offset);
|
|
125
|
-
return out;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
34
|
// Build the completion-first steering-ladder controller. `ctx` supplies live
|
|
129
35
|
// accessors so every read reflects the loop's current mutable state:
|
|
130
36
|
// - messages, sessionId, sessionAgent, tools (stable refs/values)
|
|
@@ -168,35 +74,7 @@ export function createSteeringLadder(ctx) {
|
|
|
168
74
|
// call (missed parallelism). Not reset per-iteration — only by the
|
|
169
75
|
// steering-hint fire below or by a turn that batches/edits.
|
|
170
76
|
let _serialReadOnlyStreak = 0;
|
|
171
|
-
// Same-concept grep cluster (broadened serial-rewording spiral): counts
|
|
172
|
-
// consecutive grep-only turns that stay on ONE concept — related by
|
|
173
|
-
// identical/normalized/sibling paths OR pattern-token overlap, not just an
|
|
174
|
-
// identical path. `_sameGrepPrev` holds the prior turn's {paths,tokens}.
|
|
175
|
-
let _sameGrepStreak = 0;
|
|
176
|
-
let _sameGrepPrev = null;
|
|
177
|
-
// Prior turn's uncapped content/context grep paths — seeds the
|
|
178
|
-
// grep-context-then-read detector (a read of the same file next turn).
|
|
179
|
-
let _lastGrepContentPaths = null;
|
|
180
|
-
// Consecutive turns grepping the SAME identifier-like token in source dirs
|
|
181
|
-
// (symbol lookups that belong in code_graph).
|
|
182
|
-
let _identGrepStreak = 0;
|
|
183
|
-
let _identGrepToken = null;
|
|
184
77
|
let _level2FireCount = 0;
|
|
185
|
-
// Read-fragmentation detector state: normalized path -> cumulative
|
|
186
|
-
// single-window offset reads. Mirrors session-bench read_fragmentation
|
|
187
|
-
// (3+ windowed reads of one file within an 800-line span). Multi-window
|
|
188
|
-
// same-path regions in ONE call are the recommended batched form and are
|
|
189
|
-
// never counted. Fires once per path per session.
|
|
190
|
-
const _readWindowsByPath = new Map();
|
|
191
|
-
// Detector A state: prior turn's file-scoped grep paths that ran WITHOUT
|
|
192
|
-
// any -C/-B/-A context (a bare match-line grep on a specific file). Seeds
|
|
193
|
-
// the grep-no-context-then-read nudge for the next turn.
|
|
194
|
-
let _lastFileScopedGrepNoCtxPaths = null;
|
|
195
|
-
// Detector B state: per-path consecutive-read streak (canonical path ->
|
|
196
|
-
// count). Tracked as a Map so multi-file turns keep independent streaks
|
|
197
|
-
// ([A,B]->B->B fires on B). Files not read this turn are pruned; a path is
|
|
198
|
-
// dropped after it fires.
|
|
199
|
-
const _readStreaks = new Map();
|
|
200
78
|
|
|
201
79
|
// Level-2 steering emitter shared by both ladder paths (single-call
|
|
202
80
|
// level-1 streak and the independent all-read-only streak). Sets the latch
|
|
@@ -225,25 +103,8 @@ export function createSteeringLadder(ctx) {
|
|
|
225
103
|
emitPostBatchSteering(calls, hintAlreadyFired) {
|
|
226
104
|
const iterations = getIterations();
|
|
227
105
|
const editCount = getEditCount();
|
|
228
|
-
// Steering hint gate: at most ONE hint per turn
|
|
229
|
-
// level-2 > grep/shell detectors > level-1).
|
|
106
|
+
// Steering hint gate: at most ONE hint per turn.
|
|
230
107
|
let _hintFiredThisTurn = hintAlreadyFired;
|
|
231
|
-
// Specific-detector predicates (A: grep-no-context then re-read; B:
|
|
232
|
-
// 3rd consecutive same-file read). Evaluated up front so the GENERIC
|
|
233
|
-
// level-1 batching nudge can defer to them (more specific wins),
|
|
234
|
-
// while the level-2 runaway escalation below still outranks them.
|
|
235
|
-
const _readCallsThisTurn = calls.filter((c) => c?.name === 'read');
|
|
236
|
-
const _aWouldFire = !!_lastFileScopedGrepNoCtxPaths
|
|
237
|
-
&& _readCallsThisTurn.some((c) => _pathsOf(c?.arguments?.path)
|
|
238
|
-
.some((p) => [..._lastFileScopedGrepNoCtxPaths].some((g) => _pathsMatch(p, g))));
|
|
239
|
-
let _bWouldFire = false;
|
|
240
|
-
for (const c of _readCallsThisTurn) {
|
|
241
|
-
for (const w of _readWindows(c)) {
|
|
242
|
-
for (const [k, cnt] of _readStreaks) { if (cnt >= 2 && _pathsMatch(k, w.path)) { _bWouldFire = true; break; } }
|
|
243
|
-
if (_bWouldFire) break;
|
|
244
|
-
}
|
|
245
|
-
if (_bWouldFire) break;
|
|
246
|
-
}
|
|
247
108
|
// Missed-parallelism steering: 2+ consecutive turns of a single
|
|
248
109
|
// read-only tool call suggest the model isn't batching independent
|
|
249
110
|
// lookups. Nudge once, then reset (fires again after 2 more).
|
|
@@ -252,10 +113,9 @@ export function createSteeringLadder(ctx) {
|
|
|
252
113
|
// Escalation ladder (Step 1). Cumulative level-1 fires are tracked
|
|
253
114
|
// and NEVER reset. Once level-1 has fired >=3 times with ZERO edits,
|
|
254
115
|
// escalate to level-2 steering (latched once per 5 turns). The
|
|
255
|
-
// level-2 escalation always wins
|
|
256
|
-
// DEFERS to the specific A/B detectors when either would fire.
|
|
116
|
+
// level-2 escalation always wins.
|
|
257
117
|
const _canEscalate = (_level1FireCount + 1) >= 3 && editCount === 0 && (iterations - _level2LatchAtIteration) >= 5;
|
|
258
|
-
if (_serialReadOnlyStreak >= 2 && !_hintFiredThisTurn
|
|
118
|
+
if (_serialReadOnlyStreak >= 2 && !_hintFiredThisTurn) {
|
|
259
119
|
_serialReadOnlyStreak = 0;
|
|
260
120
|
_level1FireCount += 1;
|
|
261
121
|
if (_canEscalate) {
|
|
@@ -288,227 +148,6 @@ export function createSteeringLadder(ctx) {
|
|
|
288
148
|
_allReadOnlyStreak = 0;
|
|
289
149
|
}
|
|
290
150
|
}
|
|
291
|
-
const _grepCalls = calls.filter((c) => c?.name === 'grep');
|
|
292
|
-
const _allGrep = calls.length > 0 && _grepCalls.length === calls.length;
|
|
293
|
-
// Detector: repeated identifier-like grep in source dirs → code_graph.
|
|
294
|
-
// Symbol lookups (a bare \w+ token scoped to src/) belong in
|
|
295
|
-
// code_graph (find_symbol/references/callers), not text grep. Fires
|
|
296
|
-
// when the same token is grepped on 2+ consecutive qualifying turns.
|
|
297
|
-
{
|
|
298
|
-
let _identTok = null;
|
|
299
|
-
for (const c of _grepCalls) {
|
|
300
|
-
const _srcScoped = _pathsOf(c?.arguments?.path).some((p) => _SRC_DIR_RE.test(p));
|
|
301
|
-
if (!_srcScoped) continue;
|
|
302
|
-
const _hit = _grepPatterns(c).map((p) => p.trim()).find((p) => _IDENT_RE.test(p));
|
|
303
|
-
if (_hit) { _identTok = _hit.toLowerCase(); break; }
|
|
304
|
-
}
|
|
305
|
-
if (_identTok) {
|
|
306
|
-
if (_identTok === _identGrepToken) _identGrepStreak += 1;
|
|
307
|
-
else { _identGrepToken = _identTok; _identGrepStreak = 1; }
|
|
308
|
-
if (_identGrepStreak >= 2 && !_hintFiredThisTurn) {
|
|
309
|
-
pushSystemReminder(`Grepping the symbol "${_identGrepToken}" across source dirs again — use code_graph (find_symbol/references/callers) for symbol lookups instead of text grep.`);
|
|
310
|
-
_identGrepStreak = 0;
|
|
311
|
-
_identGrepToken = null;
|
|
312
|
-
_hintFiredThisTurn = true;
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
_identGrepStreak = 0;
|
|
316
|
-
_identGrepToken = null;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
// Same-concept grep cluster (broadened serial-rewording): 3+
|
|
320
|
-
// consecutive grep-only turns on one concept — related by
|
|
321
|
-
// an exact-same path, OR a sibling/variant path that ALSO shares a
|
|
322
|
-
// pattern token (sibling alone is too loose to count).
|
|
323
|
-
// Fires once per spiral, then resets; read-the-span is the answer.
|
|
324
|
-
{
|
|
325
|
-
if (_allGrep) {
|
|
326
|
-
const _paths = [];
|
|
327
|
-
const _tokens = new Set();
|
|
328
|
-
for (const c of _grepCalls) {
|
|
329
|
-
for (const p of _pathsOf(c?.arguments?.path)) _paths.push(p);
|
|
330
|
-
for (const t of _patternTokens(c)) _tokens.add(t);
|
|
331
|
-
}
|
|
332
|
-
let _related = false;
|
|
333
|
-
if (_sameGrepPrev) {
|
|
334
|
-
// Exact same-path spiral fires alone; a merely sibling/
|
|
335
|
-
// variant path must ALSO share a pattern token to count.
|
|
336
|
-
const _pathExact = _paths.some((a) => _sameGrepPrev.paths.some((b) => _samePath(a, b)));
|
|
337
|
-
const _pathRel = _paths.some((a) => _sameGrepPrev.paths.some((b) => _pathsRelated(a, b)));
|
|
338
|
-
const _tokRel = _setsOverlap(_tokens, _sameGrepPrev.tokens);
|
|
339
|
-
_related = _pathExact || (_pathRel && _tokRel);
|
|
340
|
-
}
|
|
341
|
-
_sameGrepStreak = _related ? _sameGrepStreak + 1 : 1;
|
|
342
|
-
const _label = _paths[0] || [..._tokens][0] || 'same concept';
|
|
343
|
-
_sameGrepPrev = { paths: _paths, tokens: _tokens };
|
|
344
|
-
if (_sameGrepStreak >= 3 && !_hintFiredThisTurn) {
|
|
345
|
-
pushSystemReminder(`3+ consecutive grep turns on the same concept (${_label}) — reworded patterns / sibling paths are not converging. Read the relevant span (read with offset/limit) or act on what you have.`);
|
|
346
|
-
_sameGrepStreak = 0;
|
|
347
|
-
_sameGrepPrev = null;
|
|
348
|
-
_hintFiredThisTurn = true;
|
|
349
|
-
}
|
|
350
|
-
} else {
|
|
351
|
-
_sameGrepStreak = 0;
|
|
352
|
-
_sameGrepPrev = null;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
// Detector: content/context grep on X then read of X next turn — the
|
|
356
|
-
// grep context should have sufficed. Fire off the PRIOR turn's
|
|
357
|
-
// uncapped content grep, then recompute for the next turn (skip when
|
|
358
|
-
// the grep was capped via head_limit or paths-only mode).
|
|
359
|
-
{
|
|
360
|
-
if (_lastGrepContentPaths && !_hintFiredThisTurn) {
|
|
361
|
-
const _readCall = calls.find((c) => c?.name === 'read');
|
|
362
|
-
if (_readCall) {
|
|
363
|
-
const _rp = _pathsOf(_readCall?.arguments?.path);
|
|
364
|
-
// Same-file only — a sibling read is a new lookup, not a re-read.
|
|
365
|
-
const _hit = _rp.find((p) => [..._lastGrepContentPaths].some((g) => _samePath(p, g)));
|
|
366
|
-
if (_hit) {
|
|
367
|
-
pushSystemReminder(`Reading ${_baseName(_hit)} right after a content grep on it — the grep context should have sufficed; answer from grep output or widen -C instead of re-reading.`);
|
|
368
|
-
_hintFiredThisTurn = true;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
const _next = new Set();
|
|
373
|
-
for (const c of _grepCalls) {
|
|
374
|
-
const a = c?.arguments || {};
|
|
375
|
-
// Bare output_mode:'content' WITHOUT real context is routed to
|
|
376
|
-
// Detector A (which gives the add-`-C` advice); this detector
|
|
377
|
-
// only owns greps that actually returned context.
|
|
378
|
-
const _isContent = _hasGrepContext(a) || a.output_mode === 'content_with_context';
|
|
379
|
-
const _pathsOnly = a.output_mode === 'files_with_matches' || a.output_mode === 'count';
|
|
380
|
-
const _capped = a.head_limit != null;
|
|
381
|
-
if (_isContent && !_pathsOnly && !_capped) {
|
|
382
|
-
for (const p of _pathsOf(a.path)) _next.add(p);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
_lastGrepContentPaths = _next.size ? _next : null;
|
|
386
|
-
}
|
|
387
|
-
// Detector A (fire): a file-scoped grep WITHOUT context last turn,
|
|
388
|
-
// then a read of that same file this turn. Fires ahead of the read-
|
|
389
|
-
// fragmentation / B detectors below; the generic level-1 nudge
|
|
390
|
-
// already deferred to it via _aWouldFire, while the level-2
|
|
391
|
-
// escalation above still outranks it. On pre-emption by an
|
|
392
|
-
// earlier hint, the pending set is kept (not cleared) so A can fire
|
|
393
|
-
// next turn.
|
|
394
|
-
let _aPending = false;
|
|
395
|
-
if (_lastFileScopedGrepNoCtxPaths) {
|
|
396
|
-
const _rp = [];
|
|
397
|
-
for (const c of _readCallsThisTurn) { for (const p of _pathsOf(c?.arguments?.path)) _rp.push(p); }
|
|
398
|
-
const _hit = _rp.find((p) => [..._lastFileScopedGrepNoCtxPaths].some((g) => _pathsMatch(p, g)));
|
|
399
|
-
if (_hit) {
|
|
400
|
-
if (!_hintFiredThisTurn) {
|
|
401
|
-
pushSystemReminder(`You grepped ${_baseName(_hit)} last turn then re-opened it — request context with -C on file-scoped grep so the match window is already on screen.`);
|
|
402
|
-
try {
|
|
403
|
-
appendAgentTrace({
|
|
404
|
-
sessionId,
|
|
405
|
-
iteration: iterations,
|
|
406
|
-
kind: 'steer',
|
|
407
|
-
payload: { tag: 'grep_nocontext_reread', file: _baseName(_hit) },
|
|
408
|
-
agent: sessionAgent || null,
|
|
409
|
-
});
|
|
410
|
-
} catch { /* best-effort */ }
|
|
411
|
-
_hintFiredThisTurn = true;
|
|
412
|
-
_lastFileScopedGrepNoCtxPaths = null;
|
|
413
|
-
} else {
|
|
414
|
-
_aPending = true; // pre-empted this turn — keep pending for next turn
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
// Detector A (seed): record this turn's file-scoped greps that ran
|
|
419
|
-
// WITHOUT context so a same-file read next turn triggers the fire
|
|
420
|
-
// block above. When A was pre-empted this turn (_aPending) the
|
|
421
|
-
// existing pending set is kept rather than cleared.
|
|
422
|
-
if (!_aPending) {
|
|
423
|
-
const _next = new Set();
|
|
424
|
-
for (const c of _grepCalls) {
|
|
425
|
-
const a = c?.arguments || {};
|
|
426
|
-
// Exact complement of the content detector's seed: skip greps
|
|
427
|
-
// that carried real context (positive -A/-B/-C) OR ran in
|
|
428
|
-
// content_with_context mode — those belong to that detector.
|
|
429
|
-
if (_hasGrepContext(a) || a.output_mode === 'content_with_context') continue;
|
|
430
|
-
for (const p of _pathsOf(a.path)) {
|
|
431
|
-
if (_isFileScopedPath(p)) _next.add(p);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
_lastFileScopedGrepNoCtxPaths = _next.size ? _next : null;
|
|
435
|
-
}
|
|
436
|
-
// Detector: read fragmentation — 2nd+ single-window read into the
|
|
437
|
-
// SAME file with offsets inside an 800-line span means the model is
|
|
438
|
-
// paging small windows across turns instead of reading one wider
|
|
439
|
-
// span (or batching {path,offset,limit}[] regions in one call).
|
|
440
|
-
{
|
|
441
|
-
for (const c of calls.filter((x) => x?.name === 'read')) {
|
|
442
|
-
const _byPath = new Map();
|
|
443
|
-
for (const w of _readWindows(c)) {
|
|
444
|
-
const k = _normPath(w.path);
|
|
445
|
-
if (!_byPath.has(k)) _byPath.set(k, []);
|
|
446
|
-
_byPath.get(k).push(w);
|
|
447
|
-
}
|
|
448
|
-
for (const [k, ws] of _byPath) {
|
|
449
|
-
if (ws.length !== 1) continue; // batched regions on one path — the good form
|
|
450
|
-
const off = ws[0].offset;
|
|
451
|
-
if (!Number.isFinite(off) || off <= 0) continue;
|
|
452
|
-
let rec = _readWindowsByPath.get(k);
|
|
453
|
-
if (!rec) {
|
|
454
|
-
rec = { offsets: [], fired: false };
|
|
455
|
-
_readWindowsByPath.set(k, rec);
|
|
456
|
-
if (_readWindowsByPath.size > 32) _readWindowsByPath.delete(_readWindowsByPath.keys().next().value);
|
|
457
|
-
}
|
|
458
|
-
rec.offsets.push(off);
|
|
459
|
-
if (!rec.fired && !_hintFiredThisTurn && rec.offsets.length >= 2
|
|
460
|
-
&& (Math.max(...rec.offsets) - Math.min(...rec.offsets)) <= 800) {
|
|
461
|
-
pushSystemReminder(`Windowed reads are fragmenting ${_baseName(k)} — stop paging small windows; read ONE wider span, or batch all needed spans as {path,offset,limit}[] regions in a single call.`);
|
|
462
|
-
rec.fired = true;
|
|
463
|
-
_hintFiredThisTurn = true;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
// Detector B: 3rd consecutive turn reading the SAME file (any
|
|
469
|
-
// window). Per-path streaks in a Map so multi-file turns keep
|
|
470
|
-
// independent counts ([A,B]->B->B fires on B). rel/abs spellings of
|
|
471
|
-
// one file collapse onto a single streak key; files not read this
|
|
472
|
-
// turn are pruned; a path is dropped after it fires.
|
|
473
|
-
{
|
|
474
|
-
// Resolve each read path to one streak key, merging rel/abs
|
|
475
|
-
// spellings against existing keys and keys already seen this turn.
|
|
476
|
-
const _touched = new Set();
|
|
477
|
-
for (const c of calls.filter((x) => x?.name === 'read')) {
|
|
478
|
-
for (const w of _readWindows(c)) {
|
|
479
|
-
let _key = null;
|
|
480
|
-
for (const k of _readStreaks.keys()) { if (_pathsMatch(k, w.path)) { _key = k; break; } }
|
|
481
|
-
if (!_key) { for (const k of _touched) { if (_pathsMatch(k, w.path)) { _key = k; break; } } }
|
|
482
|
-
_touched.add(_key || _canonPath(w.path));
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
// Prune streaks for files not read this turn (breaks consecutiveness).
|
|
486
|
-
for (const k of [..._readStreaks.keys()]) { if (!_touched.has(k)) _readStreaks.delete(k); }
|
|
487
|
-
// Increment each file read this turn (once per turn).
|
|
488
|
-
for (const k of _touched) {
|
|
489
|
-
_readStreaks.set(k, (_readStreaks.get(k) || 0) + 1);
|
|
490
|
-
if (_readStreaks.size > 32) _readStreaks.delete(_readStreaks.keys().next().value);
|
|
491
|
-
}
|
|
492
|
-
if (!_hintFiredThisTurn) {
|
|
493
|
-
for (const [k, cnt] of _readStreaks) {
|
|
494
|
-
if (cnt >= 3) {
|
|
495
|
-
pushSystemReminder(`3rd window into ${_baseName(k)} — take the remaining span in ONE wide read (offset+limit) instead of paging.`);
|
|
496
|
-
try {
|
|
497
|
-
appendAgentTrace({
|
|
498
|
-
sessionId,
|
|
499
|
-
iteration: iterations,
|
|
500
|
-
kind: 'steer',
|
|
501
|
-
payload: { tag: 'consecutive_same_file_read', file: _baseName(k) },
|
|
502
|
-
agent: sessionAgent || null,
|
|
503
|
-
});
|
|
504
|
-
} catch { /* best-effort */ }
|
|
505
|
-
_readStreaks.delete(k);
|
|
506
|
-
_hintFiredThisTurn = true;
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
151
|
// Detector: read-only shell (git status/log/diff --stat, ls/dir/cat/
|
|
513
152
|
// type/Get-ChildItem) inspects state the dedicated tools cover.
|
|
514
153
|
// Nudge toward them; never blocks execution.
|
|
@@ -519,19 +158,10 @@ export function createSteeringLadder(ctx) {
|
|
|
519
158
|
}
|
|
520
159
|
}
|
|
521
160
|
},
|
|
522
|
-
//
|
|
523
|
-
// across non-tool turns — that would fire level-2 (or the grep/read
|
|
524
|
-
// detectors) early on a worker that paused to synthesize text mid-run.
|
|
161
|
+
// A zero-tool turn must not bridge any cross-turn ladder streak.
|
|
525
162
|
resetAllReadOnlyStreak() {
|
|
526
163
|
_allReadOnlyStreak = 0;
|
|
527
164
|
_serialReadOnlyStreak = 0;
|
|
528
|
-
_sameGrepStreak = 0;
|
|
529
|
-
_sameGrepPrev = null;
|
|
530
|
-
_lastGrepContentPaths = null;
|
|
531
|
-
_identGrepStreak = 0;
|
|
532
|
-
_identGrepToken = null;
|
|
533
|
-
_lastFileScopedGrepNoCtxPaths = null;
|
|
534
|
-
_readStreaks.clear();
|
|
535
165
|
},
|
|
536
166
|
};
|
|
537
167
|
}
|
|
@@ -106,78 +106,3 @@ function _restoreCompactedBodies(tcVal, origVal, key) {
|
|
|
106
106
|
}
|
|
107
107
|
return tcVal;
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
// Marker prefix emitted by compactStoredToolArgString for every compacted
|
|
111
|
-
// body/long arg (`[mixdog compacted <key>: <N> chars, sha256:…]`). Both the
|
|
112
|
-
// body-only form (marker alone) and the long form (marker + head/tail preview)
|
|
113
|
-
// begin with this exact span.
|
|
114
|
-
const COMPACTED_MARKER_PREFIX = '[mixdog compacted ';
|
|
115
|
-
|
|
116
|
-
function _isCompactedPlaceholderString(v) {
|
|
117
|
-
return typeof v === 'string' && v.startsWith(COMPACTED_MARKER_PREFIX);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Recursively drop every key whose stored value is a compacted-placeholder
|
|
121
|
-
// string, at any depth (batch shapes like edits[].old_string carry nested
|
|
122
|
-
// compacted bodies too). Non-placeholder values are kept verbatim.
|
|
123
|
-
function _dropCompactedPlaceholders(val) {
|
|
124
|
-
if (Array.isArray(val)) return val.map((item) => _dropCompactedPlaceholders(item));
|
|
125
|
-
if (val && typeof val === 'object') {
|
|
126
|
-
const out = {};
|
|
127
|
-
for (const [k, v] of Object.entries(val)) {
|
|
128
|
-
if (_isCompactedPlaceholderString(v)) continue; // drop the key entirely
|
|
129
|
-
out[k] = _dropCompactedPlaceholders(v);
|
|
130
|
-
}
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
|
-
return val;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// A SUCCESSFUL tool call's compacted body/long arg (patch / old_string /
|
|
137
|
-
// new_string / content / rewrite / command / script) is never needed again —
|
|
138
|
-
// the edit already applied. compactToolCallsForHistory (run at push time,
|
|
139
|
-
// before the outcome is known) leaves a `[mixdog compacted …]` placeholder in
|
|
140
|
-
// its place. For a success that placeholder persists in the stored assistant
|
|
141
|
-
// tool_use and is transmitted back as a prior apply_patch INPUT, which the
|
|
142
|
-
// model copies verbatim as new patch args — caught by the patch guard, but only
|
|
143
|
-
// after a wasted turn. Drop those placeholder keys so no resubmittable
|
|
144
|
-
// placeholder body survives in history. Failed calls take the opposite path
|
|
145
|
-
// (restoreToolCallBodyForId expands to the full original); success and failure
|
|
146
|
-
// are mutually exclusive per call id, so this never races the restore path.
|
|
147
|
-
// Cache-safe: the caller runs this before the assistant message is transmitted.
|
|
148
|
-
export function dropCompactedBodyArgsForId(assistantMsg, callId) {
|
|
149
|
-
if (!assistantMsg || !Array.isArray(assistantMsg.toolCalls) || !callId) return;
|
|
150
|
-
const tc = assistantMsg.toolCalls.find((t) => t && t.id === callId);
|
|
151
|
-
if (!tc || !tc.arguments || typeof tc.arguments !== 'object') return;
|
|
152
|
-
tc.arguments = _dropCompactedPlaceholders(tc.arguments);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// PRE-SEND INVARIANT. The per-call paths above run during tool-batch
|
|
156
|
-
// processing: a SUCCESS drops its placeholder body (dropCompactedBodyArgsForId),
|
|
157
|
-
// a FAILURE restores the full original (restoreToolCallBodyForId). But those are
|
|
158
|
-
// gated per call id and several loop paths never reach them — dedup /
|
|
159
|
-
// repeat-failure early-continue, the iteration-cap refusal stub, or a call whose
|
|
160
|
-
// id never matched. Any of those can leave a `[mixdog compacted …]` placeholder
|
|
161
|
-
// sitting in a provider-visible assistant toolCall, which the model then copies
|
|
162
|
-
// back verbatim as new apply_patch input. Enforce the invariant once, right
|
|
163
|
-
// before provider.send: sweep EVERY assistant toolCall arguments tree and drop
|
|
164
|
-
// any placeholder-valued key at any depth (batch shapes like edits[].old_string
|
|
165
|
-
// carry nested compacted bodies too).
|
|
166
|
-
//
|
|
167
|
-
// This does NOT re-inline full bodies, so it never raises history token cost. A
|
|
168
|
-
// failed call whose body was restored upstream holds real patch content (it no
|
|
169
|
-
// longer starts with the compacted marker), so the sweep leaves it untouched —
|
|
170
|
-
// the full body stays available exactly where the failed call needs it. The
|
|
171
|
-
// downstream apply_patch guard (isCompactedPlaceholderPatch) remains as a
|
|
172
|
-
// backstop for any placeholder a model synthesizes from prose.
|
|
173
|
-
export function scrubCompactedPlaceholderToolCalls(messages) {
|
|
174
|
-
if (!Array.isArray(messages)) return messages;
|
|
175
|
-
for (const msg of messages) {
|
|
176
|
-
if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.toolCalls)) continue;
|
|
177
|
-
for (const tc of msg.toolCalls) {
|
|
178
|
-
if (!tc || !tc.arguments || typeof tc.arguments !== 'object') continue;
|
|
179
|
-
tc.arguments = _dropCompactedPlaceholders(tc.arguments);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return messages;
|
|
183
|
-
}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// strip any trailing/orphaned tool_use|tool_result so provider.send sees a
|
|
6
6
|
// valid transcript instead of leaking the 400 to the user.
|
|
7
7
|
import { sanitizeToolPairs } from '../context-utils.mjs';
|
|
8
|
-
import { scrubCompactedPlaceholderToolCalls } from './stored-tool-args.mjs';
|
|
9
8
|
|
|
10
9
|
// Transcript pairing guard. Anthropic 400-rejects when an assistant message
|
|
11
10
|
// ends with tool_use blocks and the next message isn't tool results for
|
|
@@ -98,9 +97,5 @@ export function repairTranscriptBeforeProviderSend(messages, sessionId = null) {
|
|
|
98
97
|
messages.push(...sanitized);
|
|
99
98
|
}
|
|
100
99
|
_ensureTranscriptPairing(messages, sessionId);
|
|
101
|
-
// Pre-send invariant: no provider-visible assistant toolCall may carry a
|
|
102
|
-
// compacted `[mixdog compacted …]` placeholder body that looks like
|
|
103
|
-
// submittable patch input. Runs after pairing repair, still before send.
|
|
104
|
-
scrubCompactedPlaceholderToolCalls(messages);
|
|
105
100
|
return messages;
|
|
106
101
|
}
|
|
@@ -28,6 +28,7 @@ import { _mergePendingMessageEntries, drainPendingMessages } from './pending-mes
|
|
|
28
28
|
import { persistIterationMetrics, applyAskTerminalUsageTotals } from './usage-metrics.mjs';
|
|
29
29
|
import {
|
|
30
30
|
updateSessionStage,
|
|
31
|
+
linkParentSignalToSession,
|
|
31
32
|
markSessionAskStart,
|
|
32
33
|
markSessionStreamDelta,
|
|
33
34
|
markSessionDone,
|
|
@@ -164,11 +165,22 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
164
165
|
normalizeStaleCompactingStage(preSession);
|
|
165
166
|
askGeneration = typeof preSession.generation === 'number' ? preSession.generation : 0;
|
|
166
167
|
const runtime = _touchRuntime(sessionId);
|
|
168
|
+
// Preserve any parent-abort link agent-dispatch established BEFORE we
|
|
169
|
+
// swap in a fresh controller: replacing runtime.controller drops the
|
|
170
|
+
// abort state, so an already/early-aborted parent signal (user ESC /
|
|
171
|
+
// owner abort landing during setup) would be lost and provider
|
|
172
|
+
// computation would run detached. Capture the linked signal, install the
|
|
173
|
+
// fresh controller, then re-cascade it — aborting the new controller
|
|
174
|
+
// immediately when the parent already fired, or re-arming the listener.
|
|
175
|
+
const _linkedParentSignal = runtime.parentAbortLink?.signal;
|
|
167
176
|
// Fresh controller per ask — the previous ask's controller may have aborted.
|
|
168
177
|
runtime.controller = createAbortController();
|
|
169
178
|
runtime.generation = askGeneration;
|
|
170
179
|
runtime.closed = false;
|
|
171
180
|
runtime.session = preSession;
|
|
181
|
+
if (_linkedParentSignal instanceof AbortSignal) {
|
|
182
|
+
linkParentSignalToSession(sessionId, _linkedParentSignal);
|
|
183
|
+
}
|
|
172
184
|
markSessionAskStart(sessionId);
|
|
173
185
|
// Preprocessing is inside try so provider-not-available / trim failures
|
|
174
186
|
// fall into the catch and mark the session as errored rather than
|
|
@@ -389,9 +401,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
389
401
|
updateSessionStage(sessionId, stage);
|
|
390
402
|
try { askOpts?.onStageChange?.(stage); } catch {}
|
|
391
403
|
},
|
|
392
|
-
onStreamDelta: () => {
|
|
393
|
-
markSessionStreamDelta(sessionId).catch(() => {});
|
|
394
|
-
|
|
404
|
+
onStreamDelta: (kind = 'semantic') => {
|
|
405
|
+
markSessionStreamDelta(sessionId, kind).catch(() => {});
|
|
406
|
+
// Raw transport is an internal health signal, not model
|
|
407
|
+
// progress. Preserve the public callback's historical
|
|
408
|
+
// semantic-only contract.
|
|
409
|
+
if (kind !== 'transport') {
|
|
410
|
+
try { askOpts?.onStreamDelta?.(kind); } catch {}
|
|
411
|
+
}
|
|
395
412
|
},
|
|
396
413
|
}),
|
|
397
414
|
);
|