mixdog 0.9.12 → 0.9.14
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 +1 -1
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +11 -7
- package/src/defaults/cycle3-review-prompt.md +14 -0
- package/src/defaults/memory-promote-prompt.md +9 -9
- package/src/lib/rules-builder.cjs +15 -11
- package/src/mixdog-session-runtime.mjs +10 -0
- package/src/rules/agent/00-common.md +5 -17
- package/src/rules/agent/00-core.md +21 -0
- package/src/rules/lead/lead-brief.md +7 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +39 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +0 -25
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +120 -3
- package/src/runtime/agent/orchestrator/agent-trace.mjs +43 -1
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +48 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +42 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +24 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +61 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +57 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +40 -4
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +0 -62
- package/src/runtime/agent/orchestrator/session/compact.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +12 -9
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +48 -157
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +6 -23
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +0 -13
- package/src/runtime/agent/orchestrator/session/loop.mjs +32 -77
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +33 -45
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +132 -11
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -6
- package/src/runtime/agent/orchestrator/session/manager.mjs +30 -15
- package/src/runtime/agent/orchestrator/stall-policy.mjs +16 -0
- package/src/runtime/channels/index.mjs +28 -1
- package/src/runtime/channels/lib/memory-client.mjs +200 -15
- package/src/runtime/memory/index.mjs +18 -13
- package/src/runtime/memory/lib/core-memory-store.mjs +108 -4
- package/src/runtime/memory/lib/cycle-scheduler.mjs +52 -18
- package/src/runtime/memory/lib/memory-cycle1.mjs +166 -23
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +37 -25
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +37 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +17 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +106 -6
- package/src/runtime/memory/lib/memory-embed.mjs +35 -1
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-handlers.mjs +1 -0
- package/src/standalone/agent-tool.mjs +89 -7
- package/src/tui/dist/index.mjs +203 -10
- package/src/tui/engine/tui-steering-persist.mjs +175 -0
- package/src/tui/engine.mjs +46 -2
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +2 -0
- package/src/workflows/sequential/WORKFLOW.md +2 -0
- package/src/workflows/solo/WORKFLOW.md +2 -0
|
@@ -39,68 +39,6 @@ const PRUNE_TAIL_TURNS = 2;
|
|
|
39
39
|
export const MIN_PRESERVE_RECENT_TOKENS = 2_000;
|
|
40
40
|
export const PRESERVED_FACTS_MAX_CHARS = 600;
|
|
41
41
|
|
|
42
|
-
// Count raw (unchunked) pending rows still present in a dump_session_roots
|
|
43
|
-
// payload. recall-fasttrack must keep cycle1-chunking until this reaches 0 so
|
|
44
|
-
// the injected root is the chunked summary, not the raw transcript tail.
|
|
45
|
-
export function countRawPendingRows(dumpText) {
|
|
46
|
-
const text = String(dumpText || '');
|
|
47
|
-
const matches = text.match(/(?:^|\n)# raw_pending\s+\d+\s+id=/gi);
|
|
48
|
-
return matches ? matches.length : 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Drain a single session's cycle1 in fixed window×concurrency units, looping
|
|
52
|
-
// until no raw rows remain (or a pass stops making progress / the deadline
|
|
53
|
-
// elapses). This replaces the previous single-pass cycle1 so large sessions
|
|
54
|
-
// get fully chunked before their root is injected into the compacted context.
|
|
55
|
-
export async function drainSessionCycle1(runTool, { sessionId, cycleArgs = {}, dumpArgs, maxPasses = 0, deadlineMs = 0 } = {}) {
|
|
56
|
-
if (typeof runTool !== 'function') throw new Error('drainSessionCycle1: runTool is required');
|
|
57
|
-
if (!sessionId) throw new Error('drainSessionCycle1: sessionId is required');
|
|
58
|
-
if (!dumpArgs) throw new Error('drainSessionCycle1: dumpArgs is required');
|
|
59
|
-
const startedAt = Date.now();
|
|
60
|
-
const hardPasses = Math.max(1, Number(maxPasses) || 50);
|
|
61
|
-
const lines = [];
|
|
62
|
-
let cycle1Error = null;
|
|
63
|
-
let recallText = await runTool('memory', dumpArgs);
|
|
64
|
-
let rawRemaining = countRawPendingRows(recallText);
|
|
65
|
-
let pass = 0;
|
|
66
|
-
while (rawRemaining > 0 && pass < hardPasses) {
|
|
67
|
-
if (deadlineMs > 0 && (Date.now() - startedAt) >= deadlineMs) break;
|
|
68
|
-
pass += 1;
|
|
69
|
-
const passDeadline = deadlineMs > 0
|
|
70
|
-
? Math.max(1, deadlineMs - (Date.now() - startedAt))
|
|
71
|
-
: 0;
|
|
72
|
-
let passText = '';
|
|
73
|
-
try {
|
|
74
|
-
passText = await runTool('memory', {
|
|
75
|
-
action: 'cycle1',
|
|
76
|
-
sessionId,
|
|
77
|
-
...cycleArgs,
|
|
78
|
-
...(passDeadline > 0 ? { _callerDeadlineMs: passDeadline } : {}),
|
|
79
|
-
});
|
|
80
|
-
} catch (err) {
|
|
81
|
-
cycle1Error = err?.message ? String(err.message) : String(err);
|
|
82
|
-
lines.push(`cycle1 pass=${pass} error=${cycle1Error}`);
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
if (passText) lines.push(`cycle1 pass=${pass}: ${String(passText).trim()}`);
|
|
86
|
-
recallText = await runTool('memory', dumpArgs);
|
|
87
|
-
const nextRaw = countRawPendingRows(recallText);
|
|
88
|
-
// No forward progress (raw not shrinking) — stop instead of spinning.
|
|
89
|
-
if (nextRaw >= rawRemaining) {
|
|
90
|
-
rawRemaining = nextRaw;
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
rawRemaining = nextRaw;
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
recallText,
|
|
97
|
-
cycle1Text: lines.join('\n'),
|
|
98
|
-
passes: pass,
|
|
99
|
-
rawRemaining,
|
|
100
|
-
error: cycle1Error,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
42
|
function preservedFactHints() {
|
|
105
43
|
return String(process.env.MIXDOG_COMPACT_FACT_HINTS || '')
|
|
106
44
|
.split(/[\n,;]+/u)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Completion-first loop guards: escalation ladder (level-2 steering),
|
|
2
|
-
// cross-turn identical read-only call dedup, and
|
|
2
|
+
// cross-turn identical read-only call dedup, and hard-cap refusal stubs.
|
|
3
3
|
// Pure string/signature helpers extracted from loop.mjs so the loop body only
|
|
4
4
|
// wires state + messages. No provider/manager coupling.
|
|
5
5
|
|
|
@@ -41,21 +41,24 @@ export function isEditProgressTool(name, isEager) {
|
|
|
41
41
|
// `readOnlyRole` swaps the edit-oriented directive for a report-oriented one:
|
|
42
42
|
// read-permission sessions (reviewer-style) cannot apply_patch, so telling
|
|
43
43
|
// them to edit is self-contradictory and pushes premature termination.
|
|
44
|
-
|
|
44
|
+
const LEVEL2_DIMINISHING_RETURNS = ' Further reads add cost, not evidence.';
|
|
45
|
+
|
|
46
|
+
export function level2SteerMessage(n, readOnlyRole = false, level2Fires = 1) {
|
|
47
|
+
const tail = Number(level2Fires) >= 2 ? LEVEL2_DIMINISHING_RETURNS : '';
|
|
45
48
|
if (readOnlyRole) {
|
|
46
|
-
return `<system-reminder>\
|
|
49
|
+
return `<system-reminder>\nEnough evidence gathered — report findings now; name any gaps.${tail}\n</system-reminder>`;
|
|
47
50
|
}
|
|
48
|
-
return `<system-reminder>\
|
|
51
|
+
return `<system-reminder>\nStop exploring — apply the edit you know, or report blocked with what's missing.${tail}\n</system-reminder>`;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
// Step 2 — cross-turn dedup stub. `stuck` appends the escalation tail at the
|
|
52
55
|
// 5th+ dedup stub in the session.
|
|
53
56
|
export function crossTurnDedupStub(name, firstIteration, stuck) {
|
|
54
|
-
let s = `[cross-turn-dedup]
|
|
55
|
-
if (stuck) s += `
|
|
57
|
+
let s = `[cross-turn-dedup] \`${name}\` already ran in iteration ${firstIteration}; result unchanged, already in context.`;
|
|
58
|
+
if (stuck) s += ` You appear stuck — use what you have or report blocked.`;
|
|
56
59
|
return s;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
export const
|
|
62
|
+
// Hard iteration-cap final turn: model may still emit tool calls after tools
|
|
63
|
+
// are stripped from the send; refuse without executing.
|
|
64
|
+
export const ITERATION_CAP_REFUSAL_STUB = `Iteration cap reached — tools disabled; reply with your final text only.`;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// command injection), or host input injection. Explicit name list (no imports)
|
|
14
14
|
// keeps this hot-path gate dependency-free; add new owner/channel tools here.
|
|
15
15
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
16
|
+
import { recursiveWrapperToolNameForPublicAgent } from '../manager/tool-resolution.mjs';
|
|
16
17
|
|
|
17
18
|
const WORKER_DENIED_TOOLS = new Set([
|
|
18
19
|
// session control-plane — unified into the single `agent` tool
|
|
@@ -34,6 +35,17 @@ function _preDispatchDeny(call, toolKind, sessionRef) {
|
|
|
34
35
|
if (_agentOwned && _controlPlaneTool) {
|
|
35
36
|
return `Error: control-plane tool "${name}" is Lead-only and not available to agent workers.`;
|
|
36
37
|
}
|
|
38
|
+
// Anti-recursion break, moved OFF the schema so the read-only tool bundle
|
|
39
|
+
// stays bit-identical across every read role (explore ships in the schema
|
|
40
|
+
// for all of them, incl. the explore agent itself — one cache group).
|
|
41
|
+
// A public agent that IS a recursive wrapper (e.g. explore) must not call
|
|
42
|
+
// its own wrapper tool; reject it here at call time instead.
|
|
43
|
+
if (_agentOwned) {
|
|
44
|
+
const selfWrapper = recursiveWrapperToolNameForPublicAgent(sessionRef?.agent || null);
|
|
45
|
+
if (selfWrapper && name.toLowerCase() === selfWrapper.toLowerCase()) {
|
|
46
|
+
return `Error: tool "${name}" is not available inside agent "${sessionRef.agent}" (would recurse into its own wrapper). Return the answer directly.`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
const noToolAgent = sessionRef?.agent === 'cycle1-agent' || sessionRef?.agent === 'cycle2-agent';
|
|
38
50
|
if (noToolAgent) {
|
|
39
51
|
return `Error: tool "${name}" is not available in agent "${sessionRef.agent}". Re-emit the answer as pipe-separated text per the agent's output format (first character a digit, NO tool_use blocks, NO JSON, NO prose, NO apology).`;
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
// Recall-fasttrack compaction pipeline
|
|
1
|
+
// Recall-fasttrack compaction pipeline (digest injection).
|
|
2
2
|
// Hydrates the session transcript into the memory pipeline (ingest_session),
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// then injects a small newest-first digest + recall pointer into the
|
|
4
|
+
// compacted messages. The former full-dump path (dump_session_roots +
|
|
5
|
+
// synchronous cycle1 drain) was removed 2026-07: the drain ran memory-
|
|
6
|
+
// pipeline LLM chunking calls inside the compaction (11.9s of a measured
|
|
7
|
+
// 12.9s compact) and still left raw rows behind; background cycle1 already
|
|
8
|
+
// chunks ingested rows on its own schedule, and recall serves the rest.
|
|
8
9
|
import { createHash } from 'crypto';
|
|
9
10
|
import { executeInternalTool } from '../../internal-tools.mjs';
|
|
10
|
-
import { loadConfig as loadOrchestratorConfig } from '../../config.mjs';
|
|
11
11
|
import {
|
|
12
12
|
recallFastTrackCompactMessages,
|
|
13
|
-
CONTEXT_SHARE_RATIO,
|
|
14
|
-
RECALL_TOKEN_CAP_FLOOR_TOKENS,
|
|
15
|
-
drainSessionCycle1,
|
|
16
|
-
countRawPendingRows,
|
|
17
13
|
} from '../compact.mjs';
|
|
18
14
|
import {
|
|
19
15
|
compactDiagnosticError,
|
|
@@ -23,23 +19,23 @@ import {
|
|
|
23
19
|
import { positiveTokenInt } from './env.mjs';
|
|
24
20
|
import { TOOL_OUTPUT_MAX_BYTES } from '../../tools/builtin/tool-output-limit.mjs';
|
|
25
21
|
|
|
26
|
-
// ── Digest
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// modes), and raw rows are embedded synchronously at ingest, so recall serves
|
|
33
|
-
// everything the big injection used to carry.
|
|
22
|
+
// ── Digest injection ────────────────────────────────────────────────────────
|
|
23
|
+
// Inject a small newest-first digest plus an instruction telling the model to
|
|
24
|
+
// pull details lazily via recall(sessionId/query/period). The memory DB holds
|
|
25
|
+
// the full session (ingest_session below), and raw rows are embedded
|
|
26
|
+
// synchronously at ingest, so recall serves everything the old full-dump
|
|
27
|
+
// injection used to carry.
|
|
34
28
|
// Default digest cap = the SHARED tool-output limit (TOOL_OUTPUT_MAX_BYTES,
|
|
35
29
|
// 50KB default, env MIXDOG_TOOL_OUTPUT_MAX_BYTES) — the digest injection is
|
|
36
30
|
// budgeted like any other tool result, not a special context share.
|
|
37
31
|
// compaction.recallDigestMaxKb still overrides per-session.
|
|
38
|
-
const DIGEST_DEFAULT_MAX_KB = Math.max(1, Math.floor(TOOL_OUTPUT_MAX_BYTES / 1024));
|
|
32
|
+
export const DIGEST_DEFAULT_MAX_KB = Math.max(1, Math.floor(TOOL_OUTPUT_MAX_BYTES / 1024));
|
|
39
33
|
|
|
40
34
|
// Byte-capped line-boundary truncation. Digest source is newest-first, so
|
|
41
35
|
// keeping the HEAD keeps the newest turns.
|
|
42
|
-
|
|
36
|
+
// Exported for manager/compaction-runner.mjs (manual//clear digest path) so
|
|
37
|
+
// both digest producers share one cap implementation.
|
|
38
|
+
export function truncateToKb(text, maxKb) {
|
|
43
39
|
const maxBytes = Math.max(1, maxKb) * 1024;
|
|
44
40
|
const s = String(text || '');
|
|
45
41
|
if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;
|
|
@@ -120,156 +116,51 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
120
116
|
} finally {
|
|
121
117
|
diagnostics.ingestMs = Date.now() - t0;
|
|
122
118
|
}
|
|
123
|
-
// ── Digest
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
const browsed = await executeInternalTool('memory', {
|
|
134
|
-
action: 'search',
|
|
135
|
-
sessionId,
|
|
136
|
-
limit: positiveTokenInt(sessionRef?.compaction?.recallDigestLimit) || 30,
|
|
137
|
-
includeMembers: true,
|
|
138
|
-
}, callerCtx);
|
|
139
|
-
digestBody = typeof browsed === 'string' ? browsed : String(browsed?.text ?? browsed ?? '');
|
|
140
|
-
} catch (err) {
|
|
141
|
-
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
142
|
-
try { process.stderr.write(`[loop] recall-digest browse failed (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
143
|
-
}
|
|
144
|
-
diagnostics.initialDumpMs = Date.now() - t0;
|
|
145
|
-
diagnostics.cycle1Skipped = true;
|
|
146
|
-
diagnostics.cycle1SkipReason = 'digest mode';
|
|
147
|
-
diagnostics.cycle1Passes = 0;
|
|
148
|
-
const digestText = buildRecallDigestText(sessionId, digestBody, digestMaxKb);
|
|
149
|
-
diagnostics.finalRecallChars = digestText.length;
|
|
150
|
-
diagnostics.finalRecallBytes = compactByteLength(digestText);
|
|
151
|
-
const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
|
|
152
|
-
reserveTokens: compactPolicy.reserveTokens,
|
|
153
|
-
force: true,
|
|
154
|
-
recallText: digestText,
|
|
155
|
-
query,
|
|
156
|
-
querySha,
|
|
157
|
-
allowEmptyRecall: true,
|
|
158
|
-
tailTurns: compactPolicy.tailTurns,
|
|
159
|
-
keepTokens: compactPolicy.keepTokens,
|
|
160
|
-
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
161
|
-
});
|
|
162
|
-
diagnostics.totalMs = Date.now() - startedAt;
|
|
163
|
-
if (result && typeof result === 'object') {
|
|
164
|
-
result.diagnostics = { ...(result.diagnostics || {}), pipeline: { ...diagnostics, digestMode: true } };
|
|
165
|
-
}
|
|
166
|
-
compactDebugLog('recall-digest pipeline', diagnostics);
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
const dumpArgs = {
|
|
170
|
-
action: 'dump_session_roots',
|
|
171
|
-
sessionId,
|
|
172
|
-
includeRaw: true,
|
|
173
|
-
limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
|
|
174
|
-
};
|
|
175
|
-
const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
|
|
119
|
+
// ── Digest injection (the only mode) ──────────────────────────────────
|
|
120
|
+
// The old full-dump path (dump_session_roots + synchronous cycle1 drain)
|
|
121
|
+
// is gone: the drain ran memory-pipeline LLM chunking calls INSIDE the
|
|
122
|
+
// compaction (measured 11.9s of a 12.9s compact) and still often left
|
|
123
|
+
// rawRemaining>0. Instead inject a small newest-first digest plus a
|
|
124
|
+
// recall pointer; ingest_session above already put the full transcript
|
|
125
|
+
// in the memory DB, and background cycle1 chunks it on its own schedule.
|
|
126
|
+
const digestMaxKb = positiveTokenInt(sessionRef?.compaction?.recallDigestMaxKb) || DIGEST_DEFAULT_MAX_KB;
|
|
127
|
+
let digestBody = '';
|
|
176
128
|
t0 = Date.now();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
try { recapOn = loadOrchestratorConfig({ secrets: false })?.recap?.enabled !== false; } catch { /* default on */ }
|
|
190
|
-
if (hasRawRows && !recapOn) {
|
|
191
|
-
diagnostics.cycle1Skipped = true;
|
|
192
|
-
diagnostics.cycle1SkipReason = 'recap disabled';
|
|
193
|
-
diagnostics.cycle1Passes = 0;
|
|
194
|
-
diagnostics.cycle1RawRemaining = countRawPendingRows(recallText);
|
|
195
|
-
cycle1Text = 'cycle1: skipped (recap disabled — raw transcript lines kept as-is)';
|
|
196
|
-
} else if (hasRawRows) {
|
|
197
|
-
t0 = Date.now();
|
|
198
|
-
try {
|
|
199
|
-
// Drain this session's cycle1 in window×concurrency units until no
|
|
200
|
-
// raw rows remain, so the injected root is fully chunked rather than
|
|
201
|
-
// carrying the unprocessed transcript tail (single-pass left raw in).
|
|
202
|
-
const drained = await drainSessionCycle1(runTool, {
|
|
203
|
-
sessionId,
|
|
204
|
-
dumpArgs,
|
|
205
|
-
deadlineMs: positiveTokenInt(sessionRef?.compaction?.recallCycle1DeadlineMs) || 120_000,
|
|
206
|
-
maxPasses: positiveTokenInt(sessionRef?.compaction?.recallCycle1MaxPasses) || 0,
|
|
207
|
-
cycleArgs: {
|
|
208
|
-
min_batch: 1,
|
|
209
|
-
session_cap: 1,
|
|
210
|
-
batch_size: positiveTokenInt(sessionRef?.compaction?.recallCycle1BatchSize) || 100,
|
|
211
|
-
rows_per_session: positiveTokenInt(sessionRef?.compaction?.recallRowsPerSession) || 100,
|
|
212
|
-
window_size: positiveTokenInt(sessionRef?.compaction?.recallWindowSize) || 20,
|
|
213
|
-
concurrency: positiveTokenInt(sessionRef?.compaction?.recallConcurrency) || 5,
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
recallText = drained.recallText;
|
|
217
|
-
cycle1Text = drained.cycle1Text;
|
|
218
|
-
diagnostics.cycle1Passes = drained.passes;
|
|
219
|
-
diagnostics.cycle1RawRemaining = drained.rawRemaining;
|
|
220
|
-
diagnostics.cycle1TextBytes = compactByteLength(cycle1Text);
|
|
221
|
-
if (drained.error) {
|
|
222
|
-
diagnostics.cycle1Error = drained.error;
|
|
223
|
-
try { process.stderr.write(`[loop] recall-fasttrack cycle1 error (sess=${sessionId || 'unknown'}): ${drained.error}\n`); } catch {}
|
|
224
|
-
}
|
|
225
|
-
if (drained.rawRemaining > 0) {
|
|
226
|
-
try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
230
|
-
try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
231
|
-
} finally {
|
|
232
|
-
diagnostics.cycle1Ms = Date.now() - t0;
|
|
233
|
-
}
|
|
234
|
-
} else {
|
|
235
|
-
diagnostics.cycle1Skipped = true;
|
|
236
|
-
diagnostics.cycle1SkipReason = 'session chunks already hydrated';
|
|
237
|
-
diagnostics.cycle1Passes = 0;
|
|
238
|
-
diagnostics.cycle1RawRemaining = 0;
|
|
239
|
-
cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
|
|
129
|
+
try {
|
|
130
|
+
const browsed = await executeInternalTool('memory', {
|
|
131
|
+
action: 'search',
|
|
132
|
+
sessionId,
|
|
133
|
+
limit: positiveTokenInt(sessionRef?.compaction?.recallDigestLimit) || 30,
|
|
134
|
+
includeMembers: true,
|
|
135
|
+
includeRaw: true,
|
|
136
|
+
}, callerCtx);
|
|
137
|
+
digestBody = typeof browsed === 'string' ? browsed : String(browsed?.text ?? browsed ?? '');
|
|
138
|
+
} catch (err) {
|
|
139
|
+
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
140
|
+
try { process.stderr.write(`[loop] recall-digest browse failed (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
240
141
|
}
|
|
241
|
-
|
|
242
|
-
diagnostics.
|
|
243
|
-
diagnostics.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// (compact-policy.mjs COMPACT_TARGET_RATIO). Omitted when contextWindow is
|
|
249
|
-
// unknown/0 so current no-cap behavior is preserved.
|
|
250
|
-
const _recallCapWindow = Number(compactPolicy.contextWindow) || 0;
|
|
251
|
-
const recallTokenCap = _recallCapWindow > 0
|
|
252
|
-
? Math.max(RECALL_TOKEN_CAP_FLOOR_TOKENS, Math.floor(_recallCapWindow * CONTEXT_SHARE_RATIO))
|
|
253
|
-
: undefined;
|
|
142
|
+
diagnostics.initialDumpMs = Date.now() - t0;
|
|
143
|
+
diagnostics.cycle1Skipped = true;
|
|
144
|
+
diagnostics.cycle1SkipReason = 'digest mode';
|
|
145
|
+
diagnostics.cycle1Passes = 0;
|
|
146
|
+
const digestText = buildRecallDigestText(sessionId, digestBody, digestMaxKb);
|
|
147
|
+
diagnostics.finalRecallChars = digestText.length;
|
|
148
|
+
diagnostics.finalRecallBytes = compactByteLength(digestText);
|
|
254
149
|
const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
|
|
255
150
|
reserveTokens: compactPolicy.reserveTokens,
|
|
256
151
|
force: true,
|
|
257
|
-
recallText:
|
|
152
|
+
recallText: digestText,
|
|
258
153
|
query,
|
|
259
154
|
querySha,
|
|
260
155
|
allowEmptyRecall: true,
|
|
261
156
|
tailTurns: compactPolicy.tailTurns,
|
|
262
157
|
keepTokens: compactPolicy.keepTokens,
|
|
263
158
|
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
264
|
-
recallTokenCap,
|
|
265
159
|
});
|
|
266
160
|
diagnostics.totalMs = Date.now() - startedAt;
|
|
267
161
|
if (result && typeof result === 'object') {
|
|
268
|
-
result.diagnostics = {
|
|
269
|
-
...(result.diagnostics || {}),
|
|
270
|
-
pipeline: diagnostics,
|
|
271
|
-
};
|
|
162
|
+
result.diagnostics = { ...(result.diagnostics || {}), pipeline: { ...diagnostics, digestMode: true } };
|
|
272
163
|
}
|
|
273
|
-
compactDebugLog('recall-
|
|
164
|
+
compactDebugLog('recall-digest pipeline', diagnostics);
|
|
274
165
|
return result;
|
|
275
166
|
}
|
|
@@ -8,14 +8,10 @@ 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
|
-
// Consecutive ignored level-2 steers (zero edits) that force the wrap-up early.
|
|
12
|
-
export const EARLY_SOFT_CAP_LEVEL2_FIRES = 3;
|
|
13
|
-
|
|
14
11
|
// Build the completion-first steering-ladder controller. `ctx` supplies live
|
|
15
12
|
// accessors so every read reflects the loop's current mutable state:
|
|
16
13
|
// - messages, sessionId, sessionAgent, tools (stable refs/values)
|
|
17
14
|
// - getIterations() (current iteration)
|
|
18
|
-
// - softCapEnabled (constant per loop)
|
|
19
15
|
// - getEditCount() (mutated by the loop)
|
|
20
16
|
// - pushSystemReminder(text) → push a meta:'hook' user message
|
|
21
17
|
// - pushUserMessage(msg) → push a raw user message (level-2 latch text)
|
|
@@ -25,14 +21,13 @@ export function createSteeringLadder(ctx) {
|
|
|
25
21
|
sessionAgent,
|
|
26
22
|
tools,
|
|
27
23
|
getIterations,
|
|
28
|
-
softCapEnabled,
|
|
29
24
|
getEditCount,
|
|
30
25
|
} = ctx;
|
|
31
26
|
const pushSystemReminder = ctx.pushSystemReminder;
|
|
32
27
|
const pushUserMessage = ctx.pushUserMessage;
|
|
33
28
|
// Permission-based role detection (agent names are user-definable):
|
|
34
29
|
// read-permission sessions legitimately never edit, so they get the
|
|
35
|
-
// report-oriented level-2 text
|
|
30
|
+
// report-oriented level-2 text.
|
|
36
31
|
const readOnlyRole = ctx.readOnlyRole === true;
|
|
37
32
|
|
|
38
33
|
// Step 1: escalation ladder. _level1FireCount is CUMULATIVE (never reset)
|
|
@@ -54,8 +49,6 @@ export function createSteeringLadder(ctx) {
|
|
|
54
49
|
// grep on that path; any other tool/path resets it.
|
|
55
50
|
let _sameFileGrepStreak = 0;
|
|
56
51
|
let _sameFileGrepPath = null;
|
|
57
|
-
// Early soft-cap: N ignored level-2 steers with zero edits forces the
|
|
58
|
-
// wrap-up early; any edit disarms it (level-2 requires editCount === 0).
|
|
59
52
|
let _level2FireCount = 0;
|
|
60
53
|
|
|
61
54
|
// Level-2 steering emitter shared by both ladder paths (single-call
|
|
@@ -65,10 +58,7 @@ export function createSteeringLadder(ctx) {
|
|
|
65
58
|
const iterations = getIterations();
|
|
66
59
|
_level2LatchAtIteration = iterations;
|
|
67
60
|
_level2FireCount += 1;
|
|
68
|
-
|
|
69
|
-
// next loop head supersedes the level-2 text — skip the redundant hint.
|
|
70
|
-
const _armsEarlyCap = !readOnlyRole && softCapEnabled && _level2FireCount >= EARLY_SOFT_CAP_LEVEL2_FIRES && getEditCount() === 0;
|
|
71
|
-
if (!_armsEarlyCap) pushUserMessage({ role: 'user', content: level2SteerMessage(_level1FireCount, readOnlyRole), meta: 'hook' });
|
|
61
|
+
pushUserMessage({ role: 'user', content: level2SteerMessage(_level1FireCount, readOnlyRole, _level2FireCount), meta: 'hook' });
|
|
72
62
|
try {
|
|
73
63
|
appendAgentTrace({
|
|
74
64
|
sessionId,
|
|
@@ -81,21 +71,14 @@ export function createSteeringLadder(ctx) {
|
|
|
81
71
|
};
|
|
82
72
|
|
|
83
73
|
return {
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
earlySoftCapArmed() {
|
|
87
|
-
return !readOnlyRole && _level2FireCount >= EARLY_SOFT_CAP_LEVEL2_FIRES && getEditCount() === 0;
|
|
88
|
-
},
|
|
89
|
-
// Post-batch steering hint gate. `hintAlreadyFired` seeds the once-per-
|
|
90
|
-
// turn latch (soft-cap active suppresses all hints). Returns nothing;
|
|
74
|
+
// Post-batch steering hint gate. `hintAlreadyFired` seeds the once-per-turn
|
|
75
|
+
// latch. Returns nothing;
|
|
91
76
|
// pushes at most one steering message via the ctx push callbacks.
|
|
92
77
|
emitPostBatchSteering(calls, hintAlreadyFired) {
|
|
93
78
|
const iterations = getIterations();
|
|
94
79
|
const editCount = getEditCount();
|
|
95
|
-
// Steering hint gate: at most ONE hint per turn (priority:
|
|
96
|
-
// level-2 > same-file grep > level-1)
|
|
97
|
-
// wrap-up is active — its "text only" directive must not share a send
|
|
98
|
-
// with a "keep exploring" hint.
|
|
80
|
+
// Steering hint gate: at most ONE hint per turn (priority:
|
|
81
|
+
// level-2 > same-file grep > level-1).
|
|
99
82
|
let _hintFiredThisTurn = hintAlreadyFired;
|
|
100
83
|
// Missed-parallelism steering: 3+ consecutive turns of a single
|
|
101
84
|
// read-only tool call suggest the model isn't batching independent
|
|
@@ -17,8 +17,6 @@ export const INCOMPLETE_STOP_REASONS = new Set([
|
|
|
17
17
|
// branch in agentLoop does (trimmed string content, or any reasoning content).
|
|
18
18
|
export function classifyTerminationReason(response, {
|
|
19
19
|
terminatedByCap,
|
|
20
|
-
terminatedBySoftCap,
|
|
21
|
-
softCapActive,
|
|
22
20
|
sessionAgent,
|
|
23
21
|
} = {}) {
|
|
24
22
|
const _finalHasContent = (typeof response?.content === 'string' && response.content.trim().length > 0)
|
|
@@ -31,17 +29,6 @@ export function classifyTerminationReason(response, {
|
|
|
31
29
|
// on its own contract.
|
|
32
30
|
return 'iteration_cap';
|
|
33
31
|
}
|
|
34
|
-
if (terminatedBySoftCap || softCapActive) {
|
|
35
|
-
// Worker soft-cap wrap-up path: non-lead session hit the exploration
|
|
36
|
-
// budget and was steered to a text-only finish. Distinct from the hard
|
|
37
|
-
// cap (iteration_cap) — this is an expected, budget-driven termination,
|
|
38
|
-
// not a runaway. Covers BOTH the grace-exhausted path
|
|
39
|
-
// (terminatedBySoftCap) AND the compliant path where the model emitted
|
|
40
|
-
// the final text with no more tool calls while the soft cap was active
|
|
41
|
-
// (loop ends via the no-tool-call break, so terminatedBySoftCap stays
|
|
42
|
-
// false but softCapActive is still true).
|
|
43
|
-
return 'soft_cap_wrapup';
|
|
44
|
-
}
|
|
45
32
|
if (!_finalHasContent && _finalIncompleteStop) {
|
|
46
33
|
// Cut short mid-synthesis (token cap / provider pause). Real problem
|
|
47
34
|
// for hidden agents too.
|