@adhdev/daemon-core 0.9.82-rc.2 → 0.9.82-rc.200

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 (244) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +3 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/chat/source-machine.d.ts +166 -0
  4. package/dist/chat/source-resolver.d.ts +104 -0
  5. package/dist/chat/subscription-updates.d.ts +1 -0
  6. package/dist/cli-adapter-types.d.ts +23 -2
  7. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  8. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  9. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  10. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  11. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  12. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  13. package/dist/commands/cli-manager.d.ts +2 -1
  14. package/dist/commands/handler.d.ts +110 -0
  15. package/dist/commands/mesh-coordinator.d.ts +85 -1
  16. package/dist/commands/router.d.ts +29 -1
  17. package/dist/config/chat-history.d.ts +9 -0
  18. package/dist/config/config.d.ts +5 -0
  19. package/dist/config/mesh-config.d.ts +72 -1
  20. package/dist/git/git-commands.d.ts +8 -1
  21. package/dist/git/git-status.d.ts +5 -0
  22. package/dist/git/git-types.d.ts +12 -0
  23. package/dist/index.d.ts +39 -10
  24. package/dist/index.js +29425 -12026
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +25700 -8378
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/installer.d.ts +1 -4
  29. package/dist/ipc/local-ipc-server.d.ts +91 -0
  30. package/dist/launch.d.ts +1 -1
  31. package/dist/logging/async-batch-writer.d.ts +10 -0
  32. package/dist/mesh/contracts.d.ts +164 -0
  33. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  34. package/dist/mesh/coordinator-registry.d.ts +59 -0
  35. package/dist/mesh/mesh-active-work.d.ts +90 -0
  36. package/dist/mesh/mesh-delivery-policy.d.ts +126 -0
  37. package/dist/mesh/mesh-events.d.ts +167 -6
  38. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  39. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  40. package/dist/mesh/mesh-ledger.d.ts +58 -1
  41. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  42. package/dist/mesh/mesh-runtime-store.d.ts +149 -0
  43. package/dist/mesh/mesh-work-queue.d.ts +55 -6
  44. package/dist/mesh/preview-freshness.d.ts +18 -0
  45. package/dist/mesh/refine-config.d.ts +193 -0
  46. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  47. package/dist/providers/approval-utils.d.ts +13 -0
  48. package/dist/providers/chat-message-normalization.d.ts +1 -0
  49. package/dist/providers/cli-provider-instance.d.ts +39 -3
  50. package/dist/providers/contracts.d.ts +130 -6
  51. package/dist/providers/external-sources.d.ts +71 -0
  52. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  53. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  54. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  55. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  56. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  57. package/dist/providers/native-history/index.d.ts +13 -0
  58. package/dist/providers/provider-instance-manager.d.ts +12 -0
  59. package/dist/providers/provider-instance.d.ts +13 -1
  60. package/dist/providers/provider-loader.d.ts +26 -4
  61. package/dist/providers/provider-trust.d.ts +31 -0
  62. package/dist/providers/read-chat-contract.d.ts +29 -0
  63. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  64. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  65. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  66. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  67. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  68. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  69. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  70. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  71. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  72. package/dist/providers/sdk/v1/index.d.ts +30 -0
  73. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  74. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  75. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  76. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  77. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  78. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  79. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  80. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  81. package/dist/providers/spec/adapter.d.ts +60 -0
  82. package/dist/providers/spec/cli-adapter.d.ts +98 -0
  83. package/dist/providers/spec/driver.d.ts +190 -0
  84. package/dist/providers/spec/evaluator.d.ts +55 -0
  85. package/dist/providers/spec/loader.d.ts +14 -0
  86. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  87. package/dist/providers/spec/route.d.ts +4 -0
  88. package/dist/providers/spec/schema.gen.d.ts +599 -0
  89. package/dist/providers/spec/types.d.ts +282 -0
  90. package/dist/providers/transcript-v2.d.ts +176 -0
  91. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  92. package/dist/repo-mesh-types.d.ts +230 -1
  93. package/dist/sessions/registry.d.ts +3 -0
  94. package/dist/shared-types-extra.d.ts +1 -1
  95. package/dist/shared-types.d.ts +42 -1
  96. package/dist/status/normalize.d.ts +1 -1
  97. package/dist/status/normalize.js +1 -0
  98. package/dist/status/normalize.js.map +1 -1
  99. package/dist/status/normalize.mjs +1 -0
  100. package/dist/status/normalize.mjs.map +1 -1
  101. package/dist/status/reporter.d.ts +2 -0
  102. package/dist/status/snapshot.d.ts +1 -0
  103. package/dist/types.d.ts +5 -0
  104. package/package.json +7 -2
  105. package/src/agent-stream/poller.ts +2 -3
  106. package/src/boot/daemon-lifecycle.ts +22 -10
  107. package/src/boot/process-hardening.ts +89 -0
  108. package/src/chat/source-machine.ts +534 -0
  109. package/src/chat/source-resolver.ts +0 -0
  110. package/src/chat/subscription-updates.ts +20 -1
  111. package/src/cli-adapter-types.d.ts +1 -0
  112. package/src/cli-adapter-types.ts +22 -2
  113. package/src/cli-adapters/cli-script-runner.ts +421 -0
  114. package/src/cli-adapters/cli-state-engine.ts +1138 -0
  115. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  116. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  117. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  118. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  119. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  120. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  121. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  122. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  123. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  124. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  125. package/src/commands/chat-commands.ts +1787 -60
  126. package/src/commands/cli-manager.ts +283 -14
  127. package/src/commands/handler.ts +809 -2
  128. package/src/commands/mesh-coordinator.ts +331 -122
  129. package/src/commands/router.ts +3915 -546
  130. package/src/config/chat-history.ts +95 -24
  131. package/src/config/config.ts +12 -0
  132. package/src/config/mesh-config.ts +280 -2
  133. package/src/config/recent-activity.ts +8 -2
  134. package/src/daemon/dev-cli-debug.ts +10 -1
  135. package/src/detection/ide-detector.ts +26 -16
  136. package/src/git/git-commands.ts +38 -8
  137. package/src/git/git-status.ts +132 -12
  138. package/src/git/git-summary.ts +3 -0
  139. package/src/git/git-types.ts +13 -0
  140. package/src/git/git-worktree.ts +8 -1
  141. package/src/index.ts +127 -9
  142. package/src/installer.d.ts +1 -1
  143. package/src/installer.ts +8 -6
  144. package/src/ipc/local-ipc-server.ts +278 -0
  145. package/src/launch.d.ts +1 -1
  146. package/src/launch.ts +37 -28
  147. package/src/logging/async-batch-writer.ts +55 -0
  148. package/src/logging/logger.ts +2 -1
  149. package/src/mesh/contracts.ts +329 -0
  150. package/src/mesh/coordinator-prompt.ts +204 -30
  151. package/src/mesh/coordinator-registry.ts +121 -0
  152. package/src/mesh/mesh-active-work.ts +437 -0
  153. package/src/mesh/mesh-delivery-policy.ts +298 -0
  154. package/src/mesh/mesh-events.ts +1352 -109
  155. package/src/mesh/mesh-fast-forward.ts +438 -0
  156. package/src/mesh/mesh-host-ownership.ts +73 -0
  157. package/src/mesh/mesh-ledger.ts +457 -104
  158. package/src/mesh/mesh-refine-status.ts +144 -0
  159. package/src/mesh/mesh-runtime-store.ts +769 -0
  160. package/src/mesh/mesh-work-queue.ts +267 -159
  161. package/src/mesh/preview-freshness.ts +118 -0
  162. package/src/mesh/refine-config.ts +366 -0
  163. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  164. package/src/providers/approval-utils.d.ts +4 -0
  165. package/src/providers/approval-utils.ts +47 -5
  166. package/src/providers/chat-message-normalization.ts +7 -12
  167. package/src/providers/cli-provider-instance.ts +806 -64
  168. package/src/providers/contracts.d.ts +55 -0
  169. package/src/providers/contracts.ts +141 -6
  170. package/src/providers/external-sources.ts +218 -0
  171. package/src/providers/ide-provider-instance.ts +19 -5
  172. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  173. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  174. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  175. package/src/providers/native-history/dispatcher.ts +340 -0
  176. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  177. package/src/providers/native-history/index.ts +30 -0
  178. package/src/providers/provider-instance-manager.ts +30 -0
  179. package/src/providers/provider-instance.ts +10 -1
  180. package/src/providers/provider-loader.ts +582 -50
  181. package/src/providers/provider-schema.ts +87 -14
  182. package/src/providers/provider-trust.ts +114 -0
  183. package/src/providers/read-chat-contract.ts +76 -16
  184. package/src/providers/sdk/README.md +49 -0
  185. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  186. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  187. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  188. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  189. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  190. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  191. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  192. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  193. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  194. package/src/providers/sdk/v1/index.ts +152 -0
  195. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  196. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  197. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  198. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  199. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  200. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  201. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  202. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  203. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  204. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  205. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  206. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  207. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  208. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  209. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  210. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  211. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  212. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  213. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  214. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  215. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  216. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  217. package/src/providers/sdk/v1/validators/index.ts +19 -0
  218. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  219. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  220. package/src/providers/spec/adapter.ts +176 -0
  221. package/src/providers/spec/cli-adapter.ts +538 -0
  222. package/src/providers/spec/driver.ts +725 -0
  223. package/src/providers/spec/evaluator.ts +373 -0
  224. package/src/providers/spec/loader.ts +130 -0
  225. package/src/providers/spec/native-history-executor.ts +939 -0
  226. package/src/providers/spec/route.ts +51 -0
  227. package/src/providers/spec/schema.gen.ts +559 -0
  228. package/src/providers/spec/schema.json +237 -0
  229. package/src/providers/spec/types.ts +301 -0
  230. package/src/providers/transcript-v2.ts +567 -0
  231. package/src/providers/types/interactive-prompt.ts +425 -0
  232. package/src/providers/version-archive.ts +38 -20
  233. package/src/repo-mesh-types.ts +250 -1
  234. package/src/sessions/registry.ts +3 -0
  235. package/src/shared-types-extra.ts +1 -1
  236. package/src/shared-types.ts +45 -1
  237. package/src/status/builders.ts +24 -6
  238. package/src/status/normalize.ts +2 -0
  239. package/src/status/reporter.ts +15 -0
  240. package/src/status/snapshot.ts +84 -25
  241. package/src/system/host-memory.ts +29 -12
  242. package/src/types.ts +5 -0
  243. package/dist/mesh/mesh-sync.d.ts +0 -53
  244. package/src/mesh/mesh-sync.ts +0 -111
@@ -1,63 +1,344 @@
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 { MeshRuntimeStore } from './mesh-runtime-store.js';
12
+ import { fastForwardMeshNode } from './mesh-fast-forward.js';
13
+ import { createSessionDelivery, updateSessionDeliveryStatus, recordCompletionConflict } from './mesh-delivery-policy.js';
9
14
 
10
15
  // ---------------------------------------------------------------------------
11
16
  // Remote Node Idle Session Tracking
12
17
  // ---------------------------------------------------------------------------
13
- // Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
14
- // can assign tasks to them.
18
+ // Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
19
+ // can assign tasks to them. Each entry carries an expiresAt timestamp;
20
+ // entries are swept on insertion to prevent unbounded growth.
15
21
  // ---------------------------------------------------------------------------
16
- interface RemoteIdleSession {
17
- nodeId: string;
18
- sessionId: string;
19
- providerType: string;
22
+ const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Workspace-to-mesh lookup cache
26
+ // ---------------------------------------------------------------------------
27
+ // getMeshByRepo is called on every coordinator event for sessions without
28
+ // meshNodeFor. Cache results for 5 seconds to avoid repeated config reads.
29
+ const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
30
+ const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
31
+ const IDLE_AUTO_FAST_FORWARD_THROTTLE_MS = 30 * 60 * 1000;
32
+ const idleAutoFastForwardLastAttempt = new Map<string, number>();
33
+
34
+ function getCachedMeshByWorkspace(workspace: string): any {
35
+ const now = Date.now();
36
+ const cached = meshByWorkspaceCache.get(workspace);
37
+ if (cached && now - cached.cachedAt < MESH_WORKSPACE_CACHE_TTL_MS) return cached.mesh;
38
+ const mesh = getMeshByRepo(workspace);
39
+ meshByWorkspaceCache.set(workspace, { mesh, cachedAt: now });
40
+ return mesh;
41
+ }
42
+
43
+ function readWorkerResultMetadata(event: Record<string, unknown>): Record<string, unknown> | undefined {
44
+ return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
45
+ }
46
+
47
+ export function __resetIdleAutoFastForwardForTests(): void {
48
+ idleAutoFastForwardLastAttempt.clear();
49
+ }
50
+
51
+ function sweepExpiredRemoteIdleSessions(): void {
52
+ try {
53
+ MeshRuntimeStore.getInstance().pruneExpiredRemoteIdleSessions();
54
+ } catch { /* best-effort */ }
20
55
  }
21
- const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
22
56
 
23
57
  // ---------------------------------------------------------------------------
24
- // MCP coordinator pending-event queue
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 {
32
70
  event: string;
33
71
  meshId: string;
34
72
  nodeLabel: string;
73
+ nodeId?: string;
74
+ workspace?: string;
35
75
  metadataEvent: Record<string, unknown>;
76
+ coordinatorMessage?: string;
36
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
+ ];
37
235
  }
38
236
 
39
- const MAX_PENDING_EVENTS = 50;
40
- 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
41
239
 
42
- /** Drain and return all pending coordinator events, clearing the queue. */
43
- export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[] {
44
- return pendingMeshCoordinatorEvents.splice(0);
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
+ }
249
+
250
+ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
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}`);
268
+ return false;
269
+ }
270
+ }
271
+
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);
45
313
  }
46
314
 
47
315
  /** Peek at pending coordinator events without draining (non-destructive). */
48
- export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
49
- return pendingMeshCoordinatorEvents.slice();
316
+ export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
317
+ if (!meshId) return [];
318
+ return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId));
50
319
  }
51
320
 
52
- /** Explicitly clear all pending coordinator events. */
53
- export function clearPendingMeshCoordinatorEvents(): void {
54
- 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
+ }
55
330
  }
56
331
 
57
332
  function readNonEmptyString(value: unknown): string {
58
333
  return typeof value === 'string' && value.trim() ? value.trim() : '';
59
334
  }
60
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
+
61
342
  function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
62
343
  return readNonEmptyString(event.targetSessionId)
63
344
  || readNonEmptyString(event.sessionId)
@@ -72,6 +353,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
72
353
  'agent:stopped',
73
354
  'agent:ready',
74
355
  'monitor:long_generating',
356
+ 'refine:accepted',
357
+ 'refine:completed',
358
+ 'refine:failed',
75
359
  ]);
76
360
 
77
361
  const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
@@ -86,10 +370,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
86
370
  }
87
371
 
88
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
+ : '';
89
382
  const parts = [
90
383
  readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
91
384
  readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
92
385
  readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
386
+ diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
387
+ finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
93
388
  ].filter(Boolean);
94
389
  return parts.length > 0 ? ` (${parts.join('; ')})` : '';
95
390
  }
@@ -116,7 +411,7 @@ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boole
116
411
  function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
117
412
  if (!sessionId && !nodeId) return false;
118
413
  const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
119
- const entries = readLedgerEntries(meshId);
414
+ const entries = readLedgerEntries(meshId, { tail: 200 });
120
415
  for (let i = entries.length - 1; i >= 0; i--) {
121
416
  const entry = entries[i];
122
417
  const timestamp = new Date(entry.timestamp).getTime();
@@ -140,6 +435,403 @@ function shouldSuppressIntentionalCleanupStop(args: {
140
435
  return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
141
436
  }
142
437
 
438
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
439
+
440
+ function hasFingerprintSeen(fingerprint: string): boolean {
441
+ try {
442
+ return MeshRuntimeStore.getInstance().hasCompletionFingerprint(fingerprint);
443
+ } catch {
444
+ return false;
445
+ }
446
+ }
447
+
448
+ function recordFingerprintSeen(fingerprint: string): void {
449
+ try {
450
+ const db = MeshRuntimeStore.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
+ taskId?: string;
504
+ nodeId?: string;
505
+ }): boolean {
506
+ const fingerprint = buildMeshCompletionFingerprint(args);
507
+ if (!fingerprint) return false;
508
+ if (hasFingerprintSeen(fingerprint)) {
509
+ // Suppressed duplicate — but if we have a taskId and it differs from what the
510
+ // fingerprint was stamped for, record a conflict diagnostic so it doesn't disappear silently.
511
+ // (We can't recover the original taskId from the fingerprint alone, so we record
512
+ // the conflicting taskId/session as a diagnostic for coordinator inspection.)
513
+ if (args.taskId) {
514
+ recordCompletionConflict({
515
+ meshId: args.meshId,
516
+ fingerprint,
517
+ conflictingTaskId: args.taskId,
518
+ conflictingSessionId: args.sessionId,
519
+ event: args.event,
520
+ });
521
+ }
522
+ return true;
523
+ }
524
+ recordFingerprintSeen(fingerprint);
525
+ return false;
526
+ }
527
+
528
+ function isDuplicateMeshApprovalEvent(args: {
529
+ meshId: string;
530
+ sessionId: string;
531
+ providerType?: string;
532
+ timestamp?: number | null;
533
+ modalMessage?: string;
534
+ modalButtons?: unknown;
535
+ }): boolean {
536
+ const modalButtons = Array.isArray(args.modalButtons)
537
+ ? args.modalButtons.map(button => String(button).trim()).filter(Boolean)
538
+ : [];
539
+ const approvalIdentity = Number.isFinite(args.timestamp)
540
+ ? String(args.timestamp)
541
+ : JSON.stringify({ message: args.modalMessage || '', buttons: modalButtons });
542
+ if (!approvalIdentity || approvalIdentity === '{"message":"","buttons":[]}') return false;
543
+ const fingerprint = [
544
+ args.meshId,
545
+ 'agent:waiting_approval',
546
+ args.sessionId,
547
+ args.providerType || '',
548
+ approvalIdentity,
549
+ ].join('::');
550
+ if (hasFingerprintSeen(fingerprint)) return true;
551
+ recordFingerprintSeen(fingerprint);
552
+ return false;
553
+ }
554
+
555
+ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
556
+ const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
557
+ if (!fingerprint) return false;
558
+ if (hasFingerprintSeen(fingerprint)) return true;
559
+ recordFingerprintSeen(fingerprint);
560
+ return false;
561
+ }
562
+
563
+ function findRecentTerminalLedgerEvidence(args: {
564
+ meshId: string;
565
+ sessionId?: string;
566
+ nodeId?: string;
567
+ }): { id: string; kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
568
+ if (!args.sessionId && !args.nodeId) return null;
569
+ // Tail-limit: 200 entries gives a wide enough window to catch terminal events for active
570
+ // sessions while avoiding a full O(n) scan. If a terminal is older than 200 entries,
571
+ // the MeshRuntimeStore fingerprint dedup will still block duplicate processing downstream.
572
+ const entries = readLedgerEntries(args.meshId, { tail: 200 });
573
+ for (let i = entries.length - 1; i >= 0; i--) {
574
+ const entry = entries[i];
575
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
576
+ if (args.sessionId && entry.sessionId === args.sessionId) {
577
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
578
+ }
579
+ if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
580
+ return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
581
+ }
582
+ }
583
+ return null;
584
+ }
585
+
586
+ // Returns true when a task_dispatched entry for the given session appears AFTER the terminal
587
+ // entry (identified by terminalId) in ledger order. Positional (append) order is used rather
588
+ // than timestamp comparison because both entries may share the same millisecond.
589
+ function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean {
590
+ // 200-entry window matches findRecentTerminalLedgerEvidence for consistency.
591
+ const entries = readLedgerEntries(meshId, { tail: 200 });
592
+ let pastTerminal = false;
593
+ for (const entry of entries) {
594
+ if (!pastTerminal) {
595
+ if (entry.id === terminalId) pastTerminal = true;
596
+ continue;
597
+ }
598
+ if (entry.kind === 'task_dispatched' && entry.sessionId === sessionId) return true;
599
+ }
600
+ return false;
601
+ }
602
+
603
+ function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: string): boolean {
604
+ // Some dispatch paths can persist task_dispatched before the direct-dispatch DB row is
605
+ // available. Recover routing from ledger order so coordinator self-targets still emit
606
+ // task_completed and pendingCoordinatorEvents.
607
+ const entries = readLedgerEntries(meshId, { tail: 200 });
608
+ for (let i = entries.length - 1; i >= 0; i--) {
609
+ const entry = entries[i];
610
+ if (entry.sessionId !== sessionId) continue;
611
+ if (entry.kind === 'task_completed' || entry.kind === 'task_failed' || entry.kind === 'task_stalled') {
612
+ return false;
613
+ }
614
+ if (entry.kind === 'task_dispatched' && entry.payload?.source === 'direct') {
615
+ return true;
616
+ }
617
+ }
618
+ return false;
619
+ }
620
+
621
+ function findDirectDispatchLedgerEntry(args: {
622
+ meshId: string;
623
+ taskId: string;
624
+ sessionId?: string;
625
+ }): { id: string; timestamp: string; nodeId?: string; sessionId?: string; providerType?: string; payload: Record<string, unknown> } | null {
626
+ const entries = readLedgerEntries(args.meshId, { tail: 500 });
627
+ for (let i = entries.length - 1; i >= 0; i--) {
628
+ const entry = entries[i];
629
+ if (entry.kind !== 'task_dispatched') continue;
630
+ const payloadTaskId = readNonEmptyString(entry.payload?.taskId);
631
+ if (payloadTaskId !== args.taskId) continue;
632
+ if (args.sessionId && entry.sessionId && entry.sessionId !== args.sessionId) continue;
633
+ return {
634
+ id: entry.id,
635
+ timestamp: entry.timestamp,
636
+ nodeId: entry.nodeId,
637
+ sessionId: entry.sessionId,
638
+ providerType: entry.providerType,
639
+ payload: entry.payload || {},
640
+ };
641
+ }
642
+ return null;
643
+ }
644
+
645
+ function hasTerminalLedgerAfterDispatch(args: {
646
+ meshId: string;
647
+ taskId: string;
648
+ sessionId?: string;
649
+ dispatchEntryId?: string;
650
+ dispatchTimestamp?: string;
651
+ }): boolean {
652
+ const entries = readLedgerEntries(args.meshId, { tail: 500 });
653
+ let afterDispatch = !args.dispatchEntryId && !args.dispatchTimestamp;
654
+ const dispatchTime = args.dispatchTimestamp ? new Date(args.dispatchTimestamp).getTime() : Number.NaN;
655
+ for (const entry of entries) {
656
+ if (!afterDispatch) {
657
+ if (args.dispatchEntryId && entry.id === args.dispatchEntryId) {
658
+ afterDispatch = true;
659
+ continue;
660
+ }
661
+ if (!args.dispatchEntryId && Number.isFinite(dispatchTime)) {
662
+ const entryTime = new Date(entry.timestamp).getTime();
663
+ if (Number.isFinite(entryTime) && entryTime >= dispatchTime) afterDispatch = true;
664
+ }
665
+ if (!afterDispatch) continue;
666
+ }
667
+ if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
668
+ const terminalTaskId = readNonEmptyString(entry.payload?.taskId);
669
+ if (terminalTaskId && terminalTaskId === args.taskId) return true;
670
+ if (terminalTaskId && terminalTaskId !== args.taskId) continue;
671
+ if (args.sessionId && entry.sessionId === args.sessionId) return true;
672
+ }
673
+ return false;
674
+ }
675
+
676
+ export function reconcileDirectDispatchCompletionFromTranscript(args: {
677
+ meshId: string;
678
+ nodeId?: string;
679
+ sessionId: string;
680
+ providerType?: string;
681
+ providerSessionId?: string;
682
+ taskId: string;
683
+ finalSummary: string;
684
+ transcriptMessageAt?: string;
685
+ completedAt?: string;
686
+ targetCoordinatorDaemonId?: string;
687
+ source?: string;
688
+ }): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
689
+ const finalSummary = readNonEmptyString(args.finalSummary);
690
+ if (!args.meshId || !args.taskId || !args.sessionId || !finalSummary) {
691
+ return { reconciled: false, reason: 'missing_required_completion_evidence' };
692
+ }
693
+
694
+ const dispatch = findDirectDispatchLedgerEntry({
695
+ meshId: args.meshId,
696
+ taskId: args.taskId,
697
+ sessionId: args.sessionId,
698
+ });
699
+ if (hasTerminalLedgerAfterDispatch({
700
+ meshId: args.meshId,
701
+ taskId: args.taskId,
702
+ sessionId: args.sessionId,
703
+ dispatchEntryId: dispatch?.id,
704
+ dispatchTimestamp: dispatch?.timestamp,
705
+ })) {
706
+ return { reconciled: false, alreadyTerminal: true, reason: 'terminal_ledger_entry_exists' };
707
+ }
708
+
709
+ const nodeId = readNonEmptyString(args.nodeId) || dispatch?.nodeId;
710
+ const providerType = readNonEmptyString(args.providerType) || dispatch?.providerType || readNonEmptyString(dispatch?.payload.providerType);
711
+ const completedAt = args.completedAt || new Date().toISOString();
712
+ const evidence = buildTaskCompletionEvidence({
713
+ event: 'agent:generating_completed',
714
+ nodeId: nodeId || 'unknown',
715
+ sessionId: args.sessionId,
716
+ providerType,
717
+ providerSessionId: readNonEmptyString(args.providerSessionId),
718
+ finalSummary,
719
+ completedAt,
720
+ });
721
+ const workerResult = evidence.workerResult;
722
+ const dispatchTime = dispatch?.timestamp ? new Date(dispatch.timestamp).getTime() : Number.NaN;
723
+ const transcriptTime = args.transcriptMessageAt ? new Date(args.transcriptMessageAt).getTime() : Number.NaN;
724
+ const transcriptAfterDispatch = Number.isFinite(dispatchTime) && Number.isFinite(transcriptTime) && transcriptTime >= dispatchTime;
725
+ if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
726
+ return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
727
+ }
728
+ const workerFailed = workerResult.status === 'failed' || (workerResult.status !== 'completed' && workerResult.errors.length > 0);
729
+ const kind: MeshLedgerKind = workerFailed ? 'task_failed' : 'task_completed';
730
+
731
+ const entry = appendLedgerEntry(args.meshId, {
732
+ kind,
733
+ nodeId: nodeId || undefined,
734
+ sessionId: args.sessionId,
735
+ providerType: providerType || undefined,
736
+ payload: {
737
+ event: 'agent:generating_completed',
738
+ source: args.source || 'direct_task_transcript_reconciliation',
739
+ taskId: args.taskId,
740
+ providerSessionId: readNonEmptyString(args.providerSessionId),
741
+ finalSummary,
742
+ workerResult,
743
+ completionDiagnostic: {
744
+ reason: 'direct_task_transcript_reconciliation',
745
+ dispatchEntryId: dispatch?.id,
746
+ dispatchTimestamp: dispatch?.timestamp,
747
+ transcriptMessageAt: readNonEmptyString(args.transcriptMessageAt),
748
+ transcriptFinalAssistantPresent: true,
749
+ },
750
+ evidence,
751
+ },
752
+ });
753
+ updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
754
+ setImmediate(() => cleanupTerminalDirectDispatches());
755
+ queuePendingMeshCoordinatorEvent({
756
+ event: kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped',
757
+ meshId: args.meshId,
758
+ nodeLabel: nodeId ? `Node '${nodeId}'` : 'Remote agent',
759
+ nodeId: nodeId || undefined,
760
+ metadataEvent: {
761
+ targetSessionId: args.sessionId,
762
+ providerType: providerType || undefined,
763
+ providerSessionId: readNonEmptyString(args.providerSessionId),
764
+ finalSummary,
765
+ taskId: args.taskId,
766
+ workerResult,
767
+ completionDiagnostic: {
768
+ reason: 'direct_task_transcript_reconciliation',
769
+ terminalLedgerKind: kind,
770
+ terminalLedgerId: entry.id,
771
+ },
772
+ },
773
+ coordinatorMessage: undefined,
774
+ queuedAt: Date.now(),
775
+ ...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
776
+ });
777
+
778
+ return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
779
+ }
780
+
781
+ function buildLongGeneratingCompletionReconciliation(args: {
782
+ meshId: string;
783
+ nodeId?: string;
784
+ nodeLabel: string;
785
+ metadataEvent: Record<string, unknown>;
786
+ sourceInstanceId?: string;
787
+ }): Record<string, unknown> | null {
788
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
789
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
790
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
791
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
792
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
793
+ const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
794
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
795
+ const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
796
+ const explicitCompletionEvidence = Boolean(
797
+ finalSummary
798
+ || workerResult
799
+ || completionDiagnostic?.finalAssistantPresent === true
800
+ || status === 'idle'
801
+ || status === 'ready'
802
+ || status === 'completed',
803
+ );
804
+ if (explicitCompletionEvidence) {
805
+ return {
806
+ ...args.metadataEvent,
807
+ targetSessionId: sessionId,
808
+ providerType,
809
+ providerSessionId,
810
+ finalSummary,
811
+ source: 'long_generating_reconciliation',
812
+ reconciledFromEvent: 'monitor:long_generating',
813
+ timestamp: args.metadataEvent.timestamp ?? Date.now(),
814
+ completionDiagnostic: {
815
+ ...(completionDiagnostic || {}),
816
+ reconciliationReason: 'provider_completion_evidence',
817
+ },
818
+ };
819
+ }
820
+
821
+ const terminal = findRecentTerminalLedgerEvidence({
822
+ meshId: args.meshId,
823
+ sessionId: sessionId || undefined,
824
+ nodeId: nodeId || undefined,
825
+ });
826
+ if (!terminal) return null;
827
+ return {
828
+ ...args.metadataEvent,
829
+ source: 'long_generating_terminal_ledger_suppression',
830
+ terminalLedgerKind: terminal.kind,
831
+ terminalLedgerAt: terminal.timestamp,
832
+ };
833
+ }
834
+
143
835
 
144
836
  export function tryAssignQueueTask(
145
837
  components: DaemonComponents,
@@ -148,7 +840,10 @@ export function tryAssignQueueTask(
148
840
  sessionId: string,
149
841
  providerType: string
150
842
  ): boolean {
151
- const task = claimNextTask(meshId, nodeId, sessionId);
843
+ const mesh = getMeshWithCache(components, meshId);
844
+ const node = mesh?.nodes.find((n: any) => n.id === nodeId);
845
+ const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
846
+ const task = claimNextTask(meshId, nodeId, sessionId, capabilityTags);
152
847
  if (!task) {
153
848
  return false;
154
849
  }
@@ -156,34 +851,67 @@ export function tryAssignQueueTask(
156
851
  LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
157
852
 
158
853
  // Check if the node is remote
159
- const mesh = getMeshWithCache(components, meshId);
160
- const node = mesh?.nodes.find((n: any) => n.id === nodeId);
161
-
162
854
  // If the node is explicitly remote and we have a dispatch mechanism, route via P2P
163
855
  if (node?.daemonId && components.dispatchMeshCommand) {
164
856
  const isLocalNode = components.cliManager.adapters.has(sessionId);
165
857
  if (!isLocalNode) {
858
+ // Create delivery record before attempting P2P send
859
+ const delivery = createSessionDelivery({
860
+ meshId,
861
+ nodeId,
862
+ sessionId,
863
+ providerType,
864
+ taskId: task.id,
865
+ kind: 'task',
866
+ message: task.message,
867
+ status: 'delivering',
868
+ });
166
869
  components.dispatchMeshCommand(node.daemonId, 'agent_command', {
167
870
  targetSessionId: sessionId,
168
871
  cliType: providerType,
169
872
  action: 'send_chat',
170
873
  message: task.message,
874
+ }).then(() => {
875
+ updateSessionDeliveryStatus(delivery.id, 'delivered');
171
876
  }).catch((e: any) => {
172
877
  LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
173
- updateTaskStatus(meshId, task.id, 'failed');
878
+ updateSessionDeliveryStatus(delivery.id, 'failed', { lastError: e?.message, incrementAttempt: true });
879
+ // Revert to pending so the task can be retried rather than permanently failing
880
+ updateTaskStatus(meshId, task.id, 'pending');
881
+ try {
882
+ appendLedgerEntry(meshId, {
883
+ kind: 'dispatch_failed' as any,
884
+ nodeId,
885
+ sessionId,
886
+ payload: { taskId: task.id, deliveryId: delivery.id, error: e?.message, retryable: true },
887
+ });
888
+ } catch { /* ledger write is best-effort */ }
174
889
  });
175
890
  return true;
176
891
  }
177
892
  }
178
893
 
179
- // Local routing
894
+ // Local routing — create delivery record before send_chat
895
+ const delivery = createSessionDelivery({
896
+ meshId,
897
+ nodeId,
898
+ sessionId,
899
+ providerType,
900
+ taskId: task.id,
901
+ kind: 'task',
902
+ message: task.message,
903
+ status: 'delivering',
904
+ });
180
905
  components.cliManager.handleCliCommand('agent_command', {
181
906
  targetSessionId: sessionId,
182
907
  cliType: providerType,
183
908
  action: 'send_chat',
184
909
  message: task.message,
910
+ }).then(() => {
911
+ updateSessionDeliveryStatus(delivery.id, 'delivered');
185
912
  }).catch((e: any) => {
186
913
  LOG.error('MeshQueue', `Failed to dispatch task locally to node ${nodeId}: ${e?.message}`);
914
+ updateSessionDeliveryStatus(delivery.id, 'failed', { lastError: e?.message, incrementAttempt: true });
187
915
  updateTaskStatus(meshId, task.id, 'failed');
188
916
  });
189
917
 
@@ -194,6 +922,13 @@ const autoLaunchInProgress = new Set<string>();
194
922
  const autoLaunchCooldownUntil = new Map<string, number>();
195
923
  const AUTO_LAUNCH_COOLDOWN_MS = 5_000;
196
924
 
925
+ function sweepExpiredCooldowns(): void {
926
+ const now = Date.now();
927
+ for (const [key, until] of autoLaunchCooldownUntil) {
928
+ if (now >= until) autoLaunchCooldownUntil.delete(key);
929
+ }
930
+ }
931
+
197
932
  function normalizeProviderPriority(policy: unknown): string[] {
198
933
  const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
199
934
  ? (policy as Record<string, unknown>).providerPriority
@@ -267,6 +1002,10 @@ function nodeHasActiveAssignment(meshId: string, nodeId: string): boolean {
267
1002
  return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedNodeId === nodeId);
268
1003
  }
269
1004
 
1005
+ function sessionHasActiveAssignment(meshId: string, sessionId: string): boolean {
1006
+ return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedSessionId === sessionId);
1007
+ }
1008
+
270
1009
  function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
271
1010
  return components.instanceManager.getByCategory('cli').filter((inst: any) => {
272
1011
  const state = inst.getState();
@@ -396,12 +1135,14 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
396
1135
  const nodeId = readNonEmptyString(node?.id);
397
1136
  if (!nodeId) continue;
398
1137
  const launchKey = `${meshId}:${nodeId}`;
1138
+ const now = Date.now();
399
1139
  const cooldownUntil = autoLaunchCooldownUntil.get(launchKey) || 0;
1140
+ if (cooldownUntil > 0 && now >= cooldownUntil) autoLaunchCooldownUntil.delete(launchKey);
400
1141
  if (autoLaunchInProgress.has(launchKey)) {
401
1142
  markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_in_progress', nodeId });
402
1143
  continue;
403
1144
  }
404
- if (Date.now() < cooldownUntil) {
1145
+ if (now < cooldownUntil) {
405
1146
  markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_cooldown', nodeId });
406
1147
  continue;
407
1148
  }
@@ -451,13 +1192,13 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
451
1192
  if (!launchResult?.success) {
452
1193
  const reason = launchResult?.error || 'launch_cli_failed';
453
1194
  markAutoLaunch(meshId, task.id, { status: 'failed', reason, nodeId, providerType: resolved.providerType });
454
- autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
1195
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
455
1196
  return false;
456
1197
  }
457
1198
  const sessionId = readNonEmptyString(launchResult.sessionId) || readNonEmptyString(launchResult.id) || readNonEmptyString(launchResult.runtimeSessionId);
458
1199
  if (!sessionId) {
459
1200
  markAutoLaunch(meshId, task.id, { status: 'failed', reason: 'launch_missing_session_id', nodeId, providerType: resolved.providerType });
460
- autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
1201
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
461
1202
  return false;
462
1203
  }
463
1204
  markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId });
@@ -475,13 +1216,69 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
475
1216
  return false;
476
1217
  }
477
1218
 
1219
+ export interface MeshQueueTriggerResult {
1220
+ success: true;
1221
+ meshId: string;
1222
+ pendingBefore: number;
1223
+ assignedBefore: number;
1224
+ pendingAfter: number;
1225
+ assignedAfter: number;
1226
+ claimed: boolean;
1227
+ newlyAssignedTasks: Array<{
1228
+ id: string;
1229
+ nodeId?: string;
1230
+ sessionId?: string;
1231
+ }>;
1232
+ localIdleSessionsChecked: number;
1233
+ remoteIdleSessionsChecked: number;
1234
+ skippedSessions: Array<{
1235
+ nodeId?: string;
1236
+ sessionId?: string;
1237
+ reason: string;
1238
+ status?: string;
1239
+ }>;
1240
+ autoLaunchStarted: boolean;
1241
+ noIdleMeshSessionAvailable?: boolean;
1242
+ }
1243
+
1244
+ function countQueueStatus(meshId: string, status: 'pending' | 'assigned'): number {
1245
+ return getQueue(meshId, { status: [status] as any }).length;
1246
+ }
1247
+
1248
+ function getQueueStatusById(meshId: string): Map<string, string> {
1249
+ return new Map(getQueue(meshId).map(task => [task.id, task.status]));
1250
+ }
1251
+
478
1252
  /**
479
1253
  * Triggers a queue check for all nodes in the mesh.
480
1254
  * Called when a new task is enqueued, in case nodes are already idle.
481
1255
  */
482
- export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<void> {
1256
+ export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<MeshQueueTriggerResult> {
483
1257
  const mesh = getMeshWithCache(components, meshId);
484
- if (!mesh) return;
1258
+ const pendingBefore = countQueueStatus(meshId, 'pending');
1259
+ const assignedBefore = countQueueStatus(meshId, 'assigned');
1260
+ const beforeStatus = getQueueStatusById(meshId);
1261
+ const skippedSessions: MeshQueueTriggerResult['skippedSessions'] = [];
1262
+ let localIdleSessionsChecked = 0;
1263
+ let remoteIdleSessionsChecked = 0;
1264
+ let autoLaunchStarted = false;
1265
+ if (!mesh) {
1266
+ return {
1267
+ success: true,
1268
+ meshId,
1269
+ pendingBefore,
1270
+ assignedBefore,
1271
+ pendingAfter: pendingBefore,
1272
+ assignedAfter: assignedBefore,
1273
+ claimed: false,
1274
+ newlyAssignedTasks: [],
1275
+ localIdleSessionsChecked,
1276
+ remoteIdleSessionsChecked,
1277
+ skippedSessions: [{ reason: 'mesh_not_found' }],
1278
+ autoLaunchStarted,
1279
+ noIdleMeshSessionAvailable: true,
1280
+ };
1281
+ }
485
1282
 
486
1283
  // Find all CLI instances that belong to this mesh and are idle
487
1284
  const cliInstances = components.instanceManager.getByCategory('cli');
@@ -498,30 +1295,147 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
498
1295
  // Only genuinely idle live sessions can pull work. Restored/stopped
499
1296
  // records are kept for transcript/recovery visibility, but assigning
500
1297
  // queue items to them strands tasks in assigned/pending without chat.
501
- if (!isIdleSessionState(state)) continue;
1298
+ if (!isIdleSessionState(state)) {
1299
+ const status = readNonEmptyString(state.status).toLowerCase();
1300
+ skippedSessions.push({
1301
+ nodeId,
1302
+ sessionId: readNonEmptyString(state.instanceId),
1303
+ reason: isTerminalSessionStatus(status) ? 'terminal_session' : 'session_not_idle',
1304
+ status: status || undefined,
1305
+ });
1306
+ continue;
1307
+ }
502
1308
 
503
1309
  const sessionId = state.instanceId;
504
1310
  const providerType = state.type || readNonEmptyString(settings.providerType);
505
1311
 
506
1312
  if (providerType) {
507
1313
  // Try to assign a task to this idle node
1314
+ localIdleSessionsChecked += 1;
508
1315
  tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType);
1316
+ } else {
1317
+ skippedSessions.push({
1318
+ nodeId,
1319
+ sessionId,
1320
+ reason: 'provider_type_missing',
1321
+ });
509
1322
  }
510
1323
  }
511
1324
 
512
1325
  // Also check known idle remote sessions
513
- for (const [key, idle] of remoteIdleSessions.entries()) {
1326
+ let remoteSessions: Array<{ nodeId: string; sessionId: string; providerType: string }> = [];
1327
+ try {
1328
+ remoteSessions = MeshRuntimeStore.getInstance().getRemoteIdleSessions();
1329
+ } catch { /* best-effort */ }
1330
+
1331
+ for (const idle of remoteSessions) {
514
1332
  // Find if this node is in the same mesh
515
1333
  const node = mesh.nodes.find((n: any) => n.id === idle.nodeId);
516
1334
  if (node) {
1335
+ remoteIdleSessionsChecked += 1;
517
1336
  const assigned = tryAssignQueueTask(components, meshId, idle.nodeId, idle.sessionId, idle.providerType);
518
1337
  if (assigned) {
519
- remoteIdleSessions.delete(key);
1338
+ try {
1339
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(idle.nodeId, idle.sessionId);
1340
+ } catch { /* best-effort */ }
520
1341
  }
521
1342
  }
522
1343
  }
523
1344
 
524
- await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
1345
+ autoLaunchStarted = await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
1346
+ const afterQueue = getQueue(meshId);
1347
+ const pendingAfter = afterQueue.filter(task => task.status === 'pending').length;
1348
+ const assignedAfter = afterQueue.filter(task => task.status === 'assigned').length;
1349
+ const newlyAssignedTasks = afterQueue
1350
+ .filter(task => task.status === 'assigned' && beforeStatus.get(task.id) !== 'assigned')
1351
+ .map(task => ({
1352
+ id: task.id,
1353
+ nodeId: task.assignedNodeId,
1354
+ sessionId: task.assignedSessionId,
1355
+ }));
1356
+ return {
1357
+ success: true,
1358
+ meshId,
1359
+ pendingBefore,
1360
+ assignedBefore,
1361
+ pendingAfter,
1362
+ assignedAfter,
1363
+ claimed: newlyAssignedTasks.length > 0,
1364
+ newlyAssignedTasks,
1365
+ localIdleSessionsChecked,
1366
+ remoteIdleSessionsChecked,
1367
+ skippedSessions,
1368
+ autoLaunchStarted,
1369
+ ...(pendingAfter > 0 && newlyAssignedTasks.length === 0 && localIdleSessionsChecked === 0 && remoteIdleSessionsChecked === 0 && !autoLaunchStarted
1370
+ ? { noIdleMeshSessionAvailable: true }
1371
+ : {}),
1372
+ };
1373
+ }
1374
+
1375
+ async function maybeAutoFastForwardIdleNode(components: DaemonComponents, args: {
1376
+ meshId: string;
1377
+ nodeId: string;
1378
+ sessionId?: string;
1379
+ providerType?: string;
1380
+ }): Promise<void> {
1381
+ const mesh = getMeshWithCache(components, args.meshId);
1382
+ const node = mesh?.nodes?.find((candidate: any) => candidate?.id === args.nodeId || candidate?.nodeId === args.nodeId);
1383
+ const workspace = readNonEmptyString(node?.workspace);
1384
+ if (!workspace) return;
1385
+ if (!existsSync(workspace)) return;
1386
+
1387
+ const throttleKey = `${args.meshId}:${args.nodeId}`;
1388
+ const now = Date.now();
1389
+ const lastAttempt = idleAutoFastForwardLastAttempt.get(throttleKey) || 0;
1390
+ if (now - lastAttempt < IDLE_AUTO_FAST_FORWARD_THROTTLE_MS) return;
1391
+ idleAutoFastForwardLastAttempt.set(throttleKey, now);
1392
+
1393
+ const submoduleIgnorePaths = Array.isArray(node?.policy?.submoduleIgnorePaths)
1394
+ ? node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
1395
+ : undefined;
1396
+ try {
1397
+ const dryRun = await fastForwardMeshNode({
1398
+ meshId: args.meshId,
1399
+ nodeId: args.nodeId,
1400
+ workspace,
1401
+ execute: false,
1402
+ dryRun: true,
1403
+ updateSubmodules: false,
1404
+ submoduleIgnorePaths,
1405
+ trigger: 'idle_auto',
1406
+ });
1407
+ if (!dryRun || dryRun.code !== 'fast_forward_available' || dryRun.allowed !== true) return;
1408
+ await fastForwardMeshNode({
1409
+ meshId: args.meshId,
1410
+ nodeId: args.nodeId,
1411
+ workspace,
1412
+ execute: true,
1413
+ dryRun: false,
1414
+ updateSubmodules: false,
1415
+ submoduleIgnorePaths,
1416
+ trigger: 'idle_auto',
1417
+ });
1418
+ } catch (e: any) {
1419
+ LOG.warn('MeshFastForward', `Idle auto fast-forward check failed for ${args.nodeId}: ${e?.message || e}`);
1420
+ }
1421
+ }
1422
+
1423
+ function runIdleMaintenanceThenAssignQueue(components: DaemonComponents, args: {
1424
+ meshId: string;
1425
+ nodeId: string;
1426
+ sessionId: string;
1427
+ providerType: string;
1428
+ }): void {
1429
+ setImmediate(() => {
1430
+ maybeAutoFastForwardIdleNode(components, args)
1431
+ .finally(() => {
1432
+ try {
1433
+ tryAssignQueueTask(components, args.meshId, args.nodeId, args.sessionId, args.providerType);
1434
+ } catch (e: any) {
1435
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${args.nodeId}: ${e?.message || e}`);
1436
+ }
1437
+ });
1438
+ });
525
1439
  }
526
1440
 
527
1441
  function buildMeshSystemMessage(args: {
@@ -532,6 +1446,9 @@ function buildMeshSystemMessage(args: {
532
1446
  }): string {
533
1447
  const metadata = formatCompletionMetadata(args.metadataEvent);
534
1448
  if (args.event === 'agent:generating_completed') {
1449
+ if (args.metadataEvent.source === 'long_generating_reconciliation') {
1450
+ 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.`;
1451
+ }
535
1452
  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.`;
536
1453
  }
537
1454
  if (args.event === 'agent:waiting_approval') {
@@ -563,7 +1480,72 @@ function buildMeshSystemMessage(args: {
563
1480
  return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
564
1481
  }
565
1482
  if (args.event === 'monitor:long_generating') {
566
- 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.`;
1483
+ 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.`;
1484
+ }
1485
+ if (args.event === 'refine:accepted') {
1486
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1487
+ 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.`;
1488
+ }
1489
+ if (args.event === 'refine:completed') {
1490
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1491
+ const result = readRecord(args.metadataEvent.result);
1492
+ const validationSummary = readRecord(result?.validationSummary);
1493
+ const patchEquivalence = readRecord(result?.patchEquivalence);
1494
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
1495
+ const validationStatus = readNonEmptyString(validationSummary?.status);
1496
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
1497
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
1498
+ const into = readNonEmptyString(result?.into);
1499
+ const branch = readNonEmptyString(result?.branch);
1500
+ const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
1501
+ const convergenceStatus = readNonEmptyString(finalConvergence?.status);
1502
+ const nextStep = readNonEmptyString(result?.nextStep)
1503
+ || readNonEmptyString(finalConvergence?.nextStep)
1504
+ || 'Continue from the updated mesh state.';
1505
+ const details = [
1506
+ jobId ? `job_id=${jobId}` : '',
1507
+ branch && into ? `${branch}→${into}` : '',
1508
+ validationStatus ? `validation=${validationStatus}` : '',
1509
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
1510
+ mergeStatus ? `merge=${mergeStatus}` : '',
1511
+ convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
1512
+ ].filter(Boolean).join('; ');
1513
+ return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
1514
+ }
1515
+ if (args.event === 'refine:failed') {
1516
+ const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
1517
+ const result = readRecord(args.metadataEvent.result);
1518
+ const validationSummary = readRecord(result?.validationSummary);
1519
+ const patchEquivalence = readRecord(result?.patchEquivalence);
1520
+ const finalConvergence = readRecord(result?.finalBranchConvergenceState);
1521
+ const code = readNonEmptyString(result?.code);
1522
+ const error = readNonEmptyString(result?.error);
1523
+ const validationStatus = readNonEmptyString(validationSummary?.status);
1524
+ const patchStatus = readNonEmptyString(patchEquivalence?.status)
1525
+ || (patchEquivalence?.equivalent === true ? 'passed' : '');
1526
+ const mergeStatus = result?.merged === true
1527
+ ? 'merged'
1528
+ : finalConvergence?.merged === false
1529
+ ? 'not_merged'
1530
+ : '';
1531
+ const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
1532
+ || readNonEmptyString(finalConvergence?.status);
1533
+ const blockedReason = readNonEmptyString(result?.blockedReason);
1534
+ const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
1535
+ const details = [
1536
+ jobId ? `job_id=${jobId}` : '',
1537
+ code ? `code=${code}` : '',
1538
+ validationStatus ? `validation=${validationStatus}` : '',
1539
+ patchStatus ? `patch_equivalence=${patchStatus}` : '',
1540
+ mergeStatus ? `merge=${mergeStatus}` : '',
1541
+ convergenceStatus ? `convergence=${convergenceStatus}` : '',
1542
+ blockedReason ? `reason=${blockedReason}` : '',
1543
+ ].filter(Boolean).join('; ');
1544
+ const parts = [
1545
+ `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
1546
+ nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
1547
+ ];
1548
+ return parts.join('\n');
567
1549
  }
568
1550
  return '';
569
1551
  }
@@ -578,6 +1560,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
578
1560
  }) {
579
1561
  const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
580
1562
  const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1563
+
1564
+ // Resolve coordinator ownership early — used in fingerprinting and coordinator routing.
1565
+ const sourceSession = args.sourceInstanceId
1566
+ ? components.instanceManager.getInstance(args.sourceInstanceId)
1567
+ : undefined;
1568
+ const workerCoordinatorDaemonId = readNonEmptyString(
1569
+ (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
1570
+ );
1571
+ const localDaemonId = readNonEmptyString(loadConfig().machineId);
581
1572
  const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
582
1573
  event: args.event,
583
1574
  meshId: args.meshId,
@@ -587,89 +1578,230 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
587
1578
  });
588
1579
  if (intentionalCleanupStop) {
589
1580
  if (eventSessionId && eventNodeId) {
590
- remoteIdleSessions.delete(`${eventNodeId}:${eventSessionId}`);
1581
+ try {
1582
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(eventNodeId, eventSessionId);
1583
+ } catch { /* best-effort */ }
591
1584
  }
592
1585
  LOG.info('MeshEvents', `Suppressed ${args.event} for intentionally cleanup-stopped session ${eventSessionId || '(unknown session)'}`);
593
1586
  return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
594
1587
  }
595
1588
 
1589
+ if (args.event === 'monitor:long_generating') {
1590
+ const reconciledCompletion = buildLongGeneratingCompletionReconciliation({
1591
+ meshId: args.meshId,
1592
+ nodeId: args.nodeId,
1593
+ nodeLabel: args.nodeLabel,
1594
+ metadataEvent: args.metadataEvent,
1595
+ sourceInstanceId: args.sourceInstanceId,
1596
+ });
1597
+ if (reconciledCompletion?.source === 'long_generating_reconciliation') {
1598
+ LOG.info('MeshEvents', `Reconciled long-generating monitor to completion for session ${eventSessionId || '(unknown session)'}`);
1599
+ return injectMeshSystemMessage(components, {
1600
+ ...args,
1601
+ event: 'agent:generating_completed',
1602
+ metadataEvent: reconciledCompletion,
1603
+ });
1604
+ }
1605
+ if (reconciledCompletion?.source === 'long_generating_terminal_ledger_suppression') {
1606
+ LOG.info('MeshEvents', `Suppressed long-generating monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
1607
+ return {
1608
+ success: true,
1609
+ forwarded: 0,
1610
+ suppressed: true,
1611
+ terminalLedgerEvidence: true,
1612
+ terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
1613
+ };
1614
+ }
1615
+ }
1616
+
1617
+ if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
1618
+ LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
1619
+ return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
1620
+ }
1621
+
1622
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
1623
+ if (args.event === 'agent:waiting_approval' && eventSessionId) {
1624
+ const duplicateApproval = isDuplicateMeshApprovalEvent({
1625
+ meshId: args.meshId,
1626
+ sessionId: eventSessionId,
1627
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1628
+ timestamp: eventTimestamp,
1629
+ modalMessage: readNonEmptyString(args.metadataEvent.modalMessage) || undefined,
1630
+ modalButtons: args.metadataEvent.modalButtons,
1631
+ });
1632
+ if (duplicateApproval) {
1633
+ LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
1634
+ return { success: true, forwarded: 0, suppressed: true, duplicateApproval: true };
1635
+ }
1636
+ }
1637
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
1638
+ const terminal = findRecentTerminalLedgerEvidence({
1639
+ meshId: args.meshId,
1640
+ sessionId: eventSessionId,
1641
+ nodeId: eventNodeId || undefined,
1642
+ });
1643
+ if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
1644
+ // If a new task_dispatched was recorded for this session after the prior terminal,
1645
+ // this completion belongs to the new task — never suppress it as a duplicate.
1646
+ const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
1647
+ if (!newDispatchAfterTerminal) {
1648
+ const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
1649
+ const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
1650
+ const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
1651
+ const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
1652
+ if (
1653
+ (terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
1654
+ || (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
1655
+ || args.metadataEvent.source === 'long_generating_reconciliation'
1656
+ ) {
1657
+ LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
1658
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
1659
+ }
1660
+ }
1661
+ }
1662
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
1663
+ meshId: args.meshId,
1664
+ event: args.event,
1665
+ sessionId: eventSessionId,
1666
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1667
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
1668
+ timestamp: eventTimestamp,
1669
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1670
+ // Scope dedup to the coordinator daemon so two coordinators for the same mesh
1671
+ // don't suppress each other's completion events via shared fingerprint table.
1672
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
1673
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
1674
+ nodeId: eventNodeId || undefined,
1675
+ });
1676
+ if (duplicateCompletion) {
1677
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
1678
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
1679
+ }
1680
+ }
1681
+ if (args.event === 'agent:stopped' && eventSessionId) {
1682
+ const duplicateStopped = isDuplicateMeshCompletionEvent({
1683
+ meshId: args.meshId,
1684
+ event: args.event,
1685
+ sessionId: eventSessionId,
1686
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1687
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
1688
+ timestamp: eventTimestamp,
1689
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1690
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
1691
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
1692
+ nodeId: eventNodeId || undefined,
1693
+ });
1694
+ if (duplicateStopped) {
1695
+ LOG.info('MeshEvents', `Suppressed duplicate stopped event for mesh ${args.meshId} session ${eventSessionId}`);
1696
+ return { success: true, forwarded: 0, suppressed: true, duplicateStopped: true };
1697
+ }
1698
+ }
1699
+
596
1700
  // ── Task Queue & Ledger ──
1701
+ // Helpers that keep queue and direct-dispatch status transitions symmetric.
1702
+ // Both paths must move together so buildMeshActiveWork and the coordinator
1703
+ // view stay consistent regardless of which dispatch path was used.
1704
+ function markSessionTerminal(sessionId: string, outcome: 'completed' | 'failed', occurredAtMs?: number | null): { id?: string } | null {
1705
+ const task = updateSessionTaskStatus(args.meshId, sessionId, outcome, {
1706
+ occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
1707
+ });
1708
+ updateDirectDispatchStatus(args.meshId, sessionId, outcome);
1709
+ setImmediate(() => cleanupTerminalDirectDispatches());
1710
+ return task ? { id: task.id } : null;
1711
+ }
1712
+
597
1713
  let completedTaskForLedger: { id?: string } | null = null;
598
1714
  if (args.event === 'agent:generating_completed') {
599
1715
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
600
1716
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
601
1717
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
602
-
1718
+
603
1719
  if (sessionId) {
604
- const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
605
- completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
1720
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed', eventTimestamp);
606
1721
  if (nodeId && providerType) {
607
- // Short delay to allow completion event to propagate before pulling next
608
- setTimeout(() => {
609
- tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
610
- }, 500);
1722
+ runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
611
1723
  }
612
1724
  }
613
1725
  } else if (args.event === 'agent:ready') {
614
1726
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
615
1727
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
616
1728
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
617
- const completedTask = sessionId
618
- ? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
619
- : null;
620
- if (completedTask) {
621
- completedTaskForLedger = { id: completedTask.id };
622
- try {
623
- appendLedgerEntry(args.meshId, {
624
- kind: 'task_completed',
625
- nodeId: nodeId || undefined,
626
- sessionId,
627
- providerType: providerType || undefined,
628
- payload: {
629
- event: args.event,
630
- nodeLabel: args.nodeLabel,
631
- taskId: completedTask.id,
632
- completedViaReady: true,
633
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
634
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
635
- evidence: buildTaskCompletionEvidence({
636
- event: 'agent:ready',
637
- nodeId,
638
- sessionId,
639
- providerType: providerType || undefined,
640
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
641
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
642
- }),
643
- },
644
- });
645
- } catch (e: any) {
646
- LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
1729
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1730
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1731
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1732
+ const hasCompletionEvidence = !!finalSummary || !!workerResult;
1733
+ if (sessionId && hasCompletionEvidence) {
1734
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed');
1735
+ if (completedTaskForLedger) {
1736
+ try {
1737
+ appendLedgerEntry(args.meshId, {
1738
+ kind: 'task_completed',
1739
+ nodeId: nodeId || undefined,
1740
+ sessionId,
1741
+ providerType: providerType || undefined,
1742
+ payload: {
1743
+ event: args.event,
1744
+ nodeLabel: args.nodeLabel,
1745
+ taskId: completedTaskForLedger.id,
1746
+ completedViaReady: true,
1747
+ providerSessionId,
1748
+ finalSummary,
1749
+ workerResult,
1750
+ evidence: buildTaskCompletionEvidence({
1751
+ event: 'agent:ready',
1752
+ nodeId,
1753
+ sessionId,
1754
+ providerType: providerType || undefined,
1755
+ providerSessionId,
1756
+ finalSummary,
1757
+ workerResult,
1758
+ }),
1759
+ },
1760
+ });
1761
+ } catch (e: any) {
1762
+ LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
1763
+ }
647
1764
  }
648
1765
  }
649
-
1766
+
650
1767
  if (sessionId && nodeId && providerType) {
651
- remoteIdleSessions.set(`${nodeId}:${sessionId}`, { nodeId, sessionId, providerType });
652
- setTimeout(() => {
653
- const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
654
- if (assigned) {
655
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
656
- }
657
- }, 500);
1768
+ sweepExpiredRemoteIdleSessions();
1769
+ try {
1770
+ MeshRuntimeStore.getInstance().setRemoteIdleSession(nodeId, sessionId, providerType, Date.now() + REMOTE_IDLE_SESSION_TTL_MS);
1771
+ } catch { /* best-effort */ }
1772
+ setImmediate(() => {
1773
+ maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
1774
+ .finally(() => {
1775
+ try {
1776
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
1777
+ if (assigned) MeshRuntimeStore.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1778
+ } catch (e: any) {
1779
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
1780
+ }
1781
+ });
1782
+ });
658
1783
  }
659
1784
  } else if (args.event === 'agent:generating_started') {
660
1785
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
661
1786
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
662
1787
  if (sessionId && nodeId) {
663
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
1788
+ try {
1789
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1790
+ } catch { /* best-effort */ }
1791
+ }
1792
+ if (sessionId) {
1793
+ updateDirectDispatchStatus(args.meshId, sessionId, 'acked');
664
1794
  }
665
1795
  } else if (args.event === 'agent:stopped') {
666
1796
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
667
1797
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
668
1798
  if (sessionId && nodeId) {
669
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
1799
+ try {
1800
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1801
+ } catch { /* best-effort */ }
670
1802
  }
671
1803
  if (sessionId) {
672
- updateSessionTaskStatus(args.meshId, sessionId, 'failed');
1804
+ completedTaskForLedger = markSessionTerminal(sessionId, 'failed');
673
1805
  }
674
1806
  }
675
1807
 
@@ -679,14 +1811,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
679
1811
  const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
680
1812
  const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
681
1813
  const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
1814
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1815
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1816
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
682
1817
  const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
683
1818
  ? buildTaskCompletionEvidence({
684
1819
  event: 'agent:generating_completed',
685
1820
  nodeId: ledgerNodeId,
686
1821
  sessionId: ledgerSessionId,
687
1822
  providerType: ledgerProviderType,
688
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
689
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1823
+ providerSessionId,
1824
+ finalSummary,
1825
+ workerResult,
690
1826
  })
691
1827
  : undefined;
692
1828
  appendLedgerEntry(args.meshId, {
@@ -698,8 +1834,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
698
1834
  event: args.event,
699
1835
  nodeLabel: args.nodeLabel,
700
1836
  taskId: completedTaskForLedger?.id || undefined,
701
- providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
702
- finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1837
+ providerSessionId,
1838
+ finalSummary,
1839
+ workerResult,
1840
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
1841
+ ? args.metadataEvent.completionDiagnostic
1842
+ : undefined,
703
1843
  evidence: completionEvidence,
704
1844
  },
705
1845
  });
@@ -772,38 +1912,106 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
772
1912
  }
773
1913
  }
774
1914
 
1915
+ const messageText = buildMeshSystemMessage({
1916
+ event: args.event,
1917
+ nodeLabel: args.nodeLabel,
1918
+ metadataEvent: args.metadataEvent,
1919
+ recoveryContext,
1920
+ });
1921
+ if (!messageText) return { success: false, error: 'unsupported mesh event' };
1922
+
775
1923
  const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
776
1924
  const instState = inst.getState();
777
1925
  if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
778
1926
  if (args.sourceInstanceId && instState.instanceId === args.sourceInstanceId) return false;
1927
+ // If the worker knows which coordinator daemon launched it, only route to coordinators
1928
+ // on that specific daemon. This prevents cross-contamination when multiple coordinator
1929
+ // sessions run simultaneously for the same mesh on different daemons.
1930
+ if (workerCoordinatorDaemonId && localDaemonId && workerCoordinatorDaemonId !== localDaemonId) return false;
779
1931
  return true;
780
1932
  });
781
1933
 
1934
+ // Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
1935
+ // synchronously; only buffer them for MCP when no CLI coordinator is present.
1936
+ // Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
1937
+ // receive them regardless of whether a live CLI coordinator session is active.
1938
+ const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
1939
+
782
1940
  if (coordinatorInstances.length === 0) {
783
- // No CLI coordinator session found — buffer for MCP-based coordinators.
784
- if (pendingMeshCoordinatorEvents.length < MAX_PENDING_EVENTS) {
785
- pendingMeshCoordinatorEvents.push({
1941
+ // No local CLI coordinator — buffer for MCP-based coordinator on the target daemon.
1942
+ if (queuePendingMeshCoordinatorEvent({
786
1943
  event: args.event,
787
1944
  meshId: args.meshId,
788
1945
  nodeLabel: args.nodeLabel,
1946
+ nodeId: args.nodeId || undefined,
1947
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
789
1948
  metadataEvent: {
790
1949
  ...args.metadataEvent,
791
1950
  ...(recoveryContext ? { recoveryContext } : {}),
792
1951
  },
1952
+ coordinatorMessage: messageText,
793
1953
  queuedAt: Date.now(),
794
- });
795
- LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1954
+ // Scope to the coordinator daemon that launched this worker so drain
1955
+ // by other coordinators on the same daemon doesn't consume this event.
1956
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
1957
+ })) {
1958
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''})`);
796
1959
  }
797
1960
  return { success: true, forwarded: 0 };
798
1961
  }
799
1962
 
800
- const messageText = buildMeshSystemMessage({
801
- event: args.event,
802
- nodeLabel: args.nodeLabel,
803
- metadataEvent: args.metadataEvent,
804
- recoveryContext,
1963
+ // CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
1964
+ // that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
1965
+ // forwarded directly only — they must not accumulate in the pending queue when a live
1966
+ // coordinator already received them.
1967
+ //
1968
+ // Exception: if ALL live CLI coordinator instances are in a generating/active state
1969
+ // when a refine terminal event fires (e.g. a Codex CLI coordinator that triggered an
1970
+ // async refine job and is still in the generating turn that sent it), the coordinator
1971
+ // cannot immediately receive send_message input. Buffer the event to the pending queue
1972
+ // so it is available via get_pending_mesh_events when the coordinator returns to idle.
1973
+ // Critically: do NOT attempt send_message injection into a generating PTY coordinator for
1974
+ // terminal refine events — injecting text into an active PTY can corrupt the input stream
1975
+ // and leave the coordinator stuck in generating state, unable to process the refine result.
1976
+ const allCoordinatorsGenerating = isRefineTerminalEvent && coordinatorInstances.every((inst) => {
1977
+ const s = inst.getState();
1978
+ const status = readNonEmptyString(s.status).toLowerCase();
1979
+ const activeChatStatus = readNonEmptyString(s.activeChat?.status).toLowerCase();
1980
+ return status === 'generating' || status === 'streaming' || status === 'long_generating'
1981
+ || activeChatStatus === 'generating' || activeChatStatus === 'streaming';
805
1982
  });
806
- if (!messageText) return { success: false, error: 'unsupported mesh event' };
1983
+
1984
+ if (!isRefineTerminalEvent || allCoordinatorsGenerating) {
1985
+ if (queuePendingMeshCoordinatorEvent({
1986
+ event: args.event,
1987
+ meshId: args.meshId,
1988
+ nodeLabel: args.nodeLabel,
1989
+ nodeId: args.nodeId || undefined,
1990
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
1991
+ metadataEvent: {
1992
+ ...args.metadataEvent,
1993
+ ...(recoveryContext ? { recoveryContext } : {}),
1994
+ },
1995
+ coordinatorMessage: messageText,
1996
+ queuedAt: Date.now(),
1997
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
1998
+ })) {
1999
+ if (allCoordinatorsGenerating) {
2000
+ 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`);
2001
+ } else {
2002
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
2003
+ }
2004
+ }
2005
+ }
2006
+
2007
+ // When all CLI coordinators are actively generating and a terminal refine event fires,
2008
+ // skip send_message injection entirely. The event is already buffered to the pending queue.
2009
+ // Injecting into a generating PTY coordinator can corrupt its input stream and cause it to
2010
+ // remain stuck in generating state, never processing the refine result.
2011
+ // The coordinator will drain pending events via get_pending_mesh_events on its next idle cycle.
2012
+ if (allCoordinatorsGenerating) {
2013
+ return { success: true, forwarded: 0, bufferedForGeneratingCoordinator: true };
2014
+ }
807
2015
 
808
2016
  for (const coord of coordinatorInstances) {
809
2017
  const coordState = coord.getState();
@@ -824,6 +2032,11 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
824
2032
  const nodeId = readNonEmptyString(payload.nodeId);
825
2033
  const workspace = readNonEmptyString(payload.workspace);
826
2034
  const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
2035
+ const relayModalMessage = readNonEmptyString(payload.modalMessage);
2036
+ const relayModalButtons = Array.isArray(payload.modalButtons)
2037
+ ? (payload.modalButtons as unknown[]).filter((b): b is string => typeof b === 'string' && b.trim().length > 0)
2038
+ : null;
2039
+
827
2040
  return injectMeshSystemMessage(components, {
828
2041
  meshId,
829
2042
  nodeId,
@@ -834,6 +2047,21 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
834
2047
  providerType: readNonEmptyString(payload.providerType),
835
2048
  providerSessionId: readNonEmptyString(payload.providerSessionId),
836
2049
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
2050
+ jobId: readNonEmptyString(payload.jobId),
2051
+ interactionId: readNonEmptyString(payload.interactionId),
2052
+ status: readNonEmptyString(payload.status),
2053
+ targetDaemonId: readNonEmptyString(payload.targetDaemonId),
2054
+ startedAt: readNonEmptyString(payload.startedAt),
2055
+ completedAt: readNonEmptyString(payload.completedAt),
2056
+ retryOfJobId: readNonEmptyString(payload.retryOfJobId),
2057
+ ...(relayModalMessage ? { modalMessage: relayModalMessage } : {}),
2058
+ ...(relayModalButtons && relayModalButtons.length > 0 ? { modalButtons: relayModalButtons } : {}),
2059
+ ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
2060
+ ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
2061
+ ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
2062
+ ...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
2063
+ ...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
2064
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
837
2065
  intentional: payload.intentional === true,
838
2066
  intentionalStop: payload.intentionalStop === true,
839
2067
  operatorCleanup: payload.operatorCleanup === true,
@@ -861,21 +2089,36 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
861
2089
  if (!workspace) return;
862
2090
  const settings = state.settings && typeof state.settings === 'object' ? state.settings as Record<string, unknown> : {};
863
2091
 
864
- // Coordinator sessions must never inject events into themselves.
865
- // A coordinator instance carries meshCoordinatorFor but not meshNodeFor/launchedByCoordinator.
866
- if (readNonEmptyString(settings.meshCoordinatorFor)) return;
2092
+ // A coordinator session normally must not inject events into itself. However,
2093
+ // a coordinator can also be the direct-dispatch target of mesh_send_task from
2094
+ // another coordinator. In that case the completion event must flow through
2095
+ // injectMeshSystemMessage so the ledger records task_completed and the other
2096
+ // coordinator's pendingCoordinatorEvents queue is populated. Skip only when
2097
+ // this session has no in-flight direct dispatch.
2098
+ const coordinatorMeshId = readNonEmptyString(settings.meshCoordinatorFor);
2099
+ let meshIdFromDirectDispatch = '';
2100
+ if (coordinatorMeshId) {
2101
+ try {
2102
+ const hasActiveDispatch =
2103
+ getActiveDirectDispatches(coordinatorMeshId).some(d => d.sessionId === instanceId)
2104
+ || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId);
2105
+ if (hasActiveDispatch) meshIdFromDirectDispatch = coordinatorMeshId;
2106
+ } catch { /* best-effort */ }
2107
+ if (!meshIdFromDirectDispatch) return;
2108
+ }
867
2109
 
868
- const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor);
2110
+ const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor) || meshIdFromDirectDispatch;
869
2111
 
870
2112
  // Only forward events for sessions that were explicitly launched as mesh-node delegates
871
- // (meshNodeFor set by mesh_launch_session) or that carry the launchedByCoordinator flag.
2113
+ // (meshNodeFor set by mesh_launch_session), carry the launchedByCoordinator flag, or
2114
+ // have an active direct-dispatch entry (mesh_send_task to a pre-existing session).
872
2115
  // Do NOT fall back to workspace-based mesh lookup: that would pick up coordinator sessions
873
2116
  // and any other CLI session that happens to share the same workspace, causing spurious
874
2117
  // system-message injection into the coordinator's own conversation.
875
2118
  const isMeshDelegate = Boolean(meshIdFromRuntime || settings.launchedByCoordinator);
876
2119
  if (!isMeshDelegate) return;
877
2120
 
878
- const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getMeshByRepo(workspace);
2121
+ const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getCachedMeshByWorkspace(workspace);
879
2122
  const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
880
2123
  if (!meshId) return;
881
2124