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.
Files changed (154) hide show
  1. package/package.json +2 -1
  2. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  3. package/scripts/build-tui.mjs +13 -1
  4. package/scripts/explore-bench.mjs +124 -0
  5. package/scripts/hook-bus-test.mjs +191 -0
  6. package/scripts/path-suffix-test.mjs +57 -0
  7. package/scripts/recall-bench.mjs +207 -0
  8. package/scripts/tool-smoke.mjs +7 -4
  9. package/src/agents/debugger/AGENT.md +2 -2
  10. package/src/agents/heavy-worker/AGENT.md +20 -11
  11. package/src/agents/reviewer/AGENT.md +2 -2
  12. package/src/agents/worker/AGENT.md +17 -11
  13. package/src/mixdog-session-runtime.mjs +424 -1812
  14. package/src/repl.mjs +5 -5
  15. package/src/rules/agent/30-explorer.md +8 -11
  16. package/src/rules/lead/lead-tool.md +9 -5
  17. package/src/rules/shared/01-tool.md +11 -5
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
  19. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
  21. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
  24. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
  27. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  28. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  29. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
  30. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
  31. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
  32. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
  33. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
  34. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
  35. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  36. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  37. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  38. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  39. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  40. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  41. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  42. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  43. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  44. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  45. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  46. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  47. package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
  48. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  49. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  50. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  51. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  52. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  53. package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
  54. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
  55. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
  56. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  59. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  62. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  64. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
  65. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  69. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  71. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  73. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  74. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  75. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  76. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  77. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  78. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  79. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
  80. package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
  81. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  82. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  83. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  84. package/src/runtime/channels/index.mjs +14 -233
  85. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  86. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  87. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  88. package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
  89. package/src/runtime/channels/lib/telegram-format.mjs +19 -22
  90. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  91. package/src/runtime/memory/index.mjs +314 -359
  92. package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
  93. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  94. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  95. package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
  96. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  97. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  98. package/src/runtime/memory/lib/memory.mjs +20 -0
  99. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  100. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  101. package/src/runtime/memory/tool-defs.mjs +4 -4
  102. package/src/runtime/shared/abort-controller.mjs +1 -1
  103. package/src/runtime/shared/background-tasks.mjs +2 -3
  104. package/src/runtime/shared/buffered-appender.mjs +149 -0
  105. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  106. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  107. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  108. package/src/runtime/shared/transcript-writer.mjs +29 -2
  109. package/src/session-runtime/config-helpers.mjs +209 -0
  110. package/src/session-runtime/effort.mjs +128 -0
  111. package/src/session-runtime/fs-utils.mjs +10 -0
  112. package/src/session-runtime/model-capabilities.mjs +130 -0
  113. package/src/session-runtime/output-styles.mjs +124 -0
  114. package/src/session-runtime/plugin-mcp.mjs +114 -0
  115. package/src/session-runtime/session-text.mjs +100 -0
  116. package/src/session-runtime/statusline-route.mjs +35 -0
  117. package/src/session-runtime/tool-catalog.mjs +720 -0
  118. package/src/session-runtime/workflow.mjs +358 -0
  119. package/src/standalone/agent-tool.mjs +49 -10
  120. package/src/standalone/channel-worker.mjs +3 -2
  121. package/src/standalone/explore-tool.mjs +10 -3
  122. package/src/standalone/hook-bus.mjs +165 -8
  123. package/src/standalone/opencode-go-login.mjs +121 -0
  124. package/src/standalone/provider-admin.mjs +14 -3
  125. package/src/tui/App.jsx +884 -143
  126. package/src/tui/components/PromptInput.jsx +272 -15
  127. package/src/tui/components/ToolExecution.jsx +20 -10
  128. package/src/tui/components/tool-output-format.mjs +2 -2
  129. package/src/tui/dist/index.mjs +1771 -1947
  130. package/src/tui/engine/agent-envelope.mjs +296 -0
  131. package/src/tui/engine/boot-profile.mjs +21 -0
  132. package/src/tui/engine/labels.mjs +67 -0
  133. package/src/tui/engine/notice-text.mjs +112 -0
  134. package/src/tui/engine/queue-helpers.mjs +161 -0
  135. package/src/tui/engine/session-stats.mjs +46 -0
  136. package/src/tui/engine/tool-call-fields.mjs +23 -0
  137. package/src/tui/engine/tool-result-text.mjs +126 -0
  138. package/src/tui/engine.mjs +311 -851
  139. package/src/tui/input-editing.mjs +58 -8
  140. package/src/tui/input-editing.selection.test.mjs +75 -0
  141. package/src/tui/keyboard-protocol.mjs +2 -2
  142. package/src/tui/lib/voice-recorder.mjs +35 -19
  143. package/src/tui/markdown/format-token.mjs +7 -8
  144. package/src/tui/markdown/format-token.test.mjs +3 -3
  145. package/src/tui/paste-attachments.mjs +38 -0
  146. package/src/tui/paste-fix.test.mjs +119 -0
  147. package/src/tui/themes/base.mjs +2 -2
  148. package/src/tui/themes/kanagawa.mjs +4 -4
  149. package/src/tui/themes/teal.mjs +4 -5
  150. package/src/tui/themes/utils.mjs +1 -1
  151. package/src/ui/statusline.mjs +49 -0
  152. package/src/workflows/default/WORKFLOW.md +16 -9
  153. package/src/workflows/sequential/WORKFLOW.md +16 -11
  154. 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
+ }