@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.490

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 (473) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +9 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +68 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +220 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +137 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
  14. package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
  15. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  16. package/dist/cli-adapters/resolve-executable.d.ts +69 -0
  17. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  18. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  19. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  20. package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
  21. package/dist/commands/chat-commands-read.d.ts +23 -0
  22. package/dist/commands/chat-commands-scope.d.ts +39 -0
  23. package/dist/commands/chat-commands-shared.d.ts +33 -0
  24. package/dist/commands/chat-commands-write.d.ts +14 -0
  25. package/dist/commands/chat-commands.d.ts +10 -23
  26. package/dist/commands/cli-manager.d.ts +71 -1
  27. package/dist/commands/handler.d.ts +125 -0
  28. package/dist/commands/high-family/index.d.ts +3 -0
  29. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  30. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  31. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  32. package/dist/commands/high-family/types.d.ts +64 -0
  33. package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
  34. package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
  35. package/dist/commands/low-family/diagnostics.d.ts +2 -0
  36. package/dist/commands/low-family/index.d.ts +3 -0
  37. package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
  38. package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
  39. package/dist/commands/low-family/notification.d.ts +2 -0
  40. package/dist/commands/low-family/refine-config.d.ts +2 -0
  41. package/dist/commands/low-family/session-host.d.ts +2 -0
  42. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  43. package/dist/commands/low-family/status-meta.d.ts +2 -0
  44. package/dist/commands/low-family/types.d.ts +33 -0
  45. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  46. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  47. package/dist/commands/med-family/ide.d.ts +10 -0
  48. package/dist/commands/med-family/index.d.ts +3 -0
  49. package/dist/commands/med-family/mesh-crud.d.ts +48 -0
  50. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  51. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  52. package/dist/commands/med-family/mesh-restart.d.ts +2 -0
  53. package/dist/commands/med-family/types.d.ts +152 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router-aggregate-status.d.ts +24 -0
  56. package/dist/commands/router-mesh-session-owner.d.ts +59 -0
  57. package/dist/commands/router-refine.d.ts +151 -0
  58. package/dist/commands/router-worktree-cleanup.d.ts +133 -0
  59. package/dist/commands/router.d.ts +203 -15
  60. package/dist/commands/upgrade-helper.d.ts +41 -1
  61. package/dist/config/chat-history.d.ts +11 -0
  62. package/dist/config/config.d.ts +19 -0
  63. package/dist/config/mesh-config.d.ts +65 -0
  64. package/dist/config/mesh-json-config.d.ts +199 -0
  65. package/dist/config/registry-resolver.d.ts +54 -0
  66. package/dist/config/repo-settings.d.ts +77 -0
  67. package/dist/config/state-store.d.ts +30 -0
  68. package/dist/daemon/dev-server.d.ts +0 -2
  69. package/dist/detection/cli-detector.d.ts +17 -0
  70. package/dist/detection/ide-detector.d.ts +13 -0
  71. package/dist/detection/win32-ide-version.d.ts +37 -0
  72. package/dist/git/change-impact-config.d.ts +159 -0
  73. package/dist/git/git-commands.d.ts +26 -2
  74. package/dist/git/git-diff.d.ts +6 -0
  75. package/dist/git/git-executor.d.ts +11 -0
  76. package/dist/git/git-status.d.ts +80 -0
  77. package/dist/git/git-types.d.ts +2 -50
  78. package/dist/git/git-worktree.d.ts +71 -1
  79. package/dist/git/index.d.ts +3 -1
  80. package/dist/index.d.ts +70 -17
  81. package/dist/index.js +53532 -19073
  82. package/dist/index.js.map +1 -1
  83. package/dist/index.mjs +53283 -18989
  84. package/dist/index.mjs.map +1 -1
  85. package/dist/installer.d.ts +1 -4
  86. package/dist/ipc/local-ipc-server.d.ts +91 -0
  87. package/dist/launch.d.ts +1 -1
  88. package/dist/logging/async-batch-writer.d.ts +10 -0
  89. package/dist/logging/debug-config.d.ts +16 -0
  90. package/dist/logging/log-redactor.d.ts +24 -0
  91. package/dist/logging/log-tail-reader.d.ts +81 -0
  92. package/dist/logging/logger.d.ts +1 -1
  93. package/dist/mesh/contracts.d.ts +256 -0
  94. package/dist/mesh/coordinator-prompt.d.ts +76 -0
  95. package/dist/mesh/coordinator-registry.d.ts +59 -0
  96. package/dist/mesh/mesh-active-work.d.ts +203 -0
  97. package/dist/mesh/mesh-clone-grace.d.ts +8 -0
  98. package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
  99. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  100. package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
  101. package/dist/mesh/mesh-event-classify.d.ts +7 -0
  102. package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
  103. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  104. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  105. package/dist/mesh/mesh-events-pending.d.ts +258 -0
  106. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  107. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  108. package/dist/mesh/mesh-events.d.ts +6 -49
  109. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  110. package/dist/mesh/mesh-host-ownership.d.ts +21 -1
  111. package/dist/mesh/mesh-init.d.ts +128 -0
  112. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  113. package/dist/mesh/mesh-ledger.d.ts +139 -1
  114. package/dist/mesh/mesh-magi-status.d.ts +63 -0
  115. package/dist/mesh/mesh-missions.d.ts +281 -0
  116. package/dist/mesh/mesh-node-identity.d.ts +310 -0
  117. package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
  118. package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
  119. package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
  120. package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
  121. package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
  122. package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
  123. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  124. package/dist/mesh/mesh-refine-gates.d.ts +457 -0
  125. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  126. package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
  127. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  128. package/dist/mesh/mesh-routing.d.ts +70 -0
  129. package/dist/mesh/mesh-runtime-store.d.ts +567 -0
  130. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  131. package/dist/mesh/mesh-task-inflight.d.ts +46 -0
  132. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  133. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
  134. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  135. package/dist/mesh/mesh-work-queue.d.ts +392 -1
  136. package/dist/mesh/preview-freshness.d.ts +28 -0
  137. package/dist/mesh/refine-config.d.ts +97 -0
  138. package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
  139. package/dist/providers/acp-provider-instance.d.ts +5 -0
  140. package/dist/providers/approval-utils.d.ts +46 -0
  141. package/dist/providers/chat-message-normalization.d.ts +58 -1
  142. package/dist/providers/cli-provider-effect-format.d.ts +30 -0
  143. package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
  144. package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
  145. package/dist/providers/cli-provider-instance-types.d.ts +45 -0
  146. package/dist/providers/cli-provider-instance.d.ts +426 -30
  147. package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
  148. package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
  149. package/dist/providers/contracts.d.ts +196 -6
  150. package/dist/providers/external-sources.d.ts +71 -0
  151. package/dist/providers/manual-attendance.d.ts +79 -0
  152. package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
  153. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
  154. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  155. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  156. package/dist/providers/native-history/constants.d.ts +12 -0
  157. package/dist/providers/native-history/dispatcher.d.ts +44 -0
  158. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  159. package/dist/providers/native-history/index.d.ts +13 -0
  160. package/dist/providers/provider-instance-manager.d.ts +30 -0
  161. package/dist/providers/provider-instance.d.ts +31 -1
  162. package/dist/providers/provider-loader.d.ts +42 -5
  163. package/dist/providers/provider-trust.d.ts +31 -0
  164. package/dist/providers/read-chat-contract.d.ts +29 -0
  165. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  166. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  167. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  168. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  169. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  170. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  171. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  172. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  173. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  174. package/dist/providers/sdk/v1/index.d.ts +30 -0
  175. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  176. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  177. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  178. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  179. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  180. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  181. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  182. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  183. package/dist/providers/spec/adapter.d.ts +98 -0
  184. package/dist/providers/spec/cli-adapter.d.ts +223 -0
  185. package/dist/providers/spec/evaluator.d.ts +45 -0
  186. package/dist/providers/spec/fsm-driver.d.ts +449 -0
  187. package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
  188. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  189. package/dist/providers/spec/fsm-types.d.ts +202 -0
  190. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  191. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  192. package/dist/providers/spec/route.d.ts +4 -0
  193. package/dist/providers/spec/types.d.ts +244 -0
  194. package/dist/providers/status-monitor.d.ts +7 -7
  195. package/dist/providers/transcript-v2.d.ts +176 -0
  196. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  197. package/dist/providers/working-dir.d.ts +17 -0
  198. package/dist/repo-mesh-types.d.ts +616 -12
  199. package/dist/runtime-defaults.d.ts +2 -0
  200. package/dist/session-host/managed-host.d.ts +64 -0
  201. package/dist/session-host/runtime-surface.d.ts +10 -16
  202. package/dist/sessions/registry.d.ts +26 -0
  203. package/dist/shared-types-extra.d.ts +2 -4
  204. package/dist/shared-types.d.ts +110 -55
  205. package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
  206. package/dist/status/normalize.d.ts +1 -1
  207. package/dist/status/normalize.js +1 -0
  208. package/dist/status/normalize.js.map +1 -1
  209. package/dist/status/normalize.mjs +1 -0
  210. package/dist/status/normalize.mjs.map +1 -1
  211. package/dist/status/reporter.d.ts +2 -0
  212. package/dist/status/snapshot.d.ts +27 -0
  213. package/dist/system/hash.d.ts +8 -0
  214. package/dist/system/load-better-sqlite3.d.ts +21 -0
  215. package/dist/types.d.ts +20 -6
  216. package/package.json +11 -4
  217. package/src/agent-stream/poller.ts +2 -3
  218. package/src/agent-stream/provider-adapter.ts +1 -1
  219. package/src/boot/daemon-lifecycle.ts +80 -14
  220. package/src/boot/process-hardening.ts +89 -0
  221. package/src/build-info.ts +73 -0
  222. package/src/chat/source-machine.ts +534 -0
  223. package/src/chat/source-resolver.ts +0 -0
  224. package/src/chat/subscription-updates.ts +20 -1
  225. package/src/cli-adapter-types.d.ts +3 -1
  226. package/src/cli-adapter-types.ts +68 -2
  227. package/src/cli-adapters/cli-script-runner.ts +421 -0
  228. package/src/cli-adapters/cli-state-engine.ts +1282 -0
  229. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  230. package/src/cli-adapters/provider-cli-adapter.ts +1048 -1143
  231. package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
  232. package/src/cli-adapters/provider-cli-parse.ts +19 -0
  233. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  234. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  235. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  236. package/src/cli-adapters/pty-transport.ts +2 -1
  237. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  238. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  239. package/src/cli-adapters/resolve-executable.ts +204 -0
  240. package/src/cli-adapters/session-host-transport.ts +2 -1
  241. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
  242. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  243. package/src/cli-adapters/terminal-screen.ts +16 -81
  244. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  245. package/src/commands/chat-commands-read.ts +2788 -0
  246. package/src/commands/chat-commands-scope.ts +54 -0
  247. package/src/commands/chat-commands-shared.ts +114 -0
  248. package/src/commands/chat-commands-write.ts +891 -0
  249. package/src/commands/chat-commands.ts +19 -1841
  250. package/src/commands/cli-manager.d.ts +2 -0
  251. package/src/commands/cli-manager.ts +724 -27
  252. package/src/commands/handler.ts +865 -2
  253. package/src/commands/high-family/index.ts +28 -0
  254. package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
  255. package/src/commands/high-family/mesh-events.ts +102 -0
  256. package/src/commands/high-family/mesh-status.ts +857 -0
  257. package/src/commands/high-family/types.ts +80 -0
  258. package/src/commands/low-family/coordinator-prompt.ts +125 -0
  259. package/src/commands/low-family/daemon-lifecycle.ts +120 -0
  260. package/src/commands/low-family/diagnostics.ts +57 -0
  261. package/src/commands/low-family/index.ts +37 -0
  262. package/src/commands/low-family/mesh-ledger.ts +62 -0
  263. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  264. package/src/commands/low-family/notification.ts +116 -0
  265. package/src/commands/low-family/refine-config.ts +106 -0
  266. package/src/commands/low-family/session-host.ts +282 -0
  267. package/src/commands/low-family/spec-providerdev.ts +217 -0
  268. package/src/commands/low-family/status-meta.ts +112 -0
  269. package/src/commands/low-family/types.ts +39 -0
  270. package/src/commands/med-family/cli-agent.ts +270 -0
  271. package/src/commands/med-family/fast-forward.ts +230 -0
  272. package/src/commands/med-family/ide.ts +163 -0
  273. package/src/commands/med-family/index.ts +37 -0
  274. package/src/commands/med-family/mesh-crud.ts +1268 -0
  275. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  276. package/src/commands/med-family/mesh-queue.ts +167 -0
  277. package/src/commands/med-family/mesh-restart.ts +92 -0
  278. package/src/commands/med-family/types.ts +153 -0
  279. package/src/commands/mesh-coordinator.ts +334 -124
  280. package/src/commands/router-aggregate-status.ts +209 -0
  281. package/src/commands/router-mesh-session-owner.ts +114 -0
  282. package/src/commands/router-refine.ts +1794 -0
  283. package/src/commands/router-worktree-cleanup.ts +870 -0
  284. package/src/commands/router.ts +567 -4149
  285. package/src/commands/stream-commands.ts +8 -0
  286. package/src/commands/upgrade-helper.ts +310 -45
  287. package/src/config/chat-history.ts +517 -24
  288. package/src/config/config.ts +30 -0
  289. package/src/config/mesh-config.ts +300 -24
  290. package/src/config/mesh-json-config.ts +376 -0
  291. package/src/config/recent-activity.ts +8 -2
  292. package/src/config/registry-resolver.ts +100 -0
  293. package/src/config/repo-settings.ts +111 -0
  294. package/src/config/state-store.ts +55 -0
  295. package/src/daemon/dev-auto-implement.ts +3 -2
  296. package/src/daemon/dev-cli-debug.ts +10 -1
  297. package/src/daemon/dev-server.ts +0 -541
  298. package/src/detection/cli-detector.ts +94 -9
  299. package/src/detection/ide-detector.ts +55 -16
  300. package/src/detection/win32-ide-version.ts +106 -0
  301. package/src/git/change-impact-config.ts +354 -0
  302. package/src/git/git-commands.ts +112 -16
  303. package/src/git/git-diff.ts +81 -11
  304. package/src/git/git-executor.ts +12 -0
  305. package/src/git/git-status.ts +822 -48
  306. package/src/git/git-types.ts +14 -62
  307. package/src/git/git-worktree.ts +261 -4
  308. package/src/git/index.ts +17 -0
  309. package/src/index.ts +190 -13
  310. package/src/installer.d.ts +1 -1
  311. package/src/installer.ts +8 -6
  312. package/src/ipc/local-ipc-server.ts +278 -0
  313. package/src/launch.d.ts +1 -1
  314. package/src/launch.ts +37 -28
  315. package/src/logging/async-batch-writer.ts +55 -0
  316. package/src/logging/command-log.ts +7 -5
  317. package/src/logging/debug-config.ts +25 -0
  318. package/src/logging/debug-trace.ts +7 -2
  319. package/src/logging/log-redactor.ts +100 -0
  320. package/src/logging/log-tail-reader.ts +341 -0
  321. package/src/logging/logger.ts +14 -7
  322. package/src/mesh/contracts.ts +510 -0
  323. package/src/mesh/coordinator-prompt.ts +627 -34
  324. package/src/mesh/coordinator-registry.ts +121 -0
  325. package/src/mesh/mesh-active-work.ts +706 -0
  326. package/src/mesh/mesh-clone-grace.ts +68 -0
  327. package/src/mesh/mesh-completion-synthesis.ts +416 -0
  328. package/src/mesh/mesh-coordinator-config.ts +97 -0
  329. package/src/mesh/mesh-delivery-policy.ts +284 -0
  330. package/src/mesh/mesh-event-classify.ts +78 -0
  331. package/src/mesh/mesh-event-forwarding.ts +2030 -0
  332. package/src/mesh/mesh-event-trace.ts +67 -0
  333. package/src/mesh/mesh-events-coordinator.ts +32 -0
  334. package/src/mesh/mesh-events-pending.ts +1548 -0
  335. package/src/mesh/mesh-events-stale.ts +452 -0
  336. package/src/mesh/mesh-events-utils.ts +443 -0
  337. package/src/mesh/mesh-events.ts +43 -1035
  338. package/src/mesh/mesh-fast-forward.ts +856 -0
  339. package/src/mesh/mesh-host-ownership.ts +41 -3
  340. package/src/mesh/mesh-init.ts +350 -0
  341. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  342. package/src/mesh/mesh-ledger.ts +897 -102
  343. package/src/mesh/mesh-magi-status.ts +223 -0
  344. package/src/mesh/mesh-missions.ts +695 -0
  345. package/src/mesh/mesh-node-identity.ts +2065 -0
  346. package/src/mesh/mesh-queue-assignment.ts +2498 -0
  347. package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
  348. package/src/mesh/mesh-reconcile-config.ts +66 -0
  349. package/src/mesh/mesh-reconcile-identity.ts +103 -0
  350. package/src/mesh/mesh-reconcile-loop.ts +1696 -0
  351. package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
  352. package/src/mesh/mesh-refine-batch.ts +205 -0
  353. package/src/mesh/mesh-refine-gates.ts +1792 -0
  354. package/src/mesh/mesh-refine-status.ts +231 -0
  355. package/src/mesh/mesh-remote-event-pull.ts +279 -0
  356. package/src/mesh/mesh-review-inbox.ts +308 -0
  357. package/src/mesh/mesh-routing.ts +291 -0
  358. package/src/mesh/mesh-runtime-store.ts +2292 -0
  359. package/src/mesh/mesh-scheduling-runtime.ts +198 -0
  360. package/src/mesh/mesh-task-inflight.ts +70 -0
  361. package/src/mesh/mesh-task-stats.ts +161 -0
  362. package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
  363. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  364. package/src/mesh/mesh-work-queue.ts +1489 -145
  365. package/src/mesh/preview-freshness.ts +163 -0
  366. package/src/mesh/refine-config.ts +129 -12
  367. package/src/mesh/worktree-bootstrap-config.ts +599 -0
  368. package/src/providers/acp-provider-instance.ts +43 -10
  369. package/src/providers/approval-utils.d.ts +5 -0
  370. package/src/providers/approval-utils.ts +100 -6
  371. package/src/providers/chat-message-normalization.ts +126 -4
  372. package/src/providers/cli-provider-effect-format.ts +53 -0
  373. package/src/providers/cli-provider-history-dedup.ts +75 -0
  374. package/src/providers/cli-provider-input-prompt.ts +133 -0
  375. package/src/providers/cli-provider-instance-types.ts +131 -0
  376. package/src/providers/cli-provider-instance.ts +2771 -499
  377. package/src/providers/cli-provider-status-helpers.ts +123 -0
  378. package/src/providers/cli-provider-transcript-merge.ts +114 -0
  379. package/src/providers/contracts.d.ts +55 -0
  380. package/src/providers/contracts.ts +207 -6
  381. package/src/providers/extension-provider-instance.ts +12 -7
  382. package/src/providers/external-sources.ts +218 -0
  383. package/src/providers/ide-provider-instance.ts +35 -12
  384. package/src/providers/manual-attendance.ts +105 -0
  385. package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
  386. package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
  387. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  388. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  389. package/src/providers/native-history/constants.ts +19 -0
  390. package/src/providers/native-history/dispatcher.ts +617 -0
  391. package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
  392. package/src/providers/native-history/index.ts +30 -0
  393. package/src/providers/provider-instance-manager.ts +71 -0
  394. package/src/providers/provider-instance.ts +24 -1
  395. package/src/providers/provider-loader.ts +700 -55
  396. package/src/providers/provider-schema.ts +93 -14
  397. package/src/providers/provider-trust.ts +114 -0
  398. package/src/providers/read-chat-contract.ts +76 -16
  399. package/src/providers/sdk/README.md +49 -0
  400. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  401. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  402. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  403. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
  404. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  405. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  406. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  407. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  408. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  409. package/src/providers/sdk/v1/index.ts +152 -0
  410. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  411. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  412. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  413. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
  414. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  415. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  416. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  417. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  418. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  419. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  420. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  421. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  422. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  423. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  424. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  425. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  426. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  427. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  428. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  429. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  430. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  431. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  432. package/src/providers/sdk/v1/validators/index.ts +19 -0
  433. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  434. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  435. package/src/providers/spec/adapter.ts +246 -0
  436. package/src/providers/spec/cli-adapter.ts +1064 -0
  437. package/src/providers/spec/evaluator.ts +407 -0
  438. package/src/providers/spec/fsm-driver.ts +1528 -0
  439. package/src/providers/spec/fsm-evaluator.ts +290 -0
  440. package/src/providers/spec/fsm-loader.ts +128 -0
  441. package/src/providers/spec/fsm-types.ts +301 -0
  442. package/src/providers/spec/native-history-executor.ts +1241 -0
  443. package/src/providers/spec/pre-launch-trust.ts +104 -0
  444. package/src/providers/spec/route.ts +51 -0
  445. package/src/providers/spec/types.ts +284 -0
  446. package/src/providers/status-monitor.d.ts +7 -7
  447. package/src/providers/status-monitor.ts +37 -22
  448. package/src/providers/transcript-v2.ts +567 -0
  449. package/src/providers/types/interactive-prompt.ts +536 -0
  450. package/src/providers/version-archive.ts +64 -24
  451. package/src/providers/working-dir.ts +23 -0
  452. package/src/repo-mesh-types.ts +861 -14
  453. package/src/runtime-defaults.ts +39 -0
  454. package/src/session-host/managed-host.ts +218 -0
  455. package/src/session-host/runtime-surface.ts +20 -80
  456. package/src/sessions/registry.ts +44 -0
  457. package/src/shared-types-extra.ts +2 -4
  458. package/src/shared-types.d.ts +8 -0
  459. package/src/shared-types.ts +148 -55
  460. package/src/status/builders.ts +69 -6
  461. package/src/status/chat-tail-hot-sessions.ts +117 -2
  462. package/src/status/normalize.ts +2 -0
  463. package/src/status/reporter.ts +19 -1
  464. package/src/status/snapshot.ts +100 -27
  465. package/src/system/hash.ts +23 -0
  466. package/src/system/host-memory.ts +29 -12
  467. package/src/system/load-better-sqlite3.ts +68 -0
  468. package/src/types.ts +26 -6
  469. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  470. package/dist/mesh/mesh-sync.d.ts +0 -53
  471. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  472. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  473. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,2030 @@
1
+ import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
2
+ import { loadConfig } from '../config/config.js';
3
+ import { getMesh, getMeshByRepo, listMeshes } from '../config/mesh-config.js';
4
+ import { LOG } from '../logging/logger.js';
5
+ import { appendLedgerEntry, buildTaskCompletionEvidence, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
6
+ import type { SessionRecoveryContext } from './mesh-ledger.js';
7
+ import { updateSessionTaskStatus, enqueueTask, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches, hasPendingDependents, getQueue } from './mesh-work-queue.js';
8
+ import { markSessionDeliveriesTerminal, updateSessionDeliveryStatus } from './mesh-delivery-policy.js';
9
+ import { MeshRuntimeStore, pruneMeshRuntimeRetention } from './mesh-runtime-store.js';
10
+ import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, prunePendingMeshCoordinatorEventsRetention, readV2EnvelopeFromWire, type PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
11
+ import type { ProviderInstance } from '../providers/provider-instance.js';
12
+ import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent, isUnroutableDelegateRejection } from './mesh-routing.js';
13
+ import { resolveMeshHostStatus } from './mesh-host-ownership.js';
14
+ import { enqueueUnresolvedDelegateForward, nudgeUnresolvedForwardRetry } from './mesh-unresolved-forward-outbox.js';
15
+ import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
16
+ import { getLastDisplayMessage } from '../status/snapshot.js';
17
+ import { resolveDelegatedWorkerAutoApprove } from '../repo-mesh-types.js';
18
+ import { meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, sessionIdsEquivalent, type MeshNodeIdentified } from '@adhdev/mesh-shared';
19
+ import {
20
+ findRecentTerminalLedgerEvidence,
21
+ findTerminalLedgerEvidenceForTask,
22
+ hasDispatchAfterTerminal,
23
+ hasUnterminalDirectDispatchLedgerEntry,
24
+ buildNoProgressCompletionReconciliation,
25
+ } from './mesh-events-stale.js';
26
+ import { endTaskDispatchInFlight } from './mesh-task-inflight.js';
27
+ import { readMeshNodeDaemonId } from './mesh-node-identity.js';
28
+ import {
29
+ buildMeshSystemMessage,
30
+ readNonEmptyString,
31
+ resolveEventSessionId,
32
+ readRefineJobId,
33
+ readWorkerResultMetadata,
34
+ resolveMeshSurfacedSessionPreview,
35
+ isFalseIdleCompletion,
36
+ isWeakCompletionEvidence,
37
+ } from './mesh-events-utils.js';
38
+ import { isMeshCoordinatorEvent, shouldForceInjectMeshEvent, EVENT_TO_LEDGER_KIND } from './mesh-event-classify.js';
39
+ import {
40
+ getMeshWithCache,
41
+ tryAssignQueueTask,
42
+ triggerMeshQueue,
43
+ runIdleMaintenanceThenAssignQueue,
44
+ maybeAutoFastForwardIdleNode,
45
+ sessionHasActiveAssignment,
46
+ } from './mesh-queue-assignment.js';
47
+
48
+ // The set of coordinator-daemon ids this daemon answers to when draining the
49
+ // pending-events queue. Mirrors resolveCoordinatorDaemonIds in mesh-reconcile-loop:
50
+ // a unicast event may be stamped with the status id, the bare machineId, OR the
51
+ // config-form node daemonId (`daemon_<machineId>`) depending on which dispatch path
52
+ // created the worker. We expand to EVERY equivalent form so a `daemon_<machineId>`
53
+ // completion matches a coordinator that knows itself as bare `<machineId>` (the
54
+ // base-node completion-surface bug) and vice versa.
55
+ function resolveCoordinatorDrainDaemonIds(components: DaemonComponents): string[] {
56
+ const statusInstanceId = readNonEmptyString((components as { statusInstanceId?: string }).statusInstanceId);
57
+ const machineId = readNonEmptyString(loadConfig().machineId);
58
+ return expandDaemonIdForms([statusInstanceId, machineId]);
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Remote Node Idle Session Tracking
63
+ // ---------------------------------------------------------------------------
64
+ const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Workspace-to-mesh lookup cache
68
+ // ---------------------------------------------------------------------------
69
+ const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
70
+ const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
71
+
72
+ function getCachedMeshByWorkspace(workspace: string): any {
73
+ const now = Date.now();
74
+ const cached = meshByWorkspaceCache.get(workspace);
75
+ if (cached && now - cached.cachedAt < MESH_WORKSPACE_CACHE_TTL_MS) return cached.mesh;
76
+ const mesh = getMeshByRepo(workspace);
77
+ meshByWorkspaceCache.set(workspace, { mesh, cachedAt: now });
78
+ return mesh;
79
+ }
80
+
81
+ // Deterministic meshId recovery for a forwarded worker event that carries no meshId.
82
+ // An unresolved-mesh worker (forwardUnresolvedDelegateEvent) cannot resolve its own
83
+ // meshId locally, so it pushes the event with nodeId + workspace only and relies on
84
+ // the coordinator — which hosts the mesh — to recover the id. Workspace recovery
85
+ // (getCachedMeshByWorkspace → getMeshByRepo) is the fast path but can miss (a worktree
86
+ // clone whose repoIdentity differs, or a transient cache state), which left the retry
87
+ // permanently rejected with "meshId required". The node-id IS a stable, coordinator-side
88
+ // fact: scan the hosted meshes for the one whose node matches the forwarded nodeId
89
+ // (3-form normalizer). This is timing-independent and never depends on repo lookup.
90
+ function recoverMeshIdByNodeId(nodeId: string): string {
91
+ if (!nodeId) return '';
92
+ for (const mesh of listMeshes()) {
93
+ if (Array.isArray(mesh.nodes) && mesh.nodes.some((n: any) => meshNodeIdMatches(n, nodeId))) {
94
+ return readNonEmptyString(mesh.id);
95
+ }
96
+ }
97
+ return '';
98
+ }
99
+
100
+ // MESHID-DROP coordinator-anchor recovery (Fix B): the last-resort meshId recovery for an
101
+ // unresolved-delegate forward whose payload carries the worker's coordinator anchor
102
+ // (meshCoordinatorDaemonId) but no resolvable meshId — neither workspace nor nodeId scan
103
+ // matched (a freshly-cloned worktree node not yet registered under the forwarded id form, or
104
+ // an empty payload nodeId). The receiving daemon IS the coordinator/host, so scope to the
105
+ // meshes IT hosts whose host daemon matches the anchor (daemonIdsEquivalent — never a raw
106
+ // compare, so daemon_mach_/mach_/standalone_ forms all match the same machine). Among those:
107
+ // • a nodeId → the mesh whose nodes contain it (meshNodeIdMatches 3-form) wins;
108
+ // • no nodeId → fall back to the anchor's SINGLE hosted mesh only. Ambiguity (the anchor
109
+ // hosts >1 mesh and no node disambiguates) returns '' rather than guess — a wrong meshId
110
+ // would inject the completion into an unrelated mesh's ledger, worse than the retry-cap drop.
111
+ export function recoverMeshIdByCoordinatorAndNode(coordinatorDaemonId: string, nodeId: string): string {
112
+ if (!coordinatorDaemonId) return '';
113
+ const hosted = listMeshes().filter(mesh => {
114
+ const host = resolveMeshHostStatus(mesh);
115
+ return host.role === 'host'
116
+ && (!host.hostDaemonId || daemonIdsEquivalent(host.hostDaemonId, coordinatorDaemonId));
117
+ });
118
+ if (hosted.length === 0) return '';
119
+ if (nodeId) {
120
+ const byNode = hosted.find(mesh =>
121
+ Array.isArray(mesh.nodes) && mesh.nodes.some((n: any) => meshNodeIdMatches(n, nodeId)));
122
+ if (byNode) return readNonEmptyString(byNode.id);
123
+ // nodeId present but matched no hosted mesh — do NOT fall through to the single-mesh
124
+ // guess; the node belongs to a mesh we don't host (or under a different id), and
125
+ // guessing would misroute. Stay unresolved.
126
+ return '';
127
+ }
128
+ // No nodeId to disambiguate: only safe when the anchor hosts exactly one mesh.
129
+ return hosted.length === 1 ? readNonEmptyString(hosted[0].id) : '';
130
+ }
131
+
132
+ // RECONCILE-MESHID-DROP: WORKER-side meshId resolution for an unresolved-delegate
133
+ // forward payload. forwardUnresolvedDelegateEvent omits meshId by design (the worker
134
+ // "can't resolve it") and relies on the COORDINATOR recovering it from workspace/nodeId.
135
+ // That recovery fails when the no_node_binding session's payload has an empty nodeId AND
136
+ // the coordinator's workspace→mesh lookup misses (a worktree clone whose repoIdentity
137
+ // differs / a cache miss) — leaving the reconcile retry rejected with "meshId required"
138
+ // every 4s forever. The worker actually has MORE context than the stripped payload gives
139
+ // the coordinator: it hosts the node as a member and holds the LIVE session, whose
140
+ // settings.meshNodeFor / meshNodeId are authoritative even when they were not stamped
141
+ // onto the original event. Resolve here (worker side) and stamp meshId onto the payload so
142
+ // the coordinator accepts it. Mirrors the receiver's recovery order, then adds the live-
143
+ // session fallback. Returns '' when even the worker cannot resolve it (truly unresolvable —
144
+ // the retry cap then drops it instead of looping). No side effects; safe to call per retry.
145
+ export function resolveForwardEventMeshId(
146
+ components: DaemonComponents,
147
+ payload: Record<string, unknown>,
148
+ ): string {
149
+ const direct = readNonEmptyString(payload.meshId);
150
+ if (direct) return direct;
151
+ const workspace = readNonEmptyString(payload.workspace);
152
+ const byWorkspace = workspace ? readNonEmptyString(getCachedMeshByWorkspace(workspace)?.id) : '';
153
+ if (byWorkspace) return byWorkspace;
154
+ const byNode = recoverMeshIdByNodeId(readNonEmptyString(payload.nodeId));
155
+ if (byNode) return byNode;
156
+ // Live-session fallback: the worker session may carry meshNodeFor / meshNodeId now even
157
+ // though the original event didn't (a late stamp, or an event that fired before binding).
158
+ const sessionId = readNonEmptyString(payload.targetSessionId)
159
+ || readNonEmptyString(payload.sessionId)
160
+ || readNonEmptyString(payload.instanceId);
161
+ if (sessionId) {
162
+ try {
163
+ const state = components.instanceManager?.getInstance?.(sessionId)?.getState?.();
164
+ const settings = (state?.settings as Record<string, unknown>) || {};
165
+ const meshNodeFor = readNonEmptyString(settings.meshNodeFor);
166
+ if (meshNodeFor) return meshNodeFor;
167
+ const byStamp = recoverMeshIdByNodeId(readNonEmptyString(settings.meshNodeId));
168
+ if (byStamp) return byStamp;
169
+ const sessionWorkspace = readNonEmptyString(state?.workspace);
170
+ const bySessionWorkspace = sessionWorkspace ? readNonEmptyString(getCachedMeshByWorkspace(sessionWorkspace)?.id) : '';
171
+ if (bySessionWorkspace) return bySessionWorkspace;
172
+ } catch { /* best-effort — fall through to unresolved */ }
173
+ }
174
+ return '';
175
+ }
176
+
177
+
178
+ export function __resetMeshWorkspaceCacheForTests(): void {
179
+ meshByWorkspaceCache.clear();
180
+ }
181
+
182
+ // Throttle the pending-event retention DELETE so it runs at most hourly — the
183
+ // remote-idle sweep below fires on every completion/idle transition, but a table
184
+ // scan + DELETE should not. In-process timestamp; a restart re-arms it, which only
185
+ // means one prune shortly after boot (harmless, idempotent).
186
+ let lastPendingEventsPruneAt = 0;
187
+ const PENDING_EVENTS_PRUNE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
188
+
189
+ function sweepExpiredRemoteIdleSessions(): void {
190
+ try {
191
+ MeshRuntimeStore.getInstance().pruneExpiredRemoteIdleSessions();
192
+ } catch { /* best-effort */ }
193
+ // Piggyback the retention prunes on the same periodic sweep, but hourly — this
194
+ // is the maintenance hook that keeps mesh-runtime.db from accumulating stale
195
+ // rows without bound: mesh_pending_events (drained/orphaned rows) plus, on the
196
+ // SAME cadence (SoT 1-11 (b)), the event ledger / tool-call log / terminal
197
+ // queue retention in pruneMeshRuntimeRetention.
198
+ const now = Date.now();
199
+ if (now - lastPendingEventsPruneAt >= PENDING_EVENTS_PRUNE_INTERVAL_MS) {
200
+ lastPendingEventsPruneAt = now;
201
+ prunePendingMeshCoordinatorEventsRetention();
202
+ pruneMeshRuntimeRetention();
203
+ }
204
+ }
205
+
206
+ const INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS = 30 * 60 * 1000;
207
+
208
+ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boolean {
209
+ return event.intentional === true
210
+ || event.intentionalStop === true
211
+ || event.operatorCleanup === true
212
+ || event.reason === 'operator_cleanup'
213
+ || event.stopReason === 'operator_cleanup'
214
+ || event.cleanupReason === 'operator_cleanup'
215
+ || event.source === 'mesh_cleanup_sessions'
216
+ || event.source === 'mesh_remove_node';
217
+ }
218
+
219
+ function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
220
+ if (!sessionId && !nodeId) return false;
221
+ const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
222
+ const entries = readLedgerEntries(meshId, { tail: 200 });
223
+ for (let i = entries.length - 1; i >= 0; i--) {
224
+ const entry = entries[i];
225
+ const timestamp = new Date(entry.timestamp).getTime();
226
+ if (!Number.isNaN(timestamp) && timestamp < cutoff) break;
227
+ if (!isIntentionalCleanupStopEntry(entry)) continue;
228
+ // Session ids are single-form; sessionIdsEquivalent is the one canonical
229
+ // exact-match predicate (see its doc for why no form expansion is needed).
230
+ if (sessionId && sessionIdsEquivalent(entry.sessionId, sessionId)) return true;
231
+ // Normalized node-id match (P4): the cleanup-stop entry's node id may be stored as
232
+ // `nodeId` or `node_id` and the `nodeId` arg can be in either form — a raw `===`
233
+ // would miss a genuine intentional-cleanup entry and fail to suppress the stop event.
234
+ if (!sessionId && nodeId && meshNodeIdMatches(entry as unknown as MeshNodeIdentified, nodeId)) return true;
235
+ }
236
+ return false;
237
+ }
238
+
239
+ function shouldSuppressIntentionalCleanupStop(args: {
240
+ event: string;
241
+ meshId: string;
242
+ metadataEvent: Record<string, unknown>;
243
+ sessionId?: string;
244
+ nodeId?: string;
245
+ }): boolean {
246
+ if (args.event !== 'agent:stopped' && args.event !== 'monitor:no_progress') return false;
247
+ if (isIntentionalCleanupStopMetadata(args.metadataEvent)) return true;
248
+ return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
249
+ }
250
+
251
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
252
+
253
+ function hasFingerprintSeen(meshId: string, fingerprint: string): boolean {
254
+ try {
255
+ return MeshRuntimeStore.getInstance().hasCompletionFingerprint(meshId, fingerprint);
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+
261
+ function recordFingerprintSeen(meshId: string, fingerprint: string): void {
262
+ try {
263
+ const db = MeshRuntimeStore.getInstance();
264
+ db.recordCompletionFingerprint(meshId, fingerprint, RECENT_COMPLETION_FINGERPRINT_TTL_MS);
265
+ db.sweepExpiredFingerprints();
266
+ } catch { /* best-effort; duplicate events are preferable to a crash */ }
267
+ }
268
+
269
+ function readEventTimestamp(value: unknown): number | null {
270
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
271
+ if (typeof value === 'string' && value.trim()) {
272
+ const numeric = Number(value);
273
+ if (Number.isFinite(numeric)) return numeric;
274
+ const parsed = Date.parse(value);
275
+ if (Number.isFinite(parsed)) return parsed;
276
+ }
277
+ return null;
278
+ }
279
+
280
+ function buildMeshCompletionFingerprint(args: {
281
+ meshId: string;
282
+ event: string;
283
+ sessionId: string;
284
+ providerType?: string;
285
+ providerSessionId?: string;
286
+ timestamp?: number | null;
287
+ finalSummary?: string;
288
+ coordinatorDaemonId?: string;
289
+ }): string {
290
+ const timestampPart = Number.isFinite(args.timestamp)
291
+ ? String(args.timestamp)
292
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
293
+ return [
294
+ args.meshId,
295
+ args.event,
296
+ args.sessionId,
297
+ args.providerType || '',
298
+ args.providerSessionId || '',
299
+ timestampPart,
300
+ args.coordinatorDaemonId || '',
301
+ ].join('::');
302
+ }
303
+
304
+ function isDuplicateMeshCompletionEvent(args: {
305
+ meshId: string;
306
+ event: string;
307
+ sessionId: string;
308
+ providerType?: string;
309
+ providerSessionId?: string;
310
+ timestamp?: number | null;
311
+ finalSummary?: string;
312
+ coordinatorDaemonId?: string;
313
+ taskId?: string;
314
+ nodeId?: string;
315
+ }): boolean {
316
+ const fingerprint = buildMeshCompletionFingerprint(args);
317
+ if (!fingerprint) return false;
318
+ if (hasFingerprintSeen(args.meshId, fingerprint)) {
319
+ // MESH-COMPLEXITY-AUDIT Part 8-2: the dedup DECISION is the fingerprint
320
+ // match on the line above — that is the no-loss-neutral gate and it is
321
+ // unchanged. The former mesh_completion_conflicts diagnostic side-record
322
+ // (which task lost a fingerprint collision) had no production reader and
323
+ // played no part in the delivery contract, so it was dropped. Suppressing
324
+ // the duplicate here is unaffected: a genuine second, distinct completion
325
+ // has a different fingerprint (different taskId/timestamp/summary) and
326
+ // therefore does not match, so it is NOT suppressed here.
327
+ return true;
328
+ }
329
+ recordFingerprintSeen(args.meshId, fingerprint);
330
+ return false;
331
+ }
332
+
333
+ function isDuplicateMeshApprovalEvent(args: {
334
+ meshId: string;
335
+ sessionId: string;
336
+ providerType?: string;
337
+ timestamp?: number | null;
338
+ modalMessage?: string;
339
+ modalButtons?: unknown;
340
+ }): boolean {
341
+ const modalButtons = Array.isArray(args.modalButtons)
342
+ ? args.modalButtons.map(button => String(button).trim()).filter(Boolean)
343
+ : [];
344
+ const approvalIdentity = Number.isFinite(args.timestamp)
345
+ ? String(args.timestamp)
346
+ : JSON.stringify({ message: args.modalMessage || '', buttons: modalButtons });
347
+ if (!approvalIdentity || approvalIdentity === '{"message":"","buttons":[]}') return false;
348
+ const fingerprint = [
349
+ args.meshId,
350
+ 'agent:waiting_approval',
351
+ args.sessionId,
352
+ args.providerType || '',
353
+ approvalIdentity,
354
+ ].join('::');
355
+ if (hasFingerprintSeen(args.meshId, fingerprint)) return true;
356
+ recordFingerprintSeen(args.meshId, fingerprint);
357
+ return false;
358
+ }
359
+
360
+ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
361
+ const jobId = readRefineJobId({ metadataEvent });
362
+ const fingerprint = jobId && new Set(['refine:completed', 'refine:failed']).has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
363
+ if (!fingerprint) return false;
364
+ if (hasFingerprintSeen(meshId, fingerprint)) return true;
365
+ recordFingerprintSeen(meshId, fingerprint);
366
+ return false;
367
+ }
368
+
369
+ // NOTIF-MISS (FIX 2): the set of `source` tags the transcript-reconcile synthesis path stamps
370
+ // onto the terminal ledger entry it writes (mesh-events-stale.reconcileDirectDispatchCompletion
371
+ // FromTranscript). A terminal carrying one of these was NOT produced by an actual provider
372
+ // agent_status_event — it is a coordinator-side reconstruction from the worker's transcript. The
373
+ // authoritative real completion must never be permanently masked by such a synthesized record.
374
+ const RECONCILED_COMPLETION_SOURCES = new Set([
375
+ 'direct_task_transcript_reconciliation',
376
+ 'daemon_reconcile_transcript_completion',
377
+ 'mcp_mesh_status_transcript_reconciliation',
378
+ 'no_progress_reconciliation',
379
+ ]);
380
+
381
+ // True when the recorded terminal ledger entry was SYNTHESIZED by transcript reconciliation
382
+ // rather than emitted by the real provider event. The reconcile path tags its ledger payload
383
+ // with a reconcile `source`; a genuine provider completion carries no such tag (or a normal
384
+ // completionDiagnostic.reason). Both the explicit source AND the diagnostic reason are checked so
385
+ // either carrier identifies the synthesized record.
386
+ function isSynthesizedReconciledTerminal(terminalPayload: Record<string, unknown>): boolean {
387
+ const source = readNonEmptyString(terminalPayload.source);
388
+ if (source && RECONCILED_COMPLETION_SOURCES.has(source)) return true;
389
+ const diagnostic = terminalPayload.completionDiagnostic;
390
+ if (diagnostic && typeof diagnostic === 'object' && !Array.isArray(diagnostic)) {
391
+ const reason = readNonEmptyString((diagnostic as Record<string, unknown>).reason);
392
+ if (reason && RECONCILED_COMPLETION_SOURCES.has(reason)) return true;
393
+ }
394
+ return false;
395
+ }
396
+
397
+ // True when the INCOMING completion event is a REAL provider agent_status_event (an actual
398
+ // agent:generating_completed from the live session) rather than itself a synthesized/reconciled
399
+ // re-injection. We must only let a real event override a synthesized terminal — a second
400
+ // synthesized re-arrival should still dedup normally. A real provider event carries no reconcile
401
+ // `source` tag; a re-injected reconciliation does.
402
+ function isRealProviderCompletionEvent(metadataEvent: Record<string, unknown>): boolean {
403
+ const source = readNonEmptyString(metadataEvent.source);
404
+ return !source || !RECONCILED_COMPLETION_SOURCES.has(source);
405
+ }
406
+
407
+ // The genuine-completion counterpart: a real final summary / worker result is present and
408
+ // the completion is not flagged as a missing-final-assistant false idle. Used to decide
409
+ // whether a new completion may supersede a prior WEAK (false-idle) terminal. NOTE this gates
410
+ // only on isFalseIdleCompletion (the completionDiagnostic subset), NOT the broader
411
+ // isWeakCompletionEvidence — preserving the original semantics where a self-declared
412
+ // weak/insufficient event still counts as genuine if it carries a real final summary.
413
+ function isGenuineCompletionEvidence(metadataEvent: Record<string, unknown>): boolean {
414
+ if (isFalseIdleCompletion(metadataEvent)) return false;
415
+ return !!readWorkerResultMetadata(metadataEvent) || !!readNonEmptyString(metadataEvent.finalSummary);
416
+ }
417
+
418
+ // (FALSEIDLE-BGCHILD-b) A later genuine completion of the SAME task that carries a
419
+ // substantively different — and fuller — final summary than the recorded terminal is the REAL
420
+ // final that an earlier (false-idle) completion pre-empted, not a duplicate. The background-child
421
+ // false idle is the nasty case the plain isWeakCompletionEvidence supersession misses: the
422
+ // early completion's screen parser DID see a prior/intermediate standard assistant, so it is
423
+ // recorded as a STRONG terminal with a non-empty (but truncated) finalSummary. Without this the
424
+ // providerSessionId/finalSummary dedup below swallows the genuine final and the coordinator is
425
+ // stuck with the truncated mid-turn text forever (the one-shot-consumption symptom). Same-task,
426
+ // new event is genuine, prior terminal summary is a strict prefix of (or otherwise shorter than)
427
+ // the new one → treat as the corrected final and let it through. Conservative: requires the new
428
+ // summary to be genuine evidence AND meaningfully longer, so an identical re-arrival or a SHORTER
429
+ // later summary is still deduped.
430
+ function supersedesTruncatedTerminalSummary(args: {
431
+ terminalPayload: Record<string, unknown>;
432
+ metadataEvent: Record<string, unknown>;
433
+ terminalTaskId: string;
434
+ eventTaskId: string;
435
+ }): boolean {
436
+ // Only applies when both name the SAME task (a distinct task is handled by distinctTaskCompletion).
437
+ if (!args.terminalTaskId || !args.eventTaskId || args.terminalTaskId !== args.eventTaskId) return false;
438
+ if (!isGenuineCompletionEvidence(args.metadataEvent)) return false;
439
+ const terminalSummary = readNonEmptyString(args.terminalPayload.finalSummary);
440
+ const eventSummary = readNonEmptyString(args.metadataEvent.finalSummary);
441
+ if (!eventSummary) return false;
442
+ // Identical text → genuine duplicate, keep deduping.
443
+ if (terminalSummary === eventSummary) return false;
444
+ // The recorded terminal was a known-weak (false-idle) one → already handled by the weak
445
+ // supersession path; nothing extra to do here.
446
+ if (isWeakCompletionEvidence(args.terminalPayload)) return false;
447
+ // No prior summary at all, or the new summary strictly extends / is meaningfully longer than
448
+ // the recorded one → the recorded terminal was the truncated pre-emption; supersede it.
449
+ if (!terminalSummary) return true;
450
+ if (eventSummary.startsWith(terminalSummary)) return true;
451
+ return eventSummary.length > terminalSummary.length + 32;
452
+ }
453
+
454
+ // The latest still-active direct-dispatch taskId for a session, resolved BEFORE the
455
+ // completion flips the dispatch row terminal. Direct dispatches (mesh_send_task) have no
456
+ // work-queue row, so this is the only taskId available to attribute the terminal ledger
457
+ // entry (and thus mesh task-stats) to — without it the terminal carries no taskId and the
458
+ // task surfaces as status='unknown' / terminalKind=null in computeMeshTaskStats.
459
+ function resolveActiveDirectDispatchTaskId(meshId: string, sessionId: string): string | undefined {
460
+ try {
461
+ // Session ids are single-form; sessionIdsEquivalent is the one canonical
462
+ // exact-match predicate — no node-id-style form normalization needed.
463
+ const matches = getActiveDirectDispatches(meshId).filter(d => sessionIdsEquivalent(d.sessionId, sessionId));
464
+ if (!matches.length) return undefined;
465
+ // getActiveDirectDispatches returns rows ordered by dispatched_at ASC; the last is
466
+ // the most recent dispatch (the re-dispatch / nudge whose completion this is).
467
+ return readNonEmptyString(matches[matches.length - 1].taskId) || undefined;
468
+ } catch {
469
+ return undefined;
470
+ }
471
+ }
472
+
473
+ // Coordinator-side suppression/reconcile gate for an incoming mesh event. Each clause is a
474
+ // closed dedup/suppression concern that only inspects the event + already-resolved context and
475
+ // either (a) returns a `suppress` result the caller forwards verbatim, (b) returns a `reconcile`
476
+ // signal carrying the rewritten metadataEvent for the caller to re-inject as
477
+ // agent:generating_completed, or (c) returns null to let the event fall through to the
478
+ // terminal/ledger machinery. Extracted verbatim from injectMeshSystemMessage — no behavior
479
+ // change; the only side effects (best-effort remote-idle cleanup, LOG, trace) fire on the same
480
+ // paths as before.
481
+ function evaluateMeshEventSuppression(
482
+ args: {
483
+ meshId: string;
484
+ sourceInstanceId?: string;
485
+ nodeId?: string;
486
+ nodeLabel: string;
487
+ event: string;
488
+ metadataEvent: Record<string, unknown>;
489
+ },
490
+ ctx: {
491
+ traceCtx: Parameters<typeof traceMeshEventDrop>[1];
492
+ eventSessionId: string;
493
+ eventNodeId: string;
494
+ eventTimestamp: number | null;
495
+ workerCoordinatorDaemonId: string | undefined;
496
+ },
497
+ ):
498
+ | { kind: 'suppress'; result: { success: true; forwarded: 0; suppressed: true; [extra: string]: unknown } }
499
+ | { kind: 'reconcile'; metadataEvent: Record<string, unknown> }
500
+ | null {
501
+ const { traceCtx, eventSessionId, eventNodeId, eventTimestamp, workerCoordinatorDaemonId } = ctx;
502
+
503
+ const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
504
+ event: args.event,
505
+ meshId: args.meshId,
506
+ metadataEvent: args.metadataEvent,
507
+ sessionId: eventSessionId || undefined,
508
+ nodeId: eventNodeId || undefined,
509
+ });
510
+ if (intentionalCleanupStop) {
511
+ if (eventSessionId && eventNodeId) {
512
+ try {
513
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, eventNodeId, eventSessionId);
514
+ } catch { /* best-effort */ }
515
+ }
516
+ LOG.info('MeshEvents', `Suppressed ${args.event} for intentionally cleanup-stopped session ${eventSessionId || '(unknown session)'}`);
517
+ traceMeshEventDrop('intentional_cleanup_stop', traceCtx);
518
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true } };
519
+ }
520
+
521
+ if (args.event === 'monitor:no_progress') {
522
+ const reconciledCompletion = buildNoProgressCompletionReconciliation({
523
+ meshId: args.meshId,
524
+ nodeId: args.nodeId,
525
+ nodeLabel: args.nodeLabel,
526
+ metadataEvent: args.metadataEvent,
527
+ sourceInstanceId: args.sourceInstanceId,
528
+ });
529
+ if (reconciledCompletion?.source === 'no_progress_reconciliation') {
530
+ LOG.info('MeshEvents', `Reconciled no-progress monitor to completion for session ${eventSessionId || '(unknown session)'}`);
531
+ return { kind: 'reconcile', metadataEvent: reconciledCompletion };
532
+ }
533
+ if (reconciledCompletion?.source === 'no_progress_terminal_ledger_suppression') {
534
+ LOG.info('MeshEvents', `Suppressed no-progress monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
535
+ traceMeshEventDrop('no_progress_terminal_ledger_suppression', traceCtx, `terminalKind=${reconciledCompletion.terminalLedgerKind}`);
536
+ return {
537
+ kind: 'suppress',
538
+ result: {
539
+ success: true,
540
+ forwarded: 0,
541
+ suppressed: true,
542
+ terminalLedgerEvidence: true,
543
+ terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
544
+ },
545
+ };
546
+ }
547
+ }
548
+
549
+ if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
550
+ LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
551
+ traceMeshEventDrop('duplicate_refine_terminal', traceCtx);
552
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true } };
553
+ }
554
+
555
+ if (args.event === 'agent:waiting_approval' && eventSessionId) {
556
+ const duplicateApproval = isDuplicateMeshApprovalEvent({
557
+ meshId: args.meshId,
558
+ sessionId: eventSessionId,
559
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
560
+ timestamp: eventTimestamp,
561
+ modalMessage: readNonEmptyString(args.metadataEvent.modalMessage) || undefined,
562
+ modalButtons: args.metadataEvent.modalButtons,
563
+ });
564
+ if (duplicateApproval) {
565
+ LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
566
+ traceMeshEventDrop('duplicate_approval', traceCtx);
567
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateApproval: true } };
568
+ }
569
+ }
570
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
571
+ const terminal = findRecentTerminalLedgerEvidence({
572
+ meshId: args.meshId,
573
+ sessionId: eventSessionId,
574
+ nodeId: eventNodeId || undefined,
575
+ });
576
+ if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
577
+ const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
578
+ // Fix B (re-dispatch 2nd-completion routing): a prior terminal recorded from a FALSE
579
+ // idle (weak evidence / no confirmed final assistant) must NOT permanently suppress a
580
+ // later GENUINE completion of the same session. providerSessionId is stable across a
581
+ // session's turns, so the providerSessionId/finalSummary dedup below would otherwise
582
+ // swallow the real 2nd-turn completion that a coordinator nudge (direct re-dispatch)
583
+ // drove — exactly the missed-event bug. When the prior terminal was weak and the new
584
+ // event carries genuine completion evidence, let it through so it is recorded and
585
+ // re-attributed to the latest task (the normal task_completed path below).
586
+ const supersedesWeakTerminal = isWeakCompletionEvidence(terminal.payload)
587
+ && isGenuineCompletionEvidence(args.metadataEvent);
588
+ // CANON-B (direct-dispatch completion race): a FAST direct dispatch (mesh_send_task)
589
+ // to an already-idle, previously-used session can have its genuine completion reach
590
+ // this coordinator handler BEFORE the dispatching side records the new task's dispatch
591
+ // row / task_dispatched ledger entry — insertDirectDispatch + appendLedgerEntry both run
592
+ // AFTER the agent_command await resolves, while the worker may already be done. In that
593
+ // window sessionHasActiveAssignment is false (no active dispatch row, no unterminal
594
+ // ledger entry yet), so this prior-terminal dedup engages; and because providerSessionId
595
+ // is STABLE across a reused session's turns, the providerSessionId/finalSummary match
596
+ // below would suppress the NEW task's completion as a duplicate of the PRIOR task —
597
+ // silently losing it (the observed intermittent miss; fresh enqueue/autoLaunch is immune
598
+ // because a fresh session has no prior same-providerSessionId terminal and the queue row
599
+ // is claimed atomically before dispatch). The echoed taskId is the authoritative
600
+ // discriminator: when the completion names a DIFFERENT task than the recorded terminal,
601
+ // it is a genuinely new task's completion, never a duplicate — let it through so it is
602
+ // attributed to its own taskId. A same-task re-arrival (taskId equal) or a taskId-less
603
+ // legacy event still falls through to the providerSessionId/finalSummary dedup.
604
+ const terminalTaskId = readNonEmptyString(terminal.payload.taskId);
605
+ const eventTaskId = readNonEmptyString(args.metadataEvent.taskId);
606
+ const distinctTaskCompletion = !!eventTaskId && !!terminalTaskId && eventTaskId !== terminalTaskId;
607
+ // (FALSEIDLE-BGCHILD-b) Same-task genuine completion carrying a fuller summary than the
608
+ // recorded (truncated, false-idle-pre-empted) terminal supersedes it — see helper.
609
+ const supersedesTruncatedTerminal = supersedesTruncatedTerminalSummary({
610
+ terminalPayload: terminal.payload,
611
+ metadataEvent: args.metadataEvent,
612
+ terminalTaskId,
613
+ eventTaskId,
614
+ });
615
+ // NOTIF-MISS (FIX 2): the recorded terminal was SYNTHESIZED by transcript reconciliation
616
+ // (no real provider event), and THIS incoming event is the authoritative REAL provider
617
+ // completion. The reconcile path may fire ~1s after a direct dispatch from a reused
618
+ // session's stale transcript tail, writing a synthesized terminal whose providerSessionId
619
+ // (stable across a session's turns) and/or finalSummary then match the real completion —
620
+ // so the providerSessionId/finalSummary dedup below would drop the genuine event and the
621
+ // coordinator would never learn the task finished. A synthesized record must NEVER
622
+ // permanently mask the real completion: let the real event through (it is re-recorded by
623
+ // the normal task_completed path, superseding the synthesized one). A second SYNTHESIZED
624
+ // re-arrival is NOT a real event and still dedups normally.
625
+ // RECONCILE-SYNTH-PREEMPTS-COMPLETION: the bar here is deliberately LOWER than the
626
+ // weak/truncated supersessions above — a synthesized terminal is a coordinator-side
627
+ // reconstruction, never a real provider event, so ANY genuine real provider completion
628
+ // for the session must win over it. Requiring the full isGenuineCompletionEvidence
629
+ // (finalSummary OR workerResult present) dropped the real event whenever the relay did
630
+ // not re-populate finalSummary at this layer — exactly the observed 71s task whose real
631
+ // generating_completed hit drop:duplicate_completion_terminal_ledger after a premature
632
+ // synth. We require only that the incoming event is a REAL provider completion that is
633
+ // not itself a false idle (missing-final-assistant); a real-but-false-idle event still
634
+ // does not supersede (it is not trustworthy terminal evidence either).
635
+ const supersedesSynthesizedTerminal = isSynthesizedReconciledTerminal(terminal.payload)
636
+ && isRealProviderCompletionEvent(args.metadataEvent)
637
+ && !isFalseIdleCompletion(args.metadataEvent);
638
+ if (!newDispatchAfterTerminal && !supersedesWeakTerminal && !distinctTaskCompletion && !supersedesTruncatedTerminal && !supersedesSynthesizedTerminal) {
639
+ const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
640
+ const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
641
+ const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
642
+ const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
643
+ if (
644
+ (terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
645
+ || (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
646
+ || args.metadataEvent.source === 'no_progress_reconciliation'
647
+ ) {
648
+ LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
649
+ traceMeshEventDrop('duplicate_completion_terminal_ledger', traceCtx);
650
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true } };
651
+ }
652
+ }
653
+ }
654
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
655
+ meshId: args.meshId,
656
+ event: args.event,
657
+ sessionId: eventSessionId,
658
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
659
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
660
+ timestamp: eventTimestamp,
661
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
662
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
663
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
664
+ nodeId: eventNodeId || undefined,
665
+ });
666
+ if (duplicateCompletion) {
667
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
668
+ traceMeshEventDrop('duplicate_completion', traceCtx);
669
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true } };
670
+ }
671
+ }
672
+ if (args.event === 'agent:stopped' && eventSessionId) {
673
+ const duplicateStopped = isDuplicateMeshCompletionEvent({
674
+ meshId: args.meshId,
675
+ event: args.event,
676
+ sessionId: eventSessionId,
677
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
678
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
679
+ timestamp: eventTimestamp,
680
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
681
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
682
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
683
+ nodeId: eventNodeId || undefined,
684
+ });
685
+ if (duplicateStopped) {
686
+ LOG.info('MeshEvents', `Suppressed duplicate stopped event for mesh ${args.meshId} session ${eventSessionId}`);
687
+ traceMeshEventDrop('duplicate_stopped', traceCtx);
688
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateStopped: true } };
689
+ }
690
+ }
691
+
692
+ return null;
693
+ }
694
+
695
+ /**
696
+ * True when the worker session that emitted this event has auto-approve enabled
697
+ * (a MAGI/delegated worker is launched with autoApprove:true). Such a worker
698
+ * resolves its own approval modals locally, so an agent:waiting_approval event
699
+ * from it is transient noise for the coordinator: forwarding it injects a
700
+ * "[System] … is waiting for approval, use mesh_approve" turn that the
701
+ * coordinator cannot usefully act on (the modal is already auto-resolving) and,
702
+ * mid-MAGI-collect, HIJACKS the coordinator's synthesis turn — the observed
703
+ * failure where a replica's repeated auto-approvals drowned out the completion
704
+ * events and the coordinator answered about approvals instead of the RCA. Only
705
+ * suppress when we can positively confirm the source worker auto-approves; a
706
+ * worker that genuinely needs a human/coordinator approval (autoApprove off)
707
+ * still forwards so the coordinator is told.
708
+ */
709
+ function sourceWorkerAutoApproves(components: DaemonComponents, sessionId: string): boolean {
710
+ if (!sessionId) return false;
711
+ try {
712
+ const state = components.instanceManager?.getInstance?.(sessionId)?.getState?.();
713
+ const settings = (state?.settings as Record<string, unknown>) || {};
714
+ return settings.autoApprove === true;
715
+ } catch {
716
+ return false;
717
+ }
718
+ }
719
+
720
+ /**
721
+ * REDRIVE-DUP: stop a worker session that started a STALE (reclaimed) mesh dispatch,
722
+ * so it discards the reclaimed task before it double-executes it. Prefers the local
723
+ * transport when the session's adapter lives on this daemon; otherwise forwards a
724
+ * `stop_cli` to the worker node's daemon over P2P (best-effort — a failed stop only
725
+ * loses the belt-and-suspenders stop; the ack was already rejected, so the coordinator
726
+ * never treats the stale run as the authoritative execution).
727
+ */
728
+ function stopStaleMeshWorker(
729
+ components: DaemonComponents,
730
+ args: { meshId: string; sessionId: string; nodeId?: string; providerType?: string; daemonId?: string },
731
+ ): void {
732
+ const { meshId, sessionId, providerType } = args;
733
+ const stopArgs: Record<string, unknown> = {
734
+ targetSessionId: sessionId,
735
+ ...(providerType ? { cliType: providerType } : {}),
736
+ mode: 'hard',
737
+ reason: 'stale_mesh_dispatch_reclaimed',
738
+ };
739
+ try {
740
+ const isLocal = components.cliManager?.adapters?.has?.(sessionId) === true;
741
+ if (isLocal) {
742
+ // cliType is required by stop_cli; resolve it from the local adapter when the
743
+ // event carried no providerType.
744
+ if (!stopArgs.cliType) {
745
+ const localType = components.cliManager?.adapters?.get?.(sessionId)?.cliType;
746
+ if (localType) stopArgs.cliType = localType;
747
+ }
748
+ Promise.resolve(components.cliManager?.handleCliCommand?.('stop_cli', stopArgs))
749
+ .catch((e: any) => LOG.warn('MeshQueue', `Local stop of stale worker ${sessionId} failed: ${e?.message || e}`));
750
+ return;
751
+ }
752
+ // Remote: resolve the worker node's daemon id (event metadata first, then the mesh node).
753
+ let daemonId = args.daemonId;
754
+ if (!daemonId && args.nodeId) {
755
+ try {
756
+ const mesh = getMeshWithCache(components, meshId);
757
+ const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, args.nodeId!));
758
+ daemonId = node ? readMeshNodeDaemonId(node) || undefined : undefined;
759
+ } catch { /* best-effort */ }
760
+ }
761
+ if (daemonId && components.dispatchMeshCommand) {
762
+ Promise.resolve(components.dispatchMeshCommand(daemonId, 'stop_cli', stopArgs))
763
+ .catch((e: any) => LOG.warn('MeshQueue', `Remote stop of stale worker ${sessionId} on daemon ${daemonId} failed: ${e?.message || e}`));
764
+ } else {
765
+ LOG.warn('MeshQueue', `Cannot stop stale worker ${sessionId}: no local adapter and no resolvable remote daemon id (node ${args.nodeId ?? '?'}). Ack already rejected — task will re-strand-and-fail if the worker completes.`);
766
+ }
767
+ } catch (e: any) {
768
+ LOG.warn('MeshQueue', `stopStaleMeshWorker error for ${sessionId}: ${e?.message || e}`);
769
+ }
770
+ }
771
+
772
+ function injectMeshSystemMessage(components: DaemonComponents, args: {
773
+ meshId: string;
774
+ sourceInstanceId?: string;
775
+ nodeId?: string;
776
+ nodeLabel: string;
777
+ event: string;
778
+ metadataEvent: Record<string, unknown>;
779
+ // T4 (B3b): v2 envelope restored from a remote (P2P) relay's flat payload. Only the
780
+ // relay path (handleMeshForwardEvent) sets it; the in-process forward path leaves it
781
+ // undefined and the local emit stamp applies as usual. When present with a preserved
782
+ // eventId, it is spread onto the re-queued pending event so stampPendingEventV2's
783
+ // already-stamped short-circuit keeps the ORIGINAL eventId (cross-machine idempotency).
784
+ v2Envelope?: Partial<Pick<PendingMeshCoordinatorEvent,
785
+ 'protocolVersion' | 'eventId' | 'scope' | 'dispatchedBy' | 'intendedFor'>>;
786
+ }) {
787
+ const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
788
+ const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
789
+
790
+ // EVTTRACE correlation context for this event's coordinator-side lifecycle (queue /
791
+ // dedup / suppress). Observation only — never read by any decision below.
792
+ const traceCtx = {
793
+ taskId: args.metadataEvent.taskId,
794
+ sessionId: eventSessionId,
795
+ nodeId: eventNodeId,
796
+ meshId: args.meshId,
797
+ event: args.event,
798
+ };
799
+
800
+ const sourceSession = args.sourceInstanceId
801
+ ? components.instanceManager.getInstance(args.sourceInstanceId)
802
+ : undefined;
803
+ const workerCoordinatorDaemonId = readNonEmptyString(
804
+ (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
805
+ );
806
+ // Session-level routing anchor (multi-coordinator). Prefer the LIVE worker session's
807
+ // stamp; fall back to a relayed value carried in metadataEvent.meshCoordinatorSessionId
808
+ // (a remote worker's completion arrives via handleMeshForwardEvent with no local
809
+ // sourceSession, so the stamp can only ride in the relayed metadata). Empty on legacy /
810
+ // version-skewed dispatches → the event stays daemon-broadcast (no regression).
811
+ const workerCoordinatorSessionId = readNonEmptyString(
812
+ (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorSessionId,
813
+ ) || readNonEmptyString(args.metadataEvent.meshCoordinatorSessionId);
814
+
815
+ // T2: a summary-less completion (and any non-completion status-sync event) carries no
816
+ // assistant text on the event, so resolveMeshSurfacedSessionPreview had nothing to surface
817
+ // and the coordinator's inbox mirror stayed stuck on the first dispatched user task. When
818
+ // THIS daemon hosts the live worker instance (sourceSession present), derive the worker's
819
+ // latest display message straight from its transcript and attach it to the event as
820
+ // lastMessagePreview/lastMessageRole/lastMessageAt. resolveMeshSurfacedSessionPreview reads
821
+ // these as an assistant-only fallback; they also ride the pending-queue + P2P relay
822
+ // (handleMeshForwardEvent whitelist) so a remote coordinator can surface them. A remote
823
+ // coordinator has no local instance and keeps relying on the relayed fields — unchanged.
824
+ const enrichedMetadataEvent = ((): Record<string, unknown> => {
825
+ const last = sourceSession ? getLastDisplayMessage(sourceSession.getState()) : null;
826
+ const base = (!last || !last.preview)
827
+ ? args.metadataEvent
828
+ : {
829
+ ...args.metadataEvent,
830
+ lastMessagePreview: last.preview,
831
+ lastMessageRole: last.role,
832
+ ...(last.receivedAt > 0 ? { lastMessageAt: last.receivedAt } : {}),
833
+ };
834
+ // MAGI: stamp the queue task's consensusGroupId onto the completion metadata
835
+ // so the intentional-fan-out dedup exemption (buildPendingEventFingerprint)
836
+ // can see it. The work queue is owned by THIS host/coordinator daemon — where
837
+ // both the lookup and the dedup run — so the local lookup covers local and
838
+ // relayed workers alike. Best-effort: never fail the event path on a miss, and
839
+ // never clobber a consensusGroupId the worker already relayed.
840
+ if (readNonEmptyString((base as Record<string, unknown>).consensusGroupId)) return base;
841
+ const eventTaskId = readNonEmptyString(args.metadataEvent.taskId);
842
+ if (!eventTaskId) return base;
843
+ try {
844
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(args.meshId, eventTaskId);
845
+ const consensusGroupId = readNonEmptyString((entry as { consensusGroupId?: unknown } | null)?.consensusGroupId);
846
+ if (consensusGroupId) return { ...base, consensusGroupId };
847
+ } catch { /* queue lookup is best-effort; absence just falls back to the generic fingerprint */ }
848
+ return base;
849
+ })();
850
+
851
+ // R2: cloud P2P dashboard metadata sync. The cloud daemon used to do this from its own
852
+ // relay listener; now the single core forwarder invokes the injected hook (no-op on
853
+ // standalone) so the event path stays single-listener and the local code path is identical
854
+ // across standalone and cloud.
855
+ if (components.onMeshCoordinatorEventForwarded) {
856
+ try {
857
+ // T: the coordinator surfaces a remote worker's session but holds no local
858
+ // instance for it, so the status snapshot can't derive a preview and the
859
+ // mirror would stay stuck on the first dispatched user task. Resolve the
860
+ // worker's latest assistant reply (carried on the completion event's
861
+ // finalSummary / workerResult) into a preview the mirror can stamp, so the
862
+ // mobile inbox reflects the assistant response. Completion events carry assistant
863
+ // text as finalSummary; a summary-less completion / status sync falls back to the
864
+ // worker's latest assistant display message (enrichedMetadataEvent.lastMessage*).
865
+ // For a mid-turn user-only event this is undefined and the prior surfaced preview
866
+ // is preserved downstream (no clobber).
867
+ const surfacedPreview = resolveMeshSurfacedSessionPreview(enrichedMetadataEvent);
868
+ components.onMeshCoordinatorEventForwarded({
869
+ event: args.event,
870
+ meshId: args.meshId,
871
+ nodeId: eventNodeId || undefined,
872
+ ...enrichedMetadataEvent,
873
+ // Ensure a `workspace` field reaches updateMeshOwnedSession even when the
874
+ // worker provider event only carried `workspaceName`. The merge spread of
875
+ // metadataEvent above wins when it already has a non-empty `workspace`.
876
+ workspace: readNonEmptyString(args.metadataEvent.workspace)
877
+ || readNonEmptyString(args.metadataEvent.workspaceName)
878
+ || undefined,
879
+ ...(surfacedPreview ? {
880
+ meshSessionLastMessagePreview: surfacedPreview.preview,
881
+ meshSessionLastMessageRole: surfacedPreview.role,
882
+ meshSessionLastMessageAt: surfacedPreview.receivedAt || undefined,
883
+ } : {}),
884
+ });
885
+ } catch { /* dashboard metadata sync is best-effort */ }
886
+ }
887
+
888
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
889
+
890
+ // Auto-approving worker: never forward its approval prompts to the coordinator.
891
+ // The daemon resolves the modal locally, so the "[System] … waiting for
892
+ // approval" injection is pure noise that hijacks the coordinator's turn — the
893
+ // observed MAGI failure where a replica's repeated auto-approvals flooded the
894
+ // coordinator and derailed its final synthesis. A worker without auto-approve
895
+ // (genuinely blocked on a human/coordinator decision) still forwards.
896
+ if (args.event === 'agent:waiting_approval' && sourceWorkerAutoApproves(components, eventSessionId)) {
897
+ LOG.info('MeshEvents', `Suppressed agent:waiting_approval for auto-approving worker session ${eventSessionId || '(unknown)'} (mesh ${args.meshId}) — modal is resolved locally, coordinator not notified`);
898
+ traceMeshEventDrop('waiting_approval_auto_approving_worker', traceCtx);
899
+ return { success: true, forwarded: 0, suppressed: true, autoApprovingWorkerApproval: true };
900
+ }
901
+
902
+ // Coordinator-side dedup/suppression gate (extracted, behavior-preserving). A non-null
903
+ // outcome either short-circuits with a forwarded result or signals a no-progress→completion
904
+ // reconciliation that we re-inject; null lets the event fall through to the ledger machinery.
905
+ const suppression = evaluateMeshEventSuppression(args, {
906
+ traceCtx,
907
+ eventSessionId,
908
+ eventNodeId,
909
+ eventTimestamp,
910
+ workerCoordinatorDaemonId,
911
+ });
912
+ if (suppression) {
913
+ if (suppression.kind === 'reconcile') {
914
+ return injectMeshSystemMessage(components, {
915
+ ...args,
916
+ event: 'agent:generating_completed',
917
+ metadataEvent: suppression.metadataEvent,
918
+ });
919
+ }
920
+ return suppression.result;
921
+ }
922
+
923
+ function markSessionTerminal(sessionId: string, outcome: 'completed' | 'failed', occurredAtMs?: number | null, opts?: { tentativeIfDirect?: boolean }): { id?: string } | null {
924
+ // C2: prefer an exact taskId match when the completion event carries one —
925
+ // it's immune to coordinator↔worker clock skew that can hide the assigned row.
926
+ const eventTaskId = readNonEmptyString(args.metadataEvent.taskId) || undefined;
927
+ const task = updateSessionTaskStatus(args.meshId, sessionId, outcome, {
928
+ occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
929
+ taskId: eventTaskId,
930
+ });
931
+ // Fix A (early-terminal prevention): a false-idle completion (no confirmed final
932
+ // assistant) for a DIRECT dispatch — i.e. no work-queue row matched — must not flip the
933
+ // dispatch row terminal. Leaving it active lets the reconcile loop (PHASE 4) re-read the
934
+ // transcript and record the genuine completion once the worker truly finishes (commonly
935
+ // after a coordinator nudge / re-dispatch). A matched queue task, or a completion with
936
+ // genuine evidence, is marked terminal as before.
937
+ // WARMUPGAP: a no-taskId completion from a session that holds no active assignment is a
938
+ // pre-assignment warmup / ghost event (a worker spawns, idles, and emits idle→generating→
939
+ // completed before any task is dispatched, with meshActiveTaskId unset so the event carries
940
+ // no taskId). Letting it through would hit the session_id fallback in updateDirectDispatchStatus
941
+ // and flip a sibling/stale dispatch row this event does not own — the real task later lands on
942
+ // a corrupted row and never reaches completed. Skip the dispatch update for that case. A
943
+ // taskId-carrying completion (real task), or any completion whose session currently holds an
944
+ // active assignment (legacy/relayed worker), still flips as before.
945
+ const leaveDirectDispatchActive = (!task && opts?.tentativeIfDirect === true)
946
+ || (!eventTaskId && !sessionHasActiveAssignment(args.meshId, sessionId));
947
+ if (!leaveDirectDispatchActive) {
948
+ // CANON-B: flip the exact dispatch row the completion echoed its taskId for; the
949
+ // session_id fallback (no echoed taskId) still covers legacy/relayed workers.
950
+ updateDirectDispatchStatus(args.meshId, sessionId, outcome, eventTaskId);
951
+ }
952
+ markSessionDeliveriesTerminal(args.meshId, sessionId, outcome);
953
+ // COMPLETION-PROPAGATION F2 (double safety net): the flip found no matching assigned
954
+ // row (task === null) but the completion echoed a taskId AND a queue row for that id is
955
+ // STILL 'assigned'. This is the stranded flip-miss case F1's equivalence match is meant
956
+ // to reconcile — NOT a direct dispatch (which legitimately has no queue row and is
957
+ // covered by updateDirectDispatchStatus above). Record a terminal ledger entry keyed by
958
+ // the echoed taskId and release the single-flight lock, so the reconcile PHASE 2.5
959
+ // terminal-ledger branch (findTerminalLedgerEvidenceForTask by row.id) has a taskId-based
960
+ // path to flip the stranded row terminal even if the direct SQL flip could not resolve
961
+ // it, and a subsequent reclaim/requeue is not blocked by a stale in-flight mark. Gated
962
+ // to GENUINE terminals (a weak / false-idle completion is left for the transcript
963
+ // reconcile, matching the leaveDirectDispatchActive philosophy above).
964
+ const genuineTerminal = outcome === 'failed' || !isWeakCompletionEvidence(args.metadataEvent);
965
+ if (!task && eventTaskId && genuineTerminal) {
966
+ try {
967
+ const strandedRow = MeshRuntimeStore.getInstance().findQueueEntryById(args.meshId, eventTaskId);
968
+ if (strandedRow && strandedRow.status === 'assigned') {
969
+ endTaskDispatchInFlight(args.meshId, eventTaskId);
970
+ if (!findTerminalLedgerEvidenceForTask({ meshId: args.meshId, taskId: eventTaskId })) {
971
+ appendLedgerEntry(args.meshId, {
972
+ kind: outcome === 'completed' ? 'task_completed' : 'task_failed',
973
+ sessionId,
974
+ nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
975
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
976
+ payload: {
977
+ taskId: eventTaskId,
978
+ event: args.event,
979
+ source: 'flip_miss_safety_net',
980
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
981
+ },
982
+ });
983
+ }
984
+ }
985
+ } catch { /* best-effort safety net — never fail the completion path */ }
986
+ }
987
+ setImmediate(() => cleanupTerminalDirectDispatches());
988
+ return task ? { id: task.id } : null;
989
+ }
990
+
991
+ let completedTaskForLedger: { id?: string } | null = null;
992
+ // Fix B: direct-dispatch taskId used to attribute the terminal ledger entry when no
993
+ // work-queue row matches (resolved BEFORE markSessionTerminal flips the dispatch terminal).
994
+ let directDispatchTaskIdForLedger: string | undefined;
995
+ // BOOTSTRAP-MSG: whether a queued task already targets this node, so the
996
+ // worktree_bootstrap_complete [System] message reflects the auto-claim instead of
997
+ // advising a manual mesh_launch_session (which would spawn a duplicate session).
998
+ let worktreeHasQueuedTask = false;
999
+ if (args.event === 'agent:generating_completed') {
1000
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1001
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1002
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
1003
+
1004
+ if (sessionId) {
1005
+ // CANON-B / ARCH-REFACTOR R1: trust the taskId the completion echoed. With R1's
1006
+ // per-turn identity binding the worker stamps the COMPLETING turn's own taskId
1007
+ // (not the racy session scalar), so the echoed id is authoritative and this is
1008
+ // the path that should always be taken for an R1+ worker. The most-recent-by-
1009
+ // session heuristic (resolveActiveDirectDispatchTaskId) is retained ONLY as a
1010
+ // backward-compat fallback for legacy / version-skewed workers that carry no
1011
+ // taskId — it is the very re-derive R1 exists to make unnecessary, and must not
1012
+ // override a present echoed id, hence the `||` short-circuit order.
1013
+ directDispatchTaskIdForLedger = readNonEmptyString(args.metadataEvent.taskId)
1014
+ || resolveActiveDirectDispatchTaskId(args.meshId, sessionId);
1015
+ // A false-idle completion of a direct dispatch is recorded but kept tentative (the
1016
+ // dispatch row stays active for the reconcile fallback); a genuine completion is terminal.
1017
+ const isFalseIdle = isFalseIdleCompletion(args.metadataEvent);
1018
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed', eventTimestamp, { tentativeIfDirect: isFalseIdle });
1019
+ if (nodeId && providerType) {
1020
+ // OVEREAGER-REMOTE-IDLE (Defect A+B): re-register the now-idle remote session into
1021
+ // the remote-idle store, symmetric with the agent:ready branch. Previously
1022
+ // setRemoteIdleSession ran ONLY on agent:ready, while agent:generating_started
1023
+ // DELETES the entry — so the FIRST turn a remote worker runs permanently evicts it
1024
+ // from the store, and generating_completed never re-added it. A later
1025
+ // mesh_enqueue_task's triggerMeshQueue then read getRemoteIdleSessions() == 0
1026
+ // (remoteIdleSessionsChecked:0) for a session that is genuinely live-idle, needlessly
1027
+ // auto-launching a second worker (Defect A). Worse, the two idle-session sources then
1028
+ // disagreed: the enqueue drain saw 0 (store empty) and auto-launched + left the queue
1029
+ // task pending, while THIS completing session's runIdleMaintenanceThenAssignQueue
1030
+ // claimed the same still-pending row straight from SQL — so the task body injected into
1031
+ // BOTH the reused idle session and the auto-launched one (Defect B). Re-registering here
1032
+ // unifies the source: the next triggerMeshQueue drain sees the live idle session, reuses
1033
+ // it (claimed:true, no auto-launch), and injects exactly once. Skip a false-idle
1034
+ // (mid-turn / no-final-assistant) completion — that session is NOT genuinely idle.
1035
+ if (!isFalseIdle) {
1036
+ sweepExpiredRemoteIdleSessions();
1037
+ try {
1038
+ MeshRuntimeStore.getInstance().setRemoteIdleSession(args.meshId, nodeId, sessionId, providerType, Date.now() + REMOTE_IDLE_SESSION_TTL_MS);
1039
+ } catch { /* best-effort */ }
1040
+ setImmediate(() => {
1041
+ maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
1042
+ .finally(() => {
1043
+ try {
1044
+ // Claim for THIS session first; on success drop the just-registered
1045
+ // idle entry so the enqueue drain doesn't re-pick an already-busy session.
1046
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
1047
+ if (assigned) MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
1048
+ } catch (e: any) {
1049
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after completion for ${nodeId}: ${e?.message || e}`);
1050
+ }
1051
+ });
1052
+ });
1053
+ } else {
1054
+ runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
1055
+ }
1056
+ }
1057
+ // M1-3: wake dependents of the completed task. The maintenance path above
1058
+ // only assigns to the completing session; dependents may be claimable by
1059
+ // other idle sessions, so run a full queue trigger when any are waiting.
1060
+ const completedTaskId = completedTaskForLedger?.id;
1061
+ if (completedTaskId && hasPendingDependents(args.meshId, completedTaskId)) {
1062
+ setImmediate(() => {
1063
+ triggerMeshQueue(components, args.meshId).catch((e: any) => {
1064
+ LOG.warn('MeshQueue', `Dependent wake after task ${completedTaskId} failed: ${e?.message || e}`);
1065
+ });
1066
+ });
1067
+ }
1068
+ }
1069
+ } else if (args.event === 'agent:ready') {
1070
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1071
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1072
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
1073
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1074
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1075
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1076
+ const hasCompletionEvidence = !!finalSummary || !!workerResult;
1077
+ if (sessionId && hasCompletionEvidence) {
1078
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed');
1079
+ if (completedTaskForLedger) {
1080
+ try {
1081
+ appendLedgerEntry(args.meshId, {
1082
+ kind: 'task_completed',
1083
+ nodeId: nodeId || undefined,
1084
+ sessionId,
1085
+ providerType: providerType || undefined,
1086
+ payload: {
1087
+ event: args.event,
1088
+ nodeLabel: args.nodeLabel,
1089
+ taskId: completedTaskForLedger.id,
1090
+ completedViaReady: true,
1091
+ providerSessionId,
1092
+ finalSummary,
1093
+ workerResult,
1094
+ evidence: buildTaskCompletionEvidence({
1095
+ event: 'agent:ready',
1096
+ nodeId,
1097
+ sessionId,
1098
+ providerType: providerType || undefined,
1099
+ providerSessionId,
1100
+ finalSummary,
1101
+ workerResult,
1102
+ }),
1103
+ },
1104
+ });
1105
+ } catch (e: any) {
1106
+ LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
1107
+ }
1108
+ }
1109
+ }
1110
+
1111
+ if (sessionId && nodeId && providerType) {
1112
+ // WORKTREE-BOOTSTRAP-DISPATCH-RACE: a freshly cloned worktree session emits
1113
+ // agent:ready as soon as the CLI process reaches its idle prompt — which happens
1114
+ // BEFORE the worktree bootstrap (npm install + native-addon repair: node-datachannel,
1115
+ // better-sqlite3, ghostty-vt-node) has finished. Claiming a queued task on that early
1116
+ // ready dispatches work into a session whose runtime is half-built: the child daemon
1117
+ // dies loading the absent native addon → the worker session boots empty (no task ever
1118
+ // injected), the queue row reports assigned-but-dead, and the work silently leaks to /
1119
+ // gets re-routed onto the base node. Live evidence (the OPSRULES dispatch): node
1120
+ // node_6df455dd emitted agent:ready at 10:36:28 but worktree_bootstrap_complete only at
1121
+ // 10:36:56 — a 28s window in which a claim produced an empty session.
1122
+ //
1123
+ // Gate the claim on bootstrap state: if this node is a worktree whose bootstrap is still
1124
+ // 'running', register the idle session (so it stays a claim candidate) but DEFER the
1125
+ // claim. The claim re-fires on the next agent:ready / reconcile drain tick once bootstrap
1126
+ // reaches a terminal state. 'failed' is left to flow through unchanged — a failed
1127
+ // bootstrap surfaces its own coordinator event and a dispatch there fails loudly rather
1128
+ // than silently, which is the correct, visible behavior (deferring forever would hide it).
1129
+ let worktreeBootstrapPending = false;
1130
+ try {
1131
+ const mesh = getMeshWithCache(components, args.meshId);
1132
+ const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId)) as { worktreeBootstrap?: { status?: string } } | undefined;
1133
+ worktreeBootstrapPending = node?.worktreeBootstrap?.status === 'running';
1134
+ } catch { /* best-effort: unknown bootstrap state → do not defer (prior behavior) */ }
1135
+
1136
+ sweepExpiredRemoteIdleSessions();
1137
+ try {
1138
+ MeshRuntimeStore.getInstance().setRemoteIdleSession(args.meshId, nodeId, sessionId, providerType, Date.now() + REMOTE_IDLE_SESSION_TTL_MS);
1139
+ } catch { /* best-effort */ }
1140
+ if (worktreeBootstrapPending) {
1141
+ LOG.info('MeshQueue', `Deferring queue claim for worktree node ${nodeId} (${sessionId}): worktree bootstrap still running — early agent:ready precedes bootstrap completion; the idle session is registered and the claim will re-fire once bootstrap finishes (guards against dispatching into a half-built worktree → empty session / work leaking to the base node)`);
1142
+ } else {
1143
+ setImmediate(() => {
1144
+ maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
1145
+ .finally(() => {
1146
+ try {
1147
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
1148
+ if (assigned) MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
1149
+ } catch (e: any) {
1150
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
1151
+ }
1152
+ });
1153
+ });
1154
+ }
1155
+ }
1156
+ } else if (args.event === 'agent:generating_started') {
1157
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1158
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1159
+ if (sessionId && nodeId) {
1160
+ try {
1161
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
1162
+ } catch { /* best-effort */ }
1163
+ }
1164
+ if (sessionId) {
1165
+ // CANON-B: a generating_started that echoes its taskId acks exactly the dispatch
1166
+ // and the delivery for THAT task — not every in-flight dispatch/delivery on the
1167
+ // session. A session that already holds a freshly-dispatched (still 'dispatched')
1168
+ // sibling must keep that row 'dispatched' so its own confirm can match it; acking
1169
+ // by session would mark it 'acked' prematurely and hide a genuine non-delivery.
1170
+ const startedTaskId = readNonEmptyString(args.metadataEvent.taskId) || undefined;
1171
+ // REDRIVE-DUP: reject a STALE dispatch. When a delivered-but-unconsumed task is
1172
+ // reclaimed (reclaimStrandedAssignedTask) and re-dispatched to another node, the
1173
+ // ORIGINAL inject to the first node is not cancelled — it can still fire and make
1174
+ // that worker start the SAME taskId, double-executing it. The reclaim bumped the
1175
+ // task row's dispatchNonce, so the stranded inject's generating_started echoes a
1176
+ // nonce STRICTLY LESS than the row's current value. Detect that here: skip the ack
1177
+ // (do NOT resurrect the row onto this stale session) and stop the worker so it
1178
+ // discards the reclaimed task. A matching/greater nonce, or an absent nonce
1179
+ // (legacy worker), falls through to the normal ack — backward safe.
1180
+ const startedNonce = typeof args.metadataEvent.dispatchNonce === 'number'
1181
+ ? args.metadataEvent.dispatchNonce
1182
+ : undefined;
1183
+ if (startedTaskId && startedNonce !== undefined) {
1184
+ const currentRow = (() => {
1185
+ try { return MeshRuntimeStore.getInstance().findQueueEntryById(args.meshId, startedTaskId); }
1186
+ catch { return null; }
1187
+ })();
1188
+ const currentNonce = typeof currentRow?.dispatchNonce === 'number' ? currentRow.dispatchNonce : undefined;
1189
+ if (currentNonce !== undefined && startedNonce < currentNonce) {
1190
+ LOG.warn('MeshQueue', `Rejecting stale mesh dispatch: task ${startedTaskId} generating_started from session ${sessionId} `
1191
+ + `(node ${nodeId ?? '?'}) carries dispatchNonce ${startedNonce} < current ${currentNonce} — the task was reclaimed and `
1192
+ + `re-dispatched; stopping this worker to prevent duplicate execution.`);
1193
+ traceMeshEventDrop('stale_dispatch_nonce_rejected', {
1194
+ taskId: startedTaskId,
1195
+ sessionId,
1196
+ nodeId,
1197
+ meshId: args.meshId,
1198
+ event: 'agent:generating_started',
1199
+ }, `nonce ${startedNonce} < ${currentNonce}`);
1200
+ stopStaleMeshWorker(components, {
1201
+ meshId: args.meshId,
1202
+ sessionId,
1203
+ nodeId,
1204
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || readNonEmptyString(args.metadataEvent.cliType),
1205
+ daemonId: readNonEmptyString(args.metadataEvent.sourceDaemonId) || readNonEmptyString(args.metadataEvent.daemonId),
1206
+ });
1207
+ return { success: true, forwarded: 0, suppressed: true, staleDispatchRejected: true };
1208
+ }
1209
+ }
1210
+ // WARMUPGAP: only ack a dispatch row when the event names its task, or the session
1211
+ // currently holds an active assignment. A no-taskId generating_started from an
1212
+ // unassigned session is a pre-assignment warmup — the session_id fallback would ack a
1213
+ // sibling/stale dispatch row this event does not own, marking it 'acked' prematurely and
1214
+ // hiding a genuine non-delivery. Skip the dispatch ack for that ghost case (the delivery
1215
+ // acks below are bound to actual deliveries and stay a no-op for a warmup session).
1216
+ //
1217
+ // MESH-DISPATCH-MISROUTE (fix 3, consumer residual): when the event carries no taskId
1218
+ // (a legacy/relayed worker whose producer never stamped meshActiveTaskId) but the
1219
+ // session owns EXACTLY ONE active dispatch, resolve that row's taskId and flip it by PK
1220
+ // instead of the session_id sweep — the sweep flips every non-terminal row for the
1221
+ // session ("may flip a sibling dispatch row"). With ≥2 active rows the owner is
1222
+ // ambiguous, so resolvedAckTaskId stays undefined and we DROP the ack rather than
1223
+ // mis-flip a sibling (the genuine ack arrives once the producer/reconcile names a task).
1224
+ if (startedTaskId) {
1225
+ updateDirectDispatchStatus(args.meshId, sessionId, 'acked', startedTaskId);
1226
+ } else if (sessionHasActiveAssignment(args.meshId, sessionId)) {
1227
+ const soleTaskId = (() => {
1228
+ try { return MeshRuntimeStore.getInstance().getSoleActiveDirectDispatchTaskId(args.meshId, sessionId); }
1229
+ catch { return null; }
1230
+ })();
1231
+ if (soleTaskId) {
1232
+ updateDirectDispatchStatus(args.meshId, sessionId, 'acked', soleTaskId);
1233
+ }
1234
+ }
1235
+ const activeDeliveries = ((): { id: string; taskId: string | null }[] => {
1236
+ try { return MeshRuntimeStore.getInstance().getActiveSessionDeliveries(args.meshId, sessionId); }
1237
+ catch { return []; }
1238
+ })();
1239
+ const deliveriesToAck = startedTaskId
1240
+ ? activeDeliveries.filter(d => d.taskId === startedTaskId)
1241
+ : activeDeliveries;
1242
+ for (const d of deliveriesToAck) {
1243
+ updateSessionDeliveryStatus(d.id, 'acked');
1244
+ }
1245
+ }
1246
+ } else if (args.event === 'agent:stopped') {
1247
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1248
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1249
+ if (sessionId && nodeId) {
1250
+ try {
1251
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
1252
+ } catch { /* best-effort */ }
1253
+ }
1254
+ if (sessionId) {
1255
+ // CANON-B: prefer the echoed taskId; session heuristic is the fallback.
1256
+ directDispatchTaskIdForLedger = readNonEmptyString(args.metadataEvent.taskId)
1257
+ || resolveActiveDirectDispatchTaskId(args.meshId, sessionId);
1258
+ completedTaskForLedger = markSessionTerminal(sessionId, 'failed');
1259
+ }
1260
+ } else if (args.event === 'worktree_bootstrap_complete' || args.event === 'worktree_bootstrap_failed') {
1261
+ // WORKTREE-BOOTSTRAP-REFIRE: the agent:ready branch above DEFERS the queue claim while
1262
+ // worktreeBootstrap.status === 'running' — a freshly cloned worktree emits agent:ready at
1263
+ // its idle prompt BEFORE bootstrap finishes, and dispatching then produces a half-built
1264
+ // session. The defer registers the idle session (setRemoteIdleSession) but schedules NO
1265
+ // retry; it relies on "the next agent:ready / reconcile drain tick" to re-fire. But
1266
+ // agent:ready is a one-shot (emitAgentReadyOnce, guarded by agentReadyEmitted) and the
1267
+ // session stays idle→idle after bootstrap, so no new agent:ready edge ever fires → the
1268
+ // deferred claim is stranded → the worker session boots empty (totalMessages=0) and the
1269
+ // coordinator relaunch/stop-loops. Re-fire the queue drain on the terminal bootstrap
1270
+ // transition: the idle session registered at defer time is now claimable (tryAssignQueueTask
1271
+ // has no bootstrap gate of its own). This runs on whichever daemon processes the event —
1272
+ // the local worker (when it co-hosts the coordinator) at emit time, or the remote
1273
+ // coordinator after it pulls the queued event — so the registered local/remote idle session
1274
+ // is drained in every topology. 'failed' is re-fired too so a deferred-then-failed bootstrap
1275
+ // dispatches and fails loudly/visibly rather than stranding silently. Falls through to the
1276
+ // coordinator broadcast below — the bootstrap event is still delivered to the coordinator.
1277
+ // WORKTREE-BOOTSTRAP-COORD-STATE: stamp the terminal bootstrap status onto the
1278
+ // COORDINATOR's mesh view BEFORE re-firing the queue. The clone+bootstrap ran on
1279
+ // the worker daemon (clone_mesh_node forwards to the source node's machine), so
1280
+ // persistWorktreeSetupState only flipped status→'complete' on the worker's mesh
1281
+ // object — the coordinator still holds the 'running' state it stamped from the
1282
+ // forwarded clone reply. Without this, the claim gate (agent:ready defer above +
1283
+ // mesh-queue-assignment) reads getMeshWithCache, sees 'running' forever, and
1284
+ // defers every claim — so this very re-fire loops against a gate that never opens
1285
+ // (claim never lands; idle session re-registered each tick; auto-launch spawns a
1286
+ // fresh session every cycle → runaway worktree-session multiplication). Stamping
1287
+ // the terminal state opens the gate so the deferred claim lands on this re-fire.
1288
+ const bootstrapNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1289
+ if (bootstrapNodeId) {
1290
+ try {
1291
+ // Fix (3): pass the worktree path so a hydrate-on-miss upsert (when this
1292
+ // coordinator never received the clone reply) can seed an addressable node.
1293
+ const bootstrapWorkspace = readNonEmptyString(args.metadataEvent.worktreePath)
1294
+ || readNonEmptyString(args.metadataEvent.workspace);
1295
+ (components.router as any)?.markWorktreeBootstrapTerminalState?.(
1296
+ args.meshId,
1297
+ bootstrapNodeId,
1298
+ args.event === 'worktree_bootstrap_failed' ? 'failed' : 'complete',
1299
+ bootstrapWorkspace ? { workspace: bootstrapWorkspace } : undefined,
1300
+ );
1301
+ } catch (e: any) {
1302
+ LOG.warn('MeshQueue', `Failed to stamp terminal bootstrap state for ${bootstrapNodeId} (mesh ${args.meshId}): ${e?.message || e}`);
1303
+ }
1304
+ }
1305
+ // BOOTSTRAP-MSG: detect whether the queue re-fire below has a task to auto-claim for
1306
+ // this node BEFORE setImmediate(triggerMeshQueue) runs — at this point a deferred task
1307
+ // is still 'pending' (it has not been claimed yet); 'assigned' covers a task already
1308
+ // claimed by an earlier tick. Either case means a session is being/has been spun up by
1309
+ // the queue, so the completion message must NOT advise a manual mesh_launch_session.
1310
+ // Only meaningful for the 'complete' transition (a failed bootstrap claims nothing).
1311
+ if (args.event === 'worktree_bootstrap_complete' && bootstrapNodeId) {
1312
+ try {
1313
+ worktreeHasQueuedTask = getQueue(args.meshId, { status: ['pending', 'assigned'] })
1314
+ .some((task) => meshNodeIdMatches({ id: task.targetNodeId } as MeshNodeIdentified, bootstrapNodeId));
1315
+ } catch (e: any) {
1316
+ LOG.warn('MeshQueue', `Failed to check queued task for ${bootstrapNodeId} (mesh ${args.meshId}): ${e?.message || e}`);
1317
+ }
1318
+ }
1319
+ setImmediate(() => {
1320
+ triggerMeshQueue(components, args.meshId).catch((e: any) => {
1321
+ LOG.warn('MeshQueue', `Queue re-fire after ${args.event} failed (mesh ${args.meshId}): ${e?.message || e}`);
1322
+ });
1323
+ });
1324
+ }
1325
+
1326
+ const ledgerKind = EVENT_TO_LEDGER_KIND[args.event];
1327
+ if (ledgerKind) {
1328
+ try {
1329
+ const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
1330
+ const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
1331
+ const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
1332
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1333
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1334
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1335
+ const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
1336
+ ? buildTaskCompletionEvidence({
1337
+ event: 'agent:generating_completed',
1338
+ nodeId: ledgerNodeId,
1339
+ sessionId: ledgerSessionId,
1340
+ providerType: ledgerProviderType,
1341
+ providerSessionId,
1342
+ finalSummary,
1343
+ workerResult,
1344
+ })
1345
+ : undefined;
1346
+ appendLedgerEntry(args.meshId, {
1347
+ kind: ledgerKind,
1348
+ nodeId: ledgerNodeId,
1349
+ sessionId: ledgerSessionId,
1350
+ providerType: ledgerProviderType,
1351
+ payload: {
1352
+ event: args.event,
1353
+ nodeLabel: args.nodeLabel,
1354
+ // Fix B: fall back to the direct-dispatch taskId when no work-queue row
1355
+ // matched, so the terminal entry is attributable in mesh task-stats
1356
+ // (otherwise the direct task shows status='unknown' / terminalKind=null).
1357
+ taskId: completedTaskForLedger?.id || directDispatchTaskIdForLedger || undefined,
1358
+ providerSessionId,
1359
+ finalSummary,
1360
+ workerResult,
1361
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
1362
+ ? args.metadataEvent.completionDiagnostic
1363
+ : undefined,
1364
+ evidence: completionEvidence,
1365
+ // B2: evidenceLevel lets coordinator know when completion evidence is insufficient.
1366
+ // NOTIF Defect-2b: ONLY source==='default' (no parseable answer at all) is
1367
+ // 'insufficient'. 'parseable_answer' (a real JSON answer that just isn't
1368
+ // worker-result-shaped, e.g. a MAGI envelope) is concrete evidence and must
1369
+ // resolve to 'sufficient' — resolveWorkerResult now upgrades that case so a
1370
+ // complete, valid answer is no longer mislabelled insufficient/reviewRecommended.
1371
+ ...(completionEvidence
1372
+ ? completionEvidence.workerResult.source === 'default'
1373
+ ? { evidenceLevel: 'insufficient', reviewRecommended: true }
1374
+ : { evidenceLevel: 'sufficient' }
1375
+ : {}),
1376
+ },
1377
+ });
1378
+ } catch (e: any) {
1379
+ LOG.warn('MeshLedger', `Failed to record ${ledgerKind}: ${e?.message || e}`);
1380
+ }
1381
+ }
1382
+
1383
+ let recoveryContext: SessionRecoveryContext | null = null;
1384
+ if (args.event === 'agent:stopped') {
1385
+ try {
1386
+ const mesh = getMesh(args.meshId);
1387
+ const maxRetries = mesh?.policy?.maxTaskRetries ?? 1;
1388
+
1389
+ recoveryContext = getSessionRecoveryContext(args.meshId, {
1390
+ sessionId: resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined,
1391
+ nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
1392
+ maxRetries,
1393
+ });
1394
+ recoveryContext.failedProviderType = readNonEmptyString(args.metadataEvent.providerType) || null;
1395
+
1396
+ if (recoveryContext.retryRecommended && recoveryContext.consecutiveNodeFailures > 0) {
1397
+ appendLedgerEntry(args.meshId, {
1398
+ kind: 'recovery_attempted',
1399
+ nodeId: recoveryContext.failedNodeId || undefined,
1400
+ sessionId: recoveryContext.failedSessionId || undefined,
1401
+ providerType: recoveryContext.failedProviderType || undefined,
1402
+ payload: {
1403
+ consecutiveFailures: recoveryContext.consecutiveNodeFailures,
1404
+ taskAttemptCount: recoveryContext.taskAttemptCount,
1405
+ retryRecommended: recoveryContext.retryRecommended,
1406
+ advice: recoveryContext.advice,
1407
+ },
1408
+ });
1409
+
1410
+ if (recoveryContext.lastTaskMessage && recoveryContext.failedNodeId && recoveryContext.failedProviderType) {
1411
+ const autoNodeId = recoveryContext.failedNodeId;
1412
+ try {
1413
+ const task = enqueueTask(args.meshId, recoveryContext.lastTaskMessage, {
1414
+ targetNodeId: autoNodeId
1415
+ });
1416
+ LOG.info('MeshRecovery', `Auto-requeued failed task: ${task.id} for node ${autoNodeId}`);
1417
+
1418
+ const node = mesh?.nodes.find((n: any) => meshNodeIdMatches(n, autoNodeId));
1419
+ if (node) {
1420
+ components.cliManager.handleCliCommand('launch_cli', {
1421
+ cliType: recoveryContext.failedProviderType,
1422
+ dir: node.workspace,
1423
+ settings: {
1424
+ role: 'worker',
1425
+ meshNodeFor: args.meshId,
1426
+ meshNodeId: node.id,
1427
+ spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
1428
+ // Coordinator-dispatched recovery relaunch: same auto-approve
1429
+ // policy as the primary worker launch path.
1430
+ autoApprove: resolveDelegatedWorkerAutoApprove(mesh?.policy, node?.policy),
1431
+ launchedByCoordinator: true,
1432
+ }
1433
+ }).catch((e: any) => LOG.error('MeshRecovery', `Failed to auto-relaunch session for ${node.id}: ${e?.message}`));
1434
+ }
1435
+ } catch (e: any) {
1436
+ LOG.warn('MeshRecovery', `Failed to execute auto-recovery: ${e?.message}`);
1437
+ }
1438
+ }
1439
+ }
1440
+
1441
+ LOG.info('MeshRecovery', `Recovery context for ${args.nodeLabel}: ${recoveryContext.advice}`);
1442
+ } catch (e: any) {
1443
+ LOG.warn('MeshRecovery', `Failed to build recovery context: ${e?.message || e}`);
1444
+ }
1445
+ }
1446
+
1447
+ const messageText = buildMeshSystemMessage({
1448
+ event: args.event,
1449
+ nodeLabel: args.nodeLabel,
1450
+ metadataEvent: args.metadataEvent,
1451
+ recoveryContext,
1452
+ worktreeHasQueuedTask,
1453
+ });
1454
+ if (!messageText) {
1455
+ // Lifecycle events that carry no coordinator-facing message (agent:ready /
1456
+ // agent:generating_started) still drive the remote-claim state machine: the
1457
+ // coordinator's agent:ready branch above runs setRemoteIdleSession +
1458
+ // tryAssignQueueTask, and agent:generating_started clears the remote-idle entry.
1459
+ // For a LOCAL worker whose coordinator is a REMOTE daemon those side effects ran
1460
+ // on the wrong daemon (this worker's empty queue / store), so the coordinator never
1461
+ // learns the auto-launched session went idle and re-auto-launches it forever
1462
+ // (queue task stuck pending). Queue the silent event so the coordinator pulls it
1463
+ // (PHASE 1 pullRemoteNodeQueues → handleMeshForwardEvent) and re-runs the claim on
1464
+ // the daemon that actually owns the queue. Gate strictly on a present, REMOTE
1465
+ // coordinator daemon id: a co-located worker already ran the claim on the right
1466
+ // daemon, and a coordinator processing a *pulled* event has no sourceSession so
1467
+ // workerCoordinatorDaemonId is empty — neither re-queues, so there is no loop.
1468
+ const isSilentClaimRelevantEvent = args.event === 'agent:ready' || args.event === 'agent:generating_started';
1469
+ const coordinatorIsRemote = !!workerCoordinatorDaemonId
1470
+ && !resolveCoordinatorDrainDaemonIds(components).includes(workerCoordinatorDaemonId);
1471
+ if (!(isSilentClaimRelevantEvent && coordinatorIsRemote)) {
1472
+ return { success: false, error: 'unsupported mesh event' };
1473
+ }
1474
+ }
1475
+
1476
+ // ── Queue-only delivery (single-model: queue + periodic poll) ──────────────
1477
+ // Every mesh coordinator event — terminal or not, local-coordinator or
1478
+ // remote — is persisted to the pending-events queue (SQLite + JSONL) and
1479
+ // NOTHING is pushed here. The old spontaneous-forward paths were removed:
1480
+ // - F1 remote P2P `mesh_forward_event` dispatch (network/stamp-dependent,
1481
+ // silently dropped on P2P failure or missing meshCoordinatorDaemonId)
1482
+ // - F3 live-CLI PTY `send_message` fire-and-forget inject (silently
1483
+ // dropped when the coordinator was generating)
1484
+ // Delivery to a live CLI coordinator now happens via setupMeshReconcileLoop,
1485
+ // which drains this queue on a fixed interval and injects into the coordinator
1486
+ // only when it is idle. A pure stdio MCP (LLM) coordinator — which has no live
1487
+ // CLI session to inject into — drains the queue itself when it calls a mesh
1488
+ // tool (mesh_status / mesh_read_chat). Either way the queue is the single
1489
+ // source of truth and the only thing this function writes to.
1490
+ //
1491
+ // targetCoordinatorDaemonId scopes the event to a specific coordinator daemon
1492
+ // (unicast) when the worker carries one, so the reconcile loop on the right
1493
+ // daemon drains it and other daemons skip it. Absent → broadcast/backfill.
1494
+ const pendingEvent = {
1495
+ event: args.event,
1496
+ meshId: args.meshId,
1497
+ nodeLabel: args.nodeLabel,
1498
+ nodeId: args.nodeId || undefined,
1499
+ workspace: readNonEmptyString(args.metadataEvent.workspace)
1500
+ || readNonEmptyString(args.metadataEvent.workspaceName),
1501
+ metadataEvent: {
1502
+ ...enrichedMetadataEvent,
1503
+ ...(recoveryContext ? { recoveryContext } : {}),
1504
+ // Stash the coordinator session id INSIDE metadataEvent too, so it survives the
1505
+ // P2P relay serialization (buildForwardPayloadFromPending spreads metadata; the
1506
+ // handleMeshForwardEvent whitelist reads it back) — a top-level field alone would
1507
+ // be dropped when the event crosses a machine boundary.
1508
+ ...(workerCoordinatorSessionId ? { meshCoordinatorSessionId: workerCoordinatorSessionId } : {}),
1509
+ },
1510
+ // Silent lifecycle events (agent:ready / agent:generating_started) carry no
1511
+ // coordinator message; they are queued only so the coordinator re-runs the
1512
+ // remote-claim state machine on pull. injectPendingIntoCoordinator skips
1513
+ // entries without a coordinatorMessage, so a live CLI coordinator is not spammed.
1514
+ ...(messageText ? { coordinatorMessage: messageText } : {}),
1515
+ queuedAt: Date.now(),
1516
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
1517
+ // Top-level session anchor for the local PHASE 2 strict-match on the coordinator
1518
+ // daemon. Absent → daemon-level broadcast (legacy / single-coordinator path).
1519
+ ...(workerCoordinatorSessionId ? { targetCoordinatorSessionId: workerCoordinatorSessionId } : {}),
1520
+ // T4 (B3b): restore the v2 envelope from the remote relay so the re-queue keeps the
1521
+ // ORIGINAL eventId. Spread LAST so its authoritative eventId/scope/identity win over
1522
+ // any default. queuePendingMeshCoordinatorEvent → stampPendingEventV2 then no-ops
1523
+ // (already-stamped short-circuit) instead of minting a fresh eventId. Empty object
1524
+ // for a v1 relay → unchanged v1 emit-stamp path (version-skew safe).
1525
+ ...(args.v2Envelope ?? {}),
1526
+ };
1527
+ if (queuePendingMeshCoordinatorEvent(pendingEvent)) {
1528
+ LOG.info('MeshEvents', `Queued ${args.event} for coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''}${workerCoordinatorSessionId ? `, coordinator session ${workerCoordinatorSessionId}` : ''})`);
1529
+ // EVTTRACE: event persisted to the coordinator pending queue (awaiting reconcile drain).
1530
+ traceMeshEventStage('queued', traceCtx, workerCoordinatorDaemonId ? `coordinatorDaemon=${workerCoordinatorDaemonId}` : 'broadcast');
1531
+ } else {
1532
+ // EVTTRACE: queue rejected the event (dedup at queue time / persistence guard).
1533
+ traceMeshEventDrop('queue_dedup', traceCtx);
1534
+ }
1535
+ return { success: true, forwarded: 0 };
1536
+ }
1537
+
1538
+ // Reconstruct the metadataEvent that injectMeshSystemMessage consumes from a forwarded
1539
+ // (cross-machine) mesh event. The remote relay hop arrives as a flat payload, NOT the
1540
+ // original provider event object, so this whitelists the fields the coordinator-side
1541
+ // pipeline reads and re-projects them. Kept pure + exported so the relay-path field
1542
+ // preservation (esp. taskId) is unit-testable without driving injectMeshSystemMessage.
1543
+ //
1544
+ // IMPORTANT asymmetry: the LOCAL in-process forward path (onMeshCoordinatorEventForwarded)
1545
+ // passes the whole event through as metadataEvent, so every field on the event survives
1546
+ // there for free. This remote-only path must explicitly mirror each field it needs.
1547
+ export function buildRelayMetadataEvent(payload: Record<string, unknown>): Record<string, unknown> {
1548
+ const relayModalMessage = readNonEmptyString(payload.modalMessage);
1549
+ const relayModalButtons = Array.isArray(payload.modalButtons)
1550
+ ? (payload.modalButtons as unknown[]).filter((b): b is string => typeof b === 'string' && b.trim().length > 0)
1551
+ : null;
1552
+ return {
1553
+ // Preserve the dispatch task id across the machine boundary. The `received` trace
1554
+ // stage reads payload.taskId; without mirroring it here the rebuilt metadataEvent
1555
+ // loses it, so injectMeshSystemMessage's traceCtx.taskId and the
1556
+ // updateDirectDispatchStatus(eventTaskId) call go undefined — the EvtTrace
1557
+ // queued/surfaced stages show task=- and the direct-dispatch ledger falls back to a
1558
+ // session_id match (which can flip a sibling row). The local in-process forward path
1559
+ // keeps event.taskId/meshActiveTaskId for free; this mirrors it for the remote relay.
1560
+ // Same taskId/meshActiveTaskId ordering the local unroutable trace uses.
1561
+ taskId: readNonEmptyString(payload.taskId) || readNonEmptyString(payload.meshActiveTaskId),
1562
+ targetSessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId) || readNonEmptyString(payload.instanceId),
1563
+ providerType: readNonEmptyString(payload.providerType),
1564
+ providerSessionId: readNonEmptyString(payload.providerSessionId),
1565
+ // Preserve the originating coordinator SESSION id across the machine boundary so
1566
+ // the completion routes back to the exact coordinator session (multi-coordinator).
1567
+ // buildForwardPayloadFromPending spreads the worker event's metadata, so the id
1568
+ // arrives as payload.meshCoordinatorSessionId; the top-level targetCoordinatorSessionId
1569
+ // is also accepted as a fallback. injectMeshSystemMessage re-derives the routing
1570
+ // anchors from this. Absent → daemon-level fallback (version-skew safe).
1571
+ meshCoordinatorSessionId: readNonEmptyString(payload.meshCoordinatorSessionId) || readNonEmptyString(payload.targetCoordinatorSessionId),
1572
+ // Carry the session identity fields the worker provider event emits so the
1573
+ // coordinator's mirror (updateMeshOwnedSession) gets a real workspace/title/
1574
+ // settings. Without these the remote-relay hop reconstructs metadataEvent with
1575
+ // an empty workspace, and the dashboard flaps to the generic
1576
+ // "Terminal (Mesh Node)" title (and degrades the provider label) between live
1577
+ // events and the periodic get_status_metadata snapshot. The local in-process
1578
+ // forward path (onMeshCoordinatorEventForwarded) already preserves these; this
1579
+ // mirrors them for the remote-only relay path.
1580
+ workspace: readNonEmptyString(payload.workspace) || readNonEmptyString(payload.workspaceName),
1581
+ workspaceName: readNonEmptyString(payload.workspaceName) || readNonEmptyString(payload.workspace),
1582
+ sessionTitle: readNonEmptyString(payload.sessionTitle),
1583
+ sessionStatus: readNonEmptyString(payload.sessionStatus),
1584
+ sessionChatStatus: readNonEmptyString(payload.sessionChatStatus),
1585
+ providerName: readNonEmptyString(payload.providerName),
1586
+ ...(payload.sessionSettings && typeof payload.sessionSettings === 'object' && !Array.isArray(payload.sessionSettings) ? { sessionSettings: payload.sessionSettings } : {}),
1587
+ finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1588
+ // T2: carry the worker's status-snapshot last-message preview across the machine
1589
+ // boundary so a summary-less completion still surfaces the assistant reply in the
1590
+ // coordinator's inbox mirror. resolveMeshSurfacedSessionPreview reads these
1591
+ // (assistant-role only) when finalSummary is absent.
1592
+ lastMessagePreview: readNonEmptyString(payload.lastMessagePreview),
1593
+ lastMessageRole: readNonEmptyString(payload.lastMessageRole),
1594
+ ...(payload.lastMessageAt !== undefined ? { lastMessageAt: payload.lastMessageAt } : {}),
1595
+ jobId: readNonEmptyString(payload.jobId),
1596
+ interactionId: readNonEmptyString(payload.interactionId),
1597
+ status: readNonEmptyString(payload.status),
1598
+ targetDaemonId: readNonEmptyString(payload.targetDaemonId),
1599
+ startedAt: readNonEmptyString(payload.startedAt),
1600
+ completedAt: readNonEmptyString(payload.completedAt),
1601
+ retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1602
+ ...(relayModalMessage ? { modalMessage: relayModalMessage } : {}),
1603
+ ...(relayModalButtons && relayModalButtons.length > 0 ? { modalButtons: relayModalButtons } : {}),
1604
+ ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1605
+ ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
1606
+ ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
1607
+ ...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
1608
+ ...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
1609
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
1610
+ intentional: payload.intentional === true,
1611
+ intentionalStop: payload.intentionalStop === true,
1612
+ operatorCleanup: payload.operatorCleanup === true,
1613
+ reason: readNonEmptyString(payload.reason),
1614
+ stopReason: readNonEmptyString(payload.stopReason),
1615
+ cleanupReason: readNonEmptyString(payload.cleanupReason),
1616
+ source: readNonEmptyString(payload.source),
1617
+ };
1618
+ }
1619
+
1620
+ export function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>) {
1621
+ const eventName = readNonEmptyString(payload.event);
1622
+ if (!isMeshCoordinatorEvent(eventName)) {
1623
+ return { success: false, error: 'unsupported mesh event' };
1624
+ }
1625
+ const nodeId = readNonEmptyString(payload.nodeId);
1626
+ const workspace = readNonEmptyString(payload.workspace);
1627
+
1628
+ // The fallback worker-forward path (forwardUnresolvedDelegateEvent) cannot resolve a
1629
+ // mesh id locally on the remote worker, so it forwards the event with nodeId +
1630
+ // workspace only. The coordinator hosting the mesh CAN resolve it. Two recovery
1631
+ // paths, in order:
1632
+ // 1) workspace → mesh (fast path; cached repoIdentity lookup), then
1633
+ // 2) nodeId → mesh (deterministic backstop; scans hosted meshes for the node).
1634
+ // Workspace recovery alone was unreliable — a worktree clone whose repoIdentity
1635
+ // differs, or a transient cache miss, left the reconcile retry permanently rejected
1636
+ // ("meshId required") so the worker's completion never surfaced to the coordinator.
1637
+ // The nodeId is a stable coordinator-side fact and resolves timing-independently.
1638
+ const meshId = readNonEmptyString(payload.meshId)
1639
+ || (workspace ? readNonEmptyString(getCachedMeshByWorkspace(workspace)?.id) : '')
1640
+ || recoverMeshIdByNodeId(nodeId)
1641
+ // Fix B last resort: workspace + nodeId scan both missed, but the worker stamped
1642
+ // its coordinator anchor onto the forward (forwardUnresolvedDelegateEvent). Recover
1643
+ // via the hosted mesh that anchor owns (+ node disambiguation), guarding ambiguity.
1644
+ || recoverMeshIdByCoordinatorAndNode(
1645
+ readNonEmptyString(payload.meshCoordinatorDaemonId) || readNonEmptyString(payload.coordinatorDaemonId),
1646
+ nodeId,
1647
+ );
1648
+ if (!meshId) {
1649
+ // EVTTRACE: forwarded event rejected at receive — no meshId could be resolved
1650
+ // (no payload.meshId, no workspace→mesh, no nodeId→mesh). Observation only.
1651
+ traceMeshEventDrop('meshId_required', {
1652
+ taskId: payload.taskId,
1653
+ sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
1654
+ nodeId,
1655
+ event: eventName,
1656
+ }, workspace ? `workspace=${workspace} unresolved` : 'no workspace/nodeId');
1657
+ return { success: false, error: 'meshId required' };
1658
+ }
1659
+ // EVTTRACE: forwarded event accepted at receive (meshId resolved).
1660
+ // NOTIF-MISS (FIX 3): mirror buildRelayMetadataEvent's taskId fallback. A worker provider
1661
+ // completion stamps its task id as `meshActiveTaskId` (settings → event), not always the
1662
+ // top-level `taskId`, so reading payload.taskId alone rendered `task=-` at the received stage
1663
+ // (the [stage:queued] task=<id> → [stage:received] task=- loss). Falling back to
1664
+ // meshActiveTaskId keeps the trace task-scoped end-to-end so same-task redelivery is
1665
+ // distinguishable from a different task's real completion.
1666
+ traceMeshEventStage('received', {
1667
+ taskId: readNonEmptyString(payload.taskId) || readNonEmptyString(payload.meshActiveTaskId),
1668
+ sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
1669
+ nodeId,
1670
+ meshId,
1671
+ event: eventName,
1672
+ });
1673
+ const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
1674
+
1675
+ return injectMeshSystemMessage(components, {
1676
+ meshId,
1677
+ nodeId,
1678
+ nodeLabel,
1679
+ event: eventName,
1680
+ metadataEvent: buildRelayMetadataEvent(payload),
1681
+ // T4 (B3b): restore the v2 envelope carried at the top level of the relayed flat
1682
+ // payload (buildForwardPayloadFromPending → serializeV2EnvelopeToWire) so the
1683
+ // re-queue preserves the original eventId (idempotency) and unicast routing rather
1684
+ // than re-stamping a fresh v1/broadcast event. Empty for a v1 relay (version-skew safe).
1685
+ v2Envelope: readV2EnvelopeFromWire(payload),
1686
+ });
1687
+ }
1688
+
1689
+ // ---------------------------------------------------------------------------
1690
+ // Worker-side fallback forward for unresolved-mesh delegates.
1691
+ //
1692
+ // A REMOTE worker daemon that is being P2P-remote-controlled by a coordinator is
1693
+ // NOT a member of the coordinator's mesh — it has no local mesh record. So when its
1694
+ // completion event reaches the forwarder, resolveWorkerDelegateRouting() resolves the
1695
+ // coordinator anchor (meshCoordinatorDaemonId) from the worker envelope but cannot
1696
+ // resolve the mesh id (neither meshNodeFor nor a workspace→mesh lookup yields one) and
1697
+ // returns isDelegate=false / mesh_unresolved. Before this fallback the event was dropped
1698
+ // (delivery_unroutable) and only recovered later when the coordinator happened to pull
1699
+ // the worker's queue — which it can't, because the worker never queued an unroutable
1700
+ // event. Live symptom: `WARN [MeshEvents] delivery_unroutable: ... mesh unresolved`.
1701
+ //
1702
+ // The fix: the routing object still carries coordinatorDaemonId. Persist the raw event
1703
+ // to the durable worker-side outbox addressed to that coordinator daemon; the reconcile
1704
+ // loop's PHASE 0 delivers it (mesh_forward_event, acked, retry-capped). The coordinator
1705
+ // hosts the mesh, so it recovers the mesh id by workspace in handleMeshForwardEvent and
1706
+ // injects/queues it normally. meshId is intentionally omitted from the payload (the
1707
+ // worker has none); workspace is the routing anchor the coordinator resolves from.
1708
+ //
1709
+ // No loop / no double-delivery:
1710
+ // - This only fires on the WORKER (the coordinator-own session is rejected by the
1711
+ // resolver before reaching here), and the coordinator merely injects — it does not
1712
+ // re-enter this forwarder for the relayed event.
1713
+ // - It fires only when the normal queue path did NOT run (isDelegate=false), so the
1714
+ // event is never both queued locally and forwarded.
1715
+ //
1716
+ // Returns true when the event was durably accepted for delivery to the coordinator
1717
+ // daemon (so the caller skips the delivery_unroutable diagnostic); false when no
1718
+ // fallback was possible (no coordinator anchor / no dispatch transport) OR the durable
1719
+ // enqueue itself failed — the delivery_unroutable diagnostic is thereby narrowed to
1720
+ // "could not even persist to the queue" (a real potential loss), per the polling-
1721
+ // single-model design (docs/refactoring/2026-06-16-mesh-completion-polling-single-model.md §2.1/§2.6).
1722
+ //
1723
+ // Single delivery path (polling single-model): the spontaneous best-effort immediate
1724
+ // push that used to run here was REMOVED. Every unresolved-delegate event is persisted
1725
+ // to the outbox and delivered ONLY by setupMeshReconcileLoop's PHASE 0 retry (acked;
1726
+ // a failed push leaves the row queued). For happy-path latency the enqueue emits a
1727
+ // data-free reconcile NUDGE (nudgeUnresolvedForwardRetry) asking the loop to run the
1728
+ // retry soon; a lost nudge costs at most one reconcile interval, never the event.
1729
+ function forwardUnresolvedDelegateEvent(
1730
+ components: DaemonComponents,
1731
+ routing: ReturnType<typeof resolveWorkerDelegateRouting>,
1732
+ event: Record<string, unknown>,
1733
+ ): boolean {
1734
+ const coordinatorDaemonId = readNonEmptyString(routing.coordinatorDaemonId);
1735
+ if (!coordinatorDaemonId) return false;
1736
+ if (!components.dispatchMeshCommand) return false;
1737
+
1738
+ const eventName = readNonEmptyString(event.event);
1739
+ if (!eventName) return false;
1740
+
1741
+ // Flat payload mirroring buildForwardPayloadFromPending / what handleMeshForwardEvent
1742
+ // reads. nodeId/workspace come from the worker envelope so the coordinator can name and
1743
+ // locate the node.
1744
+ const payload: Record<string, unknown> = {
1745
+ ...event,
1746
+ event: eventName,
1747
+ nodeId: readNonEmptyString(routing.nodeId) || readNonEmptyString(event.meshNodeId) || undefined,
1748
+ workspace: readNonEmptyString(routing.workspace) || readNonEmptyString(event.workspace) || undefined,
1749
+ // Fix B: carry the resolved coordinator anchor so the coordinator's receive-side
1750
+ // recovery (recoverMeshIdByCoordinatorAndNode) can match this forward to one of the
1751
+ // meshes it hosts when workspace + nodeId both miss. routing.coordinatorDaemonId is
1752
+ // the same anchor this forward is addressed to (coordinatorDaemonId below).
1753
+ meshCoordinatorDaemonId: coordinatorDaemonId,
1754
+ };
1755
+ // RECONCILE-MESHID-DROP: stamp meshId when the WORKER can resolve it (member node /
1756
+ // live-session meshNodeFor). Historically omitted "because the worker can't resolve
1757
+ // it", but for a member-hosted node a no_node_binding session's coordinator-side
1758
+ // recovery (empty payload nodeId + workspace cache miss) fails and the retry is
1759
+ // rejected "meshId required" forever. Resolving here makes the forward self-sufficient;
1760
+ // when unresolvable even here it stays absent and the coordinator's own workspace/nodeId
1761
+ // recovery still runs (unchanged), with the retry cap as the loop backstop.
1762
+ const resolvedMeshId = resolveForwardEventMeshId(components, payload);
1763
+ if (resolvedMeshId) payload.meshId = resolvedMeshId;
1764
+
1765
+ // Self-addressed fallback: the resolved coordinator IS this daemon (a self-
1766
+ // coordinating / single-node mesh, or a delegate whose coordinator anchor resolved
1767
+ // to our own id). A cross-daemon mesh_forward_event to our own id is REFUSED by the
1768
+ // dispatch self-dial guard ("route via the local router instead"), so persisting it
1769
+ // to the outbox would only loop forever in PHASE 0's retry, never acked. Honour the
1770
+ // guard's advice: route the event straight through the local receiver — the exact
1771
+ // path the coordinator runs on receiving a remote push — and skip the outbox entirely.
1772
+ const selfDaemonIds = resolveCoordinatorDrainDaemonIds(components);
1773
+ if (selfDaemonIds.some(self => daemonIdsEquivalent(self, coordinatorDaemonId))) {
1774
+ try {
1775
+ handleMeshForwardEvent(components, payload);
1776
+ LOG.info('MeshEvents', `Self-addressed unresolved-delegate ${eventName} routed via local router (coordinator ${coordinatorDaemonId} is self) — outbox skipped`);
1777
+ } catch (e: any) {
1778
+ LOG.warn('MeshEvents', `Local route of self-addressed unresolved-delegate ${eventName} failed: ${e?.message || e}`);
1779
+ }
1780
+ return true;
1781
+ }
1782
+
1783
+ // Persist durably. Idempotent on fingerprint, so a re-fired completion does not
1784
+ // duplicate the outbox row. The outbox is the ONLY delivery route now (no
1785
+ // spontaneous push), so a hard persistence failure means the event has nowhere to
1786
+ // live — return false so the caller records the delivery_unroutable diagnostic,
1787
+ // which is thereby narrowed to exactly this "could not even enqueue" real-loss case.
1788
+ const persisted = enqueueUnresolvedDelegateForward(coordinatorDaemonId, eventName, payload);
1789
+ // EVTTRACE: unresolved-mesh worker persisted its completion to the outbox (no meshId
1790
+ // available locally; coordinator will recover it on receive).
1791
+ const fwdTraceCtx = {
1792
+ taskId: (payload as Record<string, unknown>).taskId,
1793
+ sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
1794
+ nodeId: readNonEmptyString(routing.nodeId) || readNonEmptyString(event.meshNodeId),
1795
+ event: eventName,
1796
+ };
1797
+ if (!persisted) {
1798
+ traceMeshEventDrop('outbox_enqueue_failed', fwdTraceCtx, `coordinatorDaemon=${coordinatorDaemonId}`);
1799
+ return false;
1800
+ }
1801
+ traceMeshEventStage('outbox_enqueue', fwdTraceCtx, `coordinatorDaemon=${coordinatorDaemonId} meshId=${readNonEmptyString(payload.meshId) || 'absent'}`);
1802
+
1803
+ // Data-free reconcile nudge (polling single-model §2.1 (B)): ask the reconcile
1804
+ // loop to run its PHASE 0 outbox retry soon instead of pushing the payload here.
1805
+ // Delivery itself stays on the single acked PHASE 0 path; losing the nudge costs
1806
+ // at most one reconcile interval of latency, never the event.
1807
+ nudgeUnresolvedForwardRetry();
1808
+ LOG.info('MeshEvents', `Durably queued ${eventName} for unresolved-mesh worker at ${routing.workspace || '(no workspace)'} to coordinator daemon ${coordinatorDaemonId} (reconcile PHASE 0 delivers)`);
1809
+ return true;
1810
+ }
1811
+
1812
+ /**
1813
+ * NOTIF-HELD-DRAIN (Fix 2): event-driven coordinator drain. The reconcile loop delivers a
1814
+ * worker's queued completion to an IDLE local coordinator only on its periodic poll. When a
1815
+ * coordinator is sitting idle awaiting exactly that completion, waiting up to a full poll
1816
+ * interval is the avoidable delivery latency the RCA flags — and combined with the (now-fixed)
1817
+ * modal-park false-positive it stretched into the multi-minute notification stall. So the
1818
+ * MOMENT a worker delegate event is persisted for a mesh, attempt the same idle-coordinator
1819
+ * drain immediately, mirroring the event-driven worker-claim path (agent:ready /
1820
+ * agent:generating_completed → triggerMeshQueue).
1821
+ *
1822
+ * Safety:
1823
+ * - drainPendingMeshCoordinatorEvents marks rows drained=1 atomically, so this races the
1824
+ * reconcile poll and the coordinator's own idle auto-flush harmlessly — exactly one consumes
1825
+ * each row.
1826
+ * - Only IDLE, non-modal-parked coordinators are delivery targets (never a generating /
1827
+ * consent-modal PTY).
1828
+ * - Strict session routing is honoured: an event naming an originating coordinator session is
1829
+ * delivered only to that live idle session; anything not currently deliverable here
1830
+ * (wrong/absent session, or a message-less lifecycle event) is RE-QUEUED — never dropped —
1831
+ * so the reconcile loop's strict hold/expire path remains the single authority for it.
1832
+ */
1833
+ export function flushPendingForMeshIdleCoordinators(components: DaemonComponents, meshId: string): void {
1834
+ // O(1) gate: skip the (relatively expensive) per-instance getState scan when the queue is
1835
+ // empty for this mesh.
1836
+ try {
1837
+ const store = MeshRuntimeStore.getInstance();
1838
+ if (store.pendingEventCount(meshId) === 0) return;
1839
+ } catch { /* store unavailable — fall through and let the drain decide */ }
1840
+
1841
+ const idleCoordinators: { instance: ProviderInstance; sessionId: string }[] = [];
1842
+ try {
1843
+ for (const inst of components.instanceManager.getByCategory('cli')) {
1844
+ const state = inst.getState();
1845
+ const settings = state.settings && typeof state.settings === 'object'
1846
+ ? state.settings as Record<string, unknown>
1847
+ : {};
1848
+ if (readNonEmptyString(settings.meshCoordinatorFor) !== meshId) continue;
1849
+ const status = readNonEmptyString(state.status).toLowerCase();
1850
+ const modalParked = typeof (inst as any).isModalParked === 'function'
1851
+ ? (inst as any).isModalParked() === true
1852
+ : (status === 'waiting_choice' || status === 'waiting_approval');
1853
+ // PTY-OVERTRUST-DRAIN (Defect B): decide idle on the RAW adapter turn-state
1854
+ // (getDrainStatus, mask-stripped) to match the reconcile loop — getState().status
1855
+ // overlays the auto-approve hold-idle mask that paints a genuinely-idle coordinator
1856
+ // `generating`, which would make this opportunistic flush skip a real drain target.
1857
+ // Fall back to the masked literal for any instance without getDrainStatus().
1858
+ const drainStatus: string | null = typeof (inst as any).getDrainStatus === 'function'
1859
+ ? (inst as any).getDrainStatus()
1860
+ : null;
1861
+ const idle = drainStatus !== null ? drainStatus === 'idle' : (status === 'idle');
1862
+ if (idle && !modalParked) {
1863
+ idleCoordinators.push({ instance: inst, sessionId: readNonEmptyString(state.instanceId) });
1864
+ }
1865
+ }
1866
+ } catch { return; }
1867
+ if (idleCoordinators.length === 0) return; // no idle target now → leave for the reconcile poll
1868
+
1869
+ const drainDaemonIds = resolveCoordinatorDrainDaemonIds(components);
1870
+ let pendingEvents: PendingMeshCoordinatorEvent[];
1871
+ try {
1872
+ pendingEvents = drainPendingMeshCoordinatorEvents(meshId, drainDaemonIds.length > 0 ? drainDaemonIds : undefined);
1873
+ } catch (e: any) {
1874
+ LOG.warn('MeshEvents', `Event-driven coordinator drain failed for mesh ${meshId}: ${e?.message || e}`);
1875
+ return;
1876
+ }
1877
+ if (pendingEvents.length === 0) return;
1878
+
1879
+ let delivered = 0;
1880
+ for (const pending of pendingEvents) {
1881
+ const wantSession = readNonEmptyString(pending.targetCoordinatorSessionId);
1882
+ const targets = wantSession
1883
+ ? idleCoordinators.filter(c => sessionIdsEquivalent(c.sessionId, wantSession))
1884
+ : idleCoordinators;
1885
+ // Not deliverable into an idle target here (wrong/absent session), or a message-less
1886
+ // lifecycle event (agent:ready / generating_started carry no coordinatorMessage and
1887
+ // must not be injected): re-queue so the reconcile loop owns it (lazy-synth / strict
1888
+ // hold/expire). Re-queue preserves queuedAt so the strict TTL measures true age.
1889
+ if (targets.length === 0 || !pending.coordinatorMessage) {
1890
+ try { queuePendingMeshCoordinatorEvent(pending); } catch { /* best-effort re-queue */ }
1891
+ continue;
1892
+ }
1893
+ const message = pending.coordinatorMessage;
1894
+ const force = shouldForceInjectMeshEvent(pending.event);
1895
+ for (const c of targets) {
1896
+ c.instance.onEvent('send_message', {
1897
+ input: { text: message, textFallback: message },
1898
+ ...(force ? { force: true } : {}),
1899
+ });
1900
+ delivered++;
1901
+ }
1902
+ }
1903
+ if (delivered > 0) {
1904
+ LOG.info('MeshEvents', `Event-driven drain delivered ${delivered} pending event(s) to ${idleCoordinators.length} idle coordinator(s) for mesh ${meshId}`);
1905
+ }
1906
+ }
1907
+
1908
+ export function setupMeshEventForwarding(components: DaemonComponents) {
1909
+ components.instanceManager.onEvent((event) => {
1910
+ // --- Coordinator idle auto-flush (fast path) ---
1911
+ // When a coordinator session becomes idle, immediately flush any pending
1912
+ // coordinator events that accumulated while it was generating, rather than
1913
+ // waiting up to one reconcile interval for setupMeshReconcileLoop to do it.
1914
+ // Both paths drain the SAME queue via drainPendingMeshCoordinatorEvents,
1915
+ // whose SQLite drained=1 marking is atomic — whichever fires first consumes
1916
+ // the events and the other gets nothing, so there is no double-delivery.
1917
+ // This runs before the delegate routing below so that coordinator-own idle
1918
+ // transitions are handled first.
1919
+ // Exception: a coordinator that is itself a direct-dispatch target still needs
1920
+ // to go through delegate routing so that the dispatching coordinator receives a
1921
+ // pendingCoordinatorEvents entry for the completion.
1922
+ if (event.event === 'agent:ready' || event.event === 'agent:generating_completed') {
1923
+ const flushInstanceId = readNonEmptyString(event.instanceId);
1924
+ if (flushInstanceId) {
1925
+ const flushSource = components.instanceManager.getInstance(flushInstanceId);
1926
+ if (flushSource && flushSource.category === 'cli') {
1927
+ const flushState = flushSource.getState();
1928
+ const flushSettings = flushState.settings && typeof flushState.settings === 'object' ? flushState.settings as Record<string, unknown> : {};
1929
+ const coordinatorMeshId = readNonEmptyString(flushSettings.meshCoordinatorFor);
1930
+ if (coordinatorMeshId) {
1931
+ const status = readNonEmptyString(flushState.status).toLowerCase();
1932
+ if (status === 'idle') {
1933
+ try {
1934
+ // Drain with the daemon's full coordinator-id set (status id + machineId).
1935
+ // The MCP layer stamps the prefixed status id (`standalone_<machineId>` /
1936
+ // `daemon_<machineId>`) as the worker's meshCoordinatorDaemonId; draining
1937
+ // with bare machineId alone would miss those unicast events. Mirrors
1938
+ // resolveCoordinatorDaemonIds in mesh-reconcile-loop.
1939
+ const drainDaemonIds = resolveCoordinatorDrainDaemonIds(components);
1940
+ const pendingEvents = drainPendingMeshCoordinatorEvents(coordinatorMeshId, drainDaemonIds.length > 0 ? drainDaemonIds : undefined);
1941
+ if (pendingEvents.length > 0) {
1942
+ LOG.info('MeshEvents', `Auto-flushing ${pendingEvents.length} pending coordinator event(s) for mesh ${coordinatorMeshId} on coordinator idle`);
1943
+ for (const pending of pendingEvents) {
1944
+ if (!pending.coordinatorMessage) continue;
1945
+ const forcePending = shouldForceInjectMeshEvent(pending.event);
1946
+ flushSource.onEvent('send_message', {
1947
+ input: { text: pending.coordinatorMessage, textFallback: pending.coordinatorMessage },
1948
+ ...(forcePending ? { force: true } : {}),
1949
+ });
1950
+ }
1951
+ }
1952
+ } catch (e: any) {
1953
+ LOG.warn('MeshEvents', `Failed to auto-flush pending coordinator events: ${e?.message || e}`);
1954
+ }
1955
+ }
1956
+ // Skip delegate routing unless this coordinator session is itself
1957
+ // a direct-dispatch target — in that case fall through so the
1958
+ // dispatching coordinator gets a pendingCoordinatorEvents entry.
1959
+ let hasDirectDispatch = false;
1960
+ try {
1961
+ hasDirectDispatch =
1962
+ getActiveDirectDispatches(coordinatorMeshId).some(d => sessionIdsEquivalent(d.sessionId, flushInstanceId))
1963
+ || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, flushInstanceId);
1964
+ } catch { /* best-effort */ }
1965
+ if (!hasDirectDispatch) return;
1966
+ }
1967
+ }
1968
+ }
1969
+ }
1970
+
1971
+ // --- Delegate event routing ---
1972
+ if (!isMeshCoordinatorEvent(event.event)) return;
1973
+
1974
+ const instanceId = readNonEmptyString(event.instanceId);
1975
+ if (!instanceId) return;
1976
+
1977
+ // R1: all session→node→mesh→coordinator interpretation is folded into the single
1978
+ // resolveWorkerDelegateRouting() resolver. No stamp (meshNodeFor / meshNodeId /
1979
+ // meshCoordinatorDaemonId / meshCoordinatorNodeId / launchedByCoordinator) is read
1980
+ // here to make a routing decision — the resolver is the one authority, and the
1981
+ // forwarder consumes its typed result only.
1982
+ const routing = resolveWorkerDelegateRouting(components, instanceId, {
1983
+ getMeshById: (meshId) => getMeshWithCache(components, meshId),
1984
+ getMeshByWorkspace: (workspace) => getCachedMeshByWorkspace(workspace),
1985
+ });
1986
+ if (!routing.isDelegate) {
1987
+ // Fallback: a REMOTE worker that isn't a member of the coordinator's mesh can't
1988
+ // resolve a mesh id locally (mesh_unresolved), but it still carries the coordinator
1989
+ // daemon anchor. Forward the event straight to that coordinator over P2P instead of
1990
+ // dropping it — the coordinator hosts the mesh and recovers the id by workspace.
1991
+ if (isUnroutableDelegateRejection(routing)
1992
+ && forwardUnresolvedDelegateEvent(components, routing, event)) {
1993
+ return;
1994
+ }
1995
+ // R4: a worker that presented a valid envelope but resolved to no mesh (and could
1996
+ // not be fallback-forwarded — e.g. no coordinator anchor) used to be dropped
1997
+ // silently. Leave a fail-loud diagnostic so the missing completion is traceable.
1998
+ // Benign non-delegate rejections (not_cli / no_workspace / etc.) are no-ops inside
1999
+ // recordUnroutableDelegateEvent.
2000
+ // EVTTRACE: a delegate event that could not be routed AND could not be
2001
+ // fallback-forwarded (no coordinator anchor). Only mesh_unresolved is a real
2002
+ // drop; the benign non-delegate rejections are ordinary non-mesh traffic.
2003
+ if (isUnroutableDelegateRejection(routing)) {
2004
+ traceMeshEventDrop('unroutable', {
2005
+ taskId: (event as Record<string, unknown>).meshActiveTaskId ?? (event as Record<string, unknown>).taskId,
2006
+ sessionId: routing.sessionId,
2007
+ nodeId: routing.nodeId,
2008
+ event: event.event,
2009
+ }, 'no coordinator anchor / mesh_unresolved');
2010
+ }
2011
+ recordUnroutableDelegateEvent(routing, event.event);
2012
+ return;
2013
+ }
2014
+
2015
+ injectMeshSystemMessage(components, {
2016
+ meshId: routing.meshId,
2017
+ sourceInstanceId: instanceId,
2018
+ nodeId: routing.nodeId,
2019
+ nodeLabel: routing.nodeLabel,
2020
+ event: event.event,
2021
+ metadataEvent: event,
2022
+ });
2023
+
2024
+ // NOTIF-HELD-DRAIN (Fix 2): the worker's event is now persisted in the pending queue.
2025
+ // If a local coordinator for this mesh is sitting idle awaiting it, deliver immediately
2026
+ // instead of waiting up to a full reconcile interval (event-driven, mirrors the
2027
+ // worker-claim path). No-op when no idle coordinator is present (held for reconcile).
2028
+ flushPendingForMeshIdleCoordinators(components, routing.meshId);
2029
+ });
2030
+ }