mixdog 0.9.87 → 0.9.89

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 (167) hide show
  1. package/package.json +13 -7
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +29 -35
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
  43. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
  44. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  46. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  50. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  51. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  54. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  55. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  56. package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  58. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  59. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  60. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  61. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  62. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  63. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  64. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  65. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  66. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  67. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  68. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  69. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  70. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  71. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  72. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  73. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  74. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  77. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  87. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  88. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
  89. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  90. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
  91. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
  93. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  94. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
  95. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  96. package/src/runtime/media/lanes.mjs +2 -2
  97. package/src/runtime/media/renditions.mjs +101 -17
  98. package/src/runtime/media/renditions.test.mjs +54 -0
  99. package/src/runtime/media/store.mjs +111 -43
  100. package/src/runtime/media/store.test.mjs +53 -11
  101. package/src/runtime/memory/index.mjs +26 -50
  102. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  103. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  104. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  105. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  106. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  107. package/src/runtime/memory/lib/http-router.mjs +4 -0
  108. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  109. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
  110. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  111. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  112. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  113. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  114. package/src/runtime/memory/tool-defs.mjs +3 -3
  115. package/src/runtime/shared/atomic-file.mjs +5 -1
  116. package/src/runtime/shared/child-guardian.mjs +73 -2
  117. package/src/runtime/shared/provider-api-key.mjs +22 -5
  118. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  119. package/src/session-runtime/config-helpers.mjs +6 -5
  120. package/src/session-runtime/lifecycle-api.mjs +16 -4
  121. package/src/session-runtime/media-api.mjs +31 -16
  122. package/src/session-runtime/prewarm.mjs +2 -24
  123. package/src/session-runtime/runtime-core.mjs +33 -15
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +0 -6
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +90 -36
  131. package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
  132. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  133. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  134. package/src/standalone/agent-tool.mjs +17 -17
  135. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  136. package/src/standalone/channel-daemon.mjs +8 -0
  137. package/src/standalone/explore-tool.mjs +1 -1
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +25 -3
  141. package/src/tui/App.jsx +5 -3
  142. package/src/tui/app/resume-picker.mjs +7 -2
  143. package/src/tui/app/route-pickers.mjs +11 -117
  144. package/src/tui/app/slash-dispatch.mjs +13 -3
  145. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  146. package/src/tui/app/transcript-window.mjs +17 -93
  147. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  148. package/src/tui/app/use-transcript-window.mjs +64 -114
  149. package/src/tui/components/Markdown.jsx +19 -2
  150. package/src/tui/components/Message.jsx +8 -3
  151. package/src/tui/components/ToolExecution.jsx +7 -3
  152. package/src/tui/dist/index.mjs +243 -223
  153. package/src/tui/engine/live-share.mjs +15 -5
  154. package/src/tui/engine/render-timing.mjs +2 -2
  155. package/src/tui/engine/session-api-ext.mjs +74 -4
  156. package/src/tui/engine/session-flow.mjs +3 -1
  157. package/src/tui/engine.mjs +2 -2
  158. package/src/tui/hooks/useSharedTick.mjs +0 -2
  159. package/src/tui/index.jsx +5 -5
  160. package/src/ui/statusline-agents.mjs +44 -11
  161. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  162. package/src/workflows/default/WORKFLOW.md +3 -3
  163. package/src/workflows/solo/WORKFLOW.md +5 -3
  164. package/src/runtime/media/index.mjs +0 -18
  165. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  166. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  167. package/src/standalone/agent-shard/shard-pool.mjs +0 -443
@@ -1,443 +0,0 @@
1
- // Lead-side agent shard pool (P2/P3 — see docs/agent-shard-design.md).
2
- //
3
- // Topology: up to `MIXDOG_AGENT_SHARD_POOL` lazily-forked shard children
4
- // (default max(2, cpus/4)); worker spawns go to the LEAST-LOADED live shard
5
- // (a fresh slot opens once every live shard already runs a spawn) and a
6
- // shard process is REUSED across spawns (runtime import in the child costs
7
- // ~1-2s and must not be paid per spawn). A codex-style execution window
8
- // (`MIXDOG_AGENT_SHARD_WINDOW`, default 16, 0=off) caps CONCURRENT shard
9
- // spawns — overflow queues FIFO in the lead and stays cancellable by
10
- // tag/sessionId. Shards idle past `MIXDOG_AGENT_SHARD_IDLE_MS` (default 5m)
11
- // are reaped (~174MB each) and re-fork on demand. cancel/enqueue route to
12
- // the shard that owns the tag/session; provider 429 cooldowns and credential
13
- // resets are bridged both ways so one account's quota window parks every
14
- // process. Flag-gated: MIXDOG_AGENT_SHARD=1 opts in; default keeps the
15
- // in-process path byte-identical.
16
- import { fork } from 'node:child_process';
17
- import { cpus } from 'node:os';
18
- import { fileURLToPath } from 'node:url';
19
- import { startChildGuardian } from '../../runtime/shared/child-guardian.mjs';
20
- import { scrubLoaderVars } from '../../runtime/agent/orchestrator/tools/env-scrub.mjs';
21
- import { providerAdmissionScheduler } from '../../runtime/agent/orchestrator/providers/admission-scheduler.mjs';
22
- import { getMcpTools, executeMcpTool } from '../../runtime/agent/orchestrator/mcp/client.mjs';
23
- import { getInternalTools, executeInternalTool } from '../../runtime/agent/orchestrator/internal-tools.mjs';
24
-
25
- const CHILD_ENTRY = fileURLToPath(new URL('./shard-child.mjs', import.meta.url));
26
- const READY_TIMEOUT_MS = 30_000;
27
- const MAX_SHARDS = 16;
28
- const IDLE_REAP_SWEEP_MS = 60_000;
29
-
30
- export function agentShardEnabled() {
31
- const value = process.env.MIXDOG_AGENT_SHARD;
32
- if (value === '1') return true;
33
- // P2 gate: sharding stays opt-in (MIXDOG_AGENT_SHARD=1). The P3
34
- // default-ON flip (lead-like hosts shard unless MIXDOG_WORKER_MODE=1)
35
- // is deferred until the remaining P2 items land — a default-ON pool
36
- // routes every spawn through shard children, which breaks hosts that
37
- // inject a custom session manager (fake/test mgr, embedded runtimes).
38
- return false;
39
- }
40
-
41
- function agentShardPoolSize() {
42
- const configured = Math.floor(Number(process.env.MIXDOG_AGENT_SHARD_POOL));
43
- if (Number.isFinite(configured) && configured >= 1) return Math.min(configured, MAX_SHARDS);
44
- let cores = 4;
45
- try { cores = cpus()?.length || 4; } catch { /* keep fallback */ }
46
- return Math.max(2, Math.min(MAX_SHARDS, Math.floor(cores / 4)));
47
- }
48
-
49
- // Concurrent shard-spawn window — REACTIVE brake, not a preemptive cap.
50
- // Concurrent shard-spawn window. UNBOUNDED unless explicitly configured
51
- // (workstream rule: no local concurrency limits and no auto-brake heuristics
52
- // — provider 429 cooldown parking in the admission scheduler is the ONLY
53
- // limiter). `MIXDOG_AGENT_SHARD_WINDOW=N` is a pure operator opt-in to a
54
- // fixed window with a bounded FIFO overflow queue.
55
- function _currentWindow() {
56
- const configured = Math.floor(Number(process.env.MIXDOG_AGENT_SHARD_WINDOW));
57
- if (Number.isFinite(configured) && configured >= 1) return configured;
58
- return 0; // unbounded
59
- }
60
-
61
- function agentShardIdleMs() {
62
- const configured = Math.floor(Number(process.env.MIXDOG_AGENT_SHARD_IDLE_MS));
63
- if (Number.isFinite(configured) && configured >= 0) return configured;
64
- return 300_000;
65
- }
66
-
67
- // Overflow-queue bound (provider-admission parity: fail fast with a visible
68
- // error instead of silently accumulating unbounded deferred spawns).
69
- function agentShardQueueMax() {
70
- const configured = Math.floor(Number(process.env.MIXDOG_AGENT_SHARD_QUEUE_MAX));
71
- if (Number.isFinite(configured) && configured >= 1) return configured;
72
- return 1_024;
73
- }
74
-
75
- const _shards = new Map(); // slot -> shard record
76
- let _callSeq = 0;
77
- // tag / sessionId -> owning shard, so cancel + tag-reuse enqueue reach the
78
- // process that hosts the live session. Entries are optimistic at spawn
79
- // dispatch (tag), confirmed by the `spawned` progress event (sessionId), and
80
- // dropped when the spawn settles (shard sessions are one-shot) or the shard
81
- // exits.
82
- const _ownerByKey = new Map();
83
- let _cooldownUnsub = null;
84
- // Execution-window state: dispatched-but-unsettled spawn count + FIFO
85
- // overflow queue of { payload, onProgress, resolve, reject, tag }.
86
- let _activeSpawns = 0;
87
- const _spawnQueue = [];
88
- let _idleReapTimer = null;
89
-
90
- function _liveShards() {
91
- return [..._shards.values()].filter((shard) => !shard.closed);
92
- }
93
-
94
- function _broadcast(msg, { except = null } = {}) {
95
- for (const shard of _liveShards()) {
96
- if (shard === except) continue;
97
- try { shard.child.send(msg); } catch { /* exit handler reaps */ }
98
- }
99
- }
100
-
101
- // Lead-recorded 429/reset → every shard. Registered once, on first fork.
102
- function _ensureCooldownBridge() {
103
- if (_cooldownUnsub || typeof providerAdmissionScheduler.onCooldownEvent !== 'function') return;
104
- _cooldownUnsub = providerAdmissionScheduler.onCooldownEvent((event) => {
105
- _broadcast({ type: 'cooldown', payload: event });
106
- });
107
- }
108
-
109
- // Shard-reported 429/reset → lead scheduler (apply, no re-emit) + sibling
110
- // shards. applyExternalCooldown never emits, so there is no feedback loop;
111
- // resets are loop-terminated by the resetCount>0 emit gate.
112
- function _applyShardCooldownReport(payload, sourceShard) {
113
- try {
114
- if (payload?.type === 'reset') {
115
- providerAdmissionScheduler.resetCooldowns(payload.provider || null);
116
- } else if (payload?.key) {
117
- providerAdmissionScheduler.applyExternalCooldown?.(payload.key, payload.cooldownUntil);
118
- _broadcast({ type: 'cooldown', payload }, { except: sourceShard });
119
- }
120
- } catch { /* best-effort */ }
121
- }
122
-
123
- function _dropOwnerKeysFor(shard) {
124
- for (const [key, owner] of [..._ownerByKey.entries()]) {
125
- if (owner === shard) _ownerByKey.delete(key);
126
- }
127
- }
128
-
129
- // Lead-tool proxy (P2 MCP proxy, generalized): shard children have neither
130
- // MCP connections nor the internal-tool executor (both are lead singletons),
131
- // so worker calls to those surfaces arrive here over IPC and execute in the
132
- // lead. The child receives the tool DEFINITIONS with each spawn payload
133
- // (below) so its session schema stays byte-identical to an in-process worker.
134
- function _leadToolDefs() {
135
- let mcp = [];
136
- let internal = [];
137
- try { mcp = getMcpTools() || []; } catch { /* defs best-effort */ }
138
- try { internal = getInternalTools() || []; } catch { /* defs best-effort */ }
139
- return { mcp, internal };
140
- }
141
-
142
- async function _handleLeadToolCall(shard, msg) {
143
- const { kind, name, args, callerCtx } = msg.payload || {};
144
- let reply;
145
- try {
146
- const value = kind === 'mcp'
147
- ? await executeMcpTool(name, args)
148
- : await executeInternalTool(name, args, callerCtx || {});
149
- reply = { type: 'lead-tool-result', callId: msg.callId, result: value };
150
- } catch (error) {
151
- reply = { type: 'lead-tool-result', callId: msg.callId, error: error?.message || String(error) };
152
- }
153
- try { shard.child.send(reply); } catch { /* child gone — its timeout reaps */ }
154
- }
155
-
156
- function _ownerFor(payload) {
157
- const bySession = _ownerByKey.get(String(payload?.sessionId || '').trim());
158
- if (bySession && !bySession.closed) return bySession;
159
- const byTag = _ownerByKey.get(String(payload?.tag || '').trim());
160
- return byTag && !byTag.closed ? byTag : null;
161
- }
162
-
163
- function _ensureShardAt(slot) {
164
- const existing = _shards.get(slot);
165
- if (existing && !existing.closed && existing.child.exitCode === null && existing.child.signalCode === null) {
166
- return existing;
167
- }
168
- const env = { ...process.env };
169
- scrubLoaderVars(env);
170
- const child = fork(CHILD_ENTRY, [], {
171
- stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
172
- // Never inherit the parent's execArgv (a driver started with
173
- // --input-type/--test would poison the child's module resolution).
174
- execArgv: [],
175
- windowsHide: true,
176
- detached: false,
177
- env: {
178
- ...env,
179
- MIXDOG_STANDALONE: '1',
180
- MIXDOG_WORKER_MODE: '1',
181
- MIXDOG_CLI_OWNED: '0',
182
- MIXDOG_QUIET_SESSION_LOG: process.env.MIXDOG_QUIET_SESSION_LOG ?? '1',
183
- // The shard must never recursively shard its own nested spawns.
184
- MIXDOG_AGENT_SHARD: '0',
185
- },
186
- });
187
- const shard = {
188
- slot,
189
- child,
190
- closed: false,
191
- pending: new Map(),
192
- inflight: 0,
193
- lastActiveAt: Date.now(),
194
- readyResolve: null,
195
- ready: null,
196
- };
197
- shard.ready = new Promise((resolve, reject) => {
198
- shard.readyResolve = resolve;
199
- const t = setTimeout(() => reject(new Error('agent shard did not become ready within 30s')), READY_TIMEOUT_MS);
200
- t.unref?.();
201
- });
202
- shard.ready.catch(() => {});
203
- child.stderr?.on('data', (chunk) => {
204
- const text = String(chunk || '').trimEnd();
205
- if (text) { try { process.stderr.write(`[agent-shard:${slot}] ${text}\n`); } catch { /* best-effort */ } }
206
- });
207
- child.on('message', (msg) => {
208
- if (!msg || typeof msg !== 'object') return;
209
- if (msg.type === 'ready') {
210
- shard.readyResolve?.(true);
211
- shard.readyResolve = null;
212
- return;
213
- }
214
- if (msg.type === 'rate-limit') {
215
- _applyShardCooldownReport(msg.payload, shard);
216
- return;
217
- }
218
- if (msg.type === 'lead-tool' && msg.callId) {
219
- void _handleLeadToolCall(shard, msg);
220
- return;
221
- }
222
- if (msg.type === 'progress' && msg.callId) {
223
- if (msg.event === 'spawned' && msg.payload) {
224
- // Confirm ownership under the sessionId (cancel-by-id) in
225
- // addition to the optimistic tag mapping set at dispatch.
226
- const sessionId = String(msg.payload.sessionId || '').trim();
227
- if (sessionId) _ownerByKey.set(sessionId, shard);
228
- const tag = String(msg.payload.tag || '').trim();
229
- if (tag) _ownerByKey.set(tag, shard);
230
- }
231
- const item = shard.pending.get(msg.callId);
232
- if (item?.onProgress) {
233
- // A lead-side hook error must never kill the IPC pump.
234
- try { item.onProgress(msg.event, msg.payload); } catch { /* best-effort */ }
235
- }
236
- return;
237
- }
238
- if (msg.type === 'result' && msg.callId) {
239
- const item = shard.pending.get(msg.callId);
240
- if (!item) return;
241
- shard.pending.delete(msg.callId);
242
- if (item.cleanupKeys) {
243
- for (const key of item.cleanupKeys) {
244
- if (_ownerByKey.get(key) === shard) _ownerByKey.delete(key);
245
- }
246
- const settledSessionId = String(msg.result?.sessionId || '').trim();
247
- if (settledSessionId && _ownerByKey.get(settledSessionId) === shard) {
248
- _ownerByKey.delete(settledSessionId);
249
- }
250
- }
251
- if (msg.error) item.reject(new Error(msg.error));
252
- else item.resolve(msg.result);
253
- }
254
- });
255
- child.on('exit', (code, signal) => {
256
- shard.closed = true;
257
- const error = new Error(`agent shard exited (${signal || (code ?? 'unknown')})`);
258
- for (const [, item] of shard.pending) item.reject(error);
259
- shard.pending.clear();
260
- _dropOwnerKeysFor(shard);
261
- if (_shards.get(slot) === shard) _shards.delete(slot);
262
- });
263
- child.on('error', () => { /* exit handler owns cleanup */ });
264
- startChildGuardian({ childPid: child.pid, label: `agent-shard-${slot}`, orphanGraceMs: 8000, forceGraceMs: 3000 });
265
- _shards.set(slot, shard);
266
- _ensureCooldownBridge();
267
- _ensureIdleReapTimer();
268
- return shard;
269
- }
270
-
271
- // ── Idle shard reap ─────────────────────────────────────────────────────────
272
- // A warmed shard holds ~174MB of session runtime. One shared sweep timer
273
- // kills shards with no inflight work past the idle window; the next spawn
274
- // re-forks on demand (~1-2s boot, acceptable after minutes of idleness).
275
- function _ensureIdleReapTimer() {
276
- if (_idleReapTimer) return;
277
- // Sweep at half the idle window (floor 5s, cap 60s) so short windows
278
- // (tests, aggressive configs) actually observe the reap.
279
- const idleMsNow = agentShardIdleMs();
280
- const sweepMs = idleMsNow > 0
281
- ? Math.max(5_000, Math.min(IDLE_REAP_SWEEP_MS, Math.floor(idleMsNow / 2)))
282
- : IDLE_REAP_SWEEP_MS;
283
- _idleReapTimer = setInterval(() => {
284
- const idleMs = agentShardIdleMs();
285
- if (idleMs <= 0) return;
286
- const now = Date.now();
287
- for (const shard of _liveShards()) {
288
- if (shard.inflight > 0 || shard.pending.size > 0) continue;
289
- if (now - shard.lastActiveAt < idleMs) continue;
290
- shard.closed = true;
291
- try { shard.child.kill(); } catch { /* exit handler reaps */ }
292
- }
293
- if (_shards.size === 0 && _idleReapTimer) {
294
- clearInterval(_idleReapTimer);
295
- _idleReapTimer = null;
296
- }
297
- }, sweepMs);
298
- _idleReapTimer.unref?.();
299
- }
300
-
301
- // Least-loaded assignment: reuse the emptiest live shard; open a fresh slot
302
- // only when every live shard is already busy and the pool has headroom.
303
- function _shardForSpawn() {
304
- const poolSize = agentShardPoolSize();
305
- const live = _liveShards();
306
- let best = null;
307
- for (const shard of live) {
308
- if (!best || shard.inflight < best.inflight) best = shard;
309
- }
310
- if (best && best.inflight === 0) return best;
311
- if (live.length < poolSize) {
312
- for (let slot = 0; slot < poolSize; slot += 1) {
313
- const existing = _shards.get(slot);
314
- if (!existing || existing.closed) return _ensureShardAt(slot);
315
- }
316
- }
317
- return best || _ensureShardAt(0);
318
- }
319
-
320
- async function _requestOn(shard, type, payload, onProgress) {
321
- await shard.ready;
322
- return new Promise((resolve, reject) => {
323
- const callId = `c${++_callSeq}`;
324
- // One-shot shard sessions: when the spawn settles, its routing keys
325
- // are stale (the child closes the session) and must not capture later
326
- // same-tag traffic once the tag is reused on another shard.
327
- const cleanupKeys = type === 'spawn'
328
- ? [String(payload?.tag || '').trim()].filter(Boolean)
329
- : null;
330
- shard.pending.set(callId, { resolve, reject, onProgress, cleanupKeys });
331
- try {
332
- shard.child.send({ type, callId, payload });
333
- } catch (error) {
334
- shard.pending.delete(callId);
335
- reject(error);
336
- }
337
- });
338
- }
339
-
340
- function _startSpawn(entry) {
341
- const shard = _shardForSpawn();
342
- if (entry.tag) _ownerByKey.set(entry.tag, shard);
343
- // Fresh lead-tool snapshot per spawn: MCP servers reconnect/toggle at
344
- // runtime and the child rebuilds its proxy surface from this payload.
345
- const payload = { ...entry.payload, leadTools: _leadToolDefs() };
346
- _activeSpawns += 1;
347
- shard.inflight += 1;
348
- shard.lastActiveAt = Date.now();
349
- _requestOn(shard, 'spawn', payload, entry.onProgress)
350
- .then(entry.resolve, entry.reject)
351
- .finally(() => {
352
- _activeSpawns = Math.max(0, _activeSpawns - 1);
353
- shard.inflight = Math.max(0, shard.inflight - 1);
354
- shard.lastActiveAt = Date.now();
355
- _drainSpawnQueue();
356
- });
357
- }
358
-
359
- function _drainSpawnQueue() {
360
- const window = _currentWindow();
361
- while (_spawnQueue.length > 0 && (window === 0 || _activeSpawns < window)) {
362
- _startSpawn(_spawnQueue.shift());
363
- }
364
- }
365
-
366
- // A cancel for a spawn still WAITING in the window queue must not fan out to
367
- // the shards (nothing was dispatched): drop the entry and fail its spawn
368
- // promise the same way a closed session fails a live one.
369
- function _cancelQueuedSpawn(payload) {
370
- const tag = String(payload?.tag || '').trim();
371
- const sessionId = String(payload?.sessionId || '').trim();
372
- if (!tag && !sessionId) return null;
373
- const index = _spawnQueue.findIndex((entry) => (tag && entry.tag === tag));
374
- if (index < 0) return null;
375
- const [entry] = _spawnQueue.splice(index, 1);
376
- entry.reject(new Error('Session closed: agent shard spawn cancelled while queued'));
377
- return { cancelled: true, queued: true };
378
- }
379
-
380
- /**
381
- * One request/response round-trip to the shard pool. `spawn` goes to the
382
- * least-loaded shard, bounded by the execution window (overflow queues FIFO,
383
- * cancellable); `cancel`/`enqueue` route to the owning shard (fan-out to
384
- * every live shard when ownership is unknown); `ping` warms shard 0.
385
- * `onProgress` receives interim `progress` messages (e.g. `spawned`).
386
- */
387
- export async function shardRequest(type, payload = {}, { onProgress } = {}) {
388
- if (type === 'cancel' || type === 'enqueue') {
389
- const fallback = type === 'cancel' ? { cancelled: false } : { queued: false };
390
- if (type === 'cancel') {
391
- const queuedCancel = _cancelQueuedSpawn(payload);
392
- if (queuedCancel) return queuedCancel;
393
- }
394
- const owner = _ownerFor(payload);
395
- if (owner) return _requestOn(owner, type, payload, onProgress);
396
- const live = _liveShards();
397
- if (live.length === 0) return fallback;
398
- const settled = await Promise.allSettled(live.map((shard) => _requestOn(shard, type, payload, onProgress)));
399
- const values = settled.filter((r) => r.status === 'fulfilled').map((r) => r.value);
400
- return values.find((v) => v?.cancelled === true || v?.queued === true) || values[0] || fallback;
401
- }
402
- if (type === 'spawn') {
403
- return new Promise((resolve, reject) => {
404
- const entry = {
405
- payload,
406
- onProgress,
407
- resolve,
408
- reject,
409
- tag: String(payload?.tag || '').trim() || null,
410
- };
411
- const window = _currentWindow();
412
- if (window > 0 && _activeSpawns >= window) {
413
- const queueMax = agentShardQueueMax();
414
- if (_spawnQueue.length >= queueMax) {
415
- reject(new Error(`agent shard spawn queue full (${queueMax} deferred spawns; window=${window}) — `
416
- + 'wait for running workers to finish, or raise MIXDOG_AGENT_SHARD_QUEUE_MAX / MIXDOG_AGENT_SHARD_WINDOW'));
417
- return;
418
- }
419
- _spawnQueue.push(entry);
420
- } else {
421
- _startSpawn(entry);
422
- }
423
- });
424
- }
425
- const shard = _ensureShardAt(0);
426
- shard.lastActiveAt = Date.now();
427
- return _requestOn(shard, type, payload, onProgress);
428
- }
429
-
430
- /**
431
- * Boot prewarm: fork the shard child now and let it import the session
432
- * runtime, so the FIRST sharded spawn skips the ~1-2s child boot. No-op when
433
- * sharding is disabled. Best-effort; resolves true when the shard answered.
434
- */
435
- export async function prewarmAgentShard() {
436
- if (!agentShardEnabled()) return false;
437
- try {
438
- const pong = await shardRequest('ping', {});
439
- return pong?.pong === true;
440
- } catch {
441
- return false;
442
- }
443
- }