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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (473) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +9 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +68 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +220 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +137 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
  14. package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
  15. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  16. package/dist/cli-adapters/resolve-executable.d.ts +69 -0
  17. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  18. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  19. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  20. package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
  21. package/dist/commands/chat-commands-read.d.ts +23 -0
  22. package/dist/commands/chat-commands-scope.d.ts +39 -0
  23. package/dist/commands/chat-commands-shared.d.ts +33 -0
  24. package/dist/commands/chat-commands-write.d.ts +14 -0
  25. package/dist/commands/chat-commands.d.ts +10 -23
  26. package/dist/commands/cli-manager.d.ts +71 -1
  27. package/dist/commands/handler.d.ts +125 -0
  28. package/dist/commands/high-family/index.d.ts +3 -0
  29. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  30. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  31. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  32. package/dist/commands/high-family/types.d.ts +64 -0
  33. package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
  34. package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
  35. package/dist/commands/low-family/diagnostics.d.ts +2 -0
  36. package/dist/commands/low-family/index.d.ts +3 -0
  37. package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
  38. package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
  39. package/dist/commands/low-family/notification.d.ts +2 -0
  40. package/dist/commands/low-family/refine-config.d.ts +2 -0
  41. package/dist/commands/low-family/session-host.d.ts +2 -0
  42. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  43. package/dist/commands/low-family/status-meta.d.ts +2 -0
  44. package/dist/commands/low-family/types.d.ts +33 -0
  45. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  46. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  47. package/dist/commands/med-family/ide.d.ts +10 -0
  48. package/dist/commands/med-family/index.d.ts +3 -0
  49. package/dist/commands/med-family/mesh-crud.d.ts +48 -0
  50. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  51. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  52. package/dist/commands/med-family/mesh-restart.d.ts +2 -0
  53. package/dist/commands/med-family/types.d.ts +152 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router-aggregate-status.d.ts +24 -0
  56. package/dist/commands/router-mesh-session-owner.d.ts +59 -0
  57. package/dist/commands/router-refine.d.ts +151 -0
  58. package/dist/commands/router-worktree-cleanup.d.ts +133 -0
  59. package/dist/commands/router.d.ts +203 -15
  60. package/dist/commands/upgrade-helper.d.ts +41 -1
  61. package/dist/config/chat-history.d.ts +11 -0
  62. package/dist/config/config.d.ts +19 -0
  63. package/dist/config/mesh-config.d.ts +66 -0
  64. package/dist/config/mesh-json-config.d.ts +199 -0
  65. package/dist/config/registry-resolver.d.ts +54 -0
  66. package/dist/config/repo-settings.d.ts +77 -0
  67. package/dist/config/state-store.d.ts +30 -0
  68. package/dist/daemon/dev-server.d.ts +0 -2
  69. package/dist/detection/cli-detector.d.ts +17 -0
  70. package/dist/detection/ide-detector.d.ts +13 -0
  71. package/dist/detection/win32-ide-version.d.ts +37 -0
  72. package/dist/git/change-impact-config.d.ts +159 -0
  73. package/dist/git/git-commands.d.ts +26 -2
  74. package/dist/git/git-diff.d.ts +6 -0
  75. package/dist/git/git-executor.d.ts +11 -0
  76. package/dist/git/git-status.d.ts +80 -0
  77. package/dist/git/git-types.d.ts +2 -50
  78. package/dist/git/git-worktree.d.ts +71 -1
  79. package/dist/git/index.d.ts +3 -1
  80. package/dist/index.d.ts +70 -17
  81. package/dist/index.js +53556 -19074
  82. package/dist/index.js.map +1 -1
  83. package/dist/index.mjs +53380 -19063
  84. package/dist/index.mjs.map +1 -1
  85. package/dist/installer.d.ts +1 -4
  86. package/dist/ipc/local-ipc-server.d.ts +91 -0
  87. package/dist/launch.d.ts +1 -1
  88. package/dist/logging/async-batch-writer.d.ts +10 -0
  89. package/dist/logging/debug-config.d.ts +16 -0
  90. package/dist/logging/log-redactor.d.ts +24 -0
  91. package/dist/logging/log-tail-reader.d.ts +81 -0
  92. package/dist/logging/logger.d.ts +1 -1
  93. package/dist/mesh/contracts.d.ts +256 -0
  94. package/dist/mesh/coordinator-prompt.d.ts +76 -0
  95. package/dist/mesh/coordinator-registry.d.ts +59 -0
  96. package/dist/mesh/mesh-active-work.d.ts +203 -0
  97. package/dist/mesh/mesh-clone-grace.d.ts +8 -0
  98. package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
  99. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  100. package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
  101. package/dist/mesh/mesh-event-classify.d.ts +7 -0
  102. package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
  103. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  104. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  105. package/dist/mesh/mesh-events-pending.d.ts +258 -0
  106. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  107. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  108. package/dist/mesh/mesh-events.d.ts +6 -49
  109. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  110. package/dist/mesh/mesh-host-ownership.d.ts +21 -1
  111. package/dist/mesh/mesh-init.d.ts +128 -0
  112. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  113. package/dist/mesh/mesh-ledger.d.ts +139 -1
  114. package/dist/mesh/mesh-magi-status.d.ts +63 -0
  115. package/dist/mesh/mesh-missions.d.ts +281 -0
  116. package/dist/mesh/mesh-node-identity.d.ts +310 -0
  117. package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
  118. package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
  119. package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
  120. package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
  121. package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
  122. package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
  123. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  124. package/dist/mesh/mesh-refine-gates.d.ts +457 -0
  125. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  126. package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
  127. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  128. package/dist/mesh/mesh-routing.d.ts +70 -0
  129. package/dist/mesh/mesh-runtime-store.d.ts +567 -0
  130. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  131. package/dist/mesh/mesh-task-inflight.d.ts +46 -0
  132. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  133. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
  134. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  135. package/dist/mesh/mesh-work-queue.d.ts +392 -1
  136. package/dist/mesh/preview-freshness.d.ts +28 -0
  137. package/dist/mesh/refine-config.d.ts +97 -0
  138. package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
  139. package/dist/providers/acp-provider-instance.d.ts +5 -0
  140. package/dist/providers/approval-utils.d.ts +46 -0
  141. package/dist/providers/chat-message-normalization.d.ts +58 -1
  142. package/dist/providers/cli-provider-effect-format.d.ts +30 -0
  143. package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
  144. package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
  145. package/dist/providers/cli-provider-instance-types.d.ts +45 -0
  146. package/dist/providers/cli-provider-instance.d.ts +426 -30
  147. package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
  148. package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
  149. package/dist/providers/contracts.d.ts +196 -6
  150. package/dist/providers/external-sources.d.ts +71 -0
  151. package/dist/providers/manual-attendance.d.ts +79 -0
  152. package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
  153. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
  154. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  155. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  156. package/dist/providers/native-history/constants.d.ts +12 -0
  157. package/dist/providers/native-history/dispatcher.d.ts +44 -0
  158. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  159. package/dist/providers/native-history/index.d.ts +13 -0
  160. package/dist/providers/provider-instance-manager.d.ts +30 -0
  161. package/dist/providers/provider-instance.d.ts +31 -1
  162. package/dist/providers/provider-loader.d.ts +42 -5
  163. package/dist/providers/provider-trust.d.ts +31 -0
  164. package/dist/providers/read-chat-contract.d.ts +29 -0
  165. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  166. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  167. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  168. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  169. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  170. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  171. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  172. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  173. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  174. package/dist/providers/sdk/v1/index.d.ts +30 -0
  175. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  176. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  177. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  178. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  179. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  180. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  181. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  182. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  183. package/dist/providers/spec/adapter.d.ts +98 -0
  184. package/dist/providers/spec/cli-adapter.d.ts +223 -0
  185. package/dist/providers/spec/evaluator.d.ts +45 -0
  186. package/dist/providers/spec/fsm-driver.d.ts +449 -0
  187. package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
  188. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  189. package/dist/providers/spec/fsm-types.d.ts +202 -0
  190. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  191. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  192. package/dist/providers/spec/route.d.ts +4 -0
  193. package/dist/providers/spec/types.d.ts +244 -0
  194. package/dist/providers/status-monitor.d.ts +7 -7
  195. package/dist/providers/transcript-v2.d.ts +176 -0
  196. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  197. package/dist/providers/working-dir.d.ts +17 -0
  198. package/dist/repo-mesh-types.d.ts +616 -12
  199. package/dist/runtime-defaults.d.ts +2 -0
  200. package/dist/session-host/managed-host.d.ts +64 -0
  201. package/dist/session-host/runtime-surface.d.ts +10 -16
  202. package/dist/sessions/registry.d.ts +26 -0
  203. package/dist/shared-types-extra.d.ts +2 -4
  204. package/dist/shared-types.d.ts +110 -55
  205. package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
  206. package/dist/status/normalize.d.ts +1 -1
  207. package/dist/status/normalize.js +1 -0
  208. package/dist/status/normalize.js.map +1 -1
  209. package/dist/status/normalize.mjs +1 -0
  210. package/dist/status/normalize.mjs.map +1 -1
  211. package/dist/status/reporter.d.ts +2 -0
  212. package/dist/status/snapshot.d.ts +27 -0
  213. package/dist/system/hash.d.ts +8 -0
  214. package/dist/system/load-better-sqlite3.d.ts +21 -0
  215. package/dist/types.d.ts +20 -6
  216. package/package.json +11 -4
  217. package/src/agent-stream/poller.ts +2 -3
  218. package/src/agent-stream/provider-adapter.ts +1 -1
  219. package/src/boot/daemon-lifecycle.ts +80 -14
  220. package/src/boot/process-hardening.ts +89 -0
  221. package/src/build-info.ts +73 -0
  222. package/src/chat/source-machine.ts +534 -0
  223. package/src/chat/source-resolver.ts +0 -0
  224. package/src/chat/subscription-updates.ts +20 -1
  225. package/src/cli-adapter-types.d.ts +3 -1
  226. package/src/cli-adapter-types.ts +68 -2
  227. package/src/cli-adapters/cli-script-runner.ts +421 -0
  228. package/src/cli-adapters/cli-state-engine.ts +1282 -0
  229. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  230. package/src/cli-adapters/provider-cli-adapter.ts +1048 -1143
  231. package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
  232. package/src/cli-adapters/provider-cli-parse.ts +19 -0
  233. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  234. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  235. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  236. package/src/cli-adapters/pty-transport.ts +2 -1
  237. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  238. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  239. package/src/cli-adapters/resolve-executable.ts +204 -0
  240. package/src/cli-adapters/session-host-transport.ts +2 -1
  241. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
  242. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  243. package/src/cli-adapters/terminal-screen.ts +16 -81
  244. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  245. package/src/commands/chat-commands-read.ts +2788 -0
  246. package/src/commands/chat-commands-scope.ts +54 -0
  247. package/src/commands/chat-commands-shared.ts +114 -0
  248. package/src/commands/chat-commands-write.ts +891 -0
  249. package/src/commands/chat-commands.ts +19 -1841
  250. package/src/commands/cli-manager.d.ts +2 -0
  251. package/src/commands/cli-manager.ts +724 -27
  252. package/src/commands/handler.ts +865 -2
  253. package/src/commands/high-family/index.ts +28 -0
  254. package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
  255. package/src/commands/high-family/mesh-events.ts +102 -0
  256. package/src/commands/high-family/mesh-status.ts +857 -0
  257. package/src/commands/high-family/types.ts +80 -0
  258. package/src/commands/low-family/coordinator-prompt.ts +125 -0
  259. package/src/commands/low-family/daemon-lifecycle.ts +120 -0
  260. package/src/commands/low-family/diagnostics.ts +57 -0
  261. package/src/commands/low-family/index.ts +37 -0
  262. package/src/commands/low-family/mesh-ledger.ts +62 -0
  263. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  264. package/src/commands/low-family/notification.ts +116 -0
  265. package/src/commands/low-family/refine-config.ts +106 -0
  266. package/src/commands/low-family/session-host.ts +282 -0
  267. package/src/commands/low-family/spec-providerdev.ts +217 -0
  268. package/src/commands/low-family/status-meta.ts +112 -0
  269. package/src/commands/low-family/types.ts +39 -0
  270. package/src/commands/med-family/cli-agent.ts +270 -0
  271. package/src/commands/med-family/fast-forward.ts +230 -0
  272. package/src/commands/med-family/ide.ts +163 -0
  273. package/src/commands/med-family/index.ts +37 -0
  274. package/src/commands/med-family/mesh-crud.ts +1309 -0
  275. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  276. package/src/commands/med-family/mesh-queue.ts +167 -0
  277. package/src/commands/med-family/mesh-restart.ts +92 -0
  278. package/src/commands/med-family/types.ts +153 -0
  279. package/src/commands/mesh-coordinator.ts +334 -124
  280. package/src/commands/router-aggregate-status.ts +209 -0
  281. package/src/commands/router-mesh-session-owner.ts +114 -0
  282. package/src/commands/router-refine.ts +1794 -0
  283. package/src/commands/router-worktree-cleanup.ts +870 -0
  284. package/src/commands/router.ts +567 -4149
  285. package/src/commands/stream-commands.ts +8 -0
  286. package/src/commands/upgrade-helper.ts +310 -45
  287. package/src/config/chat-history.ts +517 -24
  288. package/src/config/config.ts +30 -0
  289. package/src/config/mesh-config.ts +300 -24
  290. package/src/config/mesh-json-config.ts +376 -0
  291. package/src/config/recent-activity.ts +8 -2
  292. package/src/config/registry-resolver.ts +100 -0
  293. package/src/config/repo-settings.ts +111 -0
  294. package/src/config/state-store.ts +55 -0
  295. package/src/daemon/dev-auto-implement.ts +3 -2
  296. package/src/daemon/dev-cli-debug.ts +10 -1
  297. package/src/daemon/dev-server.ts +0 -541
  298. package/src/detection/cli-detector.ts +94 -9
  299. package/src/detection/ide-detector.ts +55 -16
  300. package/src/detection/win32-ide-version.ts +106 -0
  301. package/src/git/change-impact-config.ts +354 -0
  302. package/src/git/git-commands.ts +112 -16
  303. package/src/git/git-diff.ts +81 -11
  304. package/src/git/git-executor.ts +12 -0
  305. package/src/git/git-status.ts +822 -48
  306. package/src/git/git-types.ts +14 -62
  307. package/src/git/git-worktree.ts +261 -4
  308. package/src/git/index.ts +17 -0
  309. package/src/index.ts +190 -13
  310. package/src/installer.d.ts +1 -1
  311. package/src/installer.ts +8 -6
  312. package/src/ipc/local-ipc-server.ts +278 -0
  313. package/src/launch.d.ts +1 -1
  314. package/src/launch.ts +37 -28
  315. package/src/logging/async-batch-writer.ts +55 -0
  316. package/src/logging/command-log.ts +7 -5
  317. package/src/logging/debug-config.ts +25 -0
  318. package/src/logging/debug-trace.ts +7 -2
  319. package/src/logging/log-redactor.ts +100 -0
  320. package/src/logging/log-tail-reader.ts +341 -0
  321. package/src/logging/logger.ts +14 -7
  322. package/src/mesh/contracts.ts +510 -0
  323. package/src/mesh/coordinator-prompt.ts +627 -34
  324. package/src/mesh/coordinator-registry.ts +121 -0
  325. package/src/mesh/mesh-active-work.ts +706 -0
  326. package/src/mesh/mesh-clone-grace.ts +68 -0
  327. package/src/mesh/mesh-completion-synthesis.ts +416 -0
  328. package/src/mesh/mesh-coordinator-config.ts +97 -0
  329. package/src/mesh/mesh-delivery-policy.ts +284 -0
  330. package/src/mesh/mesh-event-classify.ts +78 -0
  331. package/src/mesh/mesh-event-forwarding.ts +2030 -0
  332. package/src/mesh/mesh-event-trace.ts +67 -0
  333. package/src/mesh/mesh-events-coordinator.ts +32 -0
  334. package/src/mesh/mesh-events-pending.ts +1548 -0
  335. package/src/mesh/mesh-events-stale.ts +452 -0
  336. package/src/mesh/mesh-events-utils.ts +443 -0
  337. package/src/mesh/mesh-events.ts +43 -1035
  338. package/src/mesh/mesh-fast-forward.ts +856 -0
  339. package/src/mesh/mesh-host-ownership.ts +41 -3
  340. package/src/mesh/mesh-init.ts +350 -0
  341. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  342. package/src/mesh/mesh-ledger.ts +897 -102
  343. package/src/mesh/mesh-magi-status.ts +223 -0
  344. package/src/mesh/mesh-missions.ts +695 -0
  345. package/src/mesh/mesh-node-identity.ts +2065 -0
  346. package/src/mesh/mesh-queue-assignment.ts +2498 -0
  347. package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
  348. package/src/mesh/mesh-reconcile-config.ts +66 -0
  349. package/src/mesh/mesh-reconcile-identity.ts +103 -0
  350. package/src/mesh/mesh-reconcile-loop.ts +1696 -0
  351. package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
  352. package/src/mesh/mesh-refine-batch.ts +205 -0
  353. package/src/mesh/mesh-refine-gates.ts +1792 -0
  354. package/src/mesh/mesh-refine-status.ts +231 -0
  355. package/src/mesh/mesh-remote-event-pull.ts +279 -0
  356. package/src/mesh/mesh-review-inbox.ts +308 -0
  357. package/src/mesh/mesh-routing.ts +291 -0
  358. package/src/mesh/mesh-runtime-store.ts +2292 -0
  359. package/src/mesh/mesh-scheduling-runtime.ts +198 -0
  360. package/src/mesh/mesh-task-inflight.ts +70 -0
  361. package/src/mesh/mesh-task-stats.ts +161 -0
  362. package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
  363. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  364. package/src/mesh/mesh-work-queue.ts +1489 -145
  365. package/src/mesh/preview-freshness.ts +163 -0
  366. package/src/mesh/refine-config.ts +129 -12
  367. package/src/mesh/worktree-bootstrap-config.ts +599 -0
  368. package/src/providers/acp-provider-instance.ts +43 -10
  369. package/src/providers/approval-utils.d.ts +5 -0
  370. package/src/providers/approval-utils.ts +100 -6
  371. package/src/providers/chat-message-normalization.ts +126 -4
  372. package/src/providers/cli-provider-effect-format.ts +53 -0
  373. package/src/providers/cli-provider-history-dedup.ts +75 -0
  374. package/src/providers/cli-provider-input-prompt.ts +133 -0
  375. package/src/providers/cli-provider-instance-types.ts +131 -0
  376. package/src/providers/cli-provider-instance.ts +2771 -499
  377. package/src/providers/cli-provider-status-helpers.ts +123 -0
  378. package/src/providers/cli-provider-transcript-merge.ts +114 -0
  379. package/src/providers/contracts.d.ts +55 -0
  380. package/src/providers/contracts.ts +207 -6
  381. package/src/providers/extension-provider-instance.ts +12 -7
  382. package/src/providers/external-sources.ts +218 -0
  383. package/src/providers/ide-provider-instance.ts +35 -12
  384. package/src/providers/manual-attendance.ts +105 -0
  385. package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
  386. package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
  387. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  388. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  389. package/src/providers/native-history/constants.ts +19 -0
  390. package/src/providers/native-history/dispatcher.ts +617 -0
  391. package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
  392. package/src/providers/native-history/index.ts +30 -0
  393. package/src/providers/provider-instance-manager.ts +71 -0
  394. package/src/providers/provider-instance.ts +24 -1
  395. package/src/providers/provider-loader.ts +700 -55
  396. package/src/providers/provider-schema.ts +93 -14
  397. package/src/providers/provider-trust.ts +114 -0
  398. package/src/providers/read-chat-contract.ts +76 -16
  399. package/src/providers/sdk/README.md +49 -0
  400. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  401. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  402. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  403. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
  404. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  405. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  406. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  407. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  408. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  409. package/src/providers/sdk/v1/index.ts +152 -0
  410. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  411. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  412. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  413. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
  414. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  415. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  416. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  417. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  418. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  419. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  420. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  421. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  422. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  423. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  424. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  425. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  426. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  427. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  428. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  429. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  430. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  431. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  432. package/src/providers/sdk/v1/validators/index.ts +19 -0
  433. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  434. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  435. package/src/providers/spec/adapter.ts +246 -0
  436. package/src/providers/spec/cli-adapter.ts +1064 -0
  437. package/src/providers/spec/evaluator.ts +407 -0
  438. package/src/providers/spec/fsm-driver.ts +1528 -0
  439. package/src/providers/spec/fsm-evaluator.ts +290 -0
  440. package/src/providers/spec/fsm-loader.ts +128 -0
  441. package/src/providers/spec/fsm-types.ts +301 -0
  442. package/src/providers/spec/native-history-executor.ts +1241 -0
  443. package/src/providers/spec/pre-launch-trust.ts +104 -0
  444. package/src/providers/spec/route.ts +51 -0
  445. package/src/providers/spec/types.ts +284 -0
  446. package/src/providers/status-monitor.d.ts +7 -7
  447. package/src/providers/status-monitor.ts +37 -22
  448. package/src/providers/transcript-v2.ts +567 -0
  449. package/src/providers/types/interactive-prompt.ts +536 -0
  450. package/src/providers/version-archive.ts +64 -24
  451. package/src/providers/working-dir.ts +23 -0
  452. package/src/repo-mesh-types.ts +861 -14
  453. package/src/runtime-defaults.ts +39 -0
  454. package/src/session-host/managed-host.ts +218 -0
  455. package/src/session-host/runtime-surface.ts +20 -80
  456. package/src/sessions/registry.ts +44 -0
  457. package/src/shared-types-extra.ts +2 -4
  458. package/src/shared-types.d.ts +8 -0
  459. package/src/shared-types.ts +148 -55
  460. package/src/status/builders.ts +69 -6
  461. package/src/status/chat-tail-hot-sessions.ts +117 -2
  462. package/src/status/normalize.ts +2 -0
  463. package/src/status/reporter.ts +19 -1
  464. package/src/status/snapshot.ts +100 -27
  465. package/src/system/hash.ts +23 -0
  466. package/src/system/host-memory.ts +29 -12
  467. package/src/system/load-better-sqlite3.ts +68 -0
  468. package/src/types.ts +26 -6
  469. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  470. package/dist/mesh/mesh-sync.d.ts +0 -53
  471. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  472. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  473. package/src/mesh/mesh-sync.ts +0 -111
@@ -13,11 +13,18 @@
13
13
  * Safety: mode 0o600, atomic append via appendFileSync
14
14
  */
15
15
 
16
- import { existsSync, mkdirSync, readFileSync, appendFileSync, statSync, renameSync } from 'fs';
16
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, renameSync, writeFileSync } from 'fs';
17
17
  import { join } from 'path';
18
18
  import { randomUUID } from 'crypto';
19
19
  import { getConfigDir } from '../config/config.js';
20
+ import { daemonIdsEquivalent, sessionIdsEquivalent } from '@adhdev/mesh-shared';
20
21
  import { EventEmitter } from 'events';
22
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
23
+ import {
24
+ coordinatorIdentityFromEmitFields,
25
+ MESH_PROTOCOL_VERSION_V2,
26
+ type MeshLedgerOriginatingCoordinatorV2,
27
+ } from './contracts.js';
21
28
  // ─── Types ──────────────────────────────────────
22
29
 
23
30
  export type MeshLedgerKind =
@@ -26,6 +33,7 @@ export type MeshLedgerKind =
26
33
  | 'task_failed'
27
34
  | 'task_stalled'
28
35
  | 'task_approval_needed'
36
+ | 'p2p_dispatch_failed'
29
37
  | 'session_launched'
30
38
  | 'session_auto_launch'
31
39
  | 'session_stopped'
@@ -37,6 +45,43 @@ export type MeshLedgerKind =
37
45
  | 'recovery_attempted'
38
46
  | 'ledger_replicated'
39
47
  | 'ledger_reconciled'
48
+ | 'direct_fast_forward'
49
+ | 'delivery_unroutable'
50
+ | 'direct_dispatch_pruned'
51
+ | 'event_held'
52
+ // Audit marker written by mesh_requeue_held_events when a recoverable `event_held`
53
+ // entry is restored to the pending queue (event_held→pending). Keyed by the source
54
+ // held-ledger-entry id so a second requeue pass skips already-recovered entries
55
+ // (no double-requeue). payload: { heldEntryId, event, requeued: boolean, reason?, dedupSuppressed?: boolean }
56
+ | 'event_held_requeued'
57
+ | 'task_reclaimed'
58
+ // Gap2-A: a coordinator-recorded operating note — a runtime-accumulated
59
+ // lesson (provider quirk, pattern to avoid, recovery lesson) persisted in
60
+ // the ledger so it survives coordinator restarts and is provider-neutral.
61
+ // payload: { text, category?, createdAt?, sourceCoordinator? }
62
+ | 'coordinator_operating_note'
63
+ // Retraction of a coordinator_operating_note. Append-only (history preserved);
64
+ // readers filter out the targeted note so it leaves the prompt/list. Targets by
65
+ // note id (exact) and/or by trimmed-text fingerprint (matches all notes with that
66
+ // text). payload: { targetNoteId?, targetFingerprint?, reason?, forgottenAt? }
67
+ | 'coordinator_operating_note_tombstone'
68
+ // Mission audit trail: mission record mutations (mesh_mission_upsert) so the
69
+ // ledger captures mission lifecycle, not just task events. Without these a
70
+ // mission create / goal rewrite / status transition left no ledger trace,
71
+ // breaking audit continuity and post-restart recovery.
72
+ // mission_created payload: { missionId, title, goalSummary, goalLength, goalTruncated, status }
73
+ // mission_status_changed payload: { missionId, title, fromStatus, toStatus }
74
+ // mission_goal_updated payload: { missionId, title, prevGoalSummary, nextGoalSummary, prevGoalLength, nextGoalLength, goalTruncated }
75
+ | 'mission_created'
76
+ | 'mission_status_changed'
77
+ | 'mission_goal_updated'
78
+ // MAGI (Multi-Agent Ground-truth Insight) cross-verification activity. Persisted
79
+ // so a wait=false fan-out and its later synthesis survive coordinator restarts and
80
+ // are foldable into mesh_status (keyed by consensusGroupId).
81
+ // magi_dispatched payload: { source:'magi', consensusGroupId, missionId?, panel?, question?, replicaCount }
82
+ // magi_synthesis payload: { source:'magi', consensusGroupId, missionId?, panel?, question?, synthesis }
83
+ | 'magi_dispatched'
84
+ | 'magi_synthesis'
40
85
  ;
41
86
 
42
87
  export interface MeshLedgerEntry {
@@ -62,6 +107,51 @@ export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind
62
107
  || payload.source === 'mesh_remove_node');
63
108
  }
64
109
 
110
+ export type MeshWorkerResultStatus = 'completed' | 'failed' | 'blocked' | 'partial' | 'unknown';
111
+ export type MeshProcessArtifactKind = 'process' | 'log' | 'port' | 'window' | 'session' | 'file' | 'url' | 'other';
112
+
113
+ export interface MeshValidationResultArtifact {
114
+ command?: string;
115
+ status: 'passed' | 'failed' | 'skipped' | 'unknown';
116
+ durationMs?: number;
117
+ outputPath?: string;
118
+ summary?: string;
119
+ }
120
+
121
+ export interface MeshProcessArtifact {
122
+ kind: MeshProcessArtifactKind;
123
+ id?: string;
124
+ label?: string;
125
+ locator?: string;
126
+ pid?: number;
127
+ port?: number;
128
+ url?: string;
129
+ path?: string;
130
+ sessionId?: string;
131
+ keepRunning?: boolean;
132
+ metadata?: Record<string, unknown>;
133
+ }
134
+
135
+ export interface MeshWorkerResultArtifact {
136
+ status: MeshWorkerResultStatus;
137
+ classification?: string;
138
+ changedFiles: string[];
139
+ validationResults: MeshValidationResultArtifact[];
140
+ gitStatus?: Record<string, unknown>;
141
+ processArtifacts: MeshProcessArtifact[];
142
+ errors: string[];
143
+ nextAction?: string;
144
+ requiresUserAction: boolean;
145
+ // NOTIF Defect-2b: `parseable_answer` = the final summary held a parseable JSON
146
+ // ANSWER (e.g. a MAGI claim_audit / rca envelope) that is NOT worker-result-shaped
147
+ // (no status + changedFiles/errors/…). It is still concrete evidence that the worker
148
+ // produced a real, parseable answer — so it must NOT be labelled evidenceLevel
149
+ // 'insufficient' — but it is NOT a self-attributing worker result, so it is deliberately
150
+ // distinct from 'final_summary_json' and stays subject to the direct-dispatch grace gate
151
+ // in mesh-events-stale.ts (which keys on `!== 'final_summary_json'`).
152
+ source: 'explicit_metadata' | 'final_summary_json' | 'parseable_answer' | 'default';
153
+ }
154
+
65
155
  export interface MeshTaskCompletionEvidence {
66
156
  source: 'agent_status_event';
67
157
  event: 'agent:generating_completed' | 'agent:ready';
@@ -75,6 +165,7 @@ export interface MeshTaskCompletionEvidence {
75
165
  providerSessionId?: string;
76
166
  finalSummaryAvailable: boolean;
77
167
  };
168
+ workerResult: MeshWorkerResultArtifact;
78
169
  git: {
79
170
  status: 'deferred';
80
171
  reason: string;
@@ -97,6 +188,7 @@ export interface BuildTaskCompletionEvidenceOptions {
97
188
  providerType?: string;
98
189
  providerSessionId?: string;
99
190
  finalSummary?: string;
191
+ workerResult?: Record<string, unknown>;
100
192
  completedAt?: string;
101
193
  }
102
194
 
@@ -117,6 +209,12 @@ export interface ReadLedgerOptions {
117
209
  tail?: number;
118
210
  since?: string;
119
211
  kind?: MeshLedgerKind[];
212
+ /**
213
+ * Filter to entries whose nodeId is equivalent to this daemon id. Matched via
214
+ * daemonIdsEquivalent (not raw ===) so caller-supplied identifiers in any form
215
+ * (mach_X vs daemon_mach_X) resolve correctly — see the canon-identity defect class.
216
+ */
217
+ node?: string;
120
218
  }
121
219
 
122
220
  export interface ReadLedgerSliceOptions {
@@ -161,11 +259,58 @@ export interface AppendRemoteLedgerResult {
161
259
  // ─── Constants ──────────────────────────────────
162
260
 
163
261
  const LEDGER_DIR_NAME = 'mesh-ledger';
164
- const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
262
+ const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB — full rotation threshold
263
+ const COMPACT_THRESHOLD_BYTES = 2 * 1024 * 1024; // 2 MB — compaction threshold
264
+ const ARCHIVE_TERMINAL_OLDER_THAN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
165
265
  const RECENT_FAILURE_WINDOW_MS = 30 * 60 * 1000; // 30 minutes
266
+
267
+ // Kinds that accumulate indefinitely and are safe to archive after ARCHIVE_TERMINAL_OLDER_THAN_MS.
268
+ // Non-terminal kinds (dispatched, sessions, nodes, checkpoints) are always kept in the active file.
269
+ //
270
+ // session_auto_launch is telemetry, NOT audit: it records the queue-assignment
271
+ // decision on each tick (mostly phase:'skipped' — nothing was launched). No reader
272
+ // consults it back from the ledger (unlike task_dispatched, which getSessionRecoveryContext
273
+ // scans and which therefore MUST stay non-archivable). It is the single largest
274
+ // avoidable ledger consumer after node/dispatch history, so archiving it after the
275
+ // 7-day window is the volume fix — recent entries still surface for diagnosis (and the
276
+ // per-tick dedup keeps live volume bounded), old ones move to the archive JSONL and out
277
+ // of the runtime store. This preserves observability while removing the standing bulk.
278
+ const ARCHIVABLE_KINDS: ReadonlySet<MeshLedgerKind> = new Set([
279
+ 'task_completed',
280
+ 'task_failed',
281
+ 'task_stalled',
282
+ 'recovery_attempted',
283
+ 'session_auto_launch',
284
+ ] as MeshLedgerKind[]);
166
285
  const DEFAULT_LEDGER_SLICE_LIMIT = 100;
167
286
  export const MAX_LEDGER_SLICE_LIMIT = 500;
168
287
 
288
+ // ─── Operating-note growth control ──────────────
289
+ // coordinator_operating_note is append-only and, unlike task_* entries, is never
290
+ // archived by compactLedger (it is not in ARCHIVABLE_KINDS — it must survive
291
+ // restarts and there is no time-based cutoff for a "lesson"). Without dedicated
292
+ // controls the note set grows without bound and duplicate/stale notes crowd the
293
+ // bounded tail that rides into the coordinator prompt. These three constants back
294
+ // the three growth controls: dedupe-on-record, tombstone/forget, keep-latest-N.
295
+
296
+ // Kind marking an operating note as retracted. A tombstone is itself an
297
+ // append-only ledger entry (history is never destroyed); readers filter out the
298
+ // notes it targets. payload: { targetNoteId?, targetFingerprint?, reason? }
299
+ export const OPERATING_NOTE_KIND: MeshLedgerKind = 'coordinator_operating_note';
300
+ export const OPERATING_NOTE_TOMBSTONE_KIND: MeshLedgerKind = 'coordinator_operating_note_tombstone';
301
+
302
+ // Dedupe window: when recording a note, if the same trimmed text already appears
303
+ // among the most recent OPERATING_NOTE_DEDUPE_WINDOW notes, the record is a no-op
304
+ // (the existing entry is returned). Keeps the prompt tail from filling with the
305
+ // same lesson recorded 20 times.
306
+ export const OPERATING_NOTE_DEDUPE_WINDOW = 40;
307
+
308
+ // Keep-latest-N: pruneOperatingNotes retains at most this many live (non-tombstoned)
309
+ // operating notes, removing the oldest surplus and any tombstoned notes from the
310
+ // store. The prompt reads a much smaller tail (20), so this bound never trims what
311
+ // a coordinator actually sees while still capping unbounded store growth.
312
+ export const OPERATING_NOTE_KEEP_LATEST = 100;
313
+
169
314
  // ─── Path Helpers ───────────────────────────────
170
315
 
171
316
  export function getLedgerDir(): string {
@@ -187,8 +332,294 @@ function getRotatedPath(meshId: string, index: number): string {
187
332
  return join(getLedgerDir(), `${safe}.${index}.jsonl`);
188
333
  }
189
334
 
335
+ function getArchivePath(meshId: string): string {
336
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
337
+ return join(getLedgerDir(), `${safe}.archive.jsonl`);
338
+ }
339
+
340
+ function getRotatedArchivePath(meshId: string, index: number): string {
341
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
342
+ return join(getLedgerDir(), `${safe}.archive.${index}.jsonl`);
343
+ }
344
+
345
+ function getArchivedCountsPath(meshId: string): string {
346
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
347
+ return join(getLedgerDir(), `${safe}.archived-counts.json`);
348
+ }
349
+
350
+ function rotateArchiveFile(meshId: string, archivePath: string): void {
351
+ let index = 1;
352
+ while (existsSync(getRotatedArchivePath(meshId, index))) {
353
+ index++;
354
+ if (index > 5) break;
355
+ }
356
+ if (index > 5) index = 5;
357
+ try {
358
+ renameSync(archivePath, getRotatedArchivePath(meshId, index));
359
+ } catch (e: any) {
360
+ process.stderr.write(`[adhdev-mesh] Archive rotation failed for mesh ${meshId}: ${e?.message || e}\n`);
361
+ }
362
+ }
363
+
364
+ interface LedgerArchivedCounts {
365
+ taskCompleted: number;
366
+ taskFailed: number;
367
+ taskStalled: number;
368
+ recoveryAttempted: number;
369
+ totalArchived: number;
370
+ lastArchivedAt: string;
371
+ }
372
+
373
+ function readArchivedCounts(meshId: string): LedgerArchivedCounts {
374
+ const path = getArchivedCountsPath(meshId);
375
+ if (!existsSync(path)) return { taskCompleted: 0, taskFailed: 0, taskStalled: 0, recoveryAttempted: 0, totalArchived: 0, lastArchivedAt: '' };
376
+ try { return JSON.parse(readFileSync(path, 'utf-8')) as LedgerArchivedCounts; } catch { return { taskCompleted: 0, taskFailed: 0, taskStalled: 0, recoveryAttempted: 0, totalArchived: 0, lastArchivedAt: '' }; }
377
+ }
378
+
379
+ function updateArchivedCounts(meshId: string, archived: MeshLedgerEntry[]): void {
380
+ const counts = readArchivedCounts(meshId);
381
+ for (const e of archived) {
382
+ if (e.kind === 'task_completed') counts.taskCompleted++;
383
+ else if (e.kind === 'task_failed') counts.taskFailed++;
384
+ else if (e.kind === 'task_stalled') counts.taskStalled++;
385
+ else if (e.kind === 'recovery_attempted') counts.recoveryAttempted++;
386
+ }
387
+ counts.totalArchived += archived.length;
388
+ counts.lastArchivedAt = new Date().toISOString();
389
+ try { writeFileSync(getArchivedCountsPath(meshId), JSON.stringify(counts), { encoding: 'utf-8', mode: 0o600 }); } catch { /* best-effort */ }
390
+ }
391
+
392
+ // ─── Worker Result Footer ───────────────────────
393
+
394
+ /**
395
+ * Footer to append to worker task messages so workers output structured results
396
+ * that the daemon parses via extractJsonObjectFromSummary / normalizeMeshWorkerResult.
397
+ *
398
+ * Usage: append buildWorkerTaskFooter() to the task message in mesh_send_task /
399
+ * mesh_enqueue_task. The coordinator prompt rules instruct coordinators to do this.
400
+ */
401
+ export function buildWorkerTaskFooter(): string {
402
+ return `
403
+
404
+ ---
405
+ When your task is done, end your final response with a JSON code block in this exact format (omit fields that don't apply):
406
+ \`\`\`json
407
+ {
408
+ "status": "completed",
409
+ "changedFiles": ["src/foo.ts", "tests/foo.test.ts"],
410
+ "gitStatus": { "branch": "feat/your-branch", "committed": true, "pushed": false },
411
+ "validationResults": [{ "command": "npm test", "status": "passed" }],
412
+ "errors": [],
413
+ "nextAction": "optional guidance for the coordinator"
414
+ }
415
+ \`\`\`
416
+ Valid status values: \`completed\` | \`failed\` | \`blocked\` | \`partial\`.`;
417
+ }
418
+
419
+ // ─── Ledger Compaction ──────────────────────────
420
+
421
+ /**
422
+ * Compact the active ledger file for a mesh by moving old terminal entries
423
+ * (task_completed, task_failed, task_stalled, recovery_attempted older than 7 days)
424
+ * to <meshId>.archive.jsonl, keeping the active file lean.
425
+ *
426
+ * Non-terminal entries (dispatch, sessions, node lifecycle) are always retained.
427
+ * Called automatically from appendLedgerEntry when the file exceeds COMPACT_THRESHOLD_BYTES.
428
+ */
429
+ export function compactLedger(meshId: string): { archivedCount: number; retainedCount: number } {
430
+ const filePath = getLedgerPath(meshId);
431
+ if (!existsSync(filePath)) return { archivedCount: 0, retainedCount: 0 };
432
+
433
+ const cutoff = Date.now() - ARCHIVE_TERMINAL_OLDER_THAN_MS;
434
+ const entries = readLedgerEntries(meshId);
435
+
436
+ const keep: MeshLedgerEntry[] = [];
437
+ const archive: MeshLedgerEntry[] = [];
438
+ for (const entry of entries) {
439
+ if (ARCHIVABLE_KINDS.has(entry.kind) && new Date(entry.timestamp).getTime() < cutoff) {
440
+ archive.push(entry);
441
+ } else {
442
+ keep.push(entry);
443
+ }
444
+ }
445
+
446
+ if (archive.length === 0) return { archivedCount: 0, retainedCount: keep.length };
447
+
448
+ // Append archived entries to the archive file, rotate if it exceeds 50MB
449
+ const archivePath = getArchivePath(meshId);
450
+ try {
451
+ if (existsSync(archivePath) && statSync(archivePath).size > 50 * 1024 * 1024) {
452
+ rotateArchiveFile(meshId, archivePath);
453
+ }
454
+ const archiveLines = archive.map(e => JSON.stringify(e)).join('\n') + '\n';
455
+ appendFileSync(archivePath, archiveLines, { encoding: 'utf-8', mode: 0o600 });
456
+ updateArchivedCounts(meshId, archive);
457
+ } catch (e: any) {
458
+ process.stderr.write(`[adhdev-mesh] Ledger archive write failed for mesh ${meshId}: ${e?.message || e}\n`);
459
+ return { archivedCount: 0, retainedCount: entries.length };
460
+ }
461
+
462
+ // Rewrite active file with retained entries only
463
+ try {
464
+ const keepLines = keep.length ? keep.map(e => JSON.stringify(e)).join('\n') + '\n' : '';
465
+ writeFileSync(filePath, keepLines, { encoding: 'utf-8', mode: 0o600 });
466
+ invalidateLedgerCache(meshId);
467
+ } catch (e: any) {
468
+ process.stderr.write(`[adhdev-mesh] Ledger compaction rewrite failed for mesh ${meshId}: ${e?.message || e}\n`);
469
+ return { archivedCount: archive.length, retainedCount: keep.length };
470
+ }
471
+
472
+ // G2: mirror the compaction in SQLite so the runtime store matches the active
473
+ // ledger set. Archived entries live on in the JSONL archive files (export/debug).
474
+ try {
475
+ MeshRuntimeStore.getInstance().deleteLedgerEntries(meshId, archive.map(e => e.id));
476
+ invalidateLedgerCache(meshId);
477
+ } catch { /* best-effort; summary counts absorb the difference via archived counts */ }
478
+
479
+ return { archivedCount: archive.length, retainedCount: keep.length };
480
+ }
481
+
190
482
  // ─── Core API ───────────────────────────────────
191
483
 
484
+ function readNonEmptyString(value: unknown): string | undefined {
485
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
486
+ }
487
+
488
+ function readStringArray(value: unknown): string[] {
489
+ if (!Array.isArray(value)) return [];
490
+ return value.map(item => readNonEmptyString(item)).filter(Boolean) as string[];
491
+ }
492
+
493
+ function extractJsonObjectFromSummary(summary?: string): Record<string, unknown> | undefined {
494
+ const text = readNonEmptyString(summary);
495
+ if (!text) return undefined;
496
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
497
+ const candidates = [fenced?.[1], text].filter(Boolean) as string[];
498
+ for (const candidate of candidates) {
499
+ const trimmed = candidate.trim();
500
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
501
+ try {
502
+ const parsed = JSON.parse(trimmed);
503
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
504
+ // Require at least one mesh worker result field to avoid false positives
505
+ // (e.g. JSON from tool call outputs or log lines in the final summary).
506
+ const hasWorkerShape = 'status' in parsed && (
507
+ 'changedFiles' in parsed || 'errors' in parsed
508
+ || 'gitStatus' in parsed || 'nextAction' in parsed
509
+ || 'validationResults' in parsed
510
+ );
511
+ if (hasWorkerShape) return parsed;
512
+ }
513
+ } catch { /* try next candidate */ }
514
+ }
515
+ return undefined;
516
+ }
517
+
518
+ function normalizeValidationResults(value: unknown): MeshValidationResultArtifact[] {
519
+ if (!Array.isArray(value)) return [];
520
+ return value
521
+ .filter(item => item && typeof item === 'object' && !Array.isArray(item))
522
+ .map((item: any) => {
523
+ const status = ['passed', 'failed', 'skipped', 'unknown'].includes(item.status) ? item.status : 'unknown';
524
+ return {
525
+ ...(readNonEmptyString(item.command) ? { command: readNonEmptyString(item.command) } : {}),
526
+ status,
527
+ ...(Number.isFinite(Number(item.durationMs)) ? { durationMs: Number(item.durationMs) } : {}),
528
+ ...(readNonEmptyString(item.outputPath) ? { outputPath: readNonEmptyString(item.outputPath) } : {}),
529
+ ...(readNonEmptyString(item.summary) ? { summary: readNonEmptyString(item.summary) } : {}),
530
+ };
531
+ });
532
+ }
533
+
534
+ function normalizeProcessArtifacts(value: unknown): MeshProcessArtifact[] {
535
+ if (!Array.isArray(value)) return [];
536
+ const kinds = new Set(['process', 'log', 'port', 'window', 'session', 'file', 'url', 'other']);
537
+ return value
538
+ .filter(item => item && typeof item === 'object' && !Array.isArray(item))
539
+ .map((item: any) => ({
540
+ kind: kinds.has(item.kind) ? item.kind : 'other',
541
+ ...(readNonEmptyString(item.id) ? { id: readNonEmptyString(item.id) } : {}),
542
+ ...(readNonEmptyString(item.label) ? { label: readNonEmptyString(item.label) } : {}),
543
+ ...(readNonEmptyString(item.locator) ? { locator: readNonEmptyString(item.locator) } : {}),
544
+ ...(Number.isFinite(Number(item.pid)) ? { pid: Number(item.pid) } : {}),
545
+ ...(Number.isFinite(Number(item.port)) ? { port: Number(item.port) } : {}),
546
+ ...(readNonEmptyString(item.url) ? { url: readNonEmptyString(item.url) } : {}),
547
+ ...(readNonEmptyString(item.path) ? { path: readNonEmptyString(item.path) } : {}),
548
+ ...(readNonEmptyString(item.sessionId) ? { sessionId: readNonEmptyString(item.sessionId) } : {}),
549
+ ...(typeof item.keepRunning === 'boolean' ? { keepRunning: item.keepRunning } : {}),
550
+ ...(item.metadata && typeof item.metadata === 'object' && !Array.isArray(item.metadata) ? { metadata: item.metadata as Record<string, unknown> } : {}),
551
+ }));
552
+ }
553
+
554
+ export function normalizeMeshWorkerResult(input?: Record<string, unknown>, source: MeshWorkerResultArtifact['source'] = 'explicit_metadata'): MeshWorkerResultArtifact {
555
+ const raw = input && typeof input === 'object' ? input : {};
556
+ const status = ['completed', 'failed', 'blocked', 'partial', 'unknown'].includes(String(raw.status))
557
+ ? raw.status as MeshWorkerResultStatus
558
+ : 'unknown';
559
+ const gitStatus = raw.gitStatus && typeof raw.gitStatus === 'object' && !Array.isArray(raw.gitStatus)
560
+ ? raw.gitStatus as Record<string, unknown>
561
+ : undefined;
562
+ return {
563
+ status,
564
+ ...(readNonEmptyString(raw.classification) ? { classification: readNonEmptyString(raw.classification) } : {}),
565
+ changedFiles: readStringArray(raw.changedFiles),
566
+ validationResults: normalizeValidationResults(raw.validationResults),
567
+ ...(gitStatus ? { gitStatus } : {}),
568
+ processArtifacts: normalizeProcessArtifacts(raw.processArtifacts),
569
+ errors: readStringArray(raw.errors),
570
+ ...(readNonEmptyString(raw.nextAction) ? { nextAction: readNonEmptyString(raw.nextAction) } : {}),
571
+ requiresUserAction: raw.requiresUserAction === true,
572
+ source,
573
+ };
574
+ }
575
+
576
+ /**
577
+ * NOTIF Defect-2b: does the summary contain ANY parseable JSON object answer (not just a
578
+ * worker-result-shaped one)? Some providers (and every MAGI replica) emit a complete, valid
579
+ * answer as a JSON envelope that has no `status`/`changedFiles` worker-result fields, so
580
+ * extractJsonObjectFromSummary returns undefined and the completion is mislabelled
581
+ * source='default' → evidenceLevel='insufficient' even though a real answer was produced.
582
+ * This is a conservative existence check: it only returns true when a JSON object actually
583
+ * parses out of the summary (raw or fenced), so an empty / prose-only / unparseable summary
584
+ * still resolves to 'default'.
585
+ */
586
+ function summaryHasParseableJsonAnswer(summary?: string): boolean {
587
+ const text = readNonEmptyString(summary);
588
+ if (!text) return false;
589
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
590
+ const candidates = [fenced?.[1], text].filter(Boolean) as string[];
591
+ for (const candidate of candidates) {
592
+ const trimmed = candidate.trim();
593
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
594
+ try {
595
+ const parsed = JSON.parse(trimmed);
596
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
597
+ && Object.keys(parsed).length > 0) {
598
+ return true;
599
+ }
600
+ } catch { /* try next candidate */ }
601
+ }
602
+ return false;
603
+ }
604
+
605
+ function resolveWorkerResult(opts: BuildTaskCompletionEvidenceOptions): MeshWorkerResultArtifact {
606
+ if (opts.workerResult && typeof opts.workerResult === 'object') {
607
+ return normalizeMeshWorkerResult(opts.workerResult, 'explicit_metadata');
608
+ }
609
+ const parsed = extractJsonObjectFromSummary(opts.finalSummary);
610
+ if (parsed) {
611
+ return normalizeMeshWorkerResult(parsed, 'final_summary_json');
612
+ }
613
+ // NOTIF Defect-2b: no worker-result-shaped JSON, but a parseable JSON answer IS present
614
+ // (the common MAGI / answer-only case). Treat it as concrete evidence so the completion is
615
+ // not labelled 'insufficient', while keeping the worker-result fields empty (status stays
616
+ // 'unknown') — we only know an answer parsed, not its task outcome.
617
+ if (summaryHasParseableJsonAnswer(opts.finalSummary)) {
618
+ return normalizeMeshWorkerResult(undefined, 'parseable_answer');
619
+ }
620
+ return normalizeMeshWorkerResult(undefined, 'default');
621
+ }
622
+
192
623
  export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence {
193
624
  const providerSessionId = opts.providerSessionId?.trim() || undefined;
194
625
  const providerType = opts.providerType?.trim() || undefined;
@@ -205,6 +636,7 @@ export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOpt
205
636
  providerSessionId,
206
637
  finalSummaryAvailable: typeof opts.finalSummary === 'string' && opts.finalSummary.trim().length > 0,
207
638
  },
639
+ workerResult: resolveWorkerResult(opts),
208
640
  git: {
209
641
  status: 'deferred',
210
642
  reason: 'ordinary_completion_git_status_not_checked',
@@ -221,6 +653,32 @@ export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOpt
221
653
  };
222
654
  }
223
655
 
656
+ /**
657
+ * Build the v2 originating-coordinator stamp for a task_dispatched ledger entry
658
+ * (B2a / design decision §2). This is the source of truth from which a worker's
659
+ * completion emit later restores `dispatchedBy` — it records which coordinator
660
+ * dispatched the task, so the terminal event can be routed (unicast) back to it.
661
+ *
662
+ * Nested under `payload.originatingCoordinator`; additive, so existing readers
663
+ * of the task_dispatched payload are unaffected. Returns undefined when no
664
+ * coordinator daemon id is known (the pre-v2 path) so the caller omits the stamp
665
+ * entirely rather than writing a malformed identity — those entries stay v1 and
666
+ * are broadcast-treated during rollout.
667
+ */
668
+ export function buildLedgerOriginatingCoordinatorStamp(fields: {
669
+ coordinatorDaemonId?: string | null;
670
+ coordinatorRunId?: string | null;
671
+ coordinatorSessionId?: string | null;
672
+ }): MeshLedgerOriginatingCoordinatorV2 | undefined {
673
+ const originatingCoordinator = coordinatorIdentityFromEmitFields({
674
+ daemonId: fields.coordinatorDaemonId,
675
+ coordinatorRunId: fields.coordinatorRunId,
676
+ sessionId: fields.coordinatorSessionId,
677
+ });
678
+ if (!originatingCoordinator) return undefined;
679
+ return { originatingCoordinator, protocolVersion: MESH_PROTOCOL_VERSION_V2 };
680
+ }
681
+
224
682
  /**
225
683
  * Append a new entry to the mesh ledger.
226
684
  * Handles file creation, rotation on size overflow, and atomic writes.
@@ -231,6 +689,23 @@ export function appendLedgerEntry(
231
689
  meshId: string,
232
690
  partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>,
233
691
  ): MeshLedgerEntry {
692
+ // Fix (1) dedupe-on-record: for a coordinator_operating_note, if the same
693
+ // trimmed text already appears among the most recent OPERATING_NOTE_DEDUPE_WINDOW
694
+ // notes, do NOT append a duplicate — return the existing entry so the bounded
695
+ // prompt tail can't be crowded by the same lesson recorded repeatedly. Other
696
+ // kinds (task_completed, …) are untouched.
697
+ if (partial.kind === OPERATING_NOTE_KIND) {
698
+ const text = operatingNoteText(partial.payload);
699
+ if (text) {
700
+ const recentNotes = readLedgerEntries(meshId, {
701
+ kind: [OPERATING_NOTE_KIND],
702
+ tail: OPERATING_NOTE_DEDUPE_WINDOW,
703
+ });
704
+ const existing = recentNotes.find(e => operatingNoteText(e.payload) === text);
705
+ if (existing) return existing;
706
+ }
707
+ }
708
+
234
709
  const entry: MeshLedgerEntry = {
235
710
  id: randomUUID(),
236
711
  meshId,
@@ -240,28 +715,188 @@ export function appendLedgerEntry(
240
715
 
241
716
  const filePath = getLedgerPath(meshId);
242
717
 
243
- // Rotate if file exceeds max size
718
+ // Compact or rotate based on file size
244
719
  if (existsSync(filePath)) {
245
720
  try {
246
721
  const stat = statSync(filePath);
247
722
  if (stat.size >= MAX_FILE_SIZE_BYTES) {
248
723
  rotateLedgerFile(meshId, filePath);
724
+ } else if (stat.size >= COMPACT_THRESHOLD_BYTES) {
725
+ compactLedger(meshId);
249
726
  }
250
727
  } catch {
251
728
  // stat failed — proceed with append anyway
252
729
  }
253
730
  }
254
731
 
732
+ // Write to SQLite (G2: primary runtime read/write path)
733
+ try {
734
+ MeshRuntimeStore.getInstance().appendLedgerEntry({
735
+ id: entry.id,
736
+ meshId: entry.meshId,
737
+ timestamp: entry.timestamp,
738
+ kind: entry.kind,
739
+ nodeId: entry.nodeId ?? null,
740
+ sessionId: entry.sessionId ?? null,
741
+ providerType: entry.providerType ?? null,
742
+ payload: entry.payload,
743
+ });
744
+ } catch {
745
+ // SQLite write failed but the JSONL append below still records the entry.
746
+ // Reset the one-time import flag so the next read re-imports from JSONL
747
+ // and the store self-heals instead of silently missing this entry.
748
+ ledgerImportDone.delete(meshId);
749
+ }
750
+
751
+ // Also write to JSONL (retained as export/import/debug/legacy artifact)
255
752
  try {
256
753
  const line = JSON.stringify(entry) + '\n';
257
754
  appendFileSync(filePath, line, { encoding: 'utf-8', mode: 0o600 });
755
+ invalidateLedgerCache(meshId);
258
756
  meshLedgerEvents.emit('append', meshId, entry);
757
+ // Fix (3) keep-latest-N: operating notes are never archived by compactLedger,
758
+ // so cap their store footprint here. Runs only when a note (or its tombstone)
759
+ // is recorded, and is a no-op until the live-note count exceeds the bound.
760
+ if (entry.kind === OPERATING_NOTE_KIND || entry.kind === OPERATING_NOTE_TOMBSTONE_KIND) {
761
+ try { pruneOperatingNotes(meshId); } catch { /* prune is best-effort */ }
762
+ }
259
763
  return entry;
260
764
  } catch (e: any) {
261
765
  throw new Error(`Failed to append to ledger for mesh ${meshId}: ${e.message}`);
262
766
  }
263
767
  }
264
768
 
769
+ // ─── Operating-note growth controls ─────────────
770
+
771
+ /** Extract the trimmed note text from a coordinator_operating_note payload. */
772
+ function operatingNoteText(payload: Record<string, unknown> | undefined): string | undefined {
773
+ const text = payload && typeof payload.text === 'string' ? payload.text.trim() : '';
774
+ return text || undefined;
775
+ }
776
+
777
+ /**
778
+ * Set of trimmed-text fingerprints and note ids retracted by tombstone entries in
779
+ * the given entry set. A tombstone targets by note id and/or by text fingerprint.
780
+ */
781
+ function collectOperatingNoteTombstones(entries: MeshLedgerEntry[]): { ids: Set<string>; fingerprints: Set<string> } {
782
+ const ids = new Set<string>();
783
+ const fingerprints = new Set<string>();
784
+ for (const e of entries) {
785
+ if (e.kind !== OPERATING_NOTE_TOMBSTONE_KIND) continue;
786
+ const p = e.payload || {};
787
+ const targetId = typeof p.targetNoteId === 'string' ? p.targetNoteId.trim() : '';
788
+ const targetFp = typeof p.targetFingerprint === 'string' ? p.targetFingerprint.trim() : '';
789
+ if (targetId) ids.add(targetId);
790
+ if (targetFp) fingerprints.add(targetFp);
791
+ }
792
+ return { ids, fingerprints };
793
+ }
794
+
795
+ /** True if the operating note is retracted by any tombstone in `tombstones`. */
796
+ export function isOperatingNoteTombstoned(
797
+ entry: Pick<MeshLedgerEntry, 'id' | 'payload'>,
798
+ tombstones: { ids: Set<string>; fingerprints: Set<string> },
799
+ ): boolean {
800
+ if (tombstones.ids.has(entry.id)) return true;
801
+ const text = operatingNoteText(entry.payload);
802
+ return text ? tombstones.fingerprints.has(text) : false;
803
+ }
804
+
805
+ /**
806
+ * Fix (2) supersede/remove: append a tombstone that retracts a coordinator
807
+ * operating note. Targets by note id and/or by exact trimmed text (a text target
808
+ * retracts every note with that text). History is preserved — the notes stay in
809
+ * the ledger but readers filter them out. Returns how many currently-live notes
810
+ * the tombstone will hide.
811
+ */
812
+ export function tombstoneOperatingNote(
813
+ meshId: string,
814
+ target: { noteId?: string; text?: string; reason?: string },
815
+ ): { tombstone: MeshLedgerEntry; matched: number } {
816
+ const noteId = typeof target.noteId === 'string' ? target.noteId.trim() : '';
817
+ const fingerprint = typeof target.text === 'string' ? target.text.trim() : '';
818
+ if (!noteId && !fingerprint) {
819
+ throw new Error('tombstoneOperatingNote requires a noteId or text target');
820
+ }
821
+
822
+ // Count currently-live matches (not already tombstoned) for the caller's report.
823
+ const notes = readOperatingNotes(meshId);
824
+ const matched = notes.filter(n =>
825
+ (noteId && n.id === noteId) || (fingerprint && operatingNoteText(n.payload) === fingerprint),
826
+ ).length;
827
+
828
+ const tombstone = appendLedgerEntry(meshId, {
829
+ kind: OPERATING_NOTE_TOMBSTONE_KIND,
830
+ payload: {
831
+ ...(noteId ? { targetNoteId: noteId } : {}),
832
+ ...(fingerprint ? { targetFingerprint: fingerprint } : {}),
833
+ ...(target.reason && target.reason.trim() ? { reason: target.reason.trim() } : {}),
834
+ forgottenAt: new Date().toISOString(),
835
+ },
836
+ });
837
+ return { tombstone, matched };
838
+ }
839
+
840
+ /**
841
+ * Read live operating notes (tombstoned notes filtered out), oldest→newest.
842
+ * `tail` bounds the number of live notes returned (the freshest N).
843
+ */
844
+ export function readOperatingNotes(meshId: string, opts?: { tail?: number }): MeshLedgerEntry[] {
845
+ const raw = getCachedRawEntries(meshId);
846
+ const tombstones = collectOperatingNoteTombstones(raw);
847
+ let notes = raw.filter(e => e.kind === OPERATING_NOTE_KIND && !isOperatingNoteTombstoned(e, tombstones));
848
+ if (opts?.tail && opts.tail > 0 && notes.length > opts.tail) {
849
+ notes = notes.slice(-opts.tail);
850
+ }
851
+ return notes;
852
+ }
853
+
854
+ /**
855
+ * Fix (3) keep-latest-N prune for coordinator_operating_note. Removes, from the
856
+ * store, (a) every note retracted by a tombstone, and (b) the oldest live notes
857
+ * beyond `keepLatest`. Tombstone entries themselves are retained as an audit trail
858
+ * of what was forgotten. Returns the number of note entries removed.
859
+ */
860
+ export function pruneOperatingNotes(meshId: string, keepLatest: number = OPERATING_NOTE_KEEP_LATEST): number {
861
+ const raw = getCachedRawEntries(meshId);
862
+ const tombstones = collectOperatingNoteTombstones(raw);
863
+
864
+ const removeIds: string[] = [];
865
+ const liveNotes: MeshLedgerEntry[] = [];
866
+ for (const e of raw) {
867
+ if (e.kind !== OPERATING_NOTE_KIND) continue;
868
+ if (isOperatingNoteTombstoned(e, tombstones)) {
869
+ removeIds.push(e.id); // tombstoned notes are pruned first
870
+ } else {
871
+ liveNotes.push(e);
872
+ }
873
+ }
874
+
875
+ // Drop the oldest live notes beyond keepLatest; the freshest (prompt tail) survive.
876
+ const bound = Math.max(0, Math.floor(keepLatest));
877
+ if (liveNotes.length > bound) {
878
+ for (const e of liveNotes.slice(0, liveNotes.length - bound)) removeIds.push(e.id);
879
+ }
880
+
881
+ if (removeIds.length === 0) return 0;
882
+
883
+ try {
884
+ MeshRuntimeStore.getInstance().deleteLedgerEntries(meshId, removeIds);
885
+ } catch { /* store unavailable — JSONL rewrite below still trims */ }
886
+
887
+ // Rewrite the JSONL mirror without the pruned note entries so the export
888
+ // artifact stays consistent with the store.
889
+ try {
890
+ const remaining = readLedgerFile(meshId).filter(e => !removeIds.includes(e.id));
891
+ const filePath = getLedgerPath(meshId);
892
+ const lines = remaining.length ? remaining.map(e => JSON.stringify(e)).join('\n') + '\n' : '';
893
+ writeFileSync(filePath, lines, { encoding: 'utf-8', mode: 0o600 });
894
+ } catch { /* JSONL rewrite best-effort; store is the primary read path */ }
895
+
896
+ invalidateLedgerCache(meshId);
897
+ return removeIds.length;
898
+ }
899
+
265
900
  function clampLedgerSliceLimit(limit: unknown): number {
266
901
  if (typeof limit !== 'number' || !Number.isFinite(limit)) return DEFAULT_LEDGER_SLICE_LIMIT;
267
902
  return Math.max(1, Math.min(MAX_LEDGER_SLICE_LIMIT, Math.floor(limit)));
@@ -286,8 +921,9 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
286
921
  if (entries.length === 0) return { accepted: 0, skippedDuplicate: 0, rejectedInvalid: 0, entries: [] };
287
922
  const ledgerPath = getLedgerPath(meshId);
288
923
 
289
- // Read existing to deduplicate by ID
290
- const existing = new Set(readLedgerEntries(meshId).map(e => e.id));
924
+ // Dedup against recent entries only — P2P replication is incremental (cursor-based),
925
+ // so duplicates appear in the recent tail, not deep history.
926
+ const existing = new Set(readLedgerEntries(meshId, { tail: 1000 }).map(e => e.id));
291
927
  const validEntries: MeshLedgerEntry[] = [];
292
928
  let rejectedInvalid = 0;
293
929
  let skippedDuplicate = 0;
@@ -308,9 +944,24 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
308
944
  return { accepted: 0, skippedDuplicate, rejectedInvalid, entries: [] };
309
945
  }
310
946
 
947
+ // G2: write to SQLite (primary runtime store); INSERT OR IGNORE dedups by id.
948
+ try {
949
+ MeshRuntimeStore.getInstance().importLedgerEntries(validEntries.map(e => ({
950
+ id: e.id,
951
+ meshId: e.meshId,
952
+ timestamp: e.timestamp,
953
+ kind: e.kind,
954
+ nodeId: e.nodeId ?? null,
955
+ sessionId: e.sessionId ?? null,
956
+ providerType: e.providerType ?? null,
957
+ payload: e.payload ?? {},
958
+ })));
959
+ } catch { /* best-effort; JSONL append below still records the entries */ }
960
+
311
961
  try {
312
962
  const lines = validEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
313
963
  appendFileSync(ledgerPath, lines, { encoding: 'utf-8', mode: 0o600 });
964
+ invalidateLedgerCache(meshId);
314
965
  for (const entry of validEntries) {
315
966
  meshLedgerEvents.emit('append', meshId, entry);
316
967
  }
@@ -320,103 +971,155 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
320
971
  }
321
972
  }
322
973
 
323
- /**
324
- * Read ledger entries with optional filtering.
325
- */
326
- export function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[] {
974
+ // ─── Ledger Read Cache ─────────────────────────
975
+ // Absorbs repeated reads within a single event-processing burst (e.g. agent:stopped
976
+ // triggers shouldSuppressIntentionalCleanupStop, findRecentTerminalLedgerEvidence,
977
+ // hasDispatchAfterTerminal, and getSessionRecoveryContext all reading the same store).
978
+ // TTL is 100ms: short enough to stay current, long enough to cover one event cycle.
979
+ // Cache is invalidated on every write (append, remote import, compaction).
980
+
981
+ const ledgerReadCache = new Map<string, { entries: MeshLedgerEntry[]; cachedAt: number }>();
982
+ const LEDGER_CACHE_TTL_MS = 100;
983
+
984
+ function readLedgerFile(meshId: string): MeshLedgerEntry[] {
327
985
  const filePath = getLedgerPath(meshId);
328
986
  if (!existsSync(filePath)) return [];
329
-
330
987
  let content: string;
988
+ try { content = readFileSync(filePath, 'utf-8'); } catch { return []; }
989
+ const entries: MeshLedgerEntry[] = [];
990
+ for (const line of content.split('\n')) {
991
+ if (!line.trim()) continue;
992
+ try {
993
+ const entry = JSON.parse(line) as MeshLedgerEntry;
994
+ if (entry.id && entry.kind) entries.push(entry);
995
+ } catch { /* skip malformed lines */ }
996
+ }
997
+ return entries;
998
+ }
999
+
1000
+ // ─── G2: One-Time JSONL → SQLite Import ─────────
1001
+ // On the first SQLite read for a mesh (per store instance), import any legacy
1002
+ // JSONL entries into mesh_event_ledger. INSERT OR IGNORE makes this idempotent:
1003
+ // dual-written entries are skipped, only pre-cutover legacy entries are added.
1004
+ // Keyed by the store instance so MeshRuntimeStore.resetForTests() (fresh DB)
1005
+ // naturally re-imports.
1006
+
1007
+ let ledgerImportStoreRef: MeshRuntimeStore | undefined;
1008
+ const ledgerImportDone = new Set<string>();
1009
+
1010
+ function ensureLedgerImported(store: MeshRuntimeStore, meshId: string): void {
1011
+ if (ledgerImportStoreRef !== store) {
1012
+ ledgerImportDone.clear();
1013
+ ledgerImportStoreRef = store;
1014
+ }
1015
+ if (ledgerImportDone.has(meshId)) return;
1016
+ ledgerImportDone.add(meshId);
1017
+ const fileEntries = readLedgerFile(meshId);
1018
+ if (fileEntries.length === 0) return;
331
1019
  try {
332
- content = readFileSync(filePath, 'utf-8');
1020
+ store.importLedgerEntries(fileEntries.map(e => ({
1021
+ id: e.id,
1022
+ meshId: e.meshId,
1023
+ timestamp: e.timestamp,
1024
+ kind: e.kind,
1025
+ nodeId: e.nodeId ?? null,
1026
+ sessionId: e.sessionId ?? null,
1027
+ providerType: e.providerType ?? null,
1028
+ payload: e.payload ?? {},
1029
+ })));
1030
+ } catch { /* import is best-effort; reads fall back to JSONL on store failure */ }
1031
+ }
1032
+
1033
+ function readLedgerFromStore(meshId: string): MeshLedgerEntry[] {
1034
+ const store = MeshRuntimeStore.getInstance();
1035
+ ensureLedgerImported(store, meshId);
1036
+ return store.readLedgerEntriesOrdered(meshId).map(r => ({
1037
+ id: r.id,
1038
+ meshId: r.meshId,
1039
+ timestamp: r.timestamp,
1040
+ kind: r.kind as MeshLedgerKind,
1041
+ ...(r.nodeId ? { nodeId: r.nodeId } : {}),
1042
+ ...(r.sessionId ? { sessionId: r.sessionId } : {}),
1043
+ ...(r.providerType ? { providerType: r.providerType } : {}),
1044
+ payload: (r.payload && typeof r.payload === 'object' ? r.payload : {}) as Record<string, unknown>,
1045
+ }));
1046
+ }
1047
+
1048
+ function getCachedRawEntries(meshId: string): MeshLedgerEntry[] {
1049
+ const now = Date.now();
1050
+ const cached = ledgerReadCache.get(meshId);
1051
+ if (cached && now - cached.cachedAt < LEDGER_CACHE_TTL_MS) return cached.entries;
1052
+ let entries: MeshLedgerEntry[];
1053
+ try {
1054
+ // G2: SQLite mesh_event_ledger is the primary runtime read path.
1055
+ entries = readLedgerFromStore(meshId);
333
1056
  } catch {
334
- return [];
1057
+ // Store unavailable — fall back to the JSONL export artifact.
1058
+ entries = readLedgerFile(meshId);
335
1059
  }
1060
+ ledgerReadCache.set(meshId, { entries, cachedAt: now });
1061
+ return entries;
1062
+ }
336
1063
 
337
- const lines = content.split('\n').filter(line => line.trim());
338
- let entries: MeshLedgerEntry[] = [];
1064
+ function invalidateLedgerCache(meshId: string): void {
1065
+ ledgerReadCache.delete(meshId);
1066
+ }
339
1067
 
340
- for (const line of lines) {
341
- try {
342
- const entry = JSON.parse(line) as MeshLedgerEntry;
343
- if (!entry.id || !entry.kind) continue;
344
- entries.push(entry);
345
- } catch {
346
- // Skip malformed lines
347
- }
348
- }
1068
+ /**
1069
+ * Test helper: clear all runtime ledger state for a mesh — SQLite rows, read
1070
+ * cache, and the one-time import flag. JSONL files are the caller's concern.
1071
+ */
1072
+ export function __clearMeshLedgerForTests(meshId: string): void {
1073
+ try {
1074
+ MeshRuntimeStore.getInstance().clearLedgerForMesh(meshId);
1075
+ } catch { /* store unavailable — nothing to clear */ }
1076
+ ledgerReadCache.delete(meshId);
1077
+ ledgerImportDone.delete(meshId);
1078
+ }
1079
+
1080
+ /**
1081
+ * Read ledger entries with optional filtering.
1082
+ * G2: SQLite (mesh_event_ledger) is the primary read path; legacy JSONL is
1083
+ * imported once per store instance and otherwise retained as an
1084
+ * export/import/debug artifact only.
1085
+ */
1086
+ export function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[] {
1087
+ let entries = getCachedRawEntries(meshId);
349
1088
 
350
- // Apply filters
351
1089
  if (opts?.since) {
352
1090
  const sinceDate = new Date(opts.since).getTime();
353
- if (!isNaN(sinceDate)) {
354
- entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
355
- }
1091
+ if (!isNaN(sinceDate)) entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
356
1092
  }
357
-
358
1093
  if (opts?.kind?.length) {
359
1094
  const kindSet = new Set(opts.kind);
360
1095
  entries = entries.filter(e => kindSet.has(e.kind));
361
1096
  }
362
-
363
- // Apply tail (return last N entries)
1097
+ if (opts?.node && opts.node.trim()) {
1098
+ const node = opts.node.trim();
1099
+ entries = entries.filter(e => e.nodeId && daemonIdsEquivalent(e.nodeId, node));
1100
+ }
364
1101
  if (opts?.tail && opts.tail > 0 && entries.length > opts.tail) {
365
1102
  entries = entries.slice(-opts.tail);
366
1103
  }
367
-
368
1104
  return entries;
369
1105
  }
370
1106
 
371
1107
  /**
372
- * Read a bounded, cursor-addressable ledger slice for local-first/P2P replication.
373
- * The result is intentionally small and self-describing so coordinators can query
374
- * remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
375
- */
376
- export function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice {
377
- const limit = clampLedgerSliceLimit(opts?.limit);
378
- let entries = readLedgerEntries(meshId, { since: opts?.since, kind: opts?.kind });
379
- const afterId = typeof opts?.afterId === 'string' && opts.afterId.trim() ? opts.afterId.trim() : null;
380
- if (afterId) {
381
- const index = entries.findIndex(entry => entry.id === afterId);
382
- entries = index >= 0 ? entries.slice(index + 1) : entries;
383
- }
384
- const bounded = entries.slice(0, limit);
385
- return {
386
- protocol: 'adhdev.mesh.ledger.slice.v1',
387
- meshId,
388
- entries: bounded,
389
- cursor: {
390
- afterId,
391
- nextAfterId: bounded.length ? bounded[bounded.length - 1].id : afterId,
392
- limit,
393
- hasMore: entries.length > bounded.length,
394
- },
395
- summary: getLedgerSummary(meshId),
396
- sourceOfTruth: {
397
- kind: 'local_jsonl',
398
- path: getLedgerPath(meshId),
399
- bounded: true,
400
- maxLimit: MAX_LEDGER_SLICE_LIMIT,
401
- },
402
- };
403
- }
404
-
405
- /**
406
- * Get a summary of mesh activity from the ledger.
1108
+ * Build a ledger summary from pre-loaded entries. Used by both getLedgerSummary
1109
+ * and readLedgerSlice so they share a single getCachedRawEntries() call.
407
1110
  */
408
- export function getLedgerSummary(meshId: string): MeshLedgerSummary {
409
- const entries = readLedgerEntries(meshId);
1111
+ function buildLedgerSummary(meshId: string, entries: MeshLedgerEntry[]): MeshLedgerSummary {
1112
+ const archived = readArchivedCounts(meshId);
410
1113
  const now = Date.now();
411
1114
  const recentFailureCutoff = now - RECENT_FAILURE_WINDOW_MS;
412
1115
 
413
1116
  const summary: MeshLedgerSummary = {
414
1117
  meshId,
415
- totalEntries: entries.length,
1118
+ totalEntries: entries.length + archived.totalArchived,
416
1119
  taskDispatched: 0,
417
- taskCompleted: 0,
418
- taskFailed: 0,
419
- taskStalled: 0,
1120
+ taskCompleted: archived.taskCompleted,
1121
+ taskFailed: archived.taskFailed,
1122
+ taskStalled: archived.taskStalled,
420
1123
  sessionLaunched: 0,
421
1124
  checkpointCreated: 0,
422
1125
  lastActivityAt: null,
@@ -451,6 +1154,79 @@ export function getLedgerSummary(meshId: string): MeshLedgerSummary {
451
1154
  return summary;
452
1155
  }
453
1156
 
1157
+ /**
1158
+ * Read a bounded, cursor-addressable ledger slice for local-first/P2P replication.
1159
+ * The result is intentionally small and self-describing so coordinators can query
1160
+ * remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
1161
+ */
1162
+ export function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice {
1163
+ const limit = clampLedgerSliceLimit(opts?.limit);
1164
+ // Load raw entries once and share between filtering, pagination, and summary.
1165
+ const rawEntries = getCachedRawEntries(meshId);
1166
+
1167
+ let entries: MeshLedgerEntry[] = rawEntries;
1168
+ if (opts?.since) {
1169
+ const sinceDate = new Date(opts.since).getTime();
1170
+ if (!isNaN(sinceDate)) entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
1171
+ }
1172
+ if (opts?.kind?.length) {
1173
+ const kindSet = new Set(opts.kind);
1174
+ entries = entries.filter(e => kindSet.has(e.kind));
1175
+ }
1176
+
1177
+ const afterId = typeof opts?.afterId === 'string' && opts.afterId.trim() ? opts.afterId.trim() : null;
1178
+ if (afterId) {
1179
+ const index = entries.findIndex(entry => entry.id === afterId);
1180
+ entries = index >= 0 ? entries.slice(index + 1) : entries;
1181
+ }
1182
+ const bounded = entries.slice(0, limit);
1183
+ return {
1184
+ protocol: 'adhdev.mesh.ledger.slice.v1',
1185
+ meshId,
1186
+ entries: bounded,
1187
+ cursor: {
1188
+ afterId,
1189
+ nextAfterId: bounded.length ? bounded[bounded.length - 1].id : afterId,
1190
+ limit,
1191
+ hasMore: entries.length > bounded.length,
1192
+ },
1193
+ summary: buildLedgerSummary(meshId, rawEntries),
1194
+ sourceOfTruth: {
1195
+ kind: 'local_jsonl',
1196
+ path: getLedgerPath(meshId),
1197
+ bounded: true,
1198
+ maxLimit: MAX_LEDGER_SLICE_LIMIT,
1199
+ },
1200
+ };
1201
+ }
1202
+
1203
+ /**
1204
+ * G4: Read a bounded ledger slice from the SQLite mesh_event_ledger table.
1205
+ * This is the preferred P2P reconcile read path; JSONL files are retained as
1206
+ * export/import/debug/legacy artifacts only.
1207
+ *
1208
+ * Returns a shape structurally compatible with MeshLedgerSlice (minus the
1209
+ * JSONL-specific `summary` and `sourceOfTruth.path` fields) so callers can
1210
+ * pass it to buildMeshLedgerReplicaEvidence without modification.
1211
+ */
1212
+ export function readLedgerSliceFromStore(meshId: string, opts?: ReadLedgerSliceOptions): ReturnType<typeof MeshRuntimeStore.prototype.readLedgerSlice> {
1213
+ return MeshRuntimeStore.getInstance().readLedgerSlice(meshId, {
1214
+ afterId: opts?.afterId,
1215
+ since: opts?.since,
1216
+ // ReadLedgerSliceOptions allows kind as array; SQLite path takes a single kind string.
1217
+ // Pass first kind value if provided; callers needing multi-kind filtering should use readLedgerSlice (JSONL).
1218
+ kind: opts?.kind?.length ? opts.kind[0] : undefined,
1219
+ limit: opts?.limit,
1220
+ });
1221
+ }
1222
+
1223
+ /**
1224
+ * Get a summary of mesh activity from the ledger.
1225
+ */
1226
+ export function getLedgerSummary(meshId: string): MeshLedgerSummary {
1227
+ return buildLedgerSummary(meshId, getCachedRawEntries(meshId));
1228
+ }
1229
+
454
1230
  // ─── Recovery Context ───────────────────────────
455
1231
 
456
1232
  export interface SessionRecoveryContext {
@@ -485,46 +1261,64 @@ export function getSessionRecoveryContext(
485
1261
  },
486
1262
  ): SessionRecoveryContext {
487
1263
  const maxRetries = opts.maxRetries ?? 1;
488
- const entries = readLedgerEntries(meshId);
1264
+ // tail:500 is sufficient — task_dispatched is never archived (only terminal kinds are),
1265
+ // so dispatch history is always present. The 30-min failure window means we never need
1266
+ // more than a few dozen recent entries for consecutiveNodeFailures. Bounding to 500
1267
+ // avoids a full O(n) scan for meshes with many historical entries.
1268
+ const entries = readLedgerEntries(meshId, { tail: 500 });
489
1269
 
490
- // Find the last task_dispatched for this session or node
1270
+ // Single backward pass: find last task_dispatched AND count consecutive recent failures.
1271
+ const now = Date.now();
1272
+ const recentWindow = now - RECENT_FAILURE_WINDOW_MS;
491
1273
  let lastDispatch: MeshLedgerEntry | null = null;
1274
+ let consecutiveNodeFailures = 0;
1275
+ let failureCountDone = false;
492
1276
  for (let i = entries.length - 1; i >= 0; i--) {
493
1277
  const e = entries[i];
494
- if (e.kind !== 'task_dispatched') continue;
495
- if (opts.sessionId && e.sessionId === opts.sessionId) { lastDispatch = e; break; }
496
- if (opts.nodeId && e.nodeId === opts.nodeId) { lastDispatch = e; break; }
1278
+ const ts = new Date(e.timestamp).getTime();
1279
+
1280
+ // Failure counting: scan until we exit the recent window or hit a chain-breaker
1281
+ if (!failureCountDone) {
1282
+ if (ts < recentWindow) {
1283
+ failureCountDone = true;
1284
+ } else if (opts.nodeId && !daemonIdsEquivalent(e.nodeId, opts.nodeId)) {
1285
+ // Entry for a different node — skip for failure counting but continue scanning for dispatch
1286
+ } else if (e.kind === 'task_failed') {
1287
+ if (!isIntentionalCleanupStopEntry(e)) consecutiveNodeFailures++;
1288
+ } else if (e.kind === 'task_completed' || e.kind === 'task_dispatched') {
1289
+ // A completion or new dispatch breaks the consecutive failure chain
1290
+ failureCountDone = true;
1291
+ }
1292
+ }
1293
+
1294
+ // Dispatch search: find the last dispatch matching this session or node
1295
+ if (lastDispatch === null && e.kind === 'task_dispatched') {
1296
+ if (opts.sessionId && sessionIdsEquivalent(e.sessionId, opts.sessionId)) { lastDispatch = e; }
1297
+ else if (!opts.sessionId && opts.nodeId && daemonIdsEquivalent(e.nodeId, opts.nodeId)) { lastDispatch = e; }
1298
+ }
1299
+
1300
+ // Stop once both tasks are done
1301
+ if (lastDispatch !== null && failureCountDone) break;
497
1302
  }
498
1303
 
499
1304
  const lastTaskMessage = typeof lastDispatch?.payload?.message === 'string'
500
1305
  ? lastDispatch.payload.message
501
1306
  : null;
502
1307
 
503
- // Count consecutive recent failures for this node (within 30 min window)
504
- const now = Date.now();
505
- const recentWindow = now - RECENT_FAILURE_WINDOW_MS;
506
- let consecutiveNodeFailures = 0;
507
- for (let i = entries.length - 1; i >= 0; i--) {
508
- const e = entries[i];
509
- if (new Date(e.timestamp).getTime() < recentWindow) break;
510
- if (opts.nodeId && e.nodeId !== opts.nodeId) continue;
511
- if (e.kind === 'task_failed') {
512
- if (isIntentionalCleanupStopEntry(e)) continue;
513
- consecutiveNodeFailures++;
514
- } else if (e.kind === 'task_completed' || e.kind === 'task_dispatched') {
515
- // A completion or new dispatch breaks the consecutive failure chain
516
- break;
517
- }
518
- }
519
-
520
- // Count how many times the same task was attempted (match by message prefix)
1308
+ // Count how many times the same task was attempted.
1309
+ // Prefer exact taskId match (payload.taskId) to avoid 200-char prefix collisions.
521
1310
  let taskAttemptCount = 0;
522
- if (lastTaskMessage) {
523
- const prefix = lastTaskMessage.slice(0, 200);
524
- for (const e of entries) {
525
- if (e.kind === 'task_dispatched' && typeof e.payload?.message === 'string') {
526
- if (e.payload.message.startsWith(prefix)) {
527
- taskAttemptCount++;
1311
+ if (lastDispatch) {
1312
+ const taskId = typeof lastDispatch.payload?.taskId === 'string' ? lastDispatch.payload.taskId : null;
1313
+ if (taskId) {
1314
+ for (const e of entries) {
1315
+ if (e.kind === 'task_dispatched' && e.payload?.taskId === taskId) taskAttemptCount++;
1316
+ }
1317
+ } else if (lastTaskMessage) {
1318
+ const prefix = lastTaskMessage.slice(0, 200);
1319
+ for (const e of entries) {
1320
+ if (e.kind === 'task_dispatched' && typeof e.payload?.message === 'string') {
1321
+ if (e.payload.message.startsWith(prefix)) taskAttemptCount++;
528
1322
  }
529
1323
  }
530
1324
  }
@@ -574,7 +1368,8 @@ function rotateLedgerFile(meshId: string, currentPath: string): void {
574
1368
 
575
1369
  try {
576
1370
  renameSync(currentPath, getRotatedPath(meshId, index));
577
- } catch {
1371
+ } catch (e: any) {
578
1372
  // Rotation failed — the next append will just grow the file
1373
+ process.stderr.write(`[adhdev-mesh] Ledger rotation failed for mesh ${meshId}: ${e?.message || e}. File will continue to grow.\n`);
579
1374
  }
580
1375
  }