@adhdev/daemon-core 0.9.82-rc.17 → 0.9.82-rc.170

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 (221) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/chat/source-machine.d.ts +166 -0
  3. package/dist/chat/source-resolver.d.ts +104 -0
  4. package/dist/chat/subscription-updates.d.ts +1 -0
  5. package/dist/cli-adapter-types.d.ts +22 -2
  6. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  7. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  8. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  11. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  12. package/dist/commands/handler.d.ts +110 -0
  13. package/dist/commands/mesh-coordinator.d.ts +72 -1
  14. package/dist/commands/router.d.ts +22 -0
  15. package/dist/config/chat-history.d.ts +7 -0
  16. package/dist/config/config.d.ts +5 -0
  17. package/dist/config/mesh-config.d.ts +71 -1
  18. package/dist/git/git-commands.d.ts +5 -1
  19. package/dist/index.d.ts +35 -6
  20. package/dist/index.js +18621 -3760
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +18525 -3732
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/installer.d.ts +1 -4
  25. package/dist/ipc/local-ipc-server.d.ts +91 -0
  26. package/dist/launch.d.ts +1 -1
  27. package/dist/logging/async-batch-writer.d.ts +10 -0
  28. package/dist/mesh/beads-db.d.ts +72 -0
  29. package/dist/mesh/contracts.d.ts +164 -0
  30. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  31. package/dist/mesh/coordinator-registry.d.ts +59 -0
  32. package/dist/mesh/mesh-active-work.d.ts +90 -0
  33. package/dist/mesh/mesh-events.d.ts +96 -5
  34. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  35. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  36. package/dist/mesh/mesh-ledger.d.ts +58 -1
  37. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  38. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  39. package/dist/mesh/preview-freshness.d.ts +18 -0
  40. package/dist/mesh/refine-config.d.ts +193 -0
  41. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  42. package/dist/providers/approval-utils.d.ts +13 -0
  43. package/dist/providers/cli-provider-instance.d.ts +9 -3
  44. package/dist/providers/contracts.d.ts +98 -5
  45. package/dist/providers/external-sources.d.ts +71 -0
  46. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  47. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  48. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  49. package/dist/providers/native-history/dispatcher.d.ts +24 -0
  50. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  51. package/dist/providers/native-history/index.d.ts +13 -0
  52. package/dist/providers/provider-instance.d.ts +3 -0
  53. package/dist/providers/provider-loader.d.ts +26 -4
  54. package/dist/providers/provider-trust.d.ts +31 -0
  55. package/dist/providers/read-chat-contract.d.ts +29 -0
  56. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  57. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  58. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  59. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  60. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  61. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  62. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  63. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  64. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  65. package/dist/providers/sdk/v1/index.d.ts +30 -0
  66. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  67. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  68. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  69. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  70. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  71. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  72. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  73. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  74. package/dist/providers/spec/adapter.d.ts +56 -0
  75. package/dist/providers/spec/cli-adapter.d.ts +95 -0
  76. package/dist/providers/spec/driver.d.ts +149 -0
  77. package/dist/providers/spec/evaluator.d.ts +47 -0
  78. package/dist/providers/spec/loader.d.ts +14 -0
  79. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  80. package/dist/providers/spec/route.d.ts +4 -0
  81. package/dist/providers/spec/schema.gen.d.ts +511 -0
  82. package/dist/providers/spec/types.d.ts +212 -0
  83. package/dist/providers/transcript-v2.d.ts +176 -0
  84. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  85. package/dist/repo-mesh-types.d.ts +100 -1
  86. package/dist/sessions/registry.d.ts +3 -0
  87. package/dist/shared-types.d.ts +42 -1
  88. package/dist/status/reporter.d.ts +2 -0
  89. package/dist/status/snapshot.d.ts +1 -0
  90. package/dist/types.d.ts +5 -0
  91. package/package.json +7 -2
  92. package/src/agent-stream/poller.ts +2 -3
  93. package/src/boot/daemon-lifecycle.ts +17 -10
  94. package/src/boot/process-hardening.ts +89 -0
  95. package/src/chat/source-machine.ts +534 -0
  96. package/src/chat/source-resolver.ts +0 -0
  97. package/src/chat/subscription-updates.ts +14 -1
  98. package/src/cli-adapter-types.d.ts +1 -0
  99. package/src/cli-adapter-types.ts +21 -2
  100. package/src/cli-adapters/cli-script-runner.ts +421 -0
  101. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  102. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  103. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  104. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  105. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  106. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  107. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  108. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  109. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  110. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  111. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  112. package/src/commands/chat-commands.ts +1597 -60
  113. package/src/commands/cli-manager.ts +214 -3
  114. package/src/commands/handler.ts +809 -2
  115. package/src/commands/mesh-coordinator.ts +231 -127
  116. package/src/commands/router.ts +3559 -502
  117. package/src/config/chat-history.ts +87 -24
  118. package/src/config/config.ts +12 -0
  119. package/src/config/mesh-config.ts +264 -2
  120. package/src/config/recent-activity.ts +8 -2
  121. package/src/daemon/dev-cli-debug.ts +10 -1
  122. package/src/detection/ide-detector.ts +26 -16
  123. package/src/git/git-commands.ts +17 -5
  124. package/src/git/git-worktree.ts +8 -1
  125. package/src/index.ts +113 -5
  126. package/src/installer.d.ts +1 -1
  127. package/src/installer.ts +8 -6
  128. package/src/ipc/local-ipc-server.ts +278 -0
  129. package/src/launch.d.ts +1 -1
  130. package/src/launch.ts +37 -28
  131. package/src/logging/async-batch-writer.ts +55 -0
  132. package/src/logging/logger.ts +2 -1
  133. package/src/mesh/beads-db.ts +479 -0
  134. package/src/mesh/contracts.ts +329 -0
  135. package/src/mesh/coordinator-prompt.ts +204 -30
  136. package/src/mesh/coordinator-registry.ts +121 -0
  137. package/src/mesh/mesh-active-work.ts +437 -0
  138. package/src/mesh/mesh-events.ts +879 -61
  139. package/src/mesh/mesh-fast-forward.ts +430 -0
  140. package/src/mesh/mesh-host-ownership.ts +73 -0
  141. package/src/mesh/mesh-ledger.ts +457 -104
  142. package/src/mesh/mesh-refine-status.ts +144 -0
  143. package/src/mesh/mesh-work-queue.ts +216 -158
  144. package/src/mesh/preview-freshness.ts +118 -0
  145. package/src/mesh/refine-config.ts +366 -0
  146. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  147. package/src/providers/approval-utils.d.ts +4 -0
  148. package/src/providers/approval-utils.ts +47 -5
  149. package/src/providers/chat-message-normalization.ts +4 -11
  150. package/src/providers/cli-provider-instance.ts +452 -54
  151. package/src/providers/contracts.ts +105 -5
  152. package/src/providers/external-sources.ts +218 -0
  153. package/src/providers/ide-provider-instance.ts +19 -5
  154. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  155. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  156. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  157. package/src/providers/native-history/dispatcher.ts +227 -0
  158. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  159. package/src/providers/native-history/index.ts +30 -0
  160. package/src/providers/provider-instance.ts +3 -0
  161. package/src/providers/provider-loader.ts +585 -50
  162. package/src/providers/provider-schema.ts +31 -13
  163. package/src/providers/provider-trust.ts +114 -0
  164. package/src/providers/read-chat-contract.ts +76 -16
  165. package/src/providers/sdk/README.md +49 -0
  166. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  167. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  168. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  169. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  170. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  171. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  172. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  173. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  174. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  175. package/src/providers/sdk/v1/index.ts +152 -0
  176. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  177. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  178. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  179. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
  180. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  181. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  182. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  183. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  185. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  186. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  187. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  188. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  189. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  190. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  191. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  192. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  193. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  194. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  195. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  196. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  197. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  198. package/src/providers/sdk/v1/validators/index.ts +19 -0
  199. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  200. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  201. package/src/providers/spec/adapter.ts +168 -0
  202. package/src/providers/spec/cli-adapter.ts +430 -0
  203. package/src/providers/spec/driver.ts +502 -0
  204. package/src/providers/spec/evaluator.ts +298 -0
  205. package/src/providers/spec/loader.ts +130 -0
  206. package/src/providers/spec/native-history-executor.ts +623 -0
  207. package/src/providers/spec/route.ts +51 -0
  208. package/src/providers/spec/schema.gen.ts +511 -0
  209. package/src/providers/spec/schema.json +211 -0
  210. package/src/providers/spec/types.ts +231 -0
  211. package/src/providers/transcript-v2.ts +567 -0
  212. package/src/providers/types/interactive-prompt.ts +297 -0
  213. package/src/providers/version-archive.ts +38 -20
  214. package/src/repo-mesh-types.ts +110 -1
  215. package/src/sessions/registry.ts +3 -0
  216. package/src/shared-types.ts +45 -1
  217. package/src/status/builders.ts +24 -6
  218. package/src/status/reporter.ts +15 -0
  219. package/src/status/snapshot.ts +84 -25
  220. package/src/system/host-memory.ts +29 -12
  221. package/src/types.ts +5 -0
@@ -1,31 +1,69 @@
1
+ import { appendFileSync, existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
1
3
  import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
2
4
  import { loadConfig } from '../config/config.js';
3
5
  import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
4
6
  import { detectCLI } from '../detection/cli-detector.js';
5
7
  import { LOG } from '../logging/logger.js';
6
- import { appendLedgerEntry, buildTaskCompletionEvidence, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
8
+ import { appendLedgerEntry, buildTaskCompletionEvidence, getLedgerDir, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
7
9
  import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
8
- import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch } from './mesh-work-queue.js';
10
+ import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches } from './mesh-work-queue.js';
11
+ import { BeadsDB } from './beads-db.js';
9
12
 
10
13
  // ---------------------------------------------------------------------------
11
14
  // Remote Node Idle Session Tracking
12
15
  // ---------------------------------------------------------------------------
13
- // Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
14
- // can assign tasks to them.
16
+ // Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
17
+ // can assign tasks to them. Each entry carries an expiresAt timestamp;
18
+ // entries are swept on insertion to prevent unbounded growth.
15
19
  // ---------------------------------------------------------------------------
16
20
  interface RemoteIdleSession {
17
21
  nodeId: string;
18
22
  sessionId: string;
19
23
  providerType: string;
24
+ expiresAt: number;
20
25
  }
26
+ const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
21
27
  const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
22
28
 
23
29
  // ---------------------------------------------------------------------------
24
- // MCP coordinator pending-event queue
30
+ // Workspace-to-mesh lookup cache
31
+ // ---------------------------------------------------------------------------
32
+ // getMeshByRepo is called on every coordinator event for sessions without
33
+ // meshNodeFor. Cache results for 5 seconds to avoid repeated config reads.
34
+ const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
35
+ const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
36
+
37
+ function getCachedMeshByWorkspace(workspace: string): any {
38
+ const now = Date.now();
39
+ const cached = meshByWorkspaceCache.get(workspace);
40
+ if (cached && now - cached.cachedAt < MESH_WORKSPACE_CACHE_TTL_MS) return cached.mesh;
41
+ const mesh = getMeshByRepo(workspace);
42
+ meshByWorkspaceCache.set(workspace, { mesh, cachedAt: now });
43
+ return mesh;
44
+ }
45
+
46
+ function readWorkerResultMetadata(event: Record<string, unknown>): Record<string, unknown> | undefined {
47
+ return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
48
+ }
49
+
50
+ function sweepExpiredRemoteIdleSessions(): void {
51
+ const now = Date.now();
52
+ for (const [key, session] of remoteIdleSessions) {
53
+ if (session.expiresAt <= now) remoteIdleSessions.delete(key);
54
+ }
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
25
59
  // ---------------------------------------------------------------------------
26
60
  // When a mesh event fires but no CLI coordinator session is registered (e.g.
27
- // the coordinator is Claude Code running via MCP), we buffer the event here.
28
- // The MCP server drains this queue on every mesh_status / mesh_send_task poll.
61
+ // the coordinator is Claude Code running via MCP), we persist the event to a
62
+ // per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
63
+ // is removed; the file is drained atomically on each get_pending_mesh_events
64
+ // call and limited to 100 KB to prevent runaway growth.
65
+ //
66
+ // File: <ledgerDir>/<meshId>.pending-events.jsonl
29
67
  // ---------------------------------------------------------------------------
30
68
 
31
69
  export interface PendingMeshCoordinatorEvent {
@@ -35,39 +73,272 @@ export interface PendingMeshCoordinatorEvent {
35
73
  nodeId?: string;
36
74
  workspace?: string;
37
75
  metadataEvent: Record<string, unknown>;
76
+ coordinatorMessage?: string;
38
77
  queuedAt: number;
78
+ /**
79
+ * When set, this event is intended for a specific coordinator daemon.
80
+ * Coordinators on other daemons should ignore it during drain.
81
+ * Absent on legacy events — treated as broadcast to any coordinator.
82
+ */
83
+ targetCoordinatorDaemonId?: string;
84
+ }
85
+
86
+ const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
87
+
88
+ function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
89
+ const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
90
+ const result = readRecord(metadata.result);
91
+ const refineJob = readRecord(result?.refineJob);
92
+ return readNonEmptyString(metadata.jobId) || readNonEmptyString(refineJob?.jobId);
93
+ }
94
+
95
+ function buildRefineTerminalEventFingerprint(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): string {
96
+ const jobId = readRefineJobId({ metadataEvent });
97
+ return jobId && REFINE_TERMINAL_EVENTS.has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
98
+ }
99
+
100
+ function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
101
+ if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
102
+ const jobId = readRefineJobId(event);
103
+ if (!jobId) return false;
104
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) =>
105
+ pending.event === event.event && readRefineJobId(pending) === jobId,
106
+ );
107
+ }
108
+
109
+ function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
110
+ const metadata = readRecord(event.metadataEvent) || {};
111
+ const sessionId = resolveEventSessionId(metadata);
112
+ const providerSessionId = readNonEmptyString(metadata.providerSessionId);
113
+ const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
114
+ const jobId = readRefineJobId(event);
115
+ const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
116
+ return [
117
+ event.meshId,
118
+ event.event,
119
+ event.nodeId || '',
120
+ sessionId || '',
121
+ providerSessionId || '',
122
+ taskId || '',
123
+ jobId || '',
124
+ timestamp || '',
125
+ ].join('::');
126
+ }
127
+
128
+ function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
129
+ const fingerprint = buildPendingEventFingerprint(event);
130
+ if (!fingerprint.trim()) return false;
131
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
132
+ }
133
+
134
+ function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): string {
135
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
136
+ if (coordinatorDaemonId) {
137
+ const safeDaemon = coordinatorDaemonId.replace(/[^a-zA-Z0-9_-]/g, '_');
138
+ return join(getLedgerDir(), `${safe}-${safeDaemon}.pending-events.jsonl`);
139
+ }
140
+ return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
141
+ }
142
+
143
+ function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
144
+ if (!meshId) return [];
145
+ // Read coordinator-scoped file first; fall back to legacy shared file.
146
+ const paths = coordinatorDaemonId
147
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
148
+ : [getPendingEventsPath(meshId)];
149
+ const events: PendingMeshCoordinatorEvent[] = [];
150
+ for (const path of paths) {
151
+ if (!existsSync(path)) continue;
152
+ try {
153
+ const raw = readFileSync(path, 'utf-8');
154
+ const parsed = raw.split('\n').filter(Boolean).flatMap(line => {
155
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
156
+ });
157
+ // If reading the shared file, filter to events that target this coordinator or are unscoped.
158
+ const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
159
+ ? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
160
+ : parsed;
161
+ events.push(...filtered);
162
+ } catch { /* skip unreadable files */ }
163
+ }
164
+ return events;
165
+ }
166
+
167
+ function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
168
+ const acceptedJobIds = new Set(
169
+ pending
170
+ .filter(event => event.event === 'refine:accepted')
171
+ .map(event => readRefineJobId(event))
172
+ .filter(Boolean),
173
+ );
174
+ if (acceptedJobIds.size === 0) return [];
175
+ const existingTerminalJobIds = new Set(
176
+ pending
177
+ .filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
178
+ .map(event => `${event.event}:${readRefineJobId(event)}`)
179
+ .filter(value => !value.endsWith(':')),
180
+ );
181
+ const backfilled: PendingMeshCoordinatorEvent[] = [];
182
+ const entries = readLedgerEntries(meshId, { tail: 200 });
183
+ for (let i = entries.length - 1; i >= 0; i--) {
184
+ const entry = entries[i];
185
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
186
+ const payload = readRecord(entry.payload);
187
+ if (payload?.source !== 'refine_mesh_node_async_job') continue;
188
+ const refineJob = readRecord(payload.refineJob);
189
+ const jobId = readNonEmptyString(refineJob?.jobId);
190
+ if (!jobId || !acceptedJobIds.has(jobId)) continue;
191
+ const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
192
+ if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
193
+ existingTerminalJobIds.add(`${eventName}:${jobId}`);
194
+ const result = readRecord(payload.result);
195
+ const metadataEvent = {
196
+ source: 'refine_mesh_node_async_job',
197
+ jobId,
198
+ interactionId: readNonEmptyString(refineJob?.interactionId),
199
+ meshId,
200
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
201
+ targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
202
+ workspace: readNonEmptyString(refineJob?.workspace),
203
+ status: eventName === 'refine:completed' ? 'completed' : 'failed',
204
+ startedAt: readNonEmptyString(refineJob?.startedAt),
205
+ completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
206
+ retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
207
+ ...(result ? { result } : {}),
208
+ };
209
+ backfilled.push({
210
+ event: eventName,
211
+ meshId,
212
+ nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
213
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
214
+ workspace: readNonEmptyString(refineJob?.workspace),
215
+ metadataEvent,
216
+ coordinatorMessage: buildMeshSystemMessage({
217
+ event: eventName,
218
+ nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
219
+ metadataEvent,
220
+ }),
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
+ if (backfilled.length === 0) return events;
230
+ const terminalJobIds = new Set(backfilled.map(event => readRefineJobId(event)).filter(Boolean));
231
+ return [
232
+ ...events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event)))),
233
+ ...backfilled,
234
+ ];
39
235
  }
40
236
 
41
- const MAX_PENDING_EVENTS = 50;
42
- const pendingMeshCoordinatorEvents: PendingMeshCoordinatorEvent[] = [];
237
+ const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
238
+ const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
239
+
240
+ function trimPendingEventsIfNeeded(path: string): void {
241
+ try {
242
+ if (!existsSync(path)) return;
243
+ if (statSync(path).size <= MAX_PENDING_EVENTS_BYTES) return;
244
+ const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
245
+ if (lines.length <= MAX_PENDING_EVENTS_KEEP) return;
246
+ writeFileSync(path, lines.slice(-MAX_PENDING_EVENTS_KEEP).join('\n') + '\n', 'utf-8');
247
+ } catch { /* best-effort; if trim fails, append still proceeds */ }
248
+ }
43
249
 
44
250
  export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
45
- if (pendingMeshCoordinatorEvents.length >= MAX_PENDING_EVENTS) {
251
+ try {
252
+ if (hasPendingRefineTerminalEventDuplicate(event)) {
253
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
254
+ return true;
255
+ }
256
+ if (hasPendingCoordinatorEventDuplicate(event)) {
257
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
258
+ return true;
259
+ }
260
+ // Write to the coordinator-scoped file when the target coordinator is known;
261
+ // fall back to the shared file for legacy/unscoped events.
262
+ const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
263
+ trimPendingEventsIfNeeded(path);
264
+ appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
265
+ return true;
266
+ } catch (e: any) {
267
+ LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
46
268
  return false;
47
269
  }
48
- pendingMeshCoordinatorEvents.push(event);
49
- return true;
50
270
  }
51
271
 
52
- /** Drain and return all pending coordinator events, clearing the queue. */
53
- export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
54
- return pendingMeshCoordinatorEvents.splice(0);
272
+ // Atomically rename the file before reading so concurrent drains can't both consume
273
+ // the same events. renameSync is atomic on POSIX (same filesystem); only one caller
274
+ // wins the rename — the other gets ENOENT and returns null, preventing duplicate delivery.
275
+ function atomicDrainFile(path: string): string | null {
276
+ const tmpPath = `${path}.draining`;
277
+ try {
278
+ renameSync(path, tmpPath);
279
+ } catch {
280
+ return null; // another drain already renamed it, or file doesn't exist
281
+ }
282
+ try {
283
+ const content = readFileSync(tmpPath, 'utf-8');
284
+ try { unlinkSync(tmpPath); } catch { /* already cleaned up */ }
285
+ return content;
286
+ } catch {
287
+ try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
288
+ return null;
289
+ }
290
+ }
291
+
292
+ /** Drain and return all pending coordinator events for meshId, removing them from disk. */
293
+ export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
294
+ if (!meshId) return [];
295
+ const paths = coordinatorDaemonId
296
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
297
+ : [getPendingEventsPath(meshId)];
298
+ const all: PendingMeshCoordinatorEvent[] = [];
299
+ for (const path of paths) {
300
+ const content = atomicDrainFile(path);
301
+ if (!content) continue;
302
+ const parsed = content.split('\n').filter(Boolean).flatMap(line => {
303
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
304
+ });
305
+ // If reading the shared file, filter to events that target this coordinator or are unscoped.
306
+ const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
307
+ ? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
308
+ : parsed;
309
+ all.push(...filtered);
310
+ }
311
+ if (all.length === 0) return [];
312
+ return reconcilePendingMeshCoordinatorEvents(meshId, all);
55
313
  }
56
314
 
57
315
  /** Peek at pending coordinator events without draining (non-destructive). */
58
- export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
59
- return pendingMeshCoordinatorEvents.slice();
316
+ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
317
+ if (!meshId) return [];
318
+ return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId));
60
319
  }
61
320
 
62
- /** Explicitly clear all pending coordinator events. */
63
- export function clearPendingMeshCoordinatorEvents(): void {
64
- pendingMeshCoordinatorEvents.splice(0);
321
+ /** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
322
+ export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
323
+ if (!meshId) return;
324
+ const paths = coordinatorDaemonId
325
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
326
+ : [getPendingEventsPath(meshId)];
327
+ for (const path of paths) {
328
+ if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
329
+ }
65
330
  }
66
331
 
67
332
  function readNonEmptyString(value: unknown): string {
68
333
  return typeof value === 'string' && value.trim() ? value.trim() : '';
69
334
  }
70
335
 
336
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
337
+ return value && typeof value === 'object' && !Array.isArray(value)
338
+ ? value as Record<string, unknown>
339
+ : undefined;
340
+ }
341
+
71
342
  function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
72
343
  return readNonEmptyString(event.targetSessionId)
73
344
  || readNonEmptyString(event.sessionId)
@@ -82,6 +353,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
82
353
  'agent:stopped',
83
354
  'agent:ready',
84
355
  'monitor:long_generating',
356
+ 'refine:accepted',
357
+ 'refine:completed',
358
+ 'refine:failed',
85
359
  ]);
86
360
 
87
361
  const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
@@ -96,10 +370,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
96
370
  }
97
371
 
98
372
  function formatCompletionMetadata(event: Record<string, unknown>): string {
373
+ const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
374
+ ? event.completionDiagnostic as Record<string, unknown>
375
+ : null;
376
+ const diagnosticReason = completionDiagnostic
377
+ ? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
378
+ : '';
379
+ const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
380
+ ? String(completionDiagnostic.finalAssistantPresent)
381
+ : '';
99
382
  const parts = [
100
383
  readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
101
384
  readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
102
385
  readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
386
+ diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
387
+ finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
103
388
  ].filter(Boolean);
104
389
  return parts.length > 0 ? ` (${parts.join('; ')})` : '';
105
390
  }
@@ -126,7 +411,7 @@ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boole
126
411
  function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
127
412
  if (!sessionId && !nodeId) return false;
128
413
  const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
129
- const entries = readLedgerEntries(meshId);
414
+ const entries = readLedgerEntries(meshId, { tail: 200 });
130
415
  for (let i = entries.length - 1; i >= 0; i--) {
131
416
  const entry = entries[i];
132
417
  const timestamp = new Date(entry.timestamp).getTime();
@@ -150,6 +435,226 @@ function shouldSuppressIntentionalCleanupStop(args: {
150
435
  return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
151
436
  }
152
437
 
438
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
439
+
440
+ function hasFingerprintSeen(fingerprint: string): boolean {
441
+ try {
442
+ return BeadsDB.getInstance().hasCompletionFingerprint(fingerprint);
443
+ } catch {
444
+ return false;
445
+ }
446
+ }
447
+
448
+ function recordFingerprintSeen(fingerprint: string): void {
449
+ try {
450
+ const db = BeadsDB.getInstance();
451
+ db.recordCompletionFingerprint(fingerprint, RECENT_COMPLETION_FINGERPRINT_TTL_MS);
452
+ db.sweepExpiredFingerprints();
453
+ } catch { /* best-effort; duplicate events are preferable to a crash */ }
454
+ }
455
+
456
+ function readEventTimestamp(value: unknown): number | null {
457
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
458
+ if (typeof value === 'string' && value.trim()) {
459
+ const numeric = Number(value);
460
+ if (Number.isFinite(numeric)) return numeric;
461
+ const parsed = Date.parse(value);
462
+ if (Number.isFinite(parsed)) return parsed;
463
+ }
464
+ return null;
465
+ }
466
+
467
+ function buildMeshCompletionFingerprint(args: {
468
+ meshId: string;
469
+ event: string;
470
+ sessionId: string;
471
+ providerType?: string;
472
+ providerSessionId?: string;
473
+ timestamp?: number | null;
474
+ finalSummary?: string;
475
+ /** When set, scopes the fingerprint to a specific coordinator daemon so
476
+ * two coordinators processing events from their respective workers don't
477
+ * suppress each other's completion events. */
478
+ coordinatorDaemonId?: string;
479
+ }): string {
480
+ const timestampPart = Number.isFinite(args.timestamp)
481
+ ? String(args.timestamp)
482
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
483
+ return [
484
+ args.meshId,
485
+ args.event,
486
+ args.sessionId,
487
+ args.providerType || '',
488
+ args.providerSessionId || '',
489
+ timestampPart,
490
+ args.coordinatorDaemonId || '',
491
+ ].join('::');
492
+ }
493
+
494
+ function isDuplicateMeshCompletionEvent(args: {
495
+ meshId: string;
496
+ event: string;
497
+ sessionId: string;
498
+ providerType?: string;
499
+ providerSessionId?: string;
500
+ timestamp?: number | null;
501
+ finalSummary?: string;
502
+ coordinatorDaemonId?: string;
503
+ }): boolean {
504
+ const fingerprint = buildMeshCompletionFingerprint(args);
505
+ if (!fingerprint) return false;
506
+ if (hasFingerprintSeen(fingerprint)) return true;
507
+ recordFingerprintSeen(fingerprint);
508
+ return false;
509
+ }
510
+
511
+ function isDuplicateMeshApprovalEvent(args: {
512
+ meshId: string;
513
+ sessionId: string;
514
+ providerType?: string;
515
+ timestamp?: number | null;
516
+ modalMessage?: string;
517
+ modalButtons?: unknown;
518
+ }): boolean {
519
+ const modalButtons = Array.isArray(args.modalButtons)
520
+ ? args.modalButtons.map(button => String(button).trim()).filter(Boolean)
521
+ : [];
522
+ const approvalIdentity = Number.isFinite(args.timestamp)
523
+ ? String(args.timestamp)
524
+ : JSON.stringify({ message: args.modalMessage || '', buttons: modalButtons });
525
+ if (!approvalIdentity || approvalIdentity === '{"message":"","buttons":[]}') return false;
526
+ const fingerprint = [
527
+ args.meshId,
528
+ 'agent:waiting_approval',
529
+ args.sessionId,
530
+ args.providerType || '',
531
+ approvalIdentity,
532
+ ].join('::');
533
+ if (hasFingerprintSeen(fingerprint)) return true;
534
+ recordFingerprintSeen(fingerprint);
535
+ return false;
536
+ }
537
+
538
+ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
539
+ const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
540
+ if (!fingerprint) return false;
541
+ if (hasFingerprintSeen(fingerprint)) return true;
542
+ recordFingerprintSeen(fingerprint);
543
+ return false;
544
+ }
545
+
546
+ function findRecentTerminalLedgerEvidence(args: {
547
+ meshId: string;
548
+ sessionId?: string;
549
+ nodeId?: string;
550
+ }): { id: string; kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
551
+ if (!args.sessionId && !args.nodeId) return null;
552
+ // Tail-limit: 200 entries gives a wide enough window to catch terminal events for active
553
+ // sessions while avoiding a full O(n) scan. If a terminal is older than 200 entries,
554
+ // the BeadsDB fingerprint dedup will still block duplicate processing downstream.
555
+ const entries = readLedgerEntries(args.meshId, { tail: 200 });
556
+ for (let i = entries.length - 1; i >= 0; i--) {
557
+ const entry = entries[i];
558
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
559
+ if (args.sessionId && entry.sessionId === args.sessionId) {
560
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
561
+ }
562
+ if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
563
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
564
+ }
565
+ }
566
+ return null;
567
+ }
568
+
569
+ // Returns true when a task_dispatched entry for the given session appears AFTER the terminal
570
+ // entry (identified by terminalId) in ledger order. Positional (append) order is used rather
571
+ // than timestamp comparison because both entries may share the same millisecond.
572
+ function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean {
573
+ // 200-entry window matches findRecentTerminalLedgerEvidence for consistency.
574
+ const entries = readLedgerEntries(meshId, { tail: 200 });
575
+ let pastTerminal = false;
576
+ for (const entry of entries) {
577
+ if (!pastTerminal) {
578
+ if (entry.id === terminalId) pastTerminal = true;
579
+ continue;
580
+ }
581
+ if (entry.kind === 'task_dispatched' && entry.sessionId === sessionId) return true;
582
+ }
583
+ return false;
584
+ }
585
+
586
+ function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: string): boolean {
587
+ // Some dispatch paths can persist task_dispatched before the direct-dispatch DB row is
588
+ // available. Recover routing from ledger order so coordinator self-targets still emit
589
+ // task_completed and pendingCoordinatorEvents.
590
+ const entries = readLedgerEntries(meshId, { tail: 200 });
591
+ for (let i = entries.length - 1; i >= 0; i--) {
592
+ const entry = entries[i];
593
+ if (entry.sessionId !== sessionId) continue;
594
+ if (entry.kind === 'task_completed' || entry.kind === 'task_failed' || entry.kind === 'task_stalled') {
595
+ return false;
596
+ }
597
+ if (entry.kind === 'task_dispatched' && entry.payload?.source === 'direct') {
598
+ return true;
599
+ }
600
+ }
601
+ return false;
602
+ }
603
+
604
+ function buildLongGeneratingCompletionReconciliation(args: {
605
+ meshId: string;
606
+ nodeId?: string;
607
+ nodeLabel: string;
608
+ metadataEvent: Record<string, unknown>;
609
+ sourceInstanceId?: string;
610
+ }): Record<string, unknown> | null {
611
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
612
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
613
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
614
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
615
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
616
+ const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
617
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
618
+ const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
619
+ const explicitCompletionEvidence = Boolean(
620
+ finalSummary
621
+ || workerResult
622
+ || completionDiagnostic?.finalAssistantPresent === true
623
+ || status === 'idle'
624
+ || status === 'ready'
625
+ || status === 'completed',
626
+ );
627
+ if (explicitCompletionEvidence) {
628
+ return {
629
+ ...args.metadataEvent,
630
+ targetSessionId: sessionId,
631
+ providerType,
632
+ providerSessionId,
633
+ finalSummary,
634
+ source: 'long_generating_reconciliation',
635
+ reconciledFromEvent: 'monitor:long_generating',
636
+ timestamp: args.metadataEvent.timestamp ?? Date.now(),
637
+ completionDiagnostic: {
638
+ ...(completionDiagnostic || {}),
639
+ reconciliationReason: 'provider_completion_evidence',
640
+ },
641
+ };
642
+ }
643
+
644
+ const terminal = findRecentTerminalLedgerEvidence({
645
+ meshId: args.meshId,
646
+ sessionId: sessionId || undefined,
647
+ nodeId: nodeId || undefined,
648
+ });
649
+ if (!terminal) return null;
650
+ return {
651
+ ...args.metadataEvent,
652
+ source: 'long_generating_terminal_ledger_suppression',
653
+ terminalLedgerKind: terminal.kind,
654
+ terminalLedgerAt: terminal.timestamp,
655
+ };
656
+ }
657
+
153
658
 
154
659
  export function tryAssignQueueTask(
155
660
  components: DaemonComponents,
@@ -180,7 +685,16 @@ export function tryAssignQueueTask(
180
685
  message: task.message,
181
686
  }).catch((e: any) => {
182
687
  LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
183
- updateTaskStatus(meshId, task.id, 'failed');
688
+ // Revert to pending so the task can be retried rather than permanently failing
689
+ updateTaskStatus(meshId, task.id, 'pending');
690
+ try {
691
+ appendLedgerEntry(meshId, {
692
+ kind: 'dispatch_failed' as any,
693
+ nodeId,
694
+ sessionId,
695
+ payload: { taskId: task.id, error: e?.message, retryable: true },
696
+ });
697
+ } catch { /* ledger write is best-effort */ }
184
698
  });
185
699
  return true;
186
700
  }
@@ -204,6 +718,13 @@ const autoLaunchInProgress = new Set<string>();
204
718
  const autoLaunchCooldownUntil = new Map<string, number>();
205
719
  const AUTO_LAUNCH_COOLDOWN_MS = 5_000;
206
720
 
721
+ function sweepExpiredCooldowns(): void {
722
+ const now = Date.now();
723
+ for (const [key, until] of autoLaunchCooldownUntil) {
724
+ if (now >= until) autoLaunchCooldownUntil.delete(key);
725
+ }
726
+ }
727
+
207
728
  function normalizeProviderPriority(policy: unknown): string[] {
208
729
  const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
209
730
  ? (policy as Record<string, unknown>).providerPriority
@@ -277,6 +798,10 @@ function nodeHasActiveAssignment(meshId: string, nodeId: string): boolean {
277
798
  return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedNodeId === nodeId);
278
799
  }
279
800
 
801
+ function sessionHasActiveAssignment(meshId: string, sessionId: string): boolean {
802
+ return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedSessionId === sessionId);
803
+ }
804
+
280
805
  function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
281
806
  return components.instanceManager.getByCategory('cli').filter((inst: any) => {
282
807
  const state = inst.getState();
@@ -406,12 +931,14 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
406
931
  const nodeId = readNonEmptyString(node?.id);
407
932
  if (!nodeId) continue;
408
933
  const launchKey = `${meshId}:${nodeId}`;
934
+ const now = Date.now();
409
935
  const cooldownUntil = autoLaunchCooldownUntil.get(launchKey) || 0;
936
+ if (cooldownUntil > 0 && now >= cooldownUntil) autoLaunchCooldownUntil.delete(launchKey);
410
937
  if (autoLaunchInProgress.has(launchKey)) {
411
938
  markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_in_progress', nodeId });
412
939
  continue;
413
940
  }
414
- if (Date.now() < cooldownUntil) {
941
+ if (now < cooldownUntil) {
415
942
  markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_cooldown', nodeId });
416
943
  continue;
417
944
  }
@@ -461,13 +988,13 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
461
988
  if (!launchResult?.success) {
462
989
  const reason = launchResult?.error || 'launch_cli_failed';
463
990
  markAutoLaunch(meshId, task.id, { status: 'failed', reason, nodeId, providerType: resolved.providerType });
464
- autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
991
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
465
992
  return false;
466
993
  }
467
994
  const sessionId = readNonEmptyString(launchResult.sessionId) || readNonEmptyString(launchResult.id) || readNonEmptyString(launchResult.runtimeSessionId);
468
995
  if (!sessionId) {
469
996
  markAutoLaunch(meshId, task.id, { status: 'failed', reason: 'launch_missing_session_id', nodeId, providerType: resolved.providerType });
470
- autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
997
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
471
998
  return false;
472
999
  }
473
1000
  markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId });
@@ -542,6 +1069,9 @@ function buildMeshSystemMessage(args: {
542
1069
  }): string {
543
1070
  const metadata = formatCompletionMetadata(args.metadataEvent);
544
1071
  if (args.event === 'agent:generating_completed') {
1072
+ if (args.metadataEvent.source === 'long_generating_reconciliation') {
1073
+ return `[System] ${args.nodeLabel} already has completion evidence${metadata}. The long-generating monitor reconciled the terminal handoff and marked the session complete; wait for the queued completion event/status refresh before doing any manual transcript check.`;
1074
+ }
545
1075
  return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path; use mesh_read_chat once to review its final progress, but do not poll repeatedly.`;
546
1076
  }
547
1077
  if (args.event === 'agent:waiting_approval') {
@@ -573,7 +1103,72 @@ function buildMeshSystemMessage(args: {
573
1103
  return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
574
1104
  }
575
1105
  if (args.event === 'monitor:long_generating') {
576
- return `[System] ${args.nodeLabel} has been generating for a long time${metadata}. Use mesh_read_chat once for a status check, but do not poll repeatedly.`;
1106
+ return `[System] ${args.nodeLabel} is still reported as generating after a long interval${metadata}. Wait for pendingCoordinatorEvents or a completion/status event; if the user explicitly asks for status, make one bounded status check and then wait again.`;
1107
+ }
1108
+ if (args.event === 'refine:accepted') {
1109
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1110
+ return `[System] Refinery accepted async job${jobId ? ` ${jobId}` : ''} for ${args.nodeLabel}. Completion/failure will be delivered as a terminal refine event; do not poll repeatedly.`;
1111
+ }
1112
+ if (args.event === 'refine:completed') {
1113
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1114
+ const result = readRecord(args.metadataEvent.result);
1115
+ const validationSummary = readRecord(result?.validationSummary);
1116
+ const patchEquivalence = readRecord(result?.patchEquivalence);
1117
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
1118
+ const validationStatus = readNonEmptyString(validationSummary?.status);
1119
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
1120
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
1121
+ const into = readNonEmptyString(result?.into);
1122
+ const branch = readNonEmptyString(result?.branch);
1123
+ const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
1124
+ const convergenceStatus = readNonEmptyString(finalConvergence?.status);
1125
+ const nextStep = readNonEmptyString(result?.nextStep)
1126
+ || readNonEmptyString(finalConvergence?.nextStep)
1127
+ || 'Continue from the updated mesh state.';
1128
+ const details = [
1129
+ jobId ? `job_id=${jobId}` : '',
1130
+ branch && into ? `${branch}→${into}` : '',
1131
+ validationStatus ? `validation=${validationStatus}` : '',
1132
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
1133
+ mergeStatus ? `merge=${mergeStatus}` : '',
1134
+ convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
1135
+ ].filter(Boolean).join('; ');
1136
+ return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
1137
+ }
1138
+ if (args.event === 'refine:failed') {
1139
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1140
+ const result = readRecord(args.metadataEvent.result);
1141
+ const validationSummary = readRecord(result?.validationSummary);
1142
+ const patchEquivalence = readRecord(result?.patchEquivalence);
1143
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
1144
+ const code = readNonEmptyString(result?.code);
1145
+ const error = readNonEmptyString(result?.error);
1146
+ const validationStatus = readNonEmptyString(validationSummary?.status);
1147
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
1148
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
1149
+ const mergeStatus = result?.merged === true
1150
+ ? 'merged'
1151
+ : finalConvergence?.merged === false
1152
+ ? 'not_merged'
1153
+ : '';
1154
+ const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
1155
+ || readNonEmptyString(finalConvergence?.status);
1156
+ const blockedReason = readNonEmptyString(result?.blockedReason);
1157
+ const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
1158
+ const details = [
1159
+ jobId ? `job_id=${jobId}` : '',
1160
+ code ? `code=${code}` : '',
1161
+ validationStatus ? `validation=${validationStatus}` : '',
1162
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
1163
+ mergeStatus ? `merge=${mergeStatus}` : '',
1164
+ convergenceStatus ? `convergence=${convergenceStatus}` : '',
1165
+ blockedReason ? `reason=${blockedReason}` : '',
1166
+ ].filter(Boolean).join('; ');
1167
+ const parts = [
1168
+ `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
1169
+ nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
1170
+ ];
1171
+ return parts.join('\n');
577
1172
  }
578
1173
  return '';
579
1174
  }
@@ -588,6 +1183,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
588
1183
  }) {
589
1184
  const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
590
1185
  const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1186
+
1187
+ // Resolve coordinator ownership early — used in fingerprinting and coordinator routing.
1188
+ const sourceSession = args.sourceInstanceId
1189
+ ? components.instanceManager.getInstance(args.sourceInstanceId)
1190
+ : undefined;
1191
+ const workerCoordinatorDaemonId = readNonEmptyString(
1192
+ (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
1193
+ );
1194
+ const localDaemonId = readNonEmptyString(loadConfig().machineId);
591
1195
  const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
592
1196
  event: args.event,
593
1197
  meshId: args.meshId,
@@ -603,6 +1207,97 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
603
1207
  return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
604
1208
  }
605
1209
 
1210
+ if (args.event === 'monitor:long_generating') {
1211
+ const reconciledCompletion = buildLongGeneratingCompletionReconciliation({
1212
+ meshId: args.meshId,
1213
+ nodeId: args.nodeId,
1214
+ nodeLabel: args.nodeLabel,
1215
+ metadataEvent: args.metadataEvent,
1216
+ sourceInstanceId: args.sourceInstanceId,
1217
+ });
1218
+ if (reconciledCompletion?.source === 'long_generating_reconciliation') {
1219
+ LOG.info('MeshEvents', `Reconciled long-generating monitor to completion for session ${eventSessionId || '(unknown session)'}`);
1220
+ return injectMeshSystemMessage(components, {
1221
+ ...args,
1222
+ event: 'agent:generating_completed',
1223
+ metadataEvent: reconciledCompletion,
1224
+ });
1225
+ }
1226
+ if (reconciledCompletion?.source === 'long_generating_terminal_ledger_suppression') {
1227
+ LOG.info('MeshEvents', `Suppressed long-generating monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
1228
+ return {
1229
+ success: true,
1230
+ forwarded: 0,
1231
+ suppressed: true,
1232
+ terminalLedgerEvidence: true,
1233
+ terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
1234
+ };
1235
+ }
1236
+ }
1237
+
1238
+ if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
1239
+ LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
1240
+ return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
1241
+ }
1242
+
1243
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
1244
+ if (args.event === 'agent:waiting_approval' && eventSessionId) {
1245
+ const duplicateApproval = isDuplicateMeshApprovalEvent({
1246
+ meshId: args.meshId,
1247
+ sessionId: eventSessionId,
1248
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1249
+ timestamp: eventTimestamp,
1250
+ modalMessage: readNonEmptyString(args.metadataEvent.modalMessage) || undefined,
1251
+ modalButtons: args.metadataEvent.modalButtons,
1252
+ });
1253
+ if (duplicateApproval) {
1254
+ LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
1255
+ return { success: true, forwarded: 0, suppressed: true, duplicateApproval: true };
1256
+ }
1257
+ }
1258
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
1259
+ const terminal = findRecentTerminalLedgerEvidence({
1260
+ meshId: args.meshId,
1261
+ sessionId: eventSessionId,
1262
+ nodeId: eventNodeId || undefined,
1263
+ });
1264
+ if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
1265
+ // If a new task_dispatched was recorded for this session after the prior terminal,
1266
+ // this completion belongs to the new task — never suppress it as a duplicate.
1267
+ const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
1268
+ if (!newDispatchAfterTerminal) {
1269
+ const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
1270
+ const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
1271
+ const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
1272
+ const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
1273
+ if (
1274
+ (terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
1275
+ || (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
1276
+ || args.metadataEvent.source === 'long_generating_reconciliation'
1277
+ ) {
1278
+ LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
1279
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
1280
+ }
1281
+ }
1282
+ }
1283
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
1284
+ meshId: args.meshId,
1285
+ event: args.event,
1286
+ sessionId: eventSessionId,
1287
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1288
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
1289
+ timestamp: eventTimestamp,
1290
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1291
+ // Scope dedup to the coordinator daemon so two coordinators for the same mesh
1292
+ // don't suppress each other's completion events via shared fingerprint table.
1293
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
1294
+ });
1295
+ if (duplicateCompletion) {
1296
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
1297
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
1298
+ }
1299
+ }
1300
+
606
1301
  // ── Task Queue & Ledger ──
607
1302
  let completedTaskForLedger: { id?: string } | null = null;
608
1303
  if (args.event === 'agent:generating_completed') {
@@ -611,24 +1306,33 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
611
1306
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
612
1307
 
613
1308
  if (sessionId) {
614
- const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
1309
+ const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
1310
+ occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
1311
+ });
615
1312
  completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
1313
+ updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
1314
+ setImmediate(() => cleanupTerminalDirectDispatches());
616
1315
  if (nodeId && providerType) {
617
- // Short delay to allow completion event to propagate before pulling next
618
- setTimeout(() => {
1316
+ setImmediate(() => {
619
1317
  tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
620
- }, 500);
1318
+ });
621
1319
  }
622
1320
  }
623
1321
  } else if (args.event === 'agent:ready') {
624
1322
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
625
1323
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
626
1324
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
627
- const completedTask = sessionId
1325
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1326
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1327
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1328
+ const hasCompletionEvidence = !!finalSummary || !!workerResult;
1329
+ const completedTask = sessionId && hasCompletionEvidence
628
1330
  ? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
629
1331
  : null;
630
- if (completedTask) {
1332
+ if (completedTask && sessionId) {
631
1333
  completedTaskForLedger = { id: completedTask.id };
1334
+ updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
1335
+ setImmediate(() => cleanupTerminalDirectDispatches());
632
1336
  try {
633
1337
  appendLedgerEntry(args.meshId, {
634
1338
  kind: 'task_completed',
@@ -640,15 +1344,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
640
1344
  nodeLabel: args.nodeLabel,
641
1345
  taskId: completedTask.id,
642
1346
  completedViaReady: true,
643
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
644
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1347
+ providerSessionId,
1348
+ finalSummary,
1349
+ workerResult,
645
1350
  evidence: buildTaskCompletionEvidence({
646
1351
  event: 'agent:ready',
647
1352
  nodeId,
648
1353
  sessionId,
649
1354
  providerType: providerType || undefined,
650
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
651
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1355
+ providerSessionId,
1356
+ finalSummary,
1357
+ workerResult,
652
1358
  }),
653
1359
  },
654
1360
  });
@@ -658,13 +1364,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
658
1364
  }
659
1365
 
660
1366
  if (sessionId && nodeId && providerType) {
661
- remoteIdleSessions.set(`${nodeId}:${sessionId}`, { nodeId, sessionId, providerType });
662
- setTimeout(() => {
1367
+ sweepExpiredRemoteIdleSessions();
1368
+ remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
1369
+ nodeId, sessionId, providerType,
1370
+ expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
1371
+ });
1372
+ setImmediate(() => {
663
1373
  const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
664
- if (assigned) {
665
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
666
- }
667
- }, 500);
1374
+ if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
1375
+ });
668
1376
  }
669
1377
  } else if (args.event === 'agent:generating_started') {
670
1378
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
@@ -672,6 +1380,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
672
1380
  if (sessionId && nodeId) {
673
1381
  remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
674
1382
  }
1383
+ if (sessionId) {
1384
+ // Mark direct dispatch as acknowledged — the session started generating.
1385
+ updateDirectDispatchStatus(args.meshId, sessionId, 'acked');
1386
+ }
675
1387
  } else if (args.event === 'agent:stopped') {
676
1388
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
677
1389
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
@@ -679,7 +1391,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
679
1391
  remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
680
1392
  }
681
1393
  if (sessionId) {
682
- updateSessionTaskStatus(args.meshId, sessionId, 'failed');
1394
+ const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
1395
+ completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
1396
+ updateDirectDispatchStatus(args.meshId, sessionId, 'failed');
683
1397
  }
684
1398
  }
685
1399
 
@@ -689,14 +1403,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
689
1403
  const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
690
1404
  const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
691
1405
  const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
1406
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1407
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1408
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
692
1409
  const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
693
1410
  ? buildTaskCompletionEvidence({
694
1411
  event: 'agent:generating_completed',
695
1412
  nodeId: ledgerNodeId,
696
1413
  sessionId: ledgerSessionId,
697
1414
  providerType: ledgerProviderType,
698
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
699
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1415
+ providerSessionId,
1416
+ finalSummary,
1417
+ workerResult,
700
1418
  })
701
1419
  : undefined;
702
1420
  appendLedgerEntry(args.meshId, {
@@ -708,8 +1426,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
708
1426
  event: args.event,
709
1427
  nodeLabel: args.nodeLabel,
710
1428
  taskId: completedTaskForLedger?.id || undefined,
711
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
712
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1429
+ providerSessionId,
1430
+ finalSummary,
1431
+ workerResult,
1432
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
1433
+ ? args.metadataEvent.completionDiagnostic
1434
+ : undefined,
713
1435
  evidence: completionEvidence,
714
1436
  },
715
1437
  });
@@ -782,15 +1504,33 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
782
1504
  }
783
1505
  }
784
1506
 
1507
+ const messageText = buildMeshSystemMessage({
1508
+ event: args.event,
1509
+ nodeLabel: args.nodeLabel,
1510
+ metadataEvent: args.metadataEvent,
1511
+ recoveryContext,
1512
+ });
1513
+ if (!messageText) return { success: false, error: 'unsupported mesh event' };
1514
+
785
1515
  const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
786
1516
  const instState = inst.getState();
787
1517
  if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
788
1518
  if (args.sourceInstanceId && instState.instanceId === args.sourceInstanceId) return false;
1519
+ // If the worker knows which coordinator daemon launched it, only route to coordinators
1520
+ // on that specific daemon. This prevents cross-contamination when multiple coordinator
1521
+ // sessions run simultaneously for the same mesh on different daemons.
1522
+ if (workerCoordinatorDaemonId && localDaemonId && workerCoordinatorDaemonId !== localDaemonId) return false;
789
1523
  return true;
790
1524
  });
791
1525
 
1526
+ // Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
1527
+ // synchronously; only buffer them for MCP when no CLI coordinator is present.
1528
+ // Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
1529
+ // receive them regardless of whether a live CLI coordinator session is active.
1530
+ const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
1531
+
792
1532
  if (coordinatorInstances.length === 0) {
793
- // No CLI coordinator session found — buffer for MCP-based coordinators.
1533
+ // No local CLI coordinator — buffer for MCP-based coordinator on the target daemon.
794
1534
  if (queuePendingMeshCoordinatorEvent({
795
1535
  event: args.event,
796
1536
  meshId: args.meshId,
@@ -801,20 +1541,69 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
801
1541
  ...args.metadataEvent,
802
1542
  ...(recoveryContext ? { recoveryContext } : {}),
803
1543
  },
1544
+ coordinatorMessage: messageText,
804
1545
  queuedAt: Date.now(),
1546
+ // Scope to the coordinator daemon that launched this worker so drain
1547
+ // by other coordinators on the same daemon doesn't consume this event.
1548
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
805
1549
  })) {
806
- LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1550
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''})`);
807
1551
  }
808
1552
  return { success: true, forwarded: 0 };
809
1553
  }
810
1554
 
811
- const messageText = buildMeshSystemMessage({
812
- event: args.event,
813
- nodeLabel: args.nodeLabel,
814
- metadataEvent: args.metadataEvent,
815
- recoveryContext,
1555
+ // CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
1556
+ // that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
1557
+ // forwarded directly only — they must not accumulate in the pending queue when a live
1558
+ // coordinator already received them.
1559
+ //
1560
+ // Exception: if ALL live CLI coordinator instances are in a generating/active state
1561
+ // when a refine terminal event fires (e.g. a Codex CLI coordinator that triggered an
1562
+ // async refine job and is still in the generating turn that sent it), the coordinator
1563
+ // cannot immediately receive send_message input. Buffer the event to the pending queue
1564
+ // so it is available via get_pending_mesh_events when the coordinator returns to idle.
1565
+ // Critically: do NOT attempt send_message injection into a generating PTY coordinator for
1566
+ // terminal refine events — injecting text into an active PTY can corrupt the input stream
1567
+ // and leave the coordinator stuck in generating state, unable to process the refine result.
1568
+ const allCoordinatorsGenerating = isRefineTerminalEvent && coordinatorInstances.every((inst) => {
1569
+ const s = inst.getState();
1570
+ const status = readNonEmptyString(s.status).toLowerCase();
1571
+ const activeChatStatus = readNonEmptyString(s.activeChat?.status).toLowerCase();
1572
+ return status === 'generating' || status === 'streaming' || status === 'long_generating'
1573
+ || activeChatStatus === 'generating' || activeChatStatus === 'streaming';
816
1574
  });
817
- if (!messageText) return { success: false, error: 'unsupported mesh event' };
1575
+
1576
+ if (!isRefineTerminalEvent || allCoordinatorsGenerating) {
1577
+ if (queuePendingMeshCoordinatorEvent({
1578
+ event: args.event,
1579
+ meshId: args.meshId,
1580
+ nodeLabel: args.nodeLabel,
1581
+ nodeId: args.nodeId || undefined,
1582
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
1583
+ metadataEvent: {
1584
+ ...args.metadataEvent,
1585
+ ...(recoveryContext ? { recoveryContext } : {}),
1586
+ },
1587
+ coordinatorMessage: messageText,
1588
+ queuedAt: Date.now(),
1589
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
1590
+ })) {
1591
+ if (allCoordinatorsGenerating) {
1592
+ LOG.info('MeshEvents', `Queued ${args.event} for generating CLI coordinator (mesh ${args.meshId}) — will be delivered via get_pending_mesh_events when coordinator returns to idle`);
1593
+ } else {
1594
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1595
+ }
1596
+ }
1597
+ }
1598
+
1599
+ // When all CLI coordinators are actively generating and a terminal refine event fires,
1600
+ // skip send_message injection entirely. The event is already buffered to the pending queue.
1601
+ // Injecting into a generating PTY coordinator can corrupt its input stream and cause it to
1602
+ // remain stuck in generating state, never processing the refine result.
1603
+ // The coordinator will drain pending events via get_pending_mesh_events on its next idle cycle.
1604
+ if (allCoordinatorsGenerating) {
1605
+ return { success: true, forwarded: 0, bufferedForGeneratingCoordinator: true };
1606
+ }
818
1607
 
819
1608
  for (const coord of coordinatorInstances) {
820
1609
  const coordState = coord.getState();
@@ -845,6 +1634,19 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
845
1634
  providerType: readNonEmptyString(payload.providerType),
846
1635
  providerSessionId: readNonEmptyString(payload.providerSessionId),
847
1636
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1637
+ jobId: readNonEmptyString(payload.jobId),
1638
+ interactionId: readNonEmptyString(payload.interactionId),
1639
+ status: readNonEmptyString(payload.status),
1640
+ targetDaemonId: readNonEmptyString(payload.targetDaemonId),
1641
+ startedAt: readNonEmptyString(payload.startedAt),
1642
+ completedAt: readNonEmptyString(payload.completedAt),
1643
+ retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1644
+ ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1645
+ ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
1646
+ ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
1647
+ ...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
1648
+ ...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
1649
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
848
1650
  intentional: payload.intentional === true,
849
1651
  intentionalStop: payload.intentionalStop === true,
850
1652
  operatorCleanup: payload.operatorCleanup === true,
@@ -872,21 +1674,37 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
872
1674
  if (!workspace) return;
873
1675
  const settings = state.settings && typeof state.settings === 'object' ? state.settings as Record<string, unknown> : {};
874
1676
 
875
- // Coordinator sessions must never inject events into themselves.
876
- // A coordinator instance carries meshCoordinatorFor but not meshNodeFor/launchedByCoordinator.
877
- if (readNonEmptyString(settings.meshCoordinatorFor)) return;
1677
+ // Coordinator sessions normally must not inject events into themselves, but a
1678
+ // coordinator session can also be the direct-dispatch target of mesh_send_task
1679
+ // issued by another coordinator (e.g. a remote orchestrator delegated a task to
1680
+ // this local coordinator). In that case the completion event must still flow into
1681
+ // injectMeshSystemMessage so the ledger records task_completed and the *other*
1682
+ // coordinator's pendingCoordinatorEvents queue is populated. Skip only when this
1683
+ // session is purely a coordinator with no in-flight direct dispatch waiting on it.
1684
+ const coordinatorMeshId = readNonEmptyString(settings.meshCoordinatorFor);
1685
+ let meshIdFromDirectDispatch = '';
1686
+ if (coordinatorMeshId) {
1687
+ try {
1688
+ const activeDispatches = getActiveDirectDispatches(coordinatorMeshId);
1689
+ if (activeDispatches.some(d => d.sessionId === instanceId) || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId)) {
1690
+ meshIdFromDirectDispatch = coordinatorMeshId;
1691
+ }
1692
+ } catch { /* best-effort */ }
1693
+ if (!meshIdFromDirectDispatch) return;
1694
+ }
878
1695
 
879
- const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor);
1696
+ const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor) || meshIdFromDirectDispatch;
880
1697
 
881
1698
  // Only forward events for sessions that were explicitly launched as mesh-node delegates
882
- // (meshNodeFor set by mesh_launch_session) or that carry the launchedByCoordinator flag.
1699
+ // (meshNodeFor set by mesh_launch_session), carry the launchedByCoordinator flag, or
1700
+ // have an active direct-dispatch entry (mesh_send_task to a pre-existing session).
883
1701
  // Do NOT fall back to workspace-based mesh lookup: that would pick up coordinator sessions
884
1702
  // and any other CLI session that happens to share the same workspace, causing spurious
885
1703
  // system-message injection into the coordinator's own conversation.
886
1704
  const isMeshDelegate = Boolean(meshIdFromRuntime || settings.launchedByCoordinator);
887
1705
  if (!isMeshDelegate) return;
888
1706
 
889
- const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getMeshByRepo(workspace);
1707
+ const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getCachedMeshByWorkspace(workspace);
890
1708
  const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
891
1709
  if (!meshId) return;
892
1710