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,15 +1,44 @@
1
+ export const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch hunk+ end_patch
2
+ begin_patch: "*** Begin Patch" LF
3
+ end_patch: "*** End Patch" LF?
4
+
5
+ hunk: add_hunk | delete_hunk | update_hunk
6
+ add_hunk: "*** Add File: " filename LF add_line+
7
+ delete_hunk: "*** Delete File: " filename LF
8
+ update_hunk: "*** Update File: " filename LF change_move? change?
9
+
10
+ filename: /(.+)/
11
+ add_line: "+" /(.*)/ LF -> line
12
+
13
+ change_move: "*** Move to: " filename LF
14
+ change: (change_context | change_line)+ eof_line?
15
+ change_context: ("@@" | "@@ " /(.+)/) LF
16
+ change_line: ("+" | "-" | " ") /(.*)/ LF
17
+ eof_line: "*** End of File" LF
18
+
19
+ %import common.LF
20
+ `;
21
+
22
+ export const APPLY_PATCH_FREEFORM_DESCRIPTION = 'Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.';
23
+
1
24
  export const PATCH_TOOL_DEFS = [
2
25
  {
3
26
  name: 'apply_patch',
4
27
  title: 'Mixdog Apply Patch',
5
28
  annotations: { title: 'Mixdog Apply Patch', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, compressible: false, compressibleLossless: true },
6
- description: 'Apply file patches. Prefer V4A with one file block per target file and exact current context around each edit.',
29
+ description: 'Apply file patches. Prefer V4A envelope with exact current context.',
30
+ freeformDescription: APPLY_PATCH_FREEFORM_DESCRIPTION,
31
+ freeform: {
32
+ type: 'grammar',
33
+ syntax: 'lark',
34
+ definition: APPLY_PATCH_LARK_GRAMMAR,
35
+ },
7
36
  inputSchema: {
8
37
  type: 'object',
9
38
  properties: {
10
- patch: { type: 'string', description: 'Patch text only. V4A preferred: `*** Begin Patch` envelope and `*** Update/Add/Delete File: <path>` blocks. Use one block per target file, combining all hunks for that file. Copy exact current context around edits.' },
11
- format: { type: 'string', enum: ['unified', 'v4a'], description: 'Auto-detected. Prefer v4a; unified needs counted @@ headers.' },
12
- base_path: { type: 'string', description: 'Repo root so a/... paths resolve.' },
39
+ patch: { type: 'string', description: 'Patch text. V4A preferred: `*** Begin Patch`; one file block per target file; exact current context.' },
40
+ format: { type: 'string', enum: ['unified', 'v4a'], description: 'Auto-detected.' },
41
+ base_path: { type: 'string', description: 'Repo root.' },
13
42
  dry_run: { type: 'boolean', description: 'Default false. true = validate only, no write.' },
14
43
  },
15
44
  required: ['patch'],
@@ -47,7 +47,7 @@ import {
47
47
  withBuiltinPathLocks,
48
48
  } from './builtin.mjs';
49
49
  import { withAdvisoryLocks } from './builtin/advisory-lock.mjs';
50
- import { markCodeGraphDirtyPaths } from './code-graph.mjs';
50
+ import { markCodeGraphDirtyPaths } from './code-graph-state.mjs';
51
51
  import { wrapMutationRouteOutput } from './mutation-planner.mjs';
52
52
  import { getPluginData } from '../config.mjs';
53
53
  import { ensurePatchBinary, findCachedPatchBinary } from './patch-binary-fetcher.mjs';
@@ -527,6 +527,56 @@ function classifyEntry(entry) {
527
527
  return 'modify';
528
528
  }
529
529
 
530
+ function parsedEntryTargetKey(entry, basePath) {
531
+ if (classifyEntry(entry) !== 'modify') return '';
532
+ const headerName = entry.oldFileName || entry.newFileName;
533
+ if (!headerName || DEV_NULL.test(headerName)) return '';
534
+ const fullPath = resolveEntryPath(basePath, headerName);
535
+ return process.platform === 'win32' ? fullPath.toLowerCase() : fullPath;
536
+ }
537
+
538
+ function mergeDuplicateParsedModifyEntries(parsed, basePath) {
539
+ const out = [];
540
+ const byTarget = new Map();
541
+ let changed = false;
542
+ for (const entry of parsed || []) {
543
+ const key = parsedEntryTargetKey(entry, basePath);
544
+ if (!key) {
545
+ out.push(entry);
546
+ continue;
547
+ }
548
+ const existing = byTarget.get(key);
549
+ if (!existing) {
550
+ byTarget.set(key, entry);
551
+ out.push(entry);
552
+ continue;
553
+ }
554
+ existing.hunks.push(...(entry.hunks || []));
555
+ changed = true;
556
+ }
557
+ return { parsed: out, changed };
558
+ }
559
+
560
+ function unifiedRange(start, lines) {
561
+ const s = Math.max(0, Number(start) || 0);
562
+ const n = Math.max(0, Number(lines) || 0);
563
+ return `${s},${n}`;
564
+ }
565
+
566
+ function renderParsedUnifiedPatch(parsed) {
567
+ const out = [];
568
+ for (const entry of parsed || []) {
569
+ out.push(`--- ${entry.oldFileName || '/dev/null'}`);
570
+ out.push(`+++ ${entry.newFileName || '/dev/null'}`);
571
+ for (const hunk of entry.hunks || []) {
572
+ const section = hunk.section ? ` ${hunk.section}` : '';
573
+ out.push(`@@ -${unifiedRange(hunk.oldStart, hunk.oldLines)} +${unifiedRange(hunk.newStart, hunk.newLines)} @@${section}`);
574
+ for (const line of hunk.lines || []) out.push(line);
575
+ }
576
+ }
577
+ return `${out.join('\n')}\n`;
578
+ }
579
+
530
580
  function isPatchErrorText(text) {
531
581
  return /^Error:/i.test(String(text ?? '').trimStart());
532
582
  }
@@ -637,7 +687,27 @@ async function preValidateNativeBatch(parsed, basePath) {
637
687
  const kind = classifyEntry(entry);
638
688
  const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
639
689
  if (!nativeHeaderSupported(entry, basePath)) {
640
- const display = headerName ? normalizeOutputPath(stripDiffPrefix(headerName)) : '(unknown)';
690
+ // Two distinct failure modes share nativeHeaderSupported()===false:
691
+ // (a) no usable target path could be parsed from the section header
692
+ // (oldFileName/newFileName empty or /dev/null) — a SHAPE problem,
693
+ // (b) a parsed path that escapes base_path (`..` segment / out-of-base
694
+ // absolute) — a SECURITY refusal.
695
+ // Collapsing both into the "path escapes base_path or contains `..`"
696
+ // message mislabels (a) as (b): the model sees a nonsensical reason for a
697
+ // patch that uses no `..`, cannot self-correct, and re-submits the same
698
+ // patch until the iteration cap. Emit a shape-specific, actionable hint
699
+ // for (a) so the loop breaks on the first failure.
700
+ const hasUsableHeader = headerName && !DEV_NULL.test(headerName);
701
+ if (!hasUsableHeader) {
702
+ throw new Error(
703
+ 'apply_patch: a file section header could not be parsed (no target path). '
704
+ + 'Each section must start with a valid header: `*** Update File: <path>` / '
705
+ + '`*** Add File: <path>` / `*** Delete File: <path>` (V4A), or a '
706
+ + '`--- a/<path>` + `+++ b/<path>` pair (unified). Wrap multi-hunk V4A '
707
+ + 'edits in a `*** Begin Patch` / `*** End Patch` envelope and pass format:"v4a".',
708
+ );
709
+ }
710
+ const display = normalizeOutputPath(stripDiffPrefix(headerName));
641
711
  throw new Error(`apply_patch: header ${display} is unsupported (path escapes base_path or contains \`..\`).`);
642
712
  }
643
713
  if (kind !== 'delete' && !(entry.hunks?.length > 0)) {
@@ -1240,8 +1310,15 @@ async function dispatchNativePatch({ entries, basePath, nativePatchStr, fuzz, re
1240
1310
  : `${verbLabel} ${countLabel(writtenEntries.length, 'File')} (Native)${dryRun ? ' Dry Run' : ''}`;
1241
1311
  const lines = [summary];
1242
1312
  for (const entry of writtenEntries) {
1243
- const detail = `+${countLabel(entry.added || 0, 'Line')} · -${countLabel(entry.removed || 0, 'Line')}`;
1244
- lines.push(` OK ${kindLabel(entry.kind)} ${entry.displayPath} ${detail}`);
1313
+ const added = entry.added || 0;
1314
+ const removed = entry.removed || 0;
1315
+ const parts = [];
1316
+ if (added > 0) parts.push(`+${countLabel(added, 'Line')}`);
1317
+ if (removed > 0) parts.push(`-${countLabel(removed, 'Line')}`);
1318
+ const detail = parts.join(' · ');
1319
+ lines.push(detail
1320
+ ? ` OK ${kindLabel(entry.kind)} ${entry.displayPath} — ${detail}`
1321
+ : ` OK ${kindLabel(entry.kind)} ${entry.displayPath}`);
1245
1322
  }
1246
1323
  for (const f of stats.failures || []) {
1247
1324
  lines.push(` SKIP ${f.path || '(unknown)'} — ${f.reason}${formatNativeFailureContext(parsed, basePath, f.path, { fuzz })}`);
@@ -1711,7 +1788,7 @@ function longestCommonSubstringLen(a, b, cap = 4000) {
1711
1788
  }
1712
1789
 
1713
1790
  // Normalize common typographic code-points to their ASCII equivalents, then
1714
- // trim. Mirrors the Rust mixdog-patch normalize_typographic() and Codex
1791
+ // trim. Mirrors the Rust mixdog-patch normalize_typographic() and V4A
1715
1792
  // apply_patch's normalise() so V4A->unified anchor resolution stays consistent
1716
1793
  // across engines: an ASCII-authored patch can still anchor on source that
1717
1794
  // carries curly quotes, em/en dashes, NBSP and other exotic spaces.
@@ -2643,7 +2720,7 @@ async function apply_patch(args, cwd, options = {}) {
2643
2720
  parsed = parsePatch(normalizedPatchStr);
2644
2721
  } catch (err) {
2645
2722
  if (!canFallbackCountedUnified(patchStr, requestedFormat, err)) {
2646
- throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; prefer Codex/V4A envelope for multi-hunk edits (no @@ line counts)`);
2723
+ throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; prefer V4A envelope for multi-hunk edits (no @@ line counts)`);
2647
2724
  }
2648
2725
  try {
2649
2726
  inputPatchStr = await convertUnifiedCountedToUnifiedPatchViaV4A(patchStr, basePath, v4aConvertOpts);
@@ -2661,6 +2738,13 @@ async function apply_patch(args, cwd, options = {}) {
2661
2738
  if (!v4aRenameOnly && (!Array.isArray(parsed) || parsed.length === 0)) {
2662
2739
  return 'Error: patch contained no file sections';
2663
2740
  }
2741
+ if (!v4aRenameOnly) {
2742
+ const merged = mergeDuplicateParsedModifyEntries(parsed, basePath);
2743
+ if (merged.changed) {
2744
+ parsed = merged.parsed;
2745
+ normalizedPatchStr = renderParsedUnifiedPatch(parsed);
2746
+ }
2747
+ }
2664
2748
 
2665
2749
  // Pre-validate paths / duplicates / symlink escapes — throws on any
2666
2750
  // unsupported entry. Throws bubble out to the tool dispatcher as clean
@@ -1,11 +1,11 @@
1
- // Per-tool live-status message builder (ported from claude-code's progress
1
+ // Per-tool live-status message builder (ported from a reference agent's progress
2
2
  // generalization / getActivityDescription). Dependency-light and
3
3
  // side-effect-free: given a tool name + its raw args, return a short
4
4
  // human-readable "what's happening now" string. Used by the central dispatch
5
5
  // path to emit a single start-of-tool progress notification. Every tool gets a
6
6
  // meaningful verb; `running ${name}` is only the last-resort fallback.
7
7
  //
8
- // Verbs are kept symmetric with claude-code's wording: Read→"reading",
8
+ // Verbs are kept symmetric with common agent wording: Read→"reading",
9
9
  // Edit→"editing", Write→"writing", Grep→"searching for", Glob→"finding",
10
10
  // web search→"searching web for", web fetch→"fetching".
11
11
 
@@ -45,6 +45,8 @@ export function formatToolStartProgress(name, args = {}) {
45
45
  return Array.isArray(a.pattern) ? `searching for ${_plural(a.pattern.length, 'pattern')}` : `searching for ${_t(a.pattern)}`;
46
46
  case 'glob':
47
47
  return Array.isArray(a.pattern) ? `finding ${_plural(a.pattern.length, 'glob')}` : `finding ${_t(a.pattern)}`;
48
+ case 'find':
49
+ return `finding ${_t(a.query || 'files')}`;
48
50
  case 'list':
49
51
  return a.mode === 'find' ? 'finding files' : `listing ${_t(a.path || 'cwd')}`;
50
52
  case 'code_graph':
@@ -57,12 +59,12 @@ export function formatToolStartProgress(name, args = {}) {
57
59
  case 'web_fetch':
58
60
  return Array.isArray(a.url) ? `fetching ${_plural(a.url.length, 'URL')}` : `fetching ${_t(a.url)}`;
59
61
 
60
- // ── agent module: explore / bridge / models ──────────────────────
62
+ // ── agent module: explore / agent / models ───────────────────────
61
63
  case 'explore': {
62
64
  const n = Array.isArray(a.query) ? a.query.length : (a.query ? 1 : 0);
63
65
  return `exploring ${_plural(n, 'query', 'queries')}`;
64
66
  }
65
- case 'bridge': {
67
+ case 'agent': {
66
68
  const route = [a.preset, [a.provider, a.model].filter(Boolean).join('/')].filter(Boolean).join(' ');
67
69
  const suffix = route ? ` (${_t(route, 32)})` : '';
68
70
  if (a.role) return `dispatching ${_t(a.role)}${suffix}`;
@@ -33,7 +33,7 @@
33
33
  */
34
34
 
35
35
  import * as nodeUtil from 'node:util';
36
- import { traceBridgeCompress, traceBridgeBatch } from '../bridge-trace.mjs';
36
+ import { traceAgentCompress, traceAgentBatch } from '../agent-trace.mjs';
37
37
  import { BUILTIN_TOOLS } from './builtin.mjs';
38
38
  import { PATCH_TOOL_DEFS } from './patch-tool-defs.mjs';
39
39
  import { CODE_GRAPH_TOOL_DEFS } from './code-graph-tool-defs.mjs';
@@ -137,7 +137,7 @@ function normalizeTrailingNewlines(text) {
137
137
  return text.replace(/\n+$/, '\n');
138
138
  }
139
139
 
140
- // Retained as named exports for non-tool-result consumers (bridge
140
+ // Retained as named exports for non-tool-result consumers (agent
141
141
  // aggregated worker bodies in ai-wrapped-dispatch). These functions are
142
142
  // NO LONGER part of the tool-result compression chain — that chain is
143
143
  // lossless-only.
@@ -192,7 +192,7 @@ export function compressToolResult(toolName, args, result, ctx) {
192
192
  out = normalizeTrailingNewlines(out);
193
193
  if (out.length >= before) return result;
194
194
  if (ctx?.sessionId) {
195
- try { traceBridgeCompress({ sessionId: ctx.sessionId, toolName, before, after: out.length }); } catch { /* trace best-effort */ }
195
+ try { traceAgentCompress({ sessionId: ctx.sessionId, toolName, before, after: out.length }); } catch { /* trace best-effort */ }
196
196
  }
197
197
  return out;
198
198
  }
@@ -205,7 +205,7 @@ export function compressToolResult(toolName, args, result, ctx) {
205
205
  export function recordToolBatch(sessionId, toolCallCount) {
206
206
  const n = Number(toolCallCount);
207
207
  if (!sessionId || !Number.isFinite(n) || n <= 0) return;
208
- try { traceBridgeBatch({ sessionId, toolCallCount: n }); } catch { /* trace best-effort */ }
208
+ try { traceAgentBatch({ sessionId, toolCallCount: n }); } catch { /* trace best-effort */ }
209
209
  }
210
210
 
211
211
  export const _internals = {
@@ -53,7 +53,7 @@ const SHELL_OUTPUT_DISK_CAP = 100 * 1024 * 1024;
53
53
 
54
54
  // Background-task disk watchdog cadence. The size guard polls the spilled
55
55
  // stdout/stderr files every interval and SIGKILLs the child once the
56
- // combined size exceeds SHELL_OUTPUT_DISK_CAP. 5 s matches Claude Code's
56
+ // combined size exceeds SHELL_OUTPUT_DISK_CAP. 5 s matches reference agent
57
57
  // upstream cadence — short enough that a runaway loop is caught within a
58
58
  // few seconds, long enough that the stat overhead is negligible.
59
59
  const SIZE_WATCHDOG_INTERVAL_MS = 1_000;
@@ -585,6 +585,13 @@ export function execShellCommand({
585
585
  cwd,
586
586
  windowsHide: true,
587
587
  stdio: ['ignore', 'pipe', 'pipe'],
588
+ // NOTE (child-spawn-gate): intentionally NOT routed through
589
+ // src/runtime/shared/child-spawn-gate.mjs. bash/pwsh commands can run
590
+ // for minutes (or auto-background), so holding a finite gate slot for
591
+ // the whole lifetime would let a few long shells starve rg/code_graph —
592
+ // the opposite of the gate's intent. TODO: if shell saturation becomes
593
+ // a problem, gate only the brief spawn burst (release on first output /
594
+ // adoption), not the full run.
588
595
  // POSIX: detached gives the child its own process group so treeKill can
589
596
  // signal the whole group. The child is still CLI-owned because we do
590
597
  // not unref it after adoption. Windows detached has different console
@@ -8,7 +8,7 @@
8
8
  // pyenv / mise / asdf / direnv setup the user gets in their interactive
9
9
  // terminal — without paying a fresh login-shell startup on every call.
10
10
  //
11
- // Mirrors Claude Code upstream (src/utils/bash/ShellSnapshot.ts:413,
11
+ // Mirrors upstream shell snapshot pattern (reference ShellSnapshot.ts:413,
12
12
  // createAndSaveSnapshot). Simpler scope: bash and zsh only, no embedded
13
13
  // search-tool injection (mixdog ships its own grep/glob helpers).
14
14
 
@@ -34,7 +34,7 @@ const _cache = new Map();
34
34
  // command. Cleared on process exit (process-scoped Set).
35
35
  const _failedShells = new Set();
36
36
 
37
- // Mirrors Claude Code's cleanupRegistry pattern (refs/claude-code/utils/
37
+ // Mirrors reference cleanupRegistry pattern (utils/
38
38
  // cleanupRegistry.ts + ShellSnapshot.ts:534-545). Snapshot files are
39
39
  // session-scoped and must be unlinked on graceful shutdown — otherwise
40
40
  // they pile up forever (each rc-file mtime change creates a new file).
@@ -155,7 +155,7 @@ function _runSnapshot(shellPath, snapshotPath, configFileExists) {
155
155
  return new Promise((resolve) => {
156
156
  const script = getSnapshotScript(shellPath, snapshotPath, configFileExists);
157
157
  let stderrBuf = '';
158
- // Mirror Claude Code (refs/claude-code/utils/bash/ShellSnapshot.ts:458):
158
+ // Mirror reference implementation (bash/ShellSnapshot.ts:458):
159
159
  // `-c -l` (login non-interactive). Earlier `-ic` (interactive command)
160
160
  // forced bash-completion to load — Git's completion loader spawns `find`
161
161
  // in subshells (__git_find_on_cmdline etc.) which detach on Windows when
@@ -163,7 +163,7 @@ function _runSnapshot(shellPath, snapshotPath, configFileExists) {
163
163
  // mode runs .bash_profile / .profile (which typically sources .bashrc)
164
164
  // without triggering completion init. The script also explicitly sources
165
165
  // the rc file, so the interactive-guard `[[ $- == *i* ]] && return` is
166
- // accepted as a known tradeoff (matches Claude Code's choice).
166
+ // accepted as a known tradeoff (matches the reference choice).
167
167
  const child = spawn(shellPath, ['-c', '-l', script], {
168
168
  // P3 fix: blank prompts so an interactive sourcing in -ic does not
169
169
  // print PS1 / PS2 / RPROMPT / PROMPT noise to stderr (which our
@@ -170,6 +170,8 @@ if (process.env.MIXDOG_CHANNELS_NO_CONNECT) {
170
170
  }
171
171
  const _isWorkerMode = process.env.MIXDOG_WORKER_MODE === '1'
172
172
  const _isCliOwnedMode = process.env.MIXDOG_CLI_OWNED === '1'
173
+ const _isChannelDaemonMode = process.env.MIXDOG_CHANNEL_DAEMON === '1'
174
+ const CHANNEL_DAEMON_IDLE_TTL_MS = Math.max(0, Number(process.env.MIXDOG_CHANNEL_IDLE_TTL_MS) || 60_000)
173
175
  const _bootLogEarly = path.join(
174
176
  DATA_DIR || path.join(os.tmpdir(), "mixdog"),
175
177
  "boot.log"
@@ -225,7 +227,7 @@ try {
225
227
  // GC stale ephemeral session files. closeSession plants a closed=true
226
228
  // tombstone, but bench / smoke / probe drivers historically created sessions
227
229
  // without ever calling closeSession, leaving 175-byte placeholders behind.
228
- // 7-day TTL is safe because live bridge sessions touch their JSON file on
230
+ // 7-day TTL is safe because live agent sessions touch their JSON file on
229
231
  // every ask iteration, so any file older than 7 days is provably abandoned.
230
232
  const _SESSIONS_DIR = path.join(DATA_DIR, 'sessions');
231
233
  const _STALE_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -327,39 +329,61 @@ const INSTRUCTIONS = "";
327
329
  // never `connect()`ed to any transport, so `.notification()` silently
328
330
  // threw 'Not connected' inside the SDK and every call was dropped by an
329
331
  // outer `.catch(() => {})`. That regression is what this path replaces.
330
- function sendNotifyToParent(method, params) {
331
- if (!process.send) {
332
- try { process.stderr.write(`mixdog channels: notify dropped (no IPC): ${method}\n`); } catch {}
333
- return;
334
- }
335
- // CC channel schema requires meta: Record<string,string> (channelNotification.ts).
336
- // Coerce every meta value to string so a non-string (e.g. a Discord
337
- // interaction.type number) can't fail zod and silently drop the notify.
338
- // silent_to_agent stays boolean — an internal routing flag the daemon
339
- // router / agentNotify consume (=== true) before the CC zod boundary.
340
- let outParams = params;
332
+ let _channelNotifyBusSeq = 0;
333
+ const CHANNEL_NOTIFY_BUS_FILE = path.join(RUNTIME_ROOT, 'channel-notifications.jsonl');
334
+ const CHANNEL_NOTIFY_BUS_MAX_BYTES = 5 * 1024 * 1024;
335
+
336
+ function normalizeChannelNotifyParams(method, params) {
341
337
  if (method === 'notifications/claude/channel' && params && params.meta) {
342
338
  const m = {};
343
339
  for (const [k, v] of Object.entries(params.meta)) {
344
340
  if (v === undefined || v === null) continue;
345
341
  m[k] = k === 'silent_to_agent' ? (v === true || v === 'true') : String(v);
346
342
  }
347
- outParams = { ...params, meta: m };
343
+ return { ...params, meta: m };
348
344
  }
345
+ return params;
346
+ }
347
+
348
+ function publishChannelNotify(method, params) {
349
349
  try {
350
- process.send({ type: 'notify', method, params: outParams });
350
+ fs.mkdirSync(RUNTIME_ROOT, { recursive: true });
351
+ try {
352
+ const st = fs.statSync(CHANNEL_NOTIFY_BUS_FILE);
353
+ if (st.size > CHANNEL_NOTIFY_BUS_MAX_BYTES) {
354
+ try { fs.unlinkSync(CHANNEL_NOTIFY_BUS_FILE + '.1'); } catch {}
355
+ try { fs.renameSync(CHANNEL_NOTIFY_BUS_FILE, CHANNEL_NOTIFY_BUS_FILE + '.1'); } catch {}
356
+ }
357
+ } catch {}
358
+ const item = {
359
+ id: `${Date.now()}-${process.pid}-${++_channelNotifyBusSeq}`,
360
+ ts: Date.now(),
361
+ pid: process.pid,
362
+ method,
363
+ params,
364
+ };
365
+ fs.appendFileSync(CHANNEL_NOTIFY_BUS_FILE, JSON.stringify(item) + '\n');
351
366
  } catch (err) {
352
- try { process.stderr.write(`mixdog channels: notify IPC send failed: ${err && err.message || err}\n`); } catch {}
367
+ try { process.stderr.write(`mixdog channels: notify bus write failed: ${err && err.message || err}\n`); } catch {}
353
368
  }
354
369
  }
355
370
 
356
- const recapState = { state: 'idle', running: false, startedAt: null, lastCompletedAt: null, updatedAt: null, errorMessage: null };
357
- function sendRecapStateToParent() {
358
- if (!process.send) return;
371
+ function sendNotifyToParent(method, params) {
372
+ // CC channel schema requires meta: Record<string,string> (channelNotification.ts).
373
+ // Coerce every meta value to string so a non-string (e.g. a Discord
374
+ // interaction.type number) can't fail zod and silently drop the notify.
375
+ // silent_to_agent stays boolean — an internal routing flag the daemon
376
+ // router / agentNotify consume (=== true) before the CC zod boundary.
377
+ const outParams = normalizeChannelNotifyParams(method, params);
378
+ publishChannelNotify(method, outParams);
379
+ if (!process.send) {
380
+ try { process.stderr.write(`mixdog channels: notify queued on bus (no IPC): ${method}\n`); } catch {}
381
+ return;
382
+ }
359
383
  try {
360
- process.send({ type: 'recap_status', recap: { ...recapState } });
384
+ process.send({ type: 'notify', method, params: outParams });
361
385
  } catch (err) {
362
- try { process.stderr.write(`mixdog channels: recap status IPC send failed: ${err && err.message || err}\n`); } catch {}
386
+ try { process.stderr.write(`mixdog channels: notify IPC send failed: ${err && err.message || err}\n`); } catch {}
363
387
  }
364
388
  }
365
389
 
@@ -612,6 +636,98 @@ const ACTIVE_OWNER_STALE_MS = 1e4;
612
636
  // never blocks process exit. Single JSON atomic write, no measurable load.
613
637
  const OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
614
638
  let ownerHeartbeatTimer = null;
639
+ let channelDaemonIdleTimer = null;
640
+ let channelDaemonLastClientAt = Date.now();
641
+ let channelDaemonBackgroundLogAt = 0;
642
+ const CHANNEL_CLIENT_DIR = path.join(RUNTIME_ROOT, 'channel-clients');
643
+
644
+ function pidAlive(pid) {
645
+ const n = Number(pid);
646
+ if (!Number.isInteger(n) || n <= 0) return false;
647
+ try {
648
+ process.kill(n, 0);
649
+ return true;
650
+ } catch (e) {
651
+ return e?.code === 'EPERM';
652
+ }
653
+ }
654
+
655
+ function countLiveChannelClients() {
656
+ let live = 0;
657
+ const now = Date.now();
658
+ try {
659
+ fs.mkdirSync(CHANNEL_CLIENT_DIR, { recursive: true });
660
+ for (const file of fs.readdirSync(CHANNEL_CLIENT_DIR)) {
661
+ if (!file.endsWith('.json')) continue;
662
+ const full = path.join(CHANNEL_CLIENT_DIR, file);
663
+ let item = null;
664
+ let st = null;
665
+ try {
666
+ st = fs.statSync(full);
667
+ item = JSON.parse(fs.readFileSync(full, 'utf8'));
668
+ } catch {
669
+ try { fs.unlinkSync(full); } catch {}
670
+ continue;
671
+ }
672
+ const pid = Number(item?.pid ?? file.replace(/\.json$/, ''));
673
+ const fresh = now - Math.max(Number(item?.updatedAt) || 0, st.mtimeMs) < 20_000;
674
+ if (pidAlive(pid) && fresh) {
675
+ live += 1;
676
+ } else {
677
+ try { fs.unlinkSync(full); } catch {}
678
+ }
679
+ }
680
+ } catch {}
681
+ return live;
682
+ }
683
+
684
+ function hasChannelBackgroundWork() {
685
+ if (!channelBridgeActive || !bridgeRuntimeConnected) return false;
686
+ if (config.webhook?.enabled === true) return true;
687
+ if (Array.isArray(config.events?.rules) && config.events.rules.length > 0) return true;
688
+ if (Array.isArray(config.nonInteractive) && config.nonInteractive.length > 0) return true;
689
+ if (Array.isArray(config.interactive) && config.interactive.length > 0) return true;
690
+ return false;
691
+ }
692
+
693
+ function checkChannelDaemonIdle() {
694
+ if (!_isChannelDaemonMode || CHANNEL_DAEMON_IDLE_TTL_MS <= 0) return;
695
+ const liveClients = countLiveChannelClients();
696
+ if (liveClients > 0) {
697
+ channelDaemonLastClientAt = Date.now();
698
+ return;
699
+ }
700
+ if (hasChannelBackgroundWork()) {
701
+ const now = Date.now();
702
+ if (now - channelDaemonBackgroundLogAt > 60_000) {
703
+ channelDaemonBackgroundLogAt = now;
704
+ try { process.stderr.write('[channels-worker] daemon idle: keeping alive for configured background work\n'); } catch {}
705
+ }
706
+ channelDaemonLastClientAt = Date.now();
707
+ return;
708
+ }
709
+ if (Date.now() - channelDaemonLastClientAt < CHANNEL_DAEMON_IDLE_TTL_MS) return;
710
+ try { process.stderr.write(`[channels-worker] daemon idle TTL elapsed (${CHANNEL_DAEMON_IDLE_TTL_MS}ms) — shutting down\n`); } catch {}
711
+ stop()
712
+ .then(() => process.exit(0))
713
+ .catch((e) => {
714
+ try { process.stderr.write(`[channels-worker] daemon idle shutdown failed: ${e?.message || e}\n`); } catch {}
715
+ process.exit(1);
716
+ });
717
+ }
718
+
719
+ function startChannelDaemonIdleMonitor() {
720
+ if (!_isChannelDaemonMode || CHANNEL_DAEMON_IDLE_TTL_MS <= 0 || channelDaemonIdleTimer) return;
721
+ channelDaemonLastClientAt = Date.now();
722
+ channelDaemonIdleTimer = setInterval(checkChannelDaemonIdle, 5000);
723
+ channelDaemonIdleTimer.unref?.();
724
+ }
725
+
726
+ function stopChannelDaemonIdleMonitor() {
727
+ if (!channelDaemonIdleTimer) return;
728
+ clearInterval(channelDaemonIdleTimer);
729
+ channelDaemonIdleTimer = null;
730
+ }
615
731
  // Owner gating here is multi-process runtime coordination: only the active
616
732
  // bindingReady gates all send paths until the boot-time refreshBridgeOwnership
617
733
  // ({ restoreBinding: true }) call completes. Without this, scheduler/webhook
@@ -870,7 +986,7 @@ const OWNER_ROUTES = {
870
986
  },
871
987
  "/trigger-schedule": async (req, res, body) => {
872
988
  // Native fallback for `mcp__trigger_schedule` so out-of-band
873
- // verification works when the MCP stdio bridge is down (Claude Code
989
+ // verification works when the MCP stdio bridge is down (host agent
874
990
  // disconnected, supervisor restart pending, etc.). Same authz as
875
991
  // /inject — x-owner-token must equal INSTANCE_ID.
876
992
  if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
@@ -1015,26 +1131,6 @@ const OWNER_ROUTES = {
1015
1131
  res.end(JSON.stringify({ error: "Method not allowed" }));
1016
1132
  }
1017
1133
  },
1018
- "/recap/reset": async (req, res /*, body*/) => {
1019
- if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
1020
- if (!requireOwnerToken(req, res)) return;
1021
- // Called by hooks/session-start.cjs on `/clear` (matcher startup|clear).
1022
- // The session-start hook runs in a separate cjs process with no IPC
1023
- // handle to this forked channels child, so it can't drop recap
1024
- // status directly. Reset to an `empty` baseline so the statusline
1025
- // doesn't carry the prior session's `injected`/`error` recap badge
1026
- // into the cleared session.
1027
- const now = Date.now();
1028
- recapState.state = 'empty';
1029
- recapState.running = false;
1030
- recapState.startedAt = null;
1031
- recapState.lastCompletedAt = now;
1032
- recapState.updatedAt = now;
1033
- recapState.errorMessage = null;
1034
- sendRecapStateToParent();
1035
- res.writeHead(200);
1036
- res.end(JSON.stringify({ ok: true }));
1037
- },
1038
1134
  "/cycle1": async (req, res, body) => {
1039
1135
  if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ ok: false, reason: "method-not-allowed", error: "POST required" })); return; }
1040
1136
  if (!requireOwnerToken(req, res)) return;
@@ -1241,7 +1337,7 @@ function getBridgeOwnershipSnapshot() {
1241
1337
  // MIXDOG_PIN_OWNER=1 in the owning process writes `pinned:true` into
1242
1338
  // active-instance.json. Pinned owners ignore the 10 s stale window — they
1243
1339
  // only relinquish ownership when their OS process actually dies. Set per
1244
- // session (env var on the Claude Code shell) to lock that Lead as the
1340
+ // session (env var on the host agent shell) to lock that Lead as the
1245
1341
  // schedule/webhook receiver across multi-session use.
1246
1342
  function canStealOwnership(active) {
1247
1343
  if (!active) return true;
@@ -1730,14 +1826,14 @@ function injectAndRecord(channelId, name, content, options) {
1730
1826
  // outbound body. Markers are
1731
1827
  // intentionally prepended onto tool RESULTS upstream (tool-loop-guard.mjs
1732
1828
  // build*Warn) so the model
1733
- // self-corrects, but bridge roles commonly echo them and we don't want them
1829
+ // self-corrects, but agent roles commonly echo them and we don't want them
1734
1830
  // surfacing in Discord / Lead channel push.
1735
1831
  if (typeof content === 'string') content = stripSoftWarns(content);
1736
- // Skip-protocol guard: bridge agents (webhook-handler / scheduler-task)
1832
+ // Skip-protocol guard: agents (webhook-handler / scheduler-task)
1737
1833
  // prefix `[meta:silent]` on the first line to opt out
1738
1834
  // of Lead inject for genuine no-op results (label-only events, dedup,
1739
1835
  // "nothing to report"). The body still goes to Discord for audit; only
1740
- // the Lead-context inject is suppressed. See rules/bridge/20-skip-protocol.md.
1836
+ // the Lead-context inject is suppressed. See rules/agent/20-skip-protocol.md.
1741
1837
  if (typeof content === 'string') {
1742
1838
  const m = content.match(/^\s*\[meta:silent\][^\n]*\n?([\s\S]*)$/);
1743
1839
  if (m) {
@@ -1829,7 +1925,7 @@ function wireWebhookHandlers() {
1829
1925
  + ". Relay the finding to the user naturally — summarize clearly, call out any issues, and note what needs a decision.";
1830
1926
  const notifyFn = (text, meta = {}) => {
1831
1927
  if (!text) return;
1832
- // Webhook skip protocol: when the bridge worker emits a `[meta:silent]`
1928
+ // Webhook skip protocol: when the agent worker emits a `[meta:silent]`
1833
1929
  // marker (optionally behind model/role tag prefixes), the event is a
1834
1930
  // no-op (label-only, dedup, "nothing to report"). Drop the message
1835
1931
  // entirely — neither Lead inject nor Discord forward — instead of the
@@ -1979,7 +2075,7 @@ try {
1979
2075
  }
1980
2076
  }
1981
2077
  // No matching pending request — leave the signal on disk so a
1982
- // bridge role hook (or other consumer) gets a chance to claim it.
2078
+ // agent role hook (or other consumer) gets a chance to claim it.
1983
2079
  if (!oldestKey || !oldestEntry) return;
1984
2080
  if (oldestEntry.channelId && oldestEntry.messageId) {
1985
2081
  try {
@@ -2022,7 +2118,7 @@ try {
2022
2118
  backend.onInteraction = (interaction) => {
2023
2119
  // Channel-route permission reply. Custom_id format: perm-ch-{request_id}-{allow|session|deny}.
2024
2120
  // request_id is the 5-letter short ID CC generates via shortRequestId().
2025
- // Emit notifications/claude/channel/permission back to Claude Code; the race
2121
+ // Emit notifications/claude/channel/permission back to the MCP host; the race
2026
2122
  // logic in interactiveHandler.ts resolves the pending request and dismisses
2027
2123
  // every other racer (local dialog, bridge, hook, classifier).
2028
2124
  if (interaction.customId?.startsWith("perm-ch-")) {
@@ -2989,6 +3085,7 @@ async function init(_sharedMcp) {
2989
3085
  });
2990
3086
  }
2991
3087
  async function start() {
3088
+ startChannelDaemonIdleMonitor();
2992
3089
  channelBridgeActive = true;
2993
3090
  writeBridgeState(true);
2994
3091
  if (_isCliOwnedMode) {
@@ -3012,6 +3109,7 @@ async function start() {
3012
3109
  })();
3013
3110
  }
3014
3111
  async function stop() {
3112
+ stopChannelDaemonIdleMonitor();
3015
3113
  try { await stopVoiceWhisperServer(); } catch {}
3016
3114
  await stopOwnedRuntime("unified server stop");
3017
3115
  cleanupInstanceRuntimeFiles(INSTANCE_ID);
@@ -3182,16 +3280,16 @@ if (_isWorkerMode && process.send) {
3182
3280
  } catch { /* best-effort */ }
3183
3281
  return;
3184
3282
  }
3185
- // Claude Code permission request → Discord Allow/Deny prompt.
3283
+ // Host permission request → Discord Allow/Deny prompt.
3186
3284
  // Parent (server.mjs) receives notifications/claude/channel/permission_request
3187
- // from Claude Code and forwards the params here. We post a buttoned message;
3285
+ // from the MCP host and forwards the params here. We post a buttoned message;
3188
3286
  // button clicks are handled in backend.onInteraction and sent back to CC as
3189
3287
  // notifications/claude/channel/permission via sendNotifyToParent.
3190
3288
  if (msg && msg.type === 'permission_request_inbound') {
3191
3289
  try {
3192
3290
  const { request_id, tool_name, description, input_preview } = msg.params || {};
3193
3291
  // tool_input arrives via the passthrough() schema in server.mjs when
3194
- // Claude Code includes it in the permission_request notification.
3292
+ // The host includes it in the permission_request notification.
3195
3293
  // Used to bind the pendingPermRequest to a specific file so two
3196
3294
  // concurrent Edit/Write requests cannot cross-approve via the
3197
3295
  // terminal signal.