@adhdev/daemon-core 0.9.82-rc.32 → 0.9.82-rc.320

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 (306) 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 +46 -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 +115 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
  14. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  15. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  16. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  17. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  18. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  19. package/dist/commands/cli-manager.d.ts +2 -1
  20. package/dist/commands/handler.d.ts +110 -0
  21. package/dist/commands/mesh-coordinator.d.ts +85 -1
  22. package/dist/commands/router.d.ts +229 -1
  23. package/dist/commands/upgrade-helper.d.ts +12 -0
  24. package/dist/config/chat-history.d.ts +9 -0
  25. package/dist/config/config.d.ts +5 -0
  26. package/dist/config/mesh-config.d.ts +72 -1
  27. package/dist/detection/ide-detector.d.ts +13 -0
  28. package/dist/detection/win32-ide-version.d.ts +37 -0
  29. package/dist/git/git-commands.d.ts +9 -1
  30. package/dist/git/git-diff.d.ts +6 -0
  31. package/dist/git/git-executor.d.ts +11 -0
  32. package/dist/git/git-status.d.ts +9 -0
  33. package/dist/git/git-types.d.ts +2 -50
  34. package/dist/index.d.ts +51 -12
  35. package/dist/index.js +36031 -12365
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +40382 -16833
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/installer.d.ts +1 -4
  40. package/dist/ipc/local-ipc-server.d.ts +91 -0
  41. package/dist/launch.d.ts +1 -1
  42. package/dist/logging/async-batch-writer.d.ts +10 -0
  43. package/dist/logging/log-redactor.d.ts +24 -0
  44. package/dist/logging/log-tail-reader.d.ts +46 -0
  45. package/dist/mesh/contracts.d.ts +164 -0
  46. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  47. package/dist/mesh/coordinator-registry.d.ts +59 -0
  48. package/dist/mesh/mesh-active-work.d.ts +108 -0
  49. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  50. package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
  51. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  52. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  53. package/dist/mesh/mesh-events-utils.d.ts +47 -0
  54. package/dist/mesh/mesh-events.d.ts +6 -49
  55. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  56. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  57. package/dist/mesh/mesh-init.d.ts +86 -0
  58. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  59. package/dist/mesh/mesh-ledger.d.ts +77 -1
  60. package/dist/mesh/mesh-missions.d.ts +116 -0
  61. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  62. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  63. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  64. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  65. package/dist/mesh/mesh-routing.d.ts +70 -0
  66. package/dist/mesh/mesh-runtime-store.d.ts +401 -0
  67. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  68. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  69. package/dist/mesh/mesh-work-queue.d.ts +226 -5
  70. package/dist/mesh/preview-freshness.d.ts +18 -0
  71. package/dist/mesh/refine-config.d.ts +215 -0
  72. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  73. package/dist/providers/acp-provider-instance.d.ts +2 -0
  74. package/dist/providers/approval-utils.d.ts +13 -0
  75. package/dist/providers/cli-provider-instance.d.ts +61 -3
  76. package/dist/providers/contracts.d.ts +130 -6
  77. package/dist/providers/external-sources.d.ts +71 -0
  78. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  79. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  80. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  81. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  82. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  83. package/dist/providers/native-history/index.d.ts +13 -0
  84. package/dist/providers/provider-instance-manager.d.ts +13 -0
  85. package/dist/providers/provider-instance.d.ts +13 -1
  86. package/dist/providers/provider-loader.d.ts +26 -4
  87. package/dist/providers/provider-trust.d.ts +31 -0
  88. package/dist/providers/read-chat-contract.d.ts +29 -0
  89. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  90. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  91. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  92. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  93. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  94. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  95. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  96. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  97. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  98. package/dist/providers/sdk/v1/index.d.ts +30 -0
  99. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  100. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  101. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  102. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  103. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  104. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  105. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  106. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  107. package/dist/providers/spec/adapter.d.ts +69 -0
  108. package/dist/providers/spec/cli-adapter.d.ts +217 -0
  109. package/dist/providers/spec/evaluator.d.ts +23 -0
  110. package/dist/providers/spec/fsm-driver.d.ts +278 -0
  111. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  112. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  113. package/dist/providers/spec/fsm-types.d.ts +128 -0
  114. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  115. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  116. package/dist/providers/spec/route.d.ts +4 -0
  117. package/dist/providers/spec/types.d.ts +180 -0
  118. package/dist/providers/transcript-v2.d.ts +176 -0
  119. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  120. package/dist/providers/working-dir.d.ts +17 -0
  121. package/dist/repo-mesh-types.d.ts +365 -13
  122. package/dist/sessions/registry.d.ts +6 -0
  123. package/dist/shared-types-extra.d.ts +2 -4
  124. package/dist/shared-types.d.ts +42 -1
  125. package/dist/status/normalize.d.ts +1 -1
  126. package/dist/status/normalize.js +1 -0
  127. package/dist/status/normalize.js.map +1 -1
  128. package/dist/status/normalize.mjs +1 -0
  129. package/dist/status/normalize.mjs.map +1 -1
  130. package/dist/status/reporter.d.ts +2 -0
  131. package/dist/status/snapshot.d.ts +1 -0
  132. package/dist/types.d.ts +5 -0
  133. package/package.json +7 -3
  134. package/src/agent-stream/poller.ts +2 -3
  135. package/src/boot/daemon-lifecycle.ts +55 -12
  136. package/src/boot/process-hardening.ts +89 -0
  137. package/src/build-info.ts +73 -0
  138. package/src/chat/source-machine.ts +534 -0
  139. package/src/chat/source-resolver.ts +0 -0
  140. package/src/chat/subscription-updates.ts +20 -1
  141. package/src/cli-adapter-types.d.ts +1 -0
  142. package/src/cli-adapter-types.ts +47 -2
  143. package/src/cli-adapters/cli-script-runner.ts +421 -0
  144. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  145. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  146. package/src/cli-adapters/provider-cli-adapter.ts +710 -1140
  147. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  148. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  149. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  150. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  151. package/src/cli-adapters/provider-cli-shared.ts +85 -11
  152. package/src/cli-adapters/pty-transport.ts +2 -1
  153. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  154. package/src/cli-adapters/resolve-executable.ts +90 -0
  155. package/src/cli-adapters/session-host-transport.ts +2 -1
  156. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  157. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  158. package/src/cli-adapters/terminal-screen.ts +16 -81
  159. package/src/commands/chat-commands.ts +1822 -61
  160. package/src/commands/cli-manager.ts +286 -14
  161. package/src/commands/handler.ts +809 -2
  162. package/src/commands/mesh-coordinator.ts +332 -122
  163. package/src/commands/router.ts +6370 -704
  164. package/src/commands/stream-commands.ts +8 -0
  165. package/src/commands/upgrade-helper.ts +81 -30
  166. package/src/config/chat-history.ts +483 -24
  167. package/src/config/config.ts +12 -0
  168. package/src/config/mesh-config.ts +391 -4
  169. package/src/config/recent-activity.ts +8 -2
  170. package/src/daemon/dev-auto-implement.ts +3 -2
  171. package/src/daemon/dev-cli-debug.ts +10 -1
  172. package/src/detection/cli-detector.ts +28 -9
  173. package/src/detection/ide-detector.ts +55 -16
  174. package/src/detection/win32-ide-version.ts +106 -0
  175. package/src/git/git-commands.ts +42 -12
  176. package/src/git/git-diff.ts +53 -0
  177. package/src/git/git-executor.ts +12 -0
  178. package/src/git/git-status.ts +408 -43
  179. package/src/git/git-types.ts +14 -62
  180. package/src/git/git-worktree.ts +8 -1
  181. package/src/index.ts +151 -11
  182. package/src/installer.d.ts +1 -1
  183. package/src/installer.ts +8 -6
  184. package/src/ipc/local-ipc-server.ts +278 -0
  185. package/src/launch.d.ts +1 -1
  186. package/src/launch.ts +37 -28
  187. package/src/logging/async-batch-writer.ts +55 -0
  188. package/src/logging/log-redactor.ts +100 -0
  189. package/src/logging/log-tail-reader.ts +220 -0
  190. package/src/logging/logger.ts +2 -1
  191. package/src/mesh/contracts.ts +329 -0
  192. package/src/mesh/coordinator-prompt.ts +235 -31
  193. package/src/mesh/coordinator-registry.ts +121 -0
  194. package/src/mesh/mesh-active-work.ts +469 -0
  195. package/src/mesh/mesh-delivery-policy.ts +315 -0
  196. package/src/mesh/mesh-events-coordinator.ts +2102 -0
  197. package/src/mesh/mesh-events-pending.ts +508 -0
  198. package/src/mesh/mesh-events-stale.ts +290 -0
  199. package/src/mesh/mesh-events-utils.ts +254 -0
  200. package/src/mesh/mesh-events.ts +30 -1035
  201. package/src/mesh/mesh-fast-forward.ts +839 -0
  202. package/src/mesh/mesh-host-ownership.ts +73 -0
  203. package/src/mesh/mesh-init.ts +260 -0
  204. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  205. package/src/mesh/mesh-ledger.ts +589 -102
  206. package/src/mesh/mesh-missions.ts +248 -0
  207. package/src/mesh/mesh-reconcile-loop.ts +496 -0
  208. package/src/mesh/mesh-refine-batch.ts +197 -0
  209. package/src/mesh/mesh-refine-status.ts +231 -0
  210. package/src/mesh/mesh-review-inbox.ts +307 -0
  211. package/src/mesh/mesh-routing.ts +291 -0
  212. package/src/mesh/mesh-runtime-store.ts +1578 -0
  213. package/src/mesh/mesh-task-stats.ts +154 -0
  214. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  215. package/src/mesh/mesh-work-queue.ts +780 -139
  216. package/src/mesh/preview-freshness.ts +118 -0
  217. package/src/mesh/refine-config.ts +403 -0
  218. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  219. package/src/providers/acp-provider-instance.ts +19 -4
  220. package/src/providers/approval-utils.d.ts +4 -0
  221. package/src/providers/approval-utils.ts +47 -5
  222. package/src/providers/chat-message-normalization.ts +4 -11
  223. package/src/providers/cli-provider-instance.ts +958 -91
  224. package/src/providers/contracts.d.ts +55 -0
  225. package/src/providers/contracts.ts +141 -6
  226. package/src/providers/extension-provider-instance.ts +5 -1
  227. package/src/providers/external-sources.ts +218 -0
  228. package/src/providers/ide-provider-instance.ts +25 -6
  229. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  230. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  231. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  232. package/src/providers/native-history/dispatcher.ts +340 -0
  233. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  234. package/src/providers/native-history/index.ts +30 -0
  235. package/src/providers/provider-instance-manager.ts +30 -0
  236. package/src/providers/provider-instance.ts +10 -1
  237. package/src/providers/provider-loader.ts +662 -50
  238. package/src/providers/provider-schema.ts +87 -14
  239. package/src/providers/provider-trust.ts +114 -0
  240. package/src/providers/read-chat-contract.ts +76 -16
  241. package/src/providers/sdk/README.md +49 -0
  242. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  243. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  244. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  245. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  246. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  247. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  248. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  249. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  250. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  251. package/src/providers/sdk/v1/index.ts +152 -0
  252. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  253. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  254. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  255. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  256. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  257. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  258. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  259. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  260. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  261. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  262. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  263. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  264. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  265. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  266. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  267. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  268. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  269. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  270. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  271. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  272. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  273. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  274. package/src/providers/sdk/v1/validators/index.ts +19 -0
  275. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  276. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  277. package/src/providers/spec/adapter.ts +168 -0
  278. package/src/providers/spec/cli-adapter.ts +986 -0
  279. package/src/providers/spec/evaluator.ts +345 -0
  280. package/src/providers/spec/fsm-driver.ts +909 -0
  281. package/src/providers/spec/fsm-evaluator.ts +255 -0
  282. package/src/providers/spec/fsm-loader.ts +112 -0
  283. package/src/providers/spec/fsm-types.ts +222 -0
  284. package/src/providers/spec/native-history-executor.ts +1136 -0
  285. package/src/providers/spec/pre-launch-trust.ts +104 -0
  286. package/src/providers/spec/route.ts +51 -0
  287. package/src/providers/spec/types.ts +226 -0
  288. package/src/providers/transcript-v2.ts +567 -0
  289. package/src/providers/types/interactive-prompt.ts +536 -0
  290. package/src/providers/version-archive.ts +64 -24
  291. package/src/providers/working-dir.ts +23 -0
  292. package/src/repo-mesh-types.ts +494 -13
  293. package/src/sessions/registry.ts +6 -0
  294. package/src/shared-types-extra.ts +2 -4
  295. package/src/shared-types.ts +45 -1
  296. package/src/status/builders.ts +26 -6
  297. package/src/status/normalize.ts +2 -0
  298. package/src/status/reporter.ts +15 -0
  299. package/src/status/snapshot.ts +84 -25
  300. package/src/system/host-memory.ts +29 -12
  301. package/src/types.ts +5 -0
  302. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  303. package/dist/mesh/mesh-sync.d.ts +0 -53
  304. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  305. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  306. package/src/mesh/mesh-sync.ts +0 -111
@@ -1,28 +1,160 @@
1
- import { existsSync, writeFileSync, readFileSync, openSync, closeSync, unlinkSync } from 'fs';
2
- import { join } from 'path';
3
1
  import { randomUUID } from 'crypto';
4
- import { getLedgerDir } from './mesh-ledger.js';
2
+ import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
3
+ import type { RepoMeshDaemonRole, LocalMeshNodeEntry } from '../repo-mesh-types.js';
4
+ import { MESH_CONVERGE_REFINE_TAG, resolveAutoConvergeCodeChange, resolveTaskAffinityRole } from '../repo-mesh-types.js';
5
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
6
+ import { getMesh } from '../config/mesh-config.js';
5
7
 
6
8
  export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
7
9
  export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
8
10
  export type MeshHistoricalTaskStatus = Extract<MeshTaskStatus, 'completed' | 'failed' | 'cancelled'>;
11
+ export type MeshTaskMode = 'code_change' | 'validation' | 'live_debug_readonly' | 'launch_app' | 'convergence';
9
12
 
10
13
  export const ACTIVE_MESH_QUEUE_STATUSES: MeshActiveTaskStatus[] = ['pending', 'assigned'];
11
14
  export const HISTORICAL_MESH_QUEUE_STATUSES: MeshHistoricalTaskStatus[] = ['completed', 'failed', 'cancelled'];
15
+ export const MESH_TASK_MODES: MeshTaskMode[] = ['code_change', 'validation', 'live_debug_readonly', 'launch_app', 'convergence'];
16
+
17
+ export interface MeshTaskModeValidationResult {
18
+ valid: boolean;
19
+ taskMode?: MeshTaskMode;
20
+ violations: string[];
21
+ allowedOperations?: string[];
22
+ }
23
+
24
+ const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
25
+ { label: 'source_edit', pattern: /\b(edit|modify|patch|apply\s+patch|write\s+(?:to\s+)?(?:file|source)|overwrite|delete\s+file|remove\s+file|create\s+file|touch\s+file)\b/i },
26
+ { label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
27
+ { label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release|npm\s+publish|yarn\s+publish|pnpm\s+publish)\b/i },
28
+ { label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i|shred\b)\b/i },
29
+ { label: 'package_install', pattern: /\b(npm\s+(?:install|i|add|link|uninstall|remove)|yarn\s+(?:add|remove|link)|pnpm\s+(?:add|remove|link)|pip\s+install|brew\s+install|apt\s+install|cargo\s+install)\b/i },
30
+ { label: 'container_mutation', pattern: /\b(docker\s+(?:build|run|exec|push|tag|rmi|rm|create|start|stop|kill)|kubectl\s+(?:apply|delete|patch|replace|create|scale))\b/i },
31
+ ];
32
+
33
+ /**
34
+ * Git subcommands that mutate the working tree, index, refs, or remote.
35
+ * `stash` and `checkout` are intentionally absent here: they have read-only
36
+ * variants (`git stash list`/`show`, `git checkout-index`) and are classified
37
+ * token-by-token in {@link detectGitMutation} rather than by bare keyword.
38
+ */
39
+ const GIT_MUTATION_SUBCOMMANDS = new Set([
40
+ 'add', 'commit', 'push', 'reset', 'rebase', 'clean', 'switch', 'merge',
41
+ 'tag', 'restore', 'rm', 'mv', 'cherry-pick', 'revert', 'pull', 'fetch',
42
+ 'am', 'apply', 'gc', 'prune',
43
+ ]);
44
+
45
+ /**
46
+ * Read-only `git stash` variants. Any other `git stash <x>` (pop/apply/drop/
47
+ * push/save/clear, or bare `git stash` which defaults to push) is a mutation.
48
+ */
49
+ const GIT_STASH_READONLY_SUBCOMMANDS = new Set(['list', 'show']);
50
+
51
+ /**
52
+ * Detects a true git mutation in free-text task message, token-aware so that
53
+ * read-only diagnostics (`git stash list`, `git stash show --stat`,
54
+ * `git checkout-index`, `git status`, `git diff`, `git log`, ...) are allowed.
55
+ * Returns true only when a genuine mutating git invocation is present.
56
+ */
57
+ function detectGitMutation(message: string): boolean {
58
+ const re = /\bgit\s+([a-z][a-z0-9-]*)/gi;
59
+ let match: RegExpExecArray | null;
60
+ while ((match = re.exec(message)) !== null) {
61
+ const sub = match[1].toLowerCase();
62
+ if (GIT_MUTATION_SUBCOMMANDS.has(sub)) return true;
63
+ if (sub === 'stash') {
64
+ // Token following `git stash`; read-only only for list/show.
65
+ const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
66
+ const next = after ? after[1].toLowerCase() : '';
67
+ if (!GIT_STASH_READONLY_SUBCOMMANDS.has(next)) return true; // bare stash = push, or pop/apply/drop/...
68
+ } else if (sub === 'checkout') {
69
+ // `git checkout <ref/path>` mutates; `git checkout-index` is matched
70
+ // as its own token by the regex (sub === 'checkout-index') and is read-only.
71
+ return true;
72
+ } else if (sub === 'submodule') {
73
+ // `git submodule update` mutates; `git submodule status` is read-only.
74
+ const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
75
+ const next = after ? after[1].toLowerCase() : '';
76
+ if (next === 'update' || next === 'add' || next === 'sync' || next === 'deinit') return true;
77
+ } else if (sub === 'worktree') {
78
+ const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
79
+ const next = after ? after[1].toLowerCase() : '';
80
+ if (next === 'add' || next === 'remove' || next === 'move' || next === 'prune') return true;
81
+ }
82
+ // checkout-index, stash-with-no-next-already-handled, status/diff/log/show/
83
+ // rev-parse/branch/submodule status fall through as read-only.
84
+ }
85
+ return false;
86
+ }
87
+
88
+ export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
89
+ if (typeof value !== 'string') return undefined;
90
+ const normalized = value.trim() as MeshTaskMode;
91
+ return (MESH_TASK_MODES as string[]).includes(normalized) ? normalized : undefined;
92
+ }
93
+
94
+ export function validateMeshTaskModeRequest(mode: unknown, message: string): MeshTaskModeValidationResult {
95
+ const taskMode = normalizeMeshTaskMode(mode);
96
+ if (!taskMode) {
97
+ return { valid: true, violations: [] };
98
+ }
99
+ if (taskMode !== 'live_debug_readonly') {
100
+ return { valid: true, taskMode, violations: [] };
101
+ }
102
+ const text = message || '';
103
+ const violations = LIVE_DEBUG_READONLY_FORBIDDEN
104
+ .filter(rule => rule.pattern.test(text))
105
+ .map(rule => rule.label);
106
+ if (detectGitMutation(text)) {
107
+ violations.push('git_mutation');
108
+ }
109
+ return {
110
+ valid: violations.length === 0,
111
+ taskMode,
112
+ violations,
113
+ allowedOperations: [
114
+ 'process/log/window/port/session inspection',
115
+ 'read-only filesystem listing/reading',
116
+ 'status probes and keep-running handle reporting',
117
+ 'diagnostic summaries without source edits, commits, checkpoints, pushes, deploys, resets, rebases, or destructive cleanups',
118
+ ],
119
+ };
120
+ }
12
121
 
13
122
  export interface MeshWorkQueueEntry {
14
123
  id: string;
15
124
  meshId: string;
16
125
  message: string;
17
126
  status: MeshTaskStatus;
127
+ taskMode?: MeshTaskMode;
18
128
  /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
19
129
  targetNodeId?: string;
20
130
  /** If specified, only this runtime session can claim the task */
21
131
  targetSessionId?: string;
132
+ /** If specified, a node must expose all tags before it can claim the task. */
133
+ requiredTags?: string[];
134
+ /**
135
+ * M1: ids of tasks that must reach 'completed' before this task is claimable.
136
+ * Forward references (ids not yet enqueued) are allowed for batch flows and
137
+ * simply keep the task waiting until the referenced task exists and completes.
138
+ */
139
+ dependsOn?: string[];
140
+ /** M1/M3: mission this task belongs to (joins mesh_missions). */
141
+ missionId?: string;
142
+ /**
143
+ * M1: why this task is held back (e.g. "dependency_failed:<taskId>").
144
+ * Only set by the system on dependency failure under the 'block' policy;
145
+ * waiting-on-dependency state is computed at view time, not stored.
146
+ */
147
+ blockedReason?: string;
22
148
  /** The node that actually claimed and is executing the task */
23
149
  assignedNodeId?: string;
24
150
  /** The session currently executing the task */
25
151
  assignedSessionId?: string;
152
+ /**
153
+ * Provider type of the session that claimed the task. Recorded so the queue
154
+ * can enforce per-(node, provider) maxParallel caps (RepoMeshNodePolicy
155
+ * providerRoles) by counting active assignments grouped by node + provider.
156
+ */
157
+ assignedProviderType?: string;
26
158
  /** Human/operator reason for terminal cancellation. */
27
159
  cancelReason?: string;
28
160
  cancelledAt?: string;
@@ -30,6 +162,8 @@ export interface MeshWorkQueueEntry {
30
162
  requeueReason?: string;
31
163
  requeuedAt?: string;
32
164
  requeueCount?: number;
165
+ /** Max automatic requeue attempts. When requeueCount reaches this, task is auto-failed. */
166
+ maxRetries?: number;
33
167
  /** Last automatic queue session spin-up attempt, for mesh_view_queue/debug visibility. */
34
168
  autoLaunch?: {
35
169
  status: 'skipped' | 'started' | 'failed' | 'completed';
@@ -45,50 +179,268 @@ export interface MeshWorkQueueEntry {
45
179
  updatedAt: string;
46
180
  }
47
181
 
48
- function getQueuePath(meshId: string): string {
49
- const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
50
- return join(getLedgerDir(), `${safe}.queue.json`);
182
+ export interface MeshQueueMutationOptions {
183
+ ownerRole?: RepoMeshDaemonRole;
51
184
  }
52
185
 
53
- function getLockPath(meshId: string): string {
54
- const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
55
- return join(getLedgerDir(), `${safe}.queue.lock`);
186
+ export function normalizeMeshCapabilityTags(value: unknown): string[] {
187
+ if (!Array.isArray(value)) return [];
188
+ const seen = new Set<string>();
189
+ return value
190
+ .map(tag => typeof tag === 'string' ? tag.trim() : '')
191
+ .filter(Boolean)
192
+ .filter(tag => {
193
+ if (seen.has(tag)) return false;
194
+ seen.add(tag);
195
+ return true;
196
+ });
197
+ }
198
+
199
+ function firstProviderPriority(policy: unknown): string | undefined {
200
+ const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
201
+ ? (policy as Record<string, unknown>).providerPriority
202
+ : undefined;
203
+ if (!Array.isArray(raw)) return undefined;
204
+ return raw.find(type => typeof type === 'string' && type.trim())?.trim();
56
205
  }
57
206
 
58
207
  /**
59
- * Simple advisory file lock using O_EXCL (atomic create) for queue mutations.
60
- * Retries up to 10 times at 30 ms intervals; proceeds without lock on timeout
61
- * to prevent deadlock (best-effort far better than no locking at all).
208
+ * Synthetic `role=<x>` capability tags advertised by a node's policy.providerRoles.
209
+ *
210
+ * When a specific `providerType` is being evaluated, only that provider's declared
211
+ * role is emitted — so role-based routing (requiredTags: ["role=validation"]) gates
212
+ * the *selected* provider through the ordinary capability-tag filter. When no
213
+ * provider is selected (node-level eligibility scan), every declared role is emitted
214
+ * so the node passes the filter if ANY of its providers could satisfy the role; the
215
+ * per-provider tag set then narrows it during provider selection.
216
+ *
217
+ * Roles are lowercased and deduped. Missing/empty providerRoles emits nothing, so a
218
+ * node that never declares roles advertises no `role=` tags and is therefore only
219
+ * matched by role-unconstrained tasks (full backward compatibility).
62
220
  */
63
- function withQueueLock<T>(meshId: string, fn: () => T): T {
64
- const lockPath = getLockPath(meshId);
65
- let fd = -1;
66
- for (let i = 0; i < 10; i++) {
67
- try { fd = openSync(lockPath, 'wx'); break; } catch {
68
- const deadline = Date.now() + 30;
69
- while (Date.now() < deadline) { /* spin */ }
70
- }
221
+ function roleCapabilityTags(policy: unknown, providerType: string | undefined): string[] {
222
+ const roles = policy && typeof policy === 'object' && !Array.isArray(policy)
223
+ ? (policy as Record<string, unknown>).providerRoles
224
+ : undefined;
225
+ if (!Array.isArray(roles)) return [];
226
+ const wantedProvider = typeof providerType === 'string' && providerType.trim()
227
+ ? providerType.trim().toLowerCase()
228
+ : '';
229
+ const out: string[] = [];
230
+ for (const entry of roles) {
231
+ if (!entry || typeof entry !== 'object') continue;
232
+ const type = typeof (entry as any).providerType === 'string'
233
+ ? (entry as any).providerType.trim().toLowerCase()
234
+ : '';
235
+ const role = typeof (entry as any).role === 'string'
236
+ ? (entry as any).role.trim().toLowerCase()
237
+ : '';
238
+ if (!role) continue;
239
+ // When narrowing to a selected provider, only emit that provider's role.
240
+ if (wantedProvider && type && type !== wantedProvider) continue;
241
+ out.push(`role=${role}`);
71
242
  }
72
- try { return fn(); } finally {
73
- if (fd !== -1) try { closeSync(fd); } catch { /* noop */ }
74
- try { unlinkSync(lockPath); } catch { /* already removed */ }
243
+ return out;
244
+ }
245
+
246
+ export function buildMeshNodeCapabilityTags(
247
+ node: { capabilities?: unknown; policy?: unknown; isLocalWorktree?: unknown; worktreeBranch?: unknown } | undefined,
248
+ providerType?: string,
249
+ ): string[] {
250
+ const provider = typeof providerType === 'string' && providerType.trim()
251
+ ? providerType.trim()
252
+ : firstProviderPriority(node?.policy);
253
+ const worktreeBranch = typeof node?.worktreeBranch === 'string' && node.worktreeBranch.trim()
254
+ ? node.worktreeBranch.trim()
255
+ : null;
256
+ return normalizeMeshCapabilityTags([
257
+ ...(Array.isArray(node?.capabilities) ? node.capabilities : []),
258
+ `os=${process.platform}`,
259
+ `arch=${process.arch}`,
260
+ ...(provider ? [`provider=${provider}`] : []),
261
+ // Worktree nodes automatically expose a "worktree=<branch>" tag so that
262
+ // mesh_enqueue_task with required_tags: ["worktree=<branch>"] routes
263
+ // only to the matching worktree node.
264
+ ...(node?.isLocalWorktree === true && worktreeBranch ? [`worktree=${worktreeBranch}`] : []),
265
+ // Convergence routing: advertise how this node can land its work onto base.
266
+ // - converge=refine: local worktree nodes (on ANY machine — refine_mesh_node
267
+ // now forwards to the owning daemon) can run the Refinery merge → push →
268
+ // cleanup against their own checkout, so they accept code_change tasks.
269
+ // - converge=fast_forward: non-worktree nodes (the machine itself) can only
270
+ // ff/push an already-converged branch; they are NOT a destination for
271
+ // code_change work (a worktree is created first, and that worktree node
272
+ // receives the task instead). Reuses the ordinary required-tags filter —
273
+ // the load-balancing scheduler auto-injects converge=refine for code_change
274
+ // so such work is hard-filtered onto refine-capable nodes.
275
+ ...(node?.isLocalWorktree === true ? ['converge=refine'] : ['converge=fast_forward']),
276
+ // Role-based routing: advertise role=<x> for each (node, provider) role
277
+ // declared in policy.providerRoles. Narrowed to the selected provider when
278
+ // one is given so the chosen provider must match a task's required role;
279
+ // when no provider is selected, all declared roles are advertised for the
280
+ // node-level eligibility scan. Reuses the ordinary required-tags filter —
281
+ // no separate role field/gate.
282
+ ...roleCapabilityTags(node?.policy, providerType),
283
+ ]);
284
+ }
285
+
286
+ export function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags: unknown): boolean {
287
+ const required = normalizeMeshCapabilityTags(requiredTags);
288
+ if (required.length === 0) return true;
289
+ const available = new Set(normalizeMeshCapabilityTags(capabilityTags));
290
+ return required.every(tag => available.has(tag));
291
+ }
292
+
293
+ /**
294
+ * Convergence-aware required-tags resolution (load-balancing scheduler, opt-in).
295
+ *
296
+ * When the mesh enables policy.autoConvergeCodeChange, a `converge=refine` required
297
+ * tag is merged into a code_change task's required tags at enqueue time, so the
298
+ * scheduler hard-filters the task onto refine-capable worktree nodes only (on any
299
+ * machine — refine_mesh_node forwards to the owning daemon). Because the tag is
300
+ * persisted on the queue entry, BOTH the eligibility scan (maybeAutoLaunchOneQueueSession)
301
+ * and the claim transaction (claimNextQueueTask → nodeSatisfiesRequiredTags) enforce
302
+ * it consistently.
303
+ *
304
+ * Strict backward compatibility — the injection is skipped (returns the explicit tags
305
+ * unchanged) when ANY of:
306
+ * - the mesh does not opt in (autoConvergeCodeChange !== true), or
307
+ * - the task is not code_change (validation / live_debug_readonly / launch_app /
308
+ * convergence carry no merge cost and may run anywhere), or
309
+ * - the task is explicitly targeted (targetNodeId): the operator chose the node, so
310
+ * we do not second-guess it by filtering on convergence capability.
311
+ * Idempotent: normalizeMeshCapabilityTags dedupes, so re-injection is a no-op.
312
+ */
313
+ export function resolveConvergeRequiredTags(
314
+ meshId: string,
315
+ taskMode: MeshTaskMode | undefined,
316
+ explicitRequiredTags: string[],
317
+ opts?: { targetNodeId?: string },
318
+ ): string[] {
319
+ if (taskMode !== 'code_change') return explicitRequiredTags;
320
+ if (typeof opts?.targetNodeId === 'string' && opts.targetNodeId.trim()) return explicitRequiredTags;
321
+ let optedIn = false;
322
+ try {
323
+ optedIn = resolveAutoConvergeCodeChange(getMesh(meshId)?.policy as any);
324
+ } catch {
325
+ optedIn = false;
75
326
  }
327
+ if (!optedIn) return explicitRequiredTags;
328
+ return normalizeMeshCapabilityTags([...explicitRequiredTags, MESH_CONVERGE_REFINE_TAG]);
76
329
  }
77
330
 
78
- function readQueue(meshId: string): MeshWorkQueueEntry[] {
79
- const path = getQueuePath(meshId);
80
- if (!existsSync(path)) return [];
331
+ /**
332
+ * Whether any node in the mesh advertises the given `role=<role>` capability tag.
333
+ * Reuses buildMeshNodeCapabilityTags (node-level scan: every declared providerRole is
334
+ * advertised) so the membership test matches the exact tag the claim/eligibility filter
335
+ * would see. Used to keep task-affinity SOFT — if no node can satisfy the role, the
336
+ * injection is skipped so the task falls back to ordinary least_loaded eligibility
337
+ * rather than being blocked.
338
+ */
339
+ function meshHasNodeAdvertisingRole(nodes: LocalMeshNodeEntry[] | undefined, role: string): boolean {
340
+ if (!Array.isArray(nodes) || nodes.length === 0) return false;
341
+ const wanted = `role=${role}`;
342
+ return nodes.some(node => buildMeshNodeCapabilityTags(node as any).includes(wanted));
343
+ }
344
+
345
+ /**
346
+ * Declarative task_mode → role affinity resolution (precedent: resolveConvergeRequiredTags).
347
+ *
348
+ * At enqueue time, auto-inject a `role=<role>` required tag derived from the task's
349
+ * taskMode via the mesh's policy.taskAffinity (falling back to DEFAULT_TASKMODE_ROLE_MAP),
350
+ * so the task hard-filters onto nodes advertising that role through the ordinary
351
+ * required-tags path (nodeSatisfiesRequiredTags) — no claim/scheduler change needed.
352
+ *
353
+ * Strict precedence / backward compatibility — the injection is skipped (returns the
354
+ * explicit tags unchanged) when ANY of:
355
+ * - the caller pinned an explicit targetNodeId (operator chose the node), or
356
+ * - the caller supplied their own non-empty required_tags (caller's routing wins), or
357
+ * - affinity resolves to no role (policy disabled, blank override, or unknown mode), or
358
+ * - SOFT fallback: no node in the mesh advertises the resolved role=<role> — injecting
359
+ * would block the task with zero eligible nodes, so we skip it and let least_loaded
360
+ * eligibility take over (a warning is logged so the misconfiguration is visible).
361
+ * Idempotent: normalizeMeshCapabilityTags dedupes.
362
+ */
363
+ export function resolveTaskAffinityRequiredTags(
364
+ meshId: string,
365
+ taskMode: MeshTaskMode | undefined,
366
+ explicitRequiredTags: string[],
367
+ opts?: { targetNodeId?: string; callerSpecifiedRequiredTags?: boolean },
368
+ ): string[] {
369
+ // Caller pinned the node, or brought their own required_tags → respect it verbatim.
370
+ if (typeof opts?.targetNodeId === 'string' && opts.targetNodeId.trim()) return explicitRequiredTags;
371
+ if (opts?.callerSpecifiedRequiredTags === true) return explicitRequiredTags;
372
+
373
+ let mesh;
81
374
  try {
82
- const content = readFileSync(path, 'utf-8');
83
- return JSON.parse(content) as MeshWorkQueueEntry[];
375
+ mesh = getMesh(meshId);
84
376
  } catch {
85
- return [];
377
+ return explicitRequiredTags;
378
+ }
379
+ const role = resolveTaskAffinityRole(taskMode, mesh?.policy?.taskAffinity);
380
+ if (!role) return explicitRequiredTags;
381
+
382
+ // SOFT affinity: only hard-filter when the role actually exists in the mesh.
383
+ if (!meshHasNodeAdvertisingRole(mesh?.nodes, role)) {
384
+ try {
385
+ console.warn(`[mesh] task_affinity: no node advertises role=${role} for taskMode=${taskMode} in mesh ${meshId}; skipping injection (least_loaded fallback)`);
386
+ } catch { /* logging is best-effort */ }
387
+ return explicitRequiredTags;
86
388
  }
389
+ return normalizeMeshCapabilityTags([...explicitRequiredTags, `role=${role}`]);
390
+ }
391
+
392
+ function withQueueLock<T>(_meshId: string, fn: () => T): T {
393
+ return MeshRuntimeStore.getInstance().transaction(fn);
394
+ }
395
+
396
+ function readQueue(meshId: string): MeshWorkQueueEntry[] {
397
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId);
87
398
  }
88
399
 
89
400
  function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
90
- const path = getQueuePath(meshId);
91
- writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8');
401
+ MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
402
+ }
403
+
404
+ function normalizeDependsOn(value: unknown): string[] {
405
+ if (!Array.isArray(value)) return [];
406
+ const seen = new Set<string>();
407
+ return value
408
+ .map(id => typeof id === 'string' ? id.trim() : '')
409
+ .filter(Boolean)
410
+ .filter(id => {
411
+ if (seen.has(id)) return false;
412
+ seen.add(id);
413
+ return true;
414
+ });
415
+ }
416
+
417
+ /**
418
+ * M1: detect dependency cycles before enqueue. Walks the dependency graph of
419
+ * existing queue entries plus the new task's edges. Fail-closed: a cycle
420
+ * rejects the enqueue entirely. Synchronous and bounded by queue size.
421
+ */
422
+ export function assertNoDependencyCycle(meshId: string, newTaskId: string, dependsOn: string[]): void {
423
+ if (dependsOn.length === 0) return;
424
+ if (dependsOn.includes(newTaskId)) {
425
+ throw new Error(`dependency_cycle_detected: task '${newTaskId}' cannot depend on itself`);
426
+ }
427
+ const adjacency = new Map<string, string[]>();
428
+ for (const entry of readQueue(meshId)) {
429
+ adjacency.set(entry.id, normalizeDependsOn(entry.dependsOn));
430
+ }
431
+ adjacency.set(newTaskId, dependsOn);
432
+ // DFS from the new task: if we can reach newTaskId again, the edges form a cycle.
433
+ const stack = [...dependsOn];
434
+ const visited = new Set<string>();
435
+ while (stack.length > 0) {
436
+ const current = stack.pop()!;
437
+ if (current === newTaskId) {
438
+ throw new Error(`dependency_cycle_detected: task '${newTaskId}' is part of a dependency cycle via '${dependsOn.join(', ')}'`);
439
+ }
440
+ if (visited.has(current)) continue;
441
+ visited.add(current);
442
+ stack.push(...(adjacency.get(current) ?? []));
443
+ }
92
444
  }
93
445
 
94
446
  /**
@@ -97,22 +449,133 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
97
449
  export function enqueueTask(
98
450
  meshId: string,
99
451
  message: string,
100
- opts?: { targetNodeId?: string; targetSessionId?: string }
452
+ opts?: {
453
+ targetNodeId?: string;
454
+ targetSessionId?: string;
455
+ taskMode?: MeshTaskMode | string;
456
+ requiredTags?: string[];
457
+ /** M1: tasks that must complete before this one is claimable. */
458
+ dependsOn?: string[];
459
+ /** M1/M3: mission this task belongs to. */
460
+ missionId?: string;
461
+ /** Explicit task id for batch/template flows (M5). Random UUID when omitted. */
462
+ id?: string;
463
+ } & MeshQueueMutationOptions,
101
464
  ): MeshWorkQueueEntry {
465
+ requireMeshHostQueueOwner(opts);
466
+ const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
467
+ if (!modeValidation.valid) {
468
+ throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
469
+ }
470
+ const id = typeof opts?.id === 'string' && opts.id.trim() ? opts.id.trim() : randomUUID();
471
+ const dependsOn = normalizeDependsOn(opts?.dependsOn);
102
472
  return withQueueLock(meshId, () => {
103
- const queue = readQueue(meshId);
473
+ if (MeshRuntimeStore.getInstance().findQueueEntryById(meshId, id)) {
474
+ throw new Error(`duplicate_task_id: task '${id}' already exists in mesh '${meshId}'`);
475
+ }
476
+ assertNoDependencyCycle(meshId, id, dependsOn);
477
+ // Did the caller bring their own routing tags? Affinity auto-injection is
478
+ // suppressed when so (caller's required_tags win), matching the spec — measured
479
+ // against the ORIGINAL caller tags, before convergence augmentation below.
480
+ const callerTags = normalizeMeshCapabilityTags(opts?.requiredTags);
481
+ const callerSpecifiedRequiredTags = callerTags.length > 0;
482
+ // Convergence routing (opt-in): auto-inject converge=refine for code_change
483
+ // tasks so they hard-filter onto refine-capable worktree nodes. No-op unless
484
+ // the mesh opts in; explicit target_node_id / required_tags are preserved.
485
+ let resolvedRequiredTags = resolveConvergeRequiredTags(
486
+ meshId,
487
+ modeValidation.taskMode,
488
+ callerTags,
489
+ { targetNodeId: opts?.targetNodeId },
490
+ );
491
+ // Declarative task_mode → role affinity (precedent: convergence routing above):
492
+ // inject role=<role> for the task's taskMode unless the caller pinned a node or
493
+ // supplied their own required_tags. SOFT — skipped when no node advertises the
494
+ // role (least_loaded fallback). Built-in default mapping applies even without a
495
+ // taskAffinity config block; { enabled:false } disables it.
496
+ resolvedRequiredTags = resolveTaskAffinityRequiredTags(
497
+ meshId,
498
+ modeValidation.taskMode,
499
+ resolvedRequiredTags,
500
+ { targetNodeId: opts?.targetNodeId, callerSpecifiedRequiredTags },
501
+ );
104
502
  const entry: MeshWorkQueueEntry = {
105
- id: randomUUID(),
503
+ id,
106
504
  meshId,
107
505
  message,
108
506
  status: 'pending',
507
+ taskMode: modeValidation.taskMode,
109
508
  targetNodeId: opts?.targetNodeId,
110
509
  targetSessionId: opts?.targetSessionId,
510
+ requiredTags: resolvedRequiredTags,
511
+ ...(dependsOn.length > 0 ? { dependsOn } : {}),
512
+ ...(typeof opts?.missionId === 'string' && opts.missionId.trim() ? { missionId: opts.missionId.trim() } : {}),
111
513
  createdAt: new Date().toISOString(),
112
514
  updatedAt: new Date().toISOString(),
113
515
  };
114
- queue.push(entry);
115
- writeQueue(meshId, queue);
516
+ MeshRuntimeStore.getInstance().insertQueueEntry(entry);
517
+ return entry;
518
+ });
519
+ }
520
+
521
+ /**
522
+ * Record a direct-dispatch task (mesh_send_task) as an already-assigned queue
523
+ * entry so it is attributable to a mission.
524
+ *
525
+ * Direct dispatch normally bypasses the queue entirely — the task lives only in
526
+ * the ledger + mesh_direct_dispatches table, neither of which carries a
527
+ * missionId, so {@link summarizeMissionTasks}/{@link computeMeshTaskStats}
528
+ * (which both scan the queue for `task.missionId`) count it as 0. When a
529
+ * mission is attached, we materialise the same queue entry shape an enqueued
530
+ * task would have, but pre-assigned to the dispatched node/session and stamped
531
+ * with the dispatch timestamp. The terminal event path (updateSessionTaskStatus
532
+ * → findAssignedBySession) then flips it to completed/failed exactly like a
533
+ * pulled task, so mission total + completed aggregates work with no extra wiring.
534
+ *
535
+ * Intentionally separate from {@link enqueueTask}: enqueue creates `pending`
536
+ * work for the queue to assign, whereas this records work already dispatched
537
+ * out-of-band. They share the missionId stamping rule and mode validation.
538
+ */
539
+ export function recordDirectDispatchTask(
540
+ meshId: string,
541
+ message: string,
542
+ opts: {
543
+ id: string;
544
+ missionId: string;
545
+ assignedNodeId?: string;
546
+ assignedSessionId?: string;
547
+ taskMode?: MeshTaskMode | string;
548
+ dispatchedAt?: string;
549
+ },
550
+ ): MeshWorkQueueEntry | null {
551
+ const missionId = typeof opts.missionId === 'string' ? opts.missionId.trim() : '';
552
+ if (!missionId) return null;
553
+ const taskId = typeof opts.id === 'string' ? opts.id.trim() : '';
554
+ if (!taskId) return null;
555
+ const modeValidation = validateMeshTaskModeRequest(opts.taskMode, message);
556
+ if (!modeValidation.valid) {
557
+ throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
558
+ }
559
+ const now = opts.dispatchedAt && opts.dispatchedAt.trim() ? opts.dispatchedAt : new Date().toISOString();
560
+ return withQueueLock(meshId, () => {
561
+ if (MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId)) {
562
+ // Already materialised (e.g. retry of the same dispatch) — leave it untouched.
563
+ return null;
564
+ }
565
+ const entry: MeshWorkQueueEntry = {
566
+ id: taskId,
567
+ meshId,
568
+ message,
569
+ status: 'assigned',
570
+ ...(modeValidation.taskMode ? { taskMode: modeValidation.taskMode } : {}),
571
+ missionId,
572
+ ...(opts.assignedNodeId ? { targetNodeId: opts.assignedNodeId, assignedNodeId: opts.assignedNodeId } : {}),
573
+ ...(opts.assignedSessionId ? { targetSessionId: opts.assignedSessionId, assignedSessionId: opts.assignedSessionId } : {}),
574
+ dispatchTimestamp: now,
575
+ createdAt: now,
576
+ updatedAt: now,
577
+ };
578
+ MeshRuntimeStore.getInstance().insertQueueEntry(entry);
116
579
  return entry;
117
580
  });
118
581
  }
@@ -121,43 +584,83 @@ export function enqueueTask(
121
584
  * Get all tasks in the queue, optionally filtered by status.
122
585
  */
123
586
  export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): MeshWorkQueueEntry[] {
124
- let queue = readQueue(meshId);
125
- if (opts?.status?.length) {
126
- const statuses = new Set(opts.status);
127
- queue = queue.filter(q => statuses.has(q.status));
128
- }
129
- return queue;
587
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId, opts?.status?.length ? opts.status : undefined);
588
+ }
589
+
590
+ export function getMeshQueueRevision(meshId: string): string {
591
+ return MeshRuntimeStore.getInstance().getQueueRevision(meshId);
130
592
  }
131
593
 
132
594
  /**
133
595
  * Find the next pending task that this node is allowed to claim, and mark it as assigned.
596
+ *
597
+ * `opts.providerType` is stamped onto the claimed entry (assignedProviderType) so
598
+ * per-(node, provider) caps can be counted. `opts.providerMaxParallel`, when set,
599
+ * is the enforced per-(node, provider) cap from RepoMeshNodePolicy.providerRoles:
600
+ * a task is not assigned to this (node, provider) once it already has that many
601
+ * active assignments. This composes with the global/taskMode caps (stricter wins).
134
602
  */
135
- export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
136
- return withQueueLock(meshId, () => {
137
- const queue = readQueue(meshId);
138
- const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
139
- q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
140
- ));
141
- if (hasActiveAssignment) return null;
142
- let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
143
- if (targetIdx === -1) {
144
- targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
145
- }
146
- if (targetIdx === -1) {
147
- targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
603
+ export function claimNextTask(
604
+ meshId: string,
605
+ nodeId: string,
606
+ sessionId: string,
607
+ capabilityTags?: string[],
608
+ opts?: { providerType?: string; providerMaxParallel?: number },
609
+ ): MeshWorkQueueEntry | null {
610
+ return MeshRuntimeStore.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags, opts);
611
+ }
612
+
613
+ // ─── M1: Dependency Failure Propagation ─────────
614
+
615
+ export type DependencyFailurePolicy = 'block' | 'cancel';
616
+
617
+ function resolveDependencyFailurePolicy(meshId: string): DependencyFailurePolicy {
618
+ try {
619
+ const policy = (getMesh(meshId)?.policy ?? {}) as Record<string, unknown>;
620
+ return policy.onDependencyFailure === 'cancel' ? 'cancel' : 'block';
621
+ } catch {
622
+ return 'block';
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Apply the mesh's onDependencyFailure policy to pending dependents of a task
628
+ * that just reached a failed/cancelled terminal state.
629
+ *
630
+ * - 'block' (default): dependents stay pending with blockedReason
631
+ * "dependency_failed:<taskId>" so an operator can requeue/cancel them.
632
+ * - 'cancel': dependents are cancelled (cascading to their own dependents).
633
+ *
634
+ * Must be called inside the queue lock of the triggering transition.
635
+ */
636
+ function propagateDependencyFailure(meshId: string, failedTaskId: string): void {
637
+ const policy = resolveDependencyFailurePolicy(meshId);
638
+ const store = MeshRuntimeStore.getInstance();
639
+ const frontier = [failedTaskId];
640
+ const seen = new Set<string>(frontier);
641
+ while (frontier.length > 0) {
642
+ const currentId = frontier.pop()!;
643
+ const dependents = store.getQueueEntries(meshId, ['pending'])
644
+ .filter(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(currentId));
645
+ for (const dependent of dependents) {
646
+ if (seen.has(dependent.id)) continue;
647
+ seen.add(dependent.id);
648
+ if (policy === 'cancel') {
649
+ dependent.status = 'cancelled';
650
+ dependent.cancelledAt = new Date().toISOString();
651
+ dependent.cancelReason = `dependency_failed:${currentId}`;
652
+ store.updateQueueEntry(dependent);
653
+ frontier.push(dependent.id); // cascade to transitive dependents
654
+ } else {
655
+ dependent.blockedReason = `dependency_failed:${currentId}`;
656
+ store.updateQueueEntry(dependent);
657
+ }
148
658
  }
149
- if (targetIdx === -1) return null;
150
- const entry = queue[targetIdx];
151
- entry.status = 'assigned';
152
- entry.assignedNodeId = nodeId;
153
- entry.assignedSessionId = sessionId;
154
- entry.dispatchTimestamp = new Date().toISOString();
155
- entry.updatedAt = new Date().toISOString();
156
- writeQueue(meshId, queue);
157
- return entry;
158
- });
659
+ }
159
660
  }
160
661
 
662
+ const DEPENDENCY_FAILURE_TERMINALS = new Set<MeshTaskStatus>(['failed', 'cancelled']);
663
+
161
664
  /**
162
665
  * Update the status of a specific task.
163
666
  * Used when a session completes, fails, or stalls.
@@ -166,15 +669,16 @@ export function updateTaskStatus(
166
669
  meshId: string,
167
670
  taskId: string,
168
671
  status: MeshTaskStatus,
672
+ opts?: MeshQueueMutationOptions,
169
673
  ): MeshWorkQueueEntry | null {
674
+ requireMeshHostQueueOwner(opts);
170
675
  return withQueueLock(meshId, () => {
171
- const queue = readQueue(meshId);
172
- const idx = queue.findIndex(q => q.id === taskId);
173
- if (idx === -1) return null;
174
- queue[idx].status = status;
175
- queue[idx].updatedAt = new Date().toISOString();
176
- writeQueue(meshId, queue);
177
- return queue[idx];
676
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
677
+ if (!entry) return null;
678
+ entry.status = status;
679
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
680
+ if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, taskId);
681
+ return entry;
178
682
  });
179
683
  }
180
684
 
@@ -184,14 +688,12 @@ export function recordTaskAutoLaunch(
184
688
  autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
185
689
  ): MeshWorkQueueEntry | null {
186
690
  return withQueueLock(meshId, () => {
187
- const queue = readQueue(meshId);
188
- const idx = queue.findIndex(q => q.id === taskId);
189
- if (idx === -1) return null;
691
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
692
+ if (!entry) return null;
190
693
  const now = new Date().toISOString();
191
- queue[idx].autoLaunch = { ...autoLaunch, updatedAt: now };
192
- queue[idx].updatedAt = now;
193
- writeQueue(meshId, queue);
194
- return queue[idx];
694
+ entry.autoLaunch = { ...autoLaunch, updatedAt: now };
695
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
696
+ return entry;
195
697
  });
196
698
  }
197
699
 
@@ -201,19 +703,19 @@ export function recordTaskAutoLaunch(
201
703
  export function cancelTask(
202
704
  meshId: string,
203
705
  taskId: string,
204
- opts?: { reason?: string },
706
+ opts?: { reason?: string } & MeshQueueMutationOptions,
205
707
  ): MeshWorkQueueEntry | null {
708
+ requireMeshHostQueueOwner(opts);
206
709
  return withQueueLock(meshId, () => {
207
- const queue = readQueue(meshId);
208
- const idx = queue.findIndex(q => q.id === taskId);
209
- if (idx === -1) return null;
710
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
711
+ if (!entry) return null;
210
712
  const now = new Date().toISOString();
211
- queue[idx].status = 'cancelled';
212
- queue[idx].updatedAt = now;
213
- queue[idx].cancelledAt = now;
214
- if (opts?.reason) queue[idx].cancelReason = opts.reason;
215
- writeQueue(meshId, queue);
216
- return queue[idx];
713
+ entry.status = 'cancelled';
714
+ entry.cancelledAt = now;
715
+ if (opts?.reason) entry.cancelReason = opts.reason;
716
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
717
+ propagateDependencyFailure(meshId, taskId);
718
+ return entry;
217
719
  });
218
720
  }
219
721
 
@@ -221,6 +723,11 @@ export function cancelTask(
221
723
  * Return a queue task to pending for retry. By default, dead session targeting
222
724
  * and assigned ownership are cleared so stale assignments do not strand again.
223
725
  */
726
+ export type RequeueResult =
727
+ | { status: 'requeued'; entry: MeshWorkQueueEntry }
728
+ | { status: 'failed_max_retries'; entry: MeshWorkQueueEntry; maxRetries: number; requeueCount: number }
729
+ | { status: 'not_found' };
730
+
224
731
  export function requeueTask(
225
732
  meshId: string,
226
733
  taskId: string,
@@ -230,15 +737,34 @@ export function requeueTask(
230
737
  targetSessionId?: string;
231
738
  clearTargetNode?: boolean;
232
739
  clearTargetSession?: boolean;
233
- },
740
+ /**
741
+ * Override the retry cap for this call. Use only for explicit operator actions.
742
+ * If true, the task is requeued even when requeueCount >= maxRetries.
743
+ */
744
+ force?: boolean;
745
+ /** Per-task retry cap override. Falls back to mesh policy maxTaskRetries (default 1). */
746
+ maxRetries?: number;
747
+ } & MeshQueueMutationOptions,
234
748
  ): MeshWorkQueueEntry | null {
749
+ requireMeshHostQueueOwner(opts);
235
750
  return withQueueLock(meshId, () => {
236
- const queue = readQueue(meshId);
237
- const idx = queue.findIndex(q => q.id === taskId);
238
- if (idx === -1) return null;
239
- const entry = queue[idx];
240
- const now = new Date().toISOString();
751
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
752
+ if (!entry) return null;
753
+ const currentCount = entry.requeueCount || 0;
754
+ const maxRetries = opts?.maxRetries ?? entry.maxRetries ?? 1;
755
+ if (!opts?.force && currentCount >= maxRetries) {
756
+ // Auto-fail: cap exceeded without explicit force override.
757
+ entry.status = 'failed';
758
+ entry.cancelReason = `max_retries_exceeded: requeued ${currentCount} time(s), limit is ${maxRetries}`;
759
+ entry.updatedAt = new Date().toISOString();
760
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
761
+ propagateDependencyFailure(meshId, taskId);
762
+ return entry;
763
+ }
241
764
  entry.status = 'pending';
765
+ // Operator requeue clears a dependency-failure block — the operator is
766
+ // explicitly overriding the held-back state.
767
+ delete entry.blockedReason;
242
768
  delete entry.assignedNodeId;
243
769
  delete entry.assignedSessionId;
244
770
  delete entry.cancelledAt;
@@ -247,11 +773,10 @@ export function requeueTask(
247
773
  if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
248
774
  if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
249
775
  if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
250
- entry.updatedAt = now;
251
- entry.requeuedAt = now;
252
- entry.requeueCount = (entry.requeueCount || 0) + 1;
776
+ entry.requeuedAt = new Date().toISOString();
777
+ entry.requeueCount = currentCount + 1;
253
778
  if (opts?.reason) entry.requeueReason = opts.reason;
254
- writeQueue(meshId, queue);
779
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
255
780
  return entry;
256
781
  });
257
782
  }
@@ -266,25 +791,42 @@ export function updateSessionTaskStatus(
266
791
  opts?: { occurredAt?: string },
267
792
  ): MeshWorkQueueEntry | null {
268
793
  return withQueueLock(meshId, () => {
269
- const queue = readQueue(meshId);
270
- const occurredAtTime = opts?.occurredAt ? new Date(opts.occurredAt).getTime() : Number.NaN;
271
- const hasOccurredAt = Number.isFinite(occurredAtTime);
272
- let bestIdx = -1;
273
- let bestTime = 0;
274
- for (let i = queue.length - 1; i >= 0; i--) {
275
- if (queue[i].assignedSessionId !== sessionId || queue[i].status !== 'assigned') continue;
276
- const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
277
- if (hasOccurredAt && Number.isFinite(time) && time > occurredAtTime) continue;
278
- if (time > bestTime) { bestTime = time; bestIdx = i; }
279
- }
280
- if (bestIdx === -1) return null;
281
- queue[bestIdx].status = status;
282
- queue[bestIdx].updatedAt = new Date().toISOString();
283
- writeQueue(meshId, queue);
284
- return queue[bestIdx];
794
+ const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
795
+ const entry = MeshRuntimeStore.getInstance().findAssignedBySession(meshId, sessionId, occurredAtIso);
796
+ if (!entry) return null;
797
+ entry.status = status;
798
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
799
+ if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, entry.id);
800
+ return entry;
285
801
  });
286
802
  }
287
803
 
804
+ /**
805
+ * M1-3: true when at least one pending task is waiting on the given task.
806
+ * Used by the completion event path to decide whether to wake the queue.
807
+ */
808
+ export function hasPendingDependents(meshId: string, taskId: string): boolean {
809
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId, ['pending'])
810
+ .some(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(taskId));
811
+ }
812
+
813
+ /**
814
+ * M1-4: view-time dependency state for a task — unmet dependency ids and
815
+ * whether the task is currently claimable from a dependency standpoint.
816
+ * Not stored (truth stays in task statuses).
817
+ */
818
+ export function describeTaskDependencyState(
819
+ entry: Pick<MeshWorkQueueEntry, 'dependsOn' | 'blockedReason'>,
820
+ statusById: Map<string, MeshTaskStatus | string>,
821
+ ): { waitingOn: string[]; dependenciesSatisfied: boolean } {
822
+ const deps = Array.isArray(entry.dependsOn) ? entry.dependsOn : [];
823
+ const waitingOn = deps.filter(depId => statusById.get(depId) !== 'completed');
824
+ return {
825
+ waitingOn,
826
+ dependenciesSatisfied: waitingOn.length === 0 && !entry.blockedReason,
827
+ };
828
+ }
829
+
288
830
  export interface MeshWorkQueueStats {
289
831
  total: number;
290
832
  active: number;
@@ -310,14 +852,16 @@ export interface MeshWorkQueueStats {
310
852
  * Return aggregate queue statistics for the given mesh.
311
853
  */
312
854
  export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
313
- const queue = readQueue(meshId);
314
- const pending = queue.filter(q => q.status === 'pending').length;
315
- const assigned = queue.filter(q => q.status === 'assigned').length;
316
- const completed = queue.filter(q => q.status === 'completed').length;
317
- const failed = queue.filter(q => q.status === 'failed').length;
318
- const cancelled = queue.filter(q => q.status === 'cancelled').length;
855
+ const rows = MeshRuntimeStore.getInstance().getQueueStatsByStatus(meshId);
856
+ const counts: Record<string, number> = {};
857
+ for (const r of rows) counts[r.status] = r.count;
858
+ const pending = counts['pending'] ?? 0;
859
+ const assigned = counts['assigned'] ?? 0;
860
+ const completed = counts['completed'] ?? 0;
861
+ const failed = counts['failed'] ?? 0;
862
+ const cancelled = counts['cancelled'] ?? 0;
319
863
  return {
320
- total: queue.length,
864
+ total: pending + assigned + completed + failed + cancelled,
321
865
  active: pending + assigned,
322
866
  historical: completed + failed + cancelled,
323
867
  pending,
@@ -325,22 +869,119 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
325
869
  completed,
326
870
  failed,
327
871
  cancelled,
328
- activeCounts: {
329
- pending,
330
- assigned,
331
- },
332
- historicalCounts: {
333
- completed,
334
- failed,
335
- cancelled,
336
- },
337
- activeAssignments: queue
338
- .filter(q => q.status === 'assigned')
339
- .map(q => ({
340
- id: q.id,
341
- nodeId: q.assignedNodeId,
342
- sessionId: q.assignedSessionId,
343
- message: q.message,
344
- })),
872
+ activeCounts: { pending, assigned },
873
+ historicalCounts: { completed, failed, cancelled },
874
+ activeAssignments: MeshRuntimeStore.getInstance().getActiveAssignmentDetails(meshId),
345
875
  };
346
876
  }
877
+
878
+ export function __replaceMeshQueueForTests(meshId: string, queue: MeshWorkQueueEntry[]): void {
879
+ MeshRuntimeStore.getInstance().transaction(() => {
880
+ MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
881
+ });
882
+ }
883
+
884
+ export function __clearMeshQueueForTests(meshId: string): void {
885
+ MeshRuntimeStore.getInstance().deleteQueue(meshId);
886
+ }
887
+
888
+ export function __clearDirectDispatchesForTests(meshId: string): void {
889
+ MeshRuntimeStore.getInstance().deleteDirectDispatches(meshId);
890
+ }
891
+
892
+ export function __resetMeshRuntimeStoreForTests(): void {
893
+ MeshRuntimeStore.resetForTests();
894
+ }
895
+
896
+ // ── Direct Dispatch Tracking ─────────────────────────────────────────────────
897
+ // Persists direct (non-queue) task dispatches so buildMeshActiveWork can read
898
+ // active work from MeshRuntimeStore instead of scanning ledger JSONL entries.
899
+
900
+ export type DirectDispatchRecord = ReturnType<MeshRuntimeStore['getActiveDirectDispatches']>[number];
901
+
902
+ export function insertDirectDispatch(
903
+ meshId: string,
904
+ data: {
905
+ taskId: string;
906
+ nodeId?: string;
907
+ sessionId?: string;
908
+ providerType?: string;
909
+ message: string;
910
+ taskMode?: string;
911
+ via: string;
912
+ dispatchedToIdleSession?: boolean;
913
+ dispatchedAt: string;
914
+ },
915
+ ): void {
916
+ try {
917
+ MeshRuntimeStore.getInstance().insertDirectDispatch({ ...data, meshId });
918
+ } catch (e: any) {
919
+ process.stderr.write(`[adhdev-mesh] insertDirectDispatch failed for task ${data.taskId}: ${e?.message || e}\n`);
920
+ }
921
+ }
922
+
923
+ export function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[] {
924
+ try {
925
+ return MeshRuntimeStore.getInstance().getActiveDirectDispatches(meshId);
926
+ } catch {
927
+ return [];
928
+ }
929
+ }
930
+
931
+ export function updateDirectDispatchStatus(
932
+ meshId: string,
933
+ sessionId: string,
934
+ status: 'acked' | 'completed' | 'failed' | 'stale',
935
+ ): void {
936
+ try {
937
+ MeshRuntimeStore.getInstance().updateDirectDispatchStatus(meshId, sessionId, status);
938
+ } catch { /* best-effort */ }
939
+ }
940
+
941
+ export function cleanupTerminalDirectDispatches(olderThanMs = 7 * 24 * 60 * 60_000): void {
942
+ try {
943
+ MeshRuntimeStore.getInstance().cleanupTerminalDirectDispatches(olderThanMs);
944
+ } catch { /* best-effort */ }
945
+ }
946
+
947
+ export function markStaleDirectDispatches(meshId: string, olderThanMs = 60 * 60_000): void {
948
+ try {
949
+ MeshRuntimeStore.getInstance().markStaleDirectDispatches(meshId, olderThanMs);
950
+ } catch { /* best-effort */ }
951
+ }
952
+
953
+ /**
954
+ * Delete specific direct dispatch rows by taskId. Returns the number of rows deleted.
955
+ * Used by the staleDirect prune path to evict orphaned/terminal dispatch records from the
956
+ * active staleDirect surface while leaving the append-only mesh ledger (audit history) intact.
957
+ */
958
+ export function deleteDirectDispatchesByTaskId(meshId: string, taskIds: string[]): number {
959
+ try {
960
+ return MeshRuntimeStore.getInstance().deleteDirectDispatchesByTaskId(meshId, taskIds);
961
+ } catch {
962
+ return 0;
963
+ }
964
+ }
965
+
966
+ export type MeshToolCallRateResult = { rateLimitExceeded: boolean; callsInWindow: number; advisory: string | null };
967
+
968
+ /**
969
+ * Record a coordinator tool call and return a rate-limit advisory when the
970
+ * call rate for that tool exceeds the allowed threshold.
971
+ *
972
+ * Defaults: 10-second sliding window, max 5 calls before advisory is raised.
973
+ * Returns { rateLimitExceeded: false } on any store error so callers are not blocked.
974
+ */
975
+ export function recordMeshToolCall(opts: {
976
+ meshId: string;
977
+ tool: string;
978
+ sessionId?: string | null;
979
+ windowMs?: number;
980
+ maxCalls?: number;
981
+ }): MeshToolCallRateResult {
982
+ try {
983
+ return MeshRuntimeStore.getInstance().recordMeshToolCall(opts);
984
+ } catch {
985
+ return { rateLimitExceeded: false, callsInWindow: 0, advisory: null };
986
+ }
987
+ }