@adhdev/daemon-core 0.9.82-rc.35 → 0.9.82-rc.351

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 (318) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +8 -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 +46 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +116 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +53 -0
  14. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  15. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  16. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  17. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  18. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  19. package/dist/commands/cli-manager.d.ts +2 -1
  20. package/dist/commands/handler.d.ts +110 -0
  21. package/dist/commands/mesh-coordinator.d.ts +85 -1
  22. package/dist/commands/router.d.ts +324 -1
  23. package/dist/commands/upgrade-helper.d.ts +12 -0
  24. package/dist/config/chat-history.d.ts +9 -0
  25. package/dist/config/config.d.ts +5 -0
  26. package/dist/config/mesh-config.d.ts +77 -1
  27. package/dist/detection/ide-detector.d.ts +13 -0
  28. package/dist/detection/win32-ide-version.d.ts +37 -0
  29. package/dist/git/change-impact-config.d.ts +159 -0
  30. package/dist/git/git-commands.d.ts +11 -1
  31. package/dist/git/git-diff.d.ts +6 -0
  32. package/dist/git/git-executor.d.ts +11 -0
  33. package/dist/git/git-status.d.ts +23 -0
  34. package/dist/git/git-types.d.ts +2 -50
  35. package/dist/git/index.d.ts +2 -0
  36. package/dist/index.d.ts +53 -13
  37. package/dist/index.js +39817 -14303
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +41436 -16047
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/installer.d.ts +1 -4
  42. package/dist/ipc/local-ipc-server.d.ts +91 -0
  43. package/dist/launch.d.ts +1 -1
  44. package/dist/logging/async-batch-writer.d.ts +10 -0
  45. package/dist/logging/log-redactor.d.ts +24 -0
  46. package/dist/logging/log-tail-reader.d.ts +46 -0
  47. package/dist/mesh/contracts.d.ts +164 -0
  48. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  49. package/dist/mesh/coordinator-registry.d.ts +59 -0
  50. package/dist/mesh/mesh-active-work.d.ts +174 -0
  51. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  52. package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
  53. package/dist/mesh/mesh-events-pending.d.ts +55 -0
  54. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  55. package/dist/mesh/mesh-events-utils.d.ts +62 -0
  56. package/dist/mesh/mesh-events.d.ts +6 -49
  57. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  58. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  59. package/dist/mesh/mesh-init.d.ts +86 -0
  60. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  61. package/dist/mesh/mesh-ledger.d.ts +77 -1
  62. package/dist/mesh/mesh-missions.d.ts +164 -0
  63. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  64. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  65. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  66. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  67. package/dist/mesh/mesh-routing.d.ts +70 -0
  68. package/dist/mesh/mesh-runtime-store.d.ts +420 -0
  69. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  70. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  71. package/dist/mesh/mesh-work-queue.d.ts +218 -5
  72. package/dist/mesh/preview-freshness.d.ts +18 -0
  73. package/dist/mesh/refine-config.d.ts +216 -0
  74. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  75. package/dist/providers/acp-provider-instance.d.ts +2 -0
  76. package/dist/providers/approval-utils.d.ts +13 -0
  77. package/dist/providers/chat-message-normalization.d.ts +12 -0
  78. package/dist/providers/cli-provider-instance.d.ts +77 -3
  79. package/dist/providers/contracts.d.ts +139 -6
  80. package/dist/providers/external-sources.d.ts +71 -0
  81. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  82. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  83. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  84. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  85. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  86. package/dist/providers/native-history/index.d.ts +13 -0
  87. package/dist/providers/provider-instance-manager.d.ts +14 -0
  88. package/dist/providers/provider-instance.d.ts +15 -1
  89. package/dist/providers/provider-loader.d.ts +26 -4
  90. package/dist/providers/provider-trust.d.ts +31 -0
  91. package/dist/providers/read-chat-contract.d.ts +29 -0
  92. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  93. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  94. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  95. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  96. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  97. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  98. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  99. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  100. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  101. package/dist/providers/sdk/v1/index.d.ts +30 -0
  102. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  103. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  104. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  105. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  106. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  107. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  108. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  109. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  110. package/dist/providers/spec/adapter.d.ts +69 -0
  111. package/dist/providers/spec/cli-adapter.d.ts +217 -0
  112. package/dist/providers/spec/evaluator.d.ts +23 -0
  113. package/dist/providers/spec/fsm-driver.d.ts +322 -0
  114. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  115. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  116. package/dist/providers/spec/fsm-types.d.ts +144 -0
  117. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  118. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  119. package/dist/providers/spec/route.d.ts +4 -0
  120. package/dist/providers/spec/types.d.ts +180 -0
  121. package/dist/providers/status-monitor.d.ts +7 -7
  122. package/dist/providers/transcript-v2.d.ts +176 -0
  123. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  124. package/dist/providers/working-dir.d.ts +17 -0
  125. package/dist/repo-mesh-types.d.ts +295 -13
  126. package/dist/sessions/registry.d.ts +6 -0
  127. package/dist/shared-types-extra.d.ts +2 -4
  128. package/dist/shared-types.d.ts +59 -2
  129. package/dist/status/normalize.d.ts +1 -1
  130. package/dist/status/normalize.js +1 -0
  131. package/dist/status/normalize.js.map +1 -1
  132. package/dist/status/normalize.mjs +1 -0
  133. package/dist/status/normalize.mjs.map +1 -1
  134. package/dist/status/reporter.d.ts +2 -0
  135. package/dist/status/snapshot.d.ts +26 -0
  136. package/dist/system/load-better-sqlite3.d.ts +21 -0
  137. package/dist/types.d.ts +5 -0
  138. package/package.json +7 -3
  139. package/src/agent-stream/poller.ts +2 -3
  140. package/src/agent-stream/provider-adapter.ts +1 -1
  141. package/src/boot/daemon-lifecycle.ts +55 -12
  142. package/src/boot/process-hardening.ts +89 -0
  143. package/src/build-info.ts +73 -0
  144. package/src/chat/source-machine.ts +534 -0
  145. package/src/chat/source-resolver.ts +0 -0
  146. package/src/chat/subscription-updates.ts +20 -1
  147. package/src/cli-adapter-types.d.ts +1 -0
  148. package/src/cli-adapter-types.ts +47 -2
  149. package/src/cli-adapters/cli-script-runner.ts +421 -0
  150. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  151. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  152. package/src/cli-adapters/provider-cli-adapter.ts +774 -1141
  153. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  154. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  155. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  156. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  157. package/src/cli-adapters/provider-cli-shared.ts +100 -11
  158. package/src/cli-adapters/pty-transport.ts +2 -1
  159. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  160. package/src/cli-adapters/resolve-executable.ts +90 -0
  161. package/src/cli-adapters/session-host-transport.ts +2 -1
  162. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  163. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  164. package/src/cli-adapters/terminal-screen.ts +16 -81
  165. package/src/commands/chat-commands.ts +1823 -62
  166. package/src/commands/cli-manager.ts +304 -14
  167. package/src/commands/handler.ts +809 -2
  168. package/src/commands/mesh-coordinator.ts +332 -122
  169. package/src/commands/router.ts +7505 -1311
  170. package/src/commands/stream-commands.ts +8 -0
  171. package/src/commands/upgrade-helper.ts +81 -30
  172. package/src/config/chat-history.ts +483 -24
  173. package/src/config/config.ts +12 -0
  174. package/src/config/mesh-config.ts +388 -6
  175. package/src/config/recent-activity.ts +8 -2
  176. package/src/daemon/dev-auto-implement.ts +3 -2
  177. package/src/daemon/dev-cli-debug.ts +10 -1
  178. package/src/detection/cli-detector.ts +28 -9
  179. package/src/detection/ide-detector.ts +55 -16
  180. package/src/detection/win32-ide-version.ts +106 -0
  181. package/src/git/change-impact-config.ts +354 -0
  182. package/src/git/git-commands.ts +55 -14
  183. package/src/git/git-diff.ts +53 -0
  184. package/src/git/git-executor.ts +12 -0
  185. package/src/git/git-status.ts +574 -43
  186. package/src/git/git-types.ts +14 -62
  187. package/src/git/git-worktree.ts +8 -1
  188. package/src/git/index.ts +16 -0
  189. package/src/index.ts +151 -12
  190. package/src/installer.d.ts +1 -1
  191. package/src/installer.ts +8 -6
  192. package/src/ipc/local-ipc-server.ts +278 -0
  193. package/src/launch.d.ts +1 -1
  194. package/src/launch.ts +37 -28
  195. package/src/logging/async-batch-writer.ts +55 -0
  196. package/src/logging/log-redactor.ts +100 -0
  197. package/src/logging/log-tail-reader.ts +220 -0
  198. package/src/logging/logger.ts +2 -1
  199. package/src/mesh/contracts.ts +329 -0
  200. package/src/mesh/coordinator-prompt.ts +234 -31
  201. package/src/mesh/coordinator-registry.ts +121 -0
  202. package/src/mesh/mesh-active-work.ts +667 -0
  203. package/src/mesh/mesh-delivery-policy.ts +315 -0
  204. package/src/mesh/mesh-events-coordinator.ts +2303 -0
  205. package/src/mesh/mesh-events-pending.ts +567 -0
  206. package/src/mesh/mesh-events-stale.ts +307 -0
  207. package/src/mesh/mesh-events-utils.ts +381 -0
  208. package/src/mesh/mesh-events.ts +30 -1035
  209. package/src/mesh/mesh-fast-forward.ts +839 -0
  210. package/src/mesh/mesh-host-ownership.ts +73 -0
  211. package/src/mesh/mesh-init.ts +260 -0
  212. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  213. package/src/mesh/mesh-ledger.ts +590 -102
  214. package/src/mesh/mesh-missions.ts +322 -0
  215. package/src/mesh/mesh-reconcile-loop.ts +989 -0
  216. package/src/mesh/mesh-refine-batch.ts +197 -0
  217. package/src/mesh/mesh-refine-status.ts +231 -0
  218. package/src/mesh/mesh-review-inbox.ts +307 -0
  219. package/src/mesh/mesh-routing.ts +291 -0
  220. package/src/mesh/mesh-runtime-store.ts +1664 -0
  221. package/src/mesh/mesh-task-stats.ts +161 -0
  222. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  223. package/src/mesh/mesh-work-queue.ts +901 -141
  224. package/src/mesh/preview-freshness.ts +118 -0
  225. package/src/mesh/refine-config.ts +423 -0
  226. package/src/mesh/worktree-bootstrap-config.ts +331 -0
  227. package/src/providers/acp-provider-instance.ts +25 -9
  228. package/src/providers/approval-utils.d.ts +4 -0
  229. package/src/providers/approval-utils.ts +47 -5
  230. package/src/providers/chat-message-normalization.ts +45 -5
  231. package/src/providers/cli-provider-instance.ts +1041 -99
  232. package/src/providers/contracts.d.ts +55 -0
  233. package/src/providers/contracts.ts +150 -6
  234. package/src/providers/extension-provider-instance.ts +12 -7
  235. package/src/providers/external-sources.ts +218 -0
  236. package/src/providers/ide-provider-instance.ts +35 -12
  237. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  238. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  239. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  240. package/src/providers/native-history/dispatcher.ts +340 -0
  241. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  242. package/src/providers/native-history/index.ts +30 -0
  243. package/src/providers/provider-instance-manager.ts +30 -0
  244. package/src/providers/provider-instance.ts +10 -1
  245. package/src/providers/provider-loader.ts +670 -50
  246. package/src/providers/provider-schema.ts +87 -14
  247. package/src/providers/provider-trust.ts +114 -0
  248. package/src/providers/read-chat-contract.ts +76 -16
  249. package/src/providers/sdk/README.md +49 -0
  250. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  251. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  252. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  253. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  254. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  255. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  256. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  257. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  258. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  259. package/src/providers/sdk/v1/index.ts +152 -0
  260. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  261. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  262. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  263. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  264. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  265. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  266. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  267. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  268. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  269. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  270. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  271. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  272. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  273. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  274. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  275. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  276. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  277. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  278. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  279. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  280. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  281. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  282. package/src/providers/sdk/v1/validators/index.ts +19 -0
  283. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  284. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  285. package/src/providers/spec/adapter.ts +168 -0
  286. package/src/providers/spec/cli-adapter.ts +986 -0
  287. package/src/providers/spec/evaluator.ts +345 -0
  288. package/src/providers/spec/fsm-driver.ts +1065 -0
  289. package/src/providers/spec/fsm-evaluator.ts +255 -0
  290. package/src/providers/spec/fsm-loader.ts +120 -0
  291. package/src/providers/spec/fsm-types.ts +238 -0
  292. package/src/providers/spec/native-history-executor.ts +1136 -0
  293. package/src/providers/spec/pre-launch-trust.ts +104 -0
  294. package/src/providers/spec/route.ts +51 -0
  295. package/src/providers/spec/types.ts +226 -0
  296. package/src/providers/status-monitor.d.ts +7 -7
  297. package/src/providers/status-monitor.ts +37 -22
  298. package/src/providers/transcript-v2.ts +567 -0
  299. package/src/providers/types/interactive-prompt.ts +536 -0
  300. package/src/providers/version-archive.ts +64 -24
  301. package/src/providers/working-dir.ts +23 -0
  302. package/src/repo-mesh-types.ts +378 -14
  303. package/src/sessions/registry.ts +6 -0
  304. package/src/shared-types-extra.ts +2 -4
  305. package/src/shared-types.d.ts +8 -0
  306. package/src/shared-types.ts +64 -1
  307. package/src/status/builders.ts +26 -6
  308. package/src/status/normalize.ts +2 -0
  309. package/src/status/reporter.ts +19 -1
  310. package/src/status/snapshot.ts +95 -26
  311. package/src/system/host-memory.ts +29 -12
  312. package/src/system/load-better-sqlite3.ts +68 -0
  313. package/src/types.ts +5 -0
  314. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  315. package/dist/mesh/mesh-sync.d.ts +0 -53
  316. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  317. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  318. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Daemon log tail reader — read the last N bytes of a daemon log file, newest
3
+ * bytes first, bounded so the result is safe to ship over a mesh P2P channel.
4
+ *
5
+ * Used by the mesh `get_mesh_node_logs` command: the coordinator asks a (possibly
6
+ * remote) daemon for its recent log tail instead of having to open a session and
7
+ * grep the file by hand. Because the mesh RPC envelope is sent as a single
8
+ * datachannel message (~256KB SCTP ceiling, no chunking), the returned tail is
9
+ * HARD-bounded by `tailBytes` (default 64KB, capped at MAX_TAIL_BYTES=128KB) and
10
+ * flags `truncated:true` when the file was larger.
11
+ *
12
+ * Boundary-safe: lines are cut on the newline byte (0x0A) only, which never
13
+ * appears inside a multibyte UTF-8 sequence, so decoding each complete byte
14
+ * segment never splits a multibyte char.
15
+ */
16
+
17
+ import * as fs from 'fs';
18
+ import { getCurrentDaemonLogPath, getDaemonLogDir } from './logger.js';
19
+
20
+ export const DEFAULT_TAIL_BYTES = 64 * 1024;
21
+ export const MAX_TAIL_BYTES = 128 * 1024;
22
+ const READ_CHUNK_BYTES = 64 * 1024;
23
+
24
+ export interface ReadDaemonLogTailArgs {
25
+ /** Date of the log file to read (defaults to today). YYYY-MM-DD string or Date. */
26
+ date?: string | Date;
27
+ /** Max bytes of tail to return. Clamped to (0, MAX_TAIL_BYTES]. Default 64KB. */
28
+ tailBytes?: number;
29
+ /** Optional regex source string; only lines matching (case-insensitive) are kept. */
30
+ grep?: string;
31
+ /** Optional epoch-ms floor; only lines whose leading [HH:MM:SS...] / ISO ts >= this are kept. */
32
+ sinceMs?: number;
33
+ }
34
+
35
+ export interface DaemonLogTailResult {
36
+ success: boolean;
37
+ error?: string;
38
+ lines: string[];
39
+ truncated: boolean;
40
+ logPath: string;
41
+ platform: NodeJS.Platform;
42
+ bytesReturned: number;
43
+ /** True when a grep/since filter dropped lines from the raw tail window. */
44
+ filtered: boolean;
45
+ /** The grep source actually applied (echoed back for clarity). */
46
+ grep?: string;
47
+ }
48
+
49
+ function resolveLogPath(date?: string | Date): string {
50
+ if (date instanceof Date) return getCurrentDaemonLogPath(date);
51
+ if (typeof date === 'string' && date.trim()) {
52
+ const parsed = new Date(`${date.trim()}T00:00:00.000Z`);
53
+ if (!Number.isNaN(parsed.getTime())) return getCurrentDaemonLogPath(parsed);
54
+ }
55
+ return getCurrentDaemonLogPath();
56
+ }
57
+
58
+ function clampTailBytes(tailBytes?: number): number {
59
+ if (!Number.isFinite(tailBytes) || (tailBytes as number) <= 0) return DEFAULT_TAIL_BYTES;
60
+ return Math.min(Math.floor(tailBytes as number), MAX_TAIL_BYTES);
61
+ }
62
+
63
+ /**
64
+ * Read up to `limitBytes` from the end of `filePath`, on a UTF-8 line boundary.
65
+ * Returns the decoded text, whether the read was truncated (file bigger than the
66
+ * window), and the number of bytes actually decoded.
67
+ */
68
+ function readByteBoundedTail(filePath: string, limitBytes: number): { text: string; truncated: boolean; bytesReturned: number } {
69
+ const fd = fs.openSync(filePath, 'r');
70
+ try {
71
+ const stat = fs.fstatSync(fd);
72
+ const size = stat.size;
73
+ if (size === 0) return { text: '', truncated: false, bytesReturned: 0 };
74
+
75
+ const want = Math.min(limitBytes, size);
76
+ let start = size - want;
77
+ const truncated = start > 0;
78
+
79
+ // Collect chunks newest-last into a buffer covering [start, size).
80
+ const buffers: Buffer[] = [];
81
+ let position = start;
82
+ while (position < size) {
83
+ const chunkSize = Math.min(READ_CHUNK_BYTES, size - position);
84
+ const chunk = Buffer.alloc(chunkSize);
85
+ fs.readSync(fd, chunk, 0, chunkSize, position);
86
+ buffers.push(chunk);
87
+ position += chunkSize;
88
+ }
89
+ let buf = Buffer.concat(buffers);
90
+
91
+ // If we truncated mid-line, drop the leading partial line so we never emit
92
+ // a half-decoded line (and never split a multibyte char at the window edge).
93
+ if (truncated) {
94
+ const firstNewline = buf.indexOf(0x0a);
95
+ if (firstNewline >= 0) {
96
+ buf = buf.subarray(firstNewline + 1);
97
+ }
98
+ }
99
+ return { text: buf.toString('utf-8'), truncated, bytesReturned: buf.length };
100
+ } finally {
101
+ fs.closeSync(fd);
102
+ }
103
+ }
104
+
105
+ // Parse a leading timestamp from a log line into epoch ms. The unified logger
106
+ // writes `[HH:MM:SS.mmm]` (local time, today's date) and the startup banner uses
107
+ // a full timestamp; we best-effort parse `[HH:MM:SS...]` against the file's date.
108
+ // Returns null when no timestamp can be extracted (line is then kept by sinceMs).
109
+ function parseLineEpochMs(line: string, fileDate: Date): number | null {
110
+ const m = line.match(/^\[(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?\]/);
111
+ if (!m) {
112
+ // Try an embedded ISO timestamp as a fallback.
113
+ const iso = line.match(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?/);
114
+ if (iso) {
115
+ const t = Date.parse(iso[0].replace(' ', 'T'));
116
+ return Number.isNaN(t) ? null : t;
117
+ }
118
+ return null;
119
+ }
120
+ const d = new Date(fileDate);
121
+ d.setHours(Number(m[1]), Number(m[2]), Number(m[3]), m[4] ? Number(m[4].padEnd(3, '0')) : 0);
122
+ return d.getTime();
123
+ }
124
+
125
+ /**
126
+ * Read the daemon log tail for `date` (default today), bounded to `tailBytes`,
127
+ * with optional grep (regex source) and sinceMs filters. Falls back to the
128
+ * size-rotation backup (`*.1.log`) when the primary file does not exist.
129
+ */
130
+ export function readDaemonLogTail(args: ReadDaemonLogTailArgs = {}): DaemonLogTailResult {
131
+ const platform = process.platform;
132
+ const limitBytes = clampTailBytes(args.tailBytes);
133
+ let logPath = resolveLogPath(args.date);
134
+
135
+ // Fall back to the size-rotation backup if the active file is absent.
136
+ if (!fs.existsSync(logPath)) {
137
+ const backup = logPath.replace(/\.log$/, '.1.log');
138
+ if (fs.existsSync(backup)) {
139
+ logPath = backup;
140
+ } else {
141
+ return {
142
+ success: false,
143
+ error: `No daemon log file at ${logPath} (dir: ${getDaemonLogDir()})`,
144
+ lines: [],
145
+ truncated: false,
146
+ logPath,
147
+ platform,
148
+ bytesReturned: 0,
149
+ filtered: false,
150
+ };
151
+ }
152
+ }
153
+
154
+ let raw: { text: string; truncated: boolean; bytesReturned: number };
155
+ try {
156
+ raw = readByteBoundedTail(logPath, limitBytes);
157
+ } catch (e: any) {
158
+ return {
159
+ success: false,
160
+ error: `Failed to read ${logPath}: ${e?.message ?? String(e)}`,
161
+ lines: [],
162
+ truncated: false,
163
+ logPath,
164
+ platform,
165
+ bytesReturned: 0,
166
+ filtered: false,
167
+ };
168
+ }
169
+
170
+ let lines = raw.text.split('\n');
171
+ // A trailing newline yields a final empty element — drop it.
172
+ if (lines.length && lines[lines.length - 1] === '') lines.pop();
173
+ const rawCount = lines.length;
174
+
175
+ // since filter
176
+ if (Number.isFinite(args.sinceMs)) {
177
+ const fileDate = args.date instanceof Date
178
+ ? args.date
179
+ : typeof args.date === 'string' && args.date.trim()
180
+ ? new Date(`${args.date.trim()}T00:00:00.000Z`)
181
+ : new Date();
182
+ const floor = args.sinceMs as number;
183
+ lines = lines.filter((line) => {
184
+ const ts = parseLineEpochMs(line, fileDate);
185
+ // Keep lines with no parseable timestamp (continuation/stack lines).
186
+ return ts === null || ts >= floor;
187
+ });
188
+ }
189
+
190
+ // grep filter
191
+ let appliedGrep: string | undefined;
192
+ if (typeof args.grep === 'string' && args.grep.trim()) {
193
+ appliedGrep = args.grep.trim();
194
+ let re: RegExp | null = null;
195
+ try {
196
+ re = new RegExp(appliedGrep, 'i');
197
+ } catch {
198
+ re = null;
199
+ }
200
+ if (re) {
201
+ const compiled = re;
202
+ lines = lines.filter((line) => compiled.test(line));
203
+ } else {
204
+ // Invalid regex → fall back to a literal substring match.
205
+ const needle = appliedGrep.toLowerCase();
206
+ lines = lines.filter((line) => line.toLowerCase().includes(needle));
207
+ }
208
+ }
209
+
210
+ return {
211
+ success: true,
212
+ lines,
213
+ truncated: raw.truncated,
214
+ logPath,
215
+ platform,
216
+ bytesReturned: raw.bytesReturned,
217
+ filtered: lines.length !== rawCount,
218
+ ...(appliedGrep ? { grep: appliedGrep } : {}),
219
+ };
220
+ }
@@ -20,6 +20,7 @@
20
20
  import * as fs from 'fs';
21
21
  import * as path from 'path';
22
22
  import * as os from 'os';
23
+ import { AsyncBatchWriter } from './async-batch-writer.js';
23
24
 
24
25
  // ─── Log Level ──────────────────────────────
25
26
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
@@ -123,7 +124,7 @@ function writeToFile(line: string): void {
123
124
  checkDateRotation();
124
125
  rotateSizeIfNeeded();
125
126
  }
126
- fs.appendFileSync(currentLogFile, line + '\n');
127
+ AsyncBatchWriter.write(currentLogFile, line + '\n');
127
128
  } catch { }
128
129
  }
129
130
 
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Mesh contract v2 — first-class identity, scopes, and protocol version.
3
+ *
4
+ * Replaces the implicit conventions that grew across mesh-events.ts,
5
+ * mesh-work-queue.ts, mesh-ledger.ts, and mesh-tools.ts where the same
6
+ * concept (a coordinator, a session, a task status) appeared in different
7
+ * shapes per file. The audit found:
8
+ *
9
+ * - PendingMeshCoordinatorEvent had no targetCoordinatorDaemonId. All
10
+ * events were broadcast to every coordinator that drained them. Two
11
+ * coordinators sharing a mesh would each receive every event,
12
+ * leading to duplicate completion handling and missed targeting.
13
+ * - drainPendingMeshCoordinatorEvents accepted only meshId. The caller's
14
+ * coordinator identity was never available, so per-coordinator
15
+ * routing was impossible by construction.
16
+ * - Session identifier keys diverged across stores: targetSessionId /
17
+ * assignedSessionId / instanceId / runtimeSessionId / providerSessionId.
18
+ * resolveEventSessionId() tried four fallbacks per call.
19
+ * - No protocol version on the JSONL ledger or MeshRuntimeStore. Schema
20
+ * evolutions had no guard rail.
21
+ * - mesh_reconcile_ledger existed as a routine recovery tool, not as
22
+ * an incident-response escape hatch. That itself signals the routing
23
+ * layer cannot be trusted.
24
+ *
25
+ * This module introduces the types; the actual wiring lands in B2 (data
26
+ * model + 3-way transactional store), B3 (MCP layer enforcement), and B4
27
+ * (frontend consumption). B1 is intentionally non-breaking — it adds the
28
+ * types and leaves runtime behaviour unchanged.
29
+ */
30
+
31
+ /** Provider type identifier (e.g. 'claude-cli', 'codex-cli', 'roo-code').
32
+ * Free-form string; no shared enum exists in daemon-core yet. */
33
+ type ProviderType = string;
34
+
35
+ // ─── Protocol version ────────────────────────────────────────────────────
36
+
37
+ export const MESH_PROTOCOL_VERSION_V1 = '1.0' as const;
38
+ export const MESH_PROTOCOL_VERSION_V2 = '2.0' as const;
39
+
40
+ export type MeshProtocolVersion =
41
+ | typeof MESH_PROTOCOL_VERSION_V1
42
+ | typeof MESH_PROTOCOL_VERSION_V2;
43
+
44
+ export const SUPPORTED_MESH_PROTOCOL_VERSIONS: readonly MeshProtocolVersion[] = [
45
+ MESH_PROTOCOL_VERSION_V1,
46
+ MESH_PROTOCOL_VERSION_V2,
47
+ ] as const;
48
+
49
+ export function isSupportedMeshProtocolVersion(value: unknown): value is MeshProtocolVersion {
50
+ return typeof value === 'string'
51
+ && (SUPPORTED_MESH_PROTOCOL_VERSIONS as readonly string[]).includes(value);
52
+ }
53
+
54
+ // ─── Coordinator identity ────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Identity of a mesh coordinator. Required (non-optional) on every dispatch
58
+ * and drain operation in v2 — opaque/missing identity is the audit's
59
+ * smoking gun for routing failures and is structurally banned by the v2
60
+ * types.
61
+ *
62
+ * - daemonId: the machineId of the daemon hosting the coordinator. Stable
63
+ * across coordinator restarts on the same machine.
64
+ * - coordinatorRunId: a UUID generated when the coordinator process starts.
65
+ * Stable for the coordinator's lifetime, fresh on restart. Required so
66
+ * two coordinators on the same daemon (one CLI, one MCP) can be told
67
+ * apart without conflating their drains.
68
+ * - sessionId: optional CLI coordinator session identifier (instanceId).
69
+ * Present when the coordinator is itself a CLI session, absent for
70
+ * pure MCP coordinators.
71
+ */
72
+ export interface CoordinatorIdentity {
73
+ readonly daemonId: string;
74
+ readonly coordinatorRunId: string;
75
+ readonly sessionId?: string;
76
+ }
77
+
78
+ export function coordinatorIdentityEquals(a: CoordinatorIdentity, b: CoordinatorIdentity): boolean {
79
+ return a.daemonId === b.daemonId
80
+ && a.coordinatorRunId === b.coordinatorRunId
81
+ && (a.sessionId ?? '') === (b.sessionId ?? '');
82
+ }
83
+
84
+ export function coordinatorIdentityKey(identity: CoordinatorIdentity): string {
85
+ // Stable string form for map keys and ledger payloads. Avoids the
86
+ // {a:b, c:d} JSON.stringify ordering trap by enforcing field order.
87
+ return `${identity.daemonId}|${identity.coordinatorRunId}|${identity.sessionId ?? ''}`;
88
+ }
89
+
90
+ // ─── Session handle ──────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Unified mesh session identifier. v1 referred to "the session" with five
94
+ * different field names depending on which store you were reading; v2
95
+ * collapses them into one explicit handle that carries every disambiguator
96
+ * a consumer might need.
97
+ *
98
+ * - nodeId: the mesh node this session belongs to (FK into mesh node table).
99
+ * - sessionId: provider-instance identifier (the runtime session id).
100
+ * - providerType: which provider category/type the session runs (e.g.
101
+ * 'claude-cli', 'codex-cli', 'roo-code').
102
+ * - coordinatorDaemonId: which coordinator dispatched the work that
103
+ * spawned this session. Used for completion event routing.
104
+ * - assignedAt: ms epoch when the session was bound to its current task.
105
+ */
106
+ export interface MeshSessionHandle {
107
+ readonly nodeId: string;
108
+ readonly sessionId: string;
109
+ readonly providerType: ProviderType;
110
+ readonly coordinatorDaemonId: string;
111
+ readonly assignedAt: number;
112
+ }
113
+
114
+ export function meshSessionHandleKey(handle: MeshSessionHandle): string {
115
+ return `${handle.nodeId}|${handle.sessionId}`;
116
+ }
117
+
118
+ // ─── Task status (canonical enum) ────────────────────────────────────────
119
+
120
+ /**
121
+ * Single canonical task status enum. v1 had at least three overlapping
122
+ * sets (queue / ledger / direct-dispatch). v2 consumers import from here.
123
+ */
124
+ export const MESH_TASK_STATUSES = [
125
+ 'pending',
126
+ 'assigned',
127
+ 'in_progress',
128
+ 'completed',
129
+ 'failed',
130
+ 'cancelled',
131
+ ] as const;
132
+ export type MeshTaskStatus = typeof MESH_TASK_STATUSES[number];
133
+
134
+ export function isMeshTaskStatus(value: unknown): value is MeshTaskStatus {
135
+ return typeof value === 'string'
136
+ && (MESH_TASK_STATUSES as readonly string[]).includes(value);
137
+ }
138
+
139
+ // ─── Event scope ─────────────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Explicit scope for pending coordinator events. v1 had no scope: every
143
+ * event was broadcast to every drainer. v2 forces producers to declare
144
+ * intent.
145
+ *
146
+ * - 'unicast': delivered to exactly one coordinator (intendedFor). Other
147
+ * coordinators' drains skip it.
148
+ * - 'broadcast': delivered to every coordinator on the mesh. Used for
149
+ * system-wide signals (e.g. mesh-wide policy changes). The v1 default
150
+ * becomes explicit here so audit tools can flag unintended broadcasts.
151
+ * - 'system': delivered to the daemon-level handler, not coordinators.
152
+ * Reserved for infrastructure events that no coordinator should see
153
+ * (e.g. ledger reconciliation outcomes).
154
+ */
155
+ export const MESH_EVENT_SCOPES = ['unicast', 'broadcast', 'system'] as const;
156
+ export type MeshEventScope = typeof MESH_EVENT_SCOPES[number];
157
+
158
+ export function isMeshEventScope(value: unknown): value is MeshEventScope {
159
+ return typeof value === 'string'
160
+ && (MESH_EVENT_SCOPES as readonly string[]).includes(value);
161
+ }
162
+
163
+ // ─── Pending coordinator event v2 ────────────────────────────────────────
164
+
165
+ /**
166
+ * v2 shape of a pending coordinator event. Strict supersets of the v1
167
+ * shape — every v1 field is preserved so existing readers do not break;
168
+ * v2 fields (scope, dispatchedBy, intendedFor, protocolVersion) are
169
+ * additive and consulted by v2-aware drainers only.
170
+ *
171
+ * Mixed v1/v2 events coexist during the rollout window. B2 fully cuts
172
+ * over once every coordinator drain is v2-aware.
173
+ */
174
+ export interface PendingMeshCoordinatorEventV2 {
175
+ // v1 fields (preserved exactly)
176
+ readonly event: string;
177
+ readonly meshId: string;
178
+ readonly nodeLabel: string;
179
+ readonly nodeId?: string;
180
+ readonly workspace?: string;
181
+ readonly metadataEvent: Record<string, unknown>;
182
+ readonly coordinatorMessage?: string;
183
+ readonly queuedAt: number;
184
+
185
+ // v2 additions
186
+ readonly protocolVersion: MeshProtocolVersion;
187
+ readonly scope: MeshEventScope;
188
+ readonly dispatchedBy: CoordinatorIdentity;
189
+ /**
190
+ * Required when scope === 'unicast', forbidden otherwise. Drainers MUST
191
+ * skip unicast events whose intendedFor does not equal their own identity.
192
+ */
193
+ readonly intendedFor?: CoordinatorIdentity;
194
+ }
195
+
196
+ // ─── Ledger entry v2 ─────────────────────────────────────────────────────
197
+
198
+ /**
199
+ * v2 ledger entry additions. The originatingCoordinator field is the
200
+ * source of truth that lets completion events route back to the
201
+ * coordinator that dispatched the task. v1's "worker settings carried it
202
+ * if it happened to be set" approach is replaced.
203
+ */
204
+ export interface MeshLedgerOriginatingCoordinatorV2 {
205
+ readonly originatingCoordinator: CoordinatorIdentity;
206
+ readonly protocolVersion: MeshProtocolVersion;
207
+ }
208
+
209
+ // ─── Validation primitives ───────────────────────────────────────────────
210
+
211
+ export class MeshContractViolationError extends Error {
212
+ readonly violationPath: string;
213
+ readonly protocolVersion: MeshProtocolVersion;
214
+ constructor(
215
+ protocolVersion: MeshProtocolVersion,
216
+ violationPath: string,
217
+ detail: string,
218
+ ) {
219
+ super(`mesh contract ${protocolVersion} violation at ${violationPath}: ${detail}`);
220
+ this.name = 'MeshContractViolationError';
221
+ this.violationPath = violationPath;
222
+ this.protocolVersion = protocolVersion;
223
+ }
224
+ }
225
+
226
+ function isNonEmptyString(value: unknown): value is string {
227
+ return typeof value === 'string' && value.length > 0;
228
+ }
229
+
230
+ export function assertCoordinatorIdentity(raw: unknown, path: string): CoordinatorIdentity {
231
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
232
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, path, 'must be an object');
233
+ }
234
+ const obj = raw as Record<string, unknown>;
235
+ if (!isNonEmptyString(obj.daemonId)) {
236
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.daemonId`, 'must be a non-empty string');
237
+ }
238
+ if (!isNonEmptyString(obj.coordinatorRunId)) {
239
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.coordinatorRunId`, 'must be a non-empty string');
240
+ }
241
+ const sessionId = obj.sessionId;
242
+ if (sessionId !== undefined && !isNonEmptyString(sessionId)) {
243
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.sessionId`, 'must be a non-empty string when provided');
244
+ }
245
+ return sessionId !== undefined
246
+ ? { daemonId: obj.daemonId, coordinatorRunId: obj.coordinatorRunId, sessionId }
247
+ : { daemonId: obj.daemonId, coordinatorRunId: obj.coordinatorRunId };
248
+ }
249
+
250
+ export function assertPendingMeshCoordinatorEventV2(raw: unknown, path = '$'): PendingMeshCoordinatorEventV2 {
251
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
252
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, path, 'must be an object');
253
+ }
254
+ const obj = raw as Record<string, unknown>;
255
+ if (!isSupportedMeshProtocolVersion(obj.protocolVersion)) {
256
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.protocolVersion`, `must be one of ${SUPPORTED_MESH_PROTOCOL_VERSIONS.join(', ')}`);
257
+ }
258
+ if (!isMeshEventScope(obj.scope)) {
259
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.scope`, `must be one of ${MESH_EVENT_SCOPES.join(', ')}`);
260
+ }
261
+ if (!isNonEmptyString(obj.event)) {
262
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.event`, 'must be a non-empty string');
263
+ }
264
+ if (!isNonEmptyString(obj.meshId)) {
265
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.meshId`, 'must be a non-empty string');
266
+ }
267
+ const dispatchedBy = assertCoordinatorIdentity(obj.dispatchedBy, `${path}.dispatchedBy`);
268
+
269
+ if (obj.scope === 'unicast') {
270
+ if (!obj.intendedFor) {
271
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.intendedFor`, 'unicast scope requires intendedFor');
272
+ }
273
+ } else if (obj.intendedFor !== undefined) {
274
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.intendedFor`, 'only unicast scope may set intendedFor');
275
+ }
276
+ const intendedFor = obj.intendedFor
277
+ ? assertCoordinatorIdentity(obj.intendedFor, `${path}.intendedFor`)
278
+ : undefined;
279
+
280
+ const metadata = obj.metadataEvent && typeof obj.metadataEvent === 'object' && !Array.isArray(obj.metadataEvent)
281
+ ? (obj.metadataEvent as Record<string, unknown>)
282
+ : null;
283
+ if (!metadata) {
284
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.metadataEvent`, 'must be an object');
285
+ }
286
+ const queuedAt = typeof obj.queuedAt === 'number' && Number.isFinite(obj.queuedAt) ? obj.queuedAt : null;
287
+ if (queuedAt === null) {
288
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.queuedAt`, 'must be a finite number');
289
+ }
290
+
291
+ return {
292
+ event: obj.event,
293
+ meshId: obj.meshId,
294
+ nodeLabel: isNonEmptyString(obj.nodeLabel) ? obj.nodeLabel : '',
295
+ nodeId: typeof obj.nodeId === 'string' ? obj.nodeId : undefined,
296
+ workspace: typeof obj.workspace === 'string' ? obj.workspace : undefined,
297
+ metadataEvent: metadata,
298
+ coordinatorMessage: typeof obj.coordinatorMessage === 'string' ? obj.coordinatorMessage : undefined,
299
+ queuedAt,
300
+ protocolVersion: obj.protocolVersion,
301
+ scope: obj.scope,
302
+ dispatchedBy,
303
+ ...(intendedFor ? { intendedFor } : {}),
304
+ };
305
+ }
306
+
307
+ // ─── Routing helper ──────────────────────────────────────────────────────
308
+
309
+ /**
310
+ * Decide whether a v2 pending event should be delivered to the given drainer.
311
+ * Centralised so every drain implementation uses the same rule.
312
+ *
313
+ * - 'broadcast': always delivered.
314
+ * - 'system': never delivered to coordinators (system handler only).
315
+ * - 'unicast': delivered iff intendedFor matches drainer identity.
316
+ *
317
+ * v1 events (no scope / no protocolVersion) are treated as broadcast for
318
+ * backward compatibility during rollout; B3 tightens this so v1 events
319
+ * are quarantined to a dedicated drain endpoint instead.
320
+ */
321
+ export function shouldDeliverPendingEventToCoordinator(
322
+ event: PendingMeshCoordinatorEventV2,
323
+ drainer: CoordinatorIdentity,
324
+ ): boolean {
325
+ if (event.scope === 'system') return false;
326
+ if (event.scope === 'broadcast') return true;
327
+ if (!event.intendedFor) return false;
328
+ return coordinatorIdentityEquals(event.intendedFor, drainer);
329
+ }