mixdog 0.9.85 → 0.9.86

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