mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  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 +205 -33
  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 +1222 -90
  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 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -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
+ }
@@ -1,25 +1,31 @@
1
1
  /**
2
- * Smart Bridge — Cache Strategy
2
+ * Agent Runtime — Cache Strategy
3
3
  *
4
4
  * Provider-level cache policy. Anthropic supports explicit cache_control
5
- * breakpoints (up to 4 per request) — we use all 4 slots. Non-breakpoint
6
- * providers rely on server-side automatic prefix matching or observation.
5
+ * breakpoints (up to 4 per request) — we spend them on the stable system /
6
+ * session prefix plus the reusable message tail. Non-breakpoint providers
7
+ * rely on provider-managed prefix routing or provider-local cache objects.
7
8
  *
8
- * Anthropic 4-BP layout (public/CLI):
9
- * BP_1 tools (1h) — tool schemas, stable per schema profile
10
- * BP_2 system (1h) — Tier 2 shared rules
11
- * BP_3 tier3 (1h) — role/permission meta (messages[1] system-reminder)
12
- * BP_4 messages (1h) last message only; sliding extends prefix loss-free
9
+ * Anthropic 4-BP layout:
10
+ * BP_1 system#1 (1h) — shared tool policy + compact skill manifest
11
+ * BP_2 system#2 (1h) — role/system rules (Lead / agent / hidden role)
12
+ * BP_3 system#3 (1h) — stable memory/meta marker (sessionMarker system block; tier3)
13
+ * BP_4 messages (1h/5m) sliding tool_result / prior user-text tail
13
14
  *
14
- * Tier 3 gets its own BP because role meta is stable per dispatch, so a
15
- * dedicated slot gives a reliable hit across the entire tool loop while
16
- * the sliding messages BP handles volatile tool_result accumulation.
15
+ * Tool schemas still sit before system in the provider prompt prefix. We do
16
+ * not spend a separate cache_control slot on tools; the first system BP covers
17
+ * the preceding tool prefix via Anthropic prefix caching semantics. Keeping
18
+ * agent worker tool schemas byte-stable is therefore still load-bearing.
19
+ *
20
+ * Tier 3 gets its own BP because memory/meta context is stable within the
21
+ * session. The sliding messages BP handles tool_result accumulation and
22
+ * per-call task/event data without pinning volatile text into the 1h tier.
17
23
  *
18
24
  * Non-breakpoint providers:
19
25
  * - OpenAI (public): prompt_cache_key + prompt_cache_retention=24h
20
- * - OpenAI OAuth (Codex): prompt_cache_key only (server in-memory 5-10min)
21
- * - Gemini: implicit caching does NOT report cachedContentTokenCount on
22
- * 3.5+. Explicit cachedContents API required for measurable cache hits.
26
+ * - OpenAI OAuth: prompt_cache_key only (server in-memory 5-10min)
27
+ * - Gemini: provider-managed explicit cachedContents with 1h TTL, plus
28
+ * implicit caching as a fallback when the prefix is below cache minimums.
23
29
  * - xAI: x-grok-conv-id (server routing pin) + prompt_cache_key on
24
30
  * Responses API. Treat as key-prefix, not implicit.
25
31
  * - DeepSeek / OpenCode Go: automatic KV/prefix cache; observe provider
@@ -34,7 +40,7 @@ import { getHiddenRole } from '../internal-roles.mjs';
34
40
  /**
35
41
  * One-shot, LLM-only maintenance hidden roles (cycle1/cycle2/cycle3-agent):
36
42
  * a fresh stateless session is created per call, asked exactly once, and
37
- * closed (bridge-llm.mjs) — the per-batch user prompt can NEVER be reused.
43
+ * closed (agent-dispatch.mjs) — the per-batch user prompt can NEVER be reused.
38
44
  * Writing a message-tail cache breakpoint on it just pays the 1.25x write
39
45
  * premium for content read back 0 times. Identified by the declarative
40
46
  * (kind:'maintenance' + toolSchemaProfile:'llm-only') pair rather than
@@ -60,7 +66,7 @@ function isOneShotMaintenanceRole(role) {
60
66
  * '5m' → ephemeral 5m TTL (1.25x write premium, 0.1x read)
61
67
  * 'none' → no breakpoint written on this layer
62
68
  *
63
- * Public bridge agents stay resumable for up to 1h (the terminal-reap window)
69
+ * Public agents stay resumable for up to 1h (the terminal-reap window)
64
70
  * for same-task reuse, so their message tail uses 1h too. Hidden multi-turn
65
71
  * roles (explorer / scheduler / webhook) run a single fan-out or entry-driven
66
72
  * session that is not resumed for same-task reuse, so their volatile tail stays
@@ -80,10 +86,10 @@ export function resolveCacheStrategy(role) {
80
86
  return { tools: 'none', system: 'none', tier3: 'none', messages: 'none' };
81
87
  }
82
88
  if (getHiddenRole(role)) {
83
- return { tools: '1h', system: '1h', tier3: '1h', messages: '5m' };
89
+ return { tools: 'none', system: '1h', tier3: '1h', messages: '5m' };
84
90
  }
85
- // Public bridge agents: resumable up to 1h for same-task reuse 1h tail.
86
- return { tools: '1h', system: '1h', tier3: '1h', messages: '1h' };
91
+ // Public agents: resumable up to 1h for same-task reuse -> 1h tail.
92
+ return { tools: 'none', system: '1h', tier3: '1h', messages: '1h' };
87
93
  }
88
94
 
89
95
  /**
@@ -98,6 +104,7 @@ export function resolveCacheStrategy(role) {
98
104
  // Provider cache capability kinds:
99
105
  // 'explicit-breakpoint' — explicit provider-side cache_control writes
100
106
  // 'key-prefix' — provider-managed shard keyed by cache key/session
107
+ // 'managed-explicit' — provider object creates/attaches explicit caches
101
108
  // 'implicit-observed' — cache hits are observable but not guaranteed warm
102
109
  // 'none' — no API-level cache knob/metric
103
110
  const PROVIDER_CACHE_CAPABILITY = Object.freeze({
@@ -107,7 +114,7 @@ const PROVIDER_CACHE_CAPABILITY = Object.freeze({
107
114
  'openai-oauth': 'key-prefix',
108
115
  'xai': 'key-prefix',
109
116
  'grok-oauth': 'key-prefix',
110
- 'gemini': 'implicit-observed',
117
+ 'gemini': 'managed-explicit',
111
118
  'deepseek': 'implicit-observed',
112
119
  'opencode-go': 'implicit-observed',
113
120
  });
@@ -118,7 +125,9 @@ export function cacheCapabilityForProvider(provider) {
118
125
 
119
126
  export function shouldMarkWarmForProvider(provider) {
120
127
  const capability = cacheCapabilityForProvider(provider);
121
- return capability === 'explicit-breakpoint' || capability === 'key-prefix';
128
+ return capability === 'explicit-breakpoint'
129
+ || capability === 'key-prefix'
130
+ || capability === 'managed-explicit';
122
131
  }
123
132
 
124
133
  export function shouldRecordObservedForProvider(provider) {
@@ -183,7 +192,7 @@ function cleanString(value) {
183
192
 
184
193
  function normalizePromptCacheNamespace(value) {
185
194
  const s = String(value || '').trim() || 'mixdog-shared';
186
- // Keep the key boring for OpenAI/Codex's 64-char prompt_cache_key cap while
195
+ // Keep the key boring for OpenAI OAuth's 64-char prompt_cache_key cap while
187
196
  // preserving user overrides as much as possible.
188
197
  return s.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'mixdog-shared';
189
198
  }
@@ -358,7 +367,7 @@ export function buildProviderCacheOpts(provider, sessionId, role) {
358
367
  }
359
368
  if (provider === 'openai') {
360
369
  // Public OpenAI API: prompt_cache_retention extends prefix retention.
361
- // openai-oauth (Codex) rejects the header — falls through to default.
370
+ // openai-oauth rejects the header — falls through to default.
362
371
  return { cacheRetention: '24h' };
363
372
  }
364
373
  return {};