mixdog 0.9.151 → 0.9.152

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 (175) hide show
  1. package/README.md +110 -68
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +3 -10
  6. package/src/headless-exec.test.mjs +2 -7
  7. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  8. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  9. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  11. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  12. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  20. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  26. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  29. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  33. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  34. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  35. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  38. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
  39. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  43. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  44. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  45. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  49. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  51. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  52. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  57. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  62. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  63. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  64. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  69. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  73. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  74. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  75. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  76. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  77. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  78. package/src/runtime/browser-bridge/client.mjs +103 -0
  79. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  80. package/src/runtime/computer-bridge/client.mjs +101 -0
  81. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  82. package/src/runtime/media/store.mjs +18 -8
  83. package/src/runtime/media/store.test.mjs +14 -0
  84. package/src/runtime/memory/index.mjs +1 -1
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  86. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  87. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  88. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  89. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  90. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  91. package/src/runtime/shared/provider-api-key.mjs +1 -0
  92. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  93. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  94. package/src/runtime/shared/skill-document.mjs +86 -0
  95. package/src/runtime/shared/skill-document.test.mjs +81 -0
  96. package/src/runtime/shared/tool-surface.mjs +18 -0
  97. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  98. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  99. package/src/runtime/shared/user-cwd.mjs +33 -14
  100. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  101. package/src/session-runtime/cwd-plugins.mjs +26 -26
  102. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  103. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  104. package/src/session-runtime/global-extensions.mjs +26 -0
  105. package/src/session-runtime/global-extensions.test.mjs +28 -0
  106. package/src/session-runtime/goal-runtime.mjs +721 -0
  107. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  108. package/src/session-runtime/lifecycle-api.mjs +18 -4
  109. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  110. package/src/session-runtime/mcp-glue.mjs +49 -47
  111. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  112. package/src/session-runtime/plugin-mcp.mjs +96 -22
  113. package/src/session-runtime/prewarm.mjs +1 -10
  114. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  115. package/src/session-runtime/resource-api.mjs +173 -58
  116. package/src/session-runtime/runtime-core.mjs +148 -20
  117. package/src/session-runtime/runtime-tunables.mjs +6 -8
  118. package/src/session-runtime/session-turn-api.mjs +22 -11
  119. package/src/session-runtime/skills-api.mjs +79 -25
  120. package/src/session-runtime/skills-api.test.mjs +99 -0
  121. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  122. package/src/session-runtime/tool-catalog.mjs +4 -13
  123. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  124. package/src/session-runtime/tool-surface.mjs +12 -0
  125. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  126. package/src/standalone/agent-tool/notify.mjs +6 -8
  127. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  128. package/src/standalone/daemon.mjs +31 -15
  129. package/src/standalone/hook-bus/config.mjs +1 -0
  130. package/src/standalone/plugin-admin.mjs +17 -0
  131. package/src/standalone/plugin-admin.test.mjs +7 -0
  132. package/src/standalone/provider-admin.mjs +1 -0
  133. package/src/standalone/session-protocol.mjs +5 -0
  134. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  135. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  136. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  137. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  138. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  139. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  140. package/src/standalone/session-runtime-host.mjs +696 -58
  141. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  142. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  143. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  144. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  145. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  146. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  147. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  148. package/src/standalone/session-runtime-worker.mjs +476 -18
  149. package/src/standalone/session-service.mjs +81 -6
  150. package/src/tui/app/app-view.jsx +1 -0
  151. package/src/tui/app/core-memory-picker.mjs +2 -0
  152. package/src/tui/app/extension-pickers.mjs +1 -1
  153. package/src/tui/app/maintenance-pickers.mjs +3 -3
  154. package/src/tui/app/model-options.mjs +2 -0
  155. package/src/tui/app/model-picker.mjs +14 -4
  156. package/src/tui/app/panel-handoff.test.mjs +311 -0
  157. package/src/tui/app/project-picker.mjs +1 -0
  158. package/src/tui/app/route-pickers.mjs +21 -6
  159. package/src/tui/app/settings-picker.mjs +26 -3
  160. package/src/tui/app/slash-commands.mjs +1 -0
  161. package/src/tui/app/slash-dispatch.mjs +73 -22
  162. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  163. package/src/tui/components/Picker.jsx +10 -1
  164. package/src/tui/dist/index.mjs +163 -44
  165. package/src/tui/session/context-state.mjs +1 -0
  166. package/src/tui/session/goal-continuation.mjs +110 -0
  167. package/src/tui/session/goal-continuation.test.mjs +84 -0
  168. package/src/tui/session/queue-helpers.mjs +5 -2
  169. package/src/tui/session/session-action-surface.test.mjs +108 -0
  170. package/src/tui/session/session-api-ext.mjs +39 -8
  171. package/src/tui/session/session-api.mjs +58 -0
  172. package/src/tui/session/session-flow.mjs +6 -0
  173. package/src/tui/session/turn.mjs +1 -0
  174. package/src/tui/session-local.mjs +15 -1
  175. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -1,8 +1,15 @@
1
- // Supervised child-process host for all session runtimes.
1
+ // Supervised child process for ONE session runtime shard.
2
2
  //
3
3
  // Provider parsing, transcript projection, tool execution, and runtime timers
4
4
  // stay on this event loop. The machine daemon receives only identity-preserving
5
5
  // state deltas and remains free to serve health, input, and abort dispatch.
6
+ //
7
+ // This process is one of a BOUNDED set of identical shards (see
8
+ // session-runtime-host.mjs): the host routes each session/dispatch to a shard
9
+ // deterministically, so a saturated or crashed shard degrades only the work it
10
+ // owns instead of every runtime on the machine. Shard-crossing facts —
11
+ // machine-wide spawn leases and account-wide provider cooldowns — are
12
+ // coordinated through the host, never assumed process-local.
6
13
  process.env.MIXDOG_SESSION_RUNTIME_WORKER = '1';
7
14
  // Pid-scoped marker: child tools inherit the plain flag, so callers that need
8
15
  // the actual runtime worker must also verify this exact pid.
@@ -14,16 +21,33 @@ import { safeIpcSend } from '../runtime/shared/safe-ipc-send.mjs';
14
21
  import { sanitizeForWire } from './session-service.mjs';
15
22
  import { disposeSessionRuntimeRecord } from './session-runtime-record.mjs';
16
23
  import { diffSessionState } from './session-state-patch.mjs';
24
+ import {
25
+ applyProviderCooldown,
26
+ mergeKnownProviderCooldown,
27
+ providerCooldownAdvanced,
28
+ readProviderCooldown,
29
+ } from './session-runtime-provider-cooldown.mjs';
17
30
  import {
18
31
  reportRuntimeAbortListenerPressure,
19
32
  SESSION_RUNTIME_WORKER_UNHEALTHY_EVENT,
33
+ startRuntimeEventLoopLagMonitor,
20
34
  } from '../runtime/shared/session-runtime-health.mjs';
21
35
 
36
+ // Shard identity. The host runs a BOUNDED set of identical runtime children so
37
+ // that provider parsing, tool results and transcript projection for unrelated
38
+ // sessions no longer share one event loop; this process owns exactly the
39
+ // sessions/dispatches the host routed to this shard index.
40
+ const SHARD_INDEX = Math.max(0, Math.floor(Number(process.env.MIXDOG_SESSION_RUNTIME_SHARD) || 0));
41
+
22
42
  const records = new Map();
23
43
  let sessionModulePromise = null;
24
44
  let stopping = false;
25
45
  let unhealthyDetail = null;
26
46
  let unhealthyReported = false;
47
+ let unsubscribeAgentSessionPublisher = null;
48
+ let agentSessionPublisherPromise = null;
49
+ const pendingAgentSessionProjections = new Map();
50
+ const activeAgentSessionProjections = new Set();
27
51
  const pendingAbortPressureChecks = new WeakSet();
28
52
  const ABORT_PRESSURE_RETENTION_CHECK_MS = 30_000;
29
53
 
@@ -38,7 +62,9 @@ function reportUnhealthy() {
38
62
  process.on(SESSION_RUNTIME_WORKER_UNHEALTHY_EVENT, (detail) => {
39
63
  if (stopping || unhealthyDetail) return;
40
64
  unhealthyDetail = sanitizeForWire(detail) || { reason: 'session runtime worker unhealthy' };
41
- process.stderr.write(`[session-runtime-health] ${unhealthyDetail.reason || 'unhealthy'}\n`);
65
+ process.stderr.write(
66
+ `[session-runtime-health] shard ${SHARD_INDEX}: ${unhealthyDetail.reason || 'unhealthy'}\n`,
67
+ );
42
68
  reportUnhealthy();
43
69
  });
44
70
 
@@ -67,6 +93,140 @@ function send(message) {
67
93
  return safeIpcSend(process, message);
68
94
  }
69
95
 
96
+ // ── Per-shard event-loop lag ───────────────────────────────────────────────
97
+ // The daemon can only measure its OWN loop; a shard saturated by provider
98
+ // parsing or a runaway tool is invisible there. Sampling here and reporting to
99
+ // the host gives per-shard saturation telemetry and lets the host stop placing
100
+ // NEW work on this shard. It never triggers a kill: this process still owns
101
+ // accepted input and in-flight turns.
102
+ let lastLagSample = null;
103
+
104
+ const stopEventLoopLagMonitor = startRuntimeEventLoopLagMonitor({
105
+ onSample: (sample) => {
106
+ lastLagSample = sample;
107
+ if (stopping) return;
108
+ send({ type: 'event-loop-lag', shard: SHARD_INDEX, sample });
109
+ void publishProviderCooldown();
110
+ },
111
+ });
112
+
113
+ // ── Cross-shard provider cooldown ──────────────────────────────────────────
114
+ // Fast-mode capacity cooldown is an ACCOUNT-wide fact held in process-local
115
+ // state. With several shards, one shard discovering the drained pool must not
116
+ // leave siblings hammering it, so cooldowns are published to the host and
117
+ // replayed into every other shard. Only the module's public API is used: a
118
+ // replay is expressed as the same capacity rejection the discovering shard saw.
119
+ let fastModePromise = null;
120
+ let fastModePolicy = null;
121
+ let lastCooldownSent = { untilMs: 0, disabledReason: null };
122
+ let admissionModulePromise = null;
123
+ let cooldownBridgeUnsubscribe = null;
124
+ let cooldownBridgePromise = null;
125
+
126
+ function fastModeModule() {
127
+ fastModePromise ??= import('../runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs')
128
+ .then((policy) => { fastModePolicy = policy; return policy; })
129
+ .catch(() => null);
130
+ return fastModePromise;
131
+ }
132
+
133
+ function admissionSchedulerModule() {
134
+ admissionModulePromise ??= import('../runtime/agent/orchestrator/providers/admission-scheduler.mjs')
135
+ .catch(() => null);
136
+ return admissionModulePromise;
137
+ }
138
+
139
+ /**
140
+ * Subscribe to the provider admission scheduler's cooldown events so a 429
141
+ * observed here reaches sibling shards ON THE EVENT — waiting for the next
142
+ * telemetry tick would leave every other shard probing a pool this process
143
+ * already knows is drained. Idempotent and safe to call from any provider
144
+ * entry point.
145
+ */
146
+ function ensureProviderCooldownBridge() {
147
+ if (cooldownBridgeUnsubscribe || cooldownBridgePromise || stopping) return;
148
+ cooldownBridgePromise = admissionSchedulerModule().then((module) => {
149
+ if (!module || stopping || cooldownBridgeUnsubscribe) return;
150
+ cooldownBridgeUnsubscribe = module.providerAdmissionScheduler.onCooldownEvent((event) => {
151
+ publishProviderCooldownNow(event);
152
+ });
153
+ }).catch(() => { /* provider graph is absent in this shard */ })
154
+ .finally(() => { cooldownBridgePromise = null; });
155
+ }
156
+
157
+ function normalizeAdmissionCooldownEvent(event) {
158
+ if (!event || typeof event !== 'object') return null;
159
+ const type = String(event.type || '');
160
+ if (type === 'cooldown') {
161
+ const key = String(event.key || '');
162
+ const cooldownUntil = Number(event.cooldownUntil) || 0;
163
+ if (!key || cooldownUntil <= Date.now()) return null;
164
+ return { type, key, cooldownUntil };
165
+ }
166
+ if (type === 'reset') {
167
+ return { type, provider: event.provider ? String(event.provider) : null };
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Synchronous publish path. Costs one comparison when nothing changed, so it
174
+ * can be called from provider/runtime boundaries and from the cooldown event
175
+ * itself without waiting on any import.
176
+ */
177
+ function publishProviderCooldownNow(admissionEvent = null) {
178
+ if (stopping) return false;
179
+ const admission = normalizeAdmissionCooldownEvent(admissionEvent);
180
+ const observed = fastModePolicy ? readProviderCooldown(fastModePolicy) : null;
181
+ const advanced = Boolean(observed) && providerCooldownAdvanced(lastCooldownSent, observed);
182
+ if (!admission && !advanced) return false;
183
+ if (advanced) lastCooldownSent = mergeKnownProviderCooldown(lastCooldownSent, observed);
184
+ send({
185
+ type: 'provider-cooldown',
186
+ shard: SHARD_INDEX,
187
+ cooldown: observed || { untilMs: 0, disabledReason: null, observedAt: Date.now() },
188
+ ...(admission ? { admission } : {}),
189
+ });
190
+ return true;
191
+ }
192
+
193
+ /** Backstop sweep (telemetry tick): loads the policy module if provider work
194
+ * has happened, then reuses the synchronous path. */
195
+ async function publishProviderCooldown() {
196
+ // Only shards that actually run provider work carry cooldown state.
197
+ if (stopping || (!agentGraphPromise && records.size === 0)) return;
198
+ const policy = await fastModeModule();
199
+ if (!policy || stopping) return;
200
+ publishProviderCooldownNow();
201
+ }
202
+
203
+ async function applyReplayedProviderCooldown(frame) {
204
+ const cooldown = frame?.cooldown ?? frame ?? null;
205
+ const admission = frame?.admission && typeof frame.admission === 'object' ? frame.admission : null;
206
+ if (admission) {
207
+ const module = await admissionSchedulerModule();
208
+ if (module) {
209
+ // applyExternalCooldown/resetCooldowns are the documented cross-process
210
+ // seams: neither re-emits for a broadcast that changed nothing, so the
211
+ // replay cannot loop between shards.
212
+ if (admission.type === 'cooldown') {
213
+ module.providerAdmissionScheduler.applyExternalCooldown(
214
+ String(admission.key || ''),
215
+ Number(admission.cooldownUntil) || 0,
216
+ );
217
+ } else if (admission.type === 'reset') {
218
+ module.resetProviderAdmissionCooldowns(admission.provider || null);
219
+ }
220
+ }
221
+ }
222
+ const policy = await fastModeModule();
223
+ if (!policy) return Boolean(admission);
224
+ const applied = applyProviderCooldown(policy, cooldown);
225
+ // A replayed cooldown is not a local discovery: suppress the echo back.
226
+ lastCooldownSent = mergeKnownProviderCooldown(lastCooldownSent, cooldown);
227
+ return applied || Boolean(admission);
228
+ }
229
+
70
230
  process.on('mixdog:turn-timing', (row = {}) => {
71
231
  send({
72
232
  type: 'turn-timing',
@@ -101,6 +261,94 @@ function publish(record, forceFull = false) {
101
261
  });
102
262
  }
103
263
 
264
+ function agentOwnedSession(session) {
265
+ const owner = String(session?.owner || '').trim().toLowerCase();
266
+ const agent = String(session?.agent || '').trim().toLowerCase();
267
+ return Boolean(session?.id)
268
+ && (owner === 'agent' || (agent && agent !== 'lead'));
269
+ }
270
+
271
+ async function projectAgentSession(session) {
272
+ const [
273
+ { restoreTranscriptItems },
274
+ { getSessionProgressSnapshot },
275
+ ] = await Promise.all([
276
+ import('../tui/session/session-api-ext.mjs'),
277
+ import('../runtime/agent/orchestrator/session/manager.mjs'),
278
+ ]);
279
+ const sessionId = String(session.id || '');
280
+ const status = String(session.status || '').trim().toLowerCase();
281
+ const progress = getSessionProgressSnapshot?.(sessionId) || null;
282
+ const stage = String(progress?.stage || '').trim().toLowerCase();
283
+ const busy = status === 'running' || status === 'queued'
284
+ || (stage && stage !== 'idle' && stage !== 'done' && stage !== 'closed');
285
+ return sanitizeForWire({
286
+ sessionId,
287
+ items: restoreTranscriptItems(
288
+ Array.isArray(session.messages) ? session.messages : [],
289
+ { sessionId, itemLimit: 512 },
290
+ ),
291
+ queued: [],
292
+ busy,
293
+ commandBusy: false,
294
+ provider: session.provider || '',
295
+ model: session.model || '',
296
+ effort: session.effort || '',
297
+ fast: session.fast === true,
298
+ modelParameters: session.modelParameters || {},
299
+ cwd: session.cwd || '',
300
+ desktopSession: session.desktopSession || null,
301
+ workflow: session.workflow || null,
302
+ ownerClientHostPid: Number(session.clientHostPid) || process.pid,
303
+ }) || null;
304
+ }
305
+
306
+ function scheduleAgentSessionProjection(session) {
307
+ if (!agentOwnedSession(session) || stopping) return;
308
+ const sessionId = String(session.id);
309
+ pendingAgentSessionProjections.set(sessionId, session);
310
+ if (activeAgentSessionProjections.has(sessionId)) return;
311
+ activeAgentSessionProjections.add(sessionId);
312
+ queueMicrotask(() => {
313
+ void (async () => {
314
+ while (!stopping) {
315
+ const latest = pendingAgentSessionProjections.get(sessionId);
316
+ if (!latest) break;
317
+ pendingAgentSessionProjections.delete(sessionId);
318
+ const snapshot = await projectAgentSession(latest);
319
+ if (!snapshot || stopping) continue;
320
+ send({ type: 'agent-session-state', sessionId, snapshot });
321
+ }
322
+ })().catch((error) => {
323
+ try {
324
+ process.stderr.write(
325
+ `[session-runtime-worker] agent session projection failed session=${sessionId}: `
326
+ + `${error?.message || error}\n`,
327
+ );
328
+ } catch {}
329
+ }).finally(() => {
330
+ activeAgentSessionProjections.delete(sessionId);
331
+ if (pendingAgentSessionProjections.has(sessionId) && !stopping) {
332
+ scheduleAgentSessionProjection(pendingAgentSessionProjections.get(sessionId));
333
+ }
334
+ });
335
+ });
336
+ }
337
+
338
+ async function ensureAgentSessionPublisher() {
339
+ if (unsubscribeAgentSessionPublisher) return;
340
+ agentSessionPublisherPromise ??= import(
341
+ '../runtime/agent/orchestrator/session/store.mjs'
342
+ ).then((store) => {
343
+ unsubscribeAgentSessionPublisher = store.subscribeLiveSessions(
344
+ scheduleAgentSessionProjection,
345
+ );
346
+ }).finally(() => {
347
+ agentSessionPublisherPromise = null;
348
+ });
349
+ await agentSessionPublisherPromise;
350
+ }
351
+
104
352
  async function createRuntime(message) {
105
353
  const module = await sessionModule();
106
354
  const runtime = await module.createLocalSessionRuntime(message.options || {});
@@ -115,6 +363,9 @@ async function createRuntime(message) {
115
363
  records.set(record.id, record);
116
364
  record.unsubscribe = runtime.subscribe?.(() => publish(record)) ?? null;
117
365
  publish(record, true);
366
+ // Session turns run providers in this shard too: their cooldowns must reach
367
+ // sibling shards on the event, not on the next telemetry tick.
368
+ ensureProviderCooldownBridge();
118
369
  return { created: true };
119
370
  }
120
371
 
@@ -130,6 +381,9 @@ async function callRuntime(message) {
130
381
  }
131
382
  const value = await target.apply(record.runtime, Array.isArray(message.args) ? message.args : []);
132
383
  publish(record);
384
+ // Cheap synchronous check: a fast-mode cooldown that the admission scheduler
385
+ // never saw (long provider window) still leaves at the first call boundary.
386
+ publishProviderCooldownNow();
133
387
  return sanitizeForWire(value);
134
388
  }
135
389
 
@@ -157,6 +411,162 @@ function agentGraph() {
157
411
  }
158
412
  const agentDispatchers = new Map();
159
413
  const agentDispatchRuns = new Map(); // dispatchId -> AbortController
414
+ let distributedAgentToolPromise = null;
415
+ const distributedAgentControlRuns = new Map(); // controlId -> AbortController
416
+
417
+ function distributedAgentTool() {
418
+ distributedAgentToolPromise ??= Promise.all([
419
+ import('../standalone/agent-tool.mjs'),
420
+ import('../runtime/agent/orchestrator/config.mjs'),
421
+ import('../runtime/agent/orchestrator/providers/registry.mjs'),
422
+ import('../runtime/agent/orchestrator/session/manager.mjs'),
423
+ ]).then(async ([agentModule, cfgMod, reg, mgr]) => {
424
+ await ensureAgentSessionPublisher();
425
+ return agentModule.createStandaloneAgent({
426
+ cfgMod,
427
+ reg,
428
+ mgr,
429
+ dataDir: cfgMod.getPluginData(),
430
+ cwd: process.cwd(),
431
+ awaitKeychainPrewarm: async () => {},
432
+ isKeychainPrewarmReady: () => true,
433
+ notifySessionCompletion(ownerSessionId, text, meta = {}) {
434
+ return send({
435
+ type: 'agent-control-notification',
436
+ ownerSessionId: String(ownerSessionId || ''),
437
+ text: String(text || ''),
438
+ meta: sanitizeForWire(meta) || {},
439
+ });
440
+ },
441
+ });
442
+ }).catch((error) => {
443
+ distributedAgentToolPromise = null;
444
+ throw error;
445
+ });
446
+ return distributedAgentToolPromise;
447
+ }
448
+
449
+ async function runDistributedAgentControl(message) {
450
+ const controlId = String(message.controlId || '');
451
+ if (!controlId) throw new Error('agent control id is required');
452
+ if (distributedAgentControlRuns.has(controlId)) {
453
+ throw new Error(`agent control ${controlId} is already running`);
454
+ }
455
+ const controller = new AbortController();
456
+ distributedAgentControlRuns.set(controlId, controller);
457
+ try {
458
+ const tool = await distributedAgentTool();
459
+ if (controller.signal.aborted) throw controller.signal.reason;
460
+ const context = message.context && typeof message.context === 'object'
461
+ ? message.context
462
+ : {};
463
+ if (String(message.args?.type || '') === '__close_all') {
464
+ tool.closeAll?.(
465
+ String(message.args?.reason || 'agent owner closed'),
466
+ { callerSessionId: context.callerSessionId || null },
467
+ );
468
+ return 'agent close all: ok';
469
+ }
470
+ return await tool.execute(message.args || {}, {
471
+ callerCwd: context.callerCwd || process.cwd(),
472
+ invocationSource: context.invocationSource || 'model-tool',
473
+ callerSessionId: context.callerSessionId || null,
474
+ clientHostPid: Number(context.clientHostPid) || process.pid,
475
+ signal: controller.signal,
476
+ });
477
+ } finally {
478
+ distributedAgentControlRuns.delete(controlId);
479
+ }
480
+ }
481
+
482
+ async function distributedAgentStatus(message) {
483
+ const tool = await distributedAgentTool();
484
+ return tool.getStatus?.(message.context || {}) || { workers: [], jobs: [], scope: null };
485
+ }
486
+
487
+ function cancelDistributedAgentControl(message) {
488
+ const controlId = String(message.controlId || '');
489
+ const controller = distributedAgentControlRuns.get(controlId);
490
+ if (!controller) return { cancelled: false };
491
+ abortDispatchController(
492
+ controller,
493
+ new Error(String(message.reason || 'agent control canceled')),
494
+ );
495
+ return { cancelled: true };
496
+ }
497
+
498
+ async function deliverDistributedAgentNotification(message) {
499
+ const ownerSessionId = String(message.ownerSessionId || '');
500
+ if (!ownerSessionId || !message.text) return false;
501
+ for (const record of records.values()) {
502
+ if (record.disposed) continue;
503
+ try {
504
+ if (record.runtime?.deliverToolCompletion?.(
505
+ ownerSessionId,
506
+ String(message.text),
507
+ message.meta || {},
508
+ )) return true;
509
+ } catch {}
510
+ }
511
+ // The owning runtime may be between recycle and recreate. Persist the
512
+ // completion in its ordinary pending queue so the resumed session still
513
+ // consumes it exactly once.
514
+ try {
515
+ const [mgr, contract] = await Promise.all([
516
+ import('../runtime/agent/orchestrator/session/manager.mjs'),
517
+ import('../runtime/shared/tool-execution-contract.mjs'),
518
+ ]);
519
+ const visible = contract.modelVisibleToolCompletionMessage(
520
+ String(message.text),
521
+ message.meta || {},
522
+ );
523
+ return Boolean(visible && mgr.enqueuePendingMessage(
524
+ ownerSessionId,
525
+ mgr.markCompletionEntry(visible, {
526
+ executionId: message.meta?.execution_id,
527
+ }),
528
+ ) > 0);
529
+ } catch {
530
+ return false;
531
+ }
532
+ }
533
+ // A cancel can arrive while a dispatch is still in its COLD-START window
534
+ // (orchestrator import + provider init) or even before its own request frame is
535
+ // handled. Answering "not running" there would drop the abort and let the
536
+ // dispatch run anyway, so the cancellation is retained and adopted the moment
537
+ // the dispatch registers.
538
+ const retainedDispatchCancels = new Map(); // dispatchId -> { reason, at }
539
+ const RETAINED_CANCEL_MAX = 256;
540
+ const RETAINED_CANCEL_TTL_MS = 5 * 60_000;
541
+
542
+ function retainDispatchCancel(dispatchId, reason) {
543
+ retainedDispatchCancels.delete(dispatchId);
544
+ retainedDispatchCancels.set(dispatchId, { reason, at: Date.now() });
545
+ while (retainedDispatchCancels.size > RETAINED_CANCEL_MAX) {
546
+ const oldest = retainedDispatchCancels.keys().next().value;
547
+ if (oldest === undefined) break;
548
+ retainedDispatchCancels.delete(oldest);
549
+ }
550
+ }
551
+
552
+ function takeRetainedDispatchCancel(dispatchId) {
553
+ const entry = retainedDispatchCancels.get(dispatchId);
554
+ if (!entry) return null;
555
+ retainedDispatchCancels.delete(dispatchId);
556
+ // A stale tombstone must never abort an unrelated later dispatch.
557
+ if (Date.now() - entry.at > RETAINED_CANCEL_TTL_MS) return null;
558
+ return entry.reason;
559
+ }
560
+
561
+ function abortDispatchController(controller, reason) {
562
+ try { controller.abort(reason); } catch { try { controller.abort(); } catch { /* settled */ } }
563
+ }
564
+
565
+ function throwIfDispatchAborted(controller) {
566
+ if (!controller.signal.aborted) return;
567
+ const reason = controller.signal.reason;
568
+ throw reason instanceof Error ? reason : new Error(String(reason || 'agent dispatch canceled'));
569
+ }
160
570
  let preparedProviderSignature = null;
161
571
  let providerPreparePromise = null;
162
572
 
@@ -187,20 +597,31 @@ async function runAgentDispatch(message) {
187
597
  if (agentDispatchRuns.has(dispatchId)) {
188
598
  throw new Error(`agent dispatch ${dispatchId} is already running`);
189
599
  }
190
- const agent = String(message.agent || '');
191
- const { dispatch } = await agentGraph();
192
- await prepareAgentProviders();
193
- let dispatcher = agentDispatchers.get(agent);
194
- if (!dispatcher) {
195
- dispatcher = dispatch.makeAgentDispatch({
196
- agent,
197
- ...(message.options && typeof message.options === 'object' ? message.options : {}),
198
- });
199
- agentDispatchers.set(agent, dispatcher);
200
- }
600
+ // Registration happens BEFORE the first await. The cold-start window (module
601
+ // graph import + provider init) is exactly when a user abort arrives, and an
602
+ // unregistered dispatch would answer cancelled:false and then run anyway.
201
603
  const controller = new AbortController();
202
604
  agentDispatchRuns.set(dispatchId, controller);
605
+ const retained = takeRetainedDispatchCancel(dispatchId);
606
+ if (retained) abortDispatchController(controller, retained);
203
607
  try {
608
+ throwIfDispatchAborted(controller);
609
+ const agent = String(message.agent || '');
610
+ const { dispatch } = await agentGraph();
611
+ throwIfDispatchAborted(controller);
612
+ await ensureAgentSessionPublisher();
613
+ throwIfDispatchAborted(controller);
614
+ await prepareAgentProviders();
615
+ throwIfDispatchAborted(controller);
616
+ ensureProviderCooldownBridge();
617
+ let dispatcher = agentDispatchers.get(agent);
618
+ if (!dispatcher) {
619
+ dispatcher = dispatch.makeAgentDispatch({
620
+ agent,
621
+ ...(message.options && typeof message.options === 'object' ? message.options : {}),
622
+ });
623
+ agentDispatchers.set(agent, dispatcher);
624
+ }
204
625
  const params = message.params && typeof message.params === 'object' ? message.params : {};
205
626
  const value = await dispatcher({
206
627
  prompt: String(params.prompt ?? ''),
@@ -214,15 +635,24 @@ async function runAgentDispatch(message) {
214
635
  return { value: sanitizeForWire(value) ?? null };
215
636
  } finally {
216
637
  agentDispatchRuns.delete(dispatchId);
638
+ publishProviderCooldownNow();
217
639
  }
218
640
  }
219
641
 
220
642
  function cancelAgentDispatch(message) {
221
- const controller = agentDispatchRuns.get(String(message.dispatchId || ''));
222
- if (!controller) return { cancelled: false };
643
+ const dispatchId = String(message.dispatchId || '');
223
644
  const reason = new Error(String(message.reason || 'agent dispatch canceled'));
224
- try { controller.abort(reason); } catch { try { controller.abort(); } catch { /* settled */ } }
225
- return { cancelled: true };
645
+ const controller = agentDispatchRuns.get(dispatchId);
646
+ if (controller) {
647
+ abortDispatchController(controller, reason);
648
+ return { cancelled: true, retained: false };
649
+ }
650
+ if (!dispatchId) return { cancelled: false, retained: false };
651
+ // Not registered yet: retain the cancellation instead of losing it. The
652
+ // dispatch adopts it at registration, so a cold-start abort can never be
653
+ // answered "not running" and then keep running.
654
+ retainDispatchCancel(dispatchId, reason);
655
+ return { cancelled: true, retained: true };
226
656
  }
227
657
 
228
658
  async function prewarm() {
@@ -247,6 +677,8 @@ async function workloadSnapshot() {
247
677
  ]);
248
678
  const memory = process.memoryUsage();
249
679
  return {
680
+ shard: SHARD_INDEX,
681
+ eventLoopLag: lastLagSample,
250
682
  runtimes: records.size,
251
683
  memory: {
252
684
  rssBytes: memory.rss,
@@ -284,9 +716,21 @@ async function exitAfterPendingWrites(code = 0) {
284
716
  async function stopAll(reason = 'session runtime worker shutdown') {
285
717
  if (stopping) return;
286
718
  stopping = true;
719
+ try { stopEventLoopLagMonitor(); } catch {}
720
+ try { cooldownBridgeUnsubscribe?.(); } catch {}
721
+ cooldownBridgeUnsubscribe = null;
722
+ retainedDispatchCancels.clear();
723
+ for (const controller of distributedAgentControlRuns.values()) {
724
+ abortDispatchController(controller, new Error(reason));
725
+ }
726
+ distributedAgentControlRuns.clear();
287
727
  for (const controller of agentDispatchRuns.values()) {
288
728
  try { controller.abort(new Error(reason)); } catch { /* settled */ }
289
729
  }
730
+ try { unsubscribeAgentSessionPublisher?.(); } catch {}
731
+ unsubscribeAgentSessionPublisher = null;
732
+ pendingAgentSessionProjections.clear();
733
+ try { (await distributedAgentToolPromise)?.closeAll?.(); } catch {}
290
734
  for (const record of [...records.values()]) {
291
735
  try {
292
736
  await disposeSessionRuntimeRecord(
@@ -302,7 +746,16 @@ async function stopAll(reason = 'session runtime worker shutdown') {
302
746
  process.on('message', (message) => {
303
747
  if (!message || typeof message !== 'object') return;
304
748
  // token-native.mjs owns daemon-relayed token count responses.
305
- if (message.type === 'token-native-result') return;
749
+ if (message.type === 'token-native-result' || message.type === 'agent-control-result') return;
750
+ // Fire-and-forget shard coordination: no response frame, no request id.
751
+ if (message.type === 'provider-cooldown-sync') {
752
+ void applyReplayedProviderCooldown(message);
753
+ return;
754
+ }
755
+ if (message.type === 'agent-control-notification') {
756
+ void deliverDistributedAgentNotification(message);
757
+ return;
758
+ }
306
759
  const requestId = String(message.requestId || '');
307
760
  void (async () => {
308
761
  if (message.type === 'create') return createRuntime(message);
@@ -317,6 +770,11 @@ process.on('message', (message) => {
317
770
  if (message.type === 'workload') return workloadSnapshot();
318
771
  if (message.type === 'agent-dispatch') return runAgentDispatch(message);
319
772
  if (message.type === 'agent-dispatch-cancel') return cancelAgentDispatch(message);
773
+ if (message.type === 'agent-control-local') return runDistributedAgentControl(message);
774
+ if (message.type === 'agent-control-status') return distributedAgentStatus(message);
775
+ if (message.type === 'agent-control-local-cancel') {
776
+ return cancelDistributedAgentControl(message);
777
+ }
320
778
  if (message.type === 'shutdown') {
321
779
  await stopAll(message.reason);
322
780
  return { stopped: true };