@adhdev/daemon-core 0.9.82-rc.21 → 0.9.82-rc.211

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 (269) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -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 +178 -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 +29 -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 +32 -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 +46 -11
  27. package/dist/index.js +35173 -15756
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +34880 -15556
  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 +151 -0
  41. package/dist/mesh/mesh-events-pending.d.ts +33 -0
  42. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  43. package/dist/mesh/mesh-events-utils.d.ts +14 -0
  44. package/dist/mesh/mesh-events.d.ts +5 -40
  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-status.d.ts +26 -0
  51. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  52. package/dist/mesh/mesh-runtime-store.d.ts +324 -0
  53. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  54. package/dist/mesh/mesh-work-queue.d.ts +137 -6
  55. package/dist/mesh/preview-freshness.d.ts +18 -0
  56. package/dist/mesh/refine-config.d.ts +215 -0
  57. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  58. package/dist/providers/acp-provider-instance.d.ts +2 -0
  59. package/dist/providers/approval-utils.d.ts +13 -0
  60. package/dist/providers/cli-provider-instance.d.ts +39 -3
  61. package/dist/providers/contracts.d.ts +130 -6
  62. package/dist/providers/external-sources.d.ts +71 -0
  63. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  64. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  65. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  66. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  67. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  68. package/dist/providers/native-history/index.d.ts +13 -0
  69. package/dist/providers/provider-instance-manager.d.ts +12 -0
  70. package/dist/providers/provider-instance.d.ts +13 -1
  71. package/dist/providers/provider-loader.d.ts +26 -4
  72. package/dist/providers/provider-trust.d.ts +31 -0
  73. package/dist/providers/read-chat-contract.d.ts +29 -0
  74. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  75. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  76. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  77. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  78. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  79. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  80. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  81. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  82. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  83. package/dist/providers/sdk/v1/index.d.ts +30 -0
  84. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  85. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  86. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  87. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  88. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  89. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  90. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  91. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  92. package/dist/providers/spec/adapter.d.ts +52 -0
  93. package/dist/providers/spec/cli-adapter.d.ts +101 -0
  94. package/dist/providers/spec/driver.d.ts +225 -0
  95. package/dist/providers/spec/evaluator.d.ts +54 -0
  96. package/dist/providers/spec/loader.d.ts +15 -0
  97. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  98. package/dist/providers/spec/route.d.ts +4 -0
  99. package/dist/providers/spec/schema.gen.d.ts +2025 -0
  100. package/dist/providers/spec/types.d.ts +302 -0
  101. package/dist/providers/transcript-v2.d.ts +176 -0
  102. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  103. package/dist/repo-mesh-types.d.ts +109 -1
  104. package/dist/sessions/registry.d.ts +3 -0
  105. package/dist/shared-types-extra.d.ts +2 -4
  106. package/dist/shared-types.d.ts +42 -1
  107. package/dist/status/normalize.d.ts +1 -1
  108. package/dist/status/normalize.js +1 -0
  109. package/dist/status/normalize.js.map +1 -1
  110. package/dist/status/normalize.mjs +1 -0
  111. package/dist/status/normalize.mjs.map +1 -1
  112. package/dist/status/reporter.d.ts +2 -0
  113. package/dist/status/snapshot.d.ts +1 -0
  114. package/dist/types.d.ts +5 -0
  115. package/package.json +6 -3
  116. package/src/agent-stream/poller.ts +2 -3
  117. package/src/boot/daemon-lifecycle.ts +22 -10
  118. package/src/boot/process-hardening.ts +89 -0
  119. package/src/chat/source-machine.ts +534 -0
  120. package/src/chat/source-resolver.ts +0 -0
  121. package/src/chat/subscription-updates.ts +20 -1
  122. package/src/cli-adapter-types.d.ts +1 -0
  123. package/src/cli-adapter-types.ts +22 -2
  124. package/src/cli-adapters/cli-script-runner.ts +421 -0
  125. package/src/cli-adapters/cli-state-engine.ts +1138 -0
  126. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  127. package/src/cli-adapters/provider-cli-adapter.ts +673 -1139
  128. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  129. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  130. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  131. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  132. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  133. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  134. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +35 -29
  135. package/src/cli-adapters/terminal-backends/types.ts +1 -3
  136. package/src/cli-adapters/terminal-screen.ts +11 -81
  137. package/src/commands/chat-commands.ts +1787 -60
  138. package/src/commands/cli-manager.ts +283 -14
  139. package/src/commands/handler.ts +809 -2
  140. package/src/commands/mesh-coordinator.ts +332 -122
  141. package/src/commands/router.ts +4198 -557
  142. package/src/config/chat-history.ts +95 -24
  143. package/src/config/config.ts +12 -0
  144. package/src/config/mesh-config.ts +280 -2
  145. package/src/config/recent-activity.ts +8 -2
  146. package/src/daemon/dev-auto-implement.ts +3 -2
  147. package/src/daemon/dev-cli-debug.ts +10 -1
  148. package/src/detection/ide-detector.ts +26 -16
  149. package/src/git/git-commands.ts +42 -12
  150. package/src/git/git-diff.ts +53 -0
  151. package/src/git/git-status.ts +35 -6
  152. package/src/git/git-types.ts +2 -0
  153. package/src/git/git-worktree.ts +8 -1
  154. package/src/index.ts +126 -10
  155. package/src/installer.d.ts +1 -1
  156. package/src/installer.ts +8 -6
  157. package/src/ipc/local-ipc-server.ts +278 -0
  158. package/src/launch.d.ts +1 -1
  159. package/src/launch.ts +37 -28
  160. package/src/logging/async-batch-writer.ts +55 -0
  161. package/src/logging/logger.ts +2 -1
  162. package/src/mesh/contracts.ts +329 -0
  163. package/src/mesh/coordinator-prompt.ts +218 -31
  164. package/src/mesh/coordinator-registry.ts +121 -0
  165. package/src/mesh/mesh-active-work.ts +437 -0
  166. package/src/mesh/mesh-delivery-policy.ts +315 -0
  167. package/src/mesh/mesh-events-coordinator.ts +1404 -0
  168. package/src/mesh/mesh-events-pending.ts +371 -0
  169. package/src/mesh/mesh-events-stale.ts +283 -0
  170. package/src/mesh/mesh-events-utils.ts +161 -0
  171. package/src/mesh/mesh-events.ts +24 -908
  172. package/src/mesh/mesh-fast-forward.ts +438 -0
  173. package/src/mesh/mesh-host-ownership.ts +73 -0
  174. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  175. package/src/mesh/mesh-ledger.ts +587 -102
  176. package/src/mesh/mesh-missions.ts +151 -0
  177. package/src/mesh/mesh-refine-status.ts +144 -0
  178. package/src/mesh/mesh-review-inbox.ts +307 -0
  179. package/src/mesh/mesh-runtime-store.ts +1305 -0
  180. package/src/mesh/mesh-task-stats.ts +154 -0
  181. package/src/mesh/mesh-work-queue.ts +490 -166
  182. package/src/mesh/preview-freshness.ts +118 -0
  183. package/src/mesh/refine-config.ts +403 -0
  184. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  185. package/src/providers/acp-provider-instance.ts +15 -1
  186. package/src/providers/approval-utils.d.ts +4 -0
  187. package/src/providers/approval-utils.ts +47 -5
  188. package/src/providers/chat-message-normalization.ts +4 -11
  189. package/src/providers/cli-provider-instance.ts +833 -64
  190. package/src/providers/contracts.d.ts +55 -0
  191. package/src/providers/contracts.ts +141 -6
  192. package/src/providers/external-sources.ts +218 -0
  193. package/src/providers/ide-provider-instance.ts +19 -5
  194. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  195. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  196. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  197. package/src/providers/native-history/dispatcher.ts +340 -0
  198. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  199. package/src/providers/native-history/index.ts +30 -0
  200. package/src/providers/provider-instance-manager.ts +30 -0
  201. package/src/providers/provider-instance.ts +10 -1
  202. package/src/providers/provider-loader.ts +582 -50
  203. package/src/providers/provider-schema.ts +87 -14
  204. package/src/providers/provider-trust.ts +114 -0
  205. package/src/providers/read-chat-contract.ts +76 -16
  206. package/src/providers/sdk/README.md +49 -0
  207. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  208. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  209. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  210. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  211. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  212. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  213. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  214. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  215. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  216. package/src/providers/sdk/v1/index.ts +152 -0
  217. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  218. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  219. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  220. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  221. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  222. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  223. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  224. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  225. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  226. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  227. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  228. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  229. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  230. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  231. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  232. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  233. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  234. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  235. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  236. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  237. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  238. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  239. package/src/providers/sdk/v1/validators/index.ts +19 -0
  240. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  241. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  242. package/src/providers/spec/adapter.ts +138 -0
  243. package/src/providers/spec/cli-adapter.ts +626 -0
  244. package/src/providers/spec/driver.ts +922 -0
  245. package/src/providers/spec/evaluator.ts +597 -0
  246. package/src/providers/spec/loader.ts +324 -0
  247. package/src/providers/spec/native-history-executor.ts +939 -0
  248. package/src/providers/spec/route.ts +51 -0
  249. package/src/providers/spec/schema.gen.ts +691 -0
  250. package/src/providers/spec/schema.json +341 -0
  251. package/src/providers/spec/types.ts +356 -0
  252. package/src/providers/transcript-v2.ts +567 -0
  253. package/src/providers/types/interactive-prompt.ts +425 -0
  254. package/src/providers/version-archive.ts +38 -20
  255. package/src/repo-mesh-types.ts +119 -1
  256. package/src/sessions/registry.ts +3 -0
  257. package/src/shared-types-extra.ts +2 -4
  258. package/src/shared-types.ts +45 -1
  259. package/src/status/builders.ts +24 -6
  260. package/src/status/normalize.ts +2 -0
  261. package/src/status/reporter.ts +15 -0
  262. package/src/status/snapshot.ts +84 -25
  263. package/src/system/host-memory.ts +29 -12
  264. package/src/types.ts +5 -0
  265. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  266. package/dist/mesh/mesh-sync.d.ts +0 -53
  267. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  268. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  269. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,371 @@
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, 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
+ const sessionId = resolveEventSessionId(metadata);
64
+ const providerSessionId = readNonEmptyString(metadata.providerSessionId);
65
+ const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
66
+ const jobId = readRefineJobId(event);
67
+ const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
68
+ return [
69
+ event.meshId,
70
+ event.event,
71
+ event.nodeId || '',
72
+ sessionId || '',
73
+ providerSessionId || '',
74
+ taskId || '',
75
+ jobId || '',
76
+ timestamp || '',
77
+ ].join('::');
78
+ }
79
+
80
+ export function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
81
+ const fingerprint = buildPendingEventFingerprint(event);
82
+ if (!fingerprint.trim()) return false;
83
+ // Check SQLite inbox first (G3 primary path)
84
+ try {
85
+ if (MeshRuntimeStore.getInstance().hasPendingEventFingerprint(event.meshId, fingerprint)) return true;
86
+ } catch { /* fall through to JSONL check */ }
87
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
88
+ }
89
+
90
+ function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): string {
91
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
92
+ if (coordinatorDaemonId) {
93
+ const safeDaemon = coordinatorDaemonId.replace(/[^a-zA-Z0-9_-]/g, '_');
94
+ return join(getLedgerDir(), `${safe}-${safeDaemon}.pending-events.jsonl`);
95
+ }
96
+ return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
97
+ }
98
+
99
+ function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
100
+ if (!meshId) return [];
101
+ // Read coordinator-scoped file first; fall back to legacy shared file.
102
+ const paths = coordinatorDaemonId
103
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
104
+ : [getPendingEventsPath(meshId)];
105
+ const events: PendingMeshCoordinatorEvent[] = [];
106
+ for (const path of paths) {
107
+ if (!existsSync(path)) continue;
108
+ try {
109
+ const raw = readFileSync(path, 'utf-8');
110
+ const parsed = raw.split('\n').filter(Boolean).flatMap(line => {
111
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
112
+ });
113
+ // If reading the shared file, filter to events that target this coordinator or are unscoped.
114
+ const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
115
+ ? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
116
+ : parsed;
117
+ events.push(...filtered);
118
+ } catch { /* skip unreadable files */ }
119
+ }
120
+ return events;
121
+ }
122
+
123
+ function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
124
+ const acceptedJobIds = new Set(
125
+ pending
126
+ .filter(event => event.event === 'refine:accepted')
127
+ .map(event => readRefineJobId(event))
128
+ .filter(Boolean),
129
+ );
130
+ if (acceptedJobIds.size === 0) return [];
131
+ const existingTerminalJobIds = new Set(
132
+ pending
133
+ .filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
134
+ .map(event => `${event.event}:${readRefineJobId(event)}`)
135
+ .filter(value => !value.endsWith(':')),
136
+ );
137
+ const backfilled: PendingMeshCoordinatorEvent[] = [];
138
+ const entries = readLedgerEntries(meshId, { tail: 200 });
139
+ for (let i = entries.length - 1; i >= 0; i--) {
140
+ const entry = entries[i];
141
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
142
+ const payload = readRecord(entry.payload);
143
+ if (payload?.source !== 'refine_mesh_node_async_job') continue;
144
+ const refineJob = readRecord(payload.refineJob);
145
+ const jobId = readNonEmptyString(refineJob?.jobId);
146
+ if (!jobId || !acceptedJobIds.has(jobId)) continue;
147
+ const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
148
+ if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
149
+ existingTerminalJobIds.add(`${eventName}:${jobId}`);
150
+ const result = readRecord(payload.result);
151
+ const metadataEvent = {
152
+ source: 'refine_mesh_node_async_job',
153
+ jobId,
154
+ interactionId: readNonEmptyString(refineJob?.interactionId),
155
+ meshId,
156
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
157
+ targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
158
+ workspace: readNonEmptyString(refineJob?.workspace),
159
+ status: eventName === 'refine:completed' ? 'completed' : 'failed',
160
+ startedAt: readNonEmptyString(refineJob?.startedAt),
161
+ completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
162
+ retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
163
+ ...(result ? { result } : {}),
164
+ };
165
+ const nodeLabel = readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job';
166
+ backfilled.push({
167
+ event: eventName,
168
+ meshId,
169
+ nodeLabel,
170
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
171
+ workspace: readNonEmptyString(refineJob?.workspace),
172
+ metadataEvent,
173
+ coordinatorMessage: buildMeshSystemMessage({ event: eventName, nodeLabel, metadataEvent }),
174
+ queuedAt: Date.now(),
175
+ });
176
+ }
177
+ return backfilled.reverse();
178
+ }
179
+
180
+ function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
181
+ const backfilled = refineTerminalEventFromLedger(meshId, events);
182
+ if (backfilled.length === 0) return events;
183
+ const terminalJobIds = new Set(backfilled.map(event => readRefineJobId(event)).filter(Boolean));
184
+ return [
185
+ ...events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event)))),
186
+ ...backfilled,
187
+ ];
188
+ }
189
+
190
+ const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
191
+ const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
192
+
193
+ function trimPendingEventsIfNeeded(path: string): void {
194
+ try {
195
+ if (!existsSync(path)) return;
196
+ if (statSync(path).size <= MAX_PENDING_EVENTS_BYTES) return;
197
+ const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
198
+ if (lines.length <= MAX_PENDING_EVENTS_KEEP) return;
199
+ writeFileSync(path, lines.slice(-MAX_PENDING_EVENTS_KEEP).join('\n') + '\n', 'utf-8');
200
+ } catch { /* best-effort; if trim fails, append still proceeds */ }
201
+ }
202
+
203
+ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
204
+ try {
205
+ if (hasPendingRefineTerminalEventDuplicate(event)) {
206
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
207
+ return true;
208
+ }
209
+ if (hasPendingCoordinatorEventDuplicate(event)) {
210
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
211
+ return true;
212
+ }
213
+
214
+ const fingerprint = buildPendingEventFingerprint(event);
215
+
216
+ // G3: Write to SQLite inbox (primary path going forward)
217
+ try {
218
+ MeshRuntimeStore.getInstance().insertPendingEvent({
219
+ id: randomUUID(),
220
+ meshId: event.meshId,
221
+ coordinatorDaemonId: event.targetCoordinatorDaemonId ?? null,
222
+ event: event.event,
223
+ payload: event,
224
+ fingerprint: fingerprint || null,
225
+ queuedAt: event.queuedAt,
226
+ });
227
+ } catch {
228
+ // SQLite write failure is non-fatal; JSONL fallback below still works.
229
+ }
230
+
231
+ // Also write to JSONL (retained as legacy/export artifact)
232
+ const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
233
+ trimPendingEventsIfNeeded(path);
234
+ appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
235
+ return true;
236
+ } catch (e: any) {
237
+ LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
238
+ return false;
239
+ }
240
+ }
241
+
242
+ // Atomically rename the file before reading so concurrent drains can't both consume
243
+ // the same events. renameSync is atomic on POSIX (same filesystem); only one caller
244
+ // wins the rename — the other gets ENOENT and returns null, preventing duplicate delivery.
245
+ function atomicDrainFile(path: string): string | null {
246
+ const tmpPath = `${path}.draining`;
247
+ try {
248
+ renameSync(path, tmpPath);
249
+ } catch {
250
+ return null; // another drain already renamed it, or file doesn't exist
251
+ }
252
+ try {
253
+ const content = readFileSync(tmpPath, 'utf-8');
254
+ try { unlinkSync(tmpPath); } catch { /* already cleaned up */ }
255
+ return content;
256
+ } catch {
257
+ try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
258
+ return null;
259
+ }
260
+ }
261
+
262
+ /** Drain and return all pending coordinator events for meshId, removing them from disk. */
263
+ export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
264
+ if (!meshId) return [];
265
+
266
+ // Dual-write means SQLite and JSONL hold the same events. Both stores must be
267
+ // emptied in one drain call — draining only one leaves the other to re-deliver
268
+ // the same events on the next call. Merge with fingerprint dedup.
269
+ const merged: PendingMeshCoordinatorEvent[] = [];
270
+ const seenFingerprints = new Set<string>();
271
+ const pushUnique = (event: PendingMeshCoordinatorEvent) => {
272
+ const fingerprint = buildPendingEventFingerprint(event);
273
+ if (fingerprint.trim()) {
274
+ if (seenFingerprints.has(fingerprint)) return;
275
+ seenFingerprints.add(fingerprint);
276
+ }
277
+ merged.push(event);
278
+ };
279
+
280
+ // G3: SQLite inbox
281
+ try {
282
+ const store = MeshRuntimeStore.getInstance();
283
+ if (store.pendingEventCount(meshId) > 0) {
284
+ for (const row of store.drainPendingEvents(meshId, coordinatorDaemonId)) {
285
+ const event = row.payload as PendingMeshCoordinatorEvent;
286
+ if (event) pushUnique(event);
287
+ }
288
+ }
289
+ } catch {
290
+ // SQLite drain failed — JSONL below still drains
291
+ }
292
+
293
+ // JSONL (legacy / migration path) — always drained alongside SQLite
294
+ const paths = coordinatorDaemonId
295
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
296
+ : [getPendingEventsPath(meshId)];
297
+ for (const path of paths) {
298
+ const content = atomicDrainFile(path);
299
+ if (!content) continue;
300
+ const parsed = content.split('\n').filter(Boolean).flatMap(line => {
301
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
302
+ });
303
+ // If reading the shared file, filter to events that target this coordinator or are unscoped.
304
+ const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
305
+ ? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
306
+ : parsed;
307
+ for (const event of filtered) pushUnique(event);
308
+ }
309
+ if (merged.length === 0) return [];
310
+ return reconcilePendingMeshCoordinatorEvents(meshId, merged);
311
+ }
312
+
313
+ /** Peek at pending coordinator events without draining (non-destructive). */
314
+ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
315
+ if (!meshId) return [];
316
+
317
+ // Merge SQLite (primary) + JSONL (legacy) with fingerprint dedup.
318
+ const merged: PendingMeshCoordinatorEvent[] = [];
319
+ const seenFingerprints = new Set<string>();
320
+ const pushUnique = (event: PendingMeshCoordinatorEvent) => {
321
+ const fingerprint = buildPendingEventFingerprint(event);
322
+ if (fingerprint.trim()) {
323
+ if (seenFingerprints.has(fingerprint)) return;
324
+ seenFingerprints.add(fingerprint);
325
+ }
326
+ merged.push(event);
327
+ };
328
+
329
+ // G3: SQLite inbox (non-destructive peek at undrained rows)
330
+ try {
331
+ const store = MeshRuntimeStore.getInstance();
332
+ if (store.pendingEventCount(meshId) > 0) {
333
+ for (const row of store.peekPendingEvents(meshId, coordinatorDaemonId)) {
334
+ const event = row.payload as PendingMeshCoordinatorEvent;
335
+ if (event) pushUnique(event);
336
+ }
337
+ }
338
+ } catch { /* SQLite unavailable — JSONL fallback below */ }
339
+
340
+ // JSONL (legacy)
341
+ for (const event of readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId)) {
342
+ pushUnique(event);
343
+ }
344
+
345
+ return reconcilePendingMeshCoordinatorEvents(meshId, merged);
346
+ }
347
+
348
+ /**
349
+ * Test helper: purge all pending-event state for a mesh — SQLite rows
350
+ * (including drained fingerprint history) and JSONL files.
351
+ */
352
+ export function __clearMeshPendingEventsForTests(meshId: string): void {
353
+ try {
354
+ MeshRuntimeStore.getInstance().clearPendingEventsForMesh(meshId);
355
+ } catch { /* store unavailable — nothing to clear */ }
356
+ clearPendingMeshCoordinatorEvents(meshId);
357
+ }
358
+
359
+ /** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
360
+ export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
361
+ if (!meshId) return;
362
+ // Clear SQLite rows
363
+ try { MeshRuntimeStore.getInstance().clearPendingEventsForMesh(meshId); } catch { /* store unavailable */ }
364
+ // Clear JSONL files
365
+ const paths = coordinatorDaemonId
366
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
367
+ : [getPendingEventsPath(meshId)];
368
+ for (const path of paths) {
369
+ if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
370
+ }
371
+ }
@@ -0,0 +1,283 @@
1
+ import { appendLedgerEntry, buildTaskCompletionEvidence, readLedgerEntries } from './mesh-ledger.js';
2
+ import type { MeshLedgerKind } from './mesh-ledger.js';
3
+ import { updateDirectDispatchStatus, cleanupTerminalDirectDispatches } from './mesh-work-queue.js';
4
+ import { markSessionDeliveriesTerminal } from './mesh-delivery-policy.js';
5
+ import { queuePendingMeshCoordinatorEvent } from './mesh-events-pending.js';
6
+ import { readNonEmptyString, readRecord, resolveEventSessionId, readWorkerResultMetadata } from './mesh-events-utils.js';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Stale direct-dispatch detection & transcript reconciliation
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export function findRecentTerminalLedgerEvidence(args: {
13
+ meshId: string;
14
+ sessionId?: string;
15
+ nodeId?: string;
16
+ }): { id: string; kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
17
+ if (!args.sessionId && !args.nodeId) return null;
18
+ // Tail-limit: 200 entries gives a wide enough window to catch terminal events for active
19
+ // sessions while avoiding a full O(n) scan. If a terminal is older than 200 entries,
20
+ // the MeshRuntimeStore fingerprint dedup will still block duplicate processing downstream.
21
+ const entries = readLedgerEntries(args.meshId, { tail: 200 });
22
+ for (let i = entries.length - 1; i >= 0; i--) {
23
+ const entry = entries[i];
24
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
25
+ if (args.sessionId && entry.sessionId === args.sessionId) {
26
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
27
+ }
28
+ if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
29
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ // Returns true when a task_dispatched entry for the given session appears AFTER the terminal
36
+ // entry (identified by terminalId) in ledger order. Positional (append) order is used rather
37
+ // than timestamp comparison because both entries may share the same millisecond.
38
+ export function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean {
39
+ // 200-entry window matches findRecentTerminalLedgerEvidence for consistency.
40
+ const entries = readLedgerEntries(meshId, { tail: 200 });
41
+ let pastTerminal = false;
42
+ for (const entry of entries) {
43
+ if (!pastTerminal) {
44
+ if (entry.id === terminalId) pastTerminal = true;
45
+ continue;
46
+ }
47
+ if (entry.kind === 'task_dispatched' && entry.sessionId === sessionId) return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ export function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: string): boolean {
53
+ // Some dispatch paths can persist task_dispatched before the direct-dispatch DB row is
54
+ // available. Recover routing from ledger order so coordinator self-targets still emit
55
+ // task_completed and pendingCoordinatorEvents.
56
+ const entries = readLedgerEntries(meshId, { tail: 200 });
57
+ for (let i = entries.length - 1; i >= 0; i--) {
58
+ const entry = entries[i];
59
+ if (entry.sessionId !== sessionId) continue;
60
+ if (entry.kind === 'task_completed' || entry.kind === 'task_failed' || entry.kind === 'task_stalled') {
61
+ return false;
62
+ }
63
+ if (entry.kind === 'task_dispatched' && entry.payload?.source === 'direct') {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+
70
+ function findDirectDispatchLedgerEntry(args: {
71
+ meshId: string;
72
+ taskId: string;
73
+ sessionId?: string;
74
+ }): { id: string; timestamp: string; nodeId?: string; sessionId?: string; providerType?: string; payload: Record<string, unknown> } | null {
75
+ const entries = readLedgerEntries(args.meshId, { tail: 500 });
76
+ for (let i = entries.length - 1; i >= 0; i--) {
77
+ const entry = entries[i];
78
+ if (entry.kind !== 'task_dispatched') continue;
79
+ const payloadTaskId = readNonEmptyString(entry.payload?.taskId);
80
+ if (payloadTaskId !== args.taskId) continue;
81
+ if (args.sessionId && entry.sessionId && entry.sessionId !== args.sessionId) continue;
82
+ return {
83
+ id: entry.id,
84
+ timestamp: entry.timestamp,
85
+ nodeId: entry.nodeId,
86
+ sessionId: entry.sessionId,
87
+ providerType: entry.providerType,
88
+ payload: entry.payload || {},
89
+ };
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function hasTerminalLedgerAfterDispatch(args: {
95
+ meshId: string;
96
+ taskId: string;
97
+ sessionId?: string;
98
+ dispatchEntryId?: string;
99
+ dispatchTimestamp?: string;
100
+ }): boolean {
101
+ const entries = readLedgerEntries(args.meshId, { tail: 500 });
102
+ let afterDispatch = !args.dispatchEntryId && !args.dispatchTimestamp;
103
+ const dispatchTime = args.dispatchTimestamp ? new Date(args.dispatchTimestamp).getTime() : Number.NaN;
104
+ for (const entry of entries) {
105
+ if (!afterDispatch) {
106
+ if (args.dispatchEntryId && entry.id === args.dispatchEntryId) {
107
+ afterDispatch = true;
108
+ continue;
109
+ }
110
+ if (!args.dispatchEntryId && Number.isFinite(dispatchTime)) {
111
+ const entryTime = new Date(entry.timestamp).getTime();
112
+ if (Number.isFinite(entryTime) && entryTime >= dispatchTime) afterDispatch = true;
113
+ }
114
+ if (!afterDispatch) continue;
115
+ }
116
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
117
+ const terminalTaskId = readNonEmptyString(entry.payload?.taskId);
118
+ if (terminalTaskId && terminalTaskId === args.taskId) return true;
119
+ if (terminalTaskId && terminalTaskId !== args.taskId) continue;
120
+ if (args.sessionId && entry.sessionId === args.sessionId) return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ export function reconcileDirectDispatchCompletionFromTranscript(args: {
126
+ meshId: string;
127
+ nodeId?: string;
128
+ sessionId: string;
129
+ providerType?: string;
130
+ providerSessionId?: string;
131
+ taskId: string;
132
+ finalSummary: string;
133
+ transcriptMessageAt?: string;
134
+ completedAt?: string;
135
+ targetCoordinatorDaemonId?: string;
136
+ source?: string;
137
+ }): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
138
+ const finalSummary = readNonEmptyString(args.finalSummary);
139
+ if (!args.meshId || !args.taskId || !args.sessionId || !finalSummary) {
140
+ return { reconciled: false, reason: 'missing_required_completion_evidence' };
141
+ }
142
+
143
+ const dispatch = findDirectDispatchLedgerEntry({
144
+ meshId: args.meshId,
145
+ taskId: args.taskId,
146
+ sessionId: args.sessionId,
147
+ });
148
+ if (hasTerminalLedgerAfterDispatch({
149
+ meshId: args.meshId,
150
+ taskId: args.taskId,
151
+ sessionId: args.sessionId,
152
+ dispatchEntryId: dispatch?.id,
153
+ dispatchTimestamp: dispatch?.timestamp,
154
+ })) {
155
+ return { reconciled: false, alreadyTerminal: true, reason: 'terminal_ledger_entry_exists' };
156
+ }
157
+
158
+ const nodeId = readNonEmptyString(args.nodeId) || dispatch?.nodeId;
159
+ const providerType = readNonEmptyString(args.providerType) || dispatch?.providerType || readNonEmptyString(dispatch?.payload.providerType);
160
+ const completedAt = args.completedAt || new Date().toISOString();
161
+ const evidence = buildTaskCompletionEvidence({
162
+ event: 'agent:generating_completed',
163
+ nodeId: nodeId || 'unknown',
164
+ sessionId: args.sessionId,
165
+ providerType,
166
+ providerSessionId: readNonEmptyString(args.providerSessionId),
167
+ finalSummary,
168
+ completedAt,
169
+ });
170
+ const workerResult = evidence.workerResult;
171
+ const dispatchTime = dispatch?.timestamp ? new Date(dispatch.timestamp).getTime() : Number.NaN;
172
+ const transcriptTime = args.transcriptMessageAt ? new Date(args.transcriptMessageAt).getTime() : Number.NaN;
173
+ const transcriptAfterDispatch = Number.isFinite(dispatchTime) && Number.isFinite(transcriptTime) && transcriptTime >= dispatchTime;
174
+ if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
175
+ return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
176
+ }
177
+ const workerFailed = workerResult.status === 'failed' || (workerResult.status !== 'completed' && workerResult.errors.length > 0);
178
+ const kind: MeshLedgerKind = workerFailed ? 'task_failed' : 'task_completed';
179
+
180
+ const entry = appendLedgerEntry(args.meshId, {
181
+ kind,
182
+ nodeId: nodeId || undefined,
183
+ sessionId: args.sessionId,
184
+ providerType: providerType || undefined,
185
+ payload: {
186
+ event: 'agent:generating_completed',
187
+ source: args.source || 'direct_task_transcript_reconciliation',
188
+ taskId: args.taskId,
189
+ providerSessionId: readNonEmptyString(args.providerSessionId),
190
+ finalSummary,
191
+ workerResult,
192
+ completionDiagnostic: {
193
+ reason: 'direct_task_transcript_reconciliation',
194
+ dispatchEntryId: dispatch?.id,
195
+ dispatchTimestamp: dispatch?.timestamp,
196
+ transcriptMessageAt: readNonEmptyString(args.transcriptMessageAt),
197
+ transcriptFinalAssistantPresent: true,
198
+ },
199
+ evidence,
200
+ },
201
+ });
202
+ updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
203
+ markSessionDeliveriesTerminal(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
204
+ setImmediate(() => cleanupTerminalDirectDispatches());
205
+ queuePendingMeshCoordinatorEvent({
206
+ event: kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped',
207
+ meshId: args.meshId,
208
+ nodeLabel: nodeId ? `Node '${nodeId}'` : 'Remote agent',
209
+ nodeId: nodeId || undefined,
210
+ metadataEvent: {
211
+ targetSessionId: args.sessionId,
212
+ providerType: providerType || undefined,
213
+ providerSessionId: readNonEmptyString(args.providerSessionId),
214
+ finalSummary,
215
+ taskId: args.taskId,
216
+ workerResult,
217
+ completionDiagnostic: {
218
+ reason: 'direct_task_transcript_reconciliation',
219
+ terminalLedgerKind: kind,
220
+ terminalLedgerId: entry.id,
221
+ },
222
+ },
223
+ coordinatorMessage: undefined,
224
+ queuedAt: Date.now(),
225
+ ...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
226
+ });
227
+
228
+ return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
229
+ }
230
+
231
+ export function buildLongGeneratingCompletionReconciliation(args: {
232
+ meshId: string;
233
+ nodeId?: string;
234
+ nodeLabel: string;
235
+ metadataEvent: Record<string, unknown>;
236
+ sourceInstanceId?: string;
237
+ }): Record<string, unknown> | null {
238
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
239
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
240
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
241
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
242
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
243
+ const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
244
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
245
+ const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
246
+ const explicitCompletionEvidence = Boolean(
247
+ finalSummary
248
+ || workerResult
249
+ || completionDiagnostic?.finalAssistantPresent === true
250
+ || status === 'idle'
251
+ || status === 'ready'
252
+ || status === 'completed',
253
+ );
254
+ if (explicitCompletionEvidence) {
255
+ return {
256
+ ...args.metadataEvent,
257
+ targetSessionId: sessionId,
258
+ providerType,
259
+ providerSessionId,
260
+ finalSummary,
261
+ source: 'long_generating_reconciliation',
262
+ reconciledFromEvent: 'monitor:long_generating',
263
+ timestamp: args.metadataEvent.timestamp ?? Date.now(),
264
+ completionDiagnostic: {
265
+ ...(completionDiagnostic || {}),
266
+ reconciliationReason: 'provider_completion_evidence',
267
+ },
268
+ };
269
+ }
270
+
271
+ const terminal = findRecentTerminalLedgerEvidence({
272
+ meshId: args.meshId,
273
+ sessionId: sessionId || undefined,
274
+ nodeId: nodeId || undefined,
275
+ });
276
+ if (!terminal) return null;
277
+ return {
278
+ ...args.metadataEvent,
279
+ source: 'long_generating_terminal_ledger_suppression',
280
+ terminalLedgerKind: terminal.kind,
281
+ terminalLedgerAt: terminal.timestamp,
282
+ };
283
+ }