@adhdev/daemon-core 0.9.82-rc.46 → 0.9.82-rc.460

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