@adhdev/daemon-core 0.9.82-rc.25 → 0.9.82-rc.250

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 (270) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/chat/source-machine.d.ts +166 -0
  4. package/dist/chat/source-resolver.d.ts +104 -0
  5. package/dist/chat/subscription-updates.d.ts +1 -0
  6. package/dist/cli-adapter-types.d.ts +23 -2
  7. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  8. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  9. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  10. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  11. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  12. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  13. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +3 -3
  14. package/dist/cli-adapters/terminal-backends/types.d.ts +1 -2
  15. package/dist/cli-adapters/terminal-screen.d.ts +2 -11
  16. package/dist/commands/cli-manager.d.ts +2 -1
  17. package/dist/commands/handler.d.ts +110 -0
  18. package/dist/commands/mesh-coordinator.d.ts +85 -1
  19. package/dist/commands/router.d.ts +30 -1
  20. package/dist/config/chat-history.d.ts +9 -0
  21. package/dist/config/config.d.ts +5 -0
  22. package/dist/config/mesh-config.d.ts +72 -1
  23. package/dist/git/git-commands.d.ts +9 -1
  24. package/dist/git/git-diff.d.ts +6 -0
  25. package/dist/git/git-types.d.ts +2 -0
  26. package/dist/index.d.ts +47 -11
  27. package/dist/index.js +31185 -12164
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +30204 -11275
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/installer.d.ts +1 -4
  32. package/dist/ipc/local-ipc-server.d.ts +91 -0
  33. package/dist/launch.d.ts +1 -1
  34. package/dist/logging/async-batch-writer.d.ts +10 -0
  35. package/dist/mesh/contracts.d.ts +164 -0
  36. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  37. package/dist/mesh/coordinator-registry.d.ts +59 -0
  38. package/dist/mesh/mesh-active-work.d.ts +90 -0
  39. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  40. package/dist/mesh/mesh-events-coordinator.d.ts +130 -0
  41. package/dist/mesh/mesh-events-pending.d.ts +33 -0
  42. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  43. package/dist/mesh/mesh-events-utils.d.ts +14 -0
  44. package/dist/mesh/mesh-events.d.ts +5 -49
  45. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  46. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  47. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  48. package/dist/mesh/mesh-ledger.d.ts +77 -1
  49. package/dist/mesh/mesh-missions.d.ts +58 -0
  50. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  51. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  52. package/dist/mesh/mesh-runtime-store.d.ts +324 -0
  53. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  54. package/dist/mesh/mesh-work-queue.d.ts +134 -5
  55. package/dist/mesh/preview-freshness.d.ts +18 -0
  56. package/dist/mesh/refine-config.d.ts +215 -0
  57. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  58. package/dist/providers/acp-provider-instance.d.ts +2 -0
  59. package/dist/providers/approval-utils.d.ts +13 -0
  60. package/dist/providers/cli-provider-instance.d.ts +40 -3
  61. package/dist/providers/contracts.d.ts +130 -6
  62. package/dist/providers/external-sources.d.ts +71 -0
  63. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  64. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  65. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  66. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  67. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  68. package/dist/providers/native-history/index.d.ts +13 -0
  69. package/dist/providers/provider-instance-manager.d.ts +13 -0
  70. package/dist/providers/provider-instance.d.ts +13 -1
  71. package/dist/providers/provider-loader.d.ts +26 -4
  72. package/dist/providers/provider-trust.d.ts +31 -0
  73. package/dist/providers/read-chat-contract.d.ts +29 -0
  74. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  75. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  76. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  77. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  78. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  79. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  80. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  81. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  82. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  83. package/dist/providers/sdk/v1/index.d.ts +30 -0
  84. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  85. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  86. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  87. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  88. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  89. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  90. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  91. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  92. package/dist/providers/spec/adapter.d.ts +52 -0
  93. package/dist/providers/spec/cli-adapter.d.ts +102 -0
  94. package/dist/providers/spec/evaluator.d.ts +23 -0
  95. package/dist/providers/spec/fsm-driver.d.ts +231 -0
  96. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  97. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  98. package/dist/providers/spec/fsm-types.d.ts +91 -0
  99. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  100. package/dist/providers/spec/route.d.ts +4 -0
  101. package/dist/providers/spec/types.d.ts +129 -0
  102. package/dist/providers/transcript-v2.d.ts +176 -0
  103. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  104. package/dist/repo-mesh-types.d.ts +126 -1
  105. package/dist/sessions/registry.d.ts +6 -0
  106. package/dist/shared-types-extra.d.ts +2 -4
  107. package/dist/shared-types.d.ts +42 -1
  108. package/dist/status/normalize.d.ts +1 -1
  109. package/dist/status/normalize.js +1 -0
  110. package/dist/status/normalize.js.map +1 -1
  111. package/dist/status/normalize.mjs +1 -0
  112. package/dist/status/normalize.mjs.map +1 -1
  113. package/dist/status/reporter.d.ts +2 -0
  114. package/dist/status/snapshot.d.ts +1 -0
  115. package/dist/types.d.ts +5 -0
  116. package/package.json +6 -3
  117. package/src/agent-stream/poller.ts +2 -3
  118. package/src/boot/daemon-lifecycle.ts +22 -10
  119. package/src/boot/process-hardening.ts +89 -0
  120. package/src/chat/source-machine.ts +534 -0
  121. package/src/chat/source-resolver.ts +0 -0
  122. package/src/chat/subscription-updates.ts +20 -1
  123. package/src/cli-adapter-types.d.ts +1 -0
  124. package/src/cli-adapter-types.ts +22 -2
  125. package/src/cli-adapters/cli-script-runner.ts +421 -0
  126. package/src/cli-adapters/cli-state-engine.ts +1162 -0
  127. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  128. package/src/cli-adapters/provider-cli-adapter.ts +673 -1139
  129. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  130. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  131. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  132. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  133. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  134. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  135. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +35 -29
  136. package/src/cli-adapters/terminal-backends/types.ts +1 -3
  137. package/src/cli-adapters/terminal-screen.ts +11 -81
  138. package/src/commands/chat-commands.ts +1806 -60
  139. package/src/commands/cli-manager.ts +286 -14
  140. package/src/commands/handler.ts +809 -2
  141. package/src/commands/mesh-coordinator.ts +332 -122
  142. package/src/commands/router.ts +4598 -535
  143. package/src/config/chat-history.ts +95 -24
  144. package/src/config/config.ts +12 -0
  145. package/src/config/mesh-config.ts +303 -3
  146. package/src/config/recent-activity.ts +8 -2
  147. package/src/daemon/dev-auto-implement.ts +3 -2
  148. package/src/daemon/dev-cli-debug.ts +10 -1
  149. package/src/detection/ide-detector.ts +26 -16
  150. package/src/git/git-commands.ts +42 -12
  151. package/src/git/git-diff.ts +53 -0
  152. package/src/git/git-status.ts +35 -6
  153. package/src/git/git-types.ts +2 -0
  154. package/src/git/git-worktree.ts +8 -1
  155. package/src/index.ts +127 -10
  156. package/src/installer.d.ts +1 -1
  157. package/src/installer.ts +8 -6
  158. package/src/ipc/local-ipc-server.ts +278 -0
  159. package/src/launch.d.ts +1 -1
  160. package/src/launch.ts +37 -28
  161. package/src/logging/async-batch-writer.ts +55 -0
  162. package/src/logging/logger.ts +2 -1
  163. package/src/mesh/contracts.ts +329 -0
  164. package/src/mesh/coordinator-prompt.ts +219 -31
  165. package/src/mesh/coordinator-registry.ts +121 -0
  166. package/src/mesh/mesh-active-work.ts +437 -0
  167. package/src/mesh/mesh-delivery-policy.ts +315 -0
  168. package/src/mesh/mesh-events-coordinator.ts +1510 -0
  169. package/src/mesh/mesh-events-pending.ts +371 -0
  170. package/src/mesh/mesh-events-stale.ts +283 -0
  171. package/src/mesh/mesh-events-utils.ts +175 -0
  172. package/src/mesh/mesh-events.ts +24 -1035
  173. package/src/mesh/mesh-fast-forward.ts +438 -0
  174. package/src/mesh/mesh-host-ownership.ts +73 -0
  175. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  176. package/src/mesh/mesh-ledger.ts +587 -102
  177. package/src/mesh/mesh-missions.ts +151 -0
  178. package/src/mesh/mesh-refine-status.ts +144 -0
  179. package/src/mesh/mesh-review-inbox.ts +307 -0
  180. package/src/mesh/mesh-runtime-store.ts +1305 -0
  181. package/src/mesh/mesh-task-stats.ts +154 -0
  182. package/src/mesh/mesh-work-queue.ts +453 -145
  183. package/src/mesh/preview-freshness.ts +118 -0
  184. package/src/mesh/refine-config.ts +403 -0
  185. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  186. package/src/providers/acp-provider-instance.ts +15 -1
  187. package/src/providers/approval-utils.d.ts +4 -0
  188. package/src/providers/approval-utils.ts +47 -5
  189. package/src/providers/chat-message-normalization.ts +4 -11
  190. package/src/providers/cli-provider-instance.ts +850 -71
  191. package/src/providers/contracts.d.ts +55 -0
  192. package/src/providers/contracts.ts +141 -6
  193. package/src/providers/external-sources.ts +218 -0
  194. package/src/providers/ide-provider-instance.ts +19 -5
  195. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  196. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  197. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  198. package/src/providers/native-history/dispatcher.ts +340 -0
  199. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  200. package/src/providers/native-history/index.ts +30 -0
  201. package/src/providers/provider-instance-manager.ts +30 -0
  202. package/src/providers/provider-instance.ts +10 -1
  203. package/src/providers/provider-loader.ts +584 -50
  204. package/src/providers/provider-schema.ts +87 -14
  205. package/src/providers/provider-trust.ts +114 -0
  206. package/src/providers/read-chat-contract.ts +76 -16
  207. package/src/providers/sdk/README.md +49 -0
  208. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  209. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  210. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  211. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  212. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  213. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  214. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  215. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  216. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  217. package/src/providers/sdk/v1/index.ts +152 -0
  218. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  219. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  220. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  221. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  222. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  223. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  224. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  225. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  226. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  227. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  228. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  229. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  230. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  231. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  232. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  233. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  234. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  235. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  236. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  237. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  238. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  239. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  240. package/src/providers/sdk/v1/validators/index.ts +19 -0
  241. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  242. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  243. package/src/providers/spec/adapter.ts +138 -0
  244. package/src/providers/spec/cli-adapter.ts +646 -0
  245. package/src/providers/spec/evaluator.ts +321 -0
  246. package/src/providers/spec/fsm-driver.ts +749 -0
  247. package/src/providers/spec/fsm-evaluator.ts +255 -0
  248. package/src/providers/spec/fsm-loader.ts +102 -0
  249. package/src/providers/spec/fsm-types.ts +184 -0
  250. package/src/providers/spec/native-history-executor.ts +943 -0
  251. package/src/providers/spec/route.ts +51 -0
  252. package/src/providers/spec/types.ts +173 -0
  253. package/src/providers/transcript-v2.ts +567 -0
  254. package/src/providers/types/interactive-prompt.ts +425 -0
  255. package/src/providers/version-archive.ts +38 -20
  256. package/src/repo-mesh-types.ts +138 -1
  257. package/src/sessions/registry.ts +6 -0
  258. package/src/shared-types-extra.ts +2 -4
  259. package/src/shared-types.ts +45 -1
  260. package/src/status/builders.ts +26 -6
  261. package/src/status/normalize.ts +2 -0
  262. package/src/status/reporter.ts +15 -0
  263. package/src/status/snapshot.ts +84 -25
  264. package/src/system/host-memory.ts +29 -12
  265. package/src/types.ts +5 -0
  266. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  267. package/dist/mesh/mesh-sync.d.ts +0 -53
  268. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  269. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  270. package/src/mesh/mesh-sync.ts +0 -111
@@ -1,24 +1,91 @@
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 } 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: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv|stash|worktree\s+(?:add|remove|move))|push\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
+ export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
34
+ if (typeof value !== 'string') return undefined;
35
+ const normalized = value.trim() as MeshTaskMode;
36
+ return (MESH_TASK_MODES as string[]).includes(normalized) ? normalized : undefined;
37
+ }
38
+
39
+ export function validateMeshTaskModeRequest(mode: unknown, message: string): MeshTaskModeValidationResult {
40
+ const taskMode = normalizeMeshTaskMode(mode);
41
+ if (!taskMode) {
42
+ return { valid: true, violations: [] };
43
+ }
44
+ if (taskMode !== 'live_debug_readonly') {
45
+ return { valid: true, taskMode, violations: [] };
46
+ }
47
+ const violations = LIVE_DEBUG_READONLY_FORBIDDEN
48
+ .filter(rule => rule.pattern.test(message || ''))
49
+ .map(rule => rule.label);
50
+ return {
51
+ valid: violations.length === 0,
52
+ taskMode,
53
+ violations,
54
+ allowedOperations: [
55
+ 'process/log/window/port/session inspection',
56
+ 'read-only filesystem listing/reading',
57
+ 'status probes and keep-running handle reporting',
58
+ 'diagnostic summaries without source edits, commits, checkpoints, pushes, deploys, resets, rebases, or destructive cleanups',
59
+ ],
60
+ };
61
+ }
12
62
 
13
63
  export interface MeshWorkQueueEntry {
14
64
  id: string;
15
65
  meshId: string;
16
66
  message: string;
17
67
  status: MeshTaskStatus;
68
+ taskMode?: MeshTaskMode;
18
69
  /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
19
70
  targetNodeId?: string;
20
71
  /** If specified, only this runtime session can claim the task */
21
72
  targetSessionId?: string;
73
+ /** If specified, a node must expose all tags before it can claim the task. */
74
+ requiredTags?: string[];
75
+ /**
76
+ * M1: ids of tasks that must reach 'completed' before this task is claimable.
77
+ * Forward references (ids not yet enqueued) are allowed for batch flows and
78
+ * simply keep the task waiting until the referenced task exists and completes.
79
+ */
80
+ dependsOn?: string[];
81
+ /** M1/M3: mission this task belongs to (joins mesh_missions). */
82
+ missionId?: string;
83
+ /**
84
+ * M1: why this task is held back (e.g. "dependency_failed:<taskId>").
85
+ * Only set by the system on dependency failure under the 'block' policy;
86
+ * waiting-on-dependency state is computed at view time, not stored.
87
+ */
88
+ blockedReason?: string;
22
89
  /** The node that actually claimed and is executing the task */
23
90
  assignedNodeId?: string;
24
91
  /** The session currently executing the task */
@@ -30,6 +97,8 @@ export interface MeshWorkQueueEntry {
30
97
  requeueReason?: string;
31
98
  requeuedAt?: string;
32
99
  requeueCount?: number;
100
+ /** Max automatic requeue attempts. When requeueCount reaches this, task is auto-failed. */
101
+ maxRetries?: number;
33
102
  /** Last automatic queue session spin-up attempt, for mesh_view_queue/debug visibility. */
34
103
  autoLaunch?: {
35
104
  status: 'skipped' | 'started' | 'failed' | 'completed';
@@ -45,50 +114,112 @@ export interface MeshWorkQueueEntry {
45
114
  updatedAt: string;
46
115
  }
47
116
 
48
- function getQueuePath(meshId: string): string {
49
- const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
50
- return join(getLedgerDir(), `${safe}.queue.json`);
117
+ export interface MeshQueueMutationOptions {
118
+ ownerRole?: RepoMeshDaemonRole;
51
119
  }
52
120
 
53
- function getLockPath(meshId: string): string {
54
- const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
55
- return join(getLedgerDir(), `${safe}.queue.lock`);
121
+ export function normalizeMeshCapabilityTags(value: unknown): string[] {
122
+ if (!Array.isArray(value)) return [];
123
+ const seen = new Set<string>();
124
+ return value
125
+ .map(tag => typeof tag === 'string' ? tag.trim() : '')
126
+ .filter(Boolean)
127
+ .filter(tag => {
128
+ if (seen.has(tag)) return false;
129
+ seen.add(tag);
130
+ return true;
131
+ });
56
132
  }
57
133
 
58
- /**
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).
62
- */
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
- }
71
- }
72
- try { return fn(); } finally {
73
- if (fd !== -1) try { closeSync(fd); } catch { /* noop */ }
74
- try { unlinkSync(lockPath); } catch { /* already removed */ }
75
- }
134
+ function firstProviderPriority(policy: unknown): string | undefined {
135
+ const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
136
+ ? (policy as Record<string, unknown>).providerPriority
137
+ : undefined;
138
+ if (!Array.isArray(raw)) return undefined;
139
+ return raw.find(type => typeof type === 'string' && type.trim())?.trim();
140
+ }
141
+
142
+ export function buildMeshNodeCapabilityTags(
143
+ node: { capabilities?: unknown; policy?: unknown; isLocalWorktree?: unknown; worktreeBranch?: unknown } | undefined,
144
+ providerType?: string,
145
+ ): string[] {
146
+ const provider = typeof providerType === 'string' && providerType.trim()
147
+ ? providerType.trim()
148
+ : firstProviderPriority(node?.policy);
149
+ const worktreeBranch = typeof node?.worktreeBranch === 'string' && node.worktreeBranch.trim()
150
+ ? node.worktreeBranch.trim()
151
+ : null;
152
+ return normalizeMeshCapabilityTags([
153
+ ...(Array.isArray(node?.capabilities) ? node.capabilities : []),
154
+ `os=${process.platform}`,
155
+ `arch=${process.arch}`,
156
+ ...(provider ? [`provider=${provider}`] : []),
157
+ // Worktree nodes automatically expose a "worktree=<branch>" tag so that
158
+ // mesh_enqueue_task with required_tags: ["worktree=<branch>"] routes
159
+ // only to the matching worktree node.
160
+ ...(node?.isLocalWorktree === true && worktreeBranch ? [`worktree=${worktreeBranch}`] : []),
161
+ ]);
162
+ }
163
+
164
+ export function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags: unknown): boolean {
165
+ const required = normalizeMeshCapabilityTags(requiredTags);
166
+ if (required.length === 0) return true;
167
+ const available = new Set(normalizeMeshCapabilityTags(capabilityTags));
168
+ return required.every(tag => available.has(tag));
169
+ }
170
+
171
+ function withQueueLock<T>(_meshId: string, fn: () => T): T {
172
+ return MeshRuntimeStore.getInstance().transaction(fn);
76
173
  }
77
174
 
78
175
  function readQueue(meshId: string): MeshWorkQueueEntry[] {
79
- const path = getQueuePath(meshId);
80
- if (!existsSync(path)) return [];
81
- try {
82
- const content = readFileSync(path, 'utf-8');
83
- return JSON.parse(content) as MeshWorkQueueEntry[];
84
- } catch {
85
- return [];
86
- }
176
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId);
87
177
  }
88
178
 
89
179
  function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
90
- const path = getQueuePath(meshId);
91
- writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8');
180
+ MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
181
+ }
182
+
183
+ function normalizeDependsOn(value: unknown): string[] {
184
+ if (!Array.isArray(value)) return [];
185
+ const seen = new Set<string>();
186
+ return value
187
+ .map(id => typeof id === 'string' ? id.trim() : '')
188
+ .filter(Boolean)
189
+ .filter(id => {
190
+ if (seen.has(id)) return false;
191
+ seen.add(id);
192
+ return true;
193
+ });
194
+ }
195
+
196
+ /**
197
+ * M1: detect dependency cycles before enqueue. Walks the dependency graph of
198
+ * existing queue entries plus the new task's edges. Fail-closed: a cycle
199
+ * rejects the enqueue entirely. Synchronous and bounded by queue size.
200
+ */
201
+ export function assertNoDependencyCycle(meshId: string, newTaskId: string, dependsOn: string[]): void {
202
+ if (dependsOn.length === 0) return;
203
+ if (dependsOn.includes(newTaskId)) {
204
+ throw new Error(`dependency_cycle_detected: task '${newTaskId}' cannot depend on itself`);
205
+ }
206
+ const adjacency = new Map<string, string[]>();
207
+ for (const entry of readQueue(meshId)) {
208
+ adjacency.set(entry.id, normalizeDependsOn(entry.dependsOn));
209
+ }
210
+ adjacency.set(newTaskId, dependsOn);
211
+ // DFS from the new task: if we can reach newTaskId again, the edges form a cycle.
212
+ const stack = [...dependsOn];
213
+ const visited = new Set<string>();
214
+ while (stack.length > 0) {
215
+ const current = stack.pop()!;
216
+ if (current === newTaskId) {
217
+ throw new Error(`dependency_cycle_detected: task '${newTaskId}' is part of a dependency cycle via '${dependsOn.join(', ')}'`);
218
+ }
219
+ if (visited.has(current)) continue;
220
+ visited.add(current);
221
+ stack.push(...(adjacency.get(current) ?? []));
222
+ }
92
223
  }
93
224
 
94
225
  /**
@@ -97,22 +228,46 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
97
228
  export function enqueueTask(
98
229
  meshId: string,
99
230
  message: string,
100
- opts?: { targetNodeId?: string; targetSessionId?: string }
231
+ opts?: {
232
+ targetNodeId?: string;
233
+ targetSessionId?: string;
234
+ taskMode?: MeshTaskMode | string;
235
+ requiredTags?: string[];
236
+ /** M1: tasks that must complete before this one is claimable. */
237
+ dependsOn?: string[];
238
+ /** M1/M3: mission this task belongs to. */
239
+ missionId?: string;
240
+ /** Explicit task id for batch/template flows (M5). Random UUID when omitted. */
241
+ id?: string;
242
+ } & MeshQueueMutationOptions,
101
243
  ): MeshWorkQueueEntry {
244
+ requireMeshHostQueueOwner(opts);
245
+ const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
246
+ if (!modeValidation.valid) {
247
+ throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
248
+ }
249
+ const id = typeof opts?.id === 'string' && opts.id.trim() ? opts.id.trim() : randomUUID();
250
+ const dependsOn = normalizeDependsOn(opts?.dependsOn);
102
251
  return withQueueLock(meshId, () => {
103
- const queue = readQueue(meshId);
252
+ if (MeshRuntimeStore.getInstance().findQueueEntryById(meshId, id)) {
253
+ throw new Error(`duplicate_task_id: task '${id}' already exists in mesh '${meshId}'`);
254
+ }
255
+ assertNoDependencyCycle(meshId, id, dependsOn);
104
256
  const entry: MeshWorkQueueEntry = {
105
- id: randomUUID(),
257
+ id,
106
258
  meshId,
107
259
  message,
108
260
  status: 'pending',
261
+ taskMode: modeValidation.taskMode,
109
262
  targetNodeId: opts?.targetNodeId,
110
263
  targetSessionId: opts?.targetSessionId,
264
+ requiredTags: normalizeMeshCapabilityTags(opts?.requiredTags),
265
+ ...(dependsOn.length > 0 ? { dependsOn } : {}),
266
+ ...(typeof opts?.missionId === 'string' && opts.missionId.trim() ? { missionId: opts.missionId.trim() } : {}),
111
267
  createdAt: new Date().toISOString(),
112
268
  updatedAt: new Date().toISOString(),
113
269
  };
114
- queue.push(entry);
115
- writeQueue(meshId, queue);
270
+ MeshRuntimeStore.getInstance().insertQueueEntry(entry);
116
271
  return entry;
117
272
  });
118
273
  }
@@ -121,43 +276,71 @@ export function enqueueTask(
121
276
  * Get all tasks in the queue, optionally filtered by status.
122
277
  */
123
278
  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;
279
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId, opts?.status?.length ? opts.status : undefined);
280
+ }
281
+
282
+ export function getMeshQueueRevision(meshId: string): string {
283
+ return MeshRuntimeStore.getInstance().getQueueRevision(meshId);
130
284
  }
131
285
 
132
286
  /**
133
287
  * Find the next pending task that this node is allowed to claim, and mark it as assigned.
134
288
  */
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);
289
+ export function claimNextTask(meshId: string, nodeId: string, sessionId: string, capabilityTags?: string[]): MeshWorkQueueEntry | null {
290
+ return MeshRuntimeStore.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags);
291
+ }
292
+
293
+ // ─── M1: Dependency Failure Propagation ─────────
294
+
295
+ export type DependencyFailurePolicy = 'block' | 'cancel';
296
+
297
+ function resolveDependencyFailurePolicy(meshId: string): DependencyFailurePolicy {
298
+ try {
299
+ const policy = (getMesh(meshId)?.policy ?? {}) as Record<string, unknown>;
300
+ return policy.onDependencyFailure === 'cancel' ? 'cancel' : 'block';
301
+ } catch {
302
+ return 'block';
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Apply the mesh's onDependencyFailure policy to pending dependents of a task
308
+ * that just reached a failed/cancelled terminal state.
309
+ *
310
+ * - 'block' (default): dependents stay pending with blockedReason
311
+ * "dependency_failed:<taskId>" so an operator can requeue/cancel them.
312
+ * - 'cancel': dependents are cancelled (cascading to their own dependents).
313
+ *
314
+ * Must be called inside the queue lock of the triggering transition.
315
+ */
316
+ function propagateDependencyFailure(meshId: string, failedTaskId: string): void {
317
+ const policy = resolveDependencyFailurePolicy(meshId);
318
+ const store = MeshRuntimeStore.getInstance();
319
+ const frontier = [failedTaskId];
320
+ const seen = new Set<string>(frontier);
321
+ while (frontier.length > 0) {
322
+ const currentId = frontier.pop()!;
323
+ const dependents = store.getQueueEntries(meshId, ['pending'])
324
+ .filter(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(currentId));
325
+ for (const dependent of dependents) {
326
+ if (seen.has(dependent.id)) continue;
327
+ seen.add(dependent.id);
328
+ if (policy === 'cancel') {
329
+ dependent.status = 'cancelled';
330
+ dependent.cancelledAt = new Date().toISOString();
331
+ dependent.cancelReason = `dependency_failed:${currentId}`;
332
+ store.updateQueueEntry(dependent);
333
+ frontier.push(dependent.id); // cascade to transitive dependents
334
+ } else {
335
+ dependent.blockedReason = `dependency_failed:${currentId}`;
336
+ store.updateQueueEntry(dependent);
337
+ }
148
338
  }
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
- });
339
+ }
159
340
  }
160
341
 
342
+ const DEPENDENCY_FAILURE_TERMINALS = new Set<MeshTaskStatus>(['failed', 'cancelled']);
343
+
161
344
  /**
162
345
  * Update the status of a specific task.
163
346
  * Used when a session completes, fails, or stalls.
@@ -166,15 +349,16 @@ export function updateTaskStatus(
166
349
  meshId: string,
167
350
  taskId: string,
168
351
  status: MeshTaskStatus,
352
+ opts?: MeshQueueMutationOptions,
169
353
  ): MeshWorkQueueEntry | null {
354
+ requireMeshHostQueueOwner(opts);
170
355
  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];
356
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
357
+ if (!entry) return null;
358
+ entry.status = status;
359
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
360
+ if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, taskId);
361
+ return entry;
178
362
  });
179
363
  }
180
364
 
@@ -184,14 +368,12 @@ export function recordTaskAutoLaunch(
184
368
  autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
185
369
  ): MeshWorkQueueEntry | null {
186
370
  return withQueueLock(meshId, () => {
187
- const queue = readQueue(meshId);
188
- const idx = queue.findIndex(q => q.id === taskId);
189
- if (idx === -1) return null;
371
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
372
+ if (!entry) return null;
190
373
  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];
374
+ entry.autoLaunch = { ...autoLaunch, updatedAt: now };
375
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
376
+ return entry;
195
377
  });
196
378
  }
197
379
 
@@ -201,19 +383,19 @@ export function recordTaskAutoLaunch(
201
383
  export function cancelTask(
202
384
  meshId: string,
203
385
  taskId: string,
204
- opts?: { reason?: string },
386
+ opts?: { reason?: string } & MeshQueueMutationOptions,
205
387
  ): MeshWorkQueueEntry | null {
388
+ requireMeshHostQueueOwner(opts);
206
389
  return withQueueLock(meshId, () => {
207
- const queue = readQueue(meshId);
208
- const idx = queue.findIndex(q => q.id === taskId);
209
- if (idx === -1) return null;
390
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
391
+ if (!entry) return null;
210
392
  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];
393
+ entry.status = 'cancelled';
394
+ entry.cancelledAt = now;
395
+ if (opts?.reason) entry.cancelReason = opts.reason;
396
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
397
+ propagateDependencyFailure(meshId, taskId);
398
+ return entry;
217
399
  });
218
400
  }
219
401
 
@@ -221,6 +403,11 @@ export function cancelTask(
221
403
  * Return a queue task to pending for retry. By default, dead session targeting
222
404
  * and assigned ownership are cleared so stale assignments do not strand again.
223
405
  */
406
+ export type RequeueResult =
407
+ | { status: 'requeued'; entry: MeshWorkQueueEntry }
408
+ | { status: 'failed_max_retries'; entry: MeshWorkQueueEntry; maxRetries: number; requeueCount: number }
409
+ | { status: 'not_found' };
410
+
224
411
  export function requeueTask(
225
412
  meshId: string,
226
413
  taskId: string,
@@ -230,15 +417,34 @@ export function requeueTask(
230
417
  targetSessionId?: string;
231
418
  clearTargetNode?: boolean;
232
419
  clearTargetSession?: boolean;
233
- },
420
+ /**
421
+ * Override the retry cap for this call. Use only for explicit operator actions.
422
+ * If true, the task is requeued even when requeueCount >= maxRetries.
423
+ */
424
+ force?: boolean;
425
+ /** Per-task retry cap override. Falls back to mesh policy maxTaskRetries (default 1). */
426
+ maxRetries?: number;
427
+ } & MeshQueueMutationOptions,
234
428
  ): MeshWorkQueueEntry | null {
429
+ requireMeshHostQueueOwner(opts);
235
430
  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();
431
+ const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
432
+ if (!entry) return null;
433
+ const currentCount = entry.requeueCount || 0;
434
+ const maxRetries = opts?.maxRetries ?? entry.maxRetries ?? 1;
435
+ if (!opts?.force && currentCount >= maxRetries) {
436
+ // Auto-fail: cap exceeded without explicit force override.
437
+ entry.status = 'failed';
438
+ entry.cancelReason = `max_retries_exceeded: requeued ${currentCount} time(s), limit is ${maxRetries}`;
439
+ entry.updatedAt = new Date().toISOString();
440
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
441
+ propagateDependencyFailure(meshId, taskId);
442
+ return entry;
443
+ }
241
444
  entry.status = 'pending';
445
+ // Operator requeue clears a dependency-failure block — the operator is
446
+ // explicitly overriding the held-back state.
447
+ delete entry.blockedReason;
242
448
  delete entry.assignedNodeId;
243
449
  delete entry.assignedSessionId;
244
450
  delete entry.cancelledAt;
@@ -247,11 +453,10 @@ export function requeueTask(
247
453
  if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
248
454
  if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
249
455
  if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
250
- entry.updatedAt = now;
251
- entry.requeuedAt = now;
252
- entry.requeueCount = (entry.requeueCount || 0) + 1;
456
+ entry.requeuedAt = new Date().toISOString();
457
+ entry.requeueCount = currentCount + 1;
253
458
  if (opts?.reason) entry.requeueReason = opts.reason;
254
- writeQueue(meshId, queue);
459
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
255
460
  return entry;
256
461
  });
257
462
  }
@@ -266,25 +471,42 @@ export function updateSessionTaskStatus(
266
471
  opts?: { occurredAt?: string },
267
472
  ): MeshWorkQueueEntry | null {
268
473
  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];
474
+ const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
475
+ const entry = MeshRuntimeStore.getInstance().findAssignedBySession(meshId, sessionId, occurredAtIso);
476
+ if (!entry) return null;
477
+ entry.status = status;
478
+ MeshRuntimeStore.getInstance().updateQueueEntry(entry);
479
+ if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, entry.id);
480
+ return entry;
285
481
  });
286
482
  }
287
483
 
484
+ /**
485
+ * M1-3: true when at least one pending task is waiting on the given task.
486
+ * Used by the completion event path to decide whether to wake the queue.
487
+ */
488
+ export function hasPendingDependents(meshId: string, taskId: string): boolean {
489
+ return MeshRuntimeStore.getInstance().getQueueEntries(meshId, ['pending'])
490
+ .some(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(taskId));
491
+ }
492
+
493
+ /**
494
+ * M1-4: view-time dependency state for a task — unmet dependency ids and
495
+ * whether the task is currently claimable from a dependency standpoint.
496
+ * Not stored (truth stays in task statuses).
497
+ */
498
+ export function describeTaskDependencyState(
499
+ entry: Pick<MeshWorkQueueEntry, 'dependsOn' | 'blockedReason'>,
500
+ statusById: Map<string, MeshTaskStatus | string>,
501
+ ): { waitingOn: string[]; dependenciesSatisfied: boolean } {
502
+ const deps = Array.isArray(entry.dependsOn) ? entry.dependsOn : [];
503
+ const waitingOn = deps.filter(depId => statusById.get(depId) !== 'completed');
504
+ return {
505
+ waitingOn,
506
+ dependenciesSatisfied: waitingOn.length === 0 && !entry.blockedReason,
507
+ };
508
+ }
509
+
288
510
  export interface MeshWorkQueueStats {
289
511
  total: number;
290
512
  active: number;
@@ -310,14 +532,16 @@ export interface MeshWorkQueueStats {
310
532
  * Return aggregate queue statistics for the given mesh.
311
533
  */
312
534
  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;
535
+ const rows = MeshRuntimeStore.getInstance().getQueueStatsByStatus(meshId);
536
+ const counts: Record<string, number> = {};
537
+ for (const r of rows) counts[r.status] = r.count;
538
+ const pending = counts['pending'] ?? 0;
539
+ const assigned = counts['assigned'] ?? 0;
540
+ const completed = counts['completed'] ?? 0;
541
+ const failed = counts['failed'] ?? 0;
542
+ const cancelled = counts['cancelled'] ?? 0;
319
543
  return {
320
- total: queue.length,
544
+ total: pending + assigned + completed + failed + cancelled,
321
545
  active: pending + assigned,
322
546
  historical: completed + failed + cancelled,
323
547
  pending,
@@ -325,22 +549,106 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
325
549
  completed,
326
550
  failed,
327
551
  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
- })),
552
+ activeCounts: { pending, assigned },
553
+ historicalCounts: { completed, failed, cancelled },
554
+ activeAssignments: MeshRuntimeStore.getInstance().getActiveAssignmentDetails(meshId),
345
555
  };
346
556
  }
557
+
558
+ export function __replaceMeshQueueForTests(meshId: string, queue: MeshWorkQueueEntry[]): void {
559
+ MeshRuntimeStore.getInstance().transaction(() => {
560
+ MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
561
+ });
562
+ }
563
+
564
+ export function __clearMeshQueueForTests(meshId: string): void {
565
+ MeshRuntimeStore.getInstance().deleteQueue(meshId);
566
+ }
567
+
568
+ export function __clearDirectDispatchesForTests(meshId: string): void {
569
+ MeshRuntimeStore.getInstance().deleteDirectDispatches(meshId);
570
+ }
571
+
572
+ export function __resetMeshRuntimeStoreForTests(): void {
573
+ MeshRuntimeStore.resetForTests();
574
+ }
575
+
576
+ // ── Direct Dispatch Tracking ─────────────────────────────────────────────────
577
+ // Persists direct (non-queue) task dispatches so buildMeshActiveWork can read
578
+ // active work from MeshRuntimeStore instead of scanning ledger JSONL entries.
579
+
580
+ export type DirectDispatchRecord = ReturnType<MeshRuntimeStore['getActiveDirectDispatches']>[number];
581
+
582
+ export function insertDirectDispatch(
583
+ meshId: string,
584
+ data: {
585
+ taskId: string;
586
+ nodeId?: string;
587
+ sessionId?: string;
588
+ providerType?: string;
589
+ message: string;
590
+ taskMode?: string;
591
+ via: string;
592
+ dispatchedToIdleSession?: boolean;
593
+ dispatchedAt: string;
594
+ },
595
+ ): void {
596
+ try {
597
+ MeshRuntimeStore.getInstance().insertDirectDispatch({ ...data, meshId });
598
+ } catch (e: any) {
599
+ process.stderr.write(`[adhdev-mesh] insertDirectDispatch failed for task ${data.taskId}: ${e?.message || e}\n`);
600
+ }
601
+ }
602
+
603
+ export function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[] {
604
+ try {
605
+ return MeshRuntimeStore.getInstance().getActiveDirectDispatches(meshId);
606
+ } catch {
607
+ return [];
608
+ }
609
+ }
610
+
611
+ export function updateDirectDispatchStatus(
612
+ meshId: string,
613
+ sessionId: string,
614
+ status: 'acked' | 'completed' | 'failed' | 'stale',
615
+ ): void {
616
+ try {
617
+ MeshRuntimeStore.getInstance().updateDirectDispatchStatus(meshId, sessionId, status);
618
+ } catch { /* best-effort */ }
619
+ }
620
+
621
+ export function cleanupTerminalDirectDispatches(olderThanMs = 7 * 24 * 60 * 60_000): void {
622
+ try {
623
+ MeshRuntimeStore.getInstance().cleanupTerminalDirectDispatches(olderThanMs);
624
+ } catch { /* best-effort */ }
625
+ }
626
+
627
+ export function markStaleDirectDispatches(meshId: string, olderThanMs = 60 * 60_000): void {
628
+ try {
629
+ MeshRuntimeStore.getInstance().markStaleDirectDispatches(meshId, olderThanMs);
630
+ } catch { /* best-effort */ }
631
+ }
632
+
633
+ export type MeshToolCallRateResult = { rateLimitExceeded: boolean; callsInWindow: number; advisory: string | null };
634
+
635
+ /**
636
+ * Record a coordinator tool call and return a rate-limit advisory when the
637
+ * call rate for that tool exceeds the allowed threshold.
638
+ *
639
+ * Defaults: 10-second sliding window, max 5 calls before advisory is raised.
640
+ * Returns { rateLimitExceeded: false } on any store error so callers are not blocked.
641
+ */
642
+ export function recordMeshToolCall(opts: {
643
+ meshId: string;
644
+ tool: string;
645
+ sessionId?: string | null;
646
+ windowMs?: number;
647
+ maxCalls?: number;
648
+ }): MeshToolCallRateResult {
649
+ try {
650
+ return MeshRuntimeStore.getInstance().recordMeshToolCall(opts);
651
+ } catch {
652
+ return { rateLimitExceeded: false, callsInWindow: 0, advisory: null };
653
+ }
654
+ }