mixdog 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -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,24 +329,56 @@ 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 };
344
+ }
345
+ return params;
346
+ }
347
+
348
+ function publishChannelNotify(method, params) {
349
+ try {
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');
366
+ } catch (err) {
367
+ try { process.stderr.write(`mixdog channels: notify bus write failed: ${err && err.message || err}\n`); } catch {}
368
+ }
369
+ }
370
+
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;
348
382
  }
349
383
  try {
350
384
  process.send({ type: 'notify', method, params: outParams });
@@ -612,6 +646,98 @@ const ACTIVE_OWNER_STALE_MS = 1e4;
612
646
  // never blocks process exit. Single JSON atomic write, no measurable load.
613
647
  const OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
614
648
  let ownerHeartbeatTimer = null;
649
+ let channelDaemonIdleTimer = null;
650
+ let channelDaemonLastClientAt = Date.now();
651
+ let channelDaemonBackgroundLogAt = 0;
652
+ const CHANNEL_CLIENT_DIR = path.join(RUNTIME_ROOT, 'channel-clients');
653
+
654
+ function pidAlive(pid) {
655
+ const n = Number(pid);
656
+ if (!Number.isInteger(n) || n <= 0) return false;
657
+ try {
658
+ process.kill(n, 0);
659
+ return true;
660
+ } catch (e) {
661
+ return e?.code === 'EPERM';
662
+ }
663
+ }
664
+
665
+ function countLiveChannelClients() {
666
+ let live = 0;
667
+ const now = Date.now();
668
+ try {
669
+ fs.mkdirSync(CHANNEL_CLIENT_DIR, { recursive: true });
670
+ for (const file of fs.readdirSync(CHANNEL_CLIENT_DIR)) {
671
+ if (!file.endsWith('.json')) continue;
672
+ const full = path.join(CHANNEL_CLIENT_DIR, file);
673
+ let item = null;
674
+ let st = null;
675
+ try {
676
+ st = fs.statSync(full);
677
+ item = JSON.parse(fs.readFileSync(full, 'utf8'));
678
+ } catch {
679
+ try { fs.unlinkSync(full); } catch {}
680
+ continue;
681
+ }
682
+ const pid = Number(item?.pid ?? file.replace(/\.json$/, ''));
683
+ const fresh = now - Math.max(Number(item?.updatedAt) || 0, st.mtimeMs) < 20_000;
684
+ if (pidAlive(pid) && fresh) {
685
+ live += 1;
686
+ } else {
687
+ try { fs.unlinkSync(full); } catch {}
688
+ }
689
+ }
690
+ } catch {}
691
+ return live;
692
+ }
693
+
694
+ function hasChannelBackgroundWork() {
695
+ if (!channelBridgeActive || !bridgeRuntimeConnected) return false;
696
+ if (config.webhook?.enabled === true) return true;
697
+ if (Array.isArray(config.events?.rules) && config.events.rules.length > 0) return true;
698
+ if (Array.isArray(config.nonInteractive) && config.nonInteractive.length > 0) return true;
699
+ if (Array.isArray(config.interactive) && config.interactive.length > 0) return true;
700
+ return false;
701
+ }
702
+
703
+ function checkChannelDaemonIdle() {
704
+ if (!_isChannelDaemonMode || CHANNEL_DAEMON_IDLE_TTL_MS <= 0) return;
705
+ const liveClients = countLiveChannelClients();
706
+ if (liveClients > 0) {
707
+ channelDaemonLastClientAt = Date.now();
708
+ return;
709
+ }
710
+ if (hasChannelBackgroundWork()) {
711
+ const now = Date.now();
712
+ if (now - channelDaemonBackgroundLogAt > 60_000) {
713
+ channelDaemonBackgroundLogAt = now;
714
+ try { process.stderr.write('[channels-worker] daemon idle: keeping alive for configured background work\n'); } catch {}
715
+ }
716
+ channelDaemonLastClientAt = Date.now();
717
+ return;
718
+ }
719
+ if (Date.now() - channelDaemonLastClientAt < CHANNEL_DAEMON_IDLE_TTL_MS) return;
720
+ try { process.stderr.write(`[channels-worker] daemon idle TTL elapsed (${CHANNEL_DAEMON_IDLE_TTL_MS}ms) — shutting down\n`); } catch {}
721
+ stop()
722
+ .then(() => process.exit(0))
723
+ .catch((e) => {
724
+ try { process.stderr.write(`[channels-worker] daemon idle shutdown failed: ${e?.message || e}\n`); } catch {}
725
+ process.exit(1);
726
+ });
727
+ }
728
+
729
+ function startChannelDaemonIdleMonitor() {
730
+ if (!_isChannelDaemonMode || CHANNEL_DAEMON_IDLE_TTL_MS <= 0 || channelDaemonIdleTimer) return;
731
+ channelDaemonLastClientAt = Date.now();
732
+ channelDaemonIdleTimer = setInterval(checkChannelDaemonIdle, 5000);
733
+ channelDaemonIdleTimer.unref?.();
734
+ }
735
+
736
+ function stopChannelDaemonIdleMonitor() {
737
+ if (!channelDaemonIdleTimer) return;
738
+ clearInterval(channelDaemonIdleTimer);
739
+ channelDaemonIdleTimer = null;
740
+ }
615
741
  // Owner gating here is multi-process runtime coordination: only the active
616
742
  // bindingReady gates all send paths until the boot-time refreshBridgeOwnership
617
743
  // ({ restoreBinding: true }) call completes. Without this, scheduler/webhook
@@ -870,7 +996,7 @@ const OWNER_ROUTES = {
870
996
  },
871
997
  "/trigger-schedule": async (req, res, body) => {
872
998
  // Native fallback for `mcp__trigger_schedule` so out-of-band
873
- // verification works when the MCP stdio bridge is down (Claude Code
999
+ // verification works when the MCP stdio bridge is down (host agent
874
1000
  // disconnected, supervisor restart pending, etc.). Same authz as
875
1001
  // /inject — x-owner-token must equal INSTANCE_ID.
876
1002
  if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
@@ -1241,7 +1367,7 @@ function getBridgeOwnershipSnapshot() {
1241
1367
  // MIXDOG_PIN_OWNER=1 in the owning process writes `pinned:true` into
1242
1368
  // active-instance.json. Pinned owners ignore the 10 s stale window — they
1243
1369
  // 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
1370
+ // session (env var on the host agent shell) to lock that Lead as the
1245
1371
  // schedule/webhook receiver across multi-session use.
1246
1372
  function canStealOwnership(active) {
1247
1373
  if (!active) return true;
@@ -1730,14 +1856,14 @@ function injectAndRecord(channelId, name, content, options) {
1730
1856
  // outbound body. Markers are
1731
1857
  // intentionally prepended onto tool RESULTS upstream (tool-loop-guard.mjs
1732
1858
  // build*Warn) so the model
1733
- // self-corrects, but bridge roles commonly echo them and we don't want them
1859
+ // self-corrects, but agent roles commonly echo them and we don't want them
1734
1860
  // surfacing in Discord / Lead channel push.
1735
1861
  if (typeof content === 'string') content = stripSoftWarns(content);
1736
- // Skip-protocol guard: bridge agents (webhook-handler / scheduler-task)
1862
+ // Skip-protocol guard: agents (webhook-handler / scheduler-task)
1737
1863
  // prefix `[meta:silent]` on the first line to opt out
1738
1864
  // of Lead inject for genuine no-op results (label-only events, dedup,
1739
1865
  // "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.
1866
+ // the Lead-context inject is suppressed. See rules/agent/20-skip-protocol.md.
1741
1867
  if (typeof content === 'string') {
1742
1868
  const m = content.match(/^\s*\[meta:silent\][^\n]*\n?([\s\S]*)$/);
1743
1869
  if (m) {
@@ -1829,7 +1955,7 @@ function wireWebhookHandlers() {
1829
1955
  + ". Relay the finding to the user naturally — summarize clearly, call out any issues, and note what needs a decision.";
1830
1956
  const notifyFn = (text, meta = {}) => {
1831
1957
  if (!text) return;
1832
- // Webhook skip protocol: when the bridge worker emits a `[meta:silent]`
1958
+ // Webhook skip protocol: when the agent worker emits a `[meta:silent]`
1833
1959
  // marker (optionally behind model/role tag prefixes), the event is a
1834
1960
  // no-op (label-only, dedup, "nothing to report"). Drop the message
1835
1961
  // entirely — neither Lead inject nor Discord forward — instead of the
@@ -1979,7 +2105,7 @@ try {
1979
2105
  }
1980
2106
  }
1981
2107
  // No matching pending request — leave the signal on disk so a
1982
- // bridge role hook (or other consumer) gets a chance to claim it.
2108
+ // agent role hook (or other consumer) gets a chance to claim it.
1983
2109
  if (!oldestKey || !oldestEntry) return;
1984
2110
  if (oldestEntry.channelId && oldestEntry.messageId) {
1985
2111
  try {
@@ -2022,7 +2148,7 @@ try {
2022
2148
  backend.onInteraction = (interaction) => {
2023
2149
  // Channel-route permission reply. Custom_id format: perm-ch-{request_id}-{allow|session|deny}.
2024
2150
  // request_id is the 5-letter short ID CC generates via shortRequestId().
2025
- // Emit notifications/claude/channel/permission back to Claude Code; the race
2151
+ // Emit notifications/claude/channel/permission back to the MCP host; the race
2026
2152
  // logic in interactiveHandler.ts resolves the pending request and dismisses
2027
2153
  // every other racer (local dialog, bridge, hook, classifier).
2028
2154
  if (interaction.customId?.startsWith("perm-ch-")) {
@@ -2989,6 +3115,7 @@ async function init(_sharedMcp) {
2989
3115
  });
2990
3116
  }
2991
3117
  async function start() {
3118
+ startChannelDaemonIdleMonitor();
2992
3119
  channelBridgeActive = true;
2993
3120
  writeBridgeState(true);
2994
3121
  if (_isCliOwnedMode) {
@@ -3012,6 +3139,7 @@ async function start() {
3012
3139
  })();
3013
3140
  }
3014
3141
  async function stop() {
3142
+ stopChannelDaemonIdleMonitor();
3015
3143
  try { await stopVoiceWhisperServer(); } catch {}
3016
3144
  await stopOwnedRuntime("unified server stop");
3017
3145
  cleanupInstanceRuntimeFiles(INSTANCE_ID);
@@ -3182,16 +3310,16 @@ if (_isWorkerMode && process.send) {
3182
3310
  } catch { /* best-effort */ }
3183
3311
  return;
3184
3312
  }
3185
- // Claude Code permission request → Discord Allow/Deny prompt.
3313
+ // Host permission request → Discord Allow/Deny prompt.
3186
3314
  // Parent (server.mjs) receives notifications/claude/channel/permission_request
3187
- // from Claude Code and forwards the params here. We post a buttoned message;
3315
+ // from the MCP host and forwards the params here. We post a buttoned message;
3188
3316
  // button clicks are handled in backend.onInteraction and sent back to CC as
3189
3317
  // notifications/claude/channel/permission via sendNotifyToParent.
3190
3318
  if (msg && msg.type === 'permission_request_inbound') {
3191
3319
  try {
3192
3320
  const { request_id, tool_name, description, input_preview } = msg.params || {};
3193
3321
  // tool_input arrives via the passthrough() schema in server.mjs when
3194
- // Claude Code includes it in the permission_request notification.
3322
+ // The host includes it in the permission_request notification.
3195
3323
  // Used to bind the pendingPermRequest to a specific file so two
3196
3324
  // concurrent Edit/Write requests cannot cross-approve via the
3197
3325
  // terminal signal.