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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +4 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/chat/source-machine.d.ts +166 -0
  4. package/dist/chat/source-resolver.d.ts +104 -0
  5. package/dist/chat/subscription-updates.d.ts +1 -0
  6. package/dist/cli-adapter-types.d.ts +23 -2
  7. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  8. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  9. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  10. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  11. package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
  12. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  13. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +3 -3
  14. package/dist/cli-adapters/terminal-backends/types.d.ts +1 -2
  15. package/dist/cli-adapters/terminal-screen.d.ts +2 -11
  16. package/dist/commands/cli-manager.d.ts +2 -1
  17. package/dist/commands/handler.d.ts +110 -0
  18. package/dist/commands/mesh-coordinator.d.ts +85 -1
  19. package/dist/commands/router.d.ts +44 -1
  20. package/dist/config/chat-history.d.ts +9 -0
  21. package/dist/config/config.d.ts +5 -0
  22. package/dist/config/mesh-config.d.ts +72 -1
  23. package/dist/git/git-commands.d.ts +9 -1
  24. package/dist/git/git-diff.d.ts +6 -0
  25. package/dist/git/git-types.d.ts +2 -0
  26. package/dist/index.d.ts +47 -11
  27. package/dist/index.js +31114 -11255
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +31615 -11852
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/installer.d.ts +1 -4
  32. package/dist/ipc/local-ipc-server.d.ts +91 -0
  33. package/dist/launch.d.ts +1 -1
  34. package/dist/logging/async-batch-writer.d.ts +10 -0
  35. package/dist/mesh/contracts.d.ts +164 -0
  36. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  37. package/dist/mesh/coordinator-registry.d.ts +59 -0
  38. package/dist/mesh/mesh-active-work.d.ts +90 -0
  39. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  40. package/dist/mesh/mesh-events-coordinator.d.ts +152 -0
  41. package/dist/mesh/mesh-events-pending.d.ts +40 -0
  42. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  43. package/dist/mesh/mesh-events-utils.d.ts +16 -0
  44. package/dist/mesh/mesh-events.d.ts +5 -49
  45. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  46. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  47. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  48. package/dist/mesh/mesh-ledger.d.ts +77 -1
  49. package/dist/mesh/mesh-missions.d.ts +58 -0
  50. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  51. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  52. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  53. package/dist/mesh/mesh-routing.d.ts +70 -0
  54. package/dist/mesh/mesh-runtime-store.d.ts +327 -0
  55. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  56. package/dist/mesh/mesh-work-queue.d.ts +160 -5
  57. package/dist/mesh/preview-freshness.d.ts +18 -0
  58. package/dist/mesh/refine-config.d.ts +215 -0
  59. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  60. package/dist/providers/acp-provider-instance.d.ts +2 -0
  61. package/dist/providers/approval-utils.d.ts +13 -0
  62. package/dist/providers/cli-provider-instance.d.ts +37 -3
  63. package/dist/providers/contracts.d.ts +130 -6
  64. package/dist/providers/external-sources.d.ts +71 -0
  65. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  66. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  67. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  68. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  69. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  70. package/dist/providers/native-history/index.d.ts +13 -0
  71. package/dist/providers/provider-instance-manager.d.ts +13 -0
  72. package/dist/providers/provider-instance.d.ts +13 -1
  73. package/dist/providers/provider-loader.d.ts +26 -4
  74. package/dist/providers/provider-trust.d.ts +31 -0
  75. package/dist/providers/read-chat-contract.d.ts +29 -0
  76. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  77. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  78. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  79. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  80. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  81. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  82. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  83. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  84. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  85. package/dist/providers/sdk/v1/index.d.ts +30 -0
  86. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  87. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  88. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  89. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  90. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  91. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  92. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  93. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  94. package/dist/providers/spec/adapter.d.ts +52 -0
  95. package/dist/providers/spec/cli-adapter.d.ts +140 -0
  96. package/dist/providers/spec/evaluator.d.ts +23 -0
  97. package/dist/providers/spec/fsm-driver.d.ts +273 -0
  98. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  99. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  100. package/dist/providers/spec/fsm-types.d.ts +91 -0
  101. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  102. package/dist/providers/spec/route.d.ts +4 -0
  103. package/dist/providers/spec/types.d.ts +129 -0
  104. package/dist/providers/transcript-v2.d.ts +176 -0
  105. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  106. package/dist/repo-mesh-types.d.ts +126 -1
  107. package/dist/sessions/registry.d.ts +6 -0
  108. package/dist/shared-types-extra.d.ts +2 -4
  109. package/dist/shared-types.d.ts +42 -1
  110. package/dist/status/normalize.d.ts +1 -1
  111. package/dist/status/normalize.js +1 -0
  112. package/dist/status/normalize.js.map +1 -1
  113. package/dist/status/normalize.mjs +1 -0
  114. package/dist/status/normalize.mjs.map +1 -1
  115. package/dist/status/reporter.d.ts +2 -0
  116. package/dist/status/snapshot.d.ts +1 -0
  117. package/dist/types.d.ts +5 -0
  118. package/package.json +6 -3
  119. package/src/agent-stream/poller.ts +2 -3
  120. package/src/boot/daemon-lifecycle.ts +30 -10
  121. package/src/boot/process-hardening.ts +89 -0
  122. package/src/chat/source-machine.ts +534 -0
  123. package/src/chat/source-resolver.ts +0 -0
  124. package/src/chat/subscription-updates.ts +20 -1
  125. package/src/cli-adapter-types.d.ts +1 -0
  126. package/src/cli-adapter-types.ts +22 -2
  127. package/src/cli-adapters/cli-script-runner.ts +421 -0
  128. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  129. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  130. package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
  131. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  132. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  133. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  134. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  135. package/src/cli-adapters/provider-cli-shared.ts +68 -11
  136. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  137. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +35 -29
  138. package/src/cli-adapters/terminal-backends/types.ts +1 -3
  139. package/src/cli-adapters/terminal-screen.ts +11 -81
  140. package/src/commands/chat-commands.ts +1806 -60
  141. package/src/commands/cli-manager.ts +286 -14
  142. package/src/commands/handler.ts +809 -2
  143. package/src/commands/mesh-coordinator.ts +332 -122
  144. package/src/commands/router.ts +4890 -489
  145. package/src/config/chat-history.ts +95 -24
  146. package/src/config/config.ts +12 -0
  147. package/src/config/mesh-config.ts +303 -3
  148. package/src/config/recent-activity.ts +8 -2
  149. package/src/daemon/dev-auto-implement.ts +3 -2
  150. package/src/daemon/dev-cli-debug.ts +10 -1
  151. package/src/detection/ide-detector.ts +26 -16
  152. package/src/git/git-commands.ts +42 -12
  153. package/src/git/git-diff.ts +53 -0
  154. package/src/git/git-status.ts +35 -6
  155. package/src/git/git-types.ts +2 -0
  156. package/src/git/git-worktree.ts +8 -1
  157. package/src/index.ts +127 -10
  158. package/src/installer.d.ts +1 -1
  159. package/src/installer.ts +8 -6
  160. package/src/ipc/local-ipc-server.ts +278 -0
  161. package/src/launch.d.ts +1 -1
  162. package/src/launch.ts +37 -28
  163. package/src/logging/async-batch-writer.ts +55 -0
  164. package/src/logging/logger.ts +2 -1
  165. package/src/mesh/contracts.ts +329 -0
  166. package/src/mesh/coordinator-prompt.ts +219 -31
  167. package/src/mesh/coordinator-registry.ts +121 -0
  168. package/src/mesh/mesh-active-work.ts +437 -0
  169. package/src/mesh/mesh-delivery-policy.ts +315 -0
  170. package/src/mesh/mesh-events-coordinator.ts +1625 -0
  171. package/src/mesh/mesh-events-pending.ts +439 -0
  172. package/src/mesh/mesh-events-stale.ts +283 -0
  173. package/src/mesh/mesh-events-utils.ts +195 -0
  174. package/src/mesh/mesh-events.ts +26 -1035
  175. package/src/mesh/mesh-fast-forward.ts +438 -0
  176. package/src/mesh/mesh-host-ownership.ts +73 -0
  177. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  178. package/src/mesh/mesh-ledger.ts +588 -102
  179. package/src/mesh/mesh-missions.ts +151 -0
  180. package/src/mesh/mesh-refine-batch.ts +197 -0
  181. package/src/mesh/mesh-refine-status.ts +231 -0
  182. package/src/mesh/mesh-review-inbox.ts +307 -0
  183. package/src/mesh/mesh-routing.ts +272 -0
  184. package/src/mesh/mesh-runtime-store.ts +1342 -0
  185. package/src/mesh/mesh-task-stats.ts +154 -0
  186. package/src/mesh/mesh-work-queue.ts +573 -145
  187. package/src/mesh/preview-freshness.ts +118 -0
  188. package/src/mesh/refine-config.ts +403 -0
  189. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  190. package/src/providers/acp-provider-instance.ts +15 -1
  191. package/src/providers/approval-utils.d.ts +4 -0
  192. package/src/providers/approval-utils.ts +47 -5
  193. package/src/providers/chat-message-normalization.ts +4 -11
  194. package/src/providers/cli-provider-instance.ts +806 -72
  195. package/src/providers/contracts.d.ts +55 -0
  196. package/src/providers/contracts.ts +141 -6
  197. package/src/providers/external-sources.ts +218 -0
  198. package/src/providers/ide-provider-instance.ts +19 -5
  199. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  200. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  201. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  202. package/src/providers/native-history/dispatcher.ts +340 -0
  203. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  204. package/src/providers/native-history/index.ts +30 -0
  205. package/src/providers/provider-instance-manager.ts +30 -0
  206. package/src/providers/provider-instance.ts +10 -1
  207. package/src/providers/provider-loader.ts +584 -50
  208. package/src/providers/provider-schema.ts +87 -14
  209. package/src/providers/provider-trust.ts +114 -0
  210. package/src/providers/read-chat-contract.ts +76 -16
  211. package/src/providers/sdk/README.md +49 -0
  212. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  213. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  214. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  215. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  216. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  217. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  218. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  219. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  220. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  221. package/src/providers/sdk/v1/index.ts +152 -0
  222. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  223. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  224. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  225. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  226. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  227. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  228. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  229. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  230. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  231. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  232. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  233. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  234. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  235. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  236. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  237. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  238. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  239. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  240. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  241. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  242. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  243. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  244. package/src/providers/sdk/v1/validators/index.ts +19 -0
  245. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  246. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  247. package/src/providers/spec/adapter.ts +145 -0
  248. package/src/providers/spec/cli-adapter.ts +730 -0
  249. package/src/providers/spec/evaluator.ts +321 -0
  250. package/src/providers/spec/fsm-driver.ts +811 -0
  251. package/src/providers/spec/fsm-evaluator.ts +255 -0
  252. package/src/providers/spec/fsm-loader.ts +102 -0
  253. package/src/providers/spec/fsm-types.ts +184 -0
  254. package/src/providers/spec/native-history-executor.ts +943 -0
  255. package/src/providers/spec/route.ts +51 -0
  256. package/src/providers/spec/types.ts +173 -0
  257. package/src/providers/transcript-v2.ts +567 -0
  258. package/src/providers/types/interactive-prompt.ts +425 -0
  259. package/src/providers/version-archive.ts +38 -20
  260. package/src/repo-mesh-types.ts +138 -1
  261. package/src/sessions/registry.ts +6 -0
  262. package/src/shared-types-extra.ts +2 -4
  263. package/src/shared-types.ts +45 -1
  264. package/src/status/builders.ts +26 -6
  265. package/src/status/normalize.ts +2 -0
  266. package/src/status/reporter.ts +15 -0
  267. package/src/status/snapshot.ts +84 -25
  268. package/src/system/host-memory.ts +29 -12
  269. package/src/types.ts +5 -0
  270. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  271. package/dist/mesh/mesh-sync.d.ts +0 -53
  272. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  273. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  274. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,1625 @@
1
+ import { existsSync } from 'fs';
2
+ import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
3
+ import { loadConfig } from '../config/config.js';
4
+ import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
5
+ import { detectCLI } from '../detection/cli-detector.js';
6
+ import { LOG } from '../logging/logger.js';
7
+ import { appendLedgerEntry, buildTaskCompletionEvidence, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
8
+ import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
9
+ import { buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches, hasPendingDependents } from './mesh-work-queue.js';
10
+ import { fastForwardMeshNode } from './mesh-fast-forward.js';
11
+ import { createSessionDelivery, markSessionDeliveriesTerminal, updateSessionDeliveryStatus, recordCompletionConflict } from './mesh-delivery-policy.js';
12
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
13
+ import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, markMeshCoordinatorEventDirectDelivered } from './mesh-events-pending.js';
14
+ import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
15
+ import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent } from './mesh-routing.js';
16
+ import {
17
+ findRecentTerminalLedgerEvidence,
18
+ hasDispatchAfterTerminal,
19
+ hasUnterminalDirectDispatchLedgerEntry,
20
+ buildLongGeneratingCompletionReconciliation,
21
+ } from './mesh-events-stale.js';
22
+ import {
23
+ buildMeshSystemMessage,
24
+ readNonEmptyString,
25
+ readRecord,
26
+ resolveEventSessionId,
27
+ readRefineJobId,
28
+ readWorkerResultMetadata,
29
+ sameDaemonId,
30
+ } from './mesh-events-utils.js';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Remote Node Idle Session Tracking
34
+ // ---------------------------------------------------------------------------
35
+ const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Workspace-to-mesh lookup cache
39
+ // ---------------------------------------------------------------------------
40
+ const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
41
+ const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
42
+ const IDLE_AUTO_FAST_FORWARD_THROTTLE_MS = 30 * 60 * 1000;
43
+ const idleAutoFastForwardLastAttempt = new Map<string, number>();
44
+
45
+ function getCachedMeshByWorkspace(workspace: string): any {
46
+ const now = Date.now();
47
+ const cached = meshByWorkspaceCache.get(workspace);
48
+ if (cached && now - cached.cachedAt < MESH_WORKSPACE_CACHE_TTL_MS) return cached.mesh;
49
+ const mesh = getMeshByRepo(workspace);
50
+ meshByWorkspaceCache.set(workspace, { mesh, cachedAt: now });
51
+ return mesh;
52
+ }
53
+
54
+ export function __resetIdleAutoFastForwardForTests(): void {
55
+ idleAutoFastForwardLastAttempt.clear();
56
+ }
57
+
58
+ export function __resetMeshWorkspaceCacheForTests(): void {
59
+ meshByWorkspaceCache.clear();
60
+ }
61
+
62
+ function sweepExpiredRemoteIdleSessions(): void {
63
+ try {
64
+ MeshRuntimeStore.getInstance().pruneExpiredRemoteIdleSessions();
65
+ } catch { /* best-effort */ }
66
+ }
67
+
68
+ function getMeshWithCache(components: DaemonComponents, meshId: string): any | undefined {
69
+ const localMesh = getMesh(meshId);
70
+ if (localMesh) return localMesh;
71
+ return components.router?.getCachedInlineMesh(meshId);
72
+ }
73
+
74
+ const INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS = 30 * 60 * 1000;
75
+
76
+ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boolean {
77
+ return event.intentional === true
78
+ || event.intentionalStop === true
79
+ || event.operatorCleanup === true
80
+ || event.reason === 'operator_cleanup'
81
+ || event.stopReason === 'operator_cleanup'
82
+ || event.cleanupReason === 'operator_cleanup'
83
+ || event.source === 'mesh_cleanup_sessions'
84
+ || event.source === 'mesh_remove_node';
85
+ }
86
+
87
+ function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
88
+ if (!sessionId && !nodeId) return false;
89
+ const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
90
+ const entries = readLedgerEntries(meshId, { tail: 200 });
91
+ for (let i = entries.length - 1; i >= 0; i--) {
92
+ const entry = entries[i];
93
+ const timestamp = new Date(entry.timestamp).getTime();
94
+ if (!Number.isNaN(timestamp) && timestamp < cutoff) break;
95
+ if (!isIntentionalCleanupStopEntry(entry)) continue;
96
+ if (sessionId && entry.sessionId === sessionId) return true;
97
+ if (!sessionId && nodeId && entry.nodeId === nodeId) return true;
98
+ }
99
+ return false;
100
+ }
101
+
102
+ function shouldSuppressIntentionalCleanupStop(args: {
103
+ event: string;
104
+ meshId: string;
105
+ metadataEvent: Record<string, unknown>;
106
+ sessionId?: string;
107
+ nodeId?: string;
108
+ }): boolean {
109
+ if (args.event !== 'agent:stopped' && args.event !== 'monitor:long_generating') return false;
110
+ if (isIntentionalCleanupStopMetadata(args.metadataEvent)) return true;
111
+ return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
112
+ }
113
+
114
+ const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
115
+
116
+ function hasFingerprintSeen(fingerprint: string): boolean {
117
+ try {
118
+ return MeshRuntimeStore.getInstance().hasCompletionFingerprint(fingerprint);
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ function recordFingerprintSeen(fingerprint: string): void {
125
+ try {
126
+ const db = MeshRuntimeStore.getInstance();
127
+ db.recordCompletionFingerprint(fingerprint, RECENT_COMPLETION_FINGERPRINT_TTL_MS);
128
+ db.sweepExpiredFingerprints();
129
+ } catch { /* best-effort; duplicate events are preferable to a crash */ }
130
+ }
131
+
132
+ function readEventTimestamp(value: unknown): number | null {
133
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
134
+ if (typeof value === 'string' && value.trim()) {
135
+ const numeric = Number(value);
136
+ if (Number.isFinite(numeric)) return numeric;
137
+ const parsed = Date.parse(value);
138
+ if (Number.isFinite(parsed)) return parsed;
139
+ }
140
+ return null;
141
+ }
142
+
143
+ function buildMeshCompletionFingerprint(args: {
144
+ meshId: string;
145
+ event: string;
146
+ sessionId: string;
147
+ providerType?: string;
148
+ providerSessionId?: string;
149
+ timestamp?: number | null;
150
+ finalSummary?: string;
151
+ coordinatorDaemonId?: string;
152
+ }): string {
153
+ const timestampPart = Number.isFinite(args.timestamp)
154
+ ? String(args.timestamp)
155
+ : readNonEmptyString(args.finalSummary).slice(0, 200);
156
+ return [
157
+ args.meshId,
158
+ args.event,
159
+ args.sessionId,
160
+ args.providerType || '',
161
+ args.providerSessionId || '',
162
+ timestampPart,
163
+ args.coordinatorDaemonId || '',
164
+ ].join('::');
165
+ }
166
+
167
+ function isDuplicateMeshCompletionEvent(args: {
168
+ meshId: string;
169
+ event: string;
170
+ sessionId: string;
171
+ providerType?: string;
172
+ providerSessionId?: string;
173
+ timestamp?: number | null;
174
+ finalSummary?: string;
175
+ coordinatorDaemonId?: string;
176
+ taskId?: string;
177
+ nodeId?: string;
178
+ }): boolean {
179
+ const fingerprint = buildMeshCompletionFingerprint(args);
180
+ if (!fingerprint) return false;
181
+ if (hasFingerprintSeen(fingerprint)) {
182
+ if (args.taskId) {
183
+ recordCompletionConflict({
184
+ meshId: args.meshId,
185
+ fingerprint,
186
+ conflictingTaskId: args.taskId,
187
+ conflictingSessionId: args.sessionId,
188
+ event: args.event,
189
+ });
190
+ }
191
+ return true;
192
+ }
193
+ recordFingerprintSeen(fingerprint);
194
+ return false;
195
+ }
196
+
197
+ function isDuplicateMeshApprovalEvent(args: {
198
+ meshId: string;
199
+ sessionId: string;
200
+ providerType?: string;
201
+ timestamp?: number | null;
202
+ modalMessage?: string;
203
+ modalButtons?: unknown;
204
+ }): boolean {
205
+ const modalButtons = Array.isArray(args.modalButtons)
206
+ ? args.modalButtons.map(button => String(button).trim()).filter(Boolean)
207
+ : [];
208
+ const approvalIdentity = Number.isFinite(args.timestamp)
209
+ ? String(args.timestamp)
210
+ : JSON.stringify({ message: args.modalMessage || '', buttons: modalButtons });
211
+ if (!approvalIdentity || approvalIdentity === '{"message":"","buttons":[]}') return false;
212
+ const fingerprint = [
213
+ args.meshId,
214
+ 'agent:waiting_approval',
215
+ args.sessionId,
216
+ args.providerType || '',
217
+ approvalIdentity,
218
+ ].join('::');
219
+ if (hasFingerprintSeen(fingerprint)) return true;
220
+ recordFingerprintSeen(fingerprint);
221
+ return false;
222
+ }
223
+
224
+ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
225
+ const jobId = readRefineJobId({ metadataEvent });
226
+ const fingerprint = jobId && new Set(['refine:completed', 'refine:failed']).has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
227
+ if (!fingerprint) return false;
228
+ if (hasFingerprintSeen(fingerprint)) return true;
229
+ recordFingerprintSeen(fingerprint);
230
+ return false;
231
+ }
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // Queue assignment
235
+ // ---------------------------------------------------------------------------
236
+
237
+ export function tryAssignQueueTask(
238
+ components: DaemonComponents,
239
+ meshId: string,
240
+ nodeId: string,
241
+ sessionId: string,
242
+ providerType: string
243
+ ): boolean {
244
+ const mesh = getMeshWithCache(components, meshId);
245
+ const node = mesh?.nodes.find((n: any) => n.id === nodeId);
246
+ const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
247
+ const task = claimNextTask(meshId, nodeId, sessionId, capabilityTags);
248
+ if (!task) {
249
+ return false;
250
+ }
251
+
252
+ LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
253
+
254
+ if (node?.daemonId && components.dispatchMeshCommand) {
255
+ const isLocalNode = components.cliManager.adapters.has(sessionId);
256
+ if (!isLocalNode) {
257
+ const delivery = createSessionDelivery({
258
+ meshId,
259
+ nodeId,
260
+ sessionId,
261
+ providerType,
262
+ taskId: task.id,
263
+ kind: 'task',
264
+ message: task.message,
265
+ status: 'delivering',
266
+ });
267
+ const localDaemonIdForDispatch = readNonEmptyString(loadConfig().machineId) || undefined;
268
+ components.dispatchMeshCommand(node.daemonId, 'agent_command', {
269
+ targetSessionId: sessionId,
270
+ cliType: providerType,
271
+ action: 'send_chat',
272
+ message: task.message,
273
+ meshContext: {
274
+ meshId,
275
+ nodeId,
276
+ taskId: task.id,
277
+ ...(localDaemonIdForDispatch ? { coordinatorDaemonId: localDaemonIdForDispatch } : {}),
278
+ },
279
+ }).then(() => {
280
+ updateSessionDeliveryStatus(delivery.id, 'delivered');
281
+ }).catch((e: any) => {
282
+ LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
283
+ updateSessionDeliveryStatus(delivery.id, 'failed', { lastError: e?.message, incrementAttempt: true });
284
+ updateTaskStatus(meshId, task.id, 'pending');
285
+ try {
286
+ appendLedgerEntry(meshId, {
287
+ kind: 'dispatch_failed' as any,
288
+ nodeId,
289
+ sessionId,
290
+ payload: { taskId: task.id, deliveryId: delivery.id, error: e?.message, retryable: true },
291
+ });
292
+ } catch { /* ledger write is best-effort */ }
293
+ });
294
+ return true;
295
+ }
296
+ }
297
+
298
+ // Stamp mesh context onto the session so completion events route correctly
299
+ // via setupMeshEventForwarding. Without this, manually-opened idle sessions
300
+ // (mesh_launch_session without auto-launch) lack meshNodeFor/meshNodeId and
301
+ // agent:generating_completed is silently dropped as isMeshDelegate=false.
302
+ try {
303
+ const inst = components.instanceManager.getInstance(sessionId);
304
+ if (inst && typeof inst.updateSettings === 'function') {
305
+ inst.updateSettings({ meshNodeFor: meshId, meshNodeId: nodeId, launchedByCoordinator: true });
306
+ }
307
+ } catch { /* best-effort — dispatch still proceeds */ }
308
+
309
+ const delivery = createSessionDelivery({
310
+ meshId,
311
+ nodeId,
312
+ sessionId,
313
+ providerType,
314
+ taskId: task.id,
315
+ kind: 'task',
316
+ message: task.message,
317
+ status: 'delivering',
318
+ });
319
+ components.cliManager.handleCliCommand('agent_command', {
320
+ targetSessionId: sessionId,
321
+ cliType: providerType,
322
+ action: 'send_chat',
323
+ message: task.message,
324
+ }).then(() => {
325
+ updateSessionDeliveryStatus(delivery.id, 'delivered');
326
+ }).catch((e: any) => {
327
+ LOG.error('MeshQueue', `Failed to dispatch task locally to node ${nodeId}: ${e?.message}`);
328
+ updateSessionDeliveryStatus(delivery.id, 'failed', { lastError: e?.message, incrementAttempt: true });
329
+ updateTaskStatus(meshId, task.id, 'failed');
330
+ });
331
+
332
+ return true;
333
+ }
334
+
335
+ const autoLaunchInProgress = new Set<string>();
336
+ const autoLaunchCooldownUntil = new Map<string, number>();
337
+ const AUTO_LAUNCH_COOLDOWN_MS = 5_000;
338
+
339
+ function sweepExpiredCooldowns(): void {
340
+ const now = Date.now();
341
+ for (const [key, until] of autoLaunchCooldownUntil) {
342
+ if (now >= until) autoLaunchCooldownUntil.delete(key);
343
+ }
344
+ }
345
+
346
+ function normalizeProviderPriority(policy: unknown): string[] {
347
+ const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
348
+ ? (policy as Record<string, unknown>).providerPriority
349
+ : undefined;
350
+ if (!Array.isArray(raw)) return [];
351
+ const seen = new Set<string>();
352
+ return raw
353
+ .map(type => typeof type === 'string' ? type.trim() : '')
354
+ .filter(Boolean)
355
+ .filter(type => {
356
+ if (seen.has(type)) return false;
357
+ seen.add(type);
358
+ return true;
359
+ });
360
+ }
361
+
362
+ function isTerminalSessionStatus(status: string): boolean {
363
+ return ['stopped', 'failed', 'terminated', 'exited', 'closed'].includes(status);
364
+ }
365
+
366
+ function isIdleSessionState(state: any): boolean {
367
+ const status = readNonEmptyString(state?.status).toLowerCase();
368
+ if (isTerminalSessionStatus(status)) return false;
369
+ return status === 'idle' || state?.activeChat?.status === 'waiting_input';
370
+ }
371
+
372
+ function isDirtyNode(node: any): boolean {
373
+ return node?.health === 'dirty' || node?.git?.dirty === true;
374
+ }
375
+
376
+ function resolveAutoFastForwardPolicy(mesh: any): { enabled: boolean; maxBehind?: number; requireCleanSubmodules: boolean } {
377
+ const record = mesh?.policy?.autoFastForward && typeof mesh.policy.autoFastForward === 'object' && !Array.isArray(mesh.policy.autoFastForward)
378
+ ? mesh.policy.autoFastForward as Record<string, unknown>
379
+ : {};
380
+ const maxBehind = Number(record.maxBehind);
381
+ return {
382
+ enabled: record.enabled !== false,
383
+ ...(Number.isFinite(maxBehind) && maxBehind >= 0 ? { maxBehind: Math.floor(maxBehind) } : {}),
384
+ requireCleanSubmodules: record.requireCleanSubmodules !== false,
385
+ };
386
+ }
387
+
388
+ function sessionStateLooksActive(state: any): boolean {
389
+ const status = readNonEmptyString(state?.status).toLowerCase();
390
+ const chatStatus = readNonEmptyString(state?.activeChat?.status).toLowerCase();
391
+ const active = new Set(['generating', 'streaming', 'long_generating', 'working', 'starting', 'waiting_approval']);
392
+ return active.has(status) || active.has(chatStatus);
393
+ }
394
+
395
+ function nodeHasActiveMeshWork(components: DaemonComponents, meshId: string, nodeId: string, currentSessionId?: string): boolean {
396
+ if (nodeHasActiveAssignment(meshId, nodeId)) return true;
397
+ return components.instanceManager.getByCategory('cli').some((inst: any) => {
398
+ const state = inst.getState();
399
+ const settings = state.settings as Record<string, unknown> || {};
400
+ if (readNonEmptyString(settings.meshNodeFor) !== meshId) return false;
401
+ const instNodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.nodeId);
402
+ if (instNodeId !== nodeId) return false;
403
+ const sessionId = readNonEmptyString(state.instanceId);
404
+ if (currentSessionId && sessionId === currentSessionId && isIdleSessionState(state)) return false;
405
+ return sessionStateLooksActive(state);
406
+ });
407
+ }
408
+
409
+ function isLaunchableNode(node: any): boolean {
410
+ if (!node || node.status === 'disabled' || node.status === 'removed') return false;
411
+ const health = readNonEmptyString(node.health).toLowerCase();
412
+ if (!health) return true;
413
+ return health === 'online' || health === 'unknown';
414
+ }
415
+
416
+ function localAutoLaunchSkipReason(node: any): string | null {
417
+ const daemonId = readNonEmptyString(node?.daemonId);
418
+ const machineId = readNonEmptyString(node?.machineId);
419
+ const appConfig = loadConfig();
420
+ const localMachineId = readNonEmptyString(appConfig.machineId) || readNonEmptyString(appConfig.registeredMachineId);
421
+ const cloudDaemonId = localMachineId ? `daemon_${localMachineId}` : '';
422
+ const standaloneDaemonId = localMachineId ? `standalone_${localMachineId}` : '';
423
+
424
+ const daemonMatchesLocal = !daemonId || daemonId === cloudDaemonId || daemonId === standaloneDaemonId;
425
+ const machineMatchesLocal = !machineId || (localMachineId && machineId === localMachineId);
426
+
427
+ if (node?.isLocalWorktree === true) {
428
+ return daemonMatchesLocal && machineMatchesLocal ? null : 'remote_auto_launch_unsupported';
429
+ }
430
+
431
+ if (daemonId || machineId) {
432
+ return daemonMatchesLocal && machineMatchesLocal ? null : 'remote_auto_launch_unsupported';
433
+ }
434
+
435
+ return null;
436
+ }
437
+
438
+ function activeAssignedCount(meshId: string): number {
439
+ return getQueue(meshId, { status: ['assigned'] as any }).length;
440
+ }
441
+
442
+ function nodeHasActiveAssignment(meshId: string, nodeId: string): boolean {
443
+ return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedNodeId === nodeId);
444
+ }
445
+
446
+ function sessionHasActiveAssignment(meshId: string, sessionId: string): boolean {
447
+ if (getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedSessionId === sessionId)) {
448
+ return true;
449
+ }
450
+ // Direct dispatches (mesh_send_task) are tracked in mesh_direct_dispatches, not the
451
+ // work queue. A session completing a still-active direct dispatch IS an active
452
+ // assignment — without this, findRecentTerminalLedgerEvidence dedup wrongly suppresses
453
+ // the canonical agent:generating_completed for direct-dispatch tasks (validation/general),
454
+ // so the coordinator polling get_pending_mesh_events never observes task_completed and the
455
+ // session goes silently idle. This check runs before markSessionTerminal marks the
456
+ // dispatch terminal, so the in-flight dispatch is still observable here.
457
+ try {
458
+ if (getActiveDirectDispatches(meshId).some(d => d.sessionId === sessionId)) return true;
459
+ if (hasUnterminalDirectDispatchLedgerEntry(meshId, sessionId)) return true;
460
+ } catch { /* best-effort — fall through to false */ }
461
+ return false;
462
+ }
463
+
464
+ function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
465
+ return components.instanceManager.getByCategory('cli').filter((inst: any) => {
466
+ const state = inst.getState();
467
+ const settings = state.settings as Record<string, unknown> || {};
468
+ if (readNonEmptyString(settings.meshNodeFor) !== meshId) return false;
469
+ const instNodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.nodeId);
470
+ if (instNodeId !== nodeId) return false;
471
+ const status = readNonEmptyString(state.status).toLowerCase();
472
+ return !isTerminalSessionStatus(status);
473
+ }).length;
474
+ }
475
+
476
+ function recordAutoLaunchEvent(meshId: string, args: {
477
+ phase: 'skipped' | 'started' | 'failed' | 'completed';
478
+ taskId: string;
479
+ nodeId?: string;
480
+ providerType?: string;
481
+ sessionId?: string;
482
+ reason?: string;
483
+ error?: string;
484
+ }) {
485
+ try {
486
+ appendLedgerEntry(meshId, {
487
+ kind: 'session_auto_launch',
488
+ nodeId: args.nodeId,
489
+ sessionId: args.sessionId,
490
+ providerType: args.providerType,
491
+ payload: {
492
+ phase: args.phase,
493
+ taskId: args.taskId,
494
+ reason: args.reason,
495
+ error: args.error,
496
+ },
497
+ });
498
+ } catch (e: any) {
499
+ LOG.warn('MeshQueue', `Failed to record auto-launch ledger event: ${e?.message || e}`);
500
+ }
501
+ }
502
+
503
+ function markAutoLaunch(meshId: string, taskId: string, args: {
504
+ status: 'skipped' | 'started' | 'failed' | 'completed';
505
+ reason?: string;
506
+ nodeId?: string;
507
+ providerType?: string;
508
+ sessionId?: string;
509
+ error?: string;
510
+ }) {
511
+ recordTaskAutoLaunch(meshId, taskId, {
512
+ status: args.status,
513
+ reason: args.reason || args.error,
514
+ nodeId: args.nodeId,
515
+ providerType: args.providerType,
516
+ sessionId: args.sessionId,
517
+ });
518
+ recordAutoLaunchEvent(meshId, {
519
+ phase: args.status,
520
+ taskId,
521
+ nodeId: args.nodeId,
522
+ providerType: args.providerType,
523
+ sessionId: args.sessionId,
524
+ reason: args.reason,
525
+ error: args.error,
526
+ });
527
+ }
528
+
529
+ async function resolveUsableProvider(
530
+ components: DaemonComponents,
531
+ nodeId: string,
532
+ node: any,
533
+ requiredTags?: string[],
534
+ ): Promise<{ providerType?: string; reason?: string }> {
535
+ const providerPriority = normalizeProviderPriority(node?.policy);
536
+ if (!providerPriority.length) return { reason: 'missing_provider_priority' };
537
+ const providerLoader = components.providerLoader;
538
+ if (!providerLoader) return { reason: 'provider_loader_unavailable' };
539
+
540
+ const failed: string[] = [];
541
+ for (const requestedType of providerPriority) {
542
+ const normalizedType = typeof providerLoader.resolveAlias === 'function'
543
+ ? providerLoader.resolveAlias(requestedType)
544
+ : requestedType;
545
+ // Skip providers that can't satisfy the task's requiredTags (e.g. provider=hermes-cli
546
+ // means only hermes-cli qualifies, not any other type in providerPriority).
547
+ if (requiredTags?.length && !nodeSatisfiesRequiredTags(requiredTags, buildMeshNodeCapabilityTags(node, normalizedType))) {
548
+ failed.push(`${requestedType}: required_tags_mismatch`);
549
+ continue;
550
+ }
551
+ if (typeof providerLoader.isMachineProviderEnabled === 'function' && !providerLoader.isMachineProviderEnabled(normalizedType)) {
552
+ failed.push(`${requestedType}: disabled`);
553
+ continue;
554
+ }
555
+ let detected: any;
556
+ try {
557
+ detected = await detectCLI(normalizedType, providerLoader, { includeVersion: false });
558
+ } catch (e: any) {
559
+ failed.push(`${requestedType}: detect failed: ${e?.message || e}`);
560
+ continue;
561
+ }
562
+ if (typeof providerLoader.setCliDetectionResults === 'function') {
563
+ providerLoader.setCliDetectionResults([{
564
+ id: normalizedType,
565
+ installed: !!detected,
566
+ path: detected?.path,
567
+ }], false);
568
+ }
569
+ (components as any).onStatusChange?.();
570
+ if (detected) return { providerType: normalizedType };
571
+ failed.push(`${requestedType}: not detected`);
572
+ }
573
+ return { reason: `provider_priority_unusable: ${failed.join('; ') || nodeId}` };
574
+ }
575
+
576
+ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, meshId: string, mesh: any): Promise<boolean> {
577
+ const queue = getQueue(meshId);
578
+ const pending = queue.filter(task => task.status === 'pending');
579
+ if (!pending.length) return false;
580
+
581
+ const maxParallelTasks = Math.max(1, Math.floor(Number(mesh?.policy?.maxParallelTasks) || 2));
582
+ for (const task of pending) {
583
+ if (activeAssignedCount(meshId) >= maxParallelTasks) {
584
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'max_parallel_tasks_reached' });
585
+ return false;
586
+ }
587
+ if (task.targetSessionId) {
588
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'target_session_constraint' });
589
+ continue;
590
+ }
591
+
592
+ const candidateNodes = Array.isArray(mesh?.nodes)
593
+ ? mesh.nodes.filter((node: any) => {
594
+ if (task.targetNodeId && node?.id !== task.targetNodeId) return false;
595
+ // Skip nodes that can never satisfy requiredTags regardless of which provider
596
+ // from providerPriority is selected. A node satisfies tags if at least one
597
+ // provider in its priority list would produce matching capability tags.
598
+ if (task.requiredTags?.length) {
599
+ const priorities = normalizeProviderPriority(node?.policy);
600
+ const providerCandidates = priorities.length ? priorities : [undefined as unknown as string];
601
+ return providerCandidates.some(p =>
602
+ nodeSatisfiesRequiredTags(task.requiredTags, buildMeshNodeCapabilityTags(node, p))
603
+ );
604
+ }
605
+ return true;
606
+ })
607
+ : [];
608
+ if (!candidateNodes.length) {
609
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'no_node_satisfies_required_tags', nodeId: task.targetNodeId });
610
+ continue;
611
+ }
612
+
613
+ for (const node of candidateNodes) {
614
+ const nodeId = readNonEmptyString(node?.id);
615
+ if (!nodeId) continue;
616
+ const launchKey = `${meshId}:${nodeId}`;
617
+ const now = Date.now();
618
+ const cooldownUntil = autoLaunchCooldownUntil.get(launchKey) || 0;
619
+ if (cooldownUntil > 0 && now >= cooldownUntil) autoLaunchCooldownUntil.delete(launchKey);
620
+ if (autoLaunchInProgress.has(launchKey)) {
621
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_in_progress', nodeId });
622
+ continue;
623
+ }
624
+ if (now < cooldownUntil) {
625
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_cooldown', nodeId });
626
+ continue;
627
+ }
628
+ if (isDirtyNode(node)) {
629
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'dirty_workspace', nodeId });
630
+ continue;
631
+ }
632
+ if (!isLaunchableNode(node)) {
633
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'node_not_launch_ready', nodeId });
634
+ continue;
635
+ }
636
+ const localSkipReason = localAutoLaunchSkipReason(node);
637
+ if (localSkipReason) {
638
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: localSkipReason, nodeId });
639
+ continue;
640
+ }
641
+ if (nodeHasActiveAssignment(meshId, nodeId)) {
642
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'node_has_active_assignment', nodeId });
643
+ continue;
644
+ }
645
+ const maxConcurrentSessions = Number(node?.policy?.maxConcurrentSessions);
646
+ if (Number.isFinite(maxConcurrentSessions) && maxConcurrentSessions >= 0 && liveSessionCountForNode(components, meshId, nodeId) >= maxConcurrentSessions) {
647
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'max_concurrent_sessions_reached', nodeId });
648
+ continue;
649
+ }
650
+
651
+ autoLaunchInProgress.add(launchKey);
652
+ try {
653
+ const resolved = await resolveUsableProvider(components, nodeId, node, task.requiredTags);
654
+ if (!resolved.providerType) {
655
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: resolved.reason || 'provider_unusable', nodeId });
656
+ continue;
657
+ }
658
+
659
+ markAutoLaunch(meshId, task.id, { status: 'started', nodeId, providerType: resolved.providerType });
660
+ const launchResult: any = await components.cliManager.handleCliCommand('launch_cli', {
661
+ cliType: resolved.providerType,
662
+ dir: node.workspace,
663
+ settings: {
664
+ // Worker launch envelope: role + mesh context so worker can route completion events.
665
+ role: 'worker',
666
+ meshNodeFor: meshId,
667
+ meshNodeId: nodeId,
668
+ spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
669
+ launchedByCoordinator: true,
670
+ autoLaunchedForQueueTaskId: task.id,
671
+ },
672
+ });
673
+ if (!launchResult?.success) {
674
+ const reason = launchResult?.error || 'launch_cli_failed';
675
+ markAutoLaunch(meshId, task.id, { status: 'failed', reason, nodeId, providerType: resolved.providerType });
676
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
677
+ return false;
678
+ }
679
+ const sessionId = readNonEmptyString(launchResult.sessionId) || readNonEmptyString(launchResult.id) || readNonEmptyString(launchResult.runtimeSessionId);
680
+ if (!sessionId) {
681
+ markAutoLaunch(meshId, task.id, { status: 'failed', reason: 'launch_missing_session_id', nodeId, providerType: resolved.providerType });
682
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
683
+ return false;
684
+ }
685
+ markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId });
686
+ tryAssignQueueTask(components, meshId, nodeId, sessionId, resolved.providerType);
687
+ return true;
688
+ } catch (e: any) {
689
+ markAutoLaunch(meshId, task.id, { status: 'failed', error: e?.message || String(e), nodeId });
690
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
691
+ return false;
692
+ } finally {
693
+ autoLaunchInProgress.delete(launchKey);
694
+ }
695
+ }
696
+ }
697
+ return false;
698
+ }
699
+
700
+ export interface MeshQueueTriggerResult {
701
+ success: true;
702
+ meshId: string;
703
+ pendingBefore: number;
704
+ assignedBefore: number;
705
+ pendingAfter: number;
706
+ assignedAfter: number;
707
+ claimed: boolean;
708
+ newlyAssignedTasks: Array<{
709
+ id: string;
710
+ nodeId?: string;
711
+ sessionId?: string;
712
+ }>;
713
+ localIdleSessionsChecked: number;
714
+ remoteIdleSessionsChecked: number;
715
+ skippedSessions: Array<{
716
+ nodeId?: string;
717
+ sessionId?: string;
718
+ reason: string;
719
+ status?: string;
720
+ }>;
721
+ autoLaunchStarted: boolean;
722
+ noIdleMeshSessionAvailable?: boolean;
723
+ }
724
+
725
+ function countQueueStatus(meshId: string, status: 'pending' | 'assigned'): number {
726
+ return getQueue(meshId, { status: [status] as any }).length;
727
+ }
728
+
729
+ function getQueueStatusById(meshId: string): Map<string, string> {
730
+ return new Map(getQueue(meshId).map(task => [task.id, task.status]));
731
+ }
732
+
733
+ export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<MeshQueueTriggerResult> {
734
+ const mesh = getMeshWithCache(components, meshId);
735
+ const pendingBefore = countQueueStatus(meshId, 'pending');
736
+ const assignedBefore = countQueueStatus(meshId, 'assigned');
737
+ const beforeStatus = getQueueStatusById(meshId);
738
+ const skippedSessions: MeshQueueTriggerResult['skippedSessions'] = [];
739
+ let localIdleSessionsChecked = 0;
740
+ let remoteIdleSessionsChecked = 0;
741
+ let autoLaunchStarted = false;
742
+ if (!mesh) {
743
+ return {
744
+ success: true,
745
+ meshId,
746
+ pendingBefore,
747
+ assignedBefore,
748
+ pendingAfter: pendingBefore,
749
+ assignedAfter: assignedBefore,
750
+ claimed: false,
751
+ newlyAssignedTasks: [],
752
+ localIdleSessionsChecked,
753
+ remoteIdleSessionsChecked,
754
+ skippedSessions: [{ reason: 'mesh_not_found' }],
755
+ autoLaunchStarted,
756
+ noIdleMeshSessionAvailable: true,
757
+ };
758
+ }
759
+
760
+ const cliInstances = components.instanceManager.getByCategory('cli');
761
+ for (const inst of cliInstances) {
762
+ const state = inst.getState();
763
+ const settings = state.settings as Record<string, unknown> || {};
764
+
765
+ const instMeshId = readNonEmptyString(settings.meshNodeFor);
766
+ if (instMeshId !== meshId) continue;
767
+
768
+ const nodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.nodeId);
769
+ if (!nodeId) continue;
770
+
771
+ if (!isIdleSessionState(state)) {
772
+ const status = readNonEmptyString(state.status).toLowerCase();
773
+ skippedSessions.push({
774
+ nodeId,
775
+ sessionId: readNonEmptyString(state.instanceId),
776
+ reason: isTerminalSessionStatus(status) ? 'terminal_session' : 'session_not_idle',
777
+ status: status || undefined,
778
+ });
779
+ continue;
780
+ }
781
+
782
+ const sessionId = state.instanceId;
783
+ const providerType = state.type || readNonEmptyString(settings.providerType);
784
+
785
+ if (providerType) {
786
+ localIdleSessionsChecked += 1;
787
+ tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType);
788
+ } else {
789
+ skippedSessions.push({
790
+ nodeId,
791
+ sessionId,
792
+ reason: 'provider_type_missing',
793
+ });
794
+ }
795
+ }
796
+
797
+ let remoteSessions: Array<{ nodeId: string; sessionId: string; providerType: string }> = [];
798
+ try {
799
+ remoteSessions = MeshRuntimeStore.getInstance().getRemoteIdleSessions();
800
+ } catch { /* best-effort */ }
801
+
802
+ for (const idle of remoteSessions) {
803
+ const node = mesh.nodes.find((n: any) => n.id === idle.nodeId);
804
+ if (node) {
805
+ remoteIdleSessionsChecked += 1;
806
+ const assigned = tryAssignQueueTask(components, meshId, idle.nodeId, idle.sessionId, idle.providerType);
807
+ if (assigned) {
808
+ try {
809
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(idle.nodeId, idle.sessionId);
810
+ } catch { /* best-effort */ }
811
+ }
812
+ }
813
+ }
814
+
815
+ autoLaunchStarted = await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
816
+ const afterQueue = getQueue(meshId);
817
+ const pendingAfter = afterQueue.filter(task => task.status === 'pending').length;
818
+ const assignedAfter = afterQueue.filter(task => task.status === 'assigned').length;
819
+ const newlyAssignedTasks = afterQueue
820
+ .filter(task => task.status === 'assigned' && beforeStatus.get(task.id) !== 'assigned')
821
+ .map(task => ({
822
+ id: task.id,
823
+ nodeId: task.assignedNodeId,
824
+ sessionId: task.assignedSessionId,
825
+ }));
826
+ return {
827
+ success: true,
828
+ meshId,
829
+ pendingBefore,
830
+ assignedBefore,
831
+ pendingAfter,
832
+ assignedAfter,
833
+ claimed: newlyAssignedTasks.length > 0,
834
+ newlyAssignedTasks,
835
+ localIdleSessionsChecked,
836
+ remoteIdleSessionsChecked,
837
+ skippedSessions,
838
+ autoLaunchStarted,
839
+ ...(pendingAfter > 0 && newlyAssignedTasks.length === 0 && localIdleSessionsChecked === 0 && remoteIdleSessionsChecked === 0 && !autoLaunchStarted
840
+ ? { noIdleMeshSessionAvailable: true }
841
+ : {}),
842
+ };
843
+ }
844
+
845
+ async function maybeAutoFastForwardIdleNode(components: DaemonComponents, args: {
846
+ meshId: string;
847
+ nodeId: string;
848
+ sessionId?: string;
849
+ providerType?: string;
850
+ }): Promise<void> {
851
+ const mesh = getMeshWithCache(components, args.meshId);
852
+ const node = mesh?.nodes?.find((candidate: any) => candidate?.id === args.nodeId || candidate?.nodeId === args.nodeId);
853
+ const workspace = readNonEmptyString(node?.workspace);
854
+ if (!workspace) return;
855
+ if (!existsSync(workspace)) return;
856
+
857
+ const policy = resolveAutoFastForwardPolicy(mesh);
858
+ if (!policy.enabled) return;
859
+ if (nodeHasActiveMeshWork(components, args.meshId, args.nodeId, args.sessionId)) return;
860
+
861
+ const throttleKey = `${args.meshId}:${args.nodeId}`;
862
+ const now = Date.now();
863
+ const lastAttempt = idleAutoFastForwardLastAttempt.get(throttleKey) || 0;
864
+ if (now - lastAttempt < IDLE_AUTO_FAST_FORWARD_THROTTLE_MS) return;
865
+ idleAutoFastForwardLastAttempt.set(throttleKey, now);
866
+
867
+ const submoduleIgnorePaths = Array.isArray(node?.policy?.submoduleIgnorePaths)
868
+ ? node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
869
+ : undefined;
870
+ try {
871
+ const dryRun = await fastForwardMeshNode({
872
+ meshId: args.meshId,
873
+ nodeId: args.nodeId,
874
+ workspace,
875
+ execute: false,
876
+ dryRun: true,
877
+ updateSubmodules: false,
878
+ submoduleIgnorePaths,
879
+ trigger: 'idle_auto',
880
+ });
881
+ if (!dryRun || dryRun.code !== 'fast_forward_available' || dryRun.allowed !== true) return;
882
+ const behind = Number(dryRun.current?.behind);
883
+ if (policy.maxBehind !== undefined && Number.isFinite(behind) && behind > policy.maxBehind) return;
884
+ if (policy.requireCleanSubmodules) {
885
+ const submodules = Array.isArray(dryRun.current?.submodules) ? dryRun.current.submodules : [];
886
+ if (submodules.some((submodule: any) => submodule?.dirty || submodule?.outOfSync || submodule?.error)) return;
887
+ }
888
+ await fastForwardMeshNode({
889
+ meshId: args.meshId,
890
+ nodeId: args.nodeId,
891
+ workspace,
892
+ execute: true,
893
+ dryRun: false,
894
+ updateSubmodules: false,
895
+ submoduleIgnorePaths,
896
+ trigger: 'idle_auto',
897
+ });
898
+ } catch (e: any) {
899
+ LOG.warn('MeshFastForward', `Idle auto fast-forward check failed for ${args.nodeId}: ${e?.message || e}`);
900
+ }
901
+ }
902
+
903
+ function runIdleMaintenanceThenAssignQueue(components: DaemonComponents, args: {
904
+ meshId: string;
905
+ nodeId: string;
906
+ sessionId: string;
907
+ providerType: string;
908
+ }): void {
909
+ setImmediate(() => {
910
+ maybeAutoFastForwardIdleNode(components, args)
911
+ .finally(() => {
912
+ try {
913
+ tryAssignQueueTask(components, args.meshId, args.nodeId, args.sessionId, args.providerType);
914
+ } catch (e: any) {
915
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${args.nodeId}: ${e?.message || e}`);
916
+ }
917
+ });
918
+ });
919
+ }
920
+
921
+ // ---------------------------------------------------------------------------
922
+ // Core event injection
923
+ // ---------------------------------------------------------------------------
924
+
925
+ const MESH_COORDINATOR_EVENTS = new Set([
926
+ 'agent:generating_started',
927
+ 'agent:generating_completed',
928
+ 'agent:waiting_approval',
929
+ 'agent:stopped',
930
+ 'agent:ready',
931
+ 'monitor:long_generating',
932
+ 'refine:accepted',
933
+ 'refine:completed',
934
+ 'refine:failed',
935
+ 'worktree_bootstrap_complete',
936
+ 'worktree_bootstrap_failed',
937
+ ]);
938
+
939
+ const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
940
+ 'agent:generating_completed': 'task_completed',
941
+ 'agent:waiting_approval': 'task_approval_needed',
942
+ 'agent:stopped': 'task_failed',
943
+ 'monitor:long_generating': 'task_stalled',
944
+ };
945
+
946
+ export function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
947
+ return typeof eventName === 'string' && MESH_COORDINATOR_EVENTS.has(eventName);
948
+ }
949
+
950
+ // Terminal events that the coordinator is actively blocked waiting on. When the
951
+ // coordinator CLI session dispatches a task (e.g. mesh_send_task) it stays in
952
+ // `generating` until the result arrives — but a generating coordinator queues
953
+ // incoming send_message calls into its adapter's pendingOutboundQueue, which is
954
+ // only flushed on the coordinator's OWN idle transition. That transition can't
955
+ // happen until it receives this very event → deadlock. We force-inject these so
956
+ // they bypass the busy send-guard and land in the PTY while generating.
957
+ const MESH_FORCE_INJECT_EVENTS = new Set([
958
+ 'agent:generating_completed',
959
+ 'agent:stopped',
960
+ 'agent:waiting_approval',
961
+ 'refine:completed',
962
+ 'refine:failed',
963
+ 'worktree_bootstrap_complete',
964
+ 'worktree_bootstrap_failed',
965
+ ]);
966
+
967
+ function shouldForceInjectMeshEvent(eventName: unknown): boolean {
968
+ return typeof eventName === 'string' && MESH_FORCE_INJECT_EVENTS.has(eventName);
969
+ }
970
+
971
+ function injectMeshSystemMessage(components: DaemonComponents, args: {
972
+ meshId: string;
973
+ sourceInstanceId?: string;
974
+ nodeId?: string;
975
+ nodeLabel: string;
976
+ event: string;
977
+ metadataEvent: Record<string, unknown>;
978
+ }) {
979
+ const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
980
+ const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
981
+
982
+ const sourceSession = args.sourceInstanceId
983
+ ? components.instanceManager.getInstance(args.sourceInstanceId)
984
+ : undefined;
985
+ const workerCoordinatorDaemonId = readNonEmptyString(
986
+ (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
987
+ );
988
+ const localDaemonId = readNonEmptyString(loadConfig().machineId);
989
+
990
+ // R2: cloud P2P dashboard metadata sync. The cloud daemon used to do this from its own
991
+ // relay listener; now the single core forwarder invokes the injected hook (no-op on
992
+ // standalone) so the event path stays single-listener and the local code path is identical
993
+ // across standalone and cloud.
994
+ if (components.onMeshCoordinatorEventForwarded) {
995
+ try {
996
+ components.onMeshCoordinatorEventForwarded({
997
+ event: args.event,
998
+ meshId: args.meshId,
999
+ nodeId: eventNodeId || undefined,
1000
+ ...args.metadataEvent,
1001
+ });
1002
+ } catch { /* dashboard metadata sync is best-effort */ }
1003
+ }
1004
+
1005
+ const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
1006
+ event: args.event,
1007
+ meshId: args.meshId,
1008
+ metadataEvent: args.metadataEvent,
1009
+ sessionId: eventSessionId || undefined,
1010
+ nodeId: eventNodeId || undefined,
1011
+ });
1012
+ if (intentionalCleanupStop) {
1013
+ if (eventSessionId && eventNodeId) {
1014
+ try {
1015
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(eventNodeId, eventSessionId);
1016
+ } catch { /* best-effort */ }
1017
+ }
1018
+ LOG.info('MeshEvents', `Suppressed ${args.event} for intentionally cleanup-stopped session ${eventSessionId || '(unknown session)'}`);
1019
+ return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
1020
+ }
1021
+
1022
+ if (args.event === 'monitor:long_generating') {
1023
+ const reconciledCompletion = buildLongGeneratingCompletionReconciliation({
1024
+ meshId: args.meshId,
1025
+ nodeId: args.nodeId,
1026
+ nodeLabel: args.nodeLabel,
1027
+ metadataEvent: args.metadataEvent,
1028
+ sourceInstanceId: args.sourceInstanceId,
1029
+ });
1030
+ if (reconciledCompletion?.source === 'long_generating_reconciliation') {
1031
+ LOG.info('MeshEvents', `Reconciled long-generating monitor to completion for session ${eventSessionId || '(unknown session)'}`);
1032
+ return injectMeshSystemMessage(components, {
1033
+ ...args,
1034
+ event: 'agent:generating_completed',
1035
+ metadataEvent: reconciledCompletion,
1036
+ });
1037
+ }
1038
+ if (reconciledCompletion?.source === 'long_generating_terminal_ledger_suppression') {
1039
+ LOG.info('MeshEvents', `Suppressed long-generating monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
1040
+ return {
1041
+ success: true,
1042
+ forwarded: 0,
1043
+ suppressed: true,
1044
+ terminalLedgerEvidence: true,
1045
+ terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
1046
+ };
1047
+ }
1048
+ }
1049
+
1050
+ if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
1051
+ LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
1052
+ return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
1053
+ }
1054
+
1055
+ const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
1056
+ if (args.event === 'agent:waiting_approval' && eventSessionId) {
1057
+ const duplicateApproval = isDuplicateMeshApprovalEvent({
1058
+ meshId: args.meshId,
1059
+ sessionId: eventSessionId,
1060
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1061
+ timestamp: eventTimestamp,
1062
+ modalMessage: readNonEmptyString(args.metadataEvent.modalMessage) || undefined,
1063
+ modalButtons: args.metadataEvent.modalButtons,
1064
+ });
1065
+ if (duplicateApproval) {
1066
+ LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
1067
+ return { success: true, forwarded: 0, suppressed: true, duplicateApproval: true };
1068
+ }
1069
+ }
1070
+ if (args.event === 'agent:generating_completed' && eventSessionId) {
1071
+ const terminal = findRecentTerminalLedgerEvidence({
1072
+ meshId: args.meshId,
1073
+ sessionId: eventSessionId,
1074
+ nodeId: eventNodeId || undefined,
1075
+ });
1076
+ if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
1077
+ const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
1078
+ if (!newDispatchAfterTerminal) {
1079
+ const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
1080
+ const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
1081
+ const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
1082
+ const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
1083
+ if (
1084
+ (terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
1085
+ || (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
1086
+ || args.metadataEvent.source === 'long_generating_reconciliation'
1087
+ ) {
1088
+ LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
1089
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
1090
+ }
1091
+ }
1092
+ }
1093
+ const duplicateCompletion = isDuplicateMeshCompletionEvent({
1094
+ meshId: args.meshId,
1095
+ event: args.event,
1096
+ sessionId: eventSessionId,
1097
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1098
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
1099
+ timestamp: eventTimestamp,
1100
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1101
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
1102
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
1103
+ nodeId: eventNodeId || undefined,
1104
+ });
1105
+ if (duplicateCompletion) {
1106
+ LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
1107
+ return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
1108
+ }
1109
+ }
1110
+ if (args.event === 'agent:stopped' && eventSessionId) {
1111
+ const duplicateStopped = isDuplicateMeshCompletionEvent({
1112
+ meshId: args.meshId,
1113
+ event: args.event,
1114
+ sessionId: eventSessionId,
1115
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1116
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
1117
+ timestamp: eventTimestamp,
1118
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
1119
+ coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
1120
+ taskId: readNonEmptyString(args.metadataEvent.taskId) || undefined,
1121
+ nodeId: eventNodeId || undefined,
1122
+ });
1123
+ if (duplicateStopped) {
1124
+ LOG.info('MeshEvents', `Suppressed duplicate stopped event for mesh ${args.meshId} session ${eventSessionId}`);
1125
+ return { success: true, forwarded: 0, suppressed: true, duplicateStopped: true };
1126
+ }
1127
+ }
1128
+
1129
+ function markSessionTerminal(sessionId: string, outcome: 'completed' | 'failed', occurredAtMs?: number | null): { id?: string } | null {
1130
+ const task = updateSessionTaskStatus(args.meshId, sessionId, outcome, {
1131
+ occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
1132
+ });
1133
+ updateDirectDispatchStatus(args.meshId, sessionId, outcome);
1134
+ markSessionDeliveriesTerminal(args.meshId, sessionId, outcome);
1135
+ setImmediate(() => cleanupTerminalDirectDispatches());
1136
+ return task ? { id: task.id } : null;
1137
+ }
1138
+
1139
+ let completedTaskForLedger: { id?: string } | null = null;
1140
+ if (args.event === 'agent:generating_completed') {
1141
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1142
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1143
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
1144
+
1145
+ if (sessionId) {
1146
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed', eventTimestamp);
1147
+ if (nodeId && providerType) {
1148
+ runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
1149
+ }
1150
+ // M1-3: wake dependents of the completed task. The maintenance path above
1151
+ // only assigns to the completing session; dependents may be claimable by
1152
+ // other idle sessions, so run a full queue trigger when any are waiting.
1153
+ const completedTaskId = completedTaskForLedger?.id;
1154
+ if (completedTaskId && hasPendingDependents(args.meshId, completedTaskId)) {
1155
+ setImmediate(() => {
1156
+ triggerMeshQueue(components, args.meshId).catch((e: any) => {
1157
+ LOG.warn('MeshQueue', `Dependent wake after task ${completedTaskId} failed: ${e?.message || e}`);
1158
+ });
1159
+ });
1160
+ }
1161
+ }
1162
+ } else if (args.event === 'agent:ready') {
1163
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1164
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1165
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
1166
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1167
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1168
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1169
+ const hasCompletionEvidence = !!finalSummary || !!workerResult;
1170
+ if (sessionId && hasCompletionEvidence) {
1171
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed');
1172
+ if (completedTaskForLedger) {
1173
+ try {
1174
+ appendLedgerEntry(args.meshId, {
1175
+ kind: 'task_completed',
1176
+ nodeId: nodeId || undefined,
1177
+ sessionId,
1178
+ providerType: providerType || undefined,
1179
+ payload: {
1180
+ event: args.event,
1181
+ nodeLabel: args.nodeLabel,
1182
+ taskId: completedTaskForLedger.id,
1183
+ completedViaReady: true,
1184
+ providerSessionId,
1185
+ finalSummary,
1186
+ workerResult,
1187
+ evidence: buildTaskCompletionEvidence({
1188
+ event: 'agent:ready',
1189
+ nodeId,
1190
+ sessionId,
1191
+ providerType: providerType || undefined,
1192
+ providerSessionId,
1193
+ finalSummary,
1194
+ workerResult,
1195
+ }),
1196
+ },
1197
+ });
1198
+ } catch (e: any) {
1199
+ LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
1200
+ }
1201
+ }
1202
+ }
1203
+
1204
+ if (sessionId && nodeId && providerType) {
1205
+ sweepExpiredRemoteIdleSessions();
1206
+ try {
1207
+ MeshRuntimeStore.getInstance().setRemoteIdleSession(nodeId, sessionId, providerType, Date.now() + REMOTE_IDLE_SESSION_TTL_MS);
1208
+ } catch { /* best-effort */ }
1209
+ setImmediate(() => {
1210
+ maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
1211
+ .finally(() => {
1212
+ try {
1213
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
1214
+ if (assigned) MeshRuntimeStore.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1215
+ } catch (e: any) {
1216
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
1217
+ }
1218
+ });
1219
+ });
1220
+ }
1221
+ } else if (args.event === 'agent:generating_started') {
1222
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1223
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1224
+ if (sessionId && nodeId) {
1225
+ try {
1226
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1227
+ } catch { /* best-effort */ }
1228
+ }
1229
+ if (sessionId) {
1230
+ updateDirectDispatchStatus(args.meshId, sessionId, 'acked');
1231
+ const activeDeliveries = ((): { id: string }[] => {
1232
+ try { return MeshRuntimeStore.getInstance().getActiveSessionDeliveries(args.meshId, sessionId); }
1233
+ catch { return []; }
1234
+ })();
1235
+ for (const d of activeDeliveries) {
1236
+ updateSessionDeliveryStatus(d.id, 'acked');
1237
+ }
1238
+ }
1239
+ } else if (args.event === 'agent:stopped') {
1240
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1241
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1242
+ if (sessionId && nodeId) {
1243
+ try {
1244
+ MeshRuntimeStore.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1245
+ } catch { /* best-effort */ }
1246
+ }
1247
+ if (sessionId) {
1248
+ completedTaskForLedger = markSessionTerminal(sessionId, 'failed');
1249
+ }
1250
+ }
1251
+
1252
+ const ledgerKind = EVENT_TO_LEDGER_KIND[args.event];
1253
+ if (ledgerKind) {
1254
+ try {
1255
+ const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
1256
+ const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
1257
+ const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
1258
+ const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
1259
+ const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1260
+ const workerResult = readWorkerResultMetadata(args.metadataEvent);
1261
+ const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
1262
+ ? buildTaskCompletionEvidence({
1263
+ event: 'agent:generating_completed',
1264
+ nodeId: ledgerNodeId,
1265
+ sessionId: ledgerSessionId,
1266
+ providerType: ledgerProviderType,
1267
+ providerSessionId,
1268
+ finalSummary,
1269
+ workerResult,
1270
+ })
1271
+ : undefined;
1272
+ appendLedgerEntry(args.meshId, {
1273
+ kind: ledgerKind,
1274
+ nodeId: ledgerNodeId,
1275
+ sessionId: ledgerSessionId,
1276
+ providerType: ledgerProviderType,
1277
+ payload: {
1278
+ event: args.event,
1279
+ nodeLabel: args.nodeLabel,
1280
+ taskId: completedTaskForLedger?.id || undefined,
1281
+ providerSessionId,
1282
+ finalSummary,
1283
+ workerResult,
1284
+ completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
1285
+ ? args.metadataEvent.completionDiagnostic
1286
+ : undefined,
1287
+ evidence: completionEvidence,
1288
+ // B2: evidenceLevel lets coordinator know when completion evidence is insufficient.
1289
+ ...(completionEvidence
1290
+ ? completionEvidence.workerResult.source === 'default'
1291
+ ? { evidenceLevel: 'insufficient', reviewRecommended: true }
1292
+ : { evidenceLevel: 'sufficient' }
1293
+ : {}),
1294
+ },
1295
+ });
1296
+ } catch (e: any) {
1297
+ LOG.warn('MeshLedger', `Failed to record ${ledgerKind}: ${e?.message || e}`);
1298
+ }
1299
+ }
1300
+
1301
+ let recoveryContext: SessionRecoveryContext | null = null;
1302
+ if (args.event === 'agent:stopped') {
1303
+ try {
1304
+ const mesh = getMesh(args.meshId);
1305
+ const maxRetries = mesh?.policy?.maxTaskRetries ?? 1;
1306
+
1307
+ recoveryContext = getSessionRecoveryContext(args.meshId, {
1308
+ sessionId: resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined,
1309
+ nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
1310
+ maxRetries,
1311
+ });
1312
+ recoveryContext.failedProviderType = readNonEmptyString(args.metadataEvent.providerType) || null;
1313
+
1314
+ if (recoveryContext.retryRecommended && recoveryContext.consecutiveNodeFailures > 0) {
1315
+ appendLedgerEntry(args.meshId, {
1316
+ kind: 'recovery_attempted',
1317
+ nodeId: recoveryContext.failedNodeId || undefined,
1318
+ sessionId: recoveryContext.failedSessionId || undefined,
1319
+ providerType: recoveryContext.failedProviderType || undefined,
1320
+ payload: {
1321
+ consecutiveFailures: recoveryContext.consecutiveNodeFailures,
1322
+ taskAttemptCount: recoveryContext.taskAttemptCount,
1323
+ retryRecommended: recoveryContext.retryRecommended,
1324
+ advice: recoveryContext.advice,
1325
+ },
1326
+ });
1327
+
1328
+ if (recoveryContext.lastTaskMessage && recoveryContext.failedNodeId && recoveryContext.failedProviderType) {
1329
+ const autoNodeId = recoveryContext.failedNodeId;
1330
+ try {
1331
+ const task = enqueueTask(args.meshId, recoveryContext.lastTaskMessage, {
1332
+ targetNodeId: autoNodeId
1333
+ });
1334
+ LOG.info('MeshRecovery', `Auto-requeued failed task: ${task.id} for node ${autoNodeId}`);
1335
+
1336
+ const node = mesh?.nodes.find((n: any) => n.id === autoNodeId);
1337
+ if (node) {
1338
+ components.cliManager.handleCliCommand('launch_cli', {
1339
+ cliType: recoveryContext.failedProviderType,
1340
+ dir: node.workspace,
1341
+ settings: {
1342
+ role: 'worker',
1343
+ meshNodeFor: args.meshId,
1344
+ meshNodeId: node.id,
1345
+ spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
1346
+ launchedByCoordinator: true,
1347
+ }
1348
+ }).catch((e: any) => LOG.error('MeshRecovery', `Failed to auto-relaunch session for ${node.id}: ${e?.message}`));
1349
+ }
1350
+ } catch (e: any) {
1351
+ LOG.warn('MeshRecovery', `Failed to execute auto-recovery: ${e?.message}`);
1352
+ }
1353
+ }
1354
+ }
1355
+
1356
+ LOG.info('MeshRecovery', `Recovery context for ${args.nodeLabel}: ${recoveryContext.advice}`);
1357
+ } catch (e: any) {
1358
+ LOG.warn('MeshRecovery', `Failed to build recovery context: ${e?.message || e}`);
1359
+ }
1360
+ }
1361
+
1362
+ const messageText = buildMeshSystemMessage({
1363
+ event: args.event,
1364
+ nodeLabel: args.nodeLabel,
1365
+ metadataEvent: args.metadataEvent,
1366
+ recoveryContext,
1367
+ });
1368
+ if (!messageText) return { success: false, error: 'unsupported mesh event' };
1369
+
1370
+ const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
1371
+ const instState = inst.getState();
1372
+ if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
1373
+ if (args.sourceInstanceId && instState.instanceId === args.sourceInstanceId) return false;
1374
+ // canonicalize: the worker stamps the prefixed daemon id (standalone_/daemon_) while
1375
+ // localDaemonId is the raw machineId — a literal !== wrongly excludes the local coordinator.
1376
+ if (workerCoordinatorDaemonId && localDaemonId && !sameDaemonId(workerCoordinatorDaemonId, localDaemonId)) return false;
1377
+ return true;
1378
+ });
1379
+
1380
+ if (coordinatorInstances.length === 0) {
1381
+ // R2: the coordinator is not a live instance on THIS daemon. If it lives on a remote
1382
+ // daemon, forward over the injected transport (dispatchMeshCommand, supplied by cloud as
1383
+ // P2P; absent/no-op on standalone). This replaces cloud's separate
1384
+ // relayRemoteMeshCoordinatorEvent listener — remote delivery is now part of the single
1385
+ // core forwarder path, so local-vs-remote is one code path and standalone == cloud for
1386
+ // the local case. The pending queue remains the fallback when there is no remote target
1387
+ // or the transport send fails (e.g. P2P down), so MCP coordinators can still backfill.
1388
+ const remoteCoordinatorDaemonId = workerCoordinatorDaemonId && localDaemonId && !sameDaemonId(workerCoordinatorDaemonId, localDaemonId)
1389
+ ? workerCoordinatorDaemonId
1390
+ : '';
1391
+ if (remoteCoordinatorDaemonId && components.dispatchMeshCommand) {
1392
+ const forwardPayload: Record<string, unknown> = {
1393
+ event: args.event,
1394
+ meshId: args.meshId,
1395
+ nodeId: args.nodeId || undefined,
1396
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
1397
+ ...args.metadataEvent,
1398
+ ...(recoveryContext ? { recoveryContext } : {}),
1399
+ };
1400
+ components.dispatchMeshCommand(remoteCoordinatorDaemonId, 'mesh_forward_event', forwardPayload)
1401
+ .then(() => {
1402
+ LOG.info('MeshEvents', `Forwarded ${args.event} for mesh ${args.meshId} to remote coordinator daemon ${remoteCoordinatorDaemonId.slice(0, 12)}…`);
1403
+ })
1404
+ .catch((error: any) => {
1405
+ LOG.warn('MeshEvents', `Remote forward of ${args.event} failed (${error?.message || error}); queuing for backfill`);
1406
+ queuePendingMeshCoordinatorEvent({
1407
+ event: args.event,
1408
+ meshId: args.meshId,
1409
+ nodeLabel: args.nodeLabel,
1410
+ nodeId: args.nodeId || undefined,
1411
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
1412
+ metadataEvent: { ...args.metadataEvent, ...(recoveryContext ? { recoveryContext } : {}) },
1413
+ coordinatorMessage: messageText,
1414
+ queuedAt: Date.now(),
1415
+ targetCoordinatorDaemonId: remoteCoordinatorDaemonId,
1416
+ });
1417
+ });
1418
+ return { success: true, forwarded: 0, remoteForwarded: true };
1419
+ }
1420
+ if (queuePendingMeshCoordinatorEvent({
1421
+ event: args.event,
1422
+ meshId: args.meshId,
1423
+ nodeLabel: args.nodeLabel,
1424
+ nodeId: args.nodeId || undefined,
1425
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
1426
+ metadataEvent: {
1427
+ ...args.metadataEvent,
1428
+ ...(recoveryContext ? { recoveryContext } : {}),
1429
+ },
1430
+ coordinatorMessage: messageText,
1431
+ queuedAt: Date.now(),
1432
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
1433
+ })) {
1434
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''})`);
1435
+ }
1436
+ return { success: true, forwarded: 0 };
1437
+ }
1438
+
1439
+ // All events — terminal and non-terminal — are queued for MCP coordinator
1440
+ // delivery via the pending-events file/SQLite path. This ensures that MCP
1441
+ // coordinators (which poll via get_pending_mesh_events / mesh_status) always
1442
+ // receive terminal events even when a live CLI coordinator session is present.
1443
+ // Previously, terminal events skipped the queue and were only direct-injected
1444
+ // into live CLI coordinators via send_message, which silently dropped them
1445
+ // when the coordinator was in a generating state.
1446
+ const pendingEvent = {
1447
+ event: args.event,
1448
+ meshId: args.meshId,
1449
+ nodeLabel: args.nodeLabel,
1450
+ nodeId: args.nodeId || undefined,
1451
+ workspace: readNonEmptyString(args.metadataEvent.workspace),
1452
+ metadataEvent: {
1453
+ ...args.metadataEvent,
1454
+ ...(recoveryContext ? { recoveryContext } : {}),
1455
+ },
1456
+ coordinatorMessage: messageText,
1457
+ queuedAt: Date.now(),
1458
+ ...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
1459
+ };
1460
+ if (queuePendingMeshCoordinatorEvent(pendingEvent)) {
1461
+ LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
1462
+ }
1463
+
1464
+ // R3: the live coordinators below receive this event directly in their PTY. They (or their
1465
+ // MCP client) also poll get_pending_mesh_events with coordinatorDaemonId = this daemon, which
1466
+ // would re-deliver the queued copy → the user saw the same completion twice. Mark the event
1467
+ // direct-delivered to THIS daemon so that coordinator's own drain skips it. The queue entry
1468
+ // stays for every OTHER consumer (idle / MCP-only / remote) that did not get the direct inject.
1469
+ // markMeshCoordinatorEventDirectDelivered canonicalizes the id internally so the raw
1470
+ // machineId here matches the prefixed instanceId (standalone_mach_X) the coordinator drains with.
1471
+ if (localDaemonId) {
1472
+ markMeshCoordinatorEventDirectDelivered(localDaemonId, pendingEvent);
1473
+ }
1474
+
1475
+ const forceInject = shouldForceInjectMeshEvent(args.event);
1476
+ for (const coord of coordinatorInstances) {
1477
+ const coordState = coord.getState();
1478
+ LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}${forceInject ? ' (force)' : ''}`);
1479
+ coord.onEvent('send_message', {
1480
+ input: { text: messageText, textFallback: messageText },
1481
+ ...(forceInject ? { force: true } : {}),
1482
+ });
1483
+ }
1484
+ return { success: true, forwarded: coordinatorInstances.length };
1485
+ }
1486
+
1487
+ export function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>) {
1488
+ const eventName = readNonEmptyString(payload.event);
1489
+ if (!isMeshCoordinatorEvent(eventName)) {
1490
+ return { success: false, error: 'unsupported mesh event' };
1491
+ }
1492
+ const meshId = readNonEmptyString(payload.meshId);
1493
+ if (!meshId) return { success: false, error: 'meshId required' };
1494
+
1495
+ const nodeId = readNonEmptyString(payload.nodeId);
1496
+ const workspace = readNonEmptyString(payload.workspace);
1497
+ const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
1498
+ const relayModalMessage = readNonEmptyString(payload.modalMessage);
1499
+ const relayModalButtons = Array.isArray(payload.modalButtons)
1500
+ ? (payload.modalButtons as unknown[]).filter((b): b is string => typeof b === 'string' && b.trim().length > 0)
1501
+ : null;
1502
+
1503
+ return injectMeshSystemMessage(components, {
1504
+ meshId,
1505
+ nodeId,
1506
+ nodeLabel,
1507
+ event: eventName,
1508
+ metadataEvent: {
1509
+ targetSessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId) || readNonEmptyString(payload.instanceId),
1510
+ providerType: readNonEmptyString(payload.providerType),
1511
+ providerSessionId: readNonEmptyString(payload.providerSessionId),
1512
+ finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1513
+ jobId: readNonEmptyString(payload.jobId),
1514
+ interactionId: readNonEmptyString(payload.interactionId),
1515
+ status: readNonEmptyString(payload.status),
1516
+ targetDaemonId: readNonEmptyString(payload.targetDaemonId),
1517
+ startedAt: readNonEmptyString(payload.startedAt),
1518
+ completedAt: readNonEmptyString(payload.completedAt),
1519
+ retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1520
+ ...(relayModalMessage ? { modalMessage: relayModalMessage } : {}),
1521
+ ...(relayModalButtons && relayModalButtons.length > 0 ? { modalButtons: relayModalButtons } : {}),
1522
+ ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1523
+ ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
1524
+ ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
1525
+ ...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
1526
+ ...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
1527
+ ...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
1528
+ intentional: payload.intentional === true,
1529
+ intentionalStop: payload.intentionalStop === true,
1530
+ operatorCleanup: payload.operatorCleanup === true,
1531
+ reason: readNonEmptyString(payload.reason),
1532
+ stopReason: readNonEmptyString(payload.stopReason),
1533
+ cleanupReason: readNonEmptyString(payload.cleanupReason),
1534
+ source: readNonEmptyString(payload.source),
1535
+ },
1536
+ });
1537
+ }
1538
+
1539
+ export function setupMeshEventForwarding(components: DaemonComponents) {
1540
+ components.instanceManager.onEvent((event) => {
1541
+ // --- Coordinator idle auto-flush ---
1542
+ // When a coordinator session becomes idle, flush any pending coordinator events
1543
+ // that accumulated while it was generating. This runs before the delegate routing
1544
+ // below so that coordinator-own idle transitions are handled first.
1545
+ // Exception: a coordinator that is itself a direct-dispatch target still needs
1546
+ // to go through delegate routing so that the dispatching coordinator receives a
1547
+ // pendingCoordinatorEvents entry for the completion.
1548
+ if (event.event === 'agent:ready' || event.event === 'agent:generating_completed') {
1549
+ const flushInstanceId = readNonEmptyString(event.instanceId);
1550
+ if (flushInstanceId) {
1551
+ const flushSource = components.instanceManager.getInstance(flushInstanceId);
1552
+ if (flushSource && flushSource.category === 'cli') {
1553
+ const flushState = flushSource.getState();
1554
+ const flushSettings = flushState.settings && typeof flushState.settings === 'object' ? flushState.settings as Record<string, unknown> : {};
1555
+ const coordinatorMeshId = readNonEmptyString(flushSettings.meshCoordinatorFor);
1556
+ if (coordinatorMeshId) {
1557
+ const status = readNonEmptyString(flushState.status).toLowerCase();
1558
+ if (status === 'idle') {
1559
+ try {
1560
+ const localDaemonId = readNonEmptyString(loadConfig().machineId) || undefined;
1561
+ const pendingEvents = drainPendingMeshCoordinatorEvents(coordinatorMeshId, localDaemonId);
1562
+ if (pendingEvents.length > 0) {
1563
+ LOG.info('MeshEvents', `Auto-flushing ${pendingEvents.length} pending coordinator event(s) for mesh ${coordinatorMeshId} on coordinator idle`);
1564
+ for (const pending of pendingEvents) {
1565
+ if (!pending.coordinatorMessage) continue;
1566
+ const forcePending = shouldForceInjectMeshEvent(pending.event);
1567
+ flushSource.onEvent('send_message', {
1568
+ input: { text: pending.coordinatorMessage, textFallback: pending.coordinatorMessage },
1569
+ ...(forcePending ? { force: true } : {}),
1570
+ });
1571
+ }
1572
+ }
1573
+ } catch (e: any) {
1574
+ LOG.warn('MeshEvents', `Failed to auto-flush pending coordinator events: ${e?.message || e}`);
1575
+ }
1576
+ }
1577
+ // Skip delegate routing unless this coordinator session is itself
1578
+ // a direct-dispatch target — in that case fall through so the
1579
+ // dispatching coordinator gets a pendingCoordinatorEvents entry.
1580
+ let hasDirectDispatch = false;
1581
+ try {
1582
+ hasDirectDispatch =
1583
+ getActiveDirectDispatches(coordinatorMeshId).some(d => d.sessionId === flushInstanceId)
1584
+ || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, flushInstanceId);
1585
+ } catch { /* best-effort */ }
1586
+ if (!hasDirectDispatch) return;
1587
+ }
1588
+ }
1589
+ }
1590
+ }
1591
+
1592
+ // --- Delegate event routing ---
1593
+ if (!isMeshCoordinatorEvent(event.event)) return;
1594
+
1595
+ const instanceId = readNonEmptyString(event.instanceId);
1596
+ if (!instanceId) return;
1597
+
1598
+ // R1: all session→node→mesh→coordinator interpretation is folded into the single
1599
+ // resolveWorkerDelegateRouting() resolver. No stamp (meshNodeFor / meshNodeId /
1600
+ // meshCoordinatorDaemonId / meshCoordinatorNodeId / launchedByCoordinator) is read
1601
+ // here to make a routing decision — the resolver is the one authority, and the
1602
+ // forwarder consumes its typed result only.
1603
+ const routing = resolveWorkerDelegateRouting(components, instanceId, {
1604
+ getMeshById: (meshId) => getMeshWithCache(components, meshId),
1605
+ getMeshByWorkspace: (workspace) => getCachedMeshByWorkspace(workspace),
1606
+ });
1607
+ if (!routing.isDelegate) {
1608
+ // R4: a worker that presented a valid envelope but resolved to no mesh used to be
1609
+ // dropped silently. Leave a fail-loud diagnostic so the missing completion is
1610
+ // traceable. Benign non-delegate rejections (not_cli / no_workspace / etc.) are
1611
+ // no-ops inside recordUnroutableDelegateEvent.
1612
+ recordUnroutableDelegateEvent(routing, event.event);
1613
+ return;
1614
+ }
1615
+
1616
+ injectMeshSystemMessage(components, {
1617
+ meshId: routing.meshId,
1618
+ sourceInstanceId: instanceId,
1619
+ nodeId: routing.nodeId,
1620
+ nodeLabel: routing.nodeLabel,
1621
+ event: event.event,
1622
+ metadataEvent: event,
1623
+ });
1624
+ });
1625
+ }