@adhdev/daemon-core 0.9.82-rc.3 → 0.9.82-rc.300

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 (288) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +10 -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 +215 -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 +10 -1
  25. package/dist/git/git-diff.d.ts +6 -0
  26. package/dist/git/git-status.d.ts +12 -0
  27. package/dist/git/git-types.d.ts +5 -43
  28. package/dist/index.d.ts +51 -12
  29. package/dist/index.js +38833 -15876
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +38504 -15652
  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 +138 -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 -37
  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-init.d.ts +86 -0
  50. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  51. package/dist/mesh/mesh-ledger.d.ts +77 -1
  52. package/dist/mesh/mesh-missions.d.ts +107 -0
  53. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  54. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  55. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  56. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  57. package/dist/mesh/mesh-routing.d.ts +70 -0
  58. package/dist/mesh/mesh-runtime-store.d.ts +377 -0
  59. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  60. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  61. package/dist/mesh/mesh-work-queue.d.ts +184 -6
  62. package/dist/mesh/preview-freshness.d.ts +18 -0
  63. package/dist/mesh/refine-config.d.ts +215 -0
  64. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  65. package/dist/providers/acp-provider-instance.d.ts +2 -0
  66. package/dist/providers/approval-utils.d.ts +13 -0
  67. package/dist/providers/chat-message-normalization.d.ts +1 -0
  68. package/dist/providers/cli-provider-instance.d.ts +61 -3
  69. package/dist/providers/contracts.d.ts +130 -6
  70. package/dist/providers/external-sources.d.ts +71 -0
  71. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  72. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  73. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  74. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  75. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  76. package/dist/providers/native-history/index.d.ts +13 -0
  77. package/dist/providers/provider-instance-manager.d.ts +13 -0
  78. package/dist/providers/provider-instance.d.ts +13 -1
  79. package/dist/providers/provider-loader.d.ts +26 -4
  80. package/dist/providers/provider-trust.d.ts +31 -0
  81. package/dist/providers/read-chat-contract.d.ts +29 -0
  82. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  83. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  84. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  85. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  86. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  87. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  88. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  89. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  90. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  91. package/dist/providers/sdk/v1/index.d.ts +30 -0
  92. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  93. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  94. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  95. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  96. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  97. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  98. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  99. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  100. package/dist/providers/spec/adapter.d.ts +59 -0
  101. package/dist/providers/spec/cli-adapter.d.ts +186 -0
  102. package/dist/providers/spec/evaluator.d.ts +23 -0
  103. package/dist/providers/spec/fsm-driver.d.ts +278 -0
  104. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  105. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  106. package/dist/providers/spec/fsm-types.d.ts +128 -0
  107. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  108. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  109. package/dist/providers/spec/route.d.ts +4 -0
  110. package/dist/providers/spec/types.d.ts +144 -0
  111. package/dist/providers/transcript-v2.d.ts +176 -0
  112. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  113. package/dist/repo-mesh-types.d.ts +314 -1
  114. package/dist/sessions/registry.d.ts +6 -0
  115. package/dist/shared-types-extra.d.ts +2 -4
  116. package/dist/shared-types.d.ts +42 -1
  117. package/dist/status/normalize.d.ts +1 -1
  118. package/dist/status/normalize.js +1 -0
  119. package/dist/status/normalize.js.map +1 -1
  120. package/dist/status/normalize.mjs +1 -0
  121. package/dist/status/normalize.mjs.map +1 -1
  122. package/dist/status/reporter.d.ts +2 -0
  123. package/dist/status/snapshot.d.ts +1 -0
  124. package/dist/types.d.ts +5 -0
  125. package/package.json +7 -3
  126. package/src/agent-stream/poller.ts +2 -3
  127. package/src/boot/daemon-lifecycle.ts +58 -12
  128. package/src/boot/process-hardening.ts +89 -0
  129. package/src/build-info.ts +73 -0
  130. package/src/chat/source-machine.ts +534 -0
  131. package/src/chat/source-resolver.ts +0 -0
  132. package/src/chat/subscription-updates.ts +20 -1
  133. package/src/cli-adapter-types.d.ts +1 -0
  134. package/src/cli-adapter-types.ts +22 -2
  135. package/src/cli-adapters/cli-script-runner.ts +421 -0
  136. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  137. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  138. package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
  139. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  140. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  141. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  142. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  143. package/src/cli-adapters/provider-cli-shared.ts +68 -11
  144. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  145. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  146. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  147. package/src/cli-adapters/terminal-screen.ts +16 -81
  148. package/src/commands/chat-commands.ts +1813 -60
  149. package/src/commands/cli-manager.ts +286 -14
  150. package/src/commands/handler.ts +809 -2
  151. package/src/commands/mesh-coordinator.ts +332 -122
  152. package/src/commands/router.ts +6753 -867
  153. package/src/config/chat-history.ts +474 -24
  154. package/src/config/config.ts +12 -0
  155. package/src/config/mesh-config.ts +303 -3
  156. package/src/config/recent-activity.ts +8 -2
  157. package/src/daemon/dev-auto-implement.ts +3 -2
  158. package/src/daemon/dev-cli-debug.ts +10 -1
  159. package/src/detection/ide-detector.ts +26 -16
  160. package/src/git/git-commands.ts +43 -13
  161. package/src/git/git-diff.ts +53 -0
  162. package/src/git/git-status.ts +329 -12
  163. package/src/git/git-summary.ts +3 -0
  164. package/src/git/git-types.ts +17 -54
  165. package/src/git/git-worktree.ts +8 -1
  166. package/src/index.ts +139 -11
  167. package/src/installer.d.ts +1 -1
  168. package/src/installer.ts +8 -6
  169. package/src/ipc/local-ipc-server.ts +278 -0
  170. package/src/launch.d.ts +1 -1
  171. package/src/launch.ts +37 -28
  172. package/src/logging/async-batch-writer.ts +55 -0
  173. package/src/logging/logger.ts +2 -1
  174. package/src/mesh/contracts.ts +329 -0
  175. package/src/mesh/coordinator-prompt.ts +234 -31
  176. package/src/mesh/coordinator-registry.ts +121 -0
  177. package/src/mesh/mesh-active-work.ts +469 -0
  178. package/src/mesh/mesh-delivery-policy.ts +315 -0
  179. package/src/mesh/mesh-events-coordinator.ts +1786 -0
  180. package/src/mesh/mesh-events-pending.ts +508 -0
  181. package/src/mesh/mesh-events-stale.ts +290 -0
  182. package/src/mesh/mesh-events-utils.ts +254 -0
  183. package/src/mesh/mesh-events.ts +30 -897
  184. package/src/mesh/mesh-fast-forward.ts +839 -0
  185. package/src/mesh/mesh-host-ownership.ts +73 -0
  186. package/src/mesh/mesh-init.ts +260 -0
  187. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  188. package/src/mesh/mesh-ledger.ts +589 -102
  189. package/src/mesh/mesh-missions.ts +232 -0
  190. package/src/mesh/mesh-reconcile-loop.ts +466 -0
  191. package/src/mesh/mesh-refine-batch.ts +197 -0
  192. package/src/mesh/mesh-refine-status.ts +231 -0
  193. package/src/mesh/mesh-review-inbox.ts +307 -0
  194. package/src/mesh/mesh-routing.ts +291 -0
  195. package/src/mesh/mesh-runtime-store.ts +1513 -0
  196. package/src/mesh/mesh-task-stats.ts +154 -0
  197. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  198. package/src/mesh/mesh-work-queue.ts +641 -166
  199. package/src/mesh/preview-freshness.ts +118 -0
  200. package/src/mesh/refine-config.ts +403 -0
  201. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  202. package/src/providers/acp-provider-instance.ts +15 -1
  203. package/src/providers/approval-utils.d.ts +4 -0
  204. package/src/providers/approval-utils.ts +47 -5
  205. package/src/providers/chat-message-normalization.ts +7 -12
  206. package/src/providers/cli-provider-instance.ts +953 -87
  207. package/src/providers/contracts.d.ts +55 -0
  208. package/src/providers/contracts.ts +141 -6
  209. package/src/providers/extension-provider-instance.ts +5 -1
  210. package/src/providers/external-sources.ts +218 -0
  211. package/src/providers/ide-provider-instance.ts +25 -6
  212. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  213. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  214. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  215. package/src/providers/native-history/dispatcher.ts +340 -0
  216. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  217. package/src/providers/native-history/index.ts +30 -0
  218. package/src/providers/provider-instance-manager.ts +30 -0
  219. package/src/providers/provider-instance.ts +10 -1
  220. package/src/providers/provider-loader.ts +584 -50
  221. package/src/providers/provider-schema.ts +87 -14
  222. package/src/providers/provider-trust.ts +114 -0
  223. package/src/providers/read-chat-contract.ts +76 -16
  224. package/src/providers/sdk/README.md +49 -0
  225. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  226. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  227. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  228. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  229. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  230. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  231. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  232. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  233. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  234. package/src/providers/sdk/v1/index.ts +152 -0
  235. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  236. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  237. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  238. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  239. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  240. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  241. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  242. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  243. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  244. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  245. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  246. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  247. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  248. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  249. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  250. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  251. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  252. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  253. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  254. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  255. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  256. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  257. package/src/providers/sdk/v1/validators/index.ts +19 -0
  258. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  259. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  260. package/src/providers/spec/adapter.ts +155 -0
  261. package/src/providers/spec/cli-adapter.ts +838 -0
  262. package/src/providers/spec/evaluator.ts +345 -0
  263. package/src/providers/spec/fsm-driver.ts +875 -0
  264. package/src/providers/spec/fsm-evaluator.ts +255 -0
  265. package/src/providers/spec/fsm-loader.ts +112 -0
  266. package/src/providers/spec/fsm-types.ts +222 -0
  267. package/src/providers/spec/native-history-executor.ts +943 -0
  268. package/src/providers/spec/pre-launch-trust.ts +104 -0
  269. package/src/providers/spec/route.ts +51 -0
  270. package/src/providers/spec/types.ts +189 -0
  271. package/src/providers/transcript-v2.ts +567 -0
  272. package/src/providers/types/interactive-prompt.ts +536 -0
  273. package/src/providers/version-archive.ts +38 -20
  274. package/src/repo-mesh-types.ts +378 -1
  275. package/src/sessions/registry.ts +6 -0
  276. package/src/shared-types-extra.ts +2 -4
  277. package/src/shared-types.ts +45 -1
  278. package/src/status/builders.ts +26 -6
  279. package/src/status/normalize.ts +2 -0
  280. package/src/status/reporter.ts +15 -0
  281. package/src/status/snapshot.ts +84 -25
  282. package/src/system/host-memory.ts +29 -12
  283. package/src/types.ts +5 -0
  284. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  285. package/dist/mesh/mesh-sync.d.ts +0 -53
  286. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  287. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  288. package/src/mesh/mesh-sync.ts +0 -111
@@ -1,28 +1,159 @@
1
- import { existsSync, writeFileSync, readFileSync } 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 } from '../repo-mesh-types.js';
4
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
5
+ import { getMesh } from '../config/mesh-config.js';
5
6
 
6
7
  export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
7
8
  export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
8
9
  export type MeshHistoricalTaskStatus = Extract<MeshTaskStatus, 'completed' | 'failed' | 'cancelled'>;
10
+ export type MeshTaskMode = 'code_change' | 'validation' | 'live_debug_readonly' | 'launch_app' | 'convergence';
9
11
 
10
12
  export const ACTIVE_MESH_QUEUE_STATUSES: MeshActiveTaskStatus[] = ['pending', 'assigned'];
11
13
  export const HISTORICAL_MESH_QUEUE_STATUSES: MeshHistoricalTaskStatus[] = ['completed', 'failed', 'cancelled'];
14
+ export const MESH_TASK_MODES: MeshTaskMode[] = ['code_change', 'validation', 'live_debug_readonly', 'launch_app', 'convergence'];
15
+
16
+ export interface MeshTaskModeValidationResult {
17
+ valid: boolean;
18
+ taskMode?: MeshTaskMode;
19
+ violations: string[];
20
+ allowedOperations?: string[];
21
+ }
22
+
23
+ const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
24
+ { 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 },
25
+ { label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
26
+ { 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 },
27
+ { 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 },
28
+ { 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 },
29
+ { 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 },
30
+ ];
31
+
32
+ /**
33
+ * Git subcommands that mutate the working tree, index, refs, or remote.
34
+ * `stash` and `checkout` are intentionally absent here: they have read-only
35
+ * variants (`git stash list`/`show`, `git checkout-index`) and are classified
36
+ * token-by-token in {@link detectGitMutation} rather than by bare keyword.
37
+ */
38
+ const GIT_MUTATION_SUBCOMMANDS = new Set([
39
+ 'add', 'commit', 'push', 'reset', 'rebase', 'clean', 'switch', 'merge',
40
+ 'tag', 'restore', 'rm', 'mv', 'cherry-pick', 'revert', 'pull', 'fetch',
41
+ 'am', 'apply', 'gc', 'prune',
42
+ ]);
43
+
44
+ /**
45
+ * Read-only `git stash` variants. Any other `git stash <x>` (pop/apply/drop/
46
+ * push/save/clear, or bare `git stash` which defaults to push) is a mutation.
47
+ */
48
+ const GIT_STASH_READONLY_SUBCOMMANDS = new Set(['list', 'show']);
49
+
50
+ /**
51
+ * Detects a true git mutation in free-text task message, token-aware so that
52
+ * read-only diagnostics (`git stash list`, `git stash show --stat`,
53
+ * `git checkout-index`, `git status`, `git diff`, `git log`, ...) are allowed.
54
+ * Returns true only when a genuine mutating git invocation is present.
55
+ */
56
+ function detectGitMutation(message: string): boolean {
57
+ const re = /\bgit\s+([a-z][a-z0-9-]*)/gi;
58
+ let match: RegExpExecArray | null;
59
+ while ((match = re.exec(message)) !== null) {
60
+ const sub = match[1].toLowerCase();
61
+ if (GIT_MUTATION_SUBCOMMANDS.has(sub)) return true;
62
+ if (sub === 'stash') {
63
+ // Token following `git stash`; read-only only for list/show.
64
+ const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
65
+ const next = after ? after[1].toLowerCase() : '';
66
+ if (!GIT_STASH_READONLY_SUBCOMMANDS.has(next)) return true; // bare stash = push, or pop/apply/drop/...
67
+ } else if (sub === 'checkout') {
68
+ // `git checkout <ref/path>` mutates; `git checkout-index` is matched
69
+ // as its own token by the regex (sub === 'checkout-index') and is read-only.
70
+ return true;
71
+ } else if (sub === 'submodule') {
72
+ // `git submodule update` mutates; `git submodule status` is read-only.
73
+ const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
74
+ const next = after ? after[1].toLowerCase() : '';
75
+ if (next === 'update' || next === 'add' || next === 'sync' || next === 'deinit') return true;
76
+ } else if (sub === 'worktree') {
77
+ const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
78
+ const next = after ? after[1].toLowerCase() : '';
79
+ if (next === 'add' || next === 'remove' || next === 'move' || next === 'prune') return true;
80
+ }
81
+ // checkout-index, stash-with-no-next-already-handled, status/diff/log/show/
82
+ // rev-parse/branch/submodule status fall through as read-only.
83
+ }
84
+ return false;
85
+ }
86
+
87
+ export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
88
+ if (typeof value !== 'string') return undefined;
89
+ const normalized = value.trim() as MeshTaskMode;
90
+ return (MESH_TASK_MODES as string[]).includes(normalized) ? normalized : undefined;
91
+ }
92
+
93
+ export function validateMeshTaskModeRequest(mode: unknown, message: string): MeshTaskModeValidationResult {
94
+ const taskMode = normalizeMeshTaskMode(mode);
95
+ if (!taskMode) {
96
+ return { valid: true, violations: [] };
97
+ }
98
+ if (taskMode !== 'live_debug_readonly') {
99
+ return { valid: true, taskMode, violations: [] };
100
+ }
101
+ const text = message || '';
102
+ const violations = LIVE_DEBUG_READONLY_FORBIDDEN
103
+ .filter(rule => rule.pattern.test(text))
104
+ .map(rule => rule.label);
105
+ if (detectGitMutation(text)) {
106
+ violations.push('git_mutation');
107
+ }
108
+ return {
109
+ valid: violations.length === 0,
110
+ taskMode,
111
+ violations,
112
+ allowedOperations: [
113
+ 'process/log/window/port/session inspection',
114
+ 'read-only filesystem listing/reading',
115
+ 'status probes and keep-running handle reporting',
116
+ 'diagnostic summaries without source edits, commits, checkpoints, pushes, deploys, resets, rebases, or destructive cleanups',
117
+ ],
118
+ };
119
+ }
12
120
 
13
121
  export interface MeshWorkQueueEntry {
14
122
  id: string;
15
123
  meshId: string;
16
124
  message: string;
17
125
  status: MeshTaskStatus;
126
+ taskMode?: MeshTaskMode;
18
127
  /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
19
128
  targetNodeId?: string;
20
129
  /** If specified, only this runtime session can claim the task */
21
130
  targetSessionId?: string;
131
+ /** If specified, a node must expose all tags before it can claim the task. */
132
+ requiredTags?: string[];
133
+ /**
134
+ * M1: ids of tasks that must reach 'completed' before this task is claimable.
135
+ * Forward references (ids not yet enqueued) are allowed for batch flows and
136
+ * simply keep the task waiting until the referenced task exists and completes.
137
+ */
138
+ dependsOn?: string[];
139
+ /** M1/M3: mission this task belongs to (joins mesh_missions). */
140
+ missionId?: string;
141
+ /**
142
+ * M1: why this task is held back (e.g. "dependency_failed:<taskId>").
143
+ * Only set by the system on dependency failure under the 'block' policy;
144
+ * waiting-on-dependency state is computed at view time, not stored.
145
+ */
146
+ blockedReason?: string;
22
147
  /** The node that actually claimed and is executing the task */
23
148
  assignedNodeId?: string;
24
149
  /** The session currently executing the task */
25
150
  assignedSessionId?: string;
151
+ /**
152
+ * Provider type of the session that claimed the task. Recorded so the queue
153
+ * can enforce per-(node, provider) maxParallel caps (RepoMeshNodePolicy
154
+ * providerRoles) by counting active assignments grouped by node + provider.
155
+ */
156
+ assignedProviderType?: string;
26
157
  /** Human/operator reason for terminal cancellation. */
27
158
  cancelReason?: string;
28
159
  cancelledAt?: string;
@@ -30,6 +161,8 @@ export interface MeshWorkQueueEntry {
30
161
  requeueReason?: string;
31
162
  requeuedAt?: string;
32
163
  requeueCount?: number;
164
+ /** Max automatic requeue attempts. When requeueCount reaches this, task is auto-failed. */
165
+ maxRetries?: number;
33
166
  /** Last automatic queue session spin-up attempt, for mesh_view_queue/debug visibility. */
34
167
  autoLaunch?: {
35
168
  status: 'skipped' | 'started' | 'failed' | 'completed';
@@ -45,25 +178,112 @@ export interface MeshWorkQueueEntry {
45
178
  updatedAt: string;
46
179
  }
47
180
 
48
- function getQueuePath(meshId: string): string {
49
- const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
50
- return join(getLedgerDir(), `${safe}.queue.json`);
181
+ export interface MeshQueueMutationOptions {
182
+ ownerRole?: RepoMeshDaemonRole;
183
+ }
184
+
185
+ export function normalizeMeshCapabilityTags(value: unknown): string[] {
186
+ if (!Array.isArray(value)) return [];
187
+ const seen = new Set<string>();
188
+ return value
189
+ .map(tag => typeof tag === 'string' ? tag.trim() : '')
190
+ .filter(Boolean)
191
+ .filter(tag => {
192
+ if (seen.has(tag)) return false;
193
+ seen.add(tag);
194
+ return true;
195
+ });
196
+ }
197
+
198
+ function firstProviderPriority(policy: unknown): string | undefined {
199
+ const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
200
+ ? (policy as Record<string, unknown>).providerPriority
201
+ : undefined;
202
+ if (!Array.isArray(raw)) return undefined;
203
+ return raw.find(type => typeof type === 'string' && type.trim())?.trim();
204
+ }
205
+
206
+ export function buildMeshNodeCapabilityTags(
207
+ node: { capabilities?: unknown; policy?: unknown; isLocalWorktree?: unknown; worktreeBranch?: unknown } | undefined,
208
+ providerType?: string,
209
+ ): string[] {
210
+ const provider = typeof providerType === 'string' && providerType.trim()
211
+ ? providerType.trim()
212
+ : firstProviderPriority(node?.policy);
213
+ const worktreeBranch = typeof node?.worktreeBranch === 'string' && node.worktreeBranch.trim()
214
+ ? node.worktreeBranch.trim()
215
+ : null;
216
+ return normalizeMeshCapabilityTags([
217
+ ...(Array.isArray(node?.capabilities) ? node.capabilities : []),
218
+ `os=${process.platform}`,
219
+ `arch=${process.arch}`,
220
+ ...(provider ? [`provider=${provider}`] : []),
221
+ // Worktree nodes automatically expose a "worktree=<branch>" tag so that
222
+ // mesh_enqueue_task with required_tags: ["worktree=<branch>"] routes
223
+ // only to the matching worktree node.
224
+ ...(node?.isLocalWorktree === true && worktreeBranch ? [`worktree=${worktreeBranch}`] : []),
225
+ ]);
226
+ }
227
+
228
+ export function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags: unknown): boolean {
229
+ const required = normalizeMeshCapabilityTags(requiredTags);
230
+ if (required.length === 0) return true;
231
+ const available = new Set(normalizeMeshCapabilityTags(capabilityTags));
232
+ return required.every(tag => available.has(tag));
233
+ }
234
+
235
+ function withQueueLock<T>(_meshId: string, fn: () => T): T {
236
+ return MeshRuntimeStore.getInstance().transaction(fn);
51
237
  }
52
238
 
53
239
  function readQueue(meshId: string): MeshWorkQueueEntry[] {
54
- const path = getQueuePath(meshId);
55
- if (!existsSync(path)) return [];
56
- try {
57
- const content = readFileSync(path, 'utf-8');
58
- return JSON.parse(content) as MeshWorkQueueEntry[];
59
- } catch {
60
- return [];
61
- }
240
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId);
62
241
  }
63
242
 
64
243
  function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
65
- const path = getQueuePath(meshId);
66
- writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8');
244
+ MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
245
+ }
246
+
247
+ function normalizeDependsOn(value: unknown): string[] {
248
+ if (!Array.isArray(value)) return [];
249
+ const seen = new Set<string>();
250
+ return value
251
+ .map(id => typeof id === 'string' ? id.trim() : '')
252
+ .filter(Boolean)
253
+ .filter(id => {
254
+ if (seen.has(id)) return false;
255
+ seen.add(id);
256
+ return true;
257
+ });
258
+ }
259
+
260
+ /**
261
+ * M1: detect dependency cycles before enqueue. Walks the dependency graph of
262
+ * existing queue entries plus the new task's edges. Fail-closed: a cycle
263
+ * rejects the enqueue entirely. Synchronous and bounded by queue size.
264
+ */
265
+ export function assertNoDependencyCycle(meshId: string, newTaskId: string, dependsOn: string[]): void {
266
+ if (dependsOn.length === 0) return;
267
+ if (dependsOn.includes(newTaskId)) {
268
+ throw new Error(`dependency_cycle_detected: task '${newTaskId}' cannot depend on itself`);
269
+ }
270
+ const adjacency = new Map<string, string[]>();
271
+ for (const entry of readQueue(meshId)) {
272
+ adjacency.set(entry.id, normalizeDependsOn(entry.dependsOn));
273
+ }
274
+ adjacency.set(newTaskId, dependsOn);
275
+ // DFS from the new task: if we can reach newTaskId again, the edges form a cycle.
276
+ const stack = [...dependsOn];
277
+ const visited = new Set<string>();
278
+ while (stack.length > 0) {
279
+ const current = stack.pop()!;
280
+ if (current === newTaskId) {
281
+ throw new Error(`dependency_cycle_detected: task '${newTaskId}' is part of a dependency cycle via '${dependsOn.join(', ')}'`);
282
+ }
283
+ if (visited.has(current)) continue;
284
+ visited.add(current);
285
+ stack.push(...(adjacency.get(current) ?? []));
286
+ }
67
287
  }
68
288
 
69
289
  /**
@@ -72,75 +292,193 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
72
292
  export function enqueueTask(
73
293
  meshId: string,
74
294
  message: string,
75
- opts?: { targetNodeId?: string; targetSessionId?: string }
295
+ opts?: {
296
+ targetNodeId?: string;
297
+ targetSessionId?: string;
298
+ taskMode?: MeshTaskMode | string;
299
+ requiredTags?: string[];
300
+ /** M1: tasks that must complete before this one is claimable. */
301
+ dependsOn?: string[];
302
+ /** M1/M3: mission this task belongs to. */
303
+ missionId?: string;
304
+ /** Explicit task id for batch/template flows (M5). Random UUID when omitted. */
305
+ id?: string;
306
+ } & MeshQueueMutationOptions,
76
307
  ): MeshWorkQueueEntry {
77
- const queue = readQueue(meshId);
78
- const entry: MeshWorkQueueEntry = {
79
- id: randomUUID(),
80
- meshId,
81
- message,
82
- status: 'pending',
83
- targetNodeId: opts?.targetNodeId,
84
- targetSessionId: opts?.targetSessionId,
85
- createdAt: new Date().toISOString(),
86
- updatedAt: new Date().toISOString(),
87
- };
88
- queue.push(entry);
89
- writeQueue(meshId, queue);
90
- return entry;
308
+ requireMeshHostQueueOwner(opts);
309
+ const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
310
+ if (!modeValidation.valid) {
311
+ throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
312
+ }
313
+ const id = typeof opts?.id === 'string' && opts.id.trim() ? opts.id.trim() : randomUUID();
314
+ const dependsOn = normalizeDependsOn(opts?.dependsOn);
315
+ return withQueueLock(meshId, () => {
316
+ if (MeshRuntimeStore.getInstance().findQueueEntryById(meshId, id)) {
317
+ throw new Error(`duplicate_task_id: task '${id}' already exists in mesh '${meshId}'`);
318
+ }
319
+ assertNoDependencyCycle(meshId, id, dependsOn);
320
+ const entry: MeshWorkQueueEntry = {
321
+ id,
322
+ meshId,
323
+ message,
324
+ status: 'pending',
325
+ taskMode: modeValidation.taskMode,
326
+ targetNodeId: opts?.targetNodeId,
327
+ targetSessionId: opts?.targetSessionId,
328
+ requiredTags: normalizeMeshCapabilityTags(opts?.requiredTags),
329
+ ...(dependsOn.length > 0 ? { dependsOn } : {}),
330
+ ...(typeof opts?.missionId === 'string' && opts.missionId.trim() ? { missionId: opts.missionId.trim() } : {}),
331
+ createdAt: new Date().toISOString(),
332
+ updatedAt: new Date().toISOString(),
333
+ };
334
+ MeshRuntimeStore.getInstance().insertQueueEntry(entry);
335
+ return entry;
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Record a direct-dispatch task (mesh_send_task) as an already-assigned queue
341
+ * entry so it is attributable to a mission.
342
+ *
343
+ * Direct dispatch normally bypasses the queue entirely — the task lives only in
344
+ * the ledger + mesh_direct_dispatches table, neither of which carries a
345
+ * missionId, so {@link summarizeMissionTasks}/{@link computeMeshTaskStats}
346
+ * (which both scan the queue for `task.missionId`) count it as 0. When a
347
+ * mission is attached, we materialise the same queue entry shape an enqueued
348
+ * task would have, but pre-assigned to the dispatched node/session and stamped
349
+ * with the dispatch timestamp. The terminal event path (updateSessionTaskStatus
350
+ * → findAssignedBySession) then flips it to completed/failed exactly like a
351
+ * pulled task, so mission total + completed aggregates work with no extra wiring.
352
+ *
353
+ * Intentionally separate from {@link enqueueTask}: enqueue creates `pending`
354
+ * work for the queue to assign, whereas this records work already dispatched
355
+ * out-of-band. They share the missionId stamping rule and mode validation.
356
+ */
357
+ export function recordDirectDispatchTask(
358
+ meshId: string,
359
+ message: string,
360
+ opts: {
361
+ id: string;
362
+ missionId: string;
363
+ assignedNodeId?: string;
364
+ assignedSessionId?: string;
365
+ taskMode?: MeshTaskMode | string;
366
+ dispatchedAt?: string;
367
+ },
368
+ ): MeshWorkQueueEntry | null {
369
+ const missionId = typeof opts.missionId === 'string' ? opts.missionId.trim() : '';
370
+ if (!missionId) return null;
371
+ const taskId = typeof opts.id === 'string' ? opts.id.trim() : '';
372
+ if (!taskId) return null;
373
+ const modeValidation = validateMeshTaskModeRequest(opts.taskMode, message);
374
+ if (!modeValidation.valid) {
375
+ throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
376
+ }
377
+ const now = opts.dispatchedAt && opts.dispatchedAt.trim() ? opts.dispatchedAt : new Date().toISOString();
378
+ return withQueueLock(meshId, () => {
379
+ if (MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId)) {
380
+ // Already materialised (e.g. retry of the same dispatch) — leave it untouched.
381
+ return null;
382
+ }
383
+ const entry: MeshWorkQueueEntry = {
384
+ id: taskId,
385
+ meshId,
386
+ message,
387
+ status: 'assigned',
388
+ ...(modeValidation.taskMode ? { taskMode: modeValidation.taskMode } : {}),
389
+ missionId,
390
+ ...(opts.assignedNodeId ? { targetNodeId: opts.assignedNodeId, assignedNodeId: opts.assignedNodeId } : {}),
391
+ ...(opts.assignedSessionId ? { targetSessionId: opts.assignedSessionId, assignedSessionId: opts.assignedSessionId } : {}),
392
+ dispatchTimestamp: now,
393
+ createdAt: now,
394
+ updatedAt: now,
395
+ };
396
+ MeshRuntimeStore.getInstance().insertQueueEntry(entry);
397
+ return entry;
398
+ });
91
399
  }
92
400
 
93
401
  /**
94
402
  * Get all tasks in the queue, optionally filtered by status.
95
403
  */
96
404
  export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): MeshWorkQueueEntry[] {
97
- let queue = readQueue(meshId);
98
- if (opts?.status?.length) {
99
- const statuses = new Set(opts.status);
100
- queue = queue.filter(q => statuses.has(q.status));
101
- }
102
- return queue;
405
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId, opts?.status?.length ? opts.status : undefined);
406
+ }
407
+
408
+ export function getMeshQueueRevision(meshId: string): string {
409
+ return MeshRuntimeStore.getInstance().getQueueRevision(meshId);
103
410
  }
104
411
 
105
412
  /**
106
413
  * Find the next pending task that this node is allowed to claim, and mark it as assigned.
414
+ *
415
+ * `opts.providerType` is stamped onto the claimed entry (assignedProviderType) so
416
+ * per-(node, provider) caps can be counted. `opts.providerMaxParallel`, when set,
417
+ * is the enforced per-(node, provider) cap from RepoMeshNodePolicy.providerRoles:
418
+ * a task is not assigned to this (node, provider) once it already has that many
419
+ * active assignments. This composes with the global/taskMode caps (stricter wins).
107
420
  */
108
- export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
109
- const queue = readQueue(meshId);
110
-
111
- // A worker must finish or fail its current queued assignment before it can
112
- // claim another one. maxParallelTasks limits total mesh concurrency; it is
113
- // not permission for one node/session to accumulate multiple assigned items.
114
- const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
115
- q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
116
- ));
117
- if (hasActiveAssignment) return null;
118
-
119
- // Find highest priority task:
120
- // 1. Pending tasks explicitly targeted at this runtime session
121
- // 2. Pending tasks explicitly targeted at this node (but not another session)
122
- // 3. Pending tasks with no target node/session
123
- let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
124
- if (targetIdx === -1) {
125
- targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
126
- }
127
- if (targetIdx === -1) {
128
- targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
129
- }
421
+ export function claimNextTask(
422
+ meshId: string,
423
+ nodeId: string,
424
+ sessionId: string,
425
+ capabilityTags?: string[],
426
+ opts?: { providerType?: string; providerMaxParallel?: number },
427
+ ): MeshWorkQueueEntry | null {
428
+ return MeshRuntimeStore.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags, opts);
429
+ }
130
430
 
131
- if (targetIdx === -1) return null;
431
+ // ─── M1: Dependency Failure Propagation ─────────
132
432
 
133
- const entry = queue[targetIdx];
134
- entry.status = 'assigned';
135
- entry.assignedNodeId = nodeId;
136
- entry.assignedSessionId = sessionId;
137
- entry.dispatchTimestamp = new Date().toISOString();
138
- entry.updatedAt = new Date().toISOString();
433
+ export type DependencyFailurePolicy = 'block' | 'cancel';
434
+
435
+ function resolveDependencyFailurePolicy(meshId: string): DependencyFailurePolicy {
436
+ try {
437
+ const policy = (getMesh(meshId)?.policy ?? {}) as Record<string, unknown>;
438
+ return policy.onDependencyFailure === 'cancel' ? 'cancel' : 'block';
439
+ } catch {
440
+ return 'block';
441
+ }
442
+ }
139
443
 
140
- writeQueue(meshId, queue);
141
- return entry;
444
+ /**
445
+ * Apply the mesh's onDependencyFailure policy to pending dependents of a task
446
+ * that just reached a failed/cancelled terminal state.
447
+ *
448
+ * - 'block' (default): dependents stay pending with blockedReason
449
+ * "dependency_failed:<taskId>" so an operator can requeue/cancel them.
450
+ * - 'cancel': dependents are cancelled (cascading to their own dependents).
451
+ *
452
+ * Must be called inside the queue lock of the triggering transition.
453
+ */
454
+ function propagateDependencyFailure(meshId: string, failedTaskId: string): void {
455
+ const policy = resolveDependencyFailurePolicy(meshId);
456
+ const store = MeshRuntimeStore.getInstance();
457
+ const frontier = [failedTaskId];
458
+ const seen = new Set<string>(frontier);
459
+ while (frontier.length > 0) {
460
+ const currentId = frontier.pop()!;
461
+ const dependents = store.getQueueEntries(meshId, ['pending'])
462
+ .filter(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(currentId));
463
+ for (const dependent of dependents) {
464
+ if (seen.has(dependent.id)) continue;
465
+ seen.add(dependent.id);
466
+ if (policy === 'cancel') {
467
+ dependent.status = 'cancelled';
468
+ dependent.cancelledAt = new Date().toISOString();
469
+ dependent.cancelReason = `dependency_failed:${currentId}`;
470
+ store.updateQueueEntry(dependent);
471
+ frontier.push(dependent.id); // cascade to transitive dependents
472
+ } else {
473
+ dependent.blockedReason = `dependency_failed:${currentId}`;
474
+ store.updateQueueEntry(dependent);
475
+ }
476
+ }
477
+ }
142
478
  }
143
479
 
480
+ const DEPENDENCY_FAILURE_TERMINALS = new Set<MeshTaskStatus>(['failed', 'cancelled']);
481
+
144
482
  /**
145
483
  * Update the status of a specific task.
146
484
  * Used when a session completes, fails, or stalls.
@@ -149,15 +487,17 @@ export function updateTaskStatus(
149
487
  meshId: string,
150
488
  taskId: string,
151
489
  status: MeshTaskStatus,
490
+ opts?: MeshQueueMutationOptions,
152
491
  ): MeshWorkQueueEntry | null {
153
- const queue = readQueue(meshId);
154
- const idx = queue.findIndex(q => q.id === taskId);
155
- if (idx === -1) return null;
156
-
157
- queue[idx].status = status;
158
- queue[idx].updatedAt = new Date().toISOString();
159
- writeQueue(meshId, queue);
160
- return queue[idx];
492
+ requireMeshHostQueueOwner(opts);
493
+ return withQueueLock(meshId, () => {
494
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
495
+ if (!entry) return null;
496
+ entry.status = status;
497
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
498
+ if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, taskId);
499
+ return entry;
500
+ });
161
501
  }
162
502
 
163
503
  export function recordTaskAutoLaunch(
@@ -165,17 +505,14 @@ export function recordTaskAutoLaunch(
165
505
  taskId: string,
166
506
  autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
167
507
  ): MeshWorkQueueEntry | null {
168
- const queue = readQueue(meshId);
169
- const idx = queue.findIndex(q => q.id === taskId);
170
- if (idx === -1) return null;
171
- const now = new Date().toISOString();
172
- queue[idx].autoLaunch = {
173
- ...autoLaunch,
174
- updatedAt: now,
175
- };
176
- queue[idx].updatedAt = now;
177
- writeQueue(meshId, queue);
178
- return queue[idx];
508
+ return withQueueLock(meshId, () => {
509
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
510
+ if (!entry) return null;
511
+ const now = new Date().toISOString();
512
+ entry.autoLaunch = { ...autoLaunch, updatedAt: now };
513
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
514
+ return entry;
515
+ });
179
516
  }
180
517
 
181
518
  /**
@@ -184,25 +521,31 @@ export function recordTaskAutoLaunch(
184
521
  export function cancelTask(
185
522
  meshId: string,
186
523
  taskId: string,
187
- opts?: { reason?: string },
524
+ opts?: { reason?: string } & MeshQueueMutationOptions,
188
525
  ): MeshWorkQueueEntry | null {
189
- const queue = readQueue(meshId);
190
- const idx = queue.findIndex(q => q.id === taskId);
191
- if (idx === -1) return null;
192
-
193
- const now = new Date().toISOString();
194
- queue[idx].status = 'cancelled';
195
- queue[idx].updatedAt = now;
196
- queue[idx].cancelledAt = now;
197
- if (opts?.reason) queue[idx].cancelReason = opts.reason;
198
- writeQueue(meshId, queue);
199
- return queue[idx];
526
+ requireMeshHostQueueOwner(opts);
527
+ return withQueueLock(meshId, () => {
528
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
529
+ if (!entry) return null;
530
+ const now = new Date().toISOString();
531
+ entry.status = 'cancelled';
532
+ entry.cancelledAt = now;
533
+ if (opts?.reason) entry.cancelReason = opts.reason;
534
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
535
+ propagateDependencyFailure(meshId, taskId);
536
+ return entry;
537
+ });
200
538
  }
201
539
 
202
540
  /**
203
541
  * Return a queue task to pending for retry. By default, dead session targeting
204
542
  * and assigned ownership are cleared so stale assignments do not strand again.
205
543
  */
544
+ export type RequeueResult =
545
+ | { status: 'requeued'; entry: MeshWorkQueueEntry }
546
+ | { status: 'failed_max_retries'; entry: MeshWorkQueueEntry; maxRetries: number; requeueCount: number }
547
+ | { status: 'not_found' };
548
+
206
549
  export function requeueTask(
207
550
  meshId: string,
208
551
  taskId: string,
@@ -212,29 +555,48 @@ export function requeueTask(
212
555
  targetSessionId?: string;
213
556
  clearTargetNode?: boolean;
214
557
  clearTargetSession?: boolean;
215
- },
558
+ /**
559
+ * Override the retry cap for this call. Use only for explicit operator actions.
560
+ * If true, the task is requeued even when requeueCount >= maxRetries.
561
+ */
562
+ force?: boolean;
563
+ /** Per-task retry cap override. Falls back to mesh policy maxTaskRetries (default 1). */
564
+ maxRetries?: number;
565
+ } & MeshQueueMutationOptions,
216
566
  ): MeshWorkQueueEntry | null {
217
- const queue = readQueue(meshId);
218
- const idx = queue.findIndex(q => q.id === taskId);
219
- if (idx === -1) return null;
220
-
221
- const entry = queue[idx];
222
- const now = new Date().toISOString();
223
- entry.status = 'pending';
224
- delete entry.assignedNodeId;
225
- delete entry.assignedSessionId;
226
- delete entry.cancelledAt;
227
- delete entry.cancelReason;
228
- if (opts?.clearTargetNode) delete entry.targetNodeId;
229
- if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
230
- if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
231
- if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
232
- entry.updatedAt = now;
233
- entry.requeuedAt = now;
234
- entry.requeueCount = (entry.requeueCount || 0) + 1;
235
- if (opts?.reason) entry.requeueReason = opts.reason;
236
- writeQueue(meshId, queue);
237
- return entry;
567
+ requireMeshHostQueueOwner(opts);
568
+ return withQueueLock(meshId, () => {
569
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
570
+ if (!entry) return null;
571
+ const currentCount = entry.requeueCount || 0;
572
+ const maxRetries = opts?.maxRetries ?? entry.maxRetries ?? 1;
573
+ if (!opts?.force && currentCount >= maxRetries) {
574
+ // Auto-fail: cap exceeded without explicit force override.
575
+ entry.status = 'failed';
576
+ entry.cancelReason = `max_retries_exceeded: requeued ${currentCount} time(s), limit is ${maxRetries}`;
577
+ entry.updatedAt = new Date().toISOString();
578
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
579
+ propagateDependencyFailure(meshId, taskId);
580
+ return entry;
581
+ }
582
+ entry.status = 'pending';
583
+ // Operator requeue clears a dependency-failure block — the operator is
584
+ // explicitly overriding the held-back state.
585
+ delete entry.blockedReason;
586
+ delete entry.assignedNodeId;
587
+ delete entry.assignedSessionId;
588
+ delete entry.cancelledAt;
589
+ delete entry.cancelReason;
590
+ if (opts?.clearTargetNode) delete entry.targetNodeId;
591
+ if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
592
+ if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
593
+ if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
594
+ entry.requeuedAt = new Date().toISOString();
595
+ entry.requeueCount = currentCount + 1;
596
+ if (opts?.reason) entry.requeueReason = opts.reason;
597
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
598
+ return entry;
599
+ });
238
600
  }
239
601
 
240
602
  /**
@@ -244,29 +606,43 @@ export function updateSessionTaskStatus(
244
606
  meshId: string,
245
607
  sessionId: string,
246
608
  status: MeshTaskStatus,
609
+ opts?: { occurredAt?: string },
247
610
  ): MeshWorkQueueEntry | null {
248
- const queue = readQueue(meshId);
249
- // Collect all assigned tasks for this session, then pick the one with the
250
- // most recent dispatchTimestamp (or updatedAt fallback for legacy entries).
251
- // This prevents completing the wrong task when multiple tasks were assigned
252
- // to the same session in rapid succession.
253
- let bestIdx = -1;
254
- let bestTime = 0;
255
- for (let i = queue.length - 1; i >= 0; i--) {
256
- if (queue[i].assignedSessionId === sessionId && queue[i].status === 'assigned') {
257
- const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
258
- if (time > bestTime) {
259
- bestTime = time;
260
- bestIdx = i;
261
- }
262
- }
263
- }
264
- if (bestIdx === -1) return null;
611
+ return withQueueLock(meshId, () => {
612
+ const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
613
+ const entry = MeshRuntimeStore.getInstance().findAssignedBySession(meshId, sessionId, occurredAtIso);
614
+ if (!entry) return null;
615
+ entry.status = status;
616
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
617
+ if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, entry.id);
618
+ return entry;
619
+ });
620
+ }
265
621
 
266
- queue[bestIdx].status = status;
267
- queue[bestIdx].updatedAt = new Date().toISOString();
268
- writeQueue(meshId, queue);
269
- return queue[bestIdx];
622
+ /**
623
+ * M1-3: true when at least one pending task is waiting on the given task.
624
+ * Used by the completion event path to decide whether to wake the queue.
625
+ */
626
+ export function hasPendingDependents(meshId: string, taskId: string): boolean {
627
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId, ['pending'])
628
+ .some(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(taskId));
629
+ }
630
+
631
+ /**
632
+ * M1-4: view-time dependency state for a task — unmet dependency ids and
633
+ * whether the task is currently claimable from a dependency standpoint.
634
+ * Not stored (truth stays in task statuses).
635
+ */
636
+ export function describeTaskDependencyState(
637
+ entry: Pick<MeshWorkQueueEntry, 'dependsOn' | 'blockedReason'>,
638
+ statusById: Map<string, MeshTaskStatus | string>,
639
+ ): { waitingOn: string[]; dependenciesSatisfied: boolean } {
640
+ const deps = Array.isArray(entry.dependsOn) ? entry.dependsOn : [];
641
+ const waitingOn = deps.filter(depId => statusById.get(depId) !== 'completed');
642
+ return {
643
+ waitingOn,
644
+ dependenciesSatisfied: waitingOn.length === 0 && !entry.blockedReason,
645
+ };
270
646
  }
271
647
 
272
648
  export interface MeshWorkQueueStats {
@@ -294,14 +670,16 @@ export interface MeshWorkQueueStats {
294
670
  * Return aggregate queue statistics for the given mesh.
295
671
  */
296
672
  export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
297
- const queue = readQueue(meshId);
298
- const pending = queue.filter(q => q.status === 'pending').length;
299
- const assigned = queue.filter(q => q.status === 'assigned').length;
300
- const completed = queue.filter(q => q.status === 'completed').length;
301
- const failed = queue.filter(q => q.status === 'failed').length;
302
- const cancelled = queue.filter(q => q.status === 'cancelled').length;
673
+ const rows = MeshRuntimeStore.getInstance().getQueueStatsByStatus(meshId);
674
+ const counts: Record<string, number> = {};
675
+ for (const r of rows) counts[r.status] = r.count;
676
+ const pending = counts['pending'] ?? 0;
677
+ const assigned = counts['assigned'] ?? 0;
678
+ const completed = counts['completed'] ?? 0;
679
+ const failed = counts['failed'] ?? 0;
680
+ const cancelled = counts['cancelled'] ?? 0;
303
681
  return {
304
- total: queue.length,
682
+ total: pending + assigned + completed + failed + cancelled,
305
683
  active: pending + assigned,
306
684
  historical: completed + failed + cancelled,
307
685
  pending,
@@ -309,22 +687,119 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
309
687
  completed,
310
688
  failed,
311
689
  cancelled,
312
- activeCounts: {
313
- pending,
314
- assigned,
315
- },
316
- historicalCounts: {
317
- completed,
318
- failed,
319
- cancelled,
320
- },
321
- activeAssignments: queue
322
- .filter(q => q.status === 'assigned')
323
- .map(q => ({
324
- id: q.id,
325
- nodeId: q.assignedNodeId,
326
- sessionId: q.assignedSessionId,
327
- message: q.message,
328
- })),
690
+ activeCounts: { pending, assigned },
691
+ historicalCounts: { completed, failed, cancelled },
692
+ activeAssignments: MeshRuntimeStore.getInstance().getActiveAssignmentDetails(meshId),
329
693
  };
330
694
  }
695
+
696
+ export function __replaceMeshQueueForTests(meshId: string, queue: MeshWorkQueueEntry[]): void {
697
+ MeshRuntimeStore.getInstance().transaction(() => {
698
+ MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
699
+ });
700
+ }
701
+
702
+ export function __clearMeshQueueForTests(meshId: string): void {
703
+ MeshRuntimeStore.getInstance().deleteQueue(meshId);
704
+ }
705
+
706
+ export function __clearDirectDispatchesForTests(meshId: string): void {
707
+ MeshRuntimeStore.getInstance().deleteDirectDispatches(meshId);
708
+ }
709
+
710
+ export function __resetMeshRuntimeStoreForTests(): void {
711
+ MeshRuntimeStore.resetForTests();
712
+ }
713
+
714
+ // ── Direct Dispatch Tracking ─────────────────────────────────────────────────
715
+ // Persists direct (non-queue) task dispatches so buildMeshActiveWork can read
716
+ // active work from MeshRuntimeStore instead of scanning ledger JSONL entries.
717
+
718
+ export type DirectDispatchRecord = ReturnType<MeshRuntimeStore['getActiveDirectDispatches']>[number];
719
+
720
+ export function insertDirectDispatch(
721
+ meshId: string,
722
+ data: {
723
+ taskId: string;
724
+ nodeId?: string;
725
+ sessionId?: string;
726
+ providerType?: string;
727
+ message: string;
728
+ taskMode?: string;
729
+ via: string;
730
+ dispatchedToIdleSession?: boolean;
731
+ dispatchedAt: string;
732
+ },
733
+ ): void {
734
+ try {
735
+ MeshRuntimeStore.getInstance().insertDirectDispatch({ ...data, meshId });
736
+ } catch (e: any) {
737
+ process.stderr.write(`[adhdev-mesh] insertDirectDispatch failed for task ${data.taskId}: ${e?.message || e}\n`);
738
+ }
739
+ }
740
+
741
+ export function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[] {
742
+ try {
743
+ return MeshRuntimeStore.getInstance().getActiveDirectDispatches(meshId);
744
+ } catch {
745
+ return [];
746
+ }
747
+ }
748
+
749
+ export function updateDirectDispatchStatus(
750
+ meshId: string,
751
+ sessionId: string,
752
+ status: 'acked' | 'completed' | 'failed' | 'stale',
753
+ ): void {
754
+ try {
755
+ MeshRuntimeStore.getInstance().updateDirectDispatchStatus(meshId, sessionId, status);
756
+ } catch { /* best-effort */ }
757
+ }
758
+
759
+ export function cleanupTerminalDirectDispatches(olderThanMs = 7 * 24 * 60 * 60_000): void {
760
+ try {
761
+ MeshRuntimeStore.getInstance().cleanupTerminalDirectDispatches(olderThanMs);
762
+ } catch { /* best-effort */ }
763
+ }
764
+
765
+ export function markStaleDirectDispatches(meshId: string, olderThanMs = 60 * 60_000): void {
766
+ try {
767
+ MeshRuntimeStore.getInstance().markStaleDirectDispatches(meshId, olderThanMs);
768
+ } catch { /* best-effort */ }
769
+ }
770
+
771
+ /**
772
+ * Delete specific direct dispatch rows by taskId. Returns the number of rows deleted.
773
+ * Used by the staleDirect prune path to evict orphaned/terminal dispatch records from the
774
+ * active staleDirect surface while leaving the append-only mesh ledger (audit history) intact.
775
+ */
776
+ export function deleteDirectDispatchesByTaskId(meshId: string, taskIds: string[]): number {
777
+ try {
778
+ return MeshRuntimeStore.getInstance().deleteDirectDispatchesByTaskId(meshId, taskIds);
779
+ } catch {
780
+ return 0;
781
+ }
782
+ }
783
+
784
+ export type MeshToolCallRateResult = { rateLimitExceeded: boolean; callsInWindow: number; advisory: string | null };
785
+
786
+ /**
787
+ * Record a coordinator tool call and return a rate-limit advisory when the
788
+ * call rate for that tool exceeds the allowed threshold.
789
+ *
790
+ * Defaults: 10-second sliding window, max 5 calls before advisory is raised.
791
+ * Returns { rateLimitExceeded: false } on any store error so callers are not blocked.
792
+ */
793
+ export function recordMeshToolCall(opts: {
794
+ meshId: string;
795
+ tool: string;
796
+ sessionId?: string | null;
797
+ windowMs?: number;
798
+ maxCalls?: number;
799
+ }): MeshToolCallRateResult {
800
+ try {
801
+ return MeshRuntimeStore.getInstance().recordMeshToolCall(opts);
802
+ } catch {
803
+ return { rateLimitExceeded: false, callsInWindow: 0, advisory: null };
804
+ }
805
+ }