mixdog 0.9.19 → 0.9.21
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/LICENSE +21 -0
- package/README.md +100 -34
- package/package.json +3 -2
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/tool-smoke.mjs +150 -45
- package/src/defaults/skills/setup/SKILL.md +327 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +777 -0
- package/src/runtime/memory/index.mjs +163 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +811 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
- package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/memory.mjs +39 -0
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/shared/atomic-file.mjs +138 -80
- package/src/runtime/shared/child-guardian.mjs +61 -3
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2104 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +67 -7
- package/src/standalone/folder-dialog.mjs +4 -1
- package/src/standalone/memory-runtime-proxy.mjs +154 -17
- package/src/standalone/seeds.mjs +28 -1
- package/src/tui/App.jsx +40 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/doctor.mjs +175 -0
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +2851 -2162
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +564 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- package/src/workflows/sequential/WORKFLOW.md +0 -51
|
@@ -88,6 +88,12 @@ function indexLiveTurns(live) {
|
|
|
88
88
|
function splitTurnStartIndexForBudget(turn, budget) {
|
|
89
89
|
const { start, end, messages } = turn;
|
|
90
90
|
for (let i = 0; i < messages.length; i += 1) {
|
|
91
|
+
// A tool result must stay paired with its preceding assistant tool_call,
|
|
92
|
+
// so the preserved tail suffix can never begin at one (mirrors
|
|
93
|
+
// findValidCutIndices). Skip tool-result boundaries; if no non-tool
|
|
94
|
+
// suffix fits the budget, fall through to `end` so the caller keeps the
|
|
95
|
+
// whole turn in the head for summarization instead of orphaning it.
|
|
96
|
+
if (messages[i]?.role === 'tool') continue;
|
|
91
97
|
const suffixStart = start + i;
|
|
92
98
|
const suffix = messages.slice(i);
|
|
93
99
|
if (suffix.length > 0 && estimateMessagesTokens(suffix) <= budget) {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Eager tool-dispatch controller, extracted from agent-loop.mjs. Owns the
|
|
2
|
+
// per-turn pending promise map, the intra-turn in-flight signature set, and
|
|
3
|
+
// the mutation epoch. Read-only tool calls start executing the instant the
|
|
4
|
+
// provider streams a tool_use event so execution overlaps the remaining SSE
|
|
5
|
+
// parse; writes/unknown tools wait for the serial batch loop. Behavior
|
|
6
|
+
// identical to the inline closures it replaced.
|
|
7
|
+
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
8
|
+
import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
|
|
9
|
+
import { _intraTurnSig } from './loop/tool-classify.mjs';
|
|
10
|
+
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
11
|
+
import { executeTool } from './loop/tool-exec.mjs';
|
|
12
|
+
import { crossTurnSignature } from './loop/completion-guards.mjs';
|
|
13
|
+
import { getToolKind, isEagerDispatchable } from './loop/tool-helpers.mjs';
|
|
14
|
+
|
|
15
|
+
export function createEagerDispatcher({
|
|
16
|
+
tools, cwd, sessionId, sessionRef, signal, opts,
|
|
17
|
+
crossTurnCalls, getIterations, getNextIteration, repeatFailLimit,
|
|
18
|
+
}) {
|
|
19
|
+
const pending = new Map();
|
|
20
|
+
// Streaming-time intra-turn dedup. When the LLM emits two
|
|
21
|
+
// tool_use blocks with identical (name, args) signatures in
|
|
22
|
+
// sequence, the provider's onToolCall fires for both BEFORE
|
|
23
|
+
// the iter for-body runs, so the batch-level pre-pass would be
|
|
24
|
+
// too late to prevent the eager dispatch of the second one.
|
|
25
|
+
// Track signatures of in-flight eager calls and skip starting a
|
|
26
|
+
// second one for the same sig. The duplicate's executeTool is
|
|
27
|
+
// never invoked; the for-body's pre-pass marks it as a duplicate
|
|
28
|
+
// and emits a stub tool_result. The sig is NOT cleared when the
|
|
29
|
+
// eager promise settles (see finally below): a streaming onToolCall
|
|
30
|
+
// can deliver a same-turn identical call AFTER the first promise
|
|
31
|
+
// settles but BEFORE the deferred cache set (:1256), and the static
|
|
32
|
+
// pre-pass (:909) only runs after send() returns — so clearing the
|
|
33
|
+
// sig on settle would let that second streaming eager call
|
|
34
|
+
// re-execute. A fresh Map() is created per turn, so the sig set
|
|
35
|
+
// resets at the turn boundary without leaking across getIterations().
|
|
36
|
+
const _eagerInFlightSigs = new Map();
|
|
37
|
+
const epoch = { mutation: 0 };
|
|
38
|
+
const startEagerTool = (call) => {
|
|
39
|
+
if (!call?.id || pending.has(call.id) || !isEagerDispatchable(call.name, tools)) return null;
|
|
40
|
+
// Never eager-execute a call whose arguments failed to parse
|
|
41
|
+
// (invalid-args marker). It has no usable arguments; the serial
|
|
42
|
+
// body handles it via the invalid-args feedback path.
|
|
43
|
+
if (isInvalidToolArgsMarker(call.arguments)) return null;
|
|
44
|
+
const _sig = _intraTurnSig(call.name, call.arguments);
|
|
45
|
+
if (_eagerInFlightSigs.has(_sig)) return null;
|
|
46
|
+
// Repeat-failure guard also gates eager dispatch (reviewer-flagged):
|
|
47
|
+
// streaming onToolCall / startEagerRun would otherwise re-run an
|
|
48
|
+
// identical read-only call that already failed repeatFailLimit
|
|
49
|
+
// times before the serial for-body guard runs. Returning null here
|
|
50
|
+
// lets the serial body push the [repeat-failure-guard] stub.
|
|
51
|
+
{
|
|
52
|
+
const _rfg = sessionRef?._repeatFailGuard;
|
|
53
|
+
if (_rfg && _rfg.sig === _sig && _rfg.count >= repeatFailLimit) return null;
|
|
54
|
+
}
|
|
55
|
+
// Cross-turn dedup also gates eager dispatch (mirror of the
|
|
56
|
+
// repeat-failure guard above): a read-only call whose (name,args)
|
|
57
|
+
// signature already ran in an EARLIER turn must NOT be eagerly
|
|
58
|
+
// re-executed — the serial for-body pushes the [cross-turn-dedup]
|
|
59
|
+
// stub instead. Without this gate startEagerRun/onToolCall would
|
|
60
|
+
// re-run the call before the serial dedup check ever sees it.
|
|
61
|
+
{
|
|
62
|
+
const _ctSig = crossTurnSignature(call.name, call.arguments);
|
|
63
|
+
const _prior = crossTurnCalls.get(_ctSig);
|
|
64
|
+
if (_prior && _prior.firstIteration < getIterations()) return null;
|
|
65
|
+
}
|
|
66
|
+
const toolKind = getToolKind(call.name);
|
|
67
|
+
// Shared pre-dispatch deny: identical predicate runs in the
|
|
68
|
+
// serial path below. If any role/permission guard would reject
|
|
69
|
+
// this call there, never start it eagerly here.
|
|
70
|
+
if (preDispatchDenyForSession(sessionRef, call, toolKind) !== null) return null;
|
|
71
|
+
const entry = { startedAt: Date.now(), endedAt: null, mutationEpoch: epoch.mutation };
|
|
72
|
+
_eagerInFlightSigs.set(_sig, call.id);
|
|
73
|
+
entry.promise = (async () => {
|
|
74
|
+
try {
|
|
75
|
+
return { ok: true, value: await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: getNextIteration() }) };
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return { ok: false, error };
|
|
78
|
+
}
|
|
79
|
+
})()
|
|
80
|
+
.then((settled) => {
|
|
81
|
+
entry.endedAt = Date.now();
|
|
82
|
+
// EARLY UI-ONLY NOTIFY (completion-order, NOT history).
|
|
83
|
+
// The serial result-collection loop below `await`s each
|
|
84
|
+
// eager promise strictly in CALL order, so a fast call[1]
|
|
85
|
+
// that settles before a slow call[0] cannot surface its
|
|
86
|
+
// tool card completion until call[0] resolves. Fire
|
|
87
|
+
// onToolResult here — the instant THIS eager tool settles —
|
|
88
|
+
// so parallel cards complete independently in the order they
|
|
89
|
+
// actually finish.
|
|
90
|
+
//
|
|
91
|
+
// This message is NOT pushed into `messages`: provider
|
|
92
|
+
// history ordering stays exactly call-order. The serial loop
|
|
93
|
+
// still builds the REAL tool_result and pushes it via
|
|
94
|
+
// pushToolResultMessage (which fires onToolResult AGAIN for
|
|
95
|
+
// the same toolCallId in call order — the TUI dedupes by id,
|
|
96
|
+
// so the duplicate notify is harmless). __earlyNotify marks
|
|
97
|
+
// this as the pre-history, UI-only signal.
|
|
98
|
+
//
|
|
99
|
+
// Only genuinely-executed eager promises reach here:
|
|
100
|
+
// startEagerTool never creates an entry for dedup /
|
|
101
|
+
// repeat-failure-guard / pre-dispatch-deny / invalid-args
|
|
102
|
+
// calls (they return null above), so those `continue`-before-
|
|
103
|
+
// execution stub paths can never early-notify (contract #5).
|
|
104
|
+
try {
|
|
105
|
+
// UI-only: surface the model-VISIBLE result (envelope
|
|
106
|
+
// stub for envelope returns), never the envelope object
|
|
107
|
+
// or its injected newMessages body — no [object Object],
|
|
108
|
+
// no full skill body in the tool card.
|
|
109
|
+
const _earlyVisible = settled && settled.ok
|
|
110
|
+
? normalizeToolEnvelope(settled.value).result
|
|
111
|
+
: null;
|
|
112
|
+
const _earlyContent = settled && settled.ok
|
|
113
|
+
? (typeof _earlyVisible === 'string'
|
|
114
|
+
? _earlyVisible
|
|
115
|
+
: (_earlyVisible == null ? '' : String(_earlyVisible)))
|
|
116
|
+
: `Error: ${settled && settled.error instanceof Error ? settled.error.message : String(settled && settled.error)}`;
|
|
117
|
+
opts.onToolResult?.({
|
|
118
|
+
role: 'tool',
|
|
119
|
+
toolCallId: call.id,
|
|
120
|
+
content: _earlyContent,
|
|
121
|
+
isError: !(settled && settled.ok),
|
|
122
|
+
__earlyNotify: true,
|
|
123
|
+
});
|
|
124
|
+
} catch { /* best-effort — UI notify must never break the eager path */ }
|
|
125
|
+
// Intentionally do NOT delete _sig here — see the block
|
|
126
|
+
// comment above. The sig must outlive promise settlement
|
|
127
|
+
// so a later same-turn streaming duplicate stays blocked
|
|
128
|
+
// at the _eagerInFlightSigs.has(_sig) guard until the turn
|
|
129
|
+
// boundary recreates the Map.
|
|
130
|
+
return settled;
|
|
131
|
+
});
|
|
132
|
+
pending.set(call.id, entry);
|
|
133
|
+
return entry;
|
|
134
|
+
};
|
|
135
|
+
const startEagerRun = (calls, startIndex, dupSet) => {
|
|
136
|
+
for (let j = startIndex; j < calls.length; j += 1) {
|
|
137
|
+
const call = calls[j];
|
|
138
|
+
if (!call?.id || !isEagerDispatchable(call.name, tools)) break;
|
|
139
|
+
if (dupSet && dupSet.has(call.id)) continue;
|
|
140
|
+
if (!startEagerTool(call) && !pending.has(call.id)) break;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
let _streamEagerBlocked = false;
|
|
144
|
+
const onToolCall = (call) => {
|
|
145
|
+
if (!isEagerDispatchable(call?.name, tools)) {
|
|
146
|
+
_streamEagerBlocked = true;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (_streamEagerBlocked) return;
|
|
150
|
+
startEagerTool(call);
|
|
151
|
+
};
|
|
152
|
+
return { pending, epoch, startEagerTool, startEagerRun, onToolCall };
|
|
153
|
+
}
|
|
@@ -67,6 +67,20 @@ function buildRecallDigestText(sessionId, digestBody, maxKb) {
|
|
|
67
67
|
].join('\n');
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Abort/cancel detection: a cancelled session (ESC / new prompt / signal abort)
|
|
71
|
+
// surfaces as an AbortError or a DOMException with ABORT_ERR from the internal
|
|
72
|
+
// tool. That is NOT a memory-pipeline failure — rethrow it unchanged so the
|
|
73
|
+
// caller records a cancellation, never an AGENT_CONTEXT_OVERFLOW.
|
|
74
|
+
function isAbortLikeError(err, signal) {
|
|
75
|
+
if (signal?.aborted) return true;
|
|
76
|
+
if (!err) return false;
|
|
77
|
+
const name = err.name || '';
|
|
78
|
+
const code = err.code || '';
|
|
79
|
+
if (name === 'AbortError' || code === 'ABORT_ERR' || code === 'ABORT') return true;
|
|
80
|
+
const msg = String(err.message || err).toLowerCase();
|
|
81
|
+
return /\babort(ed|ing)?\b|\bcancel(l?ed|ling)?\b/.test(msg);
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
export async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
|
|
71
85
|
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
72
86
|
const startedAt = Date.now();
|
|
@@ -103,6 +117,10 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
103
117
|
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
104
118
|
diagnostics.hydrateLimit = hydrateLimit;
|
|
105
119
|
let t0 = Date.now();
|
|
120
|
+
let ingestFailed = false;
|
|
121
|
+
let searchFailed = false;
|
|
122
|
+
let ingestErr = null;
|
|
123
|
+
let searchErr = null;
|
|
106
124
|
try {
|
|
107
125
|
await executeInternalTool('memory', {
|
|
108
126
|
action: 'ingest_session',
|
|
@@ -112,6 +130,8 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
112
130
|
limit: hydrateLimit,
|
|
113
131
|
}, callerCtx);
|
|
114
132
|
} catch (err) {
|
|
133
|
+
ingestFailed = true;
|
|
134
|
+
ingestErr = err;
|
|
115
135
|
diagnostics.ingestSkipped = true;
|
|
116
136
|
diagnostics.ingestError = compactDiagnosticError(err);
|
|
117
137
|
try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
@@ -138,6 +158,8 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
138
158
|
}, callerCtx);
|
|
139
159
|
digestBody = typeof browsed === 'string' ? browsed : String(browsed?.text ?? browsed ?? '');
|
|
140
160
|
} catch (err) {
|
|
161
|
+
searchFailed = true;
|
|
162
|
+
searchErr = err;
|
|
141
163
|
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
142
164
|
try { process.stderr.write(`[loop] recall-digest browse failed (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
143
165
|
}
|
|
@@ -145,6 +167,30 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
145
167
|
diagnostics.cycle1Skipped = true;
|
|
146
168
|
diagnostics.cycle1SkipReason = 'digest mode';
|
|
147
169
|
diagnostics.cycle1Passes = 0;
|
|
170
|
+
// Fail-safe: memory ingest or search failed, so the digest cannot honestly
|
|
171
|
+
// represent "full history is in memory". Do NOT drop head messages behind a
|
|
172
|
+
// false recall notice — abort the fast-track so no context is silently lost
|
|
173
|
+
// for this cycle. The failure is already on stderr above; surface it here
|
|
174
|
+
// too and record it in the diagnostics before throwing.
|
|
175
|
+
if (ingestFailed || searchFailed) {
|
|
176
|
+
diagnostics.totalMs = Date.now() - startedAt;
|
|
177
|
+
diagnostics.failSafeAbort = true;
|
|
178
|
+
compactDebugLog('recall-digest pipeline', diagnostics);
|
|
179
|
+
// Cancellation is not a memory failure: rethrow the original abort error
|
|
180
|
+
// unchanged so the session is marked cancelled, not context-overflow.
|
|
181
|
+
const abortErr = isAbortLikeError(ingestErr, signal) ? ingestErr
|
|
182
|
+
: isAbortLikeError(searchErr, signal) ? searchErr
|
|
183
|
+
: (signal?.aborted ? (ingestErr || searchErr) : null);
|
|
184
|
+
if (abortErr) {
|
|
185
|
+
try { process.stderr.write(`[loop] recall-fasttrack cancelled (sess=${sessionId || 'unknown'}): ${abortErr?.message || abortErr}\n`); } catch {}
|
|
186
|
+
throw abortErr;
|
|
187
|
+
}
|
|
188
|
+
const reason = ingestFailed
|
|
189
|
+
? (searchFailed ? 'ingest+search failed' : 'ingest failed')
|
|
190
|
+
: 'search failed';
|
|
191
|
+
try { process.stderr.write(`[loop] recall-fasttrack fail-safe abort (sess=${sessionId || 'unknown'}): ${reason} — keeping full history, no recall notice injected\n`); } catch {}
|
|
192
|
+
throw new Error(`recall-fasttrack aborted: memory ${reason}; head preserved`);
|
|
193
|
+
}
|
|
148
194
|
const digestText = buildRecallDigestText(sessionId, digestBody, digestMaxKb);
|
|
149
195
|
diagnostics.finalRecallChars = digestText.length;
|
|
150
196
|
diagnostics.finalRecallBytes = compactByteLength(digestText);
|
|
@@ -161,6 +207,9 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
161
207
|
recallText: digestText,
|
|
162
208
|
query,
|
|
163
209
|
querySha,
|
|
210
|
+
// Ingest + search both succeeded above, so the memory DB genuinely holds
|
|
211
|
+
// this transcript and the recall notice is truthful even when the digest
|
|
212
|
+
// body is small. A failure would have aborted before reaching here.
|
|
164
213
|
allowEmptyRecall: true,
|
|
165
214
|
tailTurns: compactPolicy.tailTurns,
|
|
166
215
|
keepTokens: compactPolicy.keepTokens,
|
|
@@ -18,9 +18,17 @@ function compactStoredToolArgString(value, key = '') {
|
|
|
18
18
|
const limit = isBody ? STORED_TOOL_ARG_BODY_LIMIT : (isLong ? STORED_TOOL_ARG_LONG_LIMIT : Infinity);
|
|
19
19
|
if (value.length <= limit) return value;
|
|
20
20
|
const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
21
|
+
const marker = `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}]`;
|
|
22
|
+
// Body args (patch / old_string / new_string / content / rewrite) are
|
|
23
|
+
// apply_patch / edit inputs. Keeping a head/tail preview leaves real patch
|
|
24
|
+
// fragments (a "*** Begin Patch" opening, diff lines) inside a SUCCESSFUL
|
|
25
|
+
// history entry that the model can copy back verbatim as new tool input.
|
|
26
|
+
// Emit the marker ALONE for these keys so nothing copyable survives; a
|
|
27
|
+
// FAILED call still restores the full original via restoreToolCallBodyForId.
|
|
28
|
+
if (isBody) return marker;
|
|
21
29
|
const head = value.slice(0, STORED_TOOL_ARG_PREVIEW_HEAD).replace(/\r\n/g, '\n');
|
|
22
30
|
const tail = value.slice(-STORED_TOOL_ARG_PREVIEW_TAIL).replace(/\r\n/g, '\n');
|
|
23
|
-
return
|
|
31
|
+
return `${marker}\n${head}\n... [middle omitted from stored tool-call args] ...\n${tail}`;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
function compactStoredToolArgValue(value, key = '', depth = 0) {
|