@adhdev/daemon-core 0.9.82-rc.39 → 0.9.82-rc.390

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 (415) 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 +67 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +121 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -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 +14 -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 +7 -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 +15 -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 +60 -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 +2 -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 +116 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router.d.ts +237 -2
  56. package/dist/commands/upgrade-helper.d.ts +41 -1
  57. package/dist/config/chat-history.d.ts +9 -0
  58. package/dist/config/config.d.ts +5 -0
  59. package/dist/config/mesh-config.d.ts +77 -1
  60. package/dist/daemon/dev-server.d.ts +0 -2
  61. package/dist/detection/ide-detector.d.ts +13 -0
  62. package/dist/detection/win32-ide-version.d.ts +37 -0
  63. package/dist/git/change-impact-config.d.ts +159 -0
  64. package/dist/git/git-commands.d.ts +11 -1
  65. package/dist/git/git-diff.d.ts +6 -0
  66. package/dist/git/git-executor.d.ts +11 -0
  67. package/dist/git/git-status.d.ts +23 -0
  68. package/dist/git/git-types.d.ts +2 -50
  69. package/dist/git/git-worktree.d.ts +22 -0
  70. package/dist/git/index.d.ts +2 -0
  71. package/dist/index.d.ts +57 -13
  72. package/dist/index.js +46306 -18259
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +46439 -18533
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/installer.d.ts +1 -4
  77. package/dist/ipc/local-ipc-server.d.ts +91 -0
  78. package/dist/launch.d.ts +1 -1
  79. package/dist/logging/async-batch-writer.d.ts +10 -0
  80. package/dist/logging/log-redactor.d.ts +24 -0
  81. package/dist/logging/log-tail-reader.d.ts +81 -0
  82. package/dist/logging/logger.d.ts +1 -1
  83. package/dist/mesh/contracts.d.ts +164 -0
  84. package/dist/mesh/coordinator-prompt.d.ts +89 -0
  85. package/dist/mesh/coordinator-registry.d.ts +59 -0
  86. package/dist/mesh/mesh-active-work.d.ts +174 -0
  87. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  88. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  89. package/dist/mesh/mesh-event-classify.d.ts +5 -0
  90. package/dist/mesh/mesh-event-forwarding.d.ts +19 -0
  91. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  92. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  93. package/dist/mesh/mesh-events-pending.d.ts +55 -0
  94. package/dist/mesh/mesh-events-stale.d.ts +52 -0
  95. package/dist/mesh/mesh-events-utils.d.ts +64 -0
  96. package/dist/mesh/mesh-events.d.ts +6 -49
  97. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  98. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  99. package/dist/mesh/mesh-init.d.ts +86 -0
  100. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  101. package/dist/mesh/mesh-ledger.d.ts +77 -1
  102. package/dist/mesh/mesh-missions.d.ts +164 -0
  103. package/dist/mesh/mesh-node-identity.d.ts +289 -0
  104. package/dist/mesh/mesh-queue-assignment.d.ts +86 -0
  105. package/dist/mesh/mesh-reconcile-loop.d.ts +65 -0
  106. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  107. package/dist/mesh/mesh-refine-gates.d.ts +428 -0
  108. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  109. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  110. package/dist/mesh/mesh-routing.d.ts +70 -0
  111. package/dist/mesh/mesh-runtime-store.d.ts +447 -0
  112. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  113. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  114. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  115. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  116. package/dist/mesh/mesh-work-queue.d.ts +247 -5
  117. package/dist/mesh/preview-freshness.d.ts +18 -0
  118. package/dist/mesh/refine-config.d.ts +216 -0
  119. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  120. package/dist/providers/acp-provider-instance.d.ts +5 -0
  121. package/dist/providers/approval-utils.d.ts +20 -0
  122. package/dist/providers/chat-message-normalization.d.ts +13 -1
  123. package/dist/providers/cli-provider-instance.d.ts +157 -3
  124. package/dist/providers/contracts.d.ts +139 -6
  125. package/dist/providers/external-sources.d.ts +71 -0
  126. package/dist/providers/manual-attendance.d.ts +63 -0
  127. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  128. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  129. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  130. package/dist/providers/native-history/constants.d.ts +12 -0
  131. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  132. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  133. package/dist/providers/native-history/index.d.ts +13 -0
  134. package/dist/providers/provider-instance-manager.d.ts +14 -0
  135. package/dist/providers/provider-instance.d.ts +23 -1
  136. package/dist/providers/provider-loader.d.ts +26 -4
  137. package/dist/providers/provider-trust.d.ts +31 -0
  138. package/dist/providers/read-chat-contract.d.ts +29 -0
  139. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  140. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  141. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  142. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  143. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  144. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  145. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  146. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  147. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  148. package/dist/providers/sdk/v1/index.d.ts +30 -0
  149. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  150. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  151. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  152. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  153. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  154. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  155. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  156. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  157. package/dist/providers/spec/adapter.d.ts +91 -0
  158. package/dist/providers/spec/cli-adapter.d.ts +223 -0
  159. package/dist/providers/spec/evaluator.d.ts +45 -0
  160. package/dist/providers/spec/fsm-driver.d.ts +407 -0
  161. package/dist/providers/spec/fsm-evaluator.d.ts +76 -0
  162. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  163. package/dist/providers/spec/fsm-types.d.ts +174 -0
  164. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  165. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  166. package/dist/providers/spec/route.d.ts +4 -0
  167. package/dist/providers/spec/types.d.ts +222 -0
  168. package/dist/providers/status-monitor.d.ts +7 -7
  169. package/dist/providers/transcript-v2.d.ts +176 -0
  170. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  171. package/dist/providers/working-dir.d.ts +17 -0
  172. package/dist/repo-mesh-types.d.ts +376 -13
  173. package/dist/runtime-defaults.d.ts +2 -0
  174. package/dist/sessions/registry.d.ts +6 -0
  175. package/dist/shared-types-extra.d.ts +2 -4
  176. package/dist/shared-types.d.ts +59 -2
  177. package/dist/status/normalize.d.ts +1 -1
  178. package/dist/status/normalize.js +1 -0
  179. package/dist/status/normalize.js.map +1 -1
  180. package/dist/status/normalize.mjs +1 -0
  181. package/dist/status/normalize.mjs.map +1 -1
  182. package/dist/status/reporter.d.ts +2 -0
  183. package/dist/status/snapshot.d.ts +26 -0
  184. package/dist/system/hash.d.ts +8 -0
  185. package/dist/system/load-better-sqlite3.d.ts +21 -0
  186. package/dist/types.d.ts +5 -0
  187. package/package.json +7 -3
  188. package/src/agent-stream/poller.ts +2 -3
  189. package/src/agent-stream/provider-adapter.ts +1 -1
  190. package/src/boot/daemon-lifecycle.ts +63 -12
  191. package/src/boot/process-hardening.ts +89 -0
  192. package/src/build-info.ts +73 -0
  193. package/src/chat/source-machine.ts +534 -0
  194. package/src/chat/source-resolver.ts +0 -0
  195. package/src/chat/subscription-updates.ts +20 -1
  196. package/src/cli-adapter-types.d.ts +1 -0
  197. package/src/cli-adapter-types.ts +68 -2
  198. package/src/cli-adapters/cli-script-runner.ts +421 -0
  199. package/src/cli-adapters/cli-state-engine.ts +1221 -0
  200. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  201. package/src/cli-adapters/provider-cli-adapter.ts +836 -1141
  202. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  203. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  204. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  205. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  206. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  207. package/src/cli-adapters/pty-transport.ts +2 -1
  208. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  209. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  210. package/src/cli-adapters/resolve-executable.ts +90 -0
  211. package/src/cli-adapters/session-host-transport.ts +2 -1
  212. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  213. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  214. package/src/cli-adapters/terminal-screen.ts +16 -81
  215. package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
  216. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  217. package/src/commands/chat-commands-read.ts +2327 -0
  218. package/src/commands/chat-commands-scope.ts +54 -0
  219. package/src/commands/chat-commands-shared.ts +114 -0
  220. package/src/commands/chat-commands-write.ts +880 -0
  221. package/src/commands/chat-commands.ts +19 -1841
  222. package/src/commands/cli-manager.ts +513 -26
  223. package/src/commands/handler.ts +841 -2
  224. package/src/commands/high-family/index.ts +28 -0
  225. package/src/commands/high-family/mesh-coordinator-launch.ts +657 -0
  226. package/src/commands/high-family/mesh-events.ts +80 -0
  227. package/src/commands/high-family/mesh-status.ts +678 -0
  228. package/src/commands/high-family/types.ts +76 -0
  229. package/src/commands/low-family/coordinator-prompt.ts +72 -0
  230. package/src/commands/low-family/daemon-lifecycle.ts +107 -0
  231. package/src/commands/low-family/diagnostics.ts +57 -0
  232. package/src/commands/low-family/index.ts +37 -0
  233. package/src/commands/low-family/mesh-ledger.ts +62 -0
  234. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  235. package/src/commands/low-family/notification.ts +116 -0
  236. package/src/commands/low-family/refine-config.ts +106 -0
  237. package/src/commands/low-family/session-host.ts +274 -0
  238. package/src/commands/low-family/spec-providerdev.ts +217 -0
  239. package/src/commands/low-family/status-meta.ts +112 -0
  240. package/src/commands/low-family/types.ts +39 -0
  241. package/src/commands/med-family/cli-agent.ts +230 -0
  242. package/src/commands/med-family/fast-forward.ts +230 -0
  243. package/src/commands/med-family/ide.ts +163 -0
  244. package/src/commands/med-family/index.ts +37 -0
  245. package/src/commands/med-family/mesh-crud.ts +836 -0
  246. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  247. package/src/commands/med-family/mesh-queue.ts +131 -0
  248. package/src/commands/med-family/mesh-restart.ts +92 -0
  249. package/src/commands/med-family/types.ts +120 -0
  250. package/src/commands/mesh-coordinator.ts +334 -124
  251. package/src/commands/router.ts +2476 -3207
  252. package/src/commands/stream-commands.ts +8 -0
  253. package/src/commands/upgrade-helper.ts +310 -45
  254. package/src/config/chat-history.ts +483 -24
  255. package/src/config/config.ts +12 -0
  256. package/src/config/mesh-config.ts +353 -22
  257. package/src/config/recent-activity.ts +8 -2
  258. package/src/daemon/dev-auto-implement.ts +3 -2
  259. package/src/daemon/dev-cli-debug.ts +10 -1
  260. package/src/daemon/dev-server.ts +0 -541
  261. package/src/detection/cli-detector.ts +28 -9
  262. package/src/detection/ide-detector.ts +55 -16
  263. package/src/detection/win32-ide-version.ts +106 -0
  264. package/src/git/change-impact-config.ts +354 -0
  265. package/src/git/git-commands.ts +55 -14
  266. package/src/git/git-diff.ts +81 -11
  267. package/src/git/git-executor.ts +12 -0
  268. package/src/git/git-status.ts +574 -43
  269. package/src/git/git-types.ts +14 -62
  270. package/src/git/git-worktree.ts +76 -1
  271. package/src/git/index.ts +16 -0
  272. package/src/index.ts +171 -12
  273. package/src/installer.d.ts +1 -1
  274. package/src/installer.ts +8 -6
  275. package/src/ipc/local-ipc-server.ts +278 -0
  276. package/src/launch.d.ts +1 -1
  277. package/src/launch.ts +37 -28
  278. package/src/logging/async-batch-writer.ts +55 -0
  279. package/src/logging/command-log.ts +7 -5
  280. package/src/logging/log-redactor.ts +100 -0
  281. package/src/logging/log-tail-reader.ts +341 -0
  282. package/src/logging/logger.ts +14 -7
  283. package/src/mesh/contracts.ts +338 -0
  284. package/src/mesh/coordinator-prompt.ts +381 -33
  285. package/src/mesh/coordinator-registry.ts +121 -0
  286. package/src/mesh/mesh-active-work.ts +645 -0
  287. package/src/mesh/mesh-coordinator-config.ts +97 -0
  288. package/src/mesh/mesh-delivery-policy.ts +315 -0
  289. package/src/mesh/mesh-event-classify.ts +51 -0
  290. package/src/mesh/mesh-event-forwarding.ts +1665 -0
  291. package/src/mesh/mesh-event-trace.ts +67 -0
  292. package/src/mesh/mesh-events-coordinator.ts +30 -0
  293. package/src/mesh/mesh-events-pending.ts +590 -0
  294. package/src/mesh/mesh-events-stale.ts +363 -0
  295. package/src/mesh/mesh-events-utils.ts +425 -0
  296. package/src/mesh/mesh-events.ts +32 -1035
  297. package/src/mesh/mesh-fast-forward.ts +839 -0
  298. package/src/mesh/mesh-host-ownership.ts +73 -0
  299. package/src/mesh/mesh-init.ts +260 -0
  300. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  301. package/src/mesh/mesh-ledger.ts +596 -102
  302. package/src/mesh/mesh-missions.ts +322 -0
  303. package/src/mesh/mesh-node-identity.ts +1887 -0
  304. package/src/mesh/mesh-queue-assignment.ts +1491 -0
  305. package/src/mesh/mesh-reconcile-loop.ts +1527 -0
  306. package/src/mesh/mesh-refine-batch.ts +197 -0
  307. package/src/mesh/mesh-refine-gates.ts +1652 -0
  308. package/src/mesh/mesh-refine-status.ts +231 -0
  309. package/src/mesh/mesh-review-inbox.ts +307 -0
  310. package/src/mesh/mesh-routing.ts +291 -0
  311. package/src/mesh/mesh-runtime-store.ts +1795 -0
  312. package/src/mesh/mesh-scheduling-runtime.ts +199 -0
  313. package/src/mesh/mesh-task-stats.ts +161 -0
  314. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  315. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  316. package/src/mesh/mesh-work-queue.ts +1141 -141
  317. package/src/mesh/preview-freshness.ts +118 -0
  318. package/src/mesh/refine-config.ts +423 -0
  319. package/src/mesh/worktree-bootstrap-config.ts +331 -0
  320. package/src/providers/acp-provider-instance.ts +43 -10
  321. package/src/providers/approval-utils.d.ts +5 -0
  322. package/src/providers/approval-utils.ts +57 -5
  323. package/src/providers/chat-message-normalization.ts +46 -6
  324. package/src/providers/cli-provider-instance.ts +1552 -102
  325. package/src/providers/contracts.d.ts +55 -0
  326. package/src/providers/contracts.ts +150 -6
  327. package/src/providers/extension-provider-instance.ts +12 -7
  328. package/src/providers/external-sources.ts +218 -0
  329. package/src/providers/ide-provider-instance.ts +35 -12
  330. package/src/providers/manual-attendance.ts +85 -0
  331. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  332. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  333. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  334. package/src/providers/native-history/constants.ts +19 -0
  335. package/src/providers/native-history/dispatcher.ts +339 -0
  336. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  337. package/src/providers/native-history/index.ts +30 -0
  338. package/src/providers/provider-instance-manager.ts +30 -0
  339. package/src/providers/provider-instance.ts +19 -1
  340. package/src/providers/provider-loader.ts +668 -50
  341. package/src/providers/provider-schema.ts +87 -14
  342. package/src/providers/provider-trust.ts +114 -0
  343. package/src/providers/read-chat-contract.ts +76 -16
  344. package/src/providers/sdk/README.md +49 -0
  345. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  346. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  347. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  348. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  349. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  350. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  351. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  352. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  353. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  354. package/src/providers/sdk/v1/index.ts +152 -0
  355. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  356. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  357. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  358. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  359. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  360. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  361. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  362. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  363. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  364. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  365. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  366. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  367. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  368. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  369. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  370. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  371. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  372. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  373. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  374. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  375. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  376. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  377. package/src/providers/sdk/v1/validators/index.ts +19 -0
  378. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  379. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  380. package/src/providers/spec/adapter.ts +235 -0
  381. package/src/providers/spec/cli-adapter.ts +1056 -0
  382. package/src/providers/spec/evaluator.ts +407 -0
  383. package/src/providers/spec/fsm-driver.ts +1304 -0
  384. package/src/providers/spec/fsm-evaluator.ts +272 -0
  385. package/src/providers/spec/fsm-loader.ts +120 -0
  386. package/src/providers/spec/fsm-types.ts +273 -0
  387. package/src/providers/spec/native-history-executor.ts +1128 -0
  388. package/src/providers/spec/pre-launch-trust.ts +104 -0
  389. package/src/providers/spec/route.ts +51 -0
  390. package/src/providers/spec/types.ts +262 -0
  391. package/src/providers/status-monitor.d.ts +7 -7
  392. package/src/providers/status-monitor.ts +37 -22
  393. package/src/providers/transcript-v2.ts +567 -0
  394. package/src/providers/types/interactive-prompt.ts +536 -0
  395. package/src/providers/version-archive.ts +64 -24
  396. package/src/providers/working-dir.ts +23 -0
  397. package/src/repo-mesh-types.ts +542 -14
  398. package/src/runtime-defaults.ts +39 -0
  399. package/src/sessions/registry.ts +6 -0
  400. package/src/shared-types-extra.ts +2 -4
  401. package/src/shared-types.d.ts +8 -0
  402. package/src/shared-types.ts +64 -1
  403. package/src/status/builders.ts +26 -6
  404. package/src/status/normalize.ts +2 -0
  405. package/src/status/reporter.ts +19 -1
  406. package/src/status/snapshot.ts +95 -26
  407. package/src/system/hash.ts +23 -0
  408. package/src/system/host-memory.ts +29 -12
  409. package/src/system/load-better-sqlite3.ts +68 -0
  410. package/src/types.ts +5 -0
  411. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  412. package/dist/mesh/mesh-sync.d.ts +0 -53
  413. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  414. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  415. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,1665 @@
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 } from './mesh-work-queue.js';
8
+ import { markSessionDeliveriesTerminal, updateSessionDeliveryStatus, recordCompletionConflict } from './mesh-delivery-policy.js';
9
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
10
+ import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents } from './mesh-events-pending.js';
11
+ import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent, isUnroutableDelegateRejection } from './mesh-routing.js';
12
+ import { resolveMeshHostStatus } from './mesh-host-ownership.js';
13
+ import { enqueueUnresolvedDelegateForward, peekUnresolvedDelegateForwards, ackUnresolvedDelegateForward } from './mesh-unresolved-forward-outbox.js';
14
+ import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
15
+ import { getLastDisplayMessage } from '../status/snapshot.js';
16
+ import { resolveDelegatedWorkerAutoApprove } from '../repo-mesh-types.js';
17
+ import { meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, type MeshNodeIdentified } from '@adhdev/mesh-shared';
18
+ import {
19
+ findRecentTerminalLedgerEvidence,
20
+ hasDispatchAfterTerminal,
21
+ hasUnterminalDirectDispatchLedgerEntry,
22
+ buildNoProgressCompletionReconciliation,
23
+ } from './mesh-events-stale.js';
24
+ import {
25
+ buildMeshSystemMessage,
26
+ readNonEmptyString,
27
+ resolveEventSessionId,
28
+ readRefineJobId,
29
+ readWorkerResultMetadata,
30
+ resolveMeshSurfacedSessionPreview,
31
+ isFalseIdleCompletion,
32
+ isWeakCompletionEvidence,
33
+ } from './mesh-events-utils.js';
34
+ import { isMeshCoordinatorEvent, shouldForceInjectMeshEvent, EVENT_TO_LEDGER_KIND } from './mesh-event-classify.js';
35
+ import {
36
+ getMeshWithCache,
37
+ tryAssignQueueTask,
38
+ triggerMeshQueue,
39
+ runIdleMaintenanceThenAssignQueue,
40
+ maybeAutoFastForwardIdleNode,
41
+ sessionHasActiveAssignment,
42
+ } from './mesh-queue-assignment.js';
43
+
44
+ // The set of coordinator-daemon ids this daemon answers to when draining the
45
+ // pending-events queue. Mirrors resolveCoordinatorDaemonIds in mesh-reconcile-loop:
46
+ // a unicast event may be stamped with the status id, the bare machineId, OR the
47
+ // config-form node daemonId (`daemon_<machineId>`) depending on which dispatch path
48
+ // created the worker. We expand to EVERY equivalent form so a `daemon_<machineId>`
49
+ // completion matches a coordinator that knows itself as bare `<machineId>` (the
50
+ // base-node completion-surface bug) and vice versa.
51
+ function resolveCoordinatorDrainDaemonIds(components: DaemonComponents): string[] {
52
+ const statusInstanceId = readNonEmptyString((components as { statusInstanceId?: string }).statusInstanceId);
53
+ const machineId = readNonEmptyString(loadConfig().machineId);
54
+ return expandDaemonIdForms([statusInstanceId, machineId]);
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Remote Node Idle Session Tracking
59
+ // ---------------------------------------------------------------------------
60
+ const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Workspace-to-mesh lookup cache
64
+ // ---------------------------------------------------------------------------
65
+ const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
66
+ const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
67
+
68
+ function getCachedMeshByWorkspace(workspace: string): any {
69
+ const now = Date.now();
70
+ const cached = meshByWorkspaceCache.get(workspace);
71
+ if (cached && now - cached.cachedAt < MESH_WORKSPACE_CACHE_TTL_MS) return cached.mesh;
72
+ const mesh = getMeshByRepo(workspace);
73
+ meshByWorkspaceCache.set(workspace, { mesh, cachedAt: now });
74
+ return mesh;
75
+ }
76
+
77
+ // Deterministic meshId recovery for a forwarded worker event that carries no meshId.
78
+ // An unresolved-mesh worker (forwardUnresolvedDelegateEvent) cannot resolve its own
79
+ // meshId locally, so it pushes the event with nodeId + workspace only and relies on
80
+ // the coordinator — which hosts the mesh — to recover the id. Workspace recovery
81
+ // (getCachedMeshByWorkspace → getMeshByRepo) is the fast path but can miss (a worktree
82
+ // clone whose repoIdentity differs, or a transient cache state), which left the retry
83
+ // permanently rejected with "meshId required". The node-id IS a stable, coordinator-side
84
+ // fact: scan the hosted meshes for the one whose node matches the forwarded nodeId
85
+ // (3-form normalizer). This is timing-independent and never depends on repo lookup.
86
+ function recoverMeshIdByNodeId(nodeId: string): string {
87
+ if (!nodeId) return '';
88
+ for (const mesh of listMeshes()) {
89
+ if (Array.isArray(mesh.nodes) && mesh.nodes.some((n: any) => meshNodeIdMatches(n, nodeId))) {
90
+ return readNonEmptyString(mesh.id);
91
+ }
92
+ }
93
+ return '';
94
+ }
95
+
96
+ // MESHID-DROP coordinator-anchor recovery (Fix B): the last-resort meshId recovery for an
97
+ // unresolved-delegate forward whose payload carries the worker's coordinator anchor
98
+ // (meshCoordinatorDaemonId) but no resolvable meshId — neither workspace nor nodeId scan
99
+ // matched (a freshly-cloned worktree node not yet registered under the forwarded id form, or
100
+ // an empty payload nodeId). The receiving daemon IS the coordinator/host, so scope to the
101
+ // meshes IT hosts whose host daemon matches the anchor (daemonIdsEquivalent — never a raw
102
+ // compare, so daemon_mach_/mach_/standalone_ forms all match the same machine). Among those:
103
+ // • a nodeId → the mesh whose nodes contain it (meshNodeIdMatches 3-form) wins;
104
+ // • no nodeId → fall back to the anchor's SINGLE hosted mesh only. Ambiguity (the anchor
105
+ // hosts >1 mesh and no node disambiguates) returns '' rather than guess — a wrong meshId
106
+ // would inject the completion into an unrelated mesh's ledger, worse than the retry-cap drop.
107
+ export function recoverMeshIdByCoordinatorAndNode(coordinatorDaemonId: string, nodeId: string): string {
108
+ if (!coordinatorDaemonId) return '';
109
+ const hosted = listMeshes().filter(mesh => {
110
+ const host = resolveMeshHostStatus(mesh);
111
+ return host.role === 'host'
112
+ && (!host.hostDaemonId || daemonIdsEquivalent(host.hostDaemonId, coordinatorDaemonId));
113
+ });
114
+ if (hosted.length === 0) return '';
115
+ if (nodeId) {
116
+ const byNode = hosted.find(mesh =>
117
+ Array.isArray(mesh.nodes) && mesh.nodes.some((n: any) => meshNodeIdMatches(n, nodeId)));
118
+ if (byNode) return readNonEmptyString(byNode.id);
119
+ // nodeId present but matched no hosted mesh — do NOT fall through to the single-mesh
120
+ // guess; the node belongs to a mesh we don't host (or under a different id), and
121
+ // guessing would misroute. Stay unresolved.
122
+ return '';
123
+ }
124
+ // No nodeId to disambiguate: only safe when the anchor hosts exactly one mesh.
125
+ return hosted.length === 1 ? readNonEmptyString(hosted[0].id) : '';
126
+ }
127
+
128
+ // RECONCILE-MESHID-DROP: WORKER-side meshId resolution for an unresolved-delegate
129
+ // forward payload. forwardUnresolvedDelegateEvent omits meshId by design (the worker
130
+ // "can't resolve it") and relies on the COORDINATOR recovering it from workspace/nodeId.
131
+ // That recovery fails when the no_node_binding session's payload has an empty nodeId AND
132
+ // the coordinator's workspace→mesh lookup misses (a worktree clone whose repoIdentity
133
+ // differs / a cache miss) — leaving the reconcile retry rejected with "meshId required"
134
+ // every 4s forever. The worker actually has MORE context than the stripped payload gives
135
+ // the coordinator: it hosts the node as a member and holds the LIVE session, whose
136
+ // settings.meshNodeFor / meshNodeId are authoritative even when they were not stamped
137
+ // onto the original event. Resolve here (worker side) and stamp meshId onto the payload so
138
+ // the coordinator accepts it. Mirrors the receiver's recovery order, then adds the live-
139
+ // session fallback. Returns '' when even the worker cannot resolve it (truly unresolvable —
140
+ // the retry cap then drops it instead of looping). No side effects; safe to call per retry.
141
+ export function resolveForwardEventMeshId(
142
+ components: DaemonComponents,
143
+ payload: Record<string, unknown>,
144
+ ): string {
145
+ const direct = readNonEmptyString(payload.meshId);
146
+ if (direct) return direct;
147
+ const workspace = readNonEmptyString(payload.workspace);
148
+ const byWorkspace = workspace ? readNonEmptyString(getCachedMeshByWorkspace(workspace)?.id) : '';
149
+ if (byWorkspace) return byWorkspace;
150
+ const byNode = recoverMeshIdByNodeId(readNonEmptyString(payload.nodeId));
151
+ if (byNode) return byNode;
152
+ // Live-session fallback: the worker session may carry meshNodeFor / meshNodeId now even
153
+ // though the original event didn't (a late stamp, or an event that fired before binding).
154
+ const sessionId = readNonEmptyString(payload.targetSessionId)
155
+ || readNonEmptyString(payload.sessionId)
156
+ || readNonEmptyString(payload.instanceId);
157
+ if (sessionId) {
158
+ try {
159
+ const state = components.instanceManager?.getInstance?.(sessionId)?.getState?.();
160
+ const settings = (state?.settings as Record<string, unknown>) || {};
161
+ const meshNodeFor = readNonEmptyString(settings.meshNodeFor);
162
+ if (meshNodeFor) return meshNodeFor;
163
+ const byStamp = recoverMeshIdByNodeId(readNonEmptyString(settings.meshNodeId));
164
+ if (byStamp) return byStamp;
165
+ const sessionWorkspace = readNonEmptyString(state?.workspace);
166
+ const bySessionWorkspace = sessionWorkspace ? readNonEmptyString(getCachedMeshByWorkspace(sessionWorkspace)?.id) : '';
167
+ if (bySessionWorkspace) return bySessionWorkspace;
168
+ } catch { /* best-effort — fall through to unresolved */ }
169
+ }
170
+ return '';
171
+ }
172
+
173
+
174
+ export function __resetMeshWorkspaceCacheForTests(): void {
175
+ meshByWorkspaceCache.clear();
176
+ }
177
+
178
+ function sweepExpiredRemoteIdleSessions(): void {
179
+ try {
180
+ MeshRuntimeStore.getInstance().pruneExpiredRemoteIdleSessions();
181
+ } catch { /* best-effort */ }
182
+ }
183
+
184
+ const INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS = 30 * 60 * 1000;
185
+
186
+ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boolean {
187
+ return event.intentional === true
188
+ || event.intentionalStop === true
189
+ || event.operatorCleanup === true
190
+ || event.reason === 'operator_cleanup'
191
+ || event.stopReason === 'operator_cleanup'
192
+ || event.cleanupReason === 'operator_cleanup'
193
+ || event.source === 'mesh_cleanup_sessions'
194
+ || event.source === 'mesh_remove_node';
195
+ }
196
+
197
+ function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
198
+ if (!sessionId && !nodeId) return false;
199
+ const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
200
+ const entries = readLedgerEntries(meshId, { tail: 200 });
201
+ for (let i = entries.length - 1; i >= 0; i--) {
202
+ const entry = entries[i];
203
+ const timestamp = new Date(entry.timestamp).getTime();
204
+ if (!Number.isNaN(timestamp) && timestamp < cutoff) break;
205
+ if (!isIntentionalCleanupStopEntry(entry)) continue;
206
+ if (sessionId && entry.sessionId === sessionId) return true;
207
+ // Normalized node-id match (P4): the cleanup-stop entry's node id may be stored as
208
+ // `nodeId` or `node_id` and the `nodeId` arg can be in either form — a raw `===`
209
+ // would miss a genuine intentional-cleanup entry and fail to suppress the stop event.
210
+ if (!sessionId && nodeId && meshNodeIdMatches(entry as unknown as MeshNodeIdentified, nodeId)) return true;
211
+ }
212
+ return false;
213
+ }
214
+
215
+ function shouldSuppressIntentionalCleanupStop(args: {
216
+ event: string;
217
+ meshId: string;
218
+ metadataEvent: Record<string, unknown>;
219
+ sessionId?: string;
220
+ nodeId?: string;
221
+ }): boolean {
222
+ if (args.event !== 'agent:stopped' && args.event !== 'monitor:no_progress') return false;
223
+ if (isIntentionalCleanupStopMetadata(args.metadataEvent)) return true;
224
+ return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
225
+ }
226
+
227
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
228
+
229
+ function hasFingerprintSeen(meshId: string, fingerprint: string): boolean {
230
+ try {
231
+ return MeshRuntimeStore.getInstance().hasCompletionFingerprint(meshId, fingerprint);
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+
237
+ function recordFingerprintSeen(meshId: string, fingerprint: string): void {
238
+ try {
239
+ const db = MeshRuntimeStore.getInstance();
240
+ db.recordCompletionFingerprint(meshId, fingerprint, RECENT_COMPLETION_FINGERPRINT_TTL_MS);
241
+ db.sweepExpiredFingerprints();
242
+ } catch { /* best-effort; duplicate events are preferable to a crash */ }
243
+ }
244
+
245
+ function readEventTimestamp(value: unknown): number | null {
246
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
247
+ if (typeof value === 'string' && value.trim()) {
248
+ const numeric = Number(value);
249
+ if (Number.isFinite(numeric)) return numeric;
250
+ const parsed = Date.parse(value);
251
+ if (Number.isFinite(parsed)) return parsed;
252
+ }
253
+ return null;
254
+ }
255
+
256
+ function buildMeshCompletionFingerprint(args: {
257
+ meshId: string;
258
+ event: string;
259
+ sessionId: string;
260
+ providerType?: string;
261
+ providerSessionId?: string;
262
+ timestamp?: number | null;
263
+ finalSummary?: string;
264
+ coordinatorDaemonId?: string;
265
+ }): string {
266
+ const timestampPart = Number.isFinite(args.timestamp)
267
+ ? String(args.timestamp)
268
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
269
+ return [
270
+ args.meshId,
271
+ args.event,
272
+ args.sessionId,
273
+ args.providerType || '',
274
+ args.providerSessionId || '',
275
+ timestampPart,
276
+ args.coordinatorDaemonId || '',
277
+ ].join('::');
278
+ }
279
+
280
+ function isDuplicateMeshCompletionEvent(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
+ taskId?: string;
290
+ nodeId?: string;
291
+ }): boolean {
292
+ const fingerprint = buildMeshCompletionFingerprint(args);
293
+ if (!fingerprint) return false;
294
+ if (hasFingerprintSeen(args.meshId, fingerprint)) {
295
+ if (args.taskId) {
296
+ recordCompletionConflict({
297
+ meshId: args.meshId,
298
+ fingerprint,
299
+ conflictingTaskId: args.taskId,
300
+ conflictingSessionId: args.sessionId,
301
+ event: args.event,
302
+ });
303
+ }
304
+ return true;
305
+ }
306
+ recordFingerprintSeen(args.meshId, fingerprint);
307
+ return false;
308
+ }
309
+
310
+ function isDuplicateMeshApprovalEvent(args: {
311
+ meshId: string;
312
+ sessionId: string;
313
+ providerType?: string;
314
+ timestamp?: number | null;
315
+ modalMessage?: string;
316
+ modalButtons?: unknown;
317
+ }): boolean {
318
+ const modalButtons = Array.isArray(args.modalButtons)
319
+ ? args.modalButtons.map(button => String(button).trim()).filter(Boolean)
320
+ : [];
321
+ const approvalIdentity = Number.isFinite(args.timestamp)
322
+ ? String(args.timestamp)
323
+ : JSON.stringify({ message: args.modalMessage || '', buttons: modalButtons });
324
+ if (!approvalIdentity || approvalIdentity === '{"message":"","buttons":[]}') return false;
325
+ const fingerprint = [
326
+ args.meshId,
327
+ 'agent:waiting_approval',
328
+ args.sessionId,
329
+ args.providerType || '',
330
+ approvalIdentity,
331
+ ].join('::');
332
+ if (hasFingerprintSeen(args.meshId, fingerprint)) return true;
333
+ recordFingerprintSeen(args.meshId, fingerprint);
334
+ return false;
335
+ }
336
+
337
+ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
338
+ const jobId = readRefineJobId({ metadataEvent });
339
+ const fingerprint = jobId && new Set(['refine:completed', 'refine:failed']).has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
340
+ if (!fingerprint) return false;
341
+ if (hasFingerprintSeen(meshId, fingerprint)) return true;
342
+ recordFingerprintSeen(meshId, fingerprint);
343
+ return false;
344
+ }
345
+
346
+ // NOTIF-MISS (FIX 2): the set of `source` tags the transcript-reconcile synthesis path stamps
347
+ // onto the terminal ledger entry it writes (mesh-events-stale.reconcileDirectDispatchCompletion
348
+ // FromTranscript). A terminal carrying one of these was NOT produced by an actual provider
349
+ // agent_status_event — it is a coordinator-side reconstruction from the worker's transcript. The
350
+ // authoritative real completion must never be permanently masked by such a synthesized record.
351
+ const RECONCILED_COMPLETION_SOURCES = new Set([
352
+ 'direct_task_transcript_reconciliation',
353
+ 'daemon_reconcile_transcript_completion',
354
+ 'mcp_mesh_status_transcript_reconciliation',
355
+ 'no_progress_reconciliation',
356
+ ]);
357
+
358
+ // True when the recorded terminal ledger entry was SYNTHESIZED by transcript reconciliation
359
+ // rather than emitted by the real provider event. The reconcile path tags its ledger payload
360
+ // with a reconcile `source`; a genuine provider completion carries no such tag (or a normal
361
+ // completionDiagnostic.reason). Both the explicit source AND the diagnostic reason are checked so
362
+ // either carrier identifies the synthesized record.
363
+ function isSynthesizedReconciledTerminal(terminalPayload: Record<string, unknown>): boolean {
364
+ const source = readNonEmptyString(terminalPayload.source);
365
+ if (source && RECONCILED_COMPLETION_SOURCES.has(source)) return true;
366
+ const diagnostic = terminalPayload.completionDiagnostic;
367
+ if (diagnostic && typeof diagnostic === 'object' && !Array.isArray(diagnostic)) {
368
+ const reason = readNonEmptyString((diagnostic as Record<string, unknown>).reason);
369
+ if (reason && RECONCILED_COMPLETION_SOURCES.has(reason)) return true;
370
+ }
371
+ return false;
372
+ }
373
+
374
+ // True when the INCOMING completion event is a REAL provider agent_status_event (an actual
375
+ // agent:generating_completed from the live session) rather than itself a synthesized/reconciled
376
+ // re-injection. We must only let a real event override a synthesized terminal — a second
377
+ // synthesized re-arrival should still dedup normally. A real provider event carries no reconcile
378
+ // `source` tag; a re-injected reconciliation does.
379
+ function isRealProviderCompletionEvent(metadataEvent: Record<string, unknown>): boolean {
380
+ const source = readNonEmptyString(metadataEvent.source);
381
+ return !source || !RECONCILED_COMPLETION_SOURCES.has(source);
382
+ }
383
+
384
+ // The genuine-completion counterpart: a real final summary / worker result is present and
385
+ // the completion is not flagged as a missing-final-assistant false idle. Used to decide
386
+ // whether a new completion may supersede a prior WEAK (false-idle) terminal. NOTE this gates
387
+ // only on isFalseIdleCompletion (the completionDiagnostic subset), NOT the broader
388
+ // isWeakCompletionEvidence — preserving the original semantics where a self-declared
389
+ // weak/insufficient event still counts as genuine if it carries a real final summary.
390
+ function isGenuineCompletionEvidence(metadataEvent: Record<string, unknown>): boolean {
391
+ if (isFalseIdleCompletion(metadataEvent)) return false;
392
+ return !!readWorkerResultMetadata(metadataEvent) || !!readNonEmptyString(metadataEvent.finalSummary);
393
+ }
394
+
395
+ // (FALSEIDLE-BGCHILD-b) A later genuine completion of the SAME task that carries a
396
+ // substantively different — and fuller — final summary than the recorded terminal is the REAL
397
+ // final that an earlier (false-idle) completion pre-empted, not a duplicate. The background-child
398
+ // false idle is the nasty case the plain isWeakCompletionEvidence supersession misses: the
399
+ // early completion's screen parser DID see a prior/intermediate standard assistant, so it is
400
+ // recorded as a STRONG terminal with a non-empty (but truncated) finalSummary. Without this the
401
+ // providerSessionId/finalSummary dedup below swallows the genuine final and the coordinator is
402
+ // stuck with the truncated mid-turn text forever (the one-shot-consumption symptom). Same-task,
403
+ // new event is genuine, prior terminal summary is a strict prefix of (or otherwise shorter than)
404
+ // the new one → treat as the corrected final and let it through. Conservative: requires the new
405
+ // summary to be genuine evidence AND meaningfully longer, so an identical re-arrival or a SHORTER
406
+ // later summary is still deduped.
407
+ function supersedesTruncatedTerminalSummary(args: {
408
+ terminalPayload: Record<string, unknown>;
409
+ metadataEvent: Record<string, unknown>;
410
+ terminalTaskId: string;
411
+ eventTaskId: string;
412
+ }): boolean {
413
+ // Only applies when both name the SAME task (a distinct task is handled by distinctTaskCompletion).
414
+ if (!args.terminalTaskId || !args.eventTaskId || args.terminalTaskId !== args.eventTaskId) return false;
415
+ if (!isGenuineCompletionEvidence(args.metadataEvent)) return false;
416
+ const terminalSummary = readNonEmptyString(args.terminalPayload.finalSummary);
417
+ const eventSummary = readNonEmptyString(args.metadataEvent.finalSummary);
418
+ if (!eventSummary) return false;
419
+ // Identical text → genuine duplicate, keep deduping.
420
+ if (terminalSummary === eventSummary) return false;
421
+ // The recorded terminal was a known-weak (false-idle) one → already handled by the weak
422
+ // supersession path; nothing extra to do here.
423
+ if (isWeakCompletionEvidence(args.terminalPayload)) return false;
424
+ // No prior summary at all, or the new summary strictly extends / is meaningfully longer than
425
+ // the recorded one → the recorded terminal was the truncated pre-emption; supersede it.
426
+ if (!terminalSummary) return true;
427
+ if (eventSummary.startsWith(terminalSummary)) return true;
428
+ return eventSummary.length > terminalSummary.length + 32;
429
+ }
430
+
431
+ // The latest still-active direct-dispatch taskId for a session, resolved BEFORE the
432
+ // completion flips the dispatch row terminal. Direct dispatches (mesh_send_task) have no
433
+ // work-queue row, so this is the only taskId available to attribute the terminal ledger
434
+ // entry (and thus mesh task-stats) to — without it the terminal carries no taskId and the
435
+ // task surfaces as status='unknown' / terminalKind=null in computeMeshTaskStats.
436
+ function resolveActiveDirectDispatchTaskId(meshId: string, sessionId: string): string | undefined {
437
+ try {
438
+ const matches = getActiveDirectDispatches(meshId).filter(d => d.sessionId === sessionId);
439
+ if (!matches.length) return undefined;
440
+ // getActiveDirectDispatches returns rows ordered by dispatched_at ASC; the last is
441
+ // the most recent dispatch (the re-dispatch / nudge whose completion this is).
442
+ return readNonEmptyString(matches[matches.length - 1].taskId) || undefined;
443
+ } catch {
444
+ return undefined;
445
+ }
446
+ }
447
+
448
+ // Coordinator-side suppression/reconcile gate for an incoming mesh event. Each clause is a
449
+ // closed dedup/suppression concern that only inspects the event + already-resolved context and
450
+ // either (a) returns a `suppress` result the caller forwards verbatim, (b) returns a `reconcile`
451
+ // signal carrying the rewritten metadataEvent for the caller to re-inject as
452
+ // agent:generating_completed, or (c) returns null to let the event fall through to the
453
+ // terminal/ledger machinery. Extracted verbatim from injectMeshSystemMessage — no behavior
454
+ // change; the only side effects (best-effort remote-idle cleanup, LOG, trace) fire on the same
455
+ // paths as before.
456
+ function evaluateMeshEventSuppression(
457
+ args: {
458
+ meshId: string;
459
+ sourceInstanceId?: string;
460
+ nodeId?: string;
461
+ nodeLabel: string;
462
+ event: string;
463
+ metadataEvent: Record<string, unknown>;
464
+ },
465
+ ctx: {
466
+ traceCtx: Parameters<typeof traceMeshEventDrop>[1];
467
+ eventSessionId: string;
468
+ eventNodeId: string;
469
+ eventTimestamp: number | null;
470
+ workerCoordinatorDaemonId: string | undefined;
471
+ },
472
+ ):
473
+ | { kind: 'suppress'; result: { success: true; forwarded: 0; suppressed: true; [extra: string]: unknown } }
474
+ | { kind: 'reconcile'; metadataEvent: Record<string, unknown> }
475
+ | null {
476
+ const { traceCtx, eventSessionId, eventNodeId, eventTimestamp, workerCoordinatorDaemonId } = ctx;
477
+
478
+ const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
479
+ event: args.event,
480
+ meshId: args.meshId,
481
+ metadataEvent: args.metadataEvent,
482
+ sessionId: eventSessionId || undefined,
483
+ nodeId: eventNodeId || undefined,
484
+ });
485
+ if (intentionalCleanupStop) {
486
+ if (eventSessionId && eventNodeId) {
487
+ try {
488
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, eventNodeId, eventSessionId);
489
+ } catch { /* best-effort */ }
490
+ }
491
+ LOG.info('MeshEvents', `Suppressed ${args.event} for intentionally cleanup-stopped session ${eventSessionId || '(unknown session)'}`);
492
+ traceMeshEventDrop('intentional_cleanup_stop', traceCtx);
493
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true } };
494
+ }
495
+
496
+ if (args.event === 'monitor:no_progress') {
497
+ const reconciledCompletion = buildNoProgressCompletionReconciliation({
498
+ meshId: args.meshId,
499
+ nodeId: args.nodeId,
500
+ nodeLabel: args.nodeLabel,
501
+ metadataEvent: args.metadataEvent,
502
+ sourceInstanceId: args.sourceInstanceId,
503
+ });
504
+ if (reconciledCompletion?.source === 'no_progress_reconciliation') {
505
+ LOG.info('MeshEvents', `Reconciled no-progress monitor to completion for session ${eventSessionId || '(unknown session)'}`);
506
+ return { kind: 'reconcile', metadataEvent: reconciledCompletion };
507
+ }
508
+ if (reconciledCompletion?.source === 'no_progress_terminal_ledger_suppression') {
509
+ LOG.info('MeshEvents', `Suppressed no-progress monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
510
+ traceMeshEventDrop('no_progress_terminal_ledger_suppression', traceCtx, `terminalKind=${reconciledCompletion.terminalLedgerKind}`);
511
+ return {
512
+ kind: 'suppress',
513
+ result: {
514
+ success: true,
515
+ forwarded: 0,
516
+ suppressed: true,
517
+ terminalLedgerEvidence: true,
518
+ terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
519
+ },
520
+ };
521
+ }
522
+ }
523
+
524
+ if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
525
+ LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
526
+ traceMeshEventDrop('duplicate_refine_terminal', traceCtx);
527
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true } };
528
+ }
529
+
530
+ if (args.event === 'agent:waiting_approval' && eventSessionId) {
531
+ const duplicateApproval = isDuplicateMeshApprovalEvent({
532
+ meshId: args.meshId,
533
+ sessionId: eventSessionId,
534
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
535
+ timestamp: eventTimestamp,
536
+ modalMessage: readNonEmptyString(args.metadataEvent.modalMessage) || undefined,
537
+ modalButtons: args.metadataEvent.modalButtons,
538
+ });
539
+ if (duplicateApproval) {
540
+ LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
541
+ traceMeshEventDrop('duplicate_approval', traceCtx);
542
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateApproval: true } };
543
+ }
544
+ }
545
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
546
+ const terminal = findRecentTerminalLedgerEvidence({
547
+ meshId: args.meshId,
548
+ sessionId: eventSessionId,
549
+ nodeId: eventNodeId || undefined,
550
+ });
551
+ if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
552
+ const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
553
+ // Fix B (re-dispatch 2nd-completion routing): a prior terminal recorded from a FALSE
554
+ // idle (weak evidence / no confirmed final assistant) must NOT permanently suppress a
555
+ // later GENUINE completion of the same session. providerSessionId is stable across a
556
+ // session's turns, so the providerSessionId/finalSummary dedup below would otherwise
557
+ // swallow the real 2nd-turn completion that a coordinator nudge (direct re-dispatch)
558
+ // drove — exactly the missed-event bug. When the prior terminal was weak and the new
559
+ // event carries genuine completion evidence, let it through so it is recorded and
560
+ // re-attributed to the latest task (the normal task_completed path below).
561
+ const supersedesWeakTerminal = isWeakCompletionEvidence(terminal.payload)
562
+ && isGenuineCompletionEvidence(args.metadataEvent);
563
+ // CANON-B (direct-dispatch completion race): a FAST direct dispatch (mesh_send_task)
564
+ // to an already-idle, previously-used session can have its genuine completion reach
565
+ // this coordinator handler BEFORE the dispatching side records the new task's dispatch
566
+ // row / task_dispatched ledger entry — insertDirectDispatch + appendLedgerEntry both run
567
+ // AFTER the agent_command await resolves, while the worker may already be done. In that
568
+ // window sessionHasActiveAssignment is false (no active dispatch row, no unterminal
569
+ // ledger entry yet), so this prior-terminal dedup engages; and because providerSessionId
570
+ // is STABLE across a reused session's turns, the providerSessionId/finalSummary match
571
+ // below would suppress the NEW task's completion as a duplicate of the PRIOR task —
572
+ // silently losing it (the observed intermittent miss; fresh enqueue/autoLaunch is immune
573
+ // because a fresh session has no prior same-providerSessionId terminal and the queue row
574
+ // is claimed atomically before dispatch). The echoed taskId is the authoritative
575
+ // discriminator: when the completion names a DIFFERENT task than the recorded terminal,
576
+ // it is a genuinely new task's completion, never a duplicate — let it through so it is
577
+ // attributed to its own taskId. A same-task re-arrival (taskId equal) or a taskId-less
578
+ // legacy event still falls through to the providerSessionId/finalSummary dedup.
579
+ const terminalTaskId = readNonEmptyString(terminal.payload.taskId);
580
+ const eventTaskId = readNonEmptyString(args.metadataEvent.taskId);
581
+ const distinctTaskCompletion = !!eventTaskId && !!terminalTaskId && eventTaskId !== terminalTaskId;
582
+ // (FALSEIDLE-BGCHILD-b) Same-task genuine completion carrying a fuller summary than the
583
+ // recorded (truncated, false-idle-pre-empted) terminal supersedes it — see helper.
584
+ const supersedesTruncatedTerminal = supersedesTruncatedTerminalSummary({
585
+ terminalPayload: terminal.payload,
586
+ metadataEvent: args.metadataEvent,
587
+ terminalTaskId,
588
+ eventTaskId,
589
+ });
590
+ // NOTIF-MISS (FIX 2): the recorded terminal was SYNTHESIZED by transcript reconciliation
591
+ // (no real provider event), and THIS incoming event is the authoritative REAL provider
592
+ // completion. The reconcile path may fire ~1s after a direct dispatch from a reused
593
+ // session's stale transcript tail, writing a synthesized terminal whose providerSessionId
594
+ // (stable across a session's turns) and/or finalSummary then match the real completion —
595
+ // so the providerSessionId/finalSummary dedup below would drop the genuine event and the
596
+ // coordinator would never learn the task finished. A synthesized record must NEVER
597
+ // permanently mask the real completion: let the real event through (it is re-recorded by
598
+ // the normal task_completed path, superseding the synthesized one). A second SYNTHESIZED
599
+ // re-arrival is NOT a real event and still dedups normally.
600
+ // RECONCILE-SYNTH-PREEMPTS-COMPLETION: the bar here is deliberately LOWER than the
601
+ // weak/truncated supersessions above — a synthesized terminal is a coordinator-side
602
+ // reconstruction, never a real provider event, so ANY genuine real provider completion
603
+ // for the session must win over it. Requiring the full isGenuineCompletionEvidence
604
+ // (finalSummary OR workerResult present) dropped the real event whenever the relay did
605
+ // not re-populate finalSummary at this layer — exactly the observed 71s task whose real
606
+ // generating_completed hit drop:duplicate_completion_terminal_ledger after a premature
607
+ // synth. We require only that the incoming event is a REAL provider completion that is
608
+ // not itself a false idle (missing-final-assistant); a real-but-false-idle event still
609
+ // does not supersede (it is not trustworthy terminal evidence either).
610
+ const supersedesSynthesizedTerminal = isSynthesizedReconciledTerminal(terminal.payload)
611
+ && isRealProviderCompletionEvent(args.metadataEvent)
612
+ && !isFalseIdleCompletion(args.metadataEvent);
613
+ if (!newDispatchAfterTerminal && !supersedesWeakTerminal && !distinctTaskCompletion && !supersedesTruncatedTerminal && !supersedesSynthesizedTerminal) {
614
+ const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
615
+ const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
616
+ const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
617
+ const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
618
+ if (
619
+ (terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
620
+ || (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
621
+ || args.metadataEvent.source === 'no_progress_reconciliation'
622
+ ) {
623
+ LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
624
+ traceMeshEventDrop('duplicate_completion_terminal_ledger', traceCtx);
625
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true } };
626
+ }
627
+ }
628
+ }
629
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
630
+ meshId: args.meshId,
631
+ event: args.event,
632
+ sessionId: eventSessionId,
633
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
634
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
635
+ timestamp: eventTimestamp,
636
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
637
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
638
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
639
+ nodeId: eventNodeId || undefined,
640
+ });
641
+ if (duplicateCompletion) {
642
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
643
+ traceMeshEventDrop('duplicate_completion', traceCtx);
644
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true } };
645
+ }
646
+ }
647
+ if (args.event === 'agent:stopped' && eventSessionId) {
648
+ const duplicateStopped = isDuplicateMeshCompletionEvent({
649
+ meshId: args.meshId,
650
+ event: args.event,
651
+ sessionId: eventSessionId,
652
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
653
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
654
+ timestamp: eventTimestamp,
655
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
656
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
657
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
658
+ nodeId: eventNodeId || undefined,
659
+ });
660
+ if (duplicateStopped) {
661
+ LOG.info('MeshEvents', `Suppressed duplicate stopped event for mesh ${args.meshId} session ${eventSessionId}`);
662
+ traceMeshEventDrop('duplicate_stopped', traceCtx);
663
+ return { kind: 'suppress', result: { success: true, forwarded: 0, suppressed: true, duplicateStopped: true } };
664
+ }
665
+ }
666
+
667
+ return null;
668
+ }
669
+
670
+ function injectMeshSystemMessage(components: DaemonComponents, args: {
671
+ meshId: string;
672
+ sourceInstanceId?: string;
673
+ nodeId?: string;
674
+ nodeLabel: string;
675
+ event: string;
676
+ metadataEvent: Record<string, unknown>;
677
+ }) {
678
+ const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
679
+ const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
680
+
681
+ // EVTTRACE correlation context for this event's coordinator-side lifecycle (queue /
682
+ // dedup / suppress). Observation only — never read by any decision below.
683
+ const traceCtx = {
684
+ taskId: args.metadataEvent.taskId,
685
+ sessionId: eventSessionId,
686
+ nodeId: eventNodeId,
687
+ meshId: args.meshId,
688
+ event: args.event,
689
+ };
690
+
691
+ const sourceSession = args.sourceInstanceId
692
+ ? components.instanceManager.getInstance(args.sourceInstanceId)
693
+ : undefined;
694
+ const workerCoordinatorDaemonId = readNonEmptyString(
695
+ (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
696
+ );
697
+ // Session-level routing anchor (multi-coordinator). Prefer the LIVE worker session's
698
+ // stamp; fall back to a relayed value carried in metadataEvent.meshCoordinatorSessionId
699
+ // (a remote worker's completion arrives via handleMeshForwardEvent with no local
700
+ // sourceSession, so the stamp can only ride in the relayed metadata). Empty on legacy /
701
+ // version-skewed dispatches → the event stays daemon-broadcast (no regression).
702
+ const workerCoordinatorSessionId = readNonEmptyString(
703
+ (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorSessionId,
704
+ ) || readNonEmptyString(args.metadataEvent.meshCoordinatorSessionId);
705
+
706
+ // T2: a summary-less completion (and any non-completion status-sync event) carries no
707
+ // assistant text on the event, so resolveMeshSurfacedSessionPreview had nothing to surface
708
+ // and the coordinator's inbox mirror stayed stuck on the first dispatched user task. When
709
+ // THIS daemon hosts the live worker instance (sourceSession present), derive the worker's
710
+ // latest display message straight from its transcript and attach it to the event as
711
+ // lastMessagePreview/lastMessageRole/lastMessageAt. resolveMeshSurfacedSessionPreview reads
712
+ // these as an assistant-only fallback; they also ride the pending-queue + P2P relay
713
+ // (handleMeshForwardEvent whitelist) so a remote coordinator can surface them. A remote
714
+ // coordinator has no local instance and keeps relying on the relayed fields — unchanged.
715
+ const enrichedMetadataEvent = ((): Record<string, unknown> => {
716
+ const last = sourceSession ? getLastDisplayMessage(sourceSession.getState()) : null;
717
+ if (!last || !last.preview) return args.metadataEvent;
718
+ return {
719
+ ...args.metadataEvent,
720
+ lastMessagePreview: last.preview,
721
+ lastMessageRole: last.role,
722
+ ...(last.receivedAt > 0 ? { lastMessageAt: last.receivedAt } : {}),
723
+ };
724
+ })();
725
+
726
+ // R2: cloud P2P dashboard metadata sync. The cloud daemon used to do this from its own
727
+ // relay listener; now the single core forwarder invokes the injected hook (no-op on
728
+ // standalone) so the event path stays single-listener and the local code path is identical
729
+ // across standalone and cloud.
730
+ if (components.onMeshCoordinatorEventForwarded) {
731
+ try {
732
+ // T: the coordinator surfaces a remote worker's session but holds no local
733
+ // instance for it, so the status snapshot can't derive a preview and the
734
+ // mirror would stay stuck on the first dispatched user task. Resolve the
735
+ // worker's latest assistant reply (carried on the completion event's
736
+ // finalSummary / workerResult) into a preview the mirror can stamp, so the
737
+ // mobile inbox reflects the assistant response. Completion events carry assistant
738
+ // text as finalSummary; a summary-less completion / status sync falls back to the
739
+ // worker's latest assistant display message (enrichedMetadataEvent.lastMessage*).
740
+ // For a mid-turn user-only event this is undefined and the prior surfaced preview
741
+ // is preserved downstream (no clobber).
742
+ const surfacedPreview = resolveMeshSurfacedSessionPreview(enrichedMetadataEvent);
743
+ components.onMeshCoordinatorEventForwarded({
744
+ event: args.event,
745
+ meshId: args.meshId,
746
+ nodeId: eventNodeId || undefined,
747
+ ...enrichedMetadataEvent,
748
+ // Ensure a `workspace` field reaches updateMeshOwnedSession even when the
749
+ // worker provider event only carried `workspaceName`. The merge spread of
750
+ // metadataEvent above wins when it already has a non-empty `workspace`.
751
+ workspace: readNonEmptyString(args.metadataEvent.workspace)
752
+ || readNonEmptyString(args.metadataEvent.workspaceName)
753
+ || undefined,
754
+ ...(surfacedPreview ? {
755
+ meshSessionLastMessagePreview: surfacedPreview.preview,
756
+ meshSessionLastMessageRole: surfacedPreview.role,
757
+ meshSessionLastMessageAt: surfacedPreview.receivedAt || undefined,
758
+ } : {}),
759
+ });
760
+ } catch { /* dashboard metadata sync is best-effort */ }
761
+ }
762
+
763
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
764
+ // Coordinator-side dedup/suppression gate (extracted, behavior-preserving). A non-null
765
+ // outcome either short-circuits with a forwarded result or signals a no-progress→completion
766
+ // reconciliation that we re-inject; null lets the event fall through to the ledger machinery.
767
+ const suppression = evaluateMeshEventSuppression(args, {
768
+ traceCtx,
769
+ eventSessionId,
770
+ eventNodeId,
771
+ eventTimestamp,
772
+ workerCoordinatorDaemonId,
773
+ });
774
+ if (suppression) {
775
+ if (suppression.kind === 'reconcile') {
776
+ return injectMeshSystemMessage(components, {
777
+ ...args,
778
+ event: 'agent:generating_completed',
779
+ metadataEvent: suppression.metadataEvent,
780
+ });
781
+ }
782
+ return suppression.result;
783
+ }
784
+
785
+ function markSessionTerminal(sessionId: string, outcome: 'completed' | 'failed', occurredAtMs?: number | null, opts?: { tentativeIfDirect?: boolean }): { id?: string } | null {
786
+ // C2: prefer an exact taskId match when the completion event carries one —
787
+ // it's immune to coordinator↔worker clock skew that can hide the assigned row.
788
+ const eventTaskId = readNonEmptyString(args.metadataEvent.taskId) || undefined;
789
+ const task = updateSessionTaskStatus(args.meshId, sessionId, outcome, {
790
+ occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
791
+ taskId: eventTaskId,
792
+ });
793
+ // Fix A (early-terminal prevention): a false-idle completion (no confirmed final
794
+ // assistant) for a DIRECT dispatch — i.e. no work-queue row matched — must not flip the
795
+ // dispatch row terminal. Leaving it active lets the reconcile loop (PHASE 4) re-read the
796
+ // transcript and record the genuine completion once the worker truly finishes (commonly
797
+ // after a coordinator nudge / re-dispatch). A matched queue task, or a completion with
798
+ // genuine evidence, is marked terminal as before.
799
+ // WARMUPGAP: a no-taskId completion from a session that holds no active assignment is a
800
+ // pre-assignment warmup / ghost event (a worker spawns, idles, and emits idle→generating→
801
+ // completed before any task is dispatched, with meshActiveTaskId unset so the event carries
802
+ // no taskId). Letting it through would hit the session_id fallback in updateDirectDispatchStatus
803
+ // and flip a sibling/stale dispatch row this event does not own — the real task later lands on
804
+ // a corrupted row and never reaches completed. Skip the dispatch update for that case. A
805
+ // taskId-carrying completion (real task), or any completion whose session currently holds an
806
+ // active assignment (legacy/relayed worker), still flips as before.
807
+ const leaveDirectDispatchActive = (!task && opts?.tentativeIfDirect === true)
808
+ || (!eventTaskId && !sessionHasActiveAssignment(args.meshId, sessionId));
809
+ if (!leaveDirectDispatchActive) {
810
+ // CANON-B: flip the exact dispatch row the completion echoed its taskId for; the
811
+ // session_id fallback (no echoed taskId) still covers legacy/relayed workers.
812
+ updateDirectDispatchStatus(args.meshId, sessionId, outcome, eventTaskId);
813
+ }
814
+ markSessionDeliveriesTerminal(args.meshId, sessionId, outcome);
815
+ setImmediate(() => cleanupTerminalDirectDispatches());
816
+ return task ? { id: task.id } : null;
817
+ }
818
+
819
+ let completedTaskForLedger: { id?: string } | null = null;
820
+ // Fix B: direct-dispatch taskId used to attribute the terminal ledger entry when no
821
+ // work-queue row matches (resolved BEFORE markSessionTerminal flips the dispatch terminal).
822
+ let directDispatchTaskIdForLedger: string | undefined;
823
+ if (args.event === 'agent:generating_completed') {
824
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
825
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
826
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
827
+
828
+ if (sessionId) {
829
+ // CANON-B: trust the taskId the completion echoed; only fall back to the
830
+ // most-recent-by-session heuristic when the worker carried none.
831
+ directDispatchTaskIdForLedger = readNonEmptyString(args.metadataEvent.taskId)
832
+ || resolveActiveDirectDispatchTaskId(args.meshId, sessionId);
833
+ // A false-idle completion of a direct dispatch is recorded but kept tentative (the
834
+ // dispatch row stays active for the reconcile fallback); a genuine completion is terminal.
835
+ const isFalseIdle = isFalseIdleCompletion(args.metadataEvent);
836
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed', eventTimestamp, { tentativeIfDirect: isFalseIdle });
837
+ if (nodeId && providerType) {
838
+ // OVEREAGER-REMOTE-IDLE (Defect A+B): re-register the now-idle remote session into
839
+ // the remote-idle store, symmetric with the agent:ready branch. Previously
840
+ // setRemoteIdleSession ran ONLY on agent:ready, while agent:generating_started
841
+ // DELETES the entry — so the FIRST turn a remote worker runs permanently evicts it
842
+ // from the store, and generating_completed never re-added it. A later
843
+ // mesh_enqueue_task's triggerMeshQueue then read getRemoteIdleSessions() == 0
844
+ // (remoteIdleSessionsChecked:0) for a session that is genuinely live-idle, needlessly
845
+ // auto-launching a second worker (Defect A). Worse, the two idle-session sources then
846
+ // disagreed: the enqueue drain saw 0 (store empty) and auto-launched + left the queue
847
+ // task pending, while THIS completing session's runIdleMaintenanceThenAssignQueue
848
+ // claimed the same still-pending row straight from SQL — so the task body injected into
849
+ // BOTH the reused idle session and the auto-launched one (Defect B). Re-registering here
850
+ // unifies the source: the next triggerMeshQueue drain sees the live idle session, reuses
851
+ // it (claimed:true, no auto-launch), and injects exactly once. Skip a false-idle
852
+ // (mid-turn / no-final-assistant) completion — that session is NOT genuinely idle.
853
+ if (!isFalseIdle) {
854
+ sweepExpiredRemoteIdleSessions();
855
+ try {
856
+ MeshRuntimeStore.getInstance().setRemoteIdleSession(args.meshId, nodeId, sessionId, providerType, Date.now() + REMOTE_IDLE_SESSION_TTL_MS);
857
+ } catch { /* best-effort */ }
858
+ setImmediate(() => {
859
+ maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
860
+ .finally(() => {
861
+ try {
862
+ // Claim for THIS session first; on success drop the just-registered
863
+ // idle entry so the enqueue drain doesn't re-pick an already-busy session.
864
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
865
+ if (assigned) MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
866
+ } catch (e: any) {
867
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after completion for ${nodeId}: ${e?.message || e}`);
868
+ }
869
+ });
870
+ });
871
+ } else {
872
+ runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
873
+ }
874
+ }
875
+ // M1-3: wake dependents of the completed task. The maintenance path above
876
+ // only assigns to the completing session; dependents may be claimable by
877
+ // other idle sessions, so run a full queue trigger when any are waiting.
878
+ const completedTaskId = completedTaskForLedger?.id;
879
+ if (completedTaskId && hasPendingDependents(args.meshId, completedTaskId)) {
880
+ setImmediate(() => {
881
+ triggerMeshQueue(components, args.meshId).catch((e: any) => {
882
+ LOG.warn('MeshQueue', `Dependent wake after task ${completedTaskId} failed: ${e?.message || e}`);
883
+ });
884
+ });
885
+ }
886
+ }
887
+ } else if (args.event === 'agent:ready') {
888
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
889
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
890
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
891
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
892
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
893
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
894
+ const hasCompletionEvidence = !!finalSummary || !!workerResult;
895
+ if (sessionId && hasCompletionEvidence) {
896
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed');
897
+ if (completedTaskForLedger) {
898
+ try {
899
+ appendLedgerEntry(args.meshId, {
900
+ kind: 'task_completed',
901
+ nodeId: nodeId || undefined,
902
+ sessionId,
903
+ providerType: providerType || undefined,
904
+ payload: {
905
+ event: args.event,
906
+ nodeLabel: args.nodeLabel,
907
+ taskId: completedTaskForLedger.id,
908
+ completedViaReady: true,
909
+ providerSessionId,
910
+ finalSummary,
911
+ workerResult,
912
+ evidence: buildTaskCompletionEvidence({
913
+ event: 'agent:ready',
914
+ nodeId,
915
+ sessionId,
916
+ providerType: providerType || undefined,
917
+ providerSessionId,
918
+ finalSummary,
919
+ workerResult,
920
+ }),
921
+ },
922
+ });
923
+ } catch (e: any) {
924
+ LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
925
+ }
926
+ }
927
+ }
928
+
929
+ if (sessionId && nodeId && providerType) {
930
+ sweepExpiredRemoteIdleSessions();
931
+ try {
932
+ MeshRuntimeStore.getInstance().setRemoteIdleSession(args.meshId, nodeId, sessionId, providerType, Date.now() + REMOTE_IDLE_SESSION_TTL_MS);
933
+ } catch { /* best-effort */ }
934
+ setImmediate(() => {
935
+ maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
936
+ .finally(() => {
937
+ try {
938
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
939
+ if (assigned) MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
940
+ } catch (e: any) {
941
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
942
+ }
943
+ });
944
+ });
945
+ }
946
+ } else if (args.event === 'agent:generating_started') {
947
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
948
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
949
+ if (sessionId && nodeId) {
950
+ try {
951
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
952
+ } catch { /* best-effort */ }
953
+ }
954
+ if (sessionId) {
955
+ // CANON-B: a generating_started that echoes its taskId acks exactly the dispatch
956
+ // and the delivery for THAT task — not every in-flight dispatch/delivery on the
957
+ // session. A session that already holds a freshly-dispatched (still 'dispatched')
958
+ // sibling must keep that row 'dispatched' so its own confirm can match it; acking
959
+ // by session would mark it 'acked' prematurely and hide a genuine non-delivery.
960
+ const startedTaskId = readNonEmptyString(args.metadataEvent.taskId) || undefined;
961
+ // WARMUPGAP: only ack a dispatch row when the event names its task, or the session
962
+ // currently holds an active assignment. A no-taskId generating_started from an
963
+ // unassigned session is a pre-assignment warmup — the session_id fallback would ack a
964
+ // sibling/stale dispatch row this event does not own, marking it 'acked' prematurely and
965
+ // hiding a genuine non-delivery. Skip the dispatch ack for that ghost case (the delivery
966
+ // acks below are bound to actual deliveries and stay a no-op for a warmup session).
967
+ //
968
+ // MESH-DISPATCH-MISROUTE (fix 3, consumer residual): when the event carries no taskId
969
+ // (a legacy/relayed worker whose producer never stamped meshActiveTaskId) but the
970
+ // session owns EXACTLY ONE active dispatch, resolve that row's taskId and flip it by PK
971
+ // instead of the session_id sweep — the sweep flips every non-terminal row for the
972
+ // session ("may flip a sibling dispatch row"). With ≥2 active rows the owner is
973
+ // ambiguous, so resolvedAckTaskId stays undefined and we DROP the ack rather than
974
+ // mis-flip a sibling (the genuine ack arrives once the producer/reconcile names a task).
975
+ if (startedTaskId) {
976
+ updateDirectDispatchStatus(args.meshId, sessionId, 'acked', startedTaskId);
977
+ } else if (sessionHasActiveAssignment(args.meshId, sessionId)) {
978
+ const soleTaskId = (() => {
979
+ try { return MeshRuntimeStore.getInstance().getSoleActiveDirectDispatchTaskId(args.meshId, sessionId); }
980
+ catch { return null; }
981
+ })();
982
+ if (soleTaskId) {
983
+ updateDirectDispatchStatus(args.meshId, sessionId, 'acked', soleTaskId);
984
+ }
985
+ }
986
+ const activeDeliveries = ((): { id: string; taskId: string | null }[] => {
987
+ try { return MeshRuntimeStore.getInstance().getActiveSessionDeliveries(args.meshId, sessionId); }
988
+ catch { return []; }
989
+ })();
990
+ const deliveriesToAck = startedTaskId
991
+ ? activeDeliveries.filter(d => d.taskId === startedTaskId)
992
+ : activeDeliveries;
993
+ for (const d of deliveriesToAck) {
994
+ updateSessionDeliveryStatus(d.id, 'acked');
995
+ }
996
+ }
997
+ } else if (args.event === 'agent:stopped') {
998
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
999
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1000
+ if (sessionId && nodeId) {
1001
+ try {
1002
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
1003
+ } catch { /* best-effort */ }
1004
+ }
1005
+ if (sessionId) {
1006
+ // CANON-B: prefer the echoed taskId; session heuristic is the fallback.
1007
+ directDispatchTaskIdForLedger = readNonEmptyString(args.metadataEvent.taskId)
1008
+ || resolveActiveDirectDispatchTaskId(args.meshId, sessionId);
1009
+ completedTaskForLedger = markSessionTerminal(sessionId, 'failed');
1010
+ }
1011
+ }
1012
+
1013
+ const ledgerKind = EVENT_TO_LEDGER_KIND[args.event];
1014
+ if (ledgerKind) {
1015
+ try {
1016
+ const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
1017
+ const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
1018
+ const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
1019
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1020
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1021
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1022
+ const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
1023
+ ? buildTaskCompletionEvidence({
1024
+ event: 'agent:generating_completed',
1025
+ nodeId: ledgerNodeId,
1026
+ sessionId: ledgerSessionId,
1027
+ providerType: ledgerProviderType,
1028
+ providerSessionId,
1029
+ finalSummary,
1030
+ workerResult,
1031
+ })
1032
+ : undefined;
1033
+ appendLedgerEntry(args.meshId, {
1034
+ kind: ledgerKind,
1035
+ nodeId: ledgerNodeId,
1036
+ sessionId: ledgerSessionId,
1037
+ providerType: ledgerProviderType,
1038
+ payload: {
1039
+ event: args.event,
1040
+ nodeLabel: args.nodeLabel,
1041
+ // Fix B: fall back to the direct-dispatch taskId when no work-queue row
1042
+ // matched, so the terminal entry is attributable in mesh task-stats
1043
+ // (otherwise the direct task shows status='unknown' / terminalKind=null).
1044
+ taskId: completedTaskForLedger?.id || directDispatchTaskIdForLedger || undefined,
1045
+ providerSessionId,
1046
+ finalSummary,
1047
+ workerResult,
1048
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
1049
+ ? args.metadataEvent.completionDiagnostic
1050
+ : undefined,
1051
+ evidence: completionEvidence,
1052
+ // B2: evidenceLevel lets coordinator know when completion evidence is insufficient.
1053
+ ...(completionEvidence
1054
+ ? completionEvidence.workerResult.source === 'default'
1055
+ ? { evidenceLevel: 'insufficient', reviewRecommended: true }
1056
+ : { evidenceLevel: 'sufficient' }
1057
+ : {}),
1058
+ },
1059
+ });
1060
+ } catch (e: any) {
1061
+ LOG.warn('MeshLedger', `Failed to record ${ledgerKind}: ${e?.message || e}`);
1062
+ }
1063
+ }
1064
+
1065
+ let recoveryContext: SessionRecoveryContext | null = null;
1066
+ if (args.event === 'agent:stopped') {
1067
+ try {
1068
+ const mesh = getMesh(args.meshId);
1069
+ const maxRetries = mesh?.policy?.maxTaskRetries ?? 1;
1070
+
1071
+ recoveryContext = getSessionRecoveryContext(args.meshId, {
1072
+ sessionId: resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined,
1073
+ nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
1074
+ maxRetries,
1075
+ });
1076
+ recoveryContext.failedProviderType = readNonEmptyString(args.metadataEvent.providerType) || null;
1077
+
1078
+ if (recoveryContext.retryRecommended && recoveryContext.consecutiveNodeFailures > 0) {
1079
+ appendLedgerEntry(args.meshId, {
1080
+ kind: 'recovery_attempted',
1081
+ nodeId: recoveryContext.failedNodeId || undefined,
1082
+ sessionId: recoveryContext.failedSessionId || undefined,
1083
+ providerType: recoveryContext.failedProviderType || undefined,
1084
+ payload: {
1085
+ consecutiveFailures: recoveryContext.consecutiveNodeFailures,
1086
+ taskAttemptCount: recoveryContext.taskAttemptCount,
1087
+ retryRecommended: recoveryContext.retryRecommended,
1088
+ advice: recoveryContext.advice,
1089
+ },
1090
+ });
1091
+
1092
+ if (recoveryContext.lastTaskMessage && recoveryContext.failedNodeId && recoveryContext.failedProviderType) {
1093
+ const autoNodeId = recoveryContext.failedNodeId;
1094
+ try {
1095
+ const task = enqueueTask(args.meshId, recoveryContext.lastTaskMessage, {
1096
+ targetNodeId: autoNodeId
1097
+ });
1098
+ LOG.info('MeshRecovery', `Auto-requeued failed task: ${task.id} for node ${autoNodeId}`);
1099
+
1100
+ const node = mesh?.nodes.find((n: any) => meshNodeIdMatches(n, autoNodeId));
1101
+ if (node) {
1102
+ components.cliManager.handleCliCommand('launch_cli', {
1103
+ cliType: recoveryContext.failedProviderType,
1104
+ dir: node.workspace,
1105
+ settings: {
1106
+ role: 'worker',
1107
+ meshNodeFor: args.meshId,
1108
+ meshNodeId: node.id,
1109
+ spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
1110
+ // Coordinator-dispatched recovery relaunch: same auto-approve
1111
+ // policy as the primary worker launch path.
1112
+ autoApprove: resolveDelegatedWorkerAutoApprove(mesh?.policy, node?.policy),
1113
+ launchedByCoordinator: true,
1114
+ }
1115
+ }).catch((e: any) => LOG.error('MeshRecovery', `Failed to auto-relaunch session for ${node.id}: ${e?.message}`));
1116
+ }
1117
+ } catch (e: any) {
1118
+ LOG.warn('MeshRecovery', `Failed to execute auto-recovery: ${e?.message}`);
1119
+ }
1120
+ }
1121
+ }
1122
+
1123
+ LOG.info('MeshRecovery', `Recovery context for ${args.nodeLabel}: ${recoveryContext.advice}`);
1124
+ } catch (e: any) {
1125
+ LOG.warn('MeshRecovery', `Failed to build recovery context: ${e?.message || e}`);
1126
+ }
1127
+ }
1128
+
1129
+ const messageText = buildMeshSystemMessage({
1130
+ event: args.event,
1131
+ nodeLabel: args.nodeLabel,
1132
+ metadataEvent: args.metadataEvent,
1133
+ recoveryContext,
1134
+ });
1135
+ if (!messageText) {
1136
+ // Lifecycle events that carry no coordinator-facing message (agent:ready /
1137
+ // agent:generating_started) still drive the remote-claim state machine: the
1138
+ // coordinator's agent:ready branch above runs setRemoteIdleSession +
1139
+ // tryAssignQueueTask, and agent:generating_started clears the remote-idle entry.
1140
+ // For a LOCAL worker whose coordinator is a REMOTE daemon those side effects ran
1141
+ // on the wrong daemon (this worker's empty queue / store), so the coordinator never
1142
+ // learns the auto-launched session went idle and re-auto-launches it forever
1143
+ // (queue task stuck pending). Queue the silent event so the coordinator pulls it
1144
+ // (PHASE 1 pullRemoteNodeQueues → handleMeshForwardEvent) and re-runs the claim on
1145
+ // the daemon that actually owns the queue. Gate strictly on a present, REMOTE
1146
+ // coordinator daemon id: a co-located worker already ran the claim on the right
1147
+ // daemon, and a coordinator processing a *pulled* event has no sourceSession so
1148
+ // workerCoordinatorDaemonId is empty — neither re-queues, so there is no loop.
1149
+ const isSilentClaimRelevantEvent = args.event === 'agent:ready' || args.event === 'agent:generating_started';
1150
+ const coordinatorIsRemote = !!workerCoordinatorDaemonId
1151
+ && !resolveCoordinatorDrainDaemonIds(components).includes(workerCoordinatorDaemonId);
1152
+ if (!(isSilentClaimRelevantEvent && coordinatorIsRemote)) {
1153
+ return { success: false, error: 'unsupported mesh event' };
1154
+ }
1155
+ }
1156
+
1157
+ // ── Queue-only delivery (single-model: queue + periodic poll) ──────────────
1158
+ // Every mesh coordinator event — terminal or not, local-coordinator or
1159
+ // remote — is persisted to the pending-events queue (SQLite + JSONL) and
1160
+ // NOTHING is pushed here. The old spontaneous-forward paths were removed:
1161
+ // - F1 remote P2P `mesh_forward_event` dispatch (network/stamp-dependent,
1162
+ // silently dropped on P2P failure or missing meshCoordinatorDaemonId)
1163
+ // - F3 live-CLI PTY `send_message` fire-and-forget inject (silently
1164
+ // dropped when the coordinator was generating)
1165
+ // Delivery to a live CLI coordinator now happens via setupMeshReconcileLoop,
1166
+ // which drains this queue on a fixed interval and injects into the coordinator
1167
+ // only when it is idle. A pure stdio MCP (LLM) coordinator — which has no live
1168
+ // CLI session to inject into — drains the queue itself when it calls a mesh
1169
+ // tool (mesh_status / mesh_read_chat). Either way the queue is the single
1170
+ // source of truth and the only thing this function writes to.
1171
+ //
1172
+ // targetCoordinatorDaemonId scopes the event to a specific coordinator daemon
1173
+ // (unicast) when the worker carries one, so the reconcile loop on the right
1174
+ // daemon drains it and other daemons skip it. Absent → broadcast/backfill.
1175
+ const pendingEvent = {
1176
+ event: args.event,
1177
+ meshId: args.meshId,
1178
+ nodeLabel: args.nodeLabel,
1179
+ nodeId: args.nodeId || undefined,
1180
+ workspace: readNonEmptyString(args.metadataEvent.workspace)
1181
+ || readNonEmptyString(args.metadataEvent.workspaceName),
1182
+ metadataEvent: {
1183
+ ...enrichedMetadataEvent,
1184
+ ...(recoveryContext ? { recoveryContext } : {}),
1185
+ // Stash the coordinator session id INSIDE metadataEvent too, so it survives the
1186
+ // P2P relay serialization (buildForwardPayloadFromPending spreads metadata; the
1187
+ // handleMeshForwardEvent whitelist reads it back) — a top-level field alone would
1188
+ // be dropped when the event crosses a machine boundary.
1189
+ ...(workerCoordinatorSessionId ? { meshCoordinatorSessionId: workerCoordinatorSessionId } : {}),
1190
+ },
1191
+ // Silent lifecycle events (agent:ready / agent:generating_started) carry no
1192
+ // coordinator message; they are queued only so the coordinator re-runs the
1193
+ // remote-claim state machine on pull. injectPendingIntoCoordinator skips
1194
+ // entries without a coordinatorMessage, so a live CLI coordinator is not spammed.
1195
+ ...(messageText ? { coordinatorMessage: messageText } : {}),
1196
+ queuedAt: Date.now(),
1197
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
1198
+ // Top-level session anchor for the local PHASE 2 strict-match on the coordinator
1199
+ // daemon. Absent → daemon-level broadcast (legacy / single-coordinator path).
1200
+ ...(workerCoordinatorSessionId ? { targetCoordinatorSessionId: workerCoordinatorSessionId } : {}),
1201
+ };
1202
+ if (queuePendingMeshCoordinatorEvent(pendingEvent)) {
1203
+ LOG.info('MeshEvents', `Queued ${args.event} for coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''}${workerCoordinatorSessionId ? `, coordinator session ${workerCoordinatorSessionId}` : ''})`);
1204
+ // EVTTRACE: event persisted to the coordinator pending queue (awaiting reconcile drain).
1205
+ traceMeshEventStage('queued', traceCtx, workerCoordinatorDaemonId ? `coordinatorDaemon=${workerCoordinatorDaemonId}` : 'broadcast');
1206
+ } else {
1207
+ // EVTTRACE: queue rejected the event (dedup at queue time / persistence guard).
1208
+ traceMeshEventDrop('queue_dedup', traceCtx);
1209
+ }
1210
+ return { success: true, forwarded: 0 };
1211
+ }
1212
+
1213
+ // Reconstruct the metadataEvent that injectMeshSystemMessage consumes from a forwarded
1214
+ // (cross-machine) mesh event. The remote relay hop arrives as a flat payload, NOT the
1215
+ // original provider event object, so this whitelists the fields the coordinator-side
1216
+ // pipeline reads and re-projects them. Kept pure + exported so the relay-path field
1217
+ // preservation (esp. taskId) is unit-testable without driving injectMeshSystemMessage.
1218
+ //
1219
+ // IMPORTANT asymmetry: the LOCAL in-process forward path (onMeshCoordinatorEventForwarded)
1220
+ // passes the whole event through as metadataEvent, so every field on the event survives
1221
+ // there for free. This remote-only path must explicitly mirror each field it needs.
1222
+ export function buildRelayMetadataEvent(payload: Record<string, unknown>): Record<string, unknown> {
1223
+ const relayModalMessage = readNonEmptyString(payload.modalMessage);
1224
+ const relayModalButtons = Array.isArray(payload.modalButtons)
1225
+ ? (payload.modalButtons as unknown[]).filter((b): b is string => typeof b === 'string' && b.trim().length > 0)
1226
+ : null;
1227
+ return {
1228
+ // Preserve the dispatch task id across the machine boundary. The `received` trace
1229
+ // stage reads payload.taskId; without mirroring it here the rebuilt metadataEvent
1230
+ // loses it, so injectMeshSystemMessage's traceCtx.taskId and the
1231
+ // updateDirectDispatchStatus(eventTaskId) call go undefined — the EvtTrace
1232
+ // queued/surfaced stages show task=- and the direct-dispatch ledger falls back to a
1233
+ // session_id match (which can flip a sibling row). The local in-process forward path
1234
+ // keeps event.taskId/meshActiveTaskId for free; this mirrors it for the remote relay.
1235
+ // Same taskId/meshActiveTaskId ordering the local unroutable trace uses.
1236
+ taskId: readNonEmptyString(payload.taskId) || readNonEmptyString(payload.meshActiveTaskId),
1237
+ targetSessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId) || readNonEmptyString(payload.instanceId),
1238
+ providerType: readNonEmptyString(payload.providerType),
1239
+ providerSessionId: readNonEmptyString(payload.providerSessionId),
1240
+ // Preserve the originating coordinator SESSION id across the machine boundary so
1241
+ // the completion routes back to the exact coordinator session (multi-coordinator).
1242
+ // buildForwardPayloadFromPending spreads the worker event's metadata, so the id
1243
+ // arrives as payload.meshCoordinatorSessionId; the top-level targetCoordinatorSessionId
1244
+ // is also accepted as a fallback. injectMeshSystemMessage re-derives the routing
1245
+ // anchors from this. Absent → daemon-level fallback (version-skew safe).
1246
+ meshCoordinatorSessionId: readNonEmptyString(payload.meshCoordinatorSessionId) || readNonEmptyString(payload.targetCoordinatorSessionId),
1247
+ // Carry the session identity fields the worker provider event emits so the
1248
+ // coordinator's mirror (updateMeshOwnedSession) gets a real workspace/title/
1249
+ // settings. Without these the remote-relay hop reconstructs metadataEvent with
1250
+ // an empty workspace, and the dashboard flaps to the generic
1251
+ // "Terminal (Mesh Node)" title (and degrades the provider label) between live
1252
+ // events and the periodic get_status_metadata snapshot. The local in-process
1253
+ // forward path (onMeshCoordinatorEventForwarded) already preserves these; this
1254
+ // mirrors them for the remote-only relay path.
1255
+ workspace: readNonEmptyString(payload.workspace) || readNonEmptyString(payload.workspaceName),
1256
+ workspaceName: readNonEmptyString(payload.workspaceName) || readNonEmptyString(payload.workspace),
1257
+ sessionTitle: readNonEmptyString(payload.sessionTitle),
1258
+ sessionStatus: readNonEmptyString(payload.sessionStatus),
1259
+ sessionChatStatus: readNonEmptyString(payload.sessionChatStatus),
1260
+ providerName: readNonEmptyString(payload.providerName),
1261
+ ...(payload.sessionSettings && typeof payload.sessionSettings === 'object' && !Array.isArray(payload.sessionSettings) ? { sessionSettings: payload.sessionSettings } : {}),
1262
+ finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1263
+ // T2: carry the worker's status-snapshot last-message preview across the machine
1264
+ // boundary so a summary-less completion still surfaces the assistant reply in the
1265
+ // coordinator's inbox mirror. resolveMeshSurfacedSessionPreview reads these
1266
+ // (assistant-role only) when finalSummary is absent.
1267
+ lastMessagePreview: readNonEmptyString(payload.lastMessagePreview),
1268
+ lastMessageRole: readNonEmptyString(payload.lastMessageRole),
1269
+ ...(payload.lastMessageAt !== undefined ? { lastMessageAt: payload.lastMessageAt } : {}),
1270
+ jobId: readNonEmptyString(payload.jobId),
1271
+ interactionId: readNonEmptyString(payload.interactionId),
1272
+ status: readNonEmptyString(payload.status),
1273
+ targetDaemonId: readNonEmptyString(payload.targetDaemonId),
1274
+ startedAt: readNonEmptyString(payload.startedAt),
1275
+ completedAt: readNonEmptyString(payload.completedAt),
1276
+ retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1277
+ ...(relayModalMessage ? { modalMessage: relayModalMessage } : {}),
1278
+ ...(relayModalButtons && relayModalButtons.length > 0 ? { modalButtons: relayModalButtons } : {}),
1279
+ ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1280
+ ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
1281
+ ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
1282
+ ...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
1283
+ ...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
1284
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
1285
+ intentional: payload.intentional === true,
1286
+ intentionalStop: payload.intentionalStop === true,
1287
+ operatorCleanup: payload.operatorCleanup === true,
1288
+ reason: readNonEmptyString(payload.reason),
1289
+ stopReason: readNonEmptyString(payload.stopReason),
1290
+ cleanupReason: readNonEmptyString(payload.cleanupReason),
1291
+ source: readNonEmptyString(payload.source),
1292
+ };
1293
+ }
1294
+
1295
+ export function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>) {
1296
+ const eventName = readNonEmptyString(payload.event);
1297
+ if (!isMeshCoordinatorEvent(eventName)) {
1298
+ return { success: false, error: 'unsupported mesh event' };
1299
+ }
1300
+ const nodeId = readNonEmptyString(payload.nodeId);
1301
+ const workspace = readNonEmptyString(payload.workspace);
1302
+
1303
+ // The fallback worker-forward path (forwardUnresolvedDelegateEvent) cannot resolve a
1304
+ // mesh id locally on the remote worker, so it forwards the event with nodeId +
1305
+ // workspace only. The coordinator hosting the mesh CAN resolve it. Two recovery
1306
+ // paths, in order:
1307
+ // 1) workspace → mesh (fast path; cached repoIdentity lookup), then
1308
+ // 2) nodeId → mesh (deterministic backstop; scans hosted meshes for the node).
1309
+ // Workspace recovery alone was unreliable — a worktree clone whose repoIdentity
1310
+ // differs, or a transient cache miss, left the reconcile retry permanently rejected
1311
+ // ("meshId required") so the worker's completion never surfaced to the coordinator.
1312
+ // The nodeId is a stable coordinator-side fact and resolves timing-independently.
1313
+ const meshId = readNonEmptyString(payload.meshId)
1314
+ || (workspace ? readNonEmptyString(getCachedMeshByWorkspace(workspace)?.id) : '')
1315
+ || recoverMeshIdByNodeId(nodeId)
1316
+ // Fix B last resort: workspace + nodeId scan both missed, but the worker stamped
1317
+ // its coordinator anchor onto the forward (forwardUnresolvedDelegateEvent). Recover
1318
+ // via the hosted mesh that anchor owns (+ node disambiguation), guarding ambiguity.
1319
+ || recoverMeshIdByCoordinatorAndNode(
1320
+ readNonEmptyString(payload.meshCoordinatorDaemonId) || readNonEmptyString(payload.coordinatorDaemonId),
1321
+ nodeId,
1322
+ );
1323
+ if (!meshId) {
1324
+ // EVTTRACE: forwarded event rejected at receive — no meshId could be resolved
1325
+ // (no payload.meshId, no workspace→mesh, no nodeId→mesh). Observation only.
1326
+ traceMeshEventDrop('meshId_required', {
1327
+ taskId: payload.taskId,
1328
+ sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
1329
+ nodeId,
1330
+ event: eventName,
1331
+ }, workspace ? `workspace=${workspace} unresolved` : 'no workspace/nodeId');
1332
+ return { success: false, error: 'meshId required' };
1333
+ }
1334
+ // EVTTRACE: forwarded event accepted at receive (meshId resolved).
1335
+ // NOTIF-MISS (FIX 3): mirror buildRelayMetadataEvent's taskId fallback. A worker provider
1336
+ // completion stamps its task id as `meshActiveTaskId` (settings → event), not always the
1337
+ // top-level `taskId`, so reading payload.taskId alone rendered `task=-` at the received stage
1338
+ // (the [stage:queued] task=<id> → [stage:received] task=- loss). Falling back to
1339
+ // meshActiveTaskId keeps the trace task-scoped end-to-end so same-task redelivery is
1340
+ // distinguishable from a different task's real completion.
1341
+ traceMeshEventStage('received', {
1342
+ taskId: readNonEmptyString(payload.taskId) || readNonEmptyString(payload.meshActiveTaskId),
1343
+ sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
1344
+ nodeId,
1345
+ meshId,
1346
+ event: eventName,
1347
+ });
1348
+ const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
1349
+
1350
+ return injectMeshSystemMessage(components, {
1351
+ meshId,
1352
+ nodeId,
1353
+ nodeLabel,
1354
+ event: eventName,
1355
+ metadataEvent: buildRelayMetadataEvent(payload),
1356
+ });
1357
+ }
1358
+
1359
+ // ---------------------------------------------------------------------------
1360
+ // Per-coordinator forward serialization (P2P send-backpressure relief).
1361
+ //
1362
+ // When several workers finish at once, each completion runs forwardUnresolvedDelegate
1363
+ // Event and fires its own `mesh_forward_event` push. Firing the whole burst
1364
+ // concurrently dumps it into the single per-peer P2P DataChannel buffer in one tick,
1365
+ // which starves the rpc_ack/rpc_res replies the same channel must carry — a
1366
+ // coordinator's inbound `git_status` then times out even though the worker's own
1367
+ // forward acks return in ~1s. To cap the concurrent burst we serialize the immediate
1368
+ // pushes per coordinator: at most one push is in flight to a given coordinator at a
1369
+ // time, the rest run in arrival order behind it. A lone event (idle lane) still
1370
+ // dispatches immediately — only a genuine burst is paced. Durability is unchanged:
1371
+ // every event is already persisted to the outbox before the push runs, so serializing
1372
+ // only delays the best-effort fast path; PHASE 0 retry still covers any gap. This pairs
1373
+ // with the DataChannel send-buffer gate in daemon-cloud's mesh manager (writeRequest),
1374
+ // which is the hard guarantee; this throttle keeps the burst from piling up there.
1375
+ interface CoordinatorForwardLane { tail: Promise<unknown>; depth: number; }
1376
+ const coordinatorForwardLanes = new Map<string, CoordinatorForwardLane>();
1377
+ function enqueueCoordinatorForwardPush(coordinatorDaemonId: string, run: () => Promise<unknown>): void {
1378
+ let lane = coordinatorForwardLanes.get(coordinatorDaemonId);
1379
+ if (!lane) { lane = { tail: Promise.resolve(), depth: 0 }; coordinatorForwardLanes.set(coordinatorDaemonId, lane); }
1380
+ const wasIdle = lane.depth === 0;
1381
+ lane.depth += 1;
1382
+ const dec = (): void => { lane!.depth -= 1; };
1383
+ if (wasIdle) {
1384
+ // Idle lane → dispatch synchronously, so a lone completion (the common case) has
1385
+ // ZERO added latency and the push call happens in-line. Only a genuine burst —
1386
+ // events arriving while a push is still in flight — is paced (else branch).
1387
+ lane.tail = Promise.resolve(run()).catch(() => {}).then(dec, dec);
1388
+ } else {
1389
+ // Burst: queue behind the in-flight push(es) in arrival order so the whole burst
1390
+ // is not dumped into the shared DataChannel buffer at once. The tail is guarded
1391
+ // so one rejecting push never wedges the lane for the next.
1392
+ lane.tail = lane.tail.then(() => run()).catch(() => {}).then(dec, dec);
1393
+ }
1394
+ }
1395
+
1396
+ // ---------------------------------------------------------------------------
1397
+ // Worker-side fallback forward for unresolved-mesh delegates.
1398
+ //
1399
+ // A REMOTE worker daemon that is being P2P-remote-controlled by a coordinator is
1400
+ // NOT a member of the coordinator's mesh — it has no local mesh record. So when its
1401
+ // completion event reaches the forwarder, resolveWorkerDelegateRouting() resolves the
1402
+ // coordinator anchor (meshCoordinatorDaemonId) from the worker envelope but cannot
1403
+ // resolve the mesh id (neither meshNodeFor nor a workspace→mesh lookup yields one) and
1404
+ // returns isDelegate=false / mesh_unresolved. Before this fallback the event was dropped
1405
+ // (delivery_unroutable) and only recovered later when the coordinator happened to pull
1406
+ // the worker's queue — which it can't, because the worker never queued an unroutable
1407
+ // event. Live symptom: `WARN [MeshEvents] delivery_unroutable: ... mesh unresolved`.
1408
+ //
1409
+ // The fix: the routing object still carries coordinatorDaemonId. Forward the raw event
1410
+ // straight to that coordinator daemon over P2P (mesh_forward_event). The coordinator
1411
+ // hosts the mesh, so it recovers the mesh id by workspace in handleMeshForwardEvent and
1412
+ // injects/queues it normally. meshId is intentionally omitted from the payload (the
1413
+ // worker has none); workspace is the routing anchor the coordinator resolves from.
1414
+ //
1415
+ // No loop / no double-delivery:
1416
+ // - This only fires on the WORKER (the coordinator-own session is rejected by the
1417
+ // resolver before reaching here), and the coordinator merely injects — it does not
1418
+ // re-enter this forwarder for the relayed event.
1419
+ // - It fires only when the normal queue path did NOT run (isDelegate=false), so the
1420
+ // event is never both queued locally and forwarded.
1421
+ //
1422
+ // Returns true when the event was durably accepted for delivery to the coordinator
1423
+ // daemon (so the caller skips the delivery_unroutable diagnostic); false when no
1424
+ // fallback was possible (no coordinator anchor / no dispatch transport).
1425
+ //
1426
+ // Durability: the directed push to the coordinator is the ONLY delivery route for an
1427
+ // unresolved-mesh worker (it is in no mesh.node the coordinator can pull). So instead
1428
+ // of a fire-and-forget push that drops on one transient P2P failure, the event is
1429
+ // persisted to the worker-side outbox FIRST and only acked after a successful push.
1430
+ // A best-effort immediate push keeps latency low on the happy path; a failed or
1431
+ // un-acked push leaves the durable row for setupMeshReconcileLoop's PHASE 0 to retry.
1432
+ function forwardUnresolvedDelegateEvent(
1433
+ components: DaemonComponents,
1434
+ routing: ReturnType<typeof resolveWorkerDelegateRouting>,
1435
+ event: Record<string, unknown>,
1436
+ ): boolean {
1437
+ const coordinatorDaemonId = readNonEmptyString(routing.coordinatorDaemonId);
1438
+ if (!coordinatorDaemonId) return false;
1439
+ if (!components.dispatchMeshCommand) return false;
1440
+
1441
+ const eventName = readNonEmptyString(event.event);
1442
+ if (!eventName) return false;
1443
+
1444
+ // Flat payload mirroring buildForwardPayloadFromPending / what handleMeshForwardEvent
1445
+ // reads. nodeId/workspace come from the worker envelope so the coordinator can name and
1446
+ // locate the node.
1447
+ const payload: Record<string, unknown> = {
1448
+ ...event,
1449
+ event: eventName,
1450
+ nodeId: readNonEmptyString(routing.nodeId) || readNonEmptyString(event.meshNodeId) || undefined,
1451
+ workspace: readNonEmptyString(routing.workspace) || readNonEmptyString(event.workspace) || undefined,
1452
+ // Fix B: carry the resolved coordinator anchor so the coordinator's receive-side
1453
+ // recovery (recoverMeshIdByCoordinatorAndNode) can match this forward to one of the
1454
+ // meshes it hosts when workspace + nodeId both miss. routing.coordinatorDaemonId is
1455
+ // the same anchor this forward is addressed to (coordinatorDaemonId below).
1456
+ meshCoordinatorDaemonId: coordinatorDaemonId,
1457
+ };
1458
+ // RECONCILE-MESHID-DROP: stamp meshId when the WORKER can resolve it (member node /
1459
+ // live-session meshNodeFor). Historically omitted "because the worker can't resolve
1460
+ // it", but for a member-hosted node a no_node_binding session's coordinator-side
1461
+ // recovery (empty payload nodeId + workspace cache miss) fails and the retry is
1462
+ // rejected "meshId required" forever. Resolving here makes the forward self-sufficient;
1463
+ // when unresolvable even here it stays absent and the coordinator's own workspace/nodeId
1464
+ // recovery still runs (unchanged), with the retry cap as the loop backstop.
1465
+ const resolvedMeshId = resolveForwardEventMeshId(components, payload);
1466
+ if (resolvedMeshId) payload.meshId = resolvedMeshId;
1467
+
1468
+ // Self-addressed fallback: the resolved coordinator IS this daemon (a self-
1469
+ // coordinating / single-node mesh, or a delegate whose coordinator anchor resolved
1470
+ // to our own id). A cross-daemon mesh_forward_event to our own id is REFUSED by the
1471
+ // dispatch self-dial guard ("route via the local router instead"), so persisting it
1472
+ // to the outbox would only loop forever in PHASE 0's retry, never acked. Honour the
1473
+ // guard's advice: route the event straight through the local receiver — the exact
1474
+ // path the coordinator runs on receiving a remote push — and skip the outbox entirely.
1475
+ const selfDaemonIds = resolveCoordinatorDrainDaemonIds(components);
1476
+ if (selfDaemonIds.some(self => daemonIdsEquivalent(self, coordinatorDaemonId))) {
1477
+ try {
1478
+ handleMeshForwardEvent(components, payload);
1479
+ LOG.info('MeshEvents', `Self-addressed unresolved-delegate ${eventName} routed via local router (coordinator ${coordinatorDaemonId} is self) — outbox skipped`);
1480
+ } catch (e: any) {
1481
+ LOG.warn('MeshEvents', `Local route of self-addressed unresolved-delegate ${eventName} failed: ${e?.message || e}`);
1482
+ }
1483
+ return true;
1484
+ }
1485
+
1486
+ // 1) Persist durably FIRST. Idempotent on fingerprint, so a re-fired completion
1487
+ // does not duplicate the outbox row. If persistence fails we still attempt the
1488
+ // push below (degrades to the old at-most-once behaviour rather than dropping
1489
+ // the chance entirely).
1490
+ const persisted = enqueueUnresolvedDelegateForward(coordinatorDaemonId, eventName, payload);
1491
+ // EVTTRACE: unresolved-mesh worker persisted its completion to the outbox (no meshId
1492
+ // available locally; coordinator will recover it on receive).
1493
+ const fwdTraceCtx = {
1494
+ taskId: (payload as Record<string, unknown>).taskId,
1495
+ sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
1496
+ nodeId: readNonEmptyString(routing.nodeId) || readNonEmptyString(event.meshNodeId),
1497
+ event: eventName,
1498
+ };
1499
+ traceMeshEventStage('outbox_enqueue', fwdTraceCtx, `coordinatorDaemon=${coordinatorDaemonId} meshId=absent`);
1500
+
1501
+ // 2) Best-effort immediate push for low latency. On success, ack the outbox row so
1502
+ // the retry loop won't re-send it. On failure, leave it queued — PHASE 0 retries.
1503
+ traceMeshEventStage('forward_send', fwdTraceCtx, 'immediate push');
1504
+ // Serialize per coordinator so a multi-worker completion burst is paced rather than
1505
+ // dumped concurrently into the shared P2P DataChannel buffer (see coordinator
1506
+ // ForwardLanes). dispatchMeshCommand was null-checked above; capture it for the
1507
+ // deferred closure.
1508
+ const dispatchMeshCommand = components.dispatchMeshCommand;
1509
+ enqueueCoordinatorForwardPush(coordinatorDaemonId, () =>
1510
+ Promise.resolve(dispatchMeshCommand(coordinatorDaemonId, 'mesh_forward_event', payload))
1511
+ .then((result: any) => {
1512
+ if (result && result.success === false) {
1513
+ LOG.warn('MeshEvents', `Immediate forward of ${eventName} to coordinator ${coordinatorDaemonId} rejected (${readNonEmptyString(result.error) || 'no reason'}) — left queued for retry`);
1514
+ traceMeshEventDrop('immediate_forward_rejected', fwdTraceCtx, readNonEmptyString(result.error) || 'no reason');
1515
+ return;
1516
+ }
1517
+ // Acked. Mark the durable copy delivered so the retry loop skips it.
1518
+ if (persisted) ackUnresolvedDelegateForwardByFingerprint(coordinatorDaemonId, eventName, payload);
1519
+ })
1520
+ .catch((e: any) => {
1521
+ // Coordinator momentarily unreachable; the durable row stays queued and the
1522
+ // reconcile loop retries it. Trace so the relay attempt is visible.
1523
+ LOG.warn('MeshEvents', `Immediate forward of ${eventName} to coordinator ${coordinatorDaemonId} failed: ${e?.message || e} — left queued for retry`);
1524
+ }));
1525
+ LOG.info('MeshEvents', `Durably forwarded ${eventName} for unresolved-mesh worker at ${routing.workspace || '(no workspace)'} to coordinator daemon ${coordinatorDaemonId}`);
1526
+ return true;
1527
+ }
1528
+
1529
+ // Ack a just-pushed outbox entry by re-deriving its row from the same coordinator +
1530
+ // event + payload. We don't thread the row id back from enqueue (the immediate push is
1531
+ // fire-then-ack), so locate it among the undrained entries by matching coordinator and
1532
+ // the flat payload's forward identity. A miss is harmless — the retry loop's own
1533
+ // receiver-side dedup suppresses a duplicate delivery.
1534
+ function ackUnresolvedDelegateForwardByFingerprint(
1535
+ coordinatorDaemonId: string,
1536
+ eventName: string,
1537
+ payload: Record<string, unknown>,
1538
+ ): void {
1539
+ const match = peekUnresolvedDelegateForwards().find(entry =>
1540
+ daemonIdsEquivalent(entry.coordinatorDaemonId, coordinatorDaemonId)
1541
+ && readNonEmptyString(entry.payload.event) === eventName
1542
+ && readNonEmptyString(entry.payload.targetSessionId || entry.payload.sessionId || entry.payload.instanceId)
1543
+ === readNonEmptyString(payload.targetSessionId || payload.sessionId || payload.instanceId)
1544
+ && readNonEmptyString(entry.payload.workspace) === readNonEmptyString(payload.workspace),
1545
+ );
1546
+ if (match) ackUnresolvedDelegateForward(match.id);
1547
+ }
1548
+
1549
+ export function setupMeshEventForwarding(components: DaemonComponents) {
1550
+ components.instanceManager.onEvent((event) => {
1551
+ // --- Coordinator idle auto-flush (fast path) ---
1552
+ // When a coordinator session becomes idle, immediately flush any pending
1553
+ // coordinator events that accumulated while it was generating, rather than
1554
+ // waiting up to one reconcile interval for setupMeshReconcileLoop to do it.
1555
+ // Both paths drain the SAME queue via drainPendingMeshCoordinatorEvents,
1556
+ // whose SQLite drained=1 marking is atomic — whichever fires first consumes
1557
+ // the events and the other gets nothing, so there is no double-delivery.
1558
+ // This runs before the delegate routing below so that coordinator-own idle
1559
+ // transitions are handled first.
1560
+ // Exception: a coordinator that is itself a direct-dispatch target still needs
1561
+ // to go through delegate routing so that the dispatching coordinator receives a
1562
+ // pendingCoordinatorEvents entry for the completion.
1563
+ if (event.event === 'agent:ready' || event.event === 'agent:generating_completed') {
1564
+ const flushInstanceId = readNonEmptyString(event.instanceId);
1565
+ if (flushInstanceId) {
1566
+ const flushSource = components.instanceManager.getInstance(flushInstanceId);
1567
+ if (flushSource && flushSource.category === 'cli') {
1568
+ const flushState = flushSource.getState();
1569
+ const flushSettings = flushState.settings && typeof flushState.settings === 'object' ? flushState.settings as Record<string, unknown> : {};
1570
+ const coordinatorMeshId = readNonEmptyString(flushSettings.meshCoordinatorFor);
1571
+ if (coordinatorMeshId) {
1572
+ const status = readNonEmptyString(flushState.status).toLowerCase();
1573
+ if (status === 'idle') {
1574
+ try {
1575
+ // Drain with the daemon's full coordinator-id set (status id + machineId).
1576
+ // The MCP layer stamps the prefixed status id (`standalone_<machineId>` /
1577
+ // `daemon_<machineId>`) as the worker's meshCoordinatorDaemonId; draining
1578
+ // with bare machineId alone would miss those unicast events. Mirrors
1579
+ // resolveCoordinatorDaemonIds in mesh-reconcile-loop.
1580
+ const drainDaemonIds = resolveCoordinatorDrainDaemonIds(components);
1581
+ const pendingEvents = drainPendingMeshCoordinatorEvents(coordinatorMeshId, drainDaemonIds.length > 0 ? drainDaemonIds : undefined);
1582
+ if (pendingEvents.length > 0) {
1583
+ LOG.info('MeshEvents', `Auto-flushing ${pendingEvents.length} pending coordinator event(s) for mesh ${coordinatorMeshId} on coordinator idle`);
1584
+ for (const pending of pendingEvents) {
1585
+ if (!pending.coordinatorMessage) continue;
1586
+ const forcePending = shouldForceInjectMeshEvent(pending.event);
1587
+ flushSource.onEvent('send_message', {
1588
+ input: { text: pending.coordinatorMessage, textFallback: pending.coordinatorMessage },
1589
+ ...(forcePending ? { force: true } : {}),
1590
+ });
1591
+ }
1592
+ }
1593
+ } catch (e: any) {
1594
+ LOG.warn('MeshEvents', `Failed to auto-flush pending coordinator events: ${e?.message || e}`);
1595
+ }
1596
+ }
1597
+ // Skip delegate routing unless this coordinator session is itself
1598
+ // a direct-dispatch target — in that case fall through so the
1599
+ // dispatching coordinator gets a pendingCoordinatorEvents entry.
1600
+ let hasDirectDispatch = false;
1601
+ try {
1602
+ hasDirectDispatch =
1603
+ getActiveDirectDispatches(coordinatorMeshId).some(d => d.sessionId === flushInstanceId)
1604
+ || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, flushInstanceId);
1605
+ } catch { /* best-effort */ }
1606
+ if (!hasDirectDispatch) return;
1607
+ }
1608
+ }
1609
+ }
1610
+ }
1611
+
1612
+ // --- Delegate event routing ---
1613
+ if (!isMeshCoordinatorEvent(event.event)) return;
1614
+
1615
+ const instanceId = readNonEmptyString(event.instanceId);
1616
+ if (!instanceId) return;
1617
+
1618
+ // R1: all session→node→mesh→coordinator interpretation is folded into the single
1619
+ // resolveWorkerDelegateRouting() resolver. No stamp (meshNodeFor / meshNodeId /
1620
+ // meshCoordinatorDaemonId / meshCoordinatorNodeId / launchedByCoordinator) is read
1621
+ // here to make a routing decision — the resolver is the one authority, and the
1622
+ // forwarder consumes its typed result only.
1623
+ const routing = resolveWorkerDelegateRouting(components, instanceId, {
1624
+ getMeshById: (meshId) => getMeshWithCache(components, meshId),
1625
+ getMeshByWorkspace: (workspace) => getCachedMeshByWorkspace(workspace),
1626
+ });
1627
+ if (!routing.isDelegate) {
1628
+ // Fallback: a REMOTE worker that isn't a member of the coordinator's mesh can't
1629
+ // resolve a mesh id locally (mesh_unresolved), but it still carries the coordinator
1630
+ // daemon anchor. Forward the event straight to that coordinator over P2P instead of
1631
+ // dropping it — the coordinator hosts the mesh and recovers the id by workspace.
1632
+ if (isUnroutableDelegateRejection(routing)
1633
+ && forwardUnresolvedDelegateEvent(components, routing, event)) {
1634
+ return;
1635
+ }
1636
+ // R4: a worker that presented a valid envelope but resolved to no mesh (and could
1637
+ // not be fallback-forwarded — e.g. no coordinator anchor) used to be dropped
1638
+ // silently. Leave a fail-loud diagnostic so the missing completion is traceable.
1639
+ // Benign non-delegate rejections (not_cli / no_workspace / etc.) are no-ops inside
1640
+ // recordUnroutableDelegateEvent.
1641
+ // EVTTRACE: a delegate event that could not be routed AND could not be
1642
+ // fallback-forwarded (no coordinator anchor). Only mesh_unresolved is a real
1643
+ // drop; the benign non-delegate rejections are ordinary non-mesh traffic.
1644
+ if (isUnroutableDelegateRejection(routing)) {
1645
+ traceMeshEventDrop('unroutable', {
1646
+ taskId: (event as Record<string, unknown>).meshActiveTaskId ?? (event as Record<string, unknown>).taskId,
1647
+ sessionId: routing.sessionId,
1648
+ nodeId: routing.nodeId,
1649
+ event: event.event,
1650
+ }, 'no coordinator anchor / mesh_unresolved');
1651
+ }
1652
+ recordUnroutableDelegateEvent(routing, event.event);
1653
+ return;
1654
+ }
1655
+
1656
+ injectMeshSystemMessage(components, {
1657
+ meshId: routing.meshId,
1658
+ sourceInstanceId: instanceId,
1659
+ nodeId: routing.nodeId,
1660
+ nodeLabel: routing.nodeLabel,
1661
+ event: event.event,
1662
+ metadataEvent: event,
1663
+ });
1664
+ });
1665
+ }