mixdog 0.8.0 → 0.9.0

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 (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -4,16 +4,14 @@
4
4
  * Scheduler instance (which would create a module cycle).
5
5
  *
6
6
  * channels/index.mjs registers a listener at boot that forwards into
7
- * scheduler.noteActivity(). ai-wrapped-dispatch (and any other
8
- * orchestrator-side producers) call notifyActivity() near the point
9
- * where work is kicked off.
7
+ * scheduler.noteActivity(). Producers signal activity via setListener's
8
+ * forwarded pings.
10
9
  *
11
- * Boot race: if notifyActivity() fires before channels boot registers
12
- * the listener, we buffer the most-recent ping and replay it on
13
- * setListener. Only one timestamp is buffered — "most recent activity
14
- * was at T" is all scheduler.mjs needs.
15
- *
16
- * All failures are swallowed — an activity ping is never load-bearing.
10
+ * Boot race: a ping that fires before channels boot registers the listener
11
+ * is buffered (most-recent wins) and replayed on setListener. Only one
12
+ * timestamp is buffered — "most recent activity was at T" is all
13
+ * scheduler.mjs needs. All failures are swallowed — a ping is never
14
+ * load-bearing.
17
15
  */
18
16
 
19
17
  let _listener = null;
@@ -27,12 +25,3 @@ export function setListener(fn) {
27
25
  try { _listener(ts); } catch { /* best-effort */ }
28
26
  }
29
27
  }
30
-
31
- export function notifyActivity() {
32
- const ts = Date.now();
33
- if (_listener) {
34
- try { _listener(ts); } catch { /* best-effort */ }
35
- return;
36
- }
37
- _pendingPingAt = ts; // buffer one, most recent wins
38
- }
@@ -0,0 +1,11 @@
1
+ export const AGENT_OWNER = 'agent';
2
+
3
+ function ownerValue(value) {
4
+ if (value && typeof value === 'object') return String(value.owner || '').trim().toLowerCase();
5
+ return String(value || '').trim().toLowerCase();
6
+ }
7
+
8
+ export function isAgentOwner(value) {
9
+ const owner = ownerValue(value);
10
+ return owner === AGENT_OWNER;
11
+ }
@@ -1,19 +1,19 @@
1
1
  /**
2
- * Smart Bridge — Internal LLM Helper (session-based).
2
+ * Agent Runtime — Internal LLM Helper (session-based).
3
3
  *
4
4
  * Every one-shot LLM dispatch from internal callers (memory-cycle,
5
5
  * scheduler, webhook) now flows through the SAME session pipeline as the
6
- * MCP `bridge` tool. No more parallel `provider.send()` helper — one code
7
- * path = one message shape = one usage log = "bridge single path".
6
+ * MCP `agent` tool. No more parallel `provider.send()` helper — one code
7
+ * path = one message shape = one usage log = "agent single path".
8
8
  *
9
9
  * The returned function uses the existing caller signature, so call sites
10
10
  * do not need changes:
11
11
  *
12
- * const llm = makeBridgeLlm({ role: 'maintenance', preset: 'haiku' });
12
+ * const llm = makeAgentDispatch({ role: 'maintenance', preset: 'haiku' });
13
13
  * const text = await llm({ prompt });
14
14
  *
15
15
  * Internally it:
16
- * 1. Resolves the preset (explicit arg > opts.preset > user-workflow.json[role])
16
+ * 1. Resolves the preset (explicit arg > opts.preset > hidden-role config)
17
17
  * 2. Creates or reuses a session via the session manager
18
18
  * 3. Applies stateless-reset for stateless profiles so the prefix handle
19
19
  * stays warm while per-dispatch transcripts never leak
@@ -21,33 +21,27 @@
21
21
  * `session/manager.mjs` (mode='active')
22
22
  */
23
23
 
24
- import { readFileSync } from 'fs';
25
- import { join } from 'path';
26
24
  import { loadConfig } from '../config.mjs';
27
25
  import { resolveRuntimeSpec } from '../config.mjs';
28
- import { getHiddenRole, resolveBridgeSessionPermission } from '../internal-roles.mjs';
29
- import { prepareBridgeSession } from './session-builder.mjs';
30
- import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
26
+ import { getHiddenRole, resolveAgentSessionPermission } from '../internal-roles.mjs';
27
+ import { prepareAgentSession } from './session-builder.mjs';
31
28
  import {
32
29
  askSession,
33
30
  updateSessionStatus,
34
31
  closeSession,
35
32
  } from '../session/manager.mjs';
33
+ import {
34
+ agentWatchdogPolicyActive,
35
+ evaluateAgentWatchdogAbort,
36
+ resolveAgentWatchdogPolicy,
37
+ } from './agent-progress-watchdog.mjs';
36
38
 
37
- // Cap bridge role synthesis to ~3000 tokens (~12 KB at the 4 B/tok
39
+ // Cap agent role synthesis to ~3000 tokens (~12 KB at the 4 B/tok
38
40
  // working average). Pool B explore/recall/search answers occasionally land
39
41
  // 8-10k-token walls that then ride in the Lead context for the rest of the
40
42
  // turn; the cap keeps those outliers bounded without touching the 95%+ of
41
43
  // answers already under the threshold.
42
44
  const BRIEF_CAP_BYTES = 12 * 1024;
43
- function envTimeoutMs(name, fallback) {
44
- const raw = process.env[name];
45
- if (raw === undefined || raw === '') return fallback;
46
- const n = Number(raw);
47
- return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
48
- }
49
- const DEFAULT_FIRST_RESPONSE_TIMEOUT_MS = envTimeoutMs('MIXDOG_BRIDGE_FIRST_RESPONSE_TIMEOUT_MS', 120_000);
50
- const DEFAULT_STALE_TIMEOUT_MS = envTimeoutMs('MIXDOG_BRIDGE_STALE_TIMEOUT_MS', 30 * 60_000);
51
45
  function applyBriefCap(text) {
52
46
  if (typeof text !== 'string') return text;
53
47
  if (text.length <= BRIEF_CAP_BYTES) return text;
@@ -56,25 +50,35 @@ function applyBriefCap(text) {
56
50
  return `${head}\n\n... [TRUNCATED — full answer was ~${approxTokens} tokens / ${Math.round(text.length / 1024)} KB. Re-run with brief:false for the complete synthesis]`;
57
51
  }
58
52
 
59
- // Unified-shard policy — most bridge sessions (Pool B + Pool C) share the
53
+ // Unified-shard policy — most agent sessions (Pool B + Pool C) share the
60
54
  // same tool schema so BP_1 is bit-identical across roles and one provider-side
61
55
  // cache shard serves every caller. Per-role behaviour is steered by:
62
- // 1. rules/bridge/*.md concatenated into BP2 roleCatalog (via
63
- // loadScopedRoleCatalog — every bridge session carries the full
64
- // hidden-role catalog so the shard stays bit-identical across roles)
56
+ // 1. role-scoped BP2 instructions from agents/<role>.md and
57
+ // rules/agent/<role>.md
65
58
  // 2. call-time guards (loop.mjs write-block + ai-wrapped-dispatch
66
59
  // recursion break)
67
60
  // Hidden-role exceptions are declarative: defaults/hidden-roles.json may set
68
61
  // toolSchemaProfile when first-turn routing quality is worth a separate tool
69
- // prefix. Keep "unified" as the default; new profiles must justify the cache
70
- // shard split.
62
+ // prefix. Standard profiles are none/read/full; legacy names stay as aliases.
71
63
  // See manager.mjs resolveSessionTools for the single source of truth;
72
- // bridge visibility is declared via annotations.bridgeHidden on each tool def.
64
+ // agent visibility is declared via annotations.agentHidden on each tool def.
73
65
  const HIDDEN_ROLE_TOOL_SCHEMA_PROFILES = Object.freeze({
66
+ full: null,
67
+ none: Object.freeze([]),
68
+ read: Object.freeze([
69
+ 'code_graph',
70
+ 'find',
71
+ 'glob',
72
+ 'list',
73
+ 'grep',
74
+ 'read',
75
+ ]),
76
+ // Backward-compatible aliases for older hidden-role definitions.
74
77
  unified: null,
75
78
  'llm-only': Object.freeze([]),
76
79
  'filesystem-read': Object.freeze([
77
80
  'code_graph',
81
+ 'find',
78
82
  'glob',
79
83
  'list',
80
84
  'grep',
@@ -87,99 +91,127 @@ export function resolveHiddenRoleSchemaAllowedTools(hidden) {
87
91
  if (Array.isArray(hidden.schemaAllowedTools)) {
88
92
  return hidden.schemaAllowedTools.map((name) => String(name || '').trim()).filter(Boolean);
89
93
  }
90
- const profile = String(hidden.toolSchemaProfile || 'unified').trim() || 'unified';
94
+ const profile = String(hidden.toolSchemaProfile || 'full').trim() || 'full';
91
95
  if (Object.prototype.hasOwnProperty.call(HIDDEN_ROLE_TOOL_SCHEMA_PROFILES, profile)) {
92
96
  return HIDDEN_ROLE_TOOL_SCHEMA_PROFILES[profile];
93
97
  }
94
- process.stderr.write(`[bridge-llm] unknown hidden-role toolSchemaProfile="${profile}" role="${hidden.name || 'unknown'}"; using unified schema\n`);
98
+ process.stderr.write(`[agent-dispatch] unknown hidden-role toolSchemaProfile="${profile}" role="${hidden.name || 'unknown'}"; using full schema\n`);
95
99
  return null;
96
100
  }
97
101
 
98
102
  /**
99
- * Resolve a preset name from (preset arg | opts.preset | hidden-role | user-workflow).
103
+ * Resolve the maintenance ROUTE (or legacy preset name) for a dispatch.
100
104
  *
101
- * Hidden roles (explorer, cycle1/cycle2, scheduler-task, etc.) are
102
- * Mixdog-managed and take precedence over user-workflow.jsonusers cannot
103
- * override them by redefining the same name.
105
+ * Returns one of:
106
+ * - a route object `{ provider, model, effort?, fast? }` the preferred
107
+ * shape: a maintenance slot now stores its model directly (parity with
108
+ * `agents.<role>`), so no preset-array name lookup is needed.
109
+ * - a string — a legacy preset NAME still stored in a maintenance slot, or an
110
+ * explicit `preset`/`opts.preset` override. The caller resolves the name
111
+ * against config.presets for backward compatibility.
112
+ * - null — unresolved.
113
+ *
114
+ * Hidden roles read their slot from `maint[maintKey || slot]`; the cycle1/2/3
115
+ * agents share one knob via the `maintKey: 'memory'` override.
104
116
  */
105
- export function resolvePresetName({ preset, optsPreset, role, config: cfgIn = null }) {
117
+ export function resolveMaintenanceRoute({ preset, optsPreset, role, config: cfgIn = null }) {
106
118
  if (preset) return preset;
107
119
  if (optsPreset) return optsPreset;
108
120
  if (!role) return null;
109
- // Hidden roles resolve their maintenance preset by SLOT. Every slot carries
110
- // a concrete default in DEFAULT_MAINTENANCE, so `maint[slot]` resolves
111
- // directly; the Setup panel can still tune each slot independently.
112
- // (explorer.slot = 'explore', cycle1-agent.slot = 'cycle1', …). A hidden
113
- // role may override which maintenance key it reads via `maintKey`
114
- // (e.g. the cycle1/2/3 agents all read `maint.memory` instead of their
115
- // own slot) so several agents can share one model knob while keeping
116
- // distinct slots/identity.
117
121
  const hidden = getHiddenRole(role);
118
122
  if (hidden) {
119
123
  try {
120
- const config = cfgIn || loadConfig();
124
+ const config = cfgIn || loadConfig({ secrets: false });
121
125
  const maint = config?.maintenance || {};
122
- return maint[hidden.maintKey || hidden.slot] || null;
126
+ return maint[hidden.maintKey || hidden.slot] ?? null;
123
127
  } catch { return null; }
124
128
  }
125
- try {
126
- const pluginData = resolvePluginData();
127
- if (!pluginData) return null;
128
- const wf = JSON.parse(readFileSync(join(pluginData, 'user-workflow.json'), 'utf8'));
129
- if (!Array.isArray(wf.roles)) return null;
130
- const entry = wf.roles.find((r) => r.name === role);
131
- return entry ? entry.preset : null;
132
- } catch {
133
- return null;
134
- }
129
+ return null;
130
+ }
131
+
132
+ // Back-compat alias: older callers/tests import resolvePresetName. It now
133
+ // returns whatever resolveMaintenanceRoute does (route object OR name string).
134
+ export const resolvePresetName = resolveMaintenanceRoute;
135
+
136
+ // A maintenance slot value is a direct route when it carries provider+model.
137
+ function maintenanceRouteToPreset(routeOrName, role) {
138
+ if (!routeOrName || typeof routeOrName !== 'object') return null;
139
+ const provider = String(routeOrName.provider || '').trim();
140
+ const model = String(routeOrName.model || '').trim();
141
+ if (!provider || !model) return null;
142
+ const out = {
143
+ id: `maint-${role}`,
144
+ name: `MAINT ${String(role || '').toUpperCase()}`,
145
+ type: 'agent',
146
+ provider,
147
+ model,
148
+ tools: 'full',
149
+ };
150
+ const effort = String(routeOrName.effort || '').trim();
151
+ if (effort) out.effort = effort;
152
+ if (routeOrName.fast === true) out.fast = true;
153
+ return out;
135
154
  }
136
155
 
137
156
  /**
138
- * Build a bridge-backed LLM callback.
157
+ * Build an agent-backed dispatch callback.
139
158
  *
140
159
  * @param {object} opts
141
160
  * @param {string} opts.role — REQUIRED; canonical role name (worker, cycle1-agent, scheduler-task, ...)
142
161
  * @param {string} [opts.taskType] — optional internal classification stamped on the session
143
162
  * @param {string} [opts.preset] — explicit preset override (bypasses role → preset lookup)
144
- * @param {string} [opts.parentSessionId] — parent bridge session for trace aggregation
163
+ * @param {string} [opts.parentSessionId] — parent agent session for trace aggregation
145
164
  * @param {string|null} [opts.ownerSessionId] — owning Mixdog session for statusline isolation
146
165
  * @param {AbortSignal} [opts.parentSignal] — optional AbortSignal from the fan-out coordinator;
147
- * when aborted the bridge role session's own controller is also aborted so the
166
+ * when aborted the agent role session's own controller is also aborted so the
148
167
  * provider call tears down promptly (parent→child cascade).
149
168
  * @returns {(args: { prompt, preset?, sourceName? }) => Promise<string>}
150
169
  */
151
- export function makeBridgeLlm(opts = {}) {
170
+ export function makeAgentDispatch(opts = {}) {
152
171
  if (!opts.role || typeof opts.role !== 'string') {
153
- throw new Error('[bridge-llm] opts.role is required');
172
+ throw new Error('[agent-dispatch] opts.role is required');
154
173
  }
155
174
  const role = opts.role;
156
175
 
157
- return async function bridgeLlm({ prompt, preset: presetArg, sourceName: sourceNameArg, parentSignal: callParentSignal, idleTimeoutMs: callIdleTimeoutMs }) {
176
+ return async function agentDispatch({ prompt, preset: presetArg, sourceName: sourceNameArg, parentSignal: callParentSignal, idleTimeoutMs: callIdleTimeoutMs }) {
158
177
  if (typeof prompt !== 'string' || !prompt) {
159
- throw new Error(`[bridge-llm] prompt required for role "${role}"`);
178
+ throw new Error(`[agent-dispatch] prompt required for role "${role}"`);
160
179
  }
161
180
 
162
- const config = opts.config || loadConfig();
163
- const presetName = resolvePresetName({
181
+ const config = opts.config || loadConfig({ secrets: false });
182
+ const routeOrName = resolveMaintenanceRoute({
164
183
  preset: presetArg,
165
184
  optsPreset: opts.preset,
166
185
  role,
167
186
  config,
168
187
  });
169
- if (!presetName) {
188
+ if (!routeOrName) {
170
189
  throw new Error(
171
- `[bridge-llm] preset unresolved for role "${role}" `
190
+ `[agent-dispatch] maintenance route unresolved for role "${role}" `
172
191
  + `(preset="${presetArg || opts.preset || ''}")`,
173
192
  );
174
193
  }
175
-
176
- const preset = config.presets?.find((p) => p.id === presetName || p.name === presetName);
194
+ // Preferred path: a maintenance slot that stores its model directly
195
+ // (route object). Legacy path: a slot still holding a preset NAME —
196
+ // resolve it against config.presets for backward compatibility.
197
+ let preset = maintenanceRouteToPreset(routeOrName, role);
177
198
  if (!preset) {
178
- throw new Error(`[bridge-llm] preset "${presetName}" not found in mixdog-config.json`);
199
+ const legacyName = String(routeOrName || '').trim();
200
+ preset = config.presets?.find((p) => p.id === legacyName || p.name === legacyName) || null;
201
+ if (!preset) {
202
+ throw new Error(
203
+ `[agent-dispatch] maintenance route for role "${role}" is neither a `
204
+ + `{provider,model} route nor a known preset name ("${legacyName}")`,
205
+ );
206
+ }
179
207
  }
208
+ // Stable label for traces / session metadata, derived from the resolved
209
+ // preset object regardless of whether it came from a direct route or a
210
+ // legacy preset name.
211
+ const presetName = preset.id || preset.name || `maint-${role}`;
180
212
 
181
213
  const runtimeSpec = resolveRuntimeSpec(preset, {
182
- lane: 'bridge',
214
+ lane: 'agent',
183
215
  agentId: role,
184
216
  });
185
217
 
@@ -189,38 +221,37 @@ export function makeBridgeLlm(opts = {}) {
189
221
  // we pass `null` through instead of falling back to `process.cwd()`
190
222
  // — the MCP server's launch dir is not deterministic across callers,
191
223
  // and the downstream skill-discovery path tolerates null. Combined
192
- // with the frozen bridge skill meta-tools (collect.mjs) this keeps
224
+ // with the frozen agent skill meta-tools (collect.mjs) this keeps
193
225
  // every caller on the same provider cache shard.
194
226
  const cwd = (typeof opts.cwd === 'string' && opts.cwd) ? opts.cwd : null;
195
227
 
196
228
  // Unified dispatch: Pool B/C share bit-identical tools + system prompt
197
229
  // unless a hidden role declares a narrow toolSchemaProfile. Per-role
198
- // differentiation rides in the user-message header (permission + role
199
- // line) plus an optional short Pool C snippet. The read-only contract
200
- // is still enforced at call time via loop.mjs's READ_BLOCKED_TOOLS
201
- // guard; schema profiles are a routing-efficiency layer, not safety.
230
+ // differentiation lives in scoped role rules / stable session context;
231
+ // raw role and permission labels are not repeated in the prompt.
232
+ // Runtime permission enforcement was removed (every tool call is
233
+ // trusted); schema profiles remain a routing-efficiency layer that
234
+ // narrows the advertised tool list, not a runtime safety gate.
202
235
  const hidden = getHiddenRole(role);
203
236
  const isPoolC = Boolean(hidden);
204
237
  // Permission: read-declared hidden roles are locked in
205
- // resolveBridgeSessionPermission (prepareBridgeSession applies the same).
206
- const permission = resolveBridgeSessionPermission(
238
+ // resolveAgentSessionPermission (prepareAgentSession applies the same).
239
+ const permission = resolveAgentSessionPermission(
207
240
  role,
208
241
  opts.permission ?? (isPoolC ? (hidden?.permission || 'read') : null),
209
242
  );
210
- // Pool C hidden-role instructions live in BP2 roleCatalog (loaded
211
- // by loadScopedRoleCatalog from rules/bridge/*.md) — the Tier 3
212
- // reminder is suppressed so the shard stays bit-identical across
213
- // every bridge role.
243
+ // Pool C hidden-role instructions live in BP2 role-scoped context
244
+ // (loaded by loadScopedRoleInstructions from rules/agent/*.md).
214
245
  //
215
- // User message = pure query. Permission / role ride in BP3
216
- // sessionMarker (composeSystemPrompt) only the query varies per
217
- // call, so provider cache reuses the shared prefix.
246
+ // User message = pure query. Stable role rules ride in BP2; stable
247
+ // memory/meta rides in BP3; only the query varies per call, so
248
+ // provider cache reuses the shared prefix.
218
249
  //
219
250
  // Stateless ephemeral session — created fresh per call, never
220
251
  // pooled or resumed. Cache prefix matching happens at the provider
221
252
  // layer (account-level), not the session level.
222
253
  const finalPrompt = prompt;
223
- const { session } = prepareBridgeSession({
254
+ const { session } = prepareAgentSession({
224
255
  role,
225
256
  presetName,
226
257
  preset,
@@ -242,7 +273,7 @@ export function makeBridgeLlm(opts = {}) {
242
273
  // count-only "tools=N" line.
243
274
  try {
244
275
  const _toolNames = (session.tools || []).map((t) => t?.name).filter(Boolean);
245
- process.stderr.write(`[bridge-llm] role=${role} tool-list (${_toolNames.length}): ${_toolNames.join(',')}\n`);
276
+ process.stderr.write(`[agent-dispatch] role=${role} tool-list (${_toolNames.length}): ${_toolNames.join(',')}\n`);
246
277
  } catch { /* best-effort diagnostic */ }
247
278
 
248
279
  await updateSessionStatus(session.id, 'running');
@@ -265,15 +296,16 @@ export function makeBridgeLlm(opts = {}) {
265
296
  // Watchdog policy is split:
266
297
  // - firstResponseTimeoutMs cuts quickly only when the model produces no
267
298
  // first stream/tool activity at all.
268
- // - idleTimeoutMs is a stale watchdog after work has started.
269
- // 0 disables that stale abort; default is 30 minutes.
270
- const _staleMs = Number.isFinite(callIdleTimeoutMs)
271
- ? callIdleTimeoutMs
272
- : (Number.isFinite(opts.idleTimeoutMs) ? opts.idleTimeoutMs : DEFAULT_STALE_TIMEOUT_MS);
273
- const _firstMs = Number.isFinite(opts.firstResponseTimeoutMs)
274
- ? opts.firstResponseTimeoutMs
275
- : DEFAULT_FIRST_RESPONSE_TIMEOUT_MS;
276
- const _idleController = ((_staleMs > 0 || _firstMs > 0) && _linkSignal) ? new AbortController() : null;
299
+ // - idle/tool-running caps come from role stallCap (hidden roles) or env defaults.
300
+ const _watchdogPolicy = resolveAgentWatchdogPolicy(role, {
301
+ idleTimeoutMs: Number.isFinite(callIdleTimeoutMs)
302
+ ? callIdleTimeoutMs
303
+ : opts.idleTimeoutMs,
304
+ firstResponseTimeoutMs: opts.firstResponseTimeoutMs,
305
+ });
306
+ const _idleController = (agentWatchdogPolicyActive(_watchdogPolicy) && _linkSignal)
307
+ ? new AbortController()
308
+ : null;
277
309
  if (_idleController) {
278
310
  try { _linkSignal(session.id, _idleController.signal); } catch { /* ignore */ }
279
311
  }
@@ -281,35 +313,30 @@ export function makeBridgeLlm(opts = {}) {
281
313
  ? setInterval(() => {
282
314
  const now = Date.now();
283
315
  const snapshot = typeof _getProgressSnapshot === 'function' ? _getProgressSnapshot(session.id) : null;
284
- if (snapshot) {
285
- if (snapshot.waitingForFirstActivity) {
286
- const startedAt = snapshot.modelRequestStartedAt || snapshot.askStartedAt;
287
- if (_firstMs > 0 && startedAt && now - startedAt > _firstMs) {
288
- try { _idleController.abort(new Error(`first response stale (${_firstMs}ms)`)); } catch { /* ignore */ }
289
- }
290
- return;
291
- }
292
- const last = snapshot.lastProgressAt || snapshot.firstActivityAt;
293
- if (_staleMs > 0 && last && now - last > _staleMs) {
294
- try { _idleController.abort(new Error(`bridge task stale (${_staleMs}ms without stream/tool progress)`)); } catch { /* ignore */ }
316
+ const abortErr = snapshot
317
+ ? evaluateAgentWatchdogAbort(snapshot, now, _watchdogPolicy)
318
+ : null;
319
+ if (!abortErr && !snapshot && typeof _getLastProgressAt === 'function') {
320
+ const last = _getLastProgressAt(session.id);
321
+ if (_watchdogPolicy.idleStaleMs > 0 && last && now - last > _watchdogPolicy.idleStaleMs) {
322
+ try { _idleController.abort(new Error(`agent task stale (${_watchdogPolicy.idleStaleMs}ms without progress)`)); } catch { /* ignore */ }
295
323
  }
296
324
  return;
297
325
  }
298
- const last = _getLastProgressAt?.(session.id);
299
- if (_staleMs > 0 && last && now - last > _staleMs) {
300
- try { _idleController.abort(new Error(`bridge task stale (${_staleMs}ms without progress)`)); } catch { /* ignore */ }
326
+ if (abortErr) {
327
+ try { _idleController.abort(abortErr); } catch { /* ignore */ }
301
328
  }
302
329
  }, 1000)
303
330
  : null;
304
331
  if (_idleTimer && typeof _idleTimer.unref === 'function') _idleTimer.unref();
305
332
  let terminalStatus = 'idle';
306
- process.stderr.write(`[bridge-llm] role=${role} preset=${presetName} model=${preset.model} provider=${preset.provider} session=${session.id}\n`);
307
- const _bridgeT0 = Date.now();
333
+ process.stderr.write(`[agent-dispatch] role=${role} preset=${presetName} model=${preset.model} provider=${preset.provider} session=${session.id}\n`);
334
+ const _agentDispatchT0 = Date.now();
308
335
  try {
309
336
  const result = await askSession(session.id, finalPrompt, null, null, cwd);
310
- process.stderr.write(`[bridge-llm] role=${role} session=${session.id} elapsed=${Date.now() - _bridgeT0}ms\n`);
337
+ process.stderr.write(`[agent-dispatch] role=${role} session=${session.id} elapsed=${Date.now() - _agentDispatchT0}ms\n`);
311
338
  const raw = result?.content || '';
312
- // Brief cap. Bridge role answers (explore/recall/search)
339
+ // Brief cap. Agent role answers (explore/recall/search)
313
340
  // occasionally balloon to 8-10k token walls that then ride in the
314
341
  // parent Lead's context for the rest of the turn. A 3000-token
315
342
  // (~12 KB) ceiling trims the long tail while leaving the vast
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Unified agent progress / stale watchdog policy for agent-tool spawns and
3
+ * agent-dispatch internal roles. Activity heartbeats (session manager) refresh
4
+ * lastProgressAt during long tool work; this module decides when to abort.
5
+ */
6
+
7
+ import { getHiddenRole } from '../internal-roles.mjs';
8
+ import {
9
+ resolveAgentStallThresholds,
10
+ resolveAgentToolThresholdSeconds,
11
+ } from '../stall-policy.mjs';
12
+
13
+ function envTimeoutMs(name, fallback) {
14
+ const raw = process.env[name];
15
+ if (raw === undefined || raw === '') return fallback;
16
+ const n = Number(raw);
17
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
18
+ }
19
+
20
+ export const DEFAULT_FIRST_RESPONSE_TIMEOUT_MS = envTimeoutMs(
21
+ 'MIXDOG_AGENT_FIRST_RESPONSE_TIMEOUT_MS',
22
+ 120_000,
23
+ );
24
+ export const DEFAULT_STALE_TIMEOUT_MS = envTimeoutMs(
25
+ 'MIXDOG_AGENT_STALE_TIMEOUT_MS',
26
+ 30 * 60_000,
27
+ );
28
+
29
+ function resolveExplicitMs(value, fallback) {
30
+ if (Number.isFinite(value) && value >= 0) return Math.floor(value);
31
+ return fallback;
32
+ }
33
+
34
+ export function resolveAgentWatchdogPolicy(role, overrides = {}) {
35
+ const firstResponseMs = resolveExplicitMs(
36
+ overrides.firstResponseTimeoutMs,
37
+ DEFAULT_FIRST_RESPONSE_TIMEOUT_MS,
38
+ );
39
+
40
+ let idleStaleMs;
41
+ if (Number.isFinite(overrides.idleTimeoutMs) && overrides.idleTimeoutMs >= 0) {
42
+ idleStaleMs = Math.floor(overrides.idleTimeoutMs);
43
+ } else if (getHiddenRole(role)) {
44
+ const { abort } = resolveAgentStallThresholds(role);
45
+ idleStaleMs = abort * 1000;
46
+ } else {
47
+ idleStaleMs = DEFAULT_STALE_TIMEOUT_MS;
48
+ }
49
+
50
+ const idleSec = idleStaleMs / 1000;
51
+ const toolRunningSec = resolveAgentToolThresholdSeconds(role, idleSec);
52
+ const toolRunningMs = Math.max(0, Math.floor(toolRunningSec * 1000));
53
+
54
+ return {
55
+ firstResponseMs,
56
+ idleStaleMs,
57
+ toolRunningMs,
58
+ };
59
+ }
60
+
61
+ export function evaluateAgentWatchdogAbort(snapshot, now, policy) {
62
+ if (!snapshot || !policy) return null;
63
+
64
+ if (snapshot.waitingForFirstActivity) {
65
+ const startedAt = snapshot.modelRequestStartedAt || snapshot.askStartedAt;
66
+ if (policy.firstResponseMs > 0 && startedAt && now - startedAt > policy.firstResponseMs) {
67
+ return new Error(`agent first response stale (${policy.firstResponseMs}ms)`);
68
+ }
69
+ return null;
70
+ }
71
+
72
+ const last = snapshot.lastProgressAt || snapshot.firstActivityAt;
73
+ if (policy.idleStaleMs > 0 && last && now - last > policy.idleStaleMs) {
74
+ return new Error(`agent task stale (${policy.idleStaleMs}ms without stream/tool progress)`);
75
+ }
76
+
77
+ if (
78
+ snapshot.stage === 'tool_running'
79
+ && snapshot.toolStartedAt
80
+ && policy.toolRunningMs > 0
81
+ && now - snapshot.toolStartedAt > policy.toolRunningMs
82
+ ) {
83
+ return new Error(`agent tool running stale (${policy.toolRunningMs}ms)`);
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ export function agentWatchdogPolicyActive(policy) {
90
+ if (!policy) return false;
91
+ return (policy.firstResponseMs > 0)
92
+ || (policy.idleStaleMs > 0)
93
+ || (policy.toolRunningMs > 0);
94
+ }