mixdog 0.9.2 → 0.9.3
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 +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Compaction policy resolution, pressure/target budgeting, telemetry
|
|
2
|
+
// persistence, and event emission — extracted from loop.mjs.
|
|
3
|
+
// runRecallFastTrackCompact stays in the loop (it drives the recall pipeline
|
|
4
|
+
// against live session state).
|
|
5
|
+
import {
|
|
6
|
+
estimateRequestReserveTokens,
|
|
7
|
+
resolveCompactBufferRatio,
|
|
8
|
+
resolveCompactBufferTokens,
|
|
9
|
+
} from '../context-utils.mjs';
|
|
10
|
+
import {
|
|
11
|
+
compactTypeIsRecallFastTrack,
|
|
12
|
+
compactTypeIsSemantic,
|
|
13
|
+
normalizeCompactType,
|
|
14
|
+
DEFAULT_COMPACT_TYPE,
|
|
15
|
+
DEFAULT_COMPACTION_KEEP_TOKENS,
|
|
16
|
+
} from '../compact.mjs';
|
|
17
|
+
import { positiveTokenInt, envFlag, envTokenInt } from './env.mjs';
|
|
18
|
+
|
|
19
|
+
const COMPACT_SAFETY_PERCENT = 1.00;
|
|
20
|
+
const COMPACT_TARGET_RATIO = 0.02;
|
|
21
|
+
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
22
|
+
const COMPACT_TARGET_MAX_TOKENS = 16_000;
|
|
23
|
+
|
|
24
|
+
function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
|
|
25
|
+
if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
|
|
26
|
+
if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
|
|
27
|
+
if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on') return true;
|
|
28
|
+
// The compact type is already explicit (`semantic` by default). Honor it
|
|
29
|
+
// directly instead of substituting another compaction path.
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveCompactTypeSetting(_sessionRef, cfg = {}) {
|
|
34
|
+
const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
|
|
35
|
+
?? process.env.MIXDOG_COMPACT_TYPE
|
|
36
|
+
?? cfg.type
|
|
37
|
+
?? cfg.compactType
|
|
38
|
+
?? cfg.compact_type;
|
|
39
|
+
return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveCompactTargetRatio(cfg = {}) {
|
|
43
|
+
const raw = cfg.targetPercent
|
|
44
|
+
?? cfg.targetPct
|
|
45
|
+
?? cfg.targetRatio
|
|
46
|
+
?? cfg.targetFraction
|
|
47
|
+
?? process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
48
|
+
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
49
|
+
?? COMPACT_TARGET_RATIO;
|
|
50
|
+
const n = Number(raw);
|
|
51
|
+
if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
|
|
52
|
+
return n > 1 ? n / 100 : n;
|
|
53
|
+
}
|
|
54
|
+
function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
|
|
55
|
+
const boundary = positiveTokenInt(boundaryTokens);
|
|
56
|
+
if (!boundary) return null;
|
|
57
|
+
const explicit = positiveTokenInt(cfg.targetTokens ?? cfg.target)
|
|
58
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_TOKENS')
|
|
59
|
+
|| envTokenInt('MIXDOG_COMPACT_TARGET_TOKENS');
|
|
60
|
+
if (explicit) return Math.max(1, Math.min(boundary, explicit));
|
|
61
|
+
const minTarget = Math.min(boundary, positiveTokenInt(cfg.targetMinTokens ?? cfg.minTargetTokens)
|
|
62
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MIN_TOKENS')
|
|
63
|
+
|| envTokenInt('MIXDOG_COMPACT_TARGET_MIN_TOKENS')
|
|
64
|
+
|| COMPACT_TARGET_MIN_TOKENS);
|
|
65
|
+
const maxTarget = Math.min(boundary, positiveTokenInt(cfg.targetMaxTokens ?? cfg.maxTargetTokens)
|
|
66
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MAX_TOKENS')
|
|
67
|
+
|| envTokenInt('MIXDOG_COMPACT_TARGET_MAX_TOKENS')
|
|
68
|
+
|| COMPACT_TARGET_MAX_TOKENS);
|
|
69
|
+
const byRatio = Math.max(1, Math.floor(boundary * resolveCompactTargetRatio(cfg)));
|
|
70
|
+
return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
|
|
71
|
+
}
|
|
72
|
+
function resolveCompactKeepTokens(cfg = {}) {
|
|
73
|
+
return positiveTokenInt(cfg.keepTokens ?? cfg.keep?.tokens ?? cfg.preserveRecentTokens)
|
|
74
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_KEEP_TOKENS')
|
|
75
|
+
|| DEFAULT_COMPACTION_KEEP_TOKENS;
|
|
76
|
+
}
|
|
77
|
+
export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
78
|
+
if (!sessionRef) return null;
|
|
79
|
+
const cfg = sessionRef.compaction || {};
|
|
80
|
+
const auto = cfg.auto !== false && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
|
|
81
|
+
if (!auto) return { auto: false };
|
|
82
|
+
const contextWindow = positiveTokenInt(sessionRef.contextWindow ?? cfg.contextWindow);
|
|
83
|
+
const explicitBoundary = positiveTokenInt(sessionRef.compactBoundaryTokens ?? cfg.boundaryTokens);
|
|
84
|
+
const autoLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit ?? cfg.autoCompactTokenLimit);
|
|
85
|
+
const boundaryTokens = explicitBoundary && contextWindow
|
|
86
|
+
? Math.min(explicitBoundary, contextWindow)
|
|
87
|
+
: (explicitBoundary || contextWindow || autoLimit);
|
|
88
|
+
if (!boundaryTokens) return null;
|
|
89
|
+
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
90
|
+
// Only an explicit auto-compact limit STRICTLY BELOW the boundary acts as
|
|
91
|
+
// the trigger. A persisted value == boundary (legacy derived full-window
|
|
92
|
+
// autoCompactTokenLimit) would set autoTriggerTokens == boundary and
|
|
93
|
+
// collapse/override the default trigger, so it is ignored in favor of the
|
|
94
|
+
// default boundary trigger.
|
|
95
|
+
const autoTriggerTokens = autoLimit && autoLimit < compactBoundaryTokens ? Math.max(1, autoLimit) : null;
|
|
96
|
+
// Sanitized explicit limit: only a sub-boundary value is a real auto-compact
|
|
97
|
+
// limit. Anything >= boundary is a legacy derived full-window artifact and
|
|
98
|
+
// is reported as null so rememberCompactTelemetry does not re-persist it
|
|
99
|
+
// back onto the session and re-collapse the buffer on the next turn.
|
|
100
|
+
const explicitAutoCompactTokenLimit = autoTriggerTokens;
|
|
101
|
+
const bufferTokens = autoTriggerTokens
|
|
102
|
+
? Math.max(0, compactBoundaryTokens - autoTriggerTokens)
|
|
103
|
+
: resolveCompactBufferTokens(compactBoundaryTokens, cfg);
|
|
104
|
+
const bufferRatio = compactBoundaryTokens ? (bufferTokens / compactBoundaryTokens) : resolveCompactBufferRatio(cfg);
|
|
105
|
+
const triggerTokens = autoTriggerTokens || Math.max(1, compactBoundaryTokens - bufferTokens);
|
|
106
|
+
const configuredReserve = positiveTokenInt(cfg.reservedTokens)
|
|
107
|
+
|| envTokenInt('MIXDOG_AGENT_COMPACT_RESERVED_TOKENS')
|
|
108
|
+
|| 0;
|
|
109
|
+
const requestReserve = estimateRequestReserveTokens(tools);
|
|
110
|
+
const keepTokens = resolveCompactKeepTokens(cfg);
|
|
111
|
+
const compactType = resolveCompactTypeSetting(sessionRef, cfg);
|
|
112
|
+
return {
|
|
113
|
+
auto: true,
|
|
114
|
+
type: compactType,
|
|
115
|
+
compactType,
|
|
116
|
+
prune: cfg.prune === true || envFlag('MIXDOG_AGENT_COMPACT_PRUNE', false),
|
|
117
|
+
boundaryTokens: compactBoundaryTokens,
|
|
118
|
+
triggerTokens,
|
|
119
|
+
bufferTokens,
|
|
120
|
+
bufferRatio,
|
|
121
|
+
contextWindow,
|
|
122
|
+
rawContextWindow: positiveTokenInt(sessionRef.rawContextWindow ?? cfg.rawContextWindow) || contextWindow,
|
|
123
|
+
effectiveContextWindowPercent: Number.isFinite(Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent))
|
|
124
|
+
? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
|
|
125
|
+
: null,
|
|
126
|
+
autoCompactTokenLimit: explicitAutoCompactTokenLimit,
|
|
127
|
+
semantic: compactTypeIsSemantic(compactType) && resolveSemanticCompactSetting(sessionRef, cfg),
|
|
128
|
+
recallFastTrack: compactTypeIsRecallFastTrack(compactType),
|
|
129
|
+
semanticTimeoutMs: positiveTokenInt(cfg.timeoutMs) || envTokenInt('MIXDOG_AGENT_COMPACT_TIMEOUT_MS') || 30_000,
|
|
130
|
+
tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_AGENT_COMPACT_TAIL_TURNS') || 2,
|
|
131
|
+
keepTokens,
|
|
132
|
+
preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('MIXDOG_AGENT_COMPACT_PRESERVE_RECENT_TOKENS') || keepTokens,
|
|
133
|
+
reserveTokens: requestReserve + configuredReserve,
|
|
134
|
+
requestReserveTokens: requestReserve,
|
|
135
|
+
configuredReserveTokens: configuredReserve,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Transcript + request reserve only (never provider lastContextTokens). */
|
|
139
|
+
function compactPressureTokens(messageTokensEst, policy) {
|
|
140
|
+
if (messageTokensEst === null) return 0;
|
|
141
|
+
return Math.max(0, messageTokensEst + (policy?.reserveTokens || 0));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Telemetry pressure when a reactive overflow retry forces the next compact. */
|
|
145
|
+
export function compactionTelemetryPressureTokens(messageTokensEst, policy, { reactivePending = false } = {}) {
|
|
146
|
+
const base = compactPressureTokens(messageTokensEst, policy);
|
|
147
|
+
if (!reactivePending) return base;
|
|
148
|
+
const floor = positiveTokenInt(policy?.triggerTokens) || positiveTokenInt(policy?.boundaryTokens) || 0;
|
|
149
|
+
return floor ? Math.max(base, floor) : base;
|
|
150
|
+
}
|
|
151
|
+
export function compactTargetBudget(policy) {
|
|
152
|
+
const boundary = positiveTokenInt(policy?.boundaryTokens);
|
|
153
|
+
if (!boundary) return null;
|
|
154
|
+
const reserve = Math.max(0, Number(policy?.reserveTokens) || 0);
|
|
155
|
+
const targetEffective = resolveCompactTargetTokens(boundary, policy) || boundary;
|
|
156
|
+
return Math.max(1, Math.min(boundary, targetEffective + reserve));
|
|
157
|
+
}
|
|
158
|
+
export function shouldCompactForSession(messageTokensEst, policy, { forceReactive = false } = {}) {
|
|
159
|
+
if (!policy?.auto || !policy.boundaryTokens) return false;
|
|
160
|
+
if (forceReactive) return true;
|
|
161
|
+
if (messageTokensEst === null) return true;
|
|
162
|
+
return compactPressureTokens(messageTokensEst, policy) >= (policy.triggerTokens || policy.boundaryTokens);
|
|
163
|
+
}
|
|
164
|
+
export function countPrunedToolOutputs(before, after) {
|
|
165
|
+
if (!Array.isArray(before) || !Array.isArray(after)) return 0;
|
|
166
|
+
let count = 0;
|
|
167
|
+
const n = Math.min(before.length, after.length);
|
|
168
|
+
for (let i = 0; i < n; i += 1) {
|
|
169
|
+
if (before[i]?.role !== 'tool' || after[i]?.role !== 'tool') continue;
|
|
170
|
+
if (before[i]?.content !== after[i]?.content && after[i]?.compactedKind === 'tool_output_prune') count += 1;
|
|
171
|
+
}
|
|
172
|
+
return count;
|
|
173
|
+
}
|
|
174
|
+
export function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
175
|
+
if (!sessionRef || !policy) return;
|
|
176
|
+
const prev = sessionRef.compaction && typeof sessionRef.compaction === 'object'
|
|
177
|
+
? sessionRef.compaction
|
|
178
|
+
: {};
|
|
179
|
+
const changed = meta.compactChanged === true || meta.pruneCount > 0;
|
|
180
|
+
sessionRef.compaction = {
|
|
181
|
+
...prev,
|
|
182
|
+
auto: policy.auto !== false,
|
|
183
|
+
prune: policy.prune === true,
|
|
184
|
+
reservedTokens: policy.configuredReserveTokens || prev.reservedTokens || null,
|
|
185
|
+
requestReserveTokens: policy.requestReserveTokens || 0,
|
|
186
|
+
reserveTokens: policy.reserveTokens || 0,
|
|
187
|
+
boundaryTokens: policy.boundaryTokens || null,
|
|
188
|
+
triggerTokens: policy.triggerTokens || null,
|
|
189
|
+
bufferTokens: policy.bufferTokens || 0,
|
|
190
|
+
bufferRatio: policy.bufferRatio ?? prev.bufferRatio ?? null,
|
|
191
|
+
contextWindow: policy.contextWindow || null,
|
|
192
|
+
rawContextWindow: policy.rawContextWindow || null,
|
|
193
|
+
effectiveContextWindowPercent: policy.effectiveContextWindowPercent ?? null,
|
|
194
|
+
autoCompactTokenLimit: policy.autoCompactTokenLimit || null,
|
|
195
|
+
type: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
196
|
+
compactType: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
197
|
+
semantic: policy.semantic === true ? 'auto' : false,
|
|
198
|
+
recallFastTrack: policy.recallFastTrack === true,
|
|
199
|
+
semanticModel: policy.semanticModel || null,
|
|
200
|
+
semanticTimeoutMs: policy.semanticTimeoutMs || null,
|
|
201
|
+
tailTurns: policy.tailTurns || null,
|
|
202
|
+
keepTokens: policy.keepTokens || null,
|
|
203
|
+
preserveRecentTokens: policy.preserveRecentTokens || null,
|
|
204
|
+
lastCheckedAt: Date.now(),
|
|
205
|
+
lastBeforeTokens: meta.beforeTokens ?? null,
|
|
206
|
+
lastAfterTokens: meta.afterTokens ?? null,
|
|
207
|
+
lastPressureTokens: meta.pressureTokens ?? null,
|
|
208
|
+
currentEstimatedTokens: meta.pressureTokens ?? prev.currentEstimatedTokens ?? null,
|
|
209
|
+
lastApiRequestTokens: positiveTokenInt(sessionRef?.lastContextTokens) || prev.lastApiRequestTokens || null,
|
|
210
|
+
lastStage: meta.stage || prev.lastStage || null,
|
|
211
|
+
lastChanged: changed,
|
|
212
|
+
lastTrigger: meta.trigger || prev.lastTrigger || null,
|
|
213
|
+
lastSemantic: meta.semanticCompact === true,
|
|
214
|
+
lastSemanticError: Object.hasOwn(meta, 'semanticError')
|
|
215
|
+
? (meta.semanticError ?? null)
|
|
216
|
+
: (prev.lastSemanticError ?? null),
|
|
217
|
+
lastRecallFastTrack: meta.recallFastTrack === true,
|
|
218
|
+
lastRecallFastTrackError: Object.hasOwn(meta, 'recallFastTrackError')
|
|
219
|
+
? (meta.recallFastTrackError ?? null)
|
|
220
|
+
: (prev.lastRecallFastTrackError ?? null),
|
|
221
|
+
lastError: Object.hasOwn(meta, 'compactError') || Object.hasOwn(meta, 'lastError')
|
|
222
|
+
? (meta.compactError ?? meta.lastError ?? null)
|
|
223
|
+
: (prev.lastError ?? null),
|
|
224
|
+
lastPruneCount: meta.pruneCount || 0,
|
|
225
|
+
lastDurationMs: meta.durationMs != null && Number.isFinite(Number(meta.durationMs))
|
|
226
|
+
? Math.max(0, Math.round(Number(meta.durationMs)))
|
|
227
|
+
: null,
|
|
228
|
+
compactCount: (prev.compactCount || 0) + (changed ? 1 : 0),
|
|
229
|
+
};
|
|
230
|
+
if (changed) {
|
|
231
|
+
const changedAt = Date.now();
|
|
232
|
+
sessionRef.compaction.lastChangedAt = changedAt;
|
|
233
|
+
sessionRef.compaction.lastCompactAt = changedAt;
|
|
234
|
+
sessionRef.lastContextTokensStaleAfterCompact = true;
|
|
235
|
+
}
|
|
236
|
+
sessionRef.contextWindow = policy.contextWindow || sessionRef.contextWindow;
|
|
237
|
+
sessionRef.rawContextWindow = policy.rawContextWindow || sessionRef.rawContextWindow;
|
|
238
|
+
sessionRef.compactBoundaryTokens = policy.boundaryTokens || sessionRef.compactBoundaryTokens || null;
|
|
239
|
+
// Persist only the sanitized (sub-boundary) explicit limit. policy.autoCompactTokenLimit
|
|
240
|
+
// is already null for legacy derived full-window values, so a stale
|
|
241
|
+
// boundary-sized autoCompactTokenLimit on the session is cleared here rather
|
|
242
|
+
// than carried forward to re-collapse the buffer next turn.
|
|
243
|
+
{
|
|
244
|
+
const _boundary = positiveTokenInt(sessionRef.compactBoundaryTokens);
|
|
245
|
+
const _prevLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit);
|
|
246
|
+
const _keepPrev = _prevLimit && (!_boundary || _prevLimit < _boundary) ? _prevLimit : null;
|
|
247
|
+
sessionRef.autoCompactTokenLimit = policy.autoCompactTokenLimit || _keepPrev || null;
|
|
248
|
+
}
|
|
249
|
+
if (policy.effectiveContextWindowPercent !== null) {
|
|
250
|
+
sessionRef.effectiveContextWindowPercent = policy.effectiveContextWindowPercent;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function emitCompactEvent(opts, event = {}) {
|
|
255
|
+
if (!opts || typeof opts.onCompactEvent !== 'function') return;
|
|
256
|
+
try { opts.onCompactEvent({ ts: Date.now(), ...event }); }
|
|
257
|
+
catch { /* best-effort UI/log hook */ }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function compactEventType(policy, fallback = DEFAULT_COMPACT_TYPE) {
|
|
261
|
+
return policy?.compactType || policy?.type || fallback;
|
|
262
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Agent context-overflow error, extracted from loop.mjs. Raised when the latest
|
|
2
|
+
// turn cannot fit the target context budget even after compaction.
|
|
3
|
+
|
|
4
|
+
export class AgentContextOverflowError extends Error {
|
|
5
|
+
constructor({ stage, sessionId, provider, model, contextWindow, budgetTokens, reserveTokens, messageTokensEst }, cause) {
|
|
6
|
+
const target = [provider, model].filter(Boolean).join('/') || 'target model';
|
|
7
|
+
const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
|
|
8
|
+
super(
|
|
9
|
+
`agent context overflow (${target}, stage=${stage || 'compact'}): ` +
|
|
10
|
+
`latest turn cannot fit target context budget=${budgetTokens ?? 'unknown'} ` +
|
|
11
|
+
`reserve=${reserveTokens ?? 'unknown'} contextWindow=${contextWindow ?? 'unknown'} ` +
|
|
12
|
+
`messageTokensEst=${messageTokensEst ?? 'unknown'}${causeMsg}`,
|
|
13
|
+
);
|
|
14
|
+
this.name = 'AgentContextOverflowError';
|
|
15
|
+
this.code = 'AGENT_CONTEXT_OVERFLOW';
|
|
16
|
+
this.sessionId = sessionId || null;
|
|
17
|
+
this.provider = provider || null;
|
|
18
|
+
this.model = model || null;
|
|
19
|
+
this.contextWindow = contextWindow ?? null;
|
|
20
|
+
this.budgetTokens = budgetTokens ?? null;
|
|
21
|
+
this.reserveTokens = reserveTokens ?? null;
|
|
22
|
+
this.messageTokensEst = messageTokensEst ?? null;
|
|
23
|
+
if (cause) this.cause = cause;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function agentContextOverflowError({ stage, sessionId, sessionRef, model, budgetTokens, reserveTokens, messageTokensEst }, cause) {
|
|
28
|
+
return new AgentContextOverflowError({
|
|
29
|
+
stage,
|
|
30
|
+
sessionId,
|
|
31
|
+
provider: sessionRef?.provider || null,
|
|
32
|
+
model: sessionRef?.model || model || null,
|
|
33
|
+
contextWindow: sessionRef?.contextWindow ?? null,
|
|
34
|
+
budgetTokens,
|
|
35
|
+
reserveTokens,
|
|
36
|
+
messageTokensEst,
|
|
37
|
+
}, cause);
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Env-var / token parsing helpers extracted from loop.mjs.
|
|
2
|
+
|
|
3
|
+
export function positiveTokenInt(value) {
|
|
4
|
+
const n = Number(value);
|
|
5
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
6
|
+
}
|
|
7
|
+
export function envFlag(name, fallback = false) {
|
|
8
|
+
const v = process.env[name];
|
|
9
|
+
if (v === undefined) return fallback;
|
|
10
|
+
return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
|
|
11
|
+
}
|
|
12
|
+
export function envTokenInt(name) {
|
|
13
|
+
return positiveTokenInt(process.env[name]);
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Hidden (sub-agent) role registry, extracted from loop.mjs.
|
|
2
|
+
// Source of truth: defaults/agents.json — read once and cached. HIDDEN_AGENT_NAMES
|
|
3
|
+
// is built eagerly so it stays in sync with the declarative registry (no
|
|
4
|
+
// hardcoded duplicate).
|
|
5
|
+
import { readFileSync as _readFileSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, resolve as resolvePath } from 'path';
|
|
8
|
+
|
|
9
|
+
const _AGENTS_JSON = resolvePath(dirname(fileURLToPath(import.meta.url)), '../../../../../defaults/agents.json');
|
|
10
|
+
let _hiddenAgentsCache = null;
|
|
11
|
+
function _getHiddenAgents() {
|
|
12
|
+
if (_hiddenAgentsCache) return _hiddenAgentsCache;
|
|
13
|
+
try {
|
|
14
|
+
_hiddenAgentsCache = JSON.parse(_readFileSync(_AGENTS_JSON, 'utf8'));
|
|
15
|
+
} catch { _hiddenAgentsCache = { agents: [] }; }
|
|
16
|
+
return _hiddenAgentsCache;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const HIDDEN_AGENT_NAMES = new Set(
|
|
20
|
+
(_getHiddenAgents().agents || []).map((r) => r && r.agent).filter((n) => typeof n === 'string' && n.length > 0)
|
|
21
|
+
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Shared pre-dispatch deny — single source of truth for the remaining
|
|
2
|
+
// control-plane / role scoping rejects. Called by BOTH the eager dispatch
|
|
3
|
+
// path (startEagerTool) and the serial dispatch path (executeTool body).
|
|
4
|
+
// Returns null when the call is allowed to proceed; otherwise returns the
|
|
5
|
+
// Error string the serial path would emit. The eager caller ignores the
|
|
6
|
+
// message body and just treats non-null as "do not start eager".
|
|
7
|
+
//
|
|
8
|
+
// This is NOT a permission gate — runtime permission enforcement was removed
|
|
9
|
+
// (every tool call is trusted). What remains is architectural scoping:
|
|
10
|
+
// agent workers are sandboxed to code/research tools. They must never reach
|
|
11
|
+
// owner/host control surfaces: session management, the ENTIRE channels module
|
|
12
|
+
// (Discord messaging, schedules, webhook/config, channel-bridge toggle,
|
|
13
|
+
// command injection), or host input injection. Explicit name list (no imports)
|
|
14
|
+
// keeps this hot-path gate dependency-free; add new owner/channel tools here.
|
|
15
|
+
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
16
|
+
|
|
17
|
+
const WORKER_DENIED_TOOLS = new Set([
|
|
18
|
+
// session control-plane — unified into the single `agent` tool
|
|
19
|
+
// (type=spawn|send|close|list). Denying the one name blocks all worker
|
|
20
|
+
// session control.
|
|
21
|
+
'agent',
|
|
22
|
+
// channels module (owner/Discord-facing)
|
|
23
|
+
'reply', 'react', 'edit_message', 'download_attachment', 'fetch',
|
|
24
|
+
'schedule_status', 'trigger_schedule', 'schedule_control',
|
|
25
|
+
'activate_channel_bridge', 'reload_config', 'inject_command',
|
|
26
|
+
// host input injection
|
|
27
|
+
'inject_input',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function _preDispatchDeny(call, toolKind, sessionRef) {
|
|
31
|
+
const name = call?.name;
|
|
32
|
+
if (typeof name !== 'string' || !name) return null;
|
|
33
|
+
const _agentOwned = sessionRef?.scope?.startsWith?.('agent:')
|
|
34
|
+
|| isAgentOwner(sessionRef);
|
|
35
|
+
const _controlPlaneTool = WORKER_DENIED_TOOLS.has(name);
|
|
36
|
+
if (_agentOwned && _controlPlaneTool) {
|
|
37
|
+
return `Error: control-plane tool "${name}" is Lead-only and not available to agent workers.`;
|
|
38
|
+
}
|
|
39
|
+
const noToolAgent = sessionRef?.agent === 'cycle1-agent' || sessionRef?.agent === 'cycle2-agent';
|
|
40
|
+
if (noToolAgent) {
|
|
41
|
+
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).`;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Exported for smoke tests — same runtime deny as the agent loop. */
|
|
47
|
+
export function preDispatchDenyForSession(sessionRef, call, toolKind = 'builtin') {
|
|
48
|
+
return _preDispatchDeny(call, toolKind, sessionRef);
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Steering-message normalization/merge helpers extracted from loop.mjs.
|
|
2
|
+
// Merges queued steering entries into a single content payload + display text.
|
|
3
|
+
|
|
4
|
+
export function steeringContentText(content) {
|
|
5
|
+
if (typeof content === 'string') return content;
|
|
6
|
+
if (Array.isArray(content)) {
|
|
7
|
+
return content.map((part) => {
|
|
8
|
+
if (typeof part === 'string') return part;
|
|
9
|
+
if (part?.type === 'text') return part.text || '';
|
|
10
|
+
if (part?.type === 'image') return '[Image]';
|
|
11
|
+
return part?.text || '';
|
|
12
|
+
}).filter(Boolean).join('\n');
|
|
13
|
+
}
|
|
14
|
+
return String(content ?? '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeSteeringEntry(entry) {
|
|
18
|
+
if (typeof entry === 'string') {
|
|
19
|
+
const text = entry.trim();
|
|
20
|
+
return text ? { content: text, text } : null;
|
|
21
|
+
}
|
|
22
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
23
|
+
const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : entry;
|
|
24
|
+
const text = typeof entry.text === 'string' ? entry.text.trim() : steeringContentText(content).trim();
|
|
25
|
+
if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
|
|
26
|
+
if (typeof content === 'string') {
|
|
27
|
+
const value = content.trim();
|
|
28
|
+
return value ? { content: value, text: text || value } : null;
|
|
29
|
+
}
|
|
30
|
+
const fallback = steeringContentText(content).trim();
|
|
31
|
+
return fallback ? { content: fallback, text: text || fallback } : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mergeSteeringEntries(entries) {
|
|
35
|
+
const normalized = (Array.isArray(entries) ? entries : [])
|
|
36
|
+
.map(normalizeSteeringEntry)
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
if (normalized.length === 0) return null;
|
|
39
|
+
const displayText = normalized.map((entry) => entry.text || steeringContentText(entry.content))
|
|
40
|
+
.filter((text) => String(text || '').trim())
|
|
41
|
+
.join('\n');
|
|
42
|
+
if (normalized.every((entry) => typeof entry.content === 'string')) {
|
|
43
|
+
return {
|
|
44
|
+
content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
|
|
45
|
+
text: displayText,
|
|
46
|
+
count: normalized.length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const parts = [];
|
|
50
|
+
for (const entry of normalized) {
|
|
51
|
+
if (typeof entry.content === 'string') {
|
|
52
|
+
if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
|
|
53
|
+
} else if (Array.isArray(entry.content)) {
|
|
54
|
+
parts.push(...entry.content);
|
|
55
|
+
} else {
|
|
56
|
+
const text = steeringContentText(entry.content);
|
|
57
|
+
if (text.trim()) parts.push({ type: 'text', text });
|
|
58
|
+
}
|
|
59
|
+
parts.push({ type: 'text', text: '\n' });
|
|
60
|
+
}
|
|
61
|
+
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
62
|
+
return { content: parts, text: displayText || steeringContentText(parts), count: normalized.length };
|
|
63
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Stored tool-call argument compaction/restoration, extracted from loop.mjs.
|
|
2
|
+
// Long body/command args are truncated with a sha256-tagged head/tail preview
|
|
3
|
+
// when persisted into assistant history; the FULL body of a single call can be
|
|
4
|
+
// restored on retry (e.g. a failed edit) so the model sees the original patch.
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
|
|
7
|
+
const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rewrite)$/i;
|
|
8
|
+
const STORED_TOOL_ARG_LONG_KEY_RE = /^(?:command|script)$/i;
|
|
9
|
+
const STORED_TOOL_ARG_BODY_LIMIT = 2_000;
|
|
10
|
+
const STORED_TOOL_ARG_LONG_LIMIT = 8_000;
|
|
11
|
+
const STORED_TOOL_ARG_PREVIEW_HEAD = 360;
|
|
12
|
+
const STORED_TOOL_ARG_PREVIEW_TAIL = 160;
|
|
13
|
+
|
|
14
|
+
function compactStoredToolArgString(value, key = '') {
|
|
15
|
+
if (typeof value !== 'string') return value;
|
|
16
|
+
const isBody = STORED_TOOL_ARG_BODY_KEY_RE.test(key);
|
|
17
|
+
const isLong = isBody || STORED_TOOL_ARG_LONG_KEY_RE.test(key);
|
|
18
|
+
const limit = isBody ? STORED_TOOL_ARG_BODY_LIMIT : (isLong ? STORED_TOOL_ARG_LONG_LIMIT : Infinity);
|
|
19
|
+
if (value.length <= limit) return value;
|
|
20
|
+
const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
21
|
+
const head = value.slice(0, STORED_TOOL_ARG_PREVIEW_HEAD).replace(/\r\n/g, '\n');
|
|
22
|
+
const tail = value.slice(-STORED_TOOL_ARG_PREVIEW_TAIL).replace(/\r\n/g, '\n');
|
|
23
|
+
return `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}]\n${head}\n... [middle omitted from stored tool-call args] ...\n${tail}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function compactStoredToolArgValue(value, key = '', depth = 0) {
|
|
27
|
+
if (value === null || value === undefined) return value;
|
|
28
|
+
if (typeof value === 'string') return compactStoredToolArgString(value, key);
|
|
29
|
+
if (typeof value !== 'object') return value;
|
|
30
|
+
if (depth >= 6) return Array.isArray(value) ? `[${value.length} items]` : '{...}';
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.map((item) => compactStoredToolArgValue(item, key, depth + 1));
|
|
33
|
+
}
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [k, v] of Object.entries(value)) {
|
|
36
|
+
out[k] = compactStoredToolArgValue(v, k, depth + 1);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function compactToolCallsForHistory(calls) {
|
|
42
|
+
if (!Array.isArray(calls)) return calls;
|
|
43
|
+
return calls.map((call) => {
|
|
44
|
+
if (!call || typeof call !== 'object') return call;
|
|
45
|
+
return {
|
|
46
|
+
...call,
|
|
47
|
+
arguments: compactStoredToolArgValue(call.arguments),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Restore the FULL body of ONE tool call inside a history assistant message
|
|
53
|
+
// whose toolCalls were compacted at push time. Used for a failed edit call so
|
|
54
|
+
// the model sees the original patch/old_string on retry instead of a
|
|
55
|
+
// `[mixdog compacted …]` placeholder it cannot act on. Must run BEFORE the
|
|
56
|
+
// message is first transmitted so it never mutates an already-cached prefix
|
|
57
|
+
// (the prompt cache is content-prefix matched).
|
|
58
|
+
//
|
|
59
|
+
// Only the compactable body/long keys (patch, old_string, new_string, content,
|
|
60
|
+
// rewrite, command, script) are restored, and at ANY depth — compaction is
|
|
61
|
+
// recursive (compactStoredToolArgValue), so batch shapes like edits[].old_string
|
|
62
|
+
// or writes[].content carry nested compacted bodies too. Every other field
|
|
63
|
+
// (e.g. `path`, which a tool may mutate in place during execution) is taken from
|
|
64
|
+
// the compacted snapshot captured at push time, before any mutation. The
|
|
65
|
+
// compacted args tree is built fresh by compactToolCallsForHistory and is not
|
|
66
|
+
// shared with originalCalls, so rebuilding it here is safe.
|
|
67
|
+
export function restoreToolCallBodyForId(assistantMsg, originalCalls, callId) {
|
|
68
|
+
if (!assistantMsg || !Array.isArray(assistantMsg.toolCalls) || !callId) return;
|
|
69
|
+
if (!Array.isArray(originalCalls)) return;
|
|
70
|
+
const tc = assistantMsg.toolCalls.find((t) => t && t.id === callId);
|
|
71
|
+
const orig = originalCalls.find((c) => c && c.id === callId);
|
|
72
|
+
if (!tc || !orig) return;
|
|
73
|
+
if (!tc.arguments || typeof tc.arguments !== 'object'
|
|
74
|
+
|| !orig.arguments || typeof orig.arguments !== 'object') return;
|
|
75
|
+
tc.arguments = _restoreCompactedBodies(tc.arguments, orig.arguments, '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Recursively rebuild a compacted args tree: replace ONLY compactable body/long
|
|
79
|
+
// string fields (matched by key at any depth) with their full originals, and
|
|
80
|
+
// keep every other field from the compacted snapshot. tcVal and origVal share
|
|
81
|
+
// the same structure (compaction only shortens body strings), so the walk
|
|
82
|
+
// descends them in parallel; a missing or non-object origVal falls back to the
|
|
83
|
+
// compacted value rather than throwing.
|
|
84
|
+
function _restoreCompactedBodies(tcVal, origVal, key) {
|
|
85
|
+
if ((STORED_TOOL_ARG_BODY_KEY_RE.test(key) || STORED_TOOL_ARG_LONG_KEY_RE.test(key))
|
|
86
|
+
&& typeof origVal === 'string') {
|
|
87
|
+
return origVal;
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(tcVal) && Array.isArray(origVal)) {
|
|
90
|
+
return tcVal.map((item, i) => _restoreCompactedBodies(item, origVal[i], key));
|
|
91
|
+
}
|
|
92
|
+
if (tcVal && typeof tcVal === 'object' && origVal && typeof origVal === 'object') {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const k of Object.keys(tcVal)) {
|
|
95
|
+
out[k] = (k in origVal) ? _restoreCompactedBodies(tcVal[k], origVal[k], k) : tcVal[k];
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
return tcVal;
|
|
100
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Tool-name classification + intra-turn signature helpers, extracted from
|
|
2
|
+
// loop.mjs. These drive cross-turn read dedup, scoped caching, shell routing,
|
|
3
|
+
// and duplicate-call detection. Strips the MCP prefix so direct calls and
|
|
4
|
+
// MCP-wrapped calls share the same cache.
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
|
|
7
|
+
export const MCP_TOOL_PREFIX = 'mcp__plugin_mixdog_mixdog__';
|
|
8
|
+
|
|
9
|
+
export function _stripMcpPrefix(name) {
|
|
10
|
+
return typeof name === 'string' && name.startsWith(MCP_TOOL_PREFIX)
|
|
11
|
+
? name.slice(MCP_TOOL_PREFIX.length) : name;
|
|
12
|
+
}
|
|
13
|
+
export function _isReadTool(name) {
|
|
14
|
+
return _stripMcpPrefix(name) === 'read';
|
|
15
|
+
}
|
|
16
|
+
export function _isMutationTool(name) {
|
|
17
|
+
const n = _stripMcpPrefix(name);
|
|
18
|
+
return n === 'apply_patch';
|
|
19
|
+
}
|
|
20
|
+
export const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
21
|
+
'code_graph',
|
|
22
|
+
'grep',
|
|
23
|
+
'list',
|
|
24
|
+
'glob',
|
|
25
|
+
]);
|
|
26
|
+
export function _isScopedCacheableTool(name) {
|
|
27
|
+
const n = _stripMcpPrefix(name);
|
|
28
|
+
return SCOPED_CACHEABLE_TOOLS.has(n);
|
|
29
|
+
}
|
|
30
|
+
export function _isShellTool(name) {
|
|
31
|
+
const n = _stripMcpPrefix(name);
|
|
32
|
+
return n === 'shell' || n === 'bash_session';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Canonical signature for intra-turn duplicate detection. Sorting keys
|
|
36
|
+
// produces a stable hash regardless of arg-object key order. Anything
|
|
37
|
+
// non-serializable falls back to String(args) — still deterministic for
|
|
38
|
+
// the model's typical structured-arg shape.
|
|
39
|
+
export function _canonicalArgs(args) {
|
|
40
|
+
if (args == null || typeof args !== 'object') {
|
|
41
|
+
try { return JSON.stringify(args); } catch { return String(args); }
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const keys = Object.keys(args).sort();
|
|
45
|
+
const sorted = {};
|
|
46
|
+
for (const k of keys) sorted[k] = args[k];
|
|
47
|
+
return JSON.stringify(sorted);
|
|
48
|
+
} catch { return String(args); }
|
|
49
|
+
}
|
|
50
|
+
export function _intraTurnSig(name, args) {
|
|
51
|
+
return createHash('sha256').update(`${name}:${_canonicalArgs(args)}`).digest('hex').slice(0, 16);
|
|
52
|
+
}
|