@adhdev/daemon-core 0.9.82-rc.18 → 0.9.82-rc.181

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 (225) 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 +8 -0
  16. package/dist/config/config.d.ts +5 -0
  17. package/dist/config/mesh-config.d.ts +72 -1
  18. package/dist/git/git-commands.d.ts +5 -1
  19. package/dist/index.d.ts +35 -8
  20. package/dist/index.js +20754 -5387
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +20567 -5270
  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 +97 -5
  34. package/dist/mesh/mesh-fast-forward.d.ts +41 -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 +54 -6
  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 +35 -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 +25 -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-manager.d.ts +12 -0
  53. package/dist/providers/provider-instance.d.ts +12 -0
  54. package/dist/providers/provider-loader.d.ts +26 -4
  55. package/dist/providers/provider-trust.d.ts +31 -0
  56. package/dist/providers/read-chat-contract.d.ts +29 -0
  57. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  58. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  59. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  60. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  61. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  62. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  63. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  64. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  65. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  66. package/dist/providers/sdk/v1/index.d.ts +30 -0
  67. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  68. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  69. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  70. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  71. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  72. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  73. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  74. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  75. package/dist/providers/spec/adapter.d.ts +56 -0
  76. package/dist/providers/spec/cli-adapter.d.ts +97 -0
  77. package/dist/providers/spec/driver.d.ts +159 -0
  78. package/dist/providers/spec/evaluator.d.ts +47 -0
  79. package/dist/providers/spec/loader.d.ts +14 -0
  80. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  81. package/dist/providers/spec/route.d.ts +4 -0
  82. package/dist/providers/spec/schema.gen.d.ts +511 -0
  83. package/dist/providers/spec/types.d.ts +212 -0
  84. package/dist/providers/transcript-v2.d.ts +176 -0
  85. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  86. package/dist/repo-mesh-types.d.ts +102 -1
  87. package/dist/sessions/registry.d.ts +3 -0
  88. package/dist/shared-types.d.ts +42 -1
  89. package/dist/status/reporter.d.ts +2 -0
  90. package/dist/status/snapshot.d.ts +1 -0
  91. package/dist/types.d.ts +5 -0
  92. package/package.json +7 -2
  93. package/src/agent-stream/poller.ts +2 -3
  94. package/src/boot/daemon-lifecycle.ts +17 -10
  95. package/src/boot/process-hardening.ts +89 -0
  96. package/src/chat/source-machine.ts +534 -0
  97. package/src/chat/source-resolver.ts +0 -0
  98. package/src/chat/subscription-updates.ts +14 -1
  99. package/src/cli-adapter-types.d.ts +1 -0
  100. package/src/cli-adapter-types.ts +21 -2
  101. package/src/cli-adapters/cli-script-runner.ts +421 -0
  102. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  103. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  104. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  105. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  106. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  107. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  108. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  109. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  110. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  111. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  112. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  113. package/src/commands/chat-commands.ts +1597 -60
  114. package/src/commands/cli-manager.ts +230 -3
  115. package/src/commands/handler.ts +809 -2
  116. package/src/commands/mesh-coordinator.ts +231 -127
  117. package/src/commands/router.ts +3495 -502
  118. package/src/config/chat-history.ts +91 -24
  119. package/src/config/config.ts +12 -0
  120. package/src/config/mesh-config.ts +280 -2
  121. package/src/config/recent-activity.ts +8 -2
  122. package/src/daemon/dev-cli-debug.ts +10 -1
  123. package/src/detection/ide-detector.ts +26 -16
  124. package/src/git/git-commands.ts +17 -5
  125. package/src/git/git-worktree.ts +8 -1
  126. package/src/index.ts +113 -7
  127. package/src/installer.d.ts +1 -1
  128. package/src/installer.ts +8 -6
  129. package/src/ipc/local-ipc-server.ts +278 -0
  130. package/src/launch.d.ts +1 -1
  131. package/src/launch.ts +37 -28
  132. package/src/logging/async-batch-writer.ts +55 -0
  133. package/src/logging/logger.ts +2 -1
  134. package/src/mesh/beads-db.ts +486 -0
  135. package/src/mesh/contracts.ts +329 -0
  136. package/src/mesh/coordinator-prompt.ts +204 -30
  137. package/src/mesh/coordinator-registry.ts +121 -0
  138. package/src/mesh/mesh-active-work.ts +437 -0
  139. package/src/mesh/mesh-events.ts +963 -67
  140. package/src/mesh/mesh-fast-forward.ts +438 -0
  141. package/src/mesh/mesh-host-ownership.ts +73 -0
  142. package/src/mesh/mesh-ledger.ts +457 -104
  143. package/src/mesh/mesh-refine-status.ts +144 -0
  144. package/src/mesh/mesh-work-queue.ts +263 -159
  145. package/src/mesh/preview-freshness.ts +118 -0
  146. package/src/mesh/refine-config.ts +366 -0
  147. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  148. package/src/providers/approval-utils.d.ts +4 -0
  149. package/src/providers/approval-utils.ts +47 -5
  150. package/src/providers/chat-message-normalization.ts +4 -11
  151. package/src/providers/cli-provider-instance.ts +651 -57
  152. package/src/providers/contracts.ts +105 -5
  153. package/src/providers/external-sources.ts +218 -0
  154. package/src/providers/ide-provider-instance.ts +19 -5
  155. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  156. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  157. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  158. package/src/providers/native-history/dispatcher.ts +231 -0
  159. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  160. package/src/providers/native-history/index.ts +30 -0
  161. package/src/providers/provider-instance-manager.ts +30 -0
  162. package/src/providers/provider-instance.ts +9 -0
  163. package/src/providers/provider-loader.ts +585 -50
  164. package/src/providers/provider-schema.ts +31 -13
  165. package/src/providers/provider-trust.ts +114 -0
  166. package/src/providers/read-chat-contract.ts +76 -16
  167. package/src/providers/sdk/README.md +49 -0
  168. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  169. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  170. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  171. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  172. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  173. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  174. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  175. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  176. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  177. package/src/providers/sdk/v1/index.ts +152 -0
  178. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  179. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  180. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  181. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
  182. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  183. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  185. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  186. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  187. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  188. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  189. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  190. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  191. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  192. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  193. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  194. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  195. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  196. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  197. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  198. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  199. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  200. package/src/providers/sdk/v1/validators/index.ts +19 -0
  201. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  202. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  203. package/src/providers/spec/adapter.ts +168 -0
  204. package/src/providers/spec/cli-adapter.ts +507 -0
  205. package/src/providers/spec/driver.ts +540 -0
  206. package/src/providers/spec/evaluator.ts +298 -0
  207. package/src/providers/spec/loader.ts +130 -0
  208. package/src/providers/spec/native-history-executor.ts +706 -0
  209. package/src/providers/spec/route.ts +51 -0
  210. package/src/providers/spec/schema.gen.ts +511 -0
  211. package/src/providers/spec/schema.json +211 -0
  212. package/src/providers/spec/types.ts +231 -0
  213. package/src/providers/transcript-v2.ts +567 -0
  214. package/src/providers/types/interactive-prompt.ts +396 -0
  215. package/src/providers/version-archive.ts +38 -20
  216. package/src/repo-mesh-types.ts +112 -1
  217. package/src/sessions/registry.ts +3 -0
  218. package/src/shared-types.ts +45 -1
  219. package/src/status/builders.ts +24 -6
  220. package/src/status/reporter.ts +15 -0
  221. package/src/status/snapshot.ts +84 -25
  222. package/src/system/host-memory.ts +29 -12
  223. package/src/types.ts +5 -0
  224. package/dist/mesh/mesh-sync.d.ts +0 -53
  225. package/src/mesh/mesh-sync.ts +0 -111
@@ -1,31 +1,76 @@
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 { buildMeshNodeCapabilityTags, claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches } from './mesh-work-queue.js';
11
+ import { BeadsDB } from './beads-db.js';
12
+ import { fastForwardMeshNode } from './mesh-fast-forward.js';
9
13
 
10
14
  // ---------------------------------------------------------------------------
11
15
  // Remote Node Idle Session Tracking
12
16
  // ---------------------------------------------------------------------------
13
- // Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
14
- // can assign tasks to them.
17
+ // Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
18
+ // can assign tasks to them. Each entry carries an expiresAt timestamp;
19
+ // entries are swept on insertion to prevent unbounded growth.
15
20
  // ---------------------------------------------------------------------------
16
21
  interface RemoteIdleSession {
17
22
  nodeId: string;
18
23
  sessionId: string;
19
24
  providerType: string;
25
+ expiresAt: number;
20
26
  }
27
+ const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
21
28
  const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
22
29
 
23
30
  // ---------------------------------------------------------------------------
24
- // MCP coordinator pending-event queue
31
+ // Workspace-to-mesh lookup cache
32
+ // ---------------------------------------------------------------------------
33
+ // getMeshByRepo is called on every coordinator event for sessions without
34
+ // meshNodeFor. Cache results for 5 seconds to avoid repeated config reads.
35
+ const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
36
+ const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
37
+ const IDLE_AUTO_FAST_FORWARD_THROTTLE_MS = 30 * 60 * 1000;
38
+ const idleAutoFastForwardLastAttempt = new Map<string, number>();
39
+
40
+ function getCachedMeshByWorkspace(workspace: string): any {
41
+ const now = Date.now();
42
+ const cached = meshByWorkspaceCache.get(workspace);
43
+ if (cached && now - cached.cachedAt < MESH_WORKSPACE_CACHE_TTL_MS) return cached.mesh;
44
+ const mesh = getMeshByRepo(workspace);
45
+ meshByWorkspaceCache.set(workspace, { mesh, cachedAt: now });
46
+ return mesh;
47
+ }
48
+
49
+ function readWorkerResultMetadata(event: Record<string, unknown>): Record<string, unknown> | undefined {
50
+ return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
51
+ }
52
+
53
+ export function __resetIdleAutoFastForwardForTests(): void {
54
+ idleAutoFastForwardLastAttempt.clear();
55
+ }
56
+
57
+ function sweepExpiredRemoteIdleSessions(): void {
58
+ const now = Date.now();
59
+ for (const [key, session] of remoteIdleSessions) {
60
+ if (session.expiresAt <= now) remoteIdleSessions.delete(key);
61
+ }
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
25
66
  // ---------------------------------------------------------------------------
26
67
  // 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.
68
+ // the coordinator is Claude Code running via MCP), we persist the event to a
69
+ // per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
70
+ // is removed; the file is drained atomically on each get_pending_mesh_events
71
+ // call and limited to 100 KB to prevent runaway growth.
72
+ //
73
+ // File: <ledgerDir>/<meshId>.pending-events.jsonl
29
74
  // ---------------------------------------------------------------------------
30
75
 
31
76
  export interface PendingMeshCoordinatorEvent {
@@ -35,39 +80,272 @@ export interface PendingMeshCoordinatorEvent {
35
80
  nodeId?: string;
36
81
  workspace?: string;
37
82
  metadataEvent: Record<string, unknown>;
83
+ coordinatorMessage?: string;
38
84
  queuedAt: number;
85
+ /**
86
+ * When set, this event is intended for a specific coordinator daemon.
87
+ * Coordinators on other daemons should ignore it during drain.
88
+ * Absent on legacy events — treated as broadcast to any coordinator.
89
+ */
90
+ targetCoordinatorDaemonId?: string;
91
+ }
92
+
93
+ const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
94
+
95
+ function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
96
+ const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
97
+ const result = readRecord(metadata.result);
98
+ const refineJob = readRecord(result?.refineJob);
99
+ return readNonEmptyString(metadata.jobId) || readNonEmptyString(refineJob?.jobId);
39
100
  }
40
101
 
41
- const MAX_PENDING_EVENTS = 50;
42
- const pendingMeshCoordinatorEvents: PendingMeshCoordinatorEvent[] = [];
102
+ function buildRefineTerminalEventFingerprint(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): string {
103
+ const jobId = readRefineJobId({ metadataEvent });
104
+ return jobId && REFINE_TERMINAL_EVENTS.has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
105
+ }
106
+
107
+ function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
108
+ if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
109
+ const jobId = readRefineJobId(event);
110
+ if (!jobId) return false;
111
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) =>
112
+ pending.event === event.event && readRefineJobId(pending) === jobId,
113
+ );
114
+ }
115
+
116
+ function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
117
+ const metadata = readRecord(event.metadataEvent) || {};
118
+ const sessionId = resolveEventSessionId(metadata);
119
+ const providerSessionId = readNonEmptyString(metadata.providerSessionId);
120
+ const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
121
+ const jobId = readRefineJobId(event);
122
+ const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
123
+ return [
124
+ event.meshId,
125
+ event.event,
126
+ event.nodeId || '',
127
+ sessionId || '',
128
+ providerSessionId || '',
129
+ taskId || '',
130
+ jobId || '',
131
+ timestamp || '',
132
+ ].join('::');
133
+ }
134
+
135
+ function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
136
+ const fingerprint = buildPendingEventFingerprint(event);
137
+ if (!fingerprint.trim()) return false;
138
+ return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
139
+ }
140
+
141
+ function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): string {
142
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
143
+ if (coordinatorDaemonId) {
144
+ const safeDaemon = coordinatorDaemonId.replace(/[^a-zA-Z0-9_-]/g, '_');
145
+ return join(getLedgerDir(), `${safe}-${safeDaemon}.pending-events.jsonl`);
146
+ }
147
+ return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
148
+ }
149
+
150
+ function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
151
+ if (!meshId) return [];
152
+ // Read coordinator-scoped file first; fall back to legacy shared file.
153
+ const paths = coordinatorDaemonId
154
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
155
+ : [getPendingEventsPath(meshId)];
156
+ const events: PendingMeshCoordinatorEvent[] = [];
157
+ for (const path of paths) {
158
+ if (!existsSync(path)) continue;
159
+ try {
160
+ const raw = readFileSync(path, 'utf-8');
161
+ const parsed = raw.split('\n').filter(Boolean).flatMap(line => {
162
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
163
+ });
164
+ // If reading the shared file, filter to events that target this coordinator or are unscoped.
165
+ const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
166
+ ? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
167
+ : parsed;
168
+ events.push(...filtered);
169
+ } catch { /* skip unreadable files */ }
170
+ }
171
+ return events;
172
+ }
173
+
174
+ function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
175
+ const acceptedJobIds = new Set(
176
+ pending
177
+ .filter(event => event.event === 'refine:accepted')
178
+ .map(event => readRefineJobId(event))
179
+ .filter(Boolean),
180
+ );
181
+ if (acceptedJobIds.size === 0) return [];
182
+ const existingTerminalJobIds = new Set(
183
+ pending
184
+ .filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
185
+ .map(event => `${event.event}:${readRefineJobId(event)}`)
186
+ .filter(value => !value.endsWith(':')),
187
+ );
188
+ const backfilled: PendingMeshCoordinatorEvent[] = [];
189
+ const entries = readLedgerEntries(meshId, { tail: 200 });
190
+ for (let i = entries.length - 1; i >= 0; i--) {
191
+ const entry = entries[i];
192
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
193
+ const payload = readRecord(entry.payload);
194
+ if (payload?.source !== 'refine_mesh_node_async_job') continue;
195
+ const refineJob = readRecord(payload.refineJob);
196
+ const jobId = readNonEmptyString(refineJob?.jobId);
197
+ if (!jobId || !acceptedJobIds.has(jobId)) continue;
198
+ const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
199
+ if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
200
+ existingTerminalJobIds.add(`${eventName}:${jobId}`);
201
+ const result = readRecord(payload.result);
202
+ const metadataEvent = {
203
+ source: 'refine_mesh_node_async_job',
204
+ jobId,
205
+ interactionId: readNonEmptyString(refineJob?.interactionId),
206
+ meshId,
207
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
208
+ targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
209
+ workspace: readNonEmptyString(refineJob?.workspace),
210
+ status: eventName === 'refine:completed' ? 'completed' : 'failed',
211
+ startedAt: readNonEmptyString(refineJob?.startedAt),
212
+ completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
213
+ retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
214
+ ...(result ? { result } : {}),
215
+ };
216
+ backfilled.push({
217
+ event: eventName,
218
+ meshId,
219
+ nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
220
+ nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
221
+ workspace: readNonEmptyString(refineJob?.workspace),
222
+ metadataEvent,
223
+ coordinatorMessage: buildMeshSystemMessage({
224
+ event: eventName,
225
+ nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
226
+ metadataEvent,
227
+ }),
228
+ queuedAt: Date.now(),
229
+ });
230
+ }
231
+ return backfilled.reverse();
232
+ }
233
+
234
+ function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
235
+ const backfilled = refineTerminalEventFromLedger(meshId, events);
236
+ if (backfilled.length === 0) return events;
237
+ const terminalJobIds = new Set(backfilled.map(event => readRefineJobId(event)).filter(Boolean));
238
+ return [
239
+ ...events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event)))),
240
+ ...backfilled,
241
+ ];
242
+ }
243
+
244
+ const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
245
+ const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
246
+
247
+ function trimPendingEventsIfNeeded(path: string): void {
248
+ try {
249
+ if (!existsSync(path)) return;
250
+ if (statSync(path).size <= MAX_PENDING_EVENTS_BYTES) return;
251
+ const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
252
+ if (lines.length <= MAX_PENDING_EVENTS_KEEP) return;
253
+ writeFileSync(path, lines.slice(-MAX_PENDING_EVENTS_KEEP).join('\n') + '\n', 'utf-8');
254
+ } catch { /* best-effort; if trim fails, append still proceeds */ }
255
+ }
43
256
 
44
257
  export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
45
- if (pendingMeshCoordinatorEvents.length >= MAX_PENDING_EVENTS) {
258
+ try {
259
+ if (hasPendingRefineTerminalEventDuplicate(event)) {
260
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
261
+ return true;
262
+ }
263
+ if (hasPendingCoordinatorEventDuplicate(event)) {
264
+ LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
265
+ return true;
266
+ }
267
+ // Write to the coordinator-scoped file when the target coordinator is known;
268
+ // fall back to the shared file for legacy/unscoped events.
269
+ const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
270
+ trimPendingEventsIfNeeded(path);
271
+ appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
272
+ return true;
273
+ } catch (e: any) {
274
+ LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
46
275
  return false;
47
276
  }
48
- pendingMeshCoordinatorEvents.push(event);
49
- return true;
50
277
  }
51
278
 
52
- /** Drain and return all pending coordinator events, clearing the queue. */
53
- export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
54
- return pendingMeshCoordinatorEvents.splice(0);
279
+ // Atomically rename the file before reading so concurrent drains can't both consume
280
+ // the same events. renameSync is atomic on POSIX (same filesystem); only one caller
281
+ // wins the rename — the other gets ENOENT and returns null, preventing duplicate delivery.
282
+ function atomicDrainFile(path: string): string | null {
283
+ const tmpPath = `${path}.draining`;
284
+ try {
285
+ renameSync(path, tmpPath);
286
+ } catch {
287
+ return null; // another drain already renamed it, or file doesn't exist
288
+ }
289
+ try {
290
+ const content = readFileSync(tmpPath, 'utf-8');
291
+ try { unlinkSync(tmpPath); } catch { /* already cleaned up */ }
292
+ return content;
293
+ } catch {
294
+ try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
295
+ return null;
296
+ }
297
+ }
298
+
299
+ /** Drain and return all pending coordinator events for meshId, removing them from disk. */
300
+ export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
301
+ if (!meshId) return [];
302
+ const paths = coordinatorDaemonId
303
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
304
+ : [getPendingEventsPath(meshId)];
305
+ const all: PendingMeshCoordinatorEvent[] = [];
306
+ for (const path of paths) {
307
+ const content = atomicDrainFile(path);
308
+ if (!content) continue;
309
+ const parsed = content.split('\n').filter(Boolean).flatMap(line => {
310
+ try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
311
+ });
312
+ // If reading the shared file, filter to events that target this coordinator or are unscoped.
313
+ const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
314
+ ? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
315
+ : parsed;
316
+ all.push(...filtered);
317
+ }
318
+ if (all.length === 0) return [];
319
+ return reconcilePendingMeshCoordinatorEvents(meshId, all);
55
320
  }
56
321
 
57
322
  /** Peek at pending coordinator events without draining (non-destructive). */
58
- export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
59
- return pendingMeshCoordinatorEvents.slice();
323
+ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
324
+ if (!meshId) return [];
325
+ return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId));
60
326
  }
61
327
 
62
- /** Explicitly clear all pending coordinator events. */
63
- export function clearPendingMeshCoordinatorEvents(): void {
64
- pendingMeshCoordinatorEvents.splice(0);
328
+ /** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
329
+ export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
330
+ if (!meshId) return;
331
+ const paths = coordinatorDaemonId
332
+ ? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
333
+ : [getPendingEventsPath(meshId)];
334
+ for (const path of paths) {
335
+ if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
336
+ }
65
337
  }
66
338
 
67
339
  function readNonEmptyString(value: unknown): string {
68
340
  return typeof value === 'string' && value.trim() ? value.trim() : '';
69
341
  }
70
342
 
343
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
344
+ return value && typeof value === 'object' && !Array.isArray(value)
345
+ ? value as Record<string, unknown>
346
+ : undefined;
347
+ }
348
+
71
349
  function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
72
350
  return readNonEmptyString(event.targetSessionId)
73
351
  || readNonEmptyString(event.sessionId)
@@ -82,6 +360,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
82
360
  'agent:stopped',
83
361
  'agent:ready',
84
362
  'monitor:long_generating',
363
+ 'refine:accepted',
364
+ 'refine:completed',
365
+ 'refine:failed',
85
366
  ]);
86
367
 
87
368
  const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
@@ -96,10 +377,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
96
377
  }
97
378
 
98
379
  function formatCompletionMetadata(event: Record<string, unknown>): string {
380
+ const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
381
+ ? event.completionDiagnostic as Record<string, unknown>
382
+ : null;
383
+ const diagnosticReason = completionDiagnostic
384
+ ? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
385
+ : '';
386
+ const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
387
+ ? String(completionDiagnostic.finalAssistantPresent)
388
+ : '';
99
389
  const parts = [
100
390
  readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
101
391
  readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
102
392
  readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
393
+ diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
394
+ finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
103
395
  ].filter(Boolean);
104
396
  return parts.length > 0 ? ` (${parts.join('; ')})` : '';
105
397
  }
@@ -126,7 +418,7 @@ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boole
126
418
  function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
127
419
  if (!sessionId && !nodeId) return false;
128
420
  const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
129
- const entries = readLedgerEntries(meshId);
421
+ const entries = readLedgerEntries(meshId, { tail: 200 });
130
422
  for (let i = entries.length - 1; i >= 0; i--) {
131
423
  const entry = entries[i];
132
424
  const timestamp = new Date(entry.timestamp).getTime();
@@ -150,6 +442,226 @@ function shouldSuppressIntentionalCleanupStop(args: {
150
442
  return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
151
443
  }
152
444
 
445
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
446
+
447
+ function hasFingerprintSeen(fingerprint: string): boolean {
448
+ try {
449
+ return BeadsDB.getInstance().hasCompletionFingerprint(fingerprint);
450
+ } catch {
451
+ return false;
452
+ }
453
+ }
454
+
455
+ function recordFingerprintSeen(fingerprint: string): void {
456
+ try {
457
+ const db = BeadsDB.getInstance();
458
+ db.recordCompletionFingerprint(fingerprint, RECENT_COMPLETION_FINGERPRINT_TTL_MS);
459
+ db.sweepExpiredFingerprints();
460
+ } catch { /* best-effort; duplicate events are preferable to a crash */ }
461
+ }
462
+
463
+ function readEventTimestamp(value: unknown): number | null {
464
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
465
+ if (typeof value === 'string' && value.trim()) {
466
+ const numeric = Number(value);
467
+ if (Number.isFinite(numeric)) return numeric;
468
+ const parsed = Date.parse(value);
469
+ if (Number.isFinite(parsed)) return parsed;
470
+ }
471
+ return null;
472
+ }
473
+
474
+ function buildMeshCompletionFingerprint(args: {
475
+ meshId: string;
476
+ event: string;
477
+ sessionId: string;
478
+ providerType?: string;
479
+ providerSessionId?: string;
480
+ timestamp?: number | null;
481
+ finalSummary?: string;
482
+ /** When set, scopes the fingerprint to a specific coordinator daemon so
483
+ * two coordinators processing events from their respective workers don't
484
+ * suppress each other's completion events. */
485
+ coordinatorDaemonId?: string;
486
+ }): string {
487
+ const timestampPart = Number.isFinite(args.timestamp)
488
+ ? String(args.timestamp)
489
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
490
+ return [
491
+ args.meshId,
492
+ args.event,
493
+ args.sessionId,
494
+ args.providerType || '',
495
+ args.providerSessionId || '',
496
+ timestampPart,
497
+ args.coordinatorDaemonId || '',
498
+ ].join('::');
499
+ }
500
+
501
+ function isDuplicateMeshCompletionEvent(args: {
502
+ meshId: string;
503
+ event: string;
504
+ sessionId: string;
505
+ providerType?: string;
506
+ providerSessionId?: string;
507
+ timestamp?: number | null;
508
+ finalSummary?: string;
509
+ coordinatorDaemonId?: string;
510
+ }): boolean {
511
+ const fingerprint = buildMeshCompletionFingerprint(args);
512
+ if (!fingerprint) return false;
513
+ if (hasFingerprintSeen(fingerprint)) return true;
514
+ recordFingerprintSeen(fingerprint);
515
+ return false;
516
+ }
517
+
518
+ function isDuplicateMeshApprovalEvent(args: {
519
+ meshId: string;
520
+ sessionId: string;
521
+ providerType?: string;
522
+ timestamp?: number | null;
523
+ modalMessage?: string;
524
+ modalButtons?: unknown;
525
+ }): boolean {
526
+ const modalButtons = Array.isArray(args.modalButtons)
527
+ ? args.modalButtons.map(button => String(button).trim()).filter(Boolean)
528
+ : [];
529
+ const approvalIdentity = Number.isFinite(args.timestamp)
530
+ ? String(args.timestamp)
531
+ : JSON.stringify({ message: args.modalMessage || '', buttons: modalButtons });
532
+ if (!approvalIdentity || approvalIdentity === '{"message":"","buttons":[]}') return false;
533
+ const fingerprint = [
534
+ args.meshId,
535
+ 'agent:waiting_approval',
536
+ args.sessionId,
537
+ args.providerType || '',
538
+ approvalIdentity,
539
+ ].join('::');
540
+ if (hasFingerprintSeen(fingerprint)) return true;
541
+ recordFingerprintSeen(fingerprint);
542
+ return false;
543
+ }
544
+
545
+ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
546
+ const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
547
+ if (!fingerprint) return false;
548
+ if (hasFingerprintSeen(fingerprint)) return true;
549
+ recordFingerprintSeen(fingerprint);
550
+ return false;
551
+ }
552
+
553
+ function findRecentTerminalLedgerEvidence(args: {
554
+ meshId: string;
555
+ sessionId?: string;
556
+ nodeId?: string;
557
+ }): { id: string; kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
558
+ if (!args.sessionId && !args.nodeId) return null;
559
+ // Tail-limit: 200 entries gives a wide enough window to catch terminal events for active
560
+ // sessions while avoiding a full O(n) scan. If a terminal is older than 200 entries,
561
+ // the BeadsDB fingerprint dedup will still block duplicate processing downstream.
562
+ const entries = readLedgerEntries(args.meshId, { tail: 200 });
563
+ for (let i = entries.length - 1; i >= 0; i--) {
564
+ const entry = entries[i];
565
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
566
+ if (args.sessionId && entry.sessionId === args.sessionId) {
567
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
568
+ }
569
+ if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
570
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
571
+ }
572
+ }
573
+ return null;
574
+ }
575
+
576
+ // Returns true when a task_dispatched entry for the given session appears AFTER the terminal
577
+ // entry (identified by terminalId) in ledger order. Positional (append) order is used rather
578
+ // than timestamp comparison because both entries may share the same millisecond.
579
+ function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean {
580
+ // 200-entry window matches findRecentTerminalLedgerEvidence for consistency.
581
+ const entries = readLedgerEntries(meshId, { tail: 200 });
582
+ let pastTerminal = false;
583
+ for (const entry of entries) {
584
+ if (!pastTerminal) {
585
+ if (entry.id === terminalId) pastTerminal = true;
586
+ continue;
587
+ }
588
+ if (entry.kind === 'task_dispatched' && entry.sessionId === sessionId) return true;
589
+ }
590
+ return false;
591
+ }
592
+
593
+ function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: string): boolean {
594
+ // Some dispatch paths can persist task_dispatched before the direct-dispatch DB row is
595
+ // available. Recover routing from ledger order so coordinator self-targets still emit
596
+ // task_completed and pendingCoordinatorEvents.
597
+ const entries = readLedgerEntries(meshId, { tail: 200 });
598
+ for (let i = entries.length - 1; i >= 0; i--) {
599
+ const entry = entries[i];
600
+ if (entry.sessionId !== sessionId) continue;
601
+ if (entry.kind === 'task_completed' || entry.kind === 'task_failed' || entry.kind === 'task_stalled') {
602
+ return false;
603
+ }
604
+ if (entry.kind === 'task_dispatched' && entry.payload?.source === 'direct') {
605
+ return true;
606
+ }
607
+ }
608
+ return false;
609
+ }
610
+
611
+ function buildLongGeneratingCompletionReconciliation(args: {
612
+ meshId: string;
613
+ nodeId?: string;
614
+ nodeLabel: string;
615
+ metadataEvent: Record<string, unknown>;
616
+ sourceInstanceId?: string;
617
+ }): Record<string, unknown> | null {
618
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
619
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
620
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
621
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
622
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
623
+ const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
624
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
625
+ const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
626
+ const explicitCompletionEvidence = Boolean(
627
+ finalSummary
628
+ || workerResult
629
+ || completionDiagnostic?.finalAssistantPresent === true
630
+ || status === 'idle'
631
+ || status === 'ready'
632
+ || status === 'completed',
633
+ );
634
+ if (explicitCompletionEvidence) {
635
+ return {
636
+ ...args.metadataEvent,
637
+ targetSessionId: sessionId,
638
+ providerType,
639
+ providerSessionId,
640
+ finalSummary,
641
+ source: 'long_generating_reconciliation',
642
+ reconciledFromEvent: 'monitor:long_generating',
643
+ timestamp: args.metadataEvent.timestamp ?? Date.now(),
644
+ completionDiagnostic: {
645
+ ...(completionDiagnostic || {}),
646
+ reconciliationReason: 'provider_completion_evidence',
647
+ },
648
+ };
649
+ }
650
+
651
+ const terminal = findRecentTerminalLedgerEvidence({
652
+ meshId: args.meshId,
653
+ sessionId: sessionId || undefined,
654
+ nodeId: nodeId || undefined,
655
+ });
656
+ if (!terminal) return null;
657
+ return {
658
+ ...args.metadataEvent,
659
+ source: 'long_generating_terminal_ledger_suppression',
660
+ terminalLedgerKind: terminal.kind,
661
+ terminalLedgerAt: terminal.timestamp,
662
+ };
663
+ }
664
+
153
665
 
154
666
  export function tryAssignQueueTask(
155
667
  components: DaemonComponents,
@@ -158,7 +670,10 @@ export function tryAssignQueueTask(
158
670
  sessionId: string,
159
671
  providerType: string
160
672
  ): boolean {
161
- const task = claimNextTask(meshId, nodeId, sessionId);
673
+ const mesh = getMeshWithCache(components, meshId);
674
+ const node = mesh?.nodes.find((n: any) => n.id === nodeId);
675
+ const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
676
+ const task = claimNextTask(meshId, nodeId, sessionId, capabilityTags);
162
677
  if (!task) {
163
678
  return false;
164
679
  }
@@ -166,9 +681,6 @@ export function tryAssignQueueTask(
166
681
  LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
167
682
 
168
683
  // Check if the node is remote
169
- const mesh = getMeshWithCache(components, meshId);
170
- const node = mesh?.nodes.find((n: any) => n.id === nodeId);
171
-
172
684
  // If the node is explicitly remote and we have a dispatch mechanism, route via P2P
173
685
  if (node?.daemonId && components.dispatchMeshCommand) {
174
686
  const isLocalNode = components.cliManager.adapters.has(sessionId);
@@ -180,7 +692,16 @@ export function tryAssignQueueTask(
180
692
  message: task.message,
181
693
  }).catch((e: any) => {
182
694
  LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
183
- updateTaskStatus(meshId, task.id, 'failed');
695
+ // Revert to pending so the task can be retried rather than permanently failing
696
+ updateTaskStatus(meshId, task.id, 'pending');
697
+ try {
698
+ appendLedgerEntry(meshId, {
699
+ kind: 'dispatch_failed' as any,
700
+ nodeId,
701
+ sessionId,
702
+ payload: { taskId: task.id, error: e?.message, retryable: true },
703
+ });
704
+ } catch { /* ledger write is best-effort */ }
184
705
  });
185
706
  return true;
186
707
  }
@@ -204,6 +725,13 @@ const autoLaunchInProgress = new Set<string>();
204
725
  const autoLaunchCooldownUntil = new Map<string, number>();
205
726
  const AUTO_LAUNCH_COOLDOWN_MS = 5_000;
206
727
 
728
+ function sweepExpiredCooldowns(): void {
729
+ const now = Date.now();
730
+ for (const [key, until] of autoLaunchCooldownUntil) {
731
+ if (now >= until) autoLaunchCooldownUntil.delete(key);
732
+ }
733
+ }
734
+
207
735
  function normalizeProviderPriority(policy: unknown): string[] {
208
736
  const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
209
737
  ? (policy as Record<string, unknown>).providerPriority
@@ -277,6 +805,10 @@ function nodeHasActiveAssignment(meshId: string, nodeId: string): boolean {
277
805
  return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedNodeId === nodeId);
278
806
  }
279
807
 
808
+ function sessionHasActiveAssignment(meshId: string, sessionId: string): boolean {
809
+ return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedSessionId === sessionId);
810
+ }
811
+
280
812
  function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
281
813
  return components.instanceManager.getByCategory('cli').filter((inst: any) => {
282
814
  const state = inst.getState();
@@ -406,12 +938,14 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
406
938
  const nodeId = readNonEmptyString(node?.id);
407
939
  if (!nodeId) continue;
408
940
  const launchKey = `${meshId}:${nodeId}`;
941
+ const now = Date.now();
409
942
  const cooldownUntil = autoLaunchCooldownUntil.get(launchKey) || 0;
943
+ if (cooldownUntil > 0 && now >= cooldownUntil) autoLaunchCooldownUntil.delete(launchKey);
410
944
  if (autoLaunchInProgress.has(launchKey)) {
411
945
  markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_in_progress', nodeId });
412
946
  continue;
413
947
  }
414
- if (Date.now() < cooldownUntil) {
948
+ if (now < cooldownUntil) {
415
949
  markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_cooldown', nodeId });
416
950
  continue;
417
951
  }
@@ -461,13 +995,13 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
461
995
  if (!launchResult?.success) {
462
996
  const reason = launchResult?.error || 'launch_cli_failed';
463
997
  markAutoLaunch(meshId, task.id, { status: 'failed', reason, nodeId, providerType: resolved.providerType });
464
- autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
998
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
465
999
  return false;
466
1000
  }
467
1001
  const sessionId = readNonEmptyString(launchResult.sessionId) || readNonEmptyString(launchResult.id) || readNonEmptyString(launchResult.runtimeSessionId);
468
1002
  if (!sessionId) {
469
1003
  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);
1004
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
471
1005
  return false;
472
1006
  }
473
1007
  markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId });
@@ -534,6 +1068,72 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
534
1068
  await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
535
1069
  }
536
1070
 
1071
+ async function maybeAutoFastForwardIdleNode(components: DaemonComponents, args: {
1072
+ meshId: string;
1073
+ nodeId: string;
1074
+ sessionId?: string;
1075
+ providerType?: string;
1076
+ }): Promise<void> {
1077
+ const mesh = getMeshWithCache(components, args.meshId);
1078
+ const node = mesh?.nodes?.find((candidate: any) => candidate?.id === args.nodeId || candidate?.nodeId === args.nodeId);
1079
+ const workspace = readNonEmptyString(node?.workspace);
1080
+ if (!workspace) return;
1081
+ if (!existsSync(workspace)) return;
1082
+
1083
+ const throttleKey = `${args.meshId}:${args.nodeId}`;
1084
+ const now = Date.now();
1085
+ const lastAttempt = idleAutoFastForwardLastAttempt.get(throttleKey) || 0;
1086
+ if (now - lastAttempt < IDLE_AUTO_FAST_FORWARD_THROTTLE_MS) return;
1087
+ idleAutoFastForwardLastAttempt.set(throttleKey, now);
1088
+
1089
+ const submoduleIgnorePaths = Array.isArray(node?.policy?.submoduleIgnorePaths)
1090
+ ? node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
1091
+ : undefined;
1092
+ try {
1093
+ const dryRun = await fastForwardMeshNode({
1094
+ meshId: args.meshId,
1095
+ nodeId: args.nodeId,
1096
+ workspace,
1097
+ execute: false,
1098
+ dryRun: true,
1099
+ updateSubmodules: false,
1100
+ submoduleIgnorePaths,
1101
+ trigger: 'idle_auto',
1102
+ });
1103
+ if (!dryRun || dryRun.code !== 'fast_forward_available' || dryRun.allowed !== true) return;
1104
+ await fastForwardMeshNode({
1105
+ meshId: args.meshId,
1106
+ nodeId: args.nodeId,
1107
+ workspace,
1108
+ execute: true,
1109
+ dryRun: false,
1110
+ updateSubmodules: false,
1111
+ submoduleIgnorePaths,
1112
+ trigger: 'idle_auto',
1113
+ });
1114
+ } catch (e: any) {
1115
+ LOG.warn('MeshFastForward', `Idle auto fast-forward check failed for ${args.nodeId}: ${e?.message || e}`);
1116
+ }
1117
+ }
1118
+
1119
+ function runIdleMaintenanceThenAssignQueue(components: DaemonComponents, args: {
1120
+ meshId: string;
1121
+ nodeId: string;
1122
+ sessionId: string;
1123
+ providerType: string;
1124
+ }): void {
1125
+ setImmediate(() => {
1126
+ maybeAutoFastForwardIdleNode(components, args)
1127
+ .finally(() => {
1128
+ try {
1129
+ tryAssignQueueTask(components, args.meshId, args.nodeId, args.sessionId, args.providerType);
1130
+ } catch (e: any) {
1131
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${args.nodeId}: ${e?.message || e}`);
1132
+ }
1133
+ });
1134
+ });
1135
+ }
1136
+
537
1137
  function buildMeshSystemMessage(args: {
538
1138
  event: string;
539
1139
  nodeLabel: string;
@@ -542,6 +1142,9 @@ function buildMeshSystemMessage(args: {
542
1142
  }): string {
543
1143
  const metadata = formatCompletionMetadata(args.metadataEvent);
544
1144
  if (args.event === 'agent:generating_completed') {
1145
+ if (args.metadataEvent.source === 'long_generating_reconciliation') {
1146
+ 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.`;
1147
+ }
545
1148
  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
1149
  }
547
1150
  if (args.event === 'agent:waiting_approval') {
@@ -573,7 +1176,72 @@ function buildMeshSystemMessage(args: {
573
1176
  return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
574
1177
  }
575
1178
  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.`;
1179
+ 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.`;
1180
+ }
1181
+ if (args.event === 'refine:accepted') {
1182
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1183
+ 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.`;
1184
+ }
1185
+ if (args.event === 'refine:completed') {
1186
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1187
+ const result = readRecord(args.metadataEvent.result);
1188
+ const validationSummary = readRecord(result?.validationSummary);
1189
+ const patchEquivalence = readRecord(result?.patchEquivalence);
1190
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
1191
+ const validationStatus = readNonEmptyString(validationSummary?.status);
1192
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
1193
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
1194
+ const into = readNonEmptyString(result?.into);
1195
+ const branch = readNonEmptyString(result?.branch);
1196
+ const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
1197
+ const convergenceStatus = readNonEmptyString(finalConvergence?.status);
1198
+ const nextStep = readNonEmptyString(result?.nextStep)
1199
+ || readNonEmptyString(finalConvergence?.nextStep)
1200
+ || 'Continue from the updated mesh state.';
1201
+ const details = [
1202
+ jobId ? `job_id=${jobId}` : '',
1203
+ branch && into ? `${branch}→${into}` : '',
1204
+ validationStatus ? `validation=${validationStatus}` : '',
1205
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
1206
+ mergeStatus ? `merge=${mergeStatus}` : '',
1207
+ convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
1208
+ ].filter(Boolean).join('; ');
1209
+ return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
1210
+ }
1211
+ if (args.event === 'refine:failed') {
1212
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1213
+ const result = readRecord(args.metadataEvent.result);
1214
+ const validationSummary = readRecord(result?.validationSummary);
1215
+ const patchEquivalence = readRecord(result?.patchEquivalence);
1216
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
1217
+ const code = readNonEmptyString(result?.code);
1218
+ const error = readNonEmptyString(result?.error);
1219
+ const validationStatus = readNonEmptyString(validationSummary?.status);
1220
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
1221
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
1222
+ const mergeStatus = result?.merged === true
1223
+ ? 'merged'
1224
+ : finalConvergence?.merged === false
1225
+ ? 'not_merged'
1226
+ : '';
1227
+ const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
1228
+ || readNonEmptyString(finalConvergence?.status);
1229
+ const blockedReason = readNonEmptyString(result?.blockedReason);
1230
+ const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
1231
+ const details = [
1232
+ jobId ? `job_id=${jobId}` : '',
1233
+ code ? `code=${code}` : '',
1234
+ validationStatus ? `validation=${validationStatus}` : '',
1235
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
1236
+ mergeStatus ? `merge=${mergeStatus}` : '',
1237
+ convergenceStatus ? `convergence=${convergenceStatus}` : '',
1238
+ blockedReason ? `reason=${blockedReason}` : '',
1239
+ ].filter(Boolean).join('; ');
1240
+ const parts = [
1241
+ `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
1242
+ nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
1243
+ ];
1244
+ return parts.join('\n');
577
1245
  }
578
1246
  return '';
579
1247
  }
@@ -588,6 +1256,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
588
1256
  }) {
589
1257
  const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
590
1258
  const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1259
+
1260
+ // Resolve coordinator ownership early — used in fingerprinting and coordinator routing.
1261
+ const sourceSession = args.sourceInstanceId
1262
+ ? components.instanceManager.getInstance(args.sourceInstanceId)
1263
+ : undefined;
1264
+ const workerCoordinatorDaemonId = readNonEmptyString(
1265
+ (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
1266
+ );
1267
+ const localDaemonId = readNonEmptyString(loadConfig().machineId);
591
1268
  const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
592
1269
  event: args.event,
593
1270
  meshId: args.meshId,
@@ -603,6 +1280,97 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
603
1280
  return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
604
1281
  }
605
1282
 
1283
+ if (args.event === 'monitor:long_generating') {
1284
+ const reconciledCompletion = buildLongGeneratingCompletionReconciliation({
1285
+ meshId: args.meshId,
1286
+ nodeId: args.nodeId,
1287
+ nodeLabel: args.nodeLabel,
1288
+ metadataEvent: args.metadataEvent,
1289
+ sourceInstanceId: args.sourceInstanceId,
1290
+ });
1291
+ if (reconciledCompletion?.source === 'long_generating_reconciliation') {
1292
+ LOG.info('MeshEvents', `Reconciled long-generating monitor to completion for session ${eventSessionId || '(unknown session)'}`);
1293
+ return injectMeshSystemMessage(components, {
1294
+ ...args,
1295
+ event: 'agent:generating_completed',
1296
+ metadataEvent: reconciledCompletion,
1297
+ });
1298
+ }
1299
+ if (reconciledCompletion?.source === 'long_generating_terminal_ledger_suppression') {
1300
+ LOG.info('MeshEvents', `Suppressed long-generating monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
1301
+ return {
1302
+ success: true,
1303
+ forwarded: 0,
1304
+ suppressed: true,
1305
+ terminalLedgerEvidence: true,
1306
+ terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
1307
+ };
1308
+ }
1309
+ }
1310
+
1311
+ if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
1312
+ LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
1313
+ return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
1314
+ }
1315
+
1316
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
1317
+ if (args.event === 'agent:waiting_approval' && eventSessionId) {
1318
+ const duplicateApproval = isDuplicateMeshApprovalEvent({
1319
+ meshId: args.meshId,
1320
+ sessionId: eventSessionId,
1321
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1322
+ timestamp: eventTimestamp,
1323
+ modalMessage: readNonEmptyString(args.metadataEvent.modalMessage) || undefined,
1324
+ modalButtons: args.metadataEvent.modalButtons,
1325
+ });
1326
+ if (duplicateApproval) {
1327
+ LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
1328
+ return { success: true, forwarded: 0, suppressed: true, duplicateApproval: true };
1329
+ }
1330
+ }
1331
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
1332
+ const terminal = findRecentTerminalLedgerEvidence({
1333
+ meshId: args.meshId,
1334
+ sessionId: eventSessionId,
1335
+ nodeId: eventNodeId || undefined,
1336
+ });
1337
+ if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
1338
+ // If a new task_dispatched was recorded for this session after the prior terminal,
1339
+ // this completion belongs to the new task — never suppress it as a duplicate.
1340
+ const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
1341
+ if (!newDispatchAfterTerminal) {
1342
+ const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
1343
+ const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
1344
+ const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
1345
+ const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
1346
+ if (
1347
+ (terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
1348
+ || (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
1349
+ || args.metadataEvent.source === 'long_generating_reconciliation'
1350
+ ) {
1351
+ LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
1352
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
1353
+ }
1354
+ }
1355
+ }
1356
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
1357
+ meshId: args.meshId,
1358
+ event: args.event,
1359
+ sessionId: eventSessionId,
1360
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1361
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
1362
+ timestamp: eventTimestamp,
1363
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1364
+ // Scope dedup to the coordinator daemon so two coordinators for the same mesh
1365
+ // don't suppress each other's completion events via shared fingerprint table.
1366
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
1367
+ });
1368
+ if (duplicateCompletion) {
1369
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
1370
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
1371
+ }
1372
+ }
1373
+
606
1374
  // ── Task Queue & Ledger ──
607
1375
  let completedTaskForLedger: { id?: string } | null = null;
608
1376
  if (args.event === 'agent:generating_completed') {
@@ -611,24 +1379,31 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
611
1379
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
612
1380
 
613
1381
  if (sessionId) {
614
- const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
1382
+ const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
1383
+ occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
1384
+ });
615
1385
  completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
1386
+ updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
1387
+ setImmediate(() => cleanupTerminalDirectDispatches());
616
1388
  if (nodeId && providerType) {
617
- // Short delay to allow completion event to propagate before pulling next
618
- setTimeout(() => {
619
- tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
620
- }, 500);
1389
+ runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
621
1390
  }
622
1391
  }
623
1392
  } else if (args.event === 'agent:ready') {
624
1393
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
625
1394
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
626
1395
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
627
- const completedTask = sessionId
1396
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1397
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1398
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1399
+ const hasCompletionEvidence = !!finalSummary || !!workerResult;
1400
+ const completedTask = sessionId && hasCompletionEvidence
628
1401
  ? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
629
1402
  : null;
630
- if (completedTask) {
1403
+ if (completedTask && sessionId) {
631
1404
  completedTaskForLedger = { id: completedTask.id };
1405
+ updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
1406
+ setImmediate(() => cleanupTerminalDirectDispatches());
632
1407
  try {
633
1408
  appendLedgerEntry(args.meshId, {
634
1409
  kind: 'task_completed',
@@ -640,15 +1415,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
640
1415
  nodeLabel: args.nodeLabel,
641
1416
  taskId: completedTask.id,
642
1417
  completedViaReady: true,
643
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
644
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1418
+ providerSessionId,
1419
+ finalSummary,
1420
+ workerResult,
645
1421
  evidence: buildTaskCompletionEvidence({
646
1422
  event: 'agent:ready',
647
1423
  nodeId,
648
1424
  sessionId,
649
1425
  providerType: providerType || undefined,
650
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
651
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1426
+ providerSessionId,
1427
+ finalSummary,
1428
+ workerResult,
652
1429
  }),
653
1430
  },
654
1431
  });
@@ -658,13 +1435,22 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
658
1435
  }
659
1436
 
660
1437
  if (sessionId && nodeId && providerType) {
661
- remoteIdleSessions.set(`${nodeId}:${sessionId}`, { nodeId, sessionId, providerType });
662
- setTimeout(() => {
663
- const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
664
- if (assigned) {
665
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
666
- }
667
- }, 500);
1438
+ sweepExpiredRemoteIdleSessions();
1439
+ remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
1440
+ nodeId, sessionId, providerType,
1441
+ expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
1442
+ });
1443
+ setImmediate(() => {
1444
+ maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
1445
+ .finally(() => {
1446
+ try {
1447
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
1448
+ if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
1449
+ } catch (e: any) {
1450
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
1451
+ }
1452
+ });
1453
+ });
668
1454
  }
669
1455
  } else if (args.event === 'agent:generating_started') {
670
1456
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
@@ -672,6 +1458,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
672
1458
  if (sessionId && nodeId) {
673
1459
  remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
674
1460
  }
1461
+ if (sessionId) {
1462
+ // Mark direct dispatch as acknowledged — the session started generating.
1463
+ updateDirectDispatchStatus(args.meshId, sessionId, 'acked');
1464
+ }
675
1465
  } else if (args.event === 'agent:stopped') {
676
1466
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
677
1467
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
@@ -679,7 +1469,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
679
1469
  remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
680
1470
  }
681
1471
  if (sessionId) {
682
- updateSessionTaskStatus(args.meshId, sessionId, 'failed');
1472
+ const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
1473
+ completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
1474
+ updateDirectDispatchStatus(args.meshId, sessionId, 'failed');
683
1475
  }
684
1476
  }
685
1477
 
@@ -689,14 +1481,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
689
1481
  const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
690
1482
  const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
691
1483
  const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
1484
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1485
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1486
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
692
1487
  const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
693
1488
  ? buildTaskCompletionEvidence({
694
1489
  event: 'agent:generating_completed',
695
1490
  nodeId: ledgerNodeId,
696
1491
  sessionId: ledgerSessionId,
697
1492
  providerType: ledgerProviderType,
698
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
699
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1493
+ providerSessionId,
1494
+ finalSummary,
1495
+ workerResult,
700
1496
  })
701
1497
  : undefined;
702
1498
  appendLedgerEntry(args.meshId, {
@@ -708,8 +1504,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
708
1504
  event: args.event,
709
1505
  nodeLabel: args.nodeLabel,
710
1506
  taskId: completedTaskForLedger?.id || undefined,
711
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
712
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1507
+ providerSessionId,
1508
+ finalSummary,
1509
+ workerResult,
1510
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
1511
+ ? args.metadataEvent.completionDiagnostic
1512
+ : undefined,
713
1513
  evidence: completionEvidence,
714
1514
  },
715
1515
  });
@@ -782,15 +1582,33 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
782
1582
  }
783
1583
  }
784
1584
 
1585
+ const messageText = buildMeshSystemMessage({
1586
+ event: args.event,
1587
+ nodeLabel: args.nodeLabel,
1588
+ metadataEvent: args.metadataEvent,
1589
+ recoveryContext,
1590
+ });
1591
+ if (!messageText) return { success: false, error: 'unsupported mesh event' };
1592
+
785
1593
  const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
786
1594
  const instState = inst.getState();
787
1595
  if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
788
1596
  if (args.sourceInstanceId && instState.instanceId === args.sourceInstanceId) return false;
1597
+ // If the worker knows which coordinator daemon launched it, only route to coordinators
1598
+ // on that specific daemon. This prevents cross-contamination when multiple coordinator
1599
+ // sessions run simultaneously for the same mesh on different daemons.
1600
+ if (workerCoordinatorDaemonId && localDaemonId && workerCoordinatorDaemonId !== localDaemonId) return false;
789
1601
  return true;
790
1602
  });
791
1603
 
1604
+ // Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
1605
+ // synchronously; only buffer them for MCP when no CLI coordinator is present.
1606
+ // Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
1607
+ // receive them regardless of whether a live CLI coordinator session is active.
1608
+ const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
1609
+
792
1610
  if (coordinatorInstances.length === 0) {
793
- // No CLI coordinator session found — buffer for MCP-based coordinators.
1611
+ // No local CLI coordinator — buffer for MCP-based coordinator on the target daemon.
794
1612
  if (queuePendingMeshCoordinatorEvent({
795
1613
  event: args.event,
796
1614
  meshId: args.meshId,
@@ -801,20 +1619,69 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
801
1619
  ...args.metadataEvent,
802
1620
  ...(recoveryContext ? { recoveryContext } : {}),
803
1621
  },
1622
+ coordinatorMessage: messageText,
804
1623
  queuedAt: Date.now(),
1624
+ // Scope to the coordinator daemon that launched this worker so drain
1625
+ // by other coordinators on the same daemon doesn't consume this event.
1626
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
805
1627
  })) {
806
- LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1628
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''})`);
807
1629
  }
808
1630
  return { success: true, forwarded: 0 };
809
1631
  }
810
1632
 
811
- const messageText = buildMeshSystemMessage({
812
- event: args.event,
813
- nodeLabel: args.nodeLabel,
814
- metadataEvent: args.metadataEvent,
815
- recoveryContext,
1633
+ // CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
1634
+ // that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
1635
+ // forwarded directly only — they must not accumulate in the pending queue when a live
1636
+ // coordinator already received them.
1637
+ //
1638
+ // Exception: if ALL live CLI coordinator instances are in a generating/active state
1639
+ // when a refine terminal event fires (e.g. a Codex CLI coordinator that triggered an
1640
+ // async refine job and is still in the generating turn that sent it), the coordinator
1641
+ // cannot immediately receive send_message input. Buffer the event to the pending queue
1642
+ // so it is available via get_pending_mesh_events when the coordinator returns to idle.
1643
+ // Critically: do NOT attempt send_message injection into a generating PTY coordinator for
1644
+ // terminal refine events — injecting text into an active PTY can corrupt the input stream
1645
+ // and leave the coordinator stuck in generating state, unable to process the refine result.
1646
+ const allCoordinatorsGenerating = isRefineTerminalEvent && coordinatorInstances.every((inst) => {
1647
+ const s = inst.getState();
1648
+ const status = readNonEmptyString(s.status).toLowerCase();
1649
+ const activeChatStatus = readNonEmptyString(s.activeChat?.status).toLowerCase();
1650
+ return status === 'generating' || status === 'streaming' || status === 'long_generating'
1651
+ || activeChatStatus === 'generating' || activeChatStatus === 'streaming';
816
1652
  });
817
- if (!messageText) return { success: false, error: 'unsupported mesh event' };
1653
+
1654
+ if (!isRefineTerminalEvent || allCoordinatorsGenerating) {
1655
+ if (queuePendingMeshCoordinatorEvent({
1656
+ event: args.event,
1657
+ meshId: args.meshId,
1658
+ nodeLabel: args.nodeLabel,
1659
+ nodeId: args.nodeId || undefined,
1660
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
1661
+ metadataEvent: {
1662
+ ...args.metadataEvent,
1663
+ ...(recoveryContext ? { recoveryContext } : {}),
1664
+ },
1665
+ coordinatorMessage: messageText,
1666
+ queuedAt: Date.now(),
1667
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
1668
+ })) {
1669
+ if (allCoordinatorsGenerating) {
1670
+ 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`);
1671
+ } else {
1672
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1673
+ }
1674
+ }
1675
+ }
1676
+
1677
+ // When all CLI coordinators are actively generating and a terminal refine event fires,
1678
+ // skip send_message injection entirely. The event is already buffered to the pending queue.
1679
+ // Injecting into a generating PTY coordinator can corrupt its input stream and cause it to
1680
+ // remain stuck in generating state, never processing the refine result.
1681
+ // The coordinator will drain pending events via get_pending_mesh_events on its next idle cycle.
1682
+ if (allCoordinatorsGenerating) {
1683
+ return { success: true, forwarded: 0, bufferedForGeneratingCoordinator: true };
1684
+ }
818
1685
 
819
1686
  for (const coord of coordinatorInstances) {
820
1687
  const coordState = coord.getState();
@@ -845,6 +1712,19 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
845
1712
  providerType: readNonEmptyString(payload.providerType),
846
1713
  providerSessionId: readNonEmptyString(payload.providerSessionId),
847
1714
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1715
+ jobId: readNonEmptyString(payload.jobId),
1716
+ interactionId: readNonEmptyString(payload.interactionId),
1717
+ status: readNonEmptyString(payload.status),
1718
+ targetDaemonId: readNonEmptyString(payload.targetDaemonId),
1719
+ startedAt: readNonEmptyString(payload.startedAt),
1720
+ completedAt: readNonEmptyString(payload.completedAt),
1721
+ retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1722
+ ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1723
+ ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
1724
+ ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
1725
+ ...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
1726
+ ...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
1727
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
848
1728
  intentional: payload.intentional === true,
849
1729
  intentionalStop: payload.intentionalStop === true,
850
1730
  operatorCleanup: payload.operatorCleanup === true,
@@ -872,21 +1752,37 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
872
1752
  if (!workspace) return;
873
1753
  const settings = state.settings && typeof state.settings === 'object' ? state.settings as Record<string, unknown> : {};
874
1754
 
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;
1755
+ // Coordinator sessions normally must not inject events into themselves, but a
1756
+ // coordinator session can also be the direct-dispatch target of mesh_send_task
1757
+ // issued by another coordinator (e.g. a remote orchestrator delegated a task to
1758
+ // this local coordinator). In that case the completion event must still flow into
1759
+ // injectMeshSystemMessage so the ledger records task_completed and the *other*
1760
+ // coordinator's pendingCoordinatorEvents queue is populated. Skip only when this
1761
+ // session is purely a coordinator with no in-flight direct dispatch waiting on it.
1762
+ const coordinatorMeshId = readNonEmptyString(settings.meshCoordinatorFor);
1763
+ let meshIdFromDirectDispatch = '';
1764
+ if (coordinatorMeshId) {
1765
+ try {
1766
+ const activeDispatches = getActiveDirectDispatches(coordinatorMeshId);
1767
+ if (activeDispatches.some(d => d.sessionId === instanceId) || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId)) {
1768
+ meshIdFromDirectDispatch = coordinatorMeshId;
1769
+ }
1770
+ } catch { /* best-effort */ }
1771
+ if (!meshIdFromDirectDispatch) return;
1772
+ }
878
1773
 
879
- const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor);
1774
+ const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor) || meshIdFromDirectDispatch;
880
1775
 
881
1776
  // 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.
1777
+ // (meshNodeFor set by mesh_launch_session), carry the launchedByCoordinator flag, or
1778
+ // have an active direct-dispatch entry (mesh_send_task to a pre-existing session).
883
1779
  // Do NOT fall back to workspace-based mesh lookup: that would pick up coordinator sessions
884
1780
  // and any other CLI session that happens to share the same workspace, causing spurious
885
1781
  // system-message injection into the coordinator's own conversation.
886
1782
  const isMeshDelegate = Boolean(meshIdFromRuntime || settings.launchedByCoordinator);
887
1783
  if (!isMeshDelegate) return;
888
1784
 
889
- const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getMeshByRepo(workspace);
1785
+ const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getCachedMeshByWorkspace(workspace);
890
1786
  const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
891
1787
  if (!meshId) return;
892
1788