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
@@ -1,12 +1,10 @@
1
1
  import { createRequire } from 'module';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { randomBytes, createHash } from 'crypto';
4
- import { join } from 'path';
5
4
  import { getProvider, providerInputExcludesCache } from '../providers/registry.mjs';
6
- import { getModelMetadataSync } from '../providers/model-catalog.mjs';
7
5
  import { fetchOAuthUsageSnapshot } from '../providers/oauth-usage.mjs';
8
6
  // Image content is kept in-memory and in the model-visible history so multi-turn
9
- // recognition matches reference agent behavior (live transcript always retains images). The
7
+ // recognition works reliably (live transcript always retains images). The
10
8
  // stored-history placeholder swap now happens only at disk-serialization time
11
9
  // inside the session store, so it is no longer imported here.
12
10
  import {
@@ -21,56 +19,72 @@ import {
21
19
  SUMMARY_PREFIX,
22
20
  drainSessionCycle1,
23
21
  } from './compact.mjs';
24
- import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage, resolveCompactTriggerTokens } from './context-utils.mjs';
25
- import { getMcpTools } from '../mcp/client.mjs';
26
- import { getInternalTools, executeInternalTool } from '../internal-tools.mjs';
27
- import { BUILTIN_TOOLS } from '../tools/builtin/builtin-tools.mjs';
28
- import { PATCH_TOOL_DEFS } from '../tools/patch-tool-defs.mjs';
29
- import { CODE_GRAPH_TOOL_DEFS } from '../tools/code-graph-tool-defs.mjs';
30
- import { collectSkillsCached, buildSkillManifest, buildSkillToolDefs, composeSystemPrompt } from '../context/collect.mjs';
22
+ import { estimateMessagesTokens, estimateRequestReserveTokens, estimateTranscriptContextUsage } from './context-utils.mjs';
23
+ import { executeInternalTool } from '../internal-tools.mjs';
24
+ import { collectSkillsCached, buildSkillManifest, composeSystemPrompt } from '../context/collect.mjs';
31
25
  import { saveSession, saveSessionAsync, loadSession, listStoredSessionSummaries, sweepStaleSessions, markSessionClosed, publishHeartbeat, deleteHeartbeat, setLiveSession } from './store.mjs';
32
26
  import { clearReadDedupSession, tryPrefetchCached, setPrefetchCached } from './read-dedup.mjs';
33
27
  import { clearOffloadSession } from './tool-result-offload.mjs';
34
28
  import { classifyResultKind } from './result-classification.mjs';
35
29
  import { createAbortController } from '../../../shared/abort-controller.mjs';
36
- import { isInternalRuntimeNotificationText as contractIsInternalRuntimeNotificationText } from '../../../shared/tool-execution-contract.mjs';
37
30
  import { logLlmCall } from '../../../shared/llm/usage-log.mjs';
38
- import { resolvePluginData, mixdogRoot } from '../../../shared/plugin-paths.mjs';
39
- import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
40
31
  import { appendAgentTrace } from '../agent-trace.mjs';
41
32
  import { isAgentOwner } from '../agent-owner.mjs';
42
- import { maxMtimeRecursive } from '../cache-mtime.mjs';
43
- import { getHiddenAgent, getAgentInstructionDir, listHiddenAgentNames } from '../internal-agents.mjs';
33
+ import { getHiddenAgent } from '../internal-agents.mjs';
44
34
  import { DEFAULT_ACTIVITY_HEARTBEAT_MS } from '../stall-policy.mjs';
45
35
  import {
46
36
  buildGatewayLimits,
47
37
  recordGatewayUsageEvent,
48
38
  summarizeGatewayUsage,
49
39
  } from '../providers/statusline-route-meta.mjs';
50
- // Phase B: Pool B Tier 2 content builder (common rules only).
51
- // Loaded once per process via createRequire so the CJS module reaches us.
52
- const _require = createRequire(import.meta.url);
53
- const _rulesBuilder = (() => {
54
- const candidates = [
55
- join(mixdogRoot(), 'lib', 'rules-builder.cjs'),
56
- ].filter(Boolean);
57
- for (const p of candidates) {
58
- try { return _require(p); } catch { /* fall through */ }
59
- }
60
- // Fallback: walk up from this file's location to find lib/rules-builder.cjs.
61
- try { return _require('../../../../lib/rules-builder.cjs'); } catch { return null; }
62
- })();
63
-
64
- // BP1/BP2/BP3 prompt-layer caches — invalidated by source file mtime, not a
65
- // timer. Cheap: O(sentinel-count) stat calls on each session creation, no file
66
- // I/O when warm.
67
- let _sharedRulesCache = null;
68
- let _sharedRulesMtime = 0;
69
- const _agentRulesCacheByProfile = new Map();
70
- let _leadRulesCache = null;
71
- let _leadRulesMtime = 0;
72
- let _leadMetaCache = null;
73
- let _leadMetaMtime = 0;
40
+ // Split modules see manager/ directory. manager.mjs is now a thin facade that
41
+ // orchestrates these cohesive units while keeping the exact public surface.
42
+ import {
43
+ _buildSharedRules,
44
+ _buildAgentRules,
45
+ _buildLeadRules,
46
+ _buildLeadMetaContext,
47
+ _buildAgentSpecific,
48
+ } from './manager/rules-cache.mjs';
49
+ import {
50
+ applyToolPermissionNarrowing,
51
+ finalizeSessionToolList,
52
+ resolveSessionTools,
53
+ previewSessionTools,
54
+ permissionFromToolSpec,
55
+ } from './manager/tool-resolution.mjs';
56
+ import {
57
+ guessContextWindow,
58
+ positiveContextWindow,
59
+ preserveBufferConfigFields,
60
+ resolveSessionContextMeta,
61
+ compactTriggerForSession,
62
+ compactTargetBudget,
63
+ semanticCompactionEnabledForSession,
64
+ compactTypeForSession,
65
+ } from './manager/context-meta.mjs';
66
+ import {
67
+ promptContentText,
68
+ hasModelVisiblePromptContent,
69
+ promptContentBytes,
70
+ prefixUserTurnContent,
71
+ prefixSessionStartContent,
72
+ buildCurrentTimeBlock,
73
+ buildSessionStartBlock,
74
+ hasUserConversationMessage,
75
+ isInternalRuntimeNotificationText,
76
+ } from './manager/prompt-utils.mjs';
77
+ import {
78
+ _mergePendingMessageEntries,
79
+ enqueuePendingMessage,
80
+ drainPendingMessages,
81
+ _dropPendingMessageState,
82
+ } from './manager/pending-messages.mjs';
83
+ // Re-export split-module public/test surface unchanged so every importer of the
84
+ // old symbol names keeps resolving through the facade.
85
+ export { previewSessionTools };
86
+ export { _mergePendingMessageEntries, enqueuePendingMessage, drainPendingMessages };
87
+ export { isInternalRuntimeNotificationText as _isInternalRuntimeNotificationText };
74
88
  let _codeGraphRuntimePromise = null;
75
89
  let _agentLoopPromise = null;
76
90
  let _bashSessionRuntimePromise = null;
@@ -93,127 +107,9 @@ function _closeBashSessionLazy(sessionId, reason) {
93
107
  .then((mod) => { if (typeof mod.closeBashSession === 'function') mod.closeBashSession(sessionId, reason); })
94
108
  .catch(() => {});
95
109
  }
96
- function _buildSharedRules() {
97
- if (!_rulesBuilder || typeof _rulesBuilder.buildSharedToolContent !== 'function') return '';
98
- const PLUGIN_ROOT = mixdogRoot();
99
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
100
- const mtime = maxMtimeRecursive([
101
- join(RULES_DIR, 'shared'),
102
- ]);
103
- if (_sharedRulesCache !== null && mtime <= _sharedRulesMtime) {
104
- return _sharedRulesCache;
105
- }
106
- try {
107
- const built = _rulesBuilder.buildSharedToolContent({ PLUGIN_ROOT, DATA_DIR: resolvePluginData() });
108
- _sharedRulesCache = built;
109
- _sharedRulesMtime = mtime;
110
- return built;
111
- } catch (e) {
112
- throw new Error(`[session] shared tool rules build failed: ${e.message}`);
113
- }
114
- }
115
-
116
- function _buildAgentRules(profile = 'full') {
117
- if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleContent !== 'function') return '';
118
- const key = String(profile || 'full');
119
- const PLUGIN_ROOT = mixdogRoot();
120
- const DATA_DIR = resolvePluginData();
121
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
122
- const mtime = maxMtimeRecursive([
123
- join(RULES_DIR, 'agent'),
124
- join(DATA_DIR, 'mixdog-config.json'),
125
- ]);
126
- const cached = _agentRulesCacheByProfile.get(key);
127
- if (cached && mtime <= cached.mtime) {
128
- return cached.value;
129
- }
130
- try {
131
- const built = _rulesBuilder.buildAgentRoleContent({ PLUGIN_ROOT, DATA_DIR, profile: key });
132
- _agentRulesCacheByProfile.set(key, { mtime, value: built });
133
- return built;
134
- } catch (e) {
135
- throw new Error(`[session] agent role rules build failed: ${e.message}`);
136
- }
137
- }
138
-
139
- function _buildLeadRules() {
140
- if (!_rulesBuilder || typeof _rulesBuilder.buildLeadRoleContent !== 'function') return '';
141
- const PLUGIN_ROOT = mixdogRoot();
142
- const DATA_DIR = resolvePluginData();
143
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
144
- const mtime = maxMtimeRecursive([
145
- join(RULES_DIR, 'lead'),
146
- join(DATA_DIR, 'mixdog-config.json'),
147
- ]);
148
- if (_leadRulesCache !== null && mtime <= _leadRulesMtime) {
149
- return _leadRulesCache;
150
- }
151
- try {
152
- const built = _rulesBuilder.buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR });
153
- _leadRulesCache = built;
154
- _leadRulesMtime = mtime;
155
- return built;
156
- } catch (e) {
157
- throw new Error(`[session] lead role rules build failed: ${e.message}`);
158
- }
159
- }
160
-
161
- function _buildLeadMetaContext() {
162
- if (!_rulesBuilder || typeof _rulesBuilder.buildLeadMetaContent !== 'function') return '';
163
- const PLUGIN_ROOT = mixdogRoot();
164
- const DATA_DIR = resolvePluginData();
165
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
166
- const mtime = maxMtimeRecursive([
167
- join(RULES_DIR, 'lead'),
168
- join(DATA_DIR, 'history'),
169
- join(DATA_DIR, 'mixdog-config.json'),
170
- join(DATA_DIR, 'user-workflow.md'),
171
- join(PLUGIN_ROOT, 'output-styles'),
172
- join(DATA_DIR, 'output-styles'),
173
- ]);
174
- if (_leadMetaCache !== null && mtime <= _leadMetaMtime) {
175
- return _leadMetaCache;
176
- }
177
- try {
178
- const built = _rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR });
179
- _leadMetaCache = built;
180
- _leadMetaMtime = mtime;
181
- return built;
182
- } catch (e) {
183
- throw new Error(`[session] lead meta context build failed: ${e.message}`);
184
- }
185
- }
186
-
187
- // BP4-adjacent agent-specific data cache — keyed by agent. webhook / schedule
188
- // agents each have their own scoped instruction set; other agents return ''.
189
- const _roleSpecificCache = new Map(); // agent → { value, mtime }
190
- function _buildAgentSpecific(currentAgent) {
191
- if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleSpecificContent !== 'function') return '';
192
- if (!currentAgent) return '';
193
- const PLUGIN_ROOT = mixdogRoot();
194
- const DATA_DIR = resolvePluginData();
195
- const RULES_DIR = join(PLUGIN_ROOT, 'rules');
196
- const roleInstructionDir = getAgentInstructionDir(currentAgent);
197
- const mtime = maxMtimeRecursive([
198
- join(RULES_DIR, 'shared'),
199
- join(DATA_DIR, 'mixdog-config.json'),
200
- join(DATA_DIR, 'webhooks'),
201
- join(DATA_DIR, 'schedules'),
202
- ...(roleInstructionDir ? [join(DATA_DIR, roleInstructionDir)] : []),
203
- join(PLUGIN_ROOT, 'defaults', 'agents.json'),
204
- ]);
205
- const entry = _roleSpecificCache.get(currentAgent);
206
- if (entry && mtime <= entry.mtime) {
207
- return entry.value;
208
- }
209
- try {
210
- const built = _rulesBuilder.buildAgentRoleSpecificContent({ PLUGIN_ROOT, DATA_DIR, currentAgent });
211
- _roleSpecificCache.set(currentAgent, { mtime, value: built });
212
- return built;
213
- } catch (e) {
214
- throw new Error(`[session] agent-specific rules build failed (agent: ${currentAgent}): ${e.message}`);
215
- }
216
- }
110
+ // Re-export the rules builders so any deep importer of the old symbol names
111
+ // keeps working through the facade.
112
+ export { _buildSharedRules, _buildAgentRules, _buildLeadRules, _buildLeadMetaContext, _buildAgentSpecific };
217
113
 
218
114
  // Agent Runtime is optional — injected via setAgentRuntime() during plugin init
219
115
  // so session creation never depends on a circular import. If never injected,
@@ -258,34 +154,10 @@ const HEARTBEAT_THROTTLE_MS = 60_000; // 60s
258
154
  // notification never fires. A slow write finishes in the background.
259
155
  const TERMINAL_SAVE_TIMEOUT_MS = nonNegativeIntEnv('MIXDOG_TERMINAL_SAVE_TIMEOUT_MS', 5_000);
260
156
 
261
- // Merge externally-connected MCP tools with the plugin's in-process tools
262
- // (registered by agent's toolExecutor adapter). Internal tools are exposed
263
- // under their bare names no mcp__ prefix, since the dispatcher in
264
- // server.mjs handles them directly without a transport.
265
- // Sorted deterministically by name — protects BP_1 hash stability from
266
- // listTools() ordering churn. Anthropic / OpenAI / Gemini all hash the
267
- // tools array verbatim, so any reorder rewrites the prefix.
268
- // No cache: getMcpTools() and getInternalTools() are O(n) in-memory reads;
269
- // the sort overhead on ~30 tools is negligible.
270
- function _getMcpTools() {
271
- const mcp = getMcpTools() || [];
272
- const internalRaw = getInternalTools() || [];
273
- const internal = internalRaw.map(t => ({
274
- name: t.name,
275
- description: typeof t.description === 'string' ? t.description : '',
276
- inputSchema: t.inputSchema || { type: 'object', properties: {} },
277
- // Keep annotations so the permission filter / role invariants can
278
- // tell read-only from write-capable internal tools, and so
279
- // agentHidden can be read during deny filtering.
280
- annotations: t.annotations || {},
281
- }));
282
- return [...mcp, ...internal].sort((a, b) => {
283
- const an = a?.name || '';
284
- const bn = b?.name || '';
285
- return an < bn ? -1 : an > bn ? 1 : 0;
286
- });
287
- }
288
-
157
+ // Session tool resolution + permission narrowing moved to
158
+ // manager/tool-resolution.mjs (imported above). The following section-level
159
+ // docs describe the toolSpec contract those helpers implement.
160
+ //
289
161
  // Phase D-2 — profile.tools resolution.
290
162
  //
291
163
  // `toolSpec` may be:
@@ -308,469 +180,7 @@ function _getMcpTools() {
308
180
  // surface intentionally tiny; runtime permission guards in loop.mjs remain
309
181
  // the fail-safe either way.
310
182
 
311
- const SESSION_ROUTE_TOOL_ORDER = [
312
- 'code_graph',
313
- 'find',
314
- 'glob',
315
- 'list',
316
- 'grep',
317
- 'read',
318
- 'apply_patch',
319
- 'shell',
320
- 'task',
321
- ];
322
- const SESSION_ROUTE_TOOL_RANK = new Map(SESSION_ROUTE_TOOL_ORDER.map((name, index) => [name, index]));
323
- const FILESYSTEM_TOOL_NAMES = new Set([
324
- 'code_graph',
325
- 'find',
326
- 'glob',
327
- 'list',
328
- 'grep',
329
- 'read',
330
- 'apply_patch',
331
- ]);
332
- const READONLY_TOOL_NAMES = new Set([
333
- 'code_graph',
334
- 'find',
335
- 'glob',
336
- 'list',
337
- 'grep',
338
- 'read',
339
- ]);
340
-
341
- const AGENT_STRING_PERMISSION_READ_ALLOW = Object.freeze([
342
- 'code_graph',
343
- 'find',
344
- 'glob',
345
- 'list',
346
- 'grep',
347
- 'read',
348
- 'explore',
349
- 'search',
350
- 'web_fetch',
351
- 'Skill',
352
- ]);
353
-
354
- function stringToolPermissionAllowList(toolPermission) {
355
- if (toolPermission === 'read') return AGENT_STRING_PERMISSION_READ_ALLOW;
356
- if (toolPermission === 'read-write') return AGENT_STRING_PERMISSION_READ_WRITE_ALLOW;
357
- if (toolPermission === 'none') return [];
358
- return null;
359
- }
360
-
361
- const AGENT_STRING_PERMISSION_READ_WRITE_ALLOW = Object.freeze([
362
- 'code_graph',
363
- 'find',
364
- 'glob',
365
- 'list',
366
- 'grep',
367
- 'read',
368
- 'apply_patch',
369
- 'explore',
370
- 'search',
371
- 'web_fetch',
372
- 'Skill',
373
- ]);
374
-
375
- function applyToolPermissionNarrowing(tools, toolPermission, warnRole = null) {
376
- if (toolPermission === 'none') return [];
377
- const allowList = stringToolPermissionAllowList(toolPermission);
378
- if (allowList) {
379
- const allowSet = new Set(allowList.map((n) => String(n).toLowerCase()));
380
- return tools.filter((t) => allowSet.has(String(t?.name || '').toLowerCase()));
381
- }
382
- if (toolPermission && typeof toolPermission === 'object') {
383
- const allowSet = Array.isArray(toolPermission.allow) && toolPermission.allow.length > 0
384
- ? new Set(toolPermission.allow.map(n => String(n).toLowerCase()))
385
- : null;
386
- const denySet = Array.isArray(toolPermission.deny) && toolPermission.deny.length > 0
387
- ? new Set(toolPermission.deny.map(n => String(n).toLowerCase()))
388
- : null;
389
- if (allowSet || denySet) {
390
- const filtered = tools.filter(t => {
391
- const name = String(t?.name || '').toLowerCase();
392
- if (denySet && denySet.has(name)) return false;
393
- if (allowSet && !allowSet.has(name)) return false;
394
- return true;
395
- });
396
- if (filtered.length === 0) {
397
- process.stderr.write(`[session] WARN: role permission intersection produced 0 tools — failing closed (role=${warnRole || 'unknown'})\n`);
398
- }
399
- return filtered;
400
- }
401
- }
402
- return tools;
403
- }
404
-
405
- function recursiveWrapperToolNameForPublicAgent(agent) {
406
- if (!agent) return null;
407
- const key = String(agent).trim();
408
- if (key === 'explore') return 'explore';
409
- for (const hiddenName of listHiddenAgentNames()) {
410
- const def = getHiddenAgent(hiddenName);
411
- const invokedBy = typeof def?.invokedBy === 'string' ? def.invokedBy.trim() : '';
412
- if (hiddenName === key && invokedBy) return invokedBy;
413
- if (invokedBy && invokedBy === key) return invokedBy;
414
- }
415
- return null;
416
- }
417
-
418
- function finalizeSessionToolList(tools, {
419
- schemaAllowedTools = null,
420
- disallowedTools = null,
421
- ownerIsAgent = false,
422
- resolvedAgent = null,
423
- } = {}) {
424
- let out = Array.isArray(tools) ? tools : [];
425
- const hasCallerAllow = Array.isArray(schemaAllowedTools);
426
- if (hasCallerAllow) {
427
- const allowSet = new Set(schemaAllowedTools.map(n => String(n).toLowerCase()));
428
- out = out.filter(t => allowSet.has(String(t?.name || '').toLowerCase()));
429
- }
430
- const callerDeny = Array.isArray(disallowedTools) ? disallowedTools.map(n => String(n)) : [];
431
- if (callerDeny.length) {
432
- const denySet = new Set(callerDeny.map(n => n.toLowerCase()));
433
- out = out.filter(t => !denySet.has(String(t?.name || '').toLowerCase()));
434
- }
435
- const recursiveDeny = ownerIsAgent ? recursiveWrapperToolNameForPublicAgent(resolvedAgent) : null;
436
- if (recursiveDeny) {
437
- const deny = recursiveDeny.toLowerCase();
438
- out = out.filter(t => String(t?.name || '').toLowerCase() !== deny);
439
- }
440
- if (ownerIsAgent) {
441
- out = out.filter(t => !t?.annotations?.agentHidden);
442
- out = orderSessionTools(out);
443
- }
444
- return out;
445
- }
446
-
447
- function orderSessionTools(tools) {
448
- return tools.map((tool, index) => ({ tool, index }))
449
- .sort((a, b) => {
450
- const ar = SESSION_ROUTE_TOOL_RANK.get(a.tool?.name) ?? 10_000;
451
- const br = SESSION_ROUTE_TOOL_RANK.get(b.tool?.name) ?? 10_000;
452
- if (ar !== br) return ar - br;
453
- return a.index - b.index;
454
- })
455
- .map((entry) => entry.tool);
456
- }
457
-
458
- const ALL_BUILTIN_SESSION_TOOLS = orderSessionTools(_dedupByName([
459
- ...BUILTIN_TOOLS,
460
- ...PATCH_TOOL_DEFS,
461
- ...CODE_GRAPH_TOOL_DEFS,
462
- ]));
463
-
464
- function resolveSessionTools(toolSpec, skills, { ownerIsAgentSession = false } = {}) {
465
- const mcp = _getMcpTools();
466
- // Agent sessions freeze the skill meta-tool into the schema
467
- // unconditionally — concrete skill resolution is cwd-scoped at tool-call
468
- // time (loop.mjs), so the schema bytes stay bit-identical across roles /
469
- // cwds and the provider cache shard does not fragment.
470
- const skillTools = buildSkillToolDefs(skills, { ownerIsAgentSession });
471
- return _computeBaseTools(toolSpec, mcp, skillTools);
472
- }
473
-
474
- export function previewSessionTools(toolSpec, skills = [], options = {}) {
475
- return resolveSessionTools(toolSpec, skills, options);
476
- }
477
-
478
- // Dedup by name, first occurrence wins. BUILTIN_TOOLS is passed in ahead
479
- // of the MCP-registered internal tools so plugin-side definitions take
480
- // precedence when both surfaces declare the same name (e.g. read / grep / glob).
481
- // Without this merge, Anthropic rejected the request with
482
- // "tools: Tool names must be unique" and the orchestrator burned up to
483
- // 20 iterations retrying before the final answer landed.
484
- function _dedupByName(tools) {
485
- const seen = new Map();
486
- for (const t of tools) {
487
- const n = t?.name;
488
- if (!n || seen.has(n)) continue;
489
- seen.set(n, t);
490
- }
491
- return [...seen.values()];
492
- }
493
-
494
- // Agent visibility is declared per-tool via annotations.agentHidden.
495
- // Tools with agentHidden:true are stripped from agent sessions at schema
496
- // build time (see deny filtering below). No code-level name list needed.
497
-
498
- function _computeBaseTools(toolSpec, mcp, skillTools) {
499
- if (Array.isArray(toolSpec)) {
500
- if (toolSpec.length === 0) {
501
- // Explicit "no tools" — skill meta tools still travel so the model
502
- // can at least discover and invoke skills if that is the one
503
- // dynamic surface the profile retains.
504
- return _dedupByName([...skillTools]);
505
- }
506
- if (toolSpec.includes('full')) {
507
- return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
508
- }
509
- const byName = new Map();
510
- const add = (tool) => { if (tool?.name && !byName.has(tool.name)) byName.set(tool.name, tool); };
511
- const addMany = (arr) => { for (const t of arr) add(t); };
512
- for (const tagRaw of toolSpec) {
513
- const tag = String(tagRaw || '').trim();
514
- switch (tag) {
515
- case 'tools:filesystem':
516
- addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => FILESYSTEM_TOOL_NAMES.has(t.name)));
517
- break;
518
- case 'tools:readonly':
519
- addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name)));
520
- break;
521
- case 'tools:shell':
522
- case 'tools:git':
523
- case 'tools:analysis':
524
- // Shell-class toolset. `tools:git` / `tools:analysis` exist so
525
- // profile authors can name the intent (git workflows / data
526
- // analysis) without inventing new toolset ids.
527
- addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => t.name === 'shell' || t.name === 'task'));
528
- break;
529
- case 'tools:mcp':
530
- addMany(mcp);
531
- break;
532
- case 'tools:search':
533
- // Name-pattern match: picks up `search` and any future tool
534
- // whose name contains `search`. `recall` and `explore` deliberately do NOT match
535
- // — they need `tools:mcp` (full mcp surface) or their own
536
- // toolset id if a role wants targeted retrieval. Public agent
537
- // roles never reach the wrapper bodies regardless: see the
538
- // isBlockedPublicWrapperCall guard in session/loop.mjs.
539
- addMany(mcp.filter(t => /search/i.test(t?.name || '')));
540
- break;
541
- default:
542
- process.stderr.write(`[session] unknown toolset id "${tag}" (profile.tools); skipping\n`);
543
- }
544
- }
545
- return _dedupByName([...byName.values(), ...skillTools]);
546
- }
547
-
548
- switch (toolSpec) {
549
- case 'mcp':
550
- return _dedupByName([...mcp, ...skillTools]);
551
- case 'readonly': {
552
- const readTools = ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name));
553
- return _dedupByName([...readTools, ...mcp, ...skillTools]);
554
- }
555
- case 'full':
556
- default:
557
- return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
558
- }
559
- }
560
-
561
- function permissionFromToolSpec(toolSpec) {
562
- if (toolSpec === 'readonly') return 'read';
563
- if (toolSpec === 'mcp') return 'mcp';
564
- if (Array.isArray(toolSpec)) {
565
- const tags = new Set(toolSpec.map(t => String(t || '').trim()));
566
- const hasWriteOrShell = tags.has('full')
567
- || tags.has('tools:filesystem')
568
- || tags.has('tools:shell')
569
- || tags.has('tools:git')
570
- || tags.has('tools:analysis');
571
- if (tags.has('tools:readonly') && !hasWriteOrShell) return 'read';
572
- }
573
- return null;
574
- }
575
-
576
183
  let nextId = Date.now();
577
- // Known context windows for the current-generation models this plugin
578
- // routes to. Anything not listed falls through to guessContextWindow() —
579
- // local llama/mistral/phi default to 8192, everything else 128000. Keep
580
- // this map trimmed to live models; older generations slow down reads
581
- // without buying anything.
582
- const CONTEXT_WINDOWS = {
583
- // OpenAI GPT-5.x family (openai / openai-oauth)
584
- 'gpt-5.5': 272000,
585
- 'gpt-5.4': 272000,
586
- 'gpt-5.4-mini': 272000,
587
- 'gpt-5.4-nano': 272000,
588
- // Anthropic Claude 4.x
589
- 'claude-opus-4-8': 1000000,
590
- 'claude-opus-4-7': 1000000,
591
- 'claude-sonnet-4-6': 1000000,
592
- 'claude-haiku-4-5-20251001': 200000,
593
- // Google Gemini 3.x
594
- 'gemini-3.1-pro': 1000000,
595
- 'gemini-3-pro': 1000000,
596
- 'gemini-3.5-flash': 1000000,
597
- 'gemini-3-flash': 1000000,
598
- // xAI Grok (catalog polyfill mirror — model-catalog PRICING_OVERRIDES)
599
- 'grok-build-0.1': 256000,
600
- 'grok-4.20': 1000000,
601
- };
602
- // Family-pattern fallback used only when both the provider catalog and the
603
- // exact-id table miss (cold metadata, before the LiteLLM/models.dev catalog
604
- // warms). Keep these aligned with the catalog so /context, gateway, and the
605
- // runtime agree on the boundary the first time a model is routed. Local models
606
- // (llama/mistral/phi/qwen/gemma) stay small so an unknown local id never claims
607
- // a giant window.
608
- function guessContextWindow(model) {
609
- if (CONTEXT_WINDOWS[model])
610
- return CONTEXT_WINDOWS[model];
611
- const m = String(model || '').toLowerCase();
612
- // Local/self-hosted families — never inflate an unknown local id.
613
- if (m.includes('llama') || m.includes('mistral') || m.includes('mixtral')
614
- || m.includes('phi') || m.includes('qwen') || m.includes('gemma')
615
- || m.includes('deepseek-r1') || m.includes('codellama'))
616
- return 8192;
617
- // Current hosted families by name pattern.
618
- if (m.startsWith('claude-opus') || m.startsWith('claude-sonnet')) return 1000000;
619
- if (m.startsWith('claude-haiku') || m.startsWith('claude-')) return 200000;
620
- if (m.startsWith('gemini-3') || m.startsWith('gemini-2')) return 1000000;
621
- if (m.startsWith('gpt-5')) return 272000;
622
- if (m.startsWith('grok-build')) return 256000;
623
- if (m.startsWith('grok-')) return 1000000;
624
- if (m.startsWith('deepseek-v')) return 1000000;
625
- return 128000;
626
- }
627
- function positiveContextWindow(value) {
628
- const n = Number(value);
629
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
630
- }
631
- function envFlag(name, fallback = false) {
632
- const v = process.env[name];
633
- if (v === undefined) return fallback;
634
- return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
635
- }
636
- function boundedPercent(value, fallback = null) {
637
- const n = Number(value);
638
- if (Number.isFinite(n) && n > 0 && n <= 100) return n;
639
- return fallback;
640
- }
641
- function providerNameOf(provider) {
642
- if (typeof provider === 'string') return provider.toLowerCase();
643
- return String(provider?.name || provider?.id || '').toLowerCase();
644
- }
645
- // Carry the percent/ratio-named buffer config from a compaction config object
646
- // onto session.compaction so the shared compact-policy parser honors configured
647
- // buffer
648
- // percent/ratio. Only finite positive values are copied; absent fields stay
649
- // undefined so the default-ratio fallback still applies.
650
- function preserveBufferConfigFields(cfg = {}) {
651
- const out = {};
652
- for (const key of ['bufferPercent', 'bufferPct', 'bufferRatio', 'bufferFraction']) {
653
- const n = Number(cfg?.[key]);
654
- if (Number.isFinite(n) && n > 0) out[key] = n;
655
- }
656
- return out;
657
- }
658
- const COMPACT_TARGET_RATIO = 0.02;
659
- const COMPACT_TARGET_MIN_TOKENS = 4_000;
660
- const COMPACT_TARGET_MAX_TOKENS = 16_000;
661
- function compactTargetRatio() {
662
- const raw = process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
663
- ?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
664
- ?? COMPACT_TARGET_RATIO;
665
- const n = Number(raw);
666
- if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
667
- return n > 1 ? n / 100 : n;
668
- }
669
- function compactTargetTokensForBoundary(boundaryTokens) {
670
- const boundary = positiveContextWindow(boundaryTokens);
671
- if (!boundary) return null;
672
- const explicit = positiveContextWindow(
673
- process.env.MIXDOG_AGENT_COMPACT_TARGET_TOKENS
674
- ?? process.env.MIXDOG_COMPACT_TARGET_TOKENS,
675
- );
676
- if (explicit) return Math.max(1, Math.min(boundary, explicit));
677
- const minTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MIN_TOKENS) || COMPACT_TARGET_MIN_TOKENS);
678
- const maxTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MAX_TOKENS) || COMPACT_TARGET_MAX_TOKENS);
679
- const byRatio = Math.max(1, Math.floor(boundary * compactTargetRatio()));
680
- return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
681
- }
682
- function defaultEffectiveContextWindowPercent(provider) {
683
- // Gateway/statusline route metadata reserves a universal 10% headroom from
684
- // the raw catalog window. Keep session compaction on the same effective
685
- // capacity so /context, the TUI statusline, and gateway telemetry agree.
686
- return 90;
687
- }
688
- const PROVIDER_SYNTHETIC_CONTEXT_DEFAULT = 1_000_000;
689
- function providerRawContextWindow(info, catalogInfo) {
690
- if (!info || typeof info !== 'object') return null;
691
- const fromApiFields = positiveContextWindow(info.context_window)
692
- || positiveContextWindow(info.max_context_window);
693
- if (fromApiFields) return fromApiFields;
694
- const fromCache = positiveContextWindow(info.contextWindow)
695
- || positiveContextWindow(info.maxContextWindow);
696
- if (!fromCache) return null;
697
- const catalogWindow = positiveContextWindow(catalogInfo?.contextWindow)
698
- || positiveContextWindow(catalogInfo?.maxContextWindow)
699
- || positiveContextWindow(catalogInfo?.context_window)
700
- || positiveContextWindow(catalogInfo?.max_context_window);
701
- if (fromCache === PROVIDER_SYNTHETIC_CONTEXT_DEFAULT
702
- && catalogWindow
703
- && fromCache !== catalogWindow) {
704
- return null;
705
- }
706
- return fromCache;
707
- }
708
- function resolveSessionContextMeta(provider, model, seed = {}) {
709
- const info = typeof provider?.getCachedModelInfo === 'function'
710
- ? provider.getCachedModelInfo(model)
711
- : null;
712
- const catalogInfo = getModelMetadataSync(model, providerNameOf(provider));
713
- const rawContextWindow = providerRawContextWindow(info, catalogInfo)
714
- || positiveContextWindow(catalogInfo?.contextWindow)
715
- || positiveContextWindow(catalogInfo?.maxContextWindow)
716
- || positiveContextWindow(catalogInfo?.context_window)
717
- || positiveContextWindow(catalogInfo?.max_context_window)
718
- || positiveContextWindow(seed.rawContextWindow)
719
- || positiveContextWindow(seed.raw_context_window)
720
- || positiveContextWindow(seed.contextWindow)
721
- || guessContextWindow(model);
722
- const effectiveContextWindowPercent = boundedPercent(
723
- seed.effectiveContextWindowPercent
724
- ?? seed.effective_context_window_percent
725
- ?? info?.effectiveContextWindowPercent
726
- ?? info?.effective_context_window_percent
727
- ?? catalogInfo?.effectiveContextWindowPercent
728
- ?? catalogInfo?.effective_context_window_percent,
729
- defaultEffectiveContextWindowPercent(provider),
730
- );
731
- const pct = boundedPercent(effectiveContextWindowPercent, 100);
732
- const contextWindow = Math.max(1, Math.floor(rawContextWindow * pct / 100));
733
- const compactBoundaryTokens = contextWindow;
734
- const rawCompactLimit = positiveContextWindow(
735
- seed.autoCompactTokenLimit
736
- ?? seed.auto_compact_token_limit
737
- ?? info?.autoCompactTokenLimit
738
- ?? info?.auto_compact_token_limit
739
- ?? catalogInfo?.autoCompactTokenLimit
740
- ?? catalogInfo?.auto_compact_token_limit,
741
- );
742
- // Legacy-data migration: old implementations derived autoCompactTokenLimit
743
- // from the full effective/raw window and persisted it onto the session.
744
- // A resumed session therefore re-seeds autoCompactTokenLimit == boundary
745
- // (or the raw window), which compactTriggerForSession / loop policy used to
746
- // honor as an explicit trigger, collapsing the compaction buffer to 0. Only
747
- // accept an explicit limit that is STRICTLY BELOW the boundary; a value at
748
- // or above the boundary is a derived full-window artifact and is dropped to
749
- // null so the trigger falls back to the default boundary trigger.
750
- const explicitCompactLimit = rawCompactLimit && rawCompactLimit < compactBoundaryTokens
751
- ? rawCompactLimit
752
- : null;
753
- // Do NOT derive the auto-compact limit from the full effective window.
754
- // Setting it to contextWindow makes autoTriggerTokens == boundary and the
755
- // compaction buffer collapse to 0 (loop.mjs:708-713 / compactTriggerForSession),
756
- // so auto-compact only fires when the context is already at the limit —
757
- // at which point semantic compact fails ("result exceeds budget" /
758
- // "summary cannot fit") and the turn can no longer be resumed.
759
- // Leave it null unless the provider/catalog/seed supplies an explicit
760
- // limit; the downstream buffer logic (default 10%, capped 25%) then
761
- // triggers compaction with headroom, matching the reference auto-compact threshold.
762
- const autoCompactTokenLimit = explicitCompactLimit || null;
763
- return {
764
- contextWindow,
765
- rawContextWindow,
766
- effectiveContextWindowPercent,
767
- autoCompactTokenLimit: autoCompactTokenLimit || null,
768
- compactBoundaryTokens,
769
- };
770
- }
771
- function compactTriggerForSession(session, boundaryTokens) {
772
- return resolveCompactTriggerTokens(session, boundaryTokens);
773
- }
774
184
  // Test-only exports for the legacy auto-compact-limit migration + buffer-config
775
185
  // preservation (see scripts/compact-trigger-migration-smoke.mjs).
776
186
  export const _resolveSessionContextMeta = resolveSessionContextMeta;
@@ -793,30 +203,6 @@ function normalizeStaleCompactingStage(session) {
793
203
  c.lastCheckedAt = Date.now();
794
204
  return true;
795
205
  }
796
- function compactTargetBudget(boundaryTokens, reserveTokens, _sourceTokens = null, _ratio = null) {
797
- const boundary = positiveContextWindow(boundaryTokens);
798
- if (!boundary) return null;
799
- const reserve = Math.max(0, Number(reserveTokens) || 0);
800
- const targetEffective = compactTargetTokensForBoundary(boundary) || boundary;
801
- return Math.max(1, Math.min(boundary, targetEffective + reserve));
802
- }
803
- function semanticCompactionEnabledForSession(session) {
804
- const cfg = session?.compaction || {};
805
- if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
806
- if (process.env.MIXDOG_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_COMPACT_SEMANTIC', true);
807
- if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
808
- if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on' || cfg.semantic === 'auto') return true;
809
- return true;
810
- }
811
- function compactTypeForSession(session) {
812
- const cfg = session?.compaction || {};
813
- const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
814
- ?? process.env.MIXDOG_COMPACT_TYPE
815
- ?? cfg.type
816
- ?? cfg.compactType
817
- ?? cfg.compact_type;
818
- return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
819
- }
820
206
  function addCompactUsageToSession(session, usage) {
821
207
  if (!session || !usage) return;
822
208
  const inputTokens = usage.inputTokens || 0;
@@ -898,7 +284,7 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
898
284
  }
899
285
  return { query, querySha, recallText: [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n') };
900
286
  }
901
- // Element-identity change detection (mirrors loop.mjs messagesArrayChanged): two
287
+ // Element-identity change detection (same approach as loop.mjs messagesArrayChanged): two
902
288
  // arrays are "unchanged" only when same length AND every slot is the same object
903
289
  // reference. Used to reject a no-op prune (which returns a fresh array whose
904
290
  // elements are the untouched originals) from being accepted as a recovery.
@@ -2562,363 +1948,6 @@ const _sessionLocks = new Map();
2562
1948
  // queue contract, two call sites. Rich content is kept in memory for the live
2563
1949
  // relay path; the disk mirror stores only a text fallback so image bytes do not
2564
1950
  // leak into the pending-message JSON.
2565
- const _sessionPendingMessages = new Map();
2566
- const PENDING_MESSAGES_FILE = 'session-pending-messages.json';
2567
- const PENDING_MESSAGES_MODE = 0o600;
2568
- const _pendingPersistBuffers = new Map();
2569
- let _pendingPersistImmediate = null;
2570
-
2571
- function pendingMessagesPath() {
2572
- return join(resolvePluginData(), PENDING_MESSAGES_FILE);
2573
- }
2574
-
2575
- function isValidPendingSessionId(sessionId) {
2576
- return typeof sessionId === 'string' && /^[A-Za-z0-9_-]+$/.test(sessionId);
2577
- }
2578
-
2579
- function normalizePendingStore(raw) {
2580
- const sessions = raw && typeof raw === 'object' && raw.sessions && typeof raw.sessions === 'object'
2581
- ? raw.sessions
2582
- : {};
2583
- const out = { version: 1, updatedAt: Date.now(), sessions: {} };
2584
- for (const [sid, value] of Object.entries(sessions)) {
2585
- if (!isValidPendingSessionId(sid) || !Array.isArray(value)) continue;
2586
- const q = value
2587
- .map((entry) => {
2588
- if (typeof entry === 'string') return entry;
2589
- if (entry && typeof entry === 'object' && typeof entry.message === 'string') return entry.message;
2590
- return '';
2591
- })
2592
- .filter(Boolean);
2593
- if (q.length > 0) out.sessions[sid] = q;
2594
- }
2595
- return out;
2596
- }
2597
-
2598
- function normalizePendingMessageEntry(entry) {
2599
- if (typeof entry === 'string') {
2600
- const text = entry.trim();
2601
- return text ? { content: text, text } : null;
2602
- }
2603
- if (Array.isArray(entry)) {
2604
- if (entry.length === 0) return null;
2605
- const text = promptContentText(entry).trim();
2606
- return { content: entry, text };
2607
- }
2608
- if (!entry || typeof entry !== 'object') return null;
2609
- const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : null;
2610
- if (content == null) return null;
2611
- const text = typeof entry.text === 'string' ? entry.text.trim() : promptContentText(content).trim();
2612
- if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
2613
- if (typeof content === 'string') {
2614
- const value = content.trim();
2615
- return value ? { content: value, text: text || value } : null;
2616
- }
2617
- const fallback = promptContentText(content).trim();
2618
- return fallback ? { content: fallback, text: text || fallback } : null;
2619
- }
2620
-
2621
- function pendingMessageText(entry) {
2622
- const normalized = normalizePendingMessageEntry(entry);
2623
- return normalized ? String(normalized.text || promptContentText(normalized.content) || '').trim() : '';
2624
- }
2625
-
2626
- function pendingMessageQueueEntry(entry) {
2627
- const normalized = normalizePendingMessageEntry(entry);
2628
- if (!normalized) return null;
2629
- if (typeof normalized.content === 'string' && normalized.content === normalized.text) return normalized.content;
2630
- return { content: normalized.content, text: normalized.text || promptContentText(normalized.content).trim() };
2631
- }
2632
-
2633
- function persistPendingMessages(sessionId, messages) {
2634
- if (!isValidPendingSessionId(sessionId)) return 0;
2635
- const persistedMessages = (Array.isArray(messages) ? messages : [messages])
2636
- .map(pendingMessageText)
2637
- .filter(Boolean);
2638
- if (persistedMessages.length === 0) return 0;
2639
- let depth = 0;
2640
- try {
2641
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
2642
- const next = normalizePendingStore(raw);
2643
- const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
2644
- q.push(...persistedMessages);
2645
- next.sessions[sessionId] = q;
2646
- next.updatedAt = Date.now();
2647
- depth = q.length;
2648
- return next;
2649
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
2650
- } catch (err) {
2651
- try { process.stderr.write(`[session] pending-message persist failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
2652
- }
2653
- return depth;
2654
- }
2655
-
2656
- function flushPendingMessagePersistsSync() {
2657
- if (_pendingPersistImmediate) {
2658
- try { clearImmediate(_pendingPersistImmediate); } catch {}
2659
- _pendingPersistImmediate = null;
2660
- }
2661
- if (_pendingPersistBuffers.size === 0) return;
2662
- const batches = [..._pendingPersistBuffers.entries()];
2663
- _pendingPersistBuffers.clear();
2664
- for (const [sid, messages] of batches) {
2665
- persistPendingMessages(sid, messages);
2666
- }
2667
- }
2668
-
2669
- function schedulePendingMessagePersist(sessionId, message) {
2670
- if (!isValidPendingSessionId(sessionId)) return 0;
2671
- const persistedMessage = pendingMessageText(message);
2672
- if (!persistedMessage) return 0;
2673
- const q = _pendingPersistBuffers.get(sessionId) || [];
2674
- q.push(persistedMessage);
2675
- _pendingPersistBuffers.set(sessionId, q);
2676
- if (!_pendingPersistImmediate) {
2677
- _pendingPersistImmediate = setImmediate(() => {
2678
- _pendingPersistImmediate = null;
2679
- flushPendingMessagePersistsSync();
2680
- });
2681
- }
2682
- return q.length;
2683
- }
2684
-
2685
- function takeBufferedPendingMessages(sessionId) {
2686
- if (!isValidPendingSessionId(sessionId)) return [];
2687
- const buffered = _pendingPersistBuffers.get(sessionId);
2688
- if (!buffered || buffered.length === 0) return [];
2689
- _pendingPersistBuffers.delete(sessionId);
2690
- return buffered.slice();
2691
- }
2692
-
2693
- function drainPersistedPendingMessages(sessionId) {
2694
- if (!isValidPendingSessionId(sessionId)) return [];
2695
- let drained = [];
2696
- try {
2697
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
2698
- const next = normalizePendingStore(raw);
2699
- const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
2700
- drained = q.filter((m) => typeof m === 'string' && m.length > 0);
2701
- if (drained.length === 0) return undefined;
2702
- delete next.sessions[sessionId];
2703
- next.updatedAt = Date.now();
2704
- return next;
2705
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
2706
- } catch (err) {
2707
- try { process.stderr.write(`[session] pending-message drain failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
2708
- }
2709
- return drained;
2710
- }
2711
-
2712
- export function enqueuePendingMessage(sessionId, message) {
2713
- const entry = pendingMessageQueueEntry(message);
2714
- if (!sessionId || !entry) return 0;
2715
- let q = _sessionPendingMessages.get(sessionId);
2716
- if (!q) { q = []; _sessionPendingMessages.set(sessionId, q); }
2717
- q.push(entry);
2718
- const bufferedDepth = schedulePendingMessagePersist(sessionId, entry);
2719
- return Math.max(q.length, bufferedDepth || 0);
2720
- }
2721
- export function drainPendingMessages(sessionId) {
2722
- const q = _sessionPendingMessages.get(sessionId);
2723
- const memory = q && q.length > 0 ? q.slice() : [];
2724
- _sessionPendingMessages.delete(sessionId);
2725
- const persisted = [...takeBufferedPendingMessages(sessionId), ...drainPersistedPendingMessages(sessionId)];
2726
- const memoryVisible = modelVisiblePendingMessages(memory);
2727
- const persistedVisible = modelVisiblePendingMessages(persisted);
2728
- if (memoryVisible.length === 0) return persistedVisible;
2729
- if (persistedVisible.length === 0) return memoryVisible;
2730
- const persistedTexts = persistedVisible.map(pendingMessageText);
2731
- const prefixMatches = memoryVisible.every((m, i) => persistedTexts[i] === pendingMessageText(m));
2732
- if (prefixMatches) return [...memoryVisible, ...persistedVisible.slice(memoryVisible.length)];
2733
- const out = persistedVisible.slice();
2734
- const seen = new Set(persistedTexts);
2735
- for (const m of memoryVisible) {
2736
- const text = pendingMessageText(m);
2737
- if (!text || seen.has(text)) continue;
2738
- out.push(m);
2739
- seen.add(text);
2740
- }
2741
- return out;
2742
- }
2743
-
2744
- function promptContentText(content) {
2745
- if (typeof content === 'string') return content;
2746
- if (Array.isArray(content)) {
2747
- return content.map((part) => {
2748
- if (typeof part === 'string') return part;
2749
- if (part?.type === 'text') return part.text || '';
2750
- if (part?.type === 'image') return '[Image]';
2751
- return part?.text || '';
2752
- }).filter(Boolean).join('\n');
2753
- }
2754
- return String(content ?? '');
2755
- }
2756
-
2757
- function hasModelVisiblePromptContent(prompt) {
2758
- return !!promptContentText(prompt).trim();
2759
- }
2760
-
2761
- function promptContentBytes(content) {
2762
- try {
2763
- if (typeof content === 'string') return Buffer.byteLength(content, 'utf8');
2764
- return Buffer.byteLength(JSON.stringify(content), 'utf8');
2765
- } catch {
2766
- return Buffer.byteLength(promptContentText(content), 'utf8');
2767
- }
2768
- }
2769
-
2770
- function prefixUserTurnContent(content, contextBlock) {
2771
- if (!contextBlock) return content;
2772
- if (Array.isArray(content)) {
2773
- return [{ type: 'text', text: `${contextBlock}# Task\n` }, ...content];
2774
- }
2775
- return `${contextBlock}# Task\n${content}`;
2776
- }
2777
-
2778
- function prefixSessionStartContent(content, sessionBlock) {
2779
- if (!sessionBlock) return content;
2780
- if (Array.isArray(content)) {
2781
- return [{ type: 'text', text: `${sessionBlock}\n\n` }, ...content];
2782
- }
2783
- return `${sessionBlock}\n\n${content}`;
2784
- }
2785
-
2786
- function localIsoDate(date = new Date()) {
2787
- const year = date.getFullYear();
2788
- const month = String(date.getMonth() + 1).padStart(2, '0');
2789
- const day = String(date.getDate()).padStart(2, '0');
2790
- return `${year}-${month}-${day}`;
2791
- }
2792
-
2793
- function localDateTimeWithZone(date = new Date()) {
2794
- const datePart = localIsoDate(date);
2795
- const hh = String(date.getHours()).padStart(2, '0');
2796
- const mm = String(date.getMinutes()).padStart(2, '0');
2797
- const ss = String(date.getSeconds()).padStart(2, '0');
2798
- let zone = '';
2799
- try { zone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch {}
2800
- return zone ? `${datePart} ${hh}:${mm}:${ss} ${zone}` : `${datePart} ${hh}:${mm}:${ss}`;
2801
- }
2802
-
2803
- function temporalPromptText(content) {
2804
- const text = promptContentText(content)
2805
- .replace(/\s+/g, ' ')
2806
- .trim()
2807
- .toLowerCase();
2808
- return text;
2809
- }
2810
-
2811
- function promptNeedsDateReminder(content) {
2812
- const text = temporalPromptText(content);
2813
- if (!text) return false;
2814
- return /(?:오늘|내일|어제|모레|그저께|요즘|최근|방금|아까|현재\s*(?:날짜|시간|시각)|지금\s*(?:몇\s*시|시간|날짜|요일)|몇\s*월\s*몇\s*일|몇\s*시|무슨\s*요일|요일|날짜|이번\s*(?:주|달|월|년)|지난\s*(?:주|달|월|년)|다음\s*(?:주|달|월|년)|올해|작년|내년|today|tomorrow|yesterday|recently|current\s+(?:date|time)|what\s+(?:date|time)|which\s+day|weekday|this\s+(?:week|month|year)|last\s+(?:week|month|year)|next\s+(?:week|month|year))/i.test(text);
2815
- }
2816
-
2817
- function promptNeedsTimeReminder(content) {
2818
- const text = temporalPromptText(content);
2819
- if (!text) return false;
2820
- return /(?:현재\s*(?:시간|시각)|지금\s*(?:몇\s*시|시간)|몇\s*시|시각|시간|current\s+time|what\s+time|time\s+is\s+it)/i.test(text);
2821
- }
2822
-
2823
- function buildCurrentTimeBlock(content) {
2824
- const needsTime = promptNeedsTimeReminder(content);
2825
- if (!needsTime && !promptNeedsDateReminder(content)) return '';
2826
- return localDateTimeWithZone(new Date());
2827
- }
2828
-
2829
- function sessionModelDisplay(model) {
2830
- const text = String(model || '').trim();
2831
- if (!text) return '';
2832
- return text
2833
- .replace(/-\d{4}-\d{2}-\d{2}$/, '')
2834
- .replace(/^gpt-/i, 'GPT-')
2835
- .replace(/(?:^|-)([a-z])/g, (m) => m.toUpperCase());
2836
- }
2837
-
2838
- function buildSessionStartBlock(session, cwd) {
2839
- if (!session || session.owner === 'agent') return '';
2840
- const lines = ['# Session'];
2841
- const effectiveCwd = String(cwd || session.cwd || '').trim();
2842
- if (effectiveCwd) lines.push(`Cwd: ${effectiveCwd}`);
2843
- const modelBits = [
2844
- sessionModelDisplay(session.model),
2845
- session.effort ? String(session.effort).trim().toUpperCase() : '',
2846
- session.fast === true ? 'FAST' : '',
2847
- ].filter(Boolean);
2848
- if (modelBits.length) lines.push(`Model: ${modelBits.join(' · ')}`);
2849
- const workflowName = String(session.workflow?.name || session.workflow?.id || '').trim();
2850
- if (workflowName) lines.push(`Workflow: ${workflowName}`);
2851
- return lines.length > 1 ? lines.join('\n') : '';
2852
- }
2853
-
2854
- function isReferenceFilesMessage(message) {
2855
- return message?.role === 'user'
2856
- && typeof message.content === 'string'
2857
- && /^Reference files:\s*/i.test(message.content.trimStart());
2858
- }
2859
-
2860
- function isProtectedContextUserMessage(message) {
2861
- return message?.role === 'user'
2862
- && typeof message.content === 'string'
2863
- && message.content.trimStart().startsWith('<system-reminder>');
2864
- }
2865
-
2866
- function hasUserConversationMessage(messages) {
2867
- return (Array.isArray(messages) ? messages : []).some((message) => (
2868
- message?.role === 'user'
2869
- && !isProtectedContextUserMessage(message)
2870
- && !isReferenceFilesMessage(message)
2871
- ));
2872
- }
2873
-
2874
- function modelVisiblePendingMessages(messages) {
2875
- return (Array.isArray(messages) ? messages : [])
2876
- .map(pendingMessageQueueEntry)
2877
- .filter(Boolean)
2878
- .filter((message) => !isInternalRuntimeNotificationText(
2879
- message && typeof message === 'object' && Object.prototype.hasOwnProperty.call(message, 'content')
2880
- ? message.content
2881
- : message,
2882
- ));
2883
- }
2884
-
2885
- export function _mergePendingMessageEntries(entries) {
2886
- const normalized = (Array.isArray(entries) ? entries : [])
2887
- .map(normalizePendingMessageEntry)
2888
- .filter(Boolean);
2889
- if (normalized.length === 0) return null;
2890
- const displayText = normalized.map((entry) => entry.text || promptContentText(entry.content))
2891
- .filter((text) => String(text || '').trim())
2892
- .join('\n');
2893
- if (normalized.every((entry) => typeof entry.content === 'string')) {
2894
- return {
2895
- content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
2896
- text: displayText,
2897
- count: normalized.length,
2898
- };
2899
- }
2900
- const parts = [];
2901
- for (const entry of normalized) {
2902
- if (typeof entry.content === 'string') {
2903
- if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
2904
- } else if (Array.isArray(entry.content)) {
2905
- parts.push(...entry.content);
2906
- } else {
2907
- const text = promptContentText(entry.content);
2908
- if (text.trim()) parts.push({ type: 'text', text });
2909
- }
2910
- parts.push({ type: 'text', text: '\n' });
2911
- }
2912
- while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
2913
- return { content: parts, text: displayText || promptContentText(parts), count: normalized.length };
2914
- }
2915
-
2916
- function isInternalRuntimeNotificationText(content) {
2917
- return contractIsInternalRuntimeNotificationText(promptContentText(content));
2918
- }
2919
-
2920
- export const _isInternalRuntimeNotificationText = isInternalRuntimeNotificationText;
2921
-
2922
1951
  function isInternalCancelledAssistantMessage(message) {
2923
1952
  if (!message || message.role !== 'assistant') return false;
2924
1953
  if (message.cancelled === true) return true;
@@ -3958,6 +2987,10 @@ export function closeSession(id, reason = 'manual') {
3958
2987
  // or Map entries across session lifetime. Fire-and-forget — close path
3959
2988
  // should not await disk IO; errors are swallowed inside.
3960
2989
  try { clearOffloadSession(id); } catch { /* ignore */ }
2990
+ // Drop the in-memory pending-message queue and any buffered-persist entry
2991
+ // for this session — otherwise both Maps accumulate one entry per closed
2992
+ // session for the life of the mcp-server.
2993
+ _dropPendingMessageState(id);
3961
2994
  // 4. Defer runtime map clear to next tick so any settling askSession can
3962
2995
  // observe `closed=true` / bumped generation before we yank the entry.
3963
2996
  // Disk tombstone remains — that's what blocks resurrection.