@adhdev/daemon-core 0.9.82-rc.29 → 0.9.82-rc.290

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