@adhdev/daemon-core 0.9.82-rc.26 → 0.9.82-rc.261

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 (274) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +4 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/chat/source-machine.d.ts +166 -0
  4. package/dist/chat/source-resolver.d.ts +104 -0
  5. package/dist/chat/subscription-updates.d.ts +1 -0
  6. package/dist/cli-adapter-types.d.ts +23 -2
  7. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  8. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  9. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  10. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  11. package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
  12. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  13. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +3 -3
  14. package/dist/cli-adapters/terminal-backends/types.d.ts +1 -2
  15. package/dist/cli-adapters/terminal-screen.d.ts +2 -11
  16. package/dist/commands/cli-manager.d.ts +2 -1
  17. package/dist/commands/handler.d.ts +110 -0
  18. package/dist/commands/mesh-coordinator.d.ts +85 -1
  19. package/dist/commands/router.d.ts +44 -1
  20. package/dist/config/chat-history.d.ts +9 -0
  21. package/dist/config/config.d.ts +5 -0
  22. package/dist/config/mesh-config.d.ts +72 -1
  23. package/dist/git/git-commands.d.ts +9 -1
  24. package/dist/git/git-diff.d.ts +6 -0
  25. package/dist/git/git-types.d.ts +2 -0
  26. package/dist/index.d.ts +47 -11
  27. package/dist/index.js +31114 -11255
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +31615 -11852
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/installer.d.ts +1 -4
  32. package/dist/ipc/local-ipc-server.d.ts +91 -0
  33. package/dist/launch.d.ts +1 -1
  34. package/dist/logging/async-batch-writer.d.ts +10 -0
  35. package/dist/mesh/contracts.d.ts +164 -0
  36. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  37. package/dist/mesh/coordinator-registry.d.ts +59 -0
  38. package/dist/mesh/mesh-active-work.d.ts +90 -0
  39. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  40. package/dist/mesh/mesh-events-coordinator.d.ts +152 -0
  41. package/dist/mesh/mesh-events-pending.d.ts +40 -0
  42. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  43. package/dist/mesh/mesh-events-utils.d.ts +16 -0
  44. package/dist/mesh/mesh-events.d.ts +5 -49
  45. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  46. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  47. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  48. package/dist/mesh/mesh-ledger.d.ts +77 -1
  49. package/dist/mesh/mesh-missions.d.ts +58 -0
  50. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  51. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  52. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  53. package/dist/mesh/mesh-routing.d.ts +70 -0
  54. package/dist/mesh/mesh-runtime-store.d.ts +327 -0
  55. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  56. package/dist/mesh/mesh-work-queue.d.ts +160 -5
  57. package/dist/mesh/preview-freshness.d.ts +18 -0
  58. package/dist/mesh/refine-config.d.ts +215 -0
  59. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  60. package/dist/providers/acp-provider-instance.d.ts +2 -0
  61. package/dist/providers/approval-utils.d.ts +13 -0
  62. package/dist/providers/cli-provider-instance.d.ts +37 -3
  63. package/dist/providers/contracts.d.ts +130 -6
  64. package/dist/providers/external-sources.d.ts +71 -0
  65. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  66. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  67. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  68. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  69. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  70. package/dist/providers/native-history/index.d.ts +13 -0
  71. package/dist/providers/provider-instance-manager.d.ts +13 -0
  72. package/dist/providers/provider-instance.d.ts +13 -1
  73. package/dist/providers/provider-loader.d.ts +26 -4
  74. package/dist/providers/provider-trust.d.ts +31 -0
  75. package/dist/providers/read-chat-contract.d.ts +29 -0
  76. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  77. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  78. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  79. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  80. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  81. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  82. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  83. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  84. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  85. package/dist/providers/sdk/v1/index.d.ts +30 -0
  86. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  87. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  88. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  89. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  90. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  91. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  92. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  93. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  94. package/dist/providers/spec/adapter.d.ts +52 -0
  95. package/dist/providers/spec/cli-adapter.d.ts +140 -0
  96. package/dist/providers/spec/evaluator.d.ts +23 -0
  97. package/dist/providers/spec/fsm-driver.d.ts +273 -0
  98. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  99. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  100. package/dist/providers/spec/fsm-types.d.ts +91 -0
  101. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  102. package/dist/providers/spec/route.d.ts +4 -0
  103. package/dist/providers/spec/types.d.ts +129 -0
  104. package/dist/providers/transcript-v2.d.ts +176 -0
  105. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  106. package/dist/repo-mesh-types.d.ts +126 -1
  107. package/dist/sessions/registry.d.ts +6 -0
  108. package/dist/shared-types-extra.d.ts +2 -4
  109. package/dist/shared-types.d.ts +42 -1
  110. package/dist/status/normalize.d.ts +1 -1
  111. package/dist/status/normalize.js +1 -0
  112. package/dist/status/normalize.js.map +1 -1
  113. package/dist/status/normalize.mjs +1 -0
  114. package/dist/status/normalize.mjs.map +1 -1
  115. package/dist/status/reporter.d.ts +2 -0
  116. package/dist/status/snapshot.d.ts +1 -0
  117. package/dist/types.d.ts +5 -0
  118. package/package.json +6 -3
  119. package/src/agent-stream/poller.ts +2 -3
  120. package/src/boot/daemon-lifecycle.ts +30 -10
  121. package/src/boot/process-hardening.ts +89 -0
  122. package/src/chat/source-machine.ts +534 -0
  123. package/src/chat/source-resolver.ts +0 -0
  124. package/src/chat/subscription-updates.ts +20 -1
  125. package/src/cli-adapter-types.d.ts +1 -0
  126. package/src/cli-adapter-types.ts +22 -2
  127. package/src/cli-adapters/cli-script-runner.ts +421 -0
  128. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  129. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  130. package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
  131. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  132. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  133. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  134. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  135. package/src/cli-adapters/provider-cli-shared.ts +68 -11
  136. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  137. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +35 -29
  138. package/src/cli-adapters/terminal-backends/types.ts +1 -3
  139. package/src/cli-adapters/terminal-screen.ts +11 -81
  140. package/src/commands/chat-commands.ts +1806 -60
  141. package/src/commands/cli-manager.ts +286 -14
  142. package/src/commands/handler.ts +809 -2
  143. package/src/commands/mesh-coordinator.ts +332 -122
  144. package/src/commands/router.ts +4890 -489
  145. package/src/config/chat-history.ts +95 -24
  146. package/src/config/config.ts +12 -0
  147. package/src/config/mesh-config.ts +303 -3
  148. package/src/config/recent-activity.ts +8 -2
  149. package/src/daemon/dev-auto-implement.ts +3 -2
  150. package/src/daemon/dev-cli-debug.ts +10 -1
  151. package/src/detection/ide-detector.ts +26 -16
  152. package/src/git/git-commands.ts +42 -12
  153. package/src/git/git-diff.ts +53 -0
  154. package/src/git/git-status.ts +35 -6
  155. package/src/git/git-types.ts +2 -0
  156. package/src/git/git-worktree.ts +8 -1
  157. package/src/index.ts +127 -10
  158. package/src/installer.d.ts +1 -1
  159. package/src/installer.ts +8 -6
  160. package/src/ipc/local-ipc-server.ts +278 -0
  161. package/src/launch.d.ts +1 -1
  162. package/src/launch.ts +37 -28
  163. package/src/logging/async-batch-writer.ts +55 -0
  164. package/src/logging/logger.ts +2 -1
  165. package/src/mesh/contracts.ts +329 -0
  166. package/src/mesh/coordinator-prompt.ts +219 -31
  167. package/src/mesh/coordinator-registry.ts +121 -0
  168. package/src/mesh/mesh-active-work.ts +437 -0
  169. package/src/mesh/mesh-delivery-policy.ts +315 -0
  170. package/src/mesh/mesh-events-coordinator.ts +1625 -0
  171. package/src/mesh/mesh-events-pending.ts +439 -0
  172. package/src/mesh/mesh-events-stale.ts +283 -0
  173. package/src/mesh/mesh-events-utils.ts +195 -0
  174. package/src/mesh/mesh-events.ts +26 -1035
  175. package/src/mesh/mesh-fast-forward.ts +438 -0
  176. package/src/mesh/mesh-host-ownership.ts +73 -0
  177. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  178. package/src/mesh/mesh-ledger.ts +588 -102
  179. package/src/mesh/mesh-missions.ts +151 -0
  180. package/src/mesh/mesh-refine-batch.ts +197 -0
  181. package/src/mesh/mesh-refine-status.ts +231 -0
  182. package/src/mesh/mesh-review-inbox.ts +307 -0
  183. package/src/mesh/mesh-routing.ts +272 -0
  184. package/src/mesh/mesh-runtime-store.ts +1342 -0
  185. package/src/mesh/mesh-task-stats.ts +154 -0
  186. package/src/mesh/mesh-work-queue.ts +573 -145
  187. package/src/mesh/preview-freshness.ts +118 -0
  188. package/src/mesh/refine-config.ts +403 -0
  189. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  190. package/src/providers/acp-provider-instance.ts +15 -1
  191. package/src/providers/approval-utils.d.ts +4 -0
  192. package/src/providers/approval-utils.ts +47 -5
  193. package/src/providers/chat-message-normalization.ts +4 -11
  194. package/src/providers/cli-provider-instance.ts +806 -72
  195. package/src/providers/contracts.d.ts +55 -0
  196. package/src/providers/contracts.ts +141 -6
  197. package/src/providers/external-sources.ts +218 -0
  198. package/src/providers/ide-provider-instance.ts +19 -5
  199. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  200. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  201. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  202. package/src/providers/native-history/dispatcher.ts +340 -0
  203. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  204. package/src/providers/native-history/index.ts +30 -0
  205. package/src/providers/provider-instance-manager.ts +30 -0
  206. package/src/providers/provider-instance.ts +10 -1
  207. package/src/providers/provider-loader.ts +584 -50
  208. package/src/providers/provider-schema.ts +87 -14
  209. package/src/providers/provider-trust.ts +114 -0
  210. package/src/providers/read-chat-contract.ts +76 -16
  211. package/src/providers/sdk/README.md +49 -0
  212. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  213. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  214. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  215. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  216. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  217. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  218. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  219. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  220. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  221. package/src/providers/sdk/v1/index.ts +152 -0
  222. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  223. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  224. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  225. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  226. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  227. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  228. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  229. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  230. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  231. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  232. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  233. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  234. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  235. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  236. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  237. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  238. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  239. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  240. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  241. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  242. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  243. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  244. package/src/providers/sdk/v1/validators/index.ts +19 -0
  245. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  246. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  247. package/src/providers/spec/adapter.ts +145 -0
  248. package/src/providers/spec/cli-adapter.ts +730 -0
  249. package/src/providers/spec/evaluator.ts +321 -0
  250. package/src/providers/spec/fsm-driver.ts +811 -0
  251. package/src/providers/spec/fsm-evaluator.ts +255 -0
  252. package/src/providers/spec/fsm-loader.ts +102 -0
  253. package/src/providers/spec/fsm-types.ts +184 -0
  254. package/src/providers/spec/native-history-executor.ts +943 -0
  255. package/src/providers/spec/route.ts +51 -0
  256. package/src/providers/spec/types.ts +173 -0
  257. package/src/providers/transcript-v2.ts +567 -0
  258. package/src/providers/types/interactive-prompt.ts +425 -0
  259. package/src/providers/version-archive.ts +38 -20
  260. package/src/repo-mesh-types.ts +138 -1
  261. package/src/sessions/registry.ts +6 -0
  262. package/src/shared-types-extra.ts +2 -4
  263. package/src/shared-types.ts +45 -1
  264. package/src/status/builders.ts +26 -6
  265. package/src/status/normalize.ts +2 -0
  266. package/src/status/reporter.ts +15 -0
  267. package/src/status/snapshot.ts +84 -25
  268. package/src/system/host-memory.ts +29 -12
  269. package/src/types.ts +5 -0
  270. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  271. package/dist/mesh/mesh-sync.d.ts +0 -53
  272. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  273. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  274. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,439 @@
1
+ import { appendFileSync, existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { randomUUID } from 'crypto';
4
+ import { LOG } from '../logging/logger.js';
5
+ import { getLedgerDir, readLedgerEntries } from './mesh-ledger.js';
6
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
7
+ import { buildMeshSystemMessage, canonicalDaemonId, readNonEmptyString, readRecord, resolveEventSessionId } from './mesh-events-utils.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
11
+ // ---------------------------------------------------------------------------
12
+ // When a mesh event fires but no CLI coordinator session is registered (e.g.
13
+ // the coordinator is Claude Code running via MCP), we persist the event to a
14
+ // per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
15
+ // is removed; the file is drained atomically on each get_pending_mesh_events
16
+ // call and limited to 100 KB to prevent runaway growth.
17
+ //
18
+ // File: <ledgerDir>/<meshId>.pending-events.jsonl
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface PendingMeshCoordinatorEvent {
22
+ event: string;
23
+ meshId: string;
24
+ nodeLabel: string;
25
+ nodeId?: string;
26
+ workspace?: string;
27
+ metadataEvent: Record<string, unknown>;
28
+ coordinatorMessage?: string;
29
+ queuedAt: number;
30
+ /**
31
+ * When set, this event is intended for a specific coordinator daemon.
32
+ * Coordinators on other daemons should ignore it during drain.
33
+ * Absent on legacy events — treated as broadcast to any coordinator.
34
+ */
35
+ targetCoordinatorDaemonId?: string;
36
+ }
37
+
38
+ const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
39
+
40
+ export function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
41
+ const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
42
+ const result = readRecord(metadata.result);
43
+ const refineJob = readRecord(result?.refineJob);
44
+ return readNonEmptyString(metadata.jobId) || readNonEmptyString(refineJob?.jobId);
45
+ }
46
+
47
+ function buildRefineTerminalEventFingerprint(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): string {
48
+ const jobId = readRefineJobId({ metadataEvent });
49
+ return jobId && REFINE_TERMINAL_EVENTS.has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
50
+ }
51
+
52
+ function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
53
+ if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
54
+ const jobId = readRefineJobId(event);
55
+ if (!jobId) return false;
56
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) =>
57
+ pending.event === event.event && readRefineJobId(pending) === jobId,
58
+ );
59
+ }
60
+
61
+ export function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
62
+ const metadata = readRecord(event.metadataEvent) || {};
63
+ // Bootstrap events are node-scoped: dedup by meshId+event+nodeId only.
64
+ // They carry no sessionId/taskId/timestamp — using those fields would produce
65
+ // an empty fingerprint that defeats dedup entirely.
66
+ if (event.event === 'worktree_bootstrap_complete' || event.event === 'worktree_bootstrap_failed') {
67
+ return [event.meshId, event.event, event.nodeId || ''].join('::');
68
+ }
69
+ const sessionId = resolveEventSessionId(metadata);
70
+ const providerSessionId = readNonEmptyString(metadata.providerSessionId);
71
+ const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
72
+ const jobId = readRefineJobId(event);
73
+ const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
74
+ return [
75
+ event.meshId,
76
+ event.event,
77
+ event.nodeId || '',
78
+ sessionId || '',
79
+ providerSessionId || '',
80
+ taskId || '',
81
+ jobId || '',
82
+ timestamp || '',
83
+ ].join('::');
84
+ }
85
+
86
+ // R3: TTL for the direct-delivered marker. A coordinator polls get_pending_mesh_events
87
+ // well within this window after a terminal event; after it expires the marker is swept and
88
+ // a late/duplicate drain would (harmlessly) re-surface — but by then the event is long gone
89
+ // from the queue too. 10 minutes mirrors the completion-fingerprint TTL.
90
+ const DIRECT_DELIVERED_TTL_MS = 10 * 60 * 1000;
91
+
92
+ /**
93
+ * R3: record that an event was direct-injected into a live coordinator on `coordinatorDaemonId`.
94
+ * That coordinator's own drain (get_pending_mesh_events with the same coordinatorDaemonId) will
95
+ * skip the queued copy, so it receives the event exactly once instead of twice (PTY + poll).
96
+ * Other consumers (unscoped drainers, other daemons) are unaffected — they did not get the inject.
97
+ */
98
+ export function markMeshCoordinatorEventDirectDelivered(
99
+ coordinatorDaemonId: string,
100
+ event: PendingMeshCoordinatorEvent,
101
+ ): void {
102
+ // Canonicalize so the raw machineId used at inject time and the prefixed instanceId the
103
+ // coordinator drains with resolve to the same key.
104
+ const canonical = canonicalDaemonId(coordinatorDaemonId);
105
+ if (!canonical) return;
106
+ const fingerprint = buildPendingEventFingerprint(event);
107
+ if (!fingerprint.trim()) return;
108
+ try {
109
+ const store = MeshRuntimeStore.getInstance();
110
+ store.recordDirectDelivered(canonical, fingerprint, DIRECT_DELIVERED_TTL_MS);
111
+ store.sweepExpiredDirectDelivered();
112
+ } catch { /* best-effort — a duplicate is preferable to a crash */ }
113
+ }
114
+
115
+ function wasDirectDeliveredToCoordinator(coordinatorDaemonId: string, event: PendingMeshCoordinatorEvent): boolean {
116
+ const canonical = canonicalDaemonId(coordinatorDaemonId);
117
+ if (!canonical) return false;
118
+ const fingerprint = buildPendingEventFingerprint(event);
119
+ if (!fingerprint.trim()) return false;
120
+ try {
121
+ return MeshRuntimeStore.getInstance().wasDirectDelivered(canonical, fingerprint);
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ export function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
128
+ const fingerprint = buildPendingEventFingerprint(event);
129
+ if (!fingerprint.trim()) return false;
130
+ // Check SQLite inbox first (G3 primary path)
131
+ try {
132
+ if (MeshRuntimeStore.getInstance().hasPendingEventFingerprint(event.meshId, fingerprint)) return true;
133
+ } catch { /* fall through to JSONL check */ }
134
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
135
+ }
136
+
137
+ function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): string {
138
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
139
+ if (coordinatorDaemonId) {
140
+ const safeDaemon = coordinatorDaemonId.replace(/[^a-zA-Z0-9_-]/g, '_');
141
+ return join(getLedgerDir(), `${safe}-${safeDaemon}.pending-events.jsonl`);
142
+ }
143
+ return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
144
+ }
145
+
146
+ function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
147
+ if (!meshId) return [];
148
+ // Read coordinator-scoped file first; fall back to legacy shared file.
149
+ const paths = coordinatorDaemonId
150
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
151
+ : [getPendingEventsPath(meshId)];
152
+ const events: PendingMeshCoordinatorEvent[] = [];
153
+ for (const path of paths) {
154
+ if (!existsSync(path)) continue;
155
+ try {
156
+ const raw = readFileSync(path, 'utf-8');
157
+ const parsed = raw.split('\n').filter(Boolean).flatMap(line => {
158
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
159
+ });
160
+ // If reading the shared file, filter to events that target this coordinator or are unscoped.
161
+ const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
162
+ ? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
163
+ : parsed;
164
+ events.push(...filtered);
165
+ } catch { /* skip unreadable files */ }
166
+ }
167
+ return events;
168
+ }
169
+
170
+ function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
171
+ const acceptedJobIds = new Set(
172
+ pending
173
+ .filter(event => event.event === 'refine:accepted')
174
+ .map(event => readRefineJobId(event))
175
+ .filter(Boolean),
176
+ );
177
+ if (acceptedJobIds.size === 0) return [];
178
+ const existingTerminalJobIds = new Set(
179
+ pending
180
+ .filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
181
+ .map(event => `${event.event}:${readRefineJobId(event)}`)
182
+ .filter(value => !value.endsWith(':')),
183
+ );
184
+ const backfilled: PendingMeshCoordinatorEvent[] = [];
185
+ const entries = readLedgerEntries(meshId, { tail: 200 });
186
+ for (let i = entries.length - 1; i >= 0; i--) {
187
+ const entry = entries[i];
188
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
189
+ const payload = readRecord(entry.payload);
190
+ if (payload?.source !== 'refine_mesh_node_async_job') continue;
191
+ const refineJob = readRecord(payload.refineJob);
192
+ const jobId = readNonEmptyString(refineJob?.jobId);
193
+ if (!jobId || !acceptedJobIds.has(jobId)) continue;
194
+ const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
195
+ if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
196
+ existingTerminalJobIds.add(`${eventName}:${jobId}`);
197
+ const result = readRecord(payload.result);
198
+ const metadataEvent = {
199
+ source: 'refine_mesh_node_async_job',
200
+ jobId,
201
+ interactionId: readNonEmptyString(refineJob?.interactionId),
202
+ meshId,
203
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
204
+ targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
205
+ workspace: readNonEmptyString(refineJob?.workspace),
206
+ status: eventName === 'refine:completed' ? 'completed' : 'failed',
207
+ startedAt: readNonEmptyString(refineJob?.startedAt),
208
+ completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
209
+ retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
210
+ ...(result ? { result } : {}),
211
+ };
212
+ const nodeLabel = readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job';
213
+ backfilled.push({
214
+ event: eventName,
215
+ meshId,
216
+ nodeLabel,
217
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
218
+ workspace: readNonEmptyString(refineJob?.workspace),
219
+ metadataEvent,
220
+ coordinatorMessage: buildMeshSystemMessage({ event: eventName, nodeLabel, metadataEvent }),
221
+ queuedAt: Date.now(),
222
+ });
223
+ }
224
+ return backfilled.reverse();
225
+ }
226
+
227
+ function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
228
+ const backfilled = refineTerminalEventFromLedger(meshId, events);
229
+ // A refine:accepted event is a provisional "job accepted, result to follow" signal.
230
+ // Once its terminal (completed/failed) counterpart for the same jobId exists — whether
231
+ // already direct-queued into the pending store OR backfilled from the ledger here — the
232
+ // accepted is superseded and is dropped so the coordinator isn't shown stale duplicate
233
+ // noise alongside the terminal outcome.
234
+ const terminalJobIds = new Set(
235
+ [...events.filter(event => REFINE_TERMINAL_EVENTS.has(event.event)), ...backfilled]
236
+ .map(event => readRefineJobId(event))
237
+ .filter(Boolean),
238
+ );
239
+ const reconciled = terminalJobIds.size === 0
240
+ ? events
241
+ : events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event))));
242
+ return backfilled.length === 0 ? reconciled : [...reconciled, ...backfilled];
243
+ }
244
+
245
+ const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
246
+ const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
247
+
248
+ function trimPendingEventsIfNeeded(path: string): void {
249
+ try {
250
+ if (!existsSync(path)) return;
251
+ if (statSync(path).size <= MAX_PENDING_EVENTS_BYTES) return;
252
+ const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
253
+ if (lines.length <= MAX_PENDING_EVENTS_KEEP) return;
254
+ writeFileSync(path, lines.slice(-MAX_PENDING_EVENTS_KEEP).join('\n') + '\n', 'utf-8');
255
+ } catch { /* best-effort; if trim fails, append still proceeds */ }
256
+ }
257
+
258
+ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
259
+ try {
260
+ if (hasPendingRefineTerminalEventDuplicate(event)) {
261
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
262
+ return true;
263
+ }
264
+ if (hasPendingCoordinatorEventDuplicate(event)) {
265
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
266
+ return true;
267
+ }
268
+
269
+ const fingerprint = buildPendingEventFingerprint(event);
270
+
271
+ // G3: Write to SQLite inbox (primary path going forward)
272
+ try {
273
+ MeshRuntimeStore.getInstance().insertPendingEvent({
274
+ id: randomUUID(),
275
+ meshId: event.meshId,
276
+ coordinatorDaemonId: event.targetCoordinatorDaemonId ?? null,
277
+ event: event.event,
278
+ payload: event,
279
+ fingerprint: fingerprint || null,
280
+ queuedAt: event.queuedAt,
281
+ });
282
+ } catch {
283
+ // SQLite write failure is non-fatal; JSONL fallback below still works.
284
+ }
285
+
286
+ // Also write to JSONL (retained as legacy/export artifact)
287
+ const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
288
+ trimPendingEventsIfNeeded(path);
289
+ appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
290
+ return true;
291
+ } catch (e: any) {
292
+ LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
293
+ return false;
294
+ }
295
+ }
296
+
297
+ // Atomically rename the file before reading so concurrent drains can't both consume
298
+ // the same events. renameSync is atomic on POSIX (same filesystem); only one caller
299
+ // wins the rename — the other gets ENOENT and returns null, preventing duplicate delivery.
300
+ function atomicDrainFile(path: string): string | null {
301
+ const tmpPath = `${path}.draining`;
302
+ try {
303
+ renameSync(path, tmpPath);
304
+ } catch {
305
+ return null; // another drain already renamed it, or file doesn't exist
306
+ }
307
+ try {
308
+ const content = readFileSync(tmpPath, 'utf-8');
309
+ try { unlinkSync(tmpPath); } catch { /* already cleaned up */ }
310
+ return content;
311
+ } catch {
312
+ try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
313
+ return null;
314
+ }
315
+ }
316
+
317
+ /** Drain and return all pending coordinator events for meshId, removing them from disk. */
318
+ export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
319
+ if (!meshId) return [];
320
+
321
+ // Dual-write means SQLite and JSONL hold the same events. Both stores must be
322
+ // emptied in one drain call — draining only one leaves the other to re-deliver
323
+ // the same events on the next call. Merge with fingerprint dedup.
324
+ const merged: PendingMeshCoordinatorEvent[] = [];
325
+ const seenFingerprints = new Set<string>();
326
+ const pushUnique = (event: PendingMeshCoordinatorEvent) => {
327
+ const fingerprint = buildPendingEventFingerprint(event);
328
+ if (fingerprint.trim()) {
329
+ if (seenFingerprints.has(fingerprint)) return;
330
+ seenFingerprints.add(fingerprint);
331
+ }
332
+ merged.push(event);
333
+ };
334
+
335
+ // G3: SQLite inbox
336
+ try {
337
+ const store = MeshRuntimeStore.getInstance();
338
+ if (store.pendingEventCount(meshId) > 0) {
339
+ for (const row of store.drainPendingEvents(meshId, coordinatorDaemonId)) {
340
+ const event = row.payload as PendingMeshCoordinatorEvent;
341
+ if (event) pushUnique(event);
342
+ }
343
+ }
344
+ } catch {
345
+ // SQLite drain failed — JSONL below still drains
346
+ }
347
+
348
+ // JSONL (legacy / migration path) — always drained alongside SQLite
349
+ const paths = coordinatorDaemonId
350
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
351
+ : [getPendingEventsPath(meshId)];
352
+ for (const path of paths) {
353
+ const content = atomicDrainFile(path);
354
+ if (!content) continue;
355
+ const parsed = content.split('\n').filter(Boolean).flatMap(line => {
356
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
357
+ });
358
+ // If reading the shared file, filter to events that target this coordinator or are unscoped.
359
+ const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
360
+ ? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
361
+ : parsed;
362
+ for (const event of filtered) pushUnique(event);
363
+ }
364
+ if (merged.length === 0) return [];
365
+ // R3: when this drain is scoped to a coordinator daemon, exclude events that were already
366
+ // direct-injected into that coordinator's live CLI session. Unscoped drains (no daemon id)
367
+ // keep everything — they belong to consumers that never received the direct inject.
368
+ const deliverable = coordinatorDaemonId
369
+ ? merged.filter(event => !wasDirectDeliveredToCoordinator(coordinatorDaemonId, event))
370
+ : merged;
371
+ if (deliverable.length === 0) return [];
372
+ return reconcilePendingMeshCoordinatorEvents(meshId, deliverable);
373
+ }
374
+
375
+ /** Peek at pending coordinator events without draining (non-destructive). */
376
+ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
377
+ if (!meshId) return [];
378
+
379
+ // Merge SQLite (primary) + JSONL (legacy) with fingerprint dedup.
380
+ const merged: PendingMeshCoordinatorEvent[] = [];
381
+ const seenFingerprints = new Set<string>();
382
+ const pushUnique = (event: PendingMeshCoordinatorEvent) => {
383
+ const fingerprint = buildPendingEventFingerprint(event);
384
+ if (fingerprint.trim()) {
385
+ if (seenFingerprints.has(fingerprint)) return;
386
+ seenFingerprints.add(fingerprint);
387
+ }
388
+ merged.push(event);
389
+ };
390
+
391
+ // G3: SQLite inbox (non-destructive peek at undrained rows)
392
+ try {
393
+ const store = MeshRuntimeStore.getInstance();
394
+ if (store.pendingEventCount(meshId) > 0) {
395
+ for (const row of store.peekPendingEvents(meshId, coordinatorDaemonId)) {
396
+ const event = row.payload as PendingMeshCoordinatorEvent;
397
+ if (event) pushUnique(event);
398
+ }
399
+ }
400
+ } catch { /* SQLite unavailable — JSONL fallback below */ }
401
+
402
+ // JSONL (legacy)
403
+ for (const event of readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId)) {
404
+ pushUnique(event);
405
+ }
406
+
407
+ // R3: hide events already direct-delivered to this coordinator from its status peek, so
408
+ // mesh_status doesn't report a "pending" event the coordinator has in fact already received.
409
+ const deliverable = coordinatorDaemonId
410
+ ? merged.filter(event => !wasDirectDeliveredToCoordinator(coordinatorDaemonId, event))
411
+ : merged;
412
+
413
+ return reconcilePendingMeshCoordinatorEvents(meshId, deliverable);
414
+ }
415
+
416
+ /**
417
+ * Test helper: purge all pending-event state for a mesh — SQLite rows
418
+ * (including drained fingerprint history) and JSONL files.
419
+ */
420
+ export function __clearMeshPendingEventsForTests(meshId: string): void {
421
+ try {
422
+ MeshRuntimeStore.getInstance().clearPendingEventsForMesh(meshId);
423
+ } catch { /* store unavailable — nothing to clear */ }
424
+ clearPendingMeshCoordinatorEvents(meshId);
425
+ }
426
+
427
+ /** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
428
+ export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
429
+ if (!meshId) return;
430
+ // Clear SQLite rows
431
+ try { MeshRuntimeStore.getInstance().clearPendingEventsForMesh(meshId); } catch { /* store unavailable */ }
432
+ // Clear JSONL files
433
+ const paths = coordinatorDaemonId
434
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
435
+ : [getPendingEventsPath(meshId)];
436
+ for (const path of paths) {
437
+ if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
438
+ }
439
+ }