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
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// Pre-send auto-compact pass, extracted verbatim from agent-loop.mjs (was
|
|
2
|
+
// loop.mjs). Runs the proactive/reactive compaction decision + execution
|
|
3
|
+
// before each provider.send. Threaded via a state bag so the loop keeps its
|
|
4
|
+
// mutable counters (iterations reset on compaction, usage accumulation, the
|
|
5
|
+
// reactive-overflow flag, providerState invalidation). Behavior identical to
|
|
6
|
+
// the inline block it replaced.
|
|
7
|
+
import {
|
|
8
|
+
resolveWorkerCompactPolicy,
|
|
9
|
+
compactionTelemetryPressureTokens,
|
|
10
|
+
compactTargetBudget,
|
|
11
|
+
shouldCompactForSession,
|
|
12
|
+
countPrunedToolOutputs,
|
|
13
|
+
rememberCompactTelemetry,
|
|
14
|
+
emitCompactEvent,
|
|
15
|
+
compactEventType,
|
|
16
|
+
} from './loop/compact-policy.mjs';
|
|
17
|
+
import {
|
|
18
|
+
pruneToolOutputs,
|
|
19
|
+
pruneToolOutputsUnanchored,
|
|
20
|
+
semanticCompactMessages,
|
|
21
|
+
effectiveBudget as compactEffectiveBudget,
|
|
22
|
+
DEFAULT_COMPACT_TYPE,
|
|
23
|
+
} from './compact.mjs';
|
|
24
|
+
import { runRecallFastTrackCompact } from './loop/recall-fasttrack.mjs';
|
|
25
|
+
import { estimateMessagesTokensSafe } from './loop/compact-debug.mjs';
|
|
26
|
+
import { messagesArrayChanged } from './loop/tool-helpers.mjs';
|
|
27
|
+
import { normalizeUsage, addUsage } from './loop/usage.mjs';
|
|
28
|
+
import { agentContextOverflowError } from './loop/context-overflow.mjs';
|
|
29
|
+
import { traceAgentCompact, messagePrefixHash } from '../agent-trace.mjs';
|
|
30
|
+
import { bumpUsageMetricsEpoch } from './manager.mjs';
|
|
31
|
+
|
|
32
|
+
export async function runPreSendCompactPass(state) {
|
|
33
|
+
const {
|
|
34
|
+
provider, messages, model, tools, sessionRef, sessionId, cwd, opts, signal,
|
|
35
|
+
loopUsageMetricsTurnId, loopUsageMetricsEpoch,
|
|
36
|
+
} = state;
|
|
37
|
+
let { iterations, lastUsage, firstTurnUsage, providerState, reactiveOverflowRetryPending } = state;
|
|
38
|
+
const compactPolicy = resolveWorkerCompactPolicy(sessionRef, tools);
|
|
39
|
+
if (compactPolicy?.auto) {
|
|
40
|
+
// Snapshot pre-compact shape so compact_meta can record the actual
|
|
41
|
+
// mutation (or no-op) for prefix-mutation forensics. Bytes are
|
|
42
|
+
// a best-effort JSON.stringify length — close enough to the
|
|
43
|
+
// payload we hand the provider for prefix-cache analysis.
|
|
44
|
+
const beforeCount = messages.length;
|
|
45
|
+
// beforeBytes is only ever read inside the shouldCompact telemetry
|
|
46
|
+
// branches below. Computing it eagerly serialized the ENTIRE message
|
|
47
|
+
// array (Buffer.byteLength(JSON.stringify(messages))) on every loop
|
|
48
|
+
// iteration — including the common no-compact path — which grows
|
|
49
|
+
// linearly with transcript size and was a real per-iteration drag.
|
|
50
|
+
// Defer it to a memoized lazy getter so the no-compact path pays
|
|
51
|
+
// nothing and the compact path still gets an exact byte count once.
|
|
52
|
+
let _beforeBytes;
|
|
53
|
+
let _beforeBytesComputed = false;
|
|
54
|
+
const getBeforeBytes = () => {
|
|
55
|
+
if (_beforeBytesComputed) return _beforeBytes;
|
|
56
|
+
_beforeBytesComputed = true;
|
|
57
|
+
try { _beforeBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { _beforeBytes = null; }
|
|
58
|
+
return _beforeBytes;
|
|
59
|
+
};
|
|
60
|
+
const messageTokensEst = estimateMessagesTokensSafe(messages);
|
|
61
|
+
const reactivePending = reactiveOverflowRetryPending === true;
|
|
62
|
+
const shouldCompact = shouldCompactForSession(messageTokensEst, compactPolicy, { forceReactive: reactivePending });
|
|
63
|
+
const pressureTokens = compactionTelemetryPressureTokens(messageTokensEst, compactPolicy, { reactivePending });
|
|
64
|
+
// A pending reactive-overflow retry makes THIS compact pass the
|
|
65
|
+
// recovery from a provider overflow refusal, not the proactive
|
|
66
|
+
// pressure trigger. Tag the emitted events so telemetry can tell
|
|
67
|
+
// them apart. Hoisted above the shouldCompact branch because the
|
|
68
|
+
// PostCompact hook below fires on BOTH paths (fixes a
|
|
69
|
+
// ReferenceError on the no-compact path).
|
|
70
|
+
const compactTrigger = reactivePending ? 'reactive' : 'auto';
|
|
71
|
+
const compactBudgetTokens = shouldCompact
|
|
72
|
+
? (compactTargetBudget({ ...compactPolicy, pressureTokens }) || compactPolicy.boundaryTokens)
|
|
73
|
+
: compactPolicy.boundaryTokens;
|
|
74
|
+
if (!shouldCompact) {
|
|
75
|
+
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
76
|
+
stage: 'pre_send_check',
|
|
77
|
+
beforeTokens: messageTokensEst,
|
|
78
|
+
afterTokens: messageTokensEst,
|
|
79
|
+
pressureTokens,
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
try { await opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
83
|
+
const compactStartedAt = Date.now();
|
|
84
|
+
// Clear the one-shot reactive-overflow flag now that this
|
|
85
|
+
// compact pass is consuming it (compactTrigger already
|
|
86
|
+
// captured it above).
|
|
87
|
+
reactiveOverflowRetryPending = false;
|
|
88
|
+
// PreCompact: bridge to the standard hook bus before compaction
|
|
89
|
+
// runs. session-property hook (manager/loop have no bus access).
|
|
90
|
+
// { trigger } normalized to 'auto'|'manual'. Best-effort.
|
|
91
|
+
{
|
|
92
|
+
const _preCompactHook = typeof opts.preCompactHook === 'function'
|
|
93
|
+
? opts.preCompactHook
|
|
94
|
+
: sessionRef?.preCompactHook;
|
|
95
|
+
if (typeof _preCompactHook === 'function') {
|
|
96
|
+
try { await _preCompactHook({ sessionId, cwd, trigger: compactTrigger === 'manual' ? 'manual' : 'auto' }); }
|
|
97
|
+
catch { /* best-effort: PreCompact hook must never break compaction */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
101
|
+
stage: 'compacting',
|
|
102
|
+
beforeTokens: messageTokensEst,
|
|
103
|
+
afterTokens: messageTokensEst,
|
|
104
|
+
pressureTokens,
|
|
105
|
+
trigger: compactTrigger,
|
|
106
|
+
});
|
|
107
|
+
let compacted;
|
|
108
|
+
let pruneCount = 0;
|
|
109
|
+
let summaryChanged = false;
|
|
110
|
+
let semanticCompactResult = null;
|
|
111
|
+
let semanticCompactError = null;
|
|
112
|
+
let recallFastTrackResult = null;
|
|
113
|
+
let recallFastTrackError = null;
|
|
114
|
+
try {
|
|
115
|
+
let compactInputMessages = messages;
|
|
116
|
+
if (compactPolicy.prune) {
|
|
117
|
+
const pruned = pruneToolOutputs(messages, compactPolicy.boundaryTokens, {
|
|
118
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
119
|
+
});
|
|
120
|
+
pruneCount = countPrunedToolOutputs(messages, pruned);
|
|
121
|
+
compactInputMessages = pruned;
|
|
122
|
+
}
|
|
123
|
+
if (compactPolicy.recallFastTrack) {
|
|
124
|
+
try {
|
|
125
|
+
recallFastTrackResult = await runRecallFastTrackCompact({
|
|
126
|
+
sessionRef,
|
|
127
|
+
messages: compactInputMessages,
|
|
128
|
+
compactBudgetTokens,
|
|
129
|
+
compactPolicy,
|
|
130
|
+
sessionId,
|
|
131
|
+
signal,
|
|
132
|
+
});
|
|
133
|
+
const recallMessages = Array.isArray(recallFastTrackResult?.messages)
|
|
134
|
+
? recallFastTrackResult.messages
|
|
135
|
+
: null;
|
|
136
|
+
if (!recallMessages) throw new Error('recall-fasttrack compact produced no messages');
|
|
137
|
+
compacted = recallMessages;
|
|
138
|
+
} catch (recallErr) {
|
|
139
|
+
recallFastTrackError = recallErr;
|
|
140
|
+
try {
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
`[loop] recall-fasttrack compact failed (sess=${sessionId || 'unknown'}): ` +
|
|
143
|
+
`${recallErr?.message || recallErr}\n`,
|
|
144
|
+
);
|
|
145
|
+
} catch { /* best-effort */ }
|
|
146
|
+
throw recallErr;
|
|
147
|
+
}
|
|
148
|
+
} else if (compactPolicy.semantic) {
|
|
149
|
+
try {
|
|
150
|
+
semanticCompactResult = await semanticCompactMessages(
|
|
151
|
+
provider,
|
|
152
|
+
compactInputMessages,
|
|
153
|
+
model,
|
|
154
|
+
compactBudgetTokens,
|
|
155
|
+
{
|
|
156
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
157
|
+
providerName: sessionRef.provider || provider?.name || null,
|
|
158
|
+
sessionId,
|
|
159
|
+
signal,
|
|
160
|
+
sendOpts: opts,
|
|
161
|
+
promptCacheKey: opts.promptCacheKey || null,
|
|
162
|
+
providerCacheKey: opts.providerCacheKey || null,
|
|
163
|
+
timeoutMs: compactPolicy.semanticTimeoutMs,
|
|
164
|
+
tailTurns: compactPolicy.tailTurns,
|
|
165
|
+
keepTokens: compactPolicy.keepTokens,
|
|
166
|
+
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
167
|
+
force: true,
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
const semanticMessages = Array.isArray(semanticCompactResult?.messages)
|
|
171
|
+
? semanticCompactResult.messages
|
|
172
|
+
: null;
|
|
173
|
+
if (!semanticMessages) throw new Error('semantic compact produced no messages');
|
|
174
|
+
compacted = semanticMessages;
|
|
175
|
+
if (semanticCompactResult?.usage) {
|
|
176
|
+
lastUsage = addUsage(lastUsage, semanticCompactResult.usage);
|
|
177
|
+
if (!firstTurnUsage) firstTurnUsage = normalizeUsage(semanticCompactResult.usage);
|
|
178
|
+
if (sessionId && opts.onUsageDelta) {
|
|
179
|
+
try {
|
|
180
|
+
opts.onUsageDelta({
|
|
181
|
+
sessionId,
|
|
182
|
+
iterationIndex: iterations + 1,
|
|
183
|
+
usageMetricsTurnId: loopUsageMetricsTurnId(),
|
|
184
|
+
usageMetricsEpoch: loopUsageMetricsEpoch(),
|
|
185
|
+
deltaInput: semanticCompactResult.usage.inputTokens || 0,
|
|
186
|
+
deltaOutput: semanticCompactResult.usage.outputTokens || 0,
|
|
187
|
+
deltaCachedRead: semanticCompactResult.usage.cachedTokens || 0,
|
|
188
|
+
deltaCacheWrite: semanticCompactResult.usage.cacheWriteTokens || 0,
|
|
189
|
+
source: 'semantic_compact',
|
|
190
|
+
ts: Date.now(),
|
|
191
|
+
});
|
|
192
|
+
} catch { /* best-effort */ }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (semanticErr) {
|
|
196
|
+
semanticCompactError = semanticErr;
|
|
197
|
+
try {
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
`[loop] semantic compact failed (sess=${sessionId || 'unknown'}): ` +
|
|
200
|
+
`${semanticErr?.message || semanticErr}\n`,
|
|
201
|
+
);
|
|
202
|
+
} catch { /* best-effort */ }
|
|
203
|
+
throw semanticErr;
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
throw new Error(`compact type ${compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE} is unavailable for auto compact`);
|
|
207
|
+
}
|
|
208
|
+
summaryChanged = messagesArrayChanged(compactInputMessages, compacted);
|
|
209
|
+
} catch (compactErr) {
|
|
210
|
+
// Anchor-independent prune safety net. When SEMANTIC compact
|
|
211
|
+
// throws (e.g. a degenerate single-turn transcript, or a
|
|
212
|
+
// summary that cannot fit), attempt one non-LLM prune that
|
|
213
|
+
// needs no user anchor: middle-truncate the oldest oversized
|
|
214
|
+
// tool_result bodies until the transcript fits the budget.
|
|
215
|
+
// If it shrinks the transcript we continue with that result
|
|
216
|
+
// instead of escalating to overflow. Structure/pairing is
|
|
217
|
+
// preserved (only string content shrinks) and the result is
|
|
218
|
+
// re-reconciled inside the helper.
|
|
219
|
+
//
|
|
220
|
+
// GATED to the non-recall path: a recall-fasttrack failure
|
|
221
|
+
// must NOT be silently recovered by this prune (that would
|
|
222
|
+
// change the type-2 path's contract by shipping a pruned
|
|
223
|
+
// transcript with no recall output). When recallFastTrackError
|
|
224
|
+
// is set the fallback is skipped and the original overflow
|
|
225
|
+
// escalation runs unchanged.
|
|
226
|
+
if (!recallFastTrackError) {
|
|
227
|
+
try {
|
|
228
|
+
// Accept only if the pruned transcript fits the SAME
|
|
229
|
+
// effective budget the prune targets (compactBudgetTokens
|
|
230
|
+
// minus the request reserve) — comparing against the raw
|
|
231
|
+
// compactBudgetTokens would accept a result with no
|
|
232
|
+
// reserve headroom and overflow on the very next send.
|
|
233
|
+
const acceptThreshold = compactEffectiveBudget(compactBudgetTokens, {
|
|
234
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
235
|
+
});
|
|
236
|
+
const salvaged = pruneToolOutputsUnanchored(messages, compactBudgetTokens, {
|
|
237
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
238
|
+
});
|
|
239
|
+
if (messagesArrayChanged(messages, salvaged)
|
|
240
|
+
&& estimateMessagesTokensSafe(salvaged) <= acceptThreshold) {
|
|
241
|
+
compacted = salvaged;
|
|
242
|
+
pruneCount = countPrunedToolOutputs(messages, salvaged);
|
|
243
|
+
summaryChanged = true;
|
|
244
|
+
}
|
|
245
|
+
} catch { /* fall through to overflow escalation */ }
|
|
246
|
+
}
|
|
247
|
+
if (compacted !== undefined) {
|
|
248
|
+
try {
|
|
249
|
+
process.stderr.write(
|
|
250
|
+
`[loop] compact fallback prune recovered (sess=${sessionId || 'unknown'}): ` +
|
|
251
|
+
`${compactErr?.message || compactErr}\n`,
|
|
252
|
+
);
|
|
253
|
+
} catch { /* best-effort */ }
|
|
254
|
+
} else {
|
|
255
|
+
const compactFailMsg = compactErr && compactErr.message ? compactErr.message : String(compactErr);
|
|
256
|
+
const semanticFailMsg = semanticCompactError?.message || null;
|
|
257
|
+
const recallFailMsg = recallFastTrackError?.message || null;
|
|
258
|
+
const compactFailCode = compactErr?.code
|
|
259
|
+
|| (compactErr?.name === 'AgentContextOverflowError' ? 'AGENT_CONTEXT_OVERFLOW' : null)
|
|
260
|
+
|| 'compact_failed';
|
|
261
|
+
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
262
|
+
stage: 'overflow_failed',
|
|
263
|
+
beforeTokens: messageTokensEst,
|
|
264
|
+
afterTokens: messageTokensEst,
|
|
265
|
+
pressureTokens,
|
|
266
|
+
trigger: compactTrigger,
|
|
267
|
+
semanticError: semanticFailMsg,
|
|
268
|
+
recallFastTrackError: recallFailMsg,
|
|
269
|
+
compactError: semanticFailMsg || recallFailMsg || compactFailMsg,
|
|
270
|
+
pruneCount,
|
|
271
|
+
durationMs: Date.now() - compactStartedAt,
|
|
272
|
+
});
|
|
273
|
+
traceAgentCompact({
|
|
274
|
+
sessionId,
|
|
275
|
+
iteration: iterations + 1,
|
|
276
|
+
stage: 'pre_send',
|
|
277
|
+
trigger: compactTrigger,
|
|
278
|
+
compact_type: compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE,
|
|
279
|
+
prune_count: pruneCount,
|
|
280
|
+
compact_changed: false,
|
|
281
|
+
input_prefix_hash: messagePrefixHash(messages),
|
|
282
|
+
before_count: beforeCount,
|
|
283
|
+
after_count: messages.length,
|
|
284
|
+
before_bytes: getBeforeBytes(),
|
|
285
|
+
after_bytes: getBeforeBytes(),
|
|
286
|
+
context_window: compactPolicy.contextWindow,
|
|
287
|
+
budget_tokens: compactPolicy.boundaryTokens,
|
|
288
|
+
boundary_tokens: compactPolicy.boundaryTokens,
|
|
289
|
+
target_budget_tokens: compactBudgetTokens,
|
|
290
|
+
reserve_tokens: compactPolicy.reserveTokens,
|
|
291
|
+
pressure_tokens: pressureTokens,
|
|
292
|
+
trigger_tokens: compactPolicy.triggerTokens,
|
|
293
|
+
message_tokens_est: messageTokensEst,
|
|
294
|
+
duration_ms: Date.now() - compactStartedAt,
|
|
295
|
+
provider: sessionRef.provider,
|
|
296
|
+
model: sessionRef.model || model,
|
|
297
|
+
error: compactFailMsg,
|
|
298
|
+
error_code: compactFailCode,
|
|
299
|
+
details: {
|
|
300
|
+
semantic: semanticCompactResult?.diagnostics || null,
|
|
301
|
+
recallFastTrack: recallFastTrackResult?.diagnostics || null,
|
|
302
|
+
semanticError: semanticFailMsg,
|
|
303
|
+
recallFastTrackError: recallFailMsg,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
emitCompactEvent(opts, {
|
|
307
|
+
sessionId,
|
|
308
|
+
stage: 'pre_send',
|
|
309
|
+
trigger: compactTrigger,
|
|
310
|
+
status: 'failed',
|
|
311
|
+
compactType: compactEventType(compactPolicy),
|
|
312
|
+
beforeTokens: messageTokensEst,
|
|
313
|
+
afterTokens: messageTokensEst,
|
|
314
|
+
beforeMessages: beforeCount,
|
|
315
|
+
afterMessages: messages.length,
|
|
316
|
+
pressureTokens,
|
|
317
|
+
triggerTokens: compactPolicy.triggerTokens,
|
|
318
|
+
boundaryTokens: compactPolicy.boundaryTokens,
|
|
319
|
+
targetBudgetTokens: compactBudgetTokens,
|
|
320
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
321
|
+
semantic: compactPolicy.semantic === true,
|
|
322
|
+
recallFastTrack: compactPolicy.recallFastTrack === true,
|
|
323
|
+
pruneCount,
|
|
324
|
+
durationMs: Date.now() - compactStartedAt,
|
|
325
|
+
error: compactErr && compactErr.message ? compactErr.message : String(compactErr),
|
|
326
|
+
});
|
|
327
|
+
throw agentContextOverflowError({
|
|
328
|
+
stage: 'pre_send',
|
|
329
|
+
sessionId,
|
|
330
|
+
sessionRef,
|
|
331
|
+
model,
|
|
332
|
+
budgetTokens: compactBudgetTokens,
|
|
333
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
334
|
+
messageTokensEst,
|
|
335
|
+
}, compactErr);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
try { await opts.onStageChange?.('requesting'); } catch { /* best-effort */ }
|
|
339
|
+
const compactChanged = messagesArrayChanged(messages, compacted);
|
|
340
|
+
if (compactChanged) {
|
|
341
|
+
messages.length = 0;
|
|
342
|
+
messages.push(...compacted);
|
|
343
|
+
// Compacting/pruning the transcript invalidates the
|
|
344
|
+
// server-side conversation anchor (xAI Responses / openai-oauth
|
|
345
|
+
// WS rely on previous_response_id which points at a
|
|
346
|
+
// now-mutated prefix). Drop providerState so the next send
|
|
347
|
+
// starts a fresh chain.
|
|
348
|
+
providerState = undefined;
|
|
349
|
+
// Compaction shrank the transcript, so prior turns no
|
|
350
|
+
// longer pressure the window — reset the iteration counter
|
|
351
|
+
// so a steadily-compacting long task isn't killed by the
|
|
352
|
+
// cap, while a non-compacting tight loop still hits it.
|
|
353
|
+
iterations = 0;
|
|
354
|
+
// New loop epoch so persistIterationMetrics idempotency keys do not
|
|
355
|
+
// collide when iteration indices restart at 1 (incl. iter 1 → iter 1).
|
|
356
|
+
if (sessionRef) bumpUsageMetricsEpoch(sessionRef);
|
|
357
|
+
}
|
|
358
|
+
const afterTokens = estimateMessagesTokensSafe(messages);
|
|
359
|
+
const compactDurationMs = Date.now() - compactStartedAt;
|
|
360
|
+
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
361
|
+
stage: 'pre_send',
|
|
362
|
+
beforeTokens: messageTokensEst,
|
|
363
|
+
afterTokens,
|
|
364
|
+
pressureTokens,
|
|
365
|
+
compactChanged,
|
|
366
|
+
semanticCompact: semanticCompactResult?.semantic === true,
|
|
367
|
+
semanticError: semanticCompactError?.message || null,
|
|
368
|
+
recallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
369
|
+
recallFastTrackError: recallFastTrackError?.message || null,
|
|
370
|
+
compactError: null,
|
|
371
|
+
pruneCount,
|
|
372
|
+
durationMs: compactDurationMs,
|
|
373
|
+
});
|
|
374
|
+
let afterBytes = null;
|
|
375
|
+
try { afterBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { afterBytes = null; }
|
|
376
|
+
traceAgentCompact({
|
|
377
|
+
sessionId,
|
|
378
|
+
iteration: iterations + 1,
|
|
379
|
+
stage: 'pre_send',
|
|
380
|
+
trigger: compactTrigger,
|
|
381
|
+
compact_type: compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE,
|
|
382
|
+
prune_count: pruneCount,
|
|
383
|
+
compact_changed: compactChanged || summaryChanged,
|
|
384
|
+
input_prefix_hash: messagePrefixHash(messages),
|
|
385
|
+
before_count: beforeCount,
|
|
386
|
+
after_count: messages.length,
|
|
387
|
+
before_bytes: getBeforeBytes(),
|
|
388
|
+
after_bytes: afterBytes,
|
|
389
|
+
context_window: compactPolicy.contextWindow,
|
|
390
|
+
budget_tokens: compactPolicy.boundaryTokens,
|
|
391
|
+
boundary_tokens: compactPolicy.boundaryTokens,
|
|
392
|
+
target_budget_tokens: compactBudgetTokens,
|
|
393
|
+
reserve_tokens: compactPolicy.reserveTokens,
|
|
394
|
+
pressure_tokens: pressureTokens,
|
|
395
|
+
trigger_tokens: compactPolicy.triggerTokens,
|
|
396
|
+
message_tokens_est: messageTokensEst,
|
|
397
|
+
duration_ms: compactDurationMs,
|
|
398
|
+
provider: sessionRef.provider,
|
|
399
|
+
model: sessionRef.model || model,
|
|
400
|
+
details: {
|
|
401
|
+
semantic: semanticCompactResult?.diagnostics || null,
|
|
402
|
+
recallFastTrack: recallFastTrackResult?.diagnostics || null,
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
emitCompactEvent(opts, {
|
|
406
|
+
sessionId,
|
|
407
|
+
stage: 'pre_send',
|
|
408
|
+
trigger: compactTrigger,
|
|
409
|
+
status: compactChanged || summaryChanged || pruneCount > 0 ? 'compacted' : 'no_change',
|
|
410
|
+
compactType: compactEventType(compactPolicy),
|
|
411
|
+
beforeTokens: messageTokensEst,
|
|
412
|
+
afterTokens,
|
|
413
|
+
beforeMessages: beforeCount,
|
|
414
|
+
afterMessages: messages.length,
|
|
415
|
+
pressureTokens,
|
|
416
|
+
triggerTokens: compactPolicy.triggerTokens,
|
|
417
|
+
boundaryTokens: compactPolicy.boundaryTokens,
|
|
418
|
+
targetBudgetTokens: compactBudgetTokens,
|
|
419
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
420
|
+
changed: compactChanged || summaryChanged,
|
|
421
|
+
semantic: semanticCompactResult?.semantic === true,
|
|
422
|
+
recallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
423
|
+
pruneCount,
|
|
424
|
+
durationMs: compactDurationMs,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
// PostCompact: bridge to the standard hook bus after compaction
|
|
428
|
+
// completes. session-property hook; { trigger } 'auto'|'manual'.
|
|
429
|
+
{
|
|
430
|
+
const _postCompactHook = typeof opts.postCompactHook === 'function'
|
|
431
|
+
? opts.postCompactHook
|
|
432
|
+
: sessionRef?.postCompactHook;
|
|
433
|
+
if (typeof _postCompactHook === 'function') {
|
|
434
|
+
try { await _postCompactHook({ sessionId, cwd, trigger: compactTrigger === 'manual' ? 'manual' : 'auto' }); }
|
|
435
|
+
catch { /* best-effort: PostCompact hook must never break the loop */ }
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return { iterations, lastUsage, firstTurnUsage, providerState, reactiveOverflowRetryPending };
|
|
440
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// provider.send wrapper with stall/overflow recovery, extracted from
|
|
2
|
+
// agent-loop.mjs. Returns { action } so the loop keeps control of the
|
|
3
|
+
// while-loop: proceed carries the response, retry signals a reactive
|
|
4
|
+
// context-overflow compact retry (caller re-enters the pre-send compact
|
|
5
|
+
// pass), and unrecoverable errors throw. Behavior identical to the inline
|
|
6
|
+
// try/catch it replaced.
|
|
7
|
+
import { appendAgentTrace } from '../agent-trace.mjs';
|
|
8
|
+
import { isContextOverflowError } from '../providers/retry-classifier.mjs';
|
|
9
|
+
import { resolveWorkerCompactPolicy } from './loop/compact-policy.mjs';
|
|
10
|
+
import { agentContextOverflowError } from './loop/context-overflow.mjs';
|
|
11
|
+
import { estimateMessagesTokensSafe } from './loop/compact-debug.mjs';
|
|
12
|
+
|
|
13
|
+
export async function sendWithRecovery(ctx) {
|
|
14
|
+
const {
|
|
15
|
+
provider, messages, model, sendTools, tools, opts,
|
|
16
|
+
sessionId, sessionRef, nextIteration, contextOverflowRetryUsed,
|
|
17
|
+
} = ctx;
|
|
18
|
+
let response;
|
|
19
|
+
try {
|
|
20
|
+
response = await provider.send(messages, model, sendTools.length ? sendTools : undefined, opts);
|
|
21
|
+
} catch (sendErr) {
|
|
22
|
+
// Partial-final recovery (owner-notify fix): the recurring "worker
|
|
23
|
+
// finished but the task hung / no result delivered" case is a FINAL,
|
|
24
|
+
// no-tool summary stream that wedges (ping-only) AFTER all real tool
|
|
25
|
+
// work completed in earlier iterations. The provider attaches its
|
|
26
|
+
// partial stream state to the StreamStalledError. When the stall
|
|
27
|
+
// carries streamed assistant text, has NO pending tool_use, and did
|
|
28
|
+
// NOT emit a tool call this iteration, accept the partial as a
|
|
29
|
+
// successful terminal response (deliver the summary we have) instead
|
|
30
|
+
// of throwing — which would strand/notify-as-failure a turn whose
|
|
31
|
+
// work actually succeeded. A stall WITH a pending/emitted tool call
|
|
32
|
+
// is NOT recoverable (a tool whose input never completed must never
|
|
33
|
+
// look done) and falls through to the normal error path.
|
|
34
|
+
if (
|
|
35
|
+
sendErr?.streamStalled === true
|
|
36
|
+
&& sendErr.pendingToolUse !== true
|
|
37
|
+
// NOT gated on unsafeToRetry: live-text stalls stamp
|
|
38
|
+
// unsafeToRetry=true (replay would double-render), but
|
|
39
|
+
// ACCEPTING the already-streamed partial is exactly the safe
|
|
40
|
+
// move (CC rule). Only an emitted tool call blocks acceptance.
|
|
41
|
+
&& sendErr.emittedToolCall !== true
|
|
42
|
+
&& typeof sendErr.partialContent === 'string'
|
|
43
|
+
&& sendErr.partialContent.trim().length > 0
|
|
44
|
+
&& !(Array.isArray(sendErr.partialToolCalls) && sendErr.partialToolCalls.length > 0)
|
|
45
|
+
) {
|
|
46
|
+
try {
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
`[loop] final stream stalled with partial text (sess=${sessionId || 'unknown'} `
|
|
49
|
+
+ `iter=${nextIteration} len=${sendErr.partialContent.length}); `
|
|
50
|
+
+ `accepting as partial-final success\n`,
|
|
51
|
+
);
|
|
52
|
+
} catch { /* best-effort */ }
|
|
53
|
+
response = {
|
|
54
|
+
content: sendErr.partialContent,
|
|
55
|
+
model: sendErr.partialModel || model,
|
|
56
|
+
toolCalls: undefined,
|
|
57
|
+
usage: sendErr.partialUsage || undefined,
|
|
58
|
+
stopReason: sendErr.partialStopReason || 'end_turn',
|
|
59
|
+
hasThinkingContent: sendErr.partialHasThinking === true,
|
|
60
|
+
partialFinal: true,
|
|
61
|
+
};
|
|
62
|
+
} else
|
|
63
|
+
// Partial tool-call recovery (agent-hang fix): a stream that stalls
|
|
64
|
+
// AFTER fully-parsed tool calls were emitted used to lose the whole
|
|
65
|
+
// turn — unsafeToRetry blocks the mid-stream replay (correct: a
|
|
66
|
+
// replay would re-run side-effecting tools) and the old code threw,
|
|
67
|
+
// discarding tool work that had ALREADY completed via eager dispatch.
|
|
68
|
+
// But the parsed calls are complete (pendingToolUse false ⇒ no
|
|
69
|
+
// half-streamed tool input), so instead of replaying the request we
|
|
70
|
+
// accept the partial as a normal tool-call turn and fall through to
|
|
71
|
+
// the standard execution path: eager-dispatched (read-only) calls
|
|
72
|
+
// resolve from the pending map without re-running, side-effecting
|
|
73
|
+
// calls were never started during streaming and execute exactly
|
|
74
|
+
// once. providerState stays undefined so the next iteration resends
|
|
75
|
+
// a full frame on a fresh stream.
|
|
76
|
+
if (
|
|
77
|
+
sendErr?.streamStalled === true
|
|
78
|
+
&& sendErr.pendingToolUse !== true
|
|
79
|
+
&& Array.isArray(sendErr.partialToolCalls)
|
|
80
|
+
&& sendErr.partialToolCalls.length > 0
|
|
81
|
+
) {
|
|
82
|
+
try {
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
`[loop] stream stalled after ${sendErr.partialToolCalls.length} complete tool call(s) `
|
|
85
|
+
+ `(sess=${sessionId || 'unknown'} iter=${nextIteration}); `
|
|
86
|
+
+ `recovering as tool-call turn instead of failing\n`,
|
|
87
|
+
);
|
|
88
|
+
} catch { /* best-effort */ }
|
|
89
|
+
try {
|
|
90
|
+
appendAgentTrace({
|
|
91
|
+
kind: 'stall_tool_recovery',
|
|
92
|
+
sessionId: sessionId || null,
|
|
93
|
+
iteration: nextIteration,
|
|
94
|
+
toolCalls: sendErr.partialToolCalls.length,
|
|
95
|
+
partialContentLen: typeof sendErr.partialContent === 'string' ? sendErr.partialContent.length : 0,
|
|
96
|
+
});
|
|
97
|
+
} catch { /* best-effort */ }
|
|
98
|
+
response = {
|
|
99
|
+
content: typeof sendErr.partialContent === 'string' ? sendErr.partialContent : '',
|
|
100
|
+
model: sendErr.partialModel || model,
|
|
101
|
+
toolCalls: sendErr.partialToolCalls.slice(),
|
|
102
|
+
usage: sendErr.partialUsage || undefined,
|
|
103
|
+
stopReason: 'tool_use',
|
|
104
|
+
hasThinkingContent: sendErr.partialHasThinking === true,
|
|
105
|
+
partialToolRecovery: true,
|
|
106
|
+
};
|
|
107
|
+
} else
|
|
108
|
+
// Context-window-exceeded is a deterministic refusal from the API.
|
|
109
|
+
// Recover context overflow reactively by compacting and retrying
|
|
110
|
+
// in the same active turn. MixDog's proactive estimator can miss a
|
|
111
|
+
// provider-specific overhead spike, so do one reactive retry by
|
|
112
|
+
// marking the live session over-threshold and looping back through
|
|
113
|
+
// the normal pre-send auto-compact path. If compaction/retry still
|
|
114
|
+
// fails, surface the overflow normally.
|
|
115
|
+
if (
|
|
116
|
+
!isContextOverflowError(sendErr)
|
|
117
|
+
|| !(sessionRef && typeof sessionRef.contextWindow === 'number')
|
|
118
|
+
) {
|
|
119
|
+
throw sendErr;
|
|
120
|
+
}
|
|
121
|
+
const compactPolicyForRetry = resolveWorkerCompactPolicy(sessionRef, sendTools.length ? sendTools : tools);
|
|
122
|
+
if (!contextOverflowRetryUsed && compactPolicyForRetry?.auto) {
|
|
123
|
+
// Mark the next pre-send compact as REACTIVE (driven by a
|
|
124
|
+
// provider overflow refusal) rather than the normal proactive
|
|
125
|
+
// pressure trigger, so the compact event/telemetry the loop
|
|
126
|
+
// emits on the retry is distinguishable downstream.
|
|
127
|
+
opts.onToolCall = undefined;
|
|
128
|
+
try {
|
|
129
|
+
process.stderr.write(
|
|
130
|
+
`[loop] context overflow on send (sess=${sessionId || 'unknown'} iter=${nextIteration}); ` +
|
|
131
|
+
`reactive compact retry messages=${messages.length}\n`,
|
|
132
|
+
);
|
|
133
|
+
} catch { /* best-effort */ }
|
|
134
|
+
return { action: 'retry' };
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
process.stderr.write(
|
|
138
|
+
`[loop] context overflow on send (sess=${sessionId || 'unknown'} iter=${nextIteration}); ` +
|
|
139
|
+
`surfacing overflow after reactive compact retry messages=${messages.length}\n`,
|
|
140
|
+
);
|
|
141
|
+
} catch { /* best-effort */ }
|
|
142
|
+
throw agentContextOverflowError({
|
|
143
|
+
stage: 'send',
|
|
144
|
+
sessionId,
|
|
145
|
+
sessionRef,
|
|
146
|
+
model,
|
|
147
|
+
budgetTokens: sessionRef.contextWindow,
|
|
148
|
+
reserveTokens: compactPolicyForRetry?.reserveTokens,
|
|
149
|
+
messageTokensEst: estimateMessagesTokensSafe(messages),
|
|
150
|
+
}, sendErr);
|
|
151
|
+
}
|
|
152
|
+
return { action: 'proceed', response };
|
|
153
|
+
}
|
|
@@ -13,6 +13,7 @@ import { isAgentOwner } from '../agent-owner.mjs';
|
|
|
13
13
|
import { renameWithRetrySync } from '../../../shared/atomic-file.mjs';
|
|
14
14
|
import { sanitizeContentForStoredHistory } from '../providers/media-normalization.mjs';
|
|
15
15
|
import { scanTopLevelLifecycle } from './lifecycle-scan.mjs';
|
|
16
|
+
import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../../../../lib/mixdog-debug.cjs';
|
|
16
17
|
import {
|
|
17
18
|
summaryIndexPath,
|
|
18
19
|
_sessionSummary,
|
|
@@ -517,8 +518,10 @@ export function markSessionClosed(id, reason = 'manual') {
|
|
|
517
518
|
: 0;
|
|
518
519
|
const _agent = existing.agent || '-';
|
|
519
520
|
const _owner = existing.owner || '-';
|
|
521
|
+
const _toolEventsPath = join(_dataDir, 'tool-events.log');
|
|
522
|
+
rotateBoundedLog(_toolEventsPath, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES);
|
|
520
523
|
void fsp.appendFile(
|
|
521
|
-
|
|
524
|
+
_toolEventsPath,
|
|
522
525
|
`[${_ts}] [session-close] owner=${_owner} agent=${_agent} reason=${reason} lifeMs=${_lifeMs} id=${id}\n`,
|
|
523
526
|
).catch(() => {});
|
|
524
527
|
}
|