@adhdev/daemon-core 0.9.82-rc.21 → 0.9.82-rc.211

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 (269) 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 +32 -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 +46 -11
  27. package/dist/index.js +35173 -15756
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +34880 -15556
  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 +151 -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 -40
  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 +137 -6
  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 +39 -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 +12 -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 +101 -0
  94. package/dist/providers/spec/driver.d.ts +225 -0
  95. package/dist/providers/spec/evaluator.d.ts +54 -0
  96. package/dist/providers/spec/loader.d.ts +15 -0
  97. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  98. package/dist/providers/spec/route.d.ts +4 -0
  99. package/dist/providers/spec/schema.gen.d.ts +2025 -0
  100. package/dist/providers/spec/types.d.ts +302 -0
  101. package/dist/providers/transcript-v2.d.ts +176 -0
  102. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  103. package/dist/repo-mesh-types.d.ts +109 -1
  104. package/dist/sessions/registry.d.ts +3 -0
  105. package/dist/shared-types-extra.d.ts +2 -4
  106. package/dist/shared-types.d.ts +42 -1
  107. package/dist/status/normalize.d.ts +1 -1
  108. package/dist/status/normalize.js +1 -0
  109. package/dist/status/normalize.js.map +1 -1
  110. package/dist/status/normalize.mjs +1 -0
  111. package/dist/status/normalize.mjs.map +1 -1
  112. package/dist/status/reporter.d.ts +2 -0
  113. package/dist/status/snapshot.d.ts +1 -0
  114. package/dist/types.d.ts +5 -0
  115. package/package.json +6 -3
  116. package/src/agent-stream/poller.ts +2 -3
  117. package/src/boot/daemon-lifecycle.ts +22 -10
  118. package/src/boot/process-hardening.ts +89 -0
  119. package/src/chat/source-machine.ts +534 -0
  120. package/src/chat/source-resolver.ts +0 -0
  121. package/src/chat/subscription-updates.ts +20 -1
  122. package/src/cli-adapter-types.d.ts +1 -0
  123. package/src/cli-adapter-types.ts +22 -2
  124. package/src/cli-adapters/cli-script-runner.ts +421 -0
  125. package/src/cli-adapters/cli-state-engine.ts +1138 -0
  126. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  127. package/src/cli-adapters/provider-cli-adapter.ts +673 -1139
  128. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  129. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  130. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  131. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  132. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  133. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  134. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +35 -29
  135. package/src/cli-adapters/terminal-backends/types.ts +1 -3
  136. package/src/cli-adapters/terminal-screen.ts +11 -81
  137. package/src/commands/chat-commands.ts +1787 -60
  138. package/src/commands/cli-manager.ts +283 -14
  139. package/src/commands/handler.ts +809 -2
  140. package/src/commands/mesh-coordinator.ts +332 -122
  141. package/src/commands/router.ts +4198 -557
  142. package/src/config/chat-history.ts +95 -24
  143. package/src/config/config.ts +12 -0
  144. package/src/config/mesh-config.ts +280 -2
  145. package/src/config/recent-activity.ts +8 -2
  146. package/src/daemon/dev-auto-implement.ts +3 -2
  147. package/src/daemon/dev-cli-debug.ts +10 -1
  148. package/src/detection/ide-detector.ts +26 -16
  149. package/src/git/git-commands.ts +42 -12
  150. package/src/git/git-diff.ts +53 -0
  151. package/src/git/git-status.ts +35 -6
  152. package/src/git/git-types.ts +2 -0
  153. package/src/git/git-worktree.ts +8 -1
  154. package/src/index.ts +126 -10
  155. package/src/installer.d.ts +1 -1
  156. package/src/installer.ts +8 -6
  157. package/src/ipc/local-ipc-server.ts +278 -0
  158. package/src/launch.d.ts +1 -1
  159. package/src/launch.ts +37 -28
  160. package/src/logging/async-batch-writer.ts +55 -0
  161. package/src/logging/logger.ts +2 -1
  162. package/src/mesh/contracts.ts +329 -0
  163. package/src/mesh/coordinator-prompt.ts +218 -31
  164. package/src/mesh/coordinator-registry.ts +121 -0
  165. package/src/mesh/mesh-active-work.ts +437 -0
  166. package/src/mesh/mesh-delivery-policy.ts +315 -0
  167. package/src/mesh/mesh-events-coordinator.ts +1404 -0
  168. package/src/mesh/mesh-events-pending.ts +371 -0
  169. package/src/mesh/mesh-events-stale.ts +283 -0
  170. package/src/mesh/mesh-events-utils.ts +161 -0
  171. package/src/mesh/mesh-events.ts +24 -908
  172. package/src/mesh/mesh-fast-forward.ts +438 -0
  173. package/src/mesh/mesh-host-ownership.ts +73 -0
  174. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  175. package/src/mesh/mesh-ledger.ts +587 -102
  176. package/src/mesh/mesh-missions.ts +151 -0
  177. package/src/mesh/mesh-refine-status.ts +144 -0
  178. package/src/mesh/mesh-review-inbox.ts +307 -0
  179. package/src/mesh/mesh-runtime-store.ts +1305 -0
  180. package/src/mesh/mesh-task-stats.ts +154 -0
  181. package/src/mesh/mesh-work-queue.ts +490 -166
  182. package/src/mesh/preview-freshness.ts +118 -0
  183. package/src/mesh/refine-config.ts +403 -0
  184. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  185. package/src/providers/acp-provider-instance.ts +15 -1
  186. package/src/providers/approval-utils.d.ts +4 -0
  187. package/src/providers/approval-utils.ts +47 -5
  188. package/src/providers/chat-message-normalization.ts +4 -11
  189. package/src/providers/cli-provider-instance.ts +833 -64
  190. package/src/providers/contracts.d.ts +55 -0
  191. package/src/providers/contracts.ts +141 -6
  192. package/src/providers/external-sources.ts +218 -0
  193. package/src/providers/ide-provider-instance.ts +19 -5
  194. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  195. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  196. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  197. package/src/providers/native-history/dispatcher.ts +340 -0
  198. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  199. package/src/providers/native-history/index.ts +30 -0
  200. package/src/providers/provider-instance-manager.ts +30 -0
  201. package/src/providers/provider-instance.ts +10 -1
  202. package/src/providers/provider-loader.ts +582 -50
  203. package/src/providers/provider-schema.ts +87 -14
  204. package/src/providers/provider-trust.ts +114 -0
  205. package/src/providers/read-chat-contract.ts +76 -16
  206. package/src/providers/sdk/README.md +49 -0
  207. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  208. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  209. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  210. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  211. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  212. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  213. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  214. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  215. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  216. package/src/providers/sdk/v1/index.ts +152 -0
  217. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  218. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  219. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  220. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  221. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  222. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  223. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  224. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  225. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  226. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  227. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  228. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  229. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  230. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  231. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  232. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  233. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  234. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  235. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  236. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  237. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  238. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  239. package/src/providers/sdk/v1/validators/index.ts +19 -0
  240. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  241. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  242. package/src/providers/spec/adapter.ts +138 -0
  243. package/src/providers/spec/cli-adapter.ts +626 -0
  244. package/src/providers/spec/driver.ts +922 -0
  245. package/src/providers/spec/evaluator.ts +597 -0
  246. package/src/providers/spec/loader.ts +324 -0
  247. package/src/providers/spec/native-history-executor.ts +939 -0
  248. package/src/providers/spec/route.ts +51 -0
  249. package/src/providers/spec/schema.gen.ts +691 -0
  250. package/src/providers/spec/schema.json +341 -0
  251. package/src/providers/spec/types.ts +356 -0
  252. package/src/providers/transcript-v2.ts +567 -0
  253. package/src/providers/types/interactive-prompt.ts +425 -0
  254. package/src/providers/version-archive.ts +38 -20
  255. package/src/repo-mesh-types.ts +119 -1
  256. package/src/sessions/registry.ts +3 -0
  257. package/src/shared-types-extra.ts +2 -4
  258. package/src/shared-types.ts +45 -1
  259. package/src/status/builders.ts +24 -6
  260. package/src/status/normalize.ts +2 -0
  261. package/src/status/reporter.ts +15 -0
  262. package/src/status/snapshot.ts +84 -25
  263. package/src/system/host-memory.ts +29 -12
  264. package/src/types.ts +5 -0
  265. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  266. package/dist/mesh/mesh-sync.d.ts +0 -53
  267. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  268. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  269. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,1305 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { createRequire } from 'module';
4
+ import { getLedgerDir } from './mesh-ledger.js';
5
+ import { nodeSatisfiesRequiredTags } from './mesh-work-queue.js';
6
+ import type { MeshTaskStatus, MeshWorkQueueEntry } from './mesh-work-queue.js';
7
+ import type BetterSqlite3 from 'better-sqlite3';
8
+ import type { Database as DatabaseHandle } from 'better-sqlite3';
9
+
10
+ let DatabaseCtor: typeof BetterSqlite3 | undefined;
11
+
12
+ function loadDatabaseCtor(): typeof BetterSqlite3 {
13
+ if (DatabaseCtor) return DatabaseCtor;
14
+ const runtimeRequire = typeof require === 'function'
15
+ ? require
16
+ : createRequire(import.meta.url);
17
+ DatabaseCtor = runtimeRequire('better-sqlite3') as typeof BetterSqlite3;
18
+ return DatabaseCtor;
19
+ }
20
+
21
+ function safeMeshId(meshId: string): string {
22
+ return meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
23
+ }
24
+
25
+ function legacyQueuePath(meshId: string): string {
26
+ return join(getLedgerDir(), `${safeMeshId(meshId)}.queue.json`);
27
+ }
28
+
29
+ function meshRuntimeStorePath(): string {
30
+ const dir = getLedgerDir();
31
+ const nextPath = join(dir, 'mesh-runtime.db');
32
+ if (existsSync(nextPath)) return nextPath;
33
+
34
+ const legacyPath = join(dir, 'beads.db');
35
+ if (!existsSync(legacyPath)) return nextPath;
36
+
37
+ try {
38
+ renameSync(legacyPath, nextPath);
39
+ for (const suffix of ['-wal', '-shm']) {
40
+ const legacyCompanion = `${legacyPath}${suffix}`;
41
+ if (existsSync(legacyCompanion)) {
42
+ renameSync(legacyCompanion, `${nextPath}${suffix}`);
43
+ }
44
+ }
45
+ } catch {
46
+ // Best-effort compatibility for existing installs. If migration fails,
47
+ // opening the new store will create a clean DB instead of blocking boot.
48
+ }
49
+ return nextPath;
50
+ }
51
+
52
+ export class MeshRuntimeStore {
53
+ private static instance: MeshRuntimeStore | undefined;
54
+ private readonly db: DatabaseHandle;
55
+ private readonly dbPath: string;
56
+ private readonly migratedMeshIds = new Set<string>();
57
+ private fingerprintSweepCounter = 0;
58
+ private walWriteCounter = 0;
59
+ private static readonly WAL_CHECK_INTERVAL = 500;
60
+ private static readonly WAL_MAX_BYTES = 50 * 1024 * 1024; // 50 MB
61
+
62
+ private constructor(dbPath: string) {
63
+ const dir = dirname(dbPath);
64
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
65
+
66
+ this.dbPath = dbPath;
67
+ this.db = new (loadDatabaseCtor())(dbPath);
68
+ this.db.pragma('journal_mode = WAL');
69
+ this.db.pragma('synchronous = NORMAL');
70
+ this.db.pragma('foreign_keys = ON');
71
+ this.db.pragma('busy_timeout = 5000');
72
+ this.migrate();
73
+ }
74
+
75
+ static getInstance(): MeshRuntimeStore {
76
+ if (!this.instance) {
77
+ this.instance = new MeshRuntimeStore(meshRuntimeStorePath());
78
+ }
79
+ return this.instance;
80
+ }
81
+
82
+ static resetForTests(): void {
83
+ this.instance?.close();
84
+ this.instance = undefined;
85
+ }
86
+
87
+ close(): void {
88
+ this.db.close();
89
+ }
90
+
91
+ transaction<T>(fn: () => T): T {
92
+ return this.db.transaction(fn).immediate();
93
+ }
94
+
95
+ private migrate(): void {
96
+ this.db.exec(`
97
+ CREATE TABLE IF NOT EXISTS mesh_queue (
98
+ id TEXT PRIMARY KEY,
99
+ mesh_id TEXT NOT NULL,
100
+ status TEXT NOT NULL,
101
+ target_node_id TEXT,
102
+ target_session_id TEXT,
103
+ assigned_node_id TEXT,
104
+ assigned_session_id TEXT,
105
+ created_at TEXT NOT NULL,
106
+ updated_at TEXT NOT NULL,
107
+ payload TEXT NOT NULL
108
+ );
109
+
110
+ CREATE INDEX IF NOT EXISTS idx_mesh_queue_mesh_status_created
111
+ ON mesh_queue(mesh_id, status, created_at);
112
+ CREATE INDEX IF NOT EXISTS idx_mesh_queue_assignment
113
+ ON mesh_queue(mesh_id, assigned_node_id, assigned_session_id, status);
114
+
115
+ CREATE TABLE IF NOT EXISTS mesh_completion_fingerprints (
116
+ fingerprint TEXT PRIMARY KEY,
117
+ expires_at INTEGER NOT NULL
118
+ );
119
+
120
+ CREATE TABLE IF NOT EXISTS mesh_direct_dispatches (
121
+ task_id TEXT PRIMARY KEY,
122
+ mesh_id TEXT NOT NULL,
123
+ node_id TEXT,
124
+ session_id TEXT,
125
+ provider_type TEXT,
126
+ message TEXT NOT NULL,
127
+ task_mode TEXT,
128
+ via TEXT NOT NULL,
129
+ status TEXT NOT NULL DEFAULT 'dispatched',
130
+ dispatched_to_idle_session INTEGER NOT NULL DEFAULT 0,
131
+ dispatched_at TEXT NOT NULL,
132
+ updated_at TEXT NOT NULL
133
+ );
134
+
135
+ CREATE INDEX IF NOT EXISTS idx_direct_dispatches_mesh_session
136
+ ON mesh_direct_dispatches(mesh_id, session_id, status);
137
+
138
+ CREATE TABLE IF NOT EXISTS remote_idle_sessions (
139
+ node_id TEXT NOT NULL,
140
+ session_id TEXT NOT NULL,
141
+ provider_type TEXT NOT NULL,
142
+ expires_at INTEGER NOT NULL,
143
+ metadata TEXT,
144
+ PRIMARY KEY (node_id, session_id)
145
+ );
146
+
147
+ CREATE TABLE IF NOT EXISTS mesh_session_delivery (
148
+ id TEXT PRIMARY KEY,
149
+ mesh_id TEXT NOT NULL,
150
+ node_id TEXT,
151
+ session_id TEXT,
152
+ provider_type TEXT,
153
+ task_id TEXT,
154
+ kind TEXT NOT NULL,
155
+ priority INTEGER NOT NULL DEFAULT 0,
156
+ message TEXT NOT NULL,
157
+ status TEXT NOT NULL DEFAULT 'queued',
158
+ deliver_after TEXT,
159
+ expires_at TEXT,
160
+ attempt_count INTEGER NOT NULL DEFAULT 0,
161
+ source_coordinator_session_id TEXT,
162
+ source_coordinator_daemon_id TEXT,
163
+ last_error TEXT,
164
+ created_at TEXT NOT NULL,
165
+ updated_at TEXT NOT NULL
166
+ );
167
+
168
+ CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_mesh_status
169
+ ON mesh_session_delivery(mesh_id, status, created_at);
170
+ CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_session
171
+ ON mesh_session_delivery(mesh_id, session_id, status);
172
+ CREATE INDEX IF NOT EXISTS idx_mesh_session_delivery_task
173
+ ON mesh_session_delivery(mesh_id, task_id);
174
+
175
+ CREATE TABLE IF NOT EXISTS mesh_completion_conflicts (
176
+ id TEXT PRIMARY KEY,
177
+ mesh_id TEXT NOT NULL,
178
+ fingerprint TEXT NOT NULL,
179
+ conflicting_task_id TEXT,
180
+ conflicting_session_id TEXT,
181
+ original_task_id TEXT,
182
+ original_session_id TEXT,
183
+ event TEXT NOT NULL,
184
+ created_at TEXT NOT NULL
185
+ );
186
+
187
+ CREATE INDEX IF NOT EXISTS idx_mesh_completion_conflicts_mesh
188
+ ON mesh_completion_conflicts(mesh_id, created_at);
189
+
190
+ CREATE TABLE IF NOT EXISTS mesh_tool_call_log (
191
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
192
+ mesh_id TEXT NOT NULL,
193
+ tool TEXT NOT NULL,
194
+ session_id TEXT,
195
+ called_at INTEGER NOT NULL
196
+ );
197
+
198
+ CREATE INDEX IF NOT EXISTS idx_mesh_tool_call_log_mesh_tool_time
199
+ ON mesh_tool_call_log(mesh_id, tool, called_at);
200
+
201
+ -- G2: Event ledger — runtime source of truth for task/session lifecycle events.
202
+ -- JSONL files are retained as export/import/debug/legacy artifacts only.
203
+ CREATE TABLE IF NOT EXISTS mesh_event_ledger (
204
+ id TEXT PRIMARY KEY,
205
+ mesh_id TEXT NOT NULL,
206
+ timestamp TEXT NOT NULL,
207
+ kind TEXT NOT NULL,
208
+ node_id TEXT,
209
+ session_id TEXT,
210
+ provider_type TEXT,
211
+ payload TEXT NOT NULL DEFAULT '{}'
212
+ );
213
+
214
+ CREATE INDEX IF NOT EXISTS idx_mesh_event_ledger_mesh_time
215
+ ON mesh_event_ledger(mesh_id, timestamp);
216
+ CREATE INDEX IF NOT EXISTS idx_mesh_event_ledger_mesh_kind
217
+ ON mesh_event_ledger(mesh_id, kind, timestamp);
218
+ CREATE INDEX IF NOT EXISTS idx_mesh_event_ledger_session
219
+ ON mesh_event_ledger(mesh_id, session_id, timestamp);
220
+
221
+ -- G3: Pending coordinator event inbox — replaces <meshId>.pending-events.jsonl.
222
+ -- Coordinator drains this table on get_pending_mesh_events, then deletes drained rows.
223
+ CREATE TABLE IF NOT EXISTS mesh_pending_events (
224
+ id TEXT PRIMARY KEY,
225
+ mesh_id TEXT NOT NULL,
226
+ coordinator_daemon_id TEXT,
227
+ event TEXT NOT NULL,
228
+ payload TEXT NOT NULL DEFAULT '{}',
229
+ fingerprint TEXT,
230
+ queued_at INTEGER NOT NULL,
231
+ drained INTEGER NOT NULL DEFAULT 0,
232
+ drained_at INTEGER
233
+ );
234
+
235
+ CREATE INDEX IF NOT EXISTS idx_mesh_pending_events_mesh_drained
236
+ ON mesh_pending_events(mesh_id, drained, queued_at);
237
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_mesh_pending_events_fingerprint
238
+ ON mesh_pending_events(mesh_id, fingerprint)
239
+ WHERE fingerprint IS NOT NULL;
240
+
241
+ -- M3: persistent mission records. Plans live in the system, not in the
242
+ -- coordinator LLM's context. Progress is derived from task statuses at
243
+ -- query time (mission_id on queue tasks) — never stored here.
244
+ CREATE TABLE IF NOT EXISTS mesh_missions (
245
+ id TEXT PRIMARY KEY,
246
+ mesh_id TEXT NOT NULL,
247
+ title TEXT NOT NULL,
248
+ goal TEXT NOT NULL DEFAULT '',
249
+ status TEXT NOT NULL DEFAULT 'active',
250
+ created_at TEXT NOT NULL,
251
+ updated_at TEXT NOT NULL
252
+ );
253
+
254
+ CREATE INDEX IF NOT EXISTS idx_mesh_missions_mesh_status
255
+ ON mesh_missions(mesh_id, status, updated_at);
256
+ `);
257
+ }
258
+
259
+ hasCompletionFingerprint(fingerprint: string): boolean {
260
+ const now = Date.now();
261
+ const row = this.db
262
+ .prepare('SELECT 1 FROM mesh_completion_fingerprints WHERE fingerprint = ? AND expires_at > ?')
263
+ .get(fingerprint, now) as { 1: number } | undefined;
264
+ // Sweep expired fingerprints every 100 reads so stale rows don't accumulate
265
+ // even during read-heavy (non-write) periods when recordFingerprintSeen is idle.
266
+ if (++this.fingerprintSweepCounter >= 100) {
267
+ this.fingerprintSweepCounter = 0;
268
+ this.sweepExpiredFingerprints();
269
+ }
270
+ return row !== undefined;
271
+ }
272
+
273
+ recordCompletionFingerprint(fingerprint: string, ttlMs: number): void {
274
+ const expiresAt = Date.now() + ttlMs;
275
+ this.db.prepare('INSERT OR REPLACE INTO mesh_completion_fingerprints (fingerprint, expires_at) VALUES (?, ?)')
276
+ .run(fingerprint, expiresAt);
277
+ this.maybeCheckpointWal();
278
+ }
279
+
280
+ sweepExpiredFingerprints(): void {
281
+ this.db.prepare('DELETE FROM mesh_completion_fingerprints WHERE expires_at <= ?').run(Date.now());
282
+ }
283
+
284
+ private maybeCheckpointWal(): void {
285
+ if (++this.walWriteCounter < MeshRuntimeStore.WAL_CHECK_INTERVAL) return;
286
+ this.walWriteCounter = 0;
287
+ try {
288
+ const walPath = `${this.dbPath}-wal`;
289
+ if (!existsSync(walPath)) return;
290
+ const size = statSync(walPath).size;
291
+ if (size < MeshRuntimeStore.WAL_MAX_BYTES) return;
292
+ process.stderr.write(
293
+ `[adhdev-mesh] WAL file ${Math.round(size / 1024 / 1024)}MB exceeds threshold; forcing checkpoint\n`,
294
+ );
295
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
296
+ } catch { /* best-effort */ }
297
+ }
298
+
299
+ private ensureLegacyQueueMigrated(meshId: string): void {
300
+ if (this.migratedMeshIds.has(meshId)) return;
301
+ this.migratedMeshIds.add(meshId);
302
+
303
+ const count = this.db
304
+ .prepare('SELECT COUNT(*) AS count FROM mesh_queue WHERE mesh_id = ?')
305
+ .get(meshId) as { count: number };
306
+ if (count.count > 0) return;
307
+
308
+ const path = legacyQueuePath(meshId);
309
+ if (!existsSync(path)) return;
310
+
311
+ try {
312
+ const entries = JSON.parse(readFileSync(path, 'utf-8')) as MeshWorkQueueEntry[];
313
+ if (!Array.isArray(entries)) return;
314
+ const insert = this.db.prepare(`
315
+ INSERT OR REPLACE INTO mesh_queue (
316
+ id, mesh_id, status, target_node_id, target_session_id,
317
+ assigned_node_id, assigned_session_id, created_at, updated_at, payload
318
+ ) VALUES (
319
+ @id, @meshId, @status, @targetNodeId, @targetSessionId,
320
+ @assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload
321
+ )
322
+ `);
323
+ for (const entry of entries) {
324
+ insert.run(this.toRow(entry));
325
+ }
326
+ } catch {
327
+ return;
328
+ }
329
+ }
330
+
331
+ getQueueEntries(meshId: string, statuses?: MeshTaskStatus[]): MeshWorkQueueEntry[] {
332
+ this.ensureLegacyQueueMigrated(meshId);
333
+ if (statuses?.length) {
334
+ const placeholders = statuses.map(() => '?').join(', ');
335
+ const rows = this.db
336
+ .prepare(`SELECT payload FROM mesh_queue WHERE mesh_id = ? AND status IN (${placeholders}) ORDER BY created_at ASC`)
337
+ .all(meshId, ...statuses) as Array<{ payload: string }>;
338
+ return rows.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry);
339
+ }
340
+ const rows = this.db
341
+ .prepare('SELECT payload FROM mesh_queue WHERE mesh_id = ? ORDER BY created_at ASC')
342
+ .all(meshId) as Array<{ payload: string }>;
343
+ return rows.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry);
344
+ }
345
+
346
+ getQueueRevision(meshId: string): string {
347
+ this.ensureLegacyQueueMigrated(meshId);
348
+ const rows = this.db
349
+ .prepare('SELECT id, status, updated_at FROM mesh_queue WHERE mesh_id = ? ORDER BY id ASC')
350
+ .all(meshId) as Array<{ id: string; status: string; updated_at: string }>;
351
+ // Tab as field delimiter (UUIDs and ISO timestamps never contain tabs).
352
+ return rows.map(row => `${row.id}\t${row.status}\t${row.updated_at}`).join('\n');
353
+ }
354
+
355
+ replaceQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
356
+ const deleteStmt = this.db.prepare('DELETE FROM mesh_queue WHERE mesh_id = ?');
357
+ const insert = this.db.prepare(`
358
+ INSERT INTO mesh_queue (
359
+ id, mesh_id, status, target_node_id, target_session_id,
360
+ assigned_node_id, assigned_session_id, created_at, updated_at, payload
361
+ ) VALUES (
362
+ @id, @meshId, @status, @targetNodeId, @targetSessionId,
363
+ @assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload
364
+ )
365
+ `);
366
+ deleteStmt.run(meshId);
367
+ for (const entry of queue) insert.run(this.toRow(entry));
368
+ this.maybeCheckpointWal();
369
+ }
370
+
371
+ deleteQueue(meshId: string): void {
372
+ this.db.prepare('DELETE FROM mesh_queue WHERE mesh_id = ?').run(meshId);
373
+ this.migratedMeshIds.delete(meshId);
374
+ }
375
+
376
+ insertQueueEntry(entry: MeshWorkQueueEntry): void {
377
+ this.db.prepare(`
378
+ INSERT INTO mesh_queue (
379
+ id, mesh_id, status, target_node_id, target_session_id,
380
+ assigned_node_id, assigned_session_id, created_at, updated_at, payload
381
+ ) VALUES (
382
+ @id, @meshId, @status, @targetNodeId, @targetSessionId,
383
+ @assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload
384
+ )
385
+ `).run(this.toRow(entry));
386
+ this.maybeCheckpointWal();
387
+ }
388
+
389
+ updateQueueEntry(entry: MeshWorkQueueEntry): void {
390
+ const now = new Date().toISOString();
391
+ entry.updatedAt = now;
392
+ this.db.prepare(`
393
+ UPDATE mesh_queue SET
394
+ status = @status,
395
+ target_node_id = @targetNodeId,
396
+ target_session_id = @targetSessionId,
397
+ assigned_node_id = @assignedNodeId,
398
+ assigned_session_id = @assignedSessionId,
399
+ updated_at = @updatedAt,
400
+ payload = @payload
401
+ WHERE id = @id AND mesh_id = @meshId
402
+ `).run(this.toRow(entry));
403
+ this.maybeCheckpointWal();
404
+ }
405
+
406
+ findQueueEntryById(meshId: string, id: string): MeshWorkQueueEntry | null {
407
+ this.ensureLegacyQueueMigrated(meshId);
408
+ const row = this.db.prepare(
409
+ 'SELECT payload FROM mesh_queue WHERE id = ? AND mesh_id = ?'
410
+ ).get(id, meshId) as { payload: string } | undefined;
411
+ return row ? JSON.parse(row.payload) as MeshWorkQueueEntry : null;
412
+ }
413
+
414
+ hasActiveAssignment(meshId: string, sessionId: string, nodeId: string): boolean {
415
+ this.ensureLegacyQueueMigrated(meshId);
416
+ const row = this.db.prepare(`
417
+ SELECT 1 FROM mesh_queue
418
+ WHERE mesh_id = ? AND status = 'assigned'
419
+ AND (assigned_session_id = ? OR assigned_node_id = ?)
420
+ LIMIT 1
421
+ `).get(meshId, sessionId, nodeId);
422
+ return row !== undefined;
423
+ }
424
+
425
+ // O(1) claim: transaction ensures only one session claims a pending task
426
+ claimNextQueueTask(meshId: string, nodeId: string, sessionId: string, capabilityTags: string[] = []): MeshWorkQueueEntry | null {
427
+ return this.transaction(() => {
428
+ this.ensureLegacyQueueMigrated(meshId);
429
+ if (this.hasActiveAssignment(meshId, sessionId, nodeId)) return null;
430
+
431
+ // Priority: session-targeted > node-targeted (no session) > unconstrained
432
+ const rows = [
433
+ ...(
434
+ this.db.prepare(`
435
+ SELECT payload FROM mesh_queue
436
+ WHERE mesh_id = ? AND status = 'pending' AND target_session_id = ?
437
+ ORDER BY created_at ASC
438
+ `).all(meshId, sessionId) as Array<{ payload: string }>
439
+ ),
440
+ ...(
441
+ this.db.prepare(`
442
+ SELECT payload FROM mesh_queue
443
+ WHERE mesh_id = ? AND status = 'pending' AND target_node_id = ? AND target_session_id IS NULL
444
+ ORDER BY created_at ASC
445
+ `).all(meshId, nodeId) as Array<{ payload: string }>
446
+ ),
447
+ ...(
448
+ this.db.prepare(`
449
+ SELECT payload FROM mesh_queue
450
+ WHERE mesh_id = ? AND status = 'pending' AND target_node_id IS NULL AND target_session_id IS NULL
451
+ ORDER BY created_at ASC
452
+ `).all(meshId) as Array<{ payload: string }>
453
+ ),
454
+ ];
455
+ const candidates = rows.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry);
456
+
457
+ // M1: a task with unmet dependencies (or a system blockedReason) is not claimable.
458
+ // Resolve dependency statuses in one query over the union of referenced ids.
459
+ const depIds = [...new Set(candidates.flatMap(c => Array.isArray(c.dependsOn) ? c.dependsOn : []))];
460
+ const depStatus = new Map<string, string>();
461
+ if (depIds.length > 0) {
462
+ const placeholders = depIds.map(() => '?').join(', ');
463
+ const depRows = this.db.prepare(
464
+ `SELECT id, status FROM mesh_queue WHERE mesh_id = ? AND id IN (${placeholders})`
465
+ ).all(meshId, ...depIds) as Array<{ id: string; status: string }>;
466
+ for (const r of depRows) depStatus.set(r.id, r.status);
467
+ }
468
+ const dependenciesSatisfied = (candidate: MeshWorkQueueEntry): boolean => {
469
+ if (candidate.blockedReason) return false;
470
+ const deps = Array.isArray(candidate.dependsOn) ? candidate.dependsOn : [];
471
+ return deps.every(depId => depStatus.get(depId) === 'completed');
472
+ };
473
+
474
+ const entry = candidates.find(candidate =>
475
+ nodeSatisfiesRequiredTags(candidate.requiredTags, capabilityTags)
476
+ && dependenciesSatisfied(candidate));
477
+ if (!entry) return null;
478
+
479
+ const now = new Date().toISOString();
480
+ entry.status = 'assigned';
481
+ entry.assignedNodeId = nodeId;
482
+ entry.assignedSessionId = sessionId;
483
+ entry.dispatchTimestamp = now;
484
+ entry.updatedAt = now;
485
+
486
+ this.db.prepare(`
487
+ UPDATE mesh_queue SET
488
+ status = 'assigned', assigned_node_id = ?, assigned_session_id = ?,
489
+ updated_at = ?, payload = ?
490
+ WHERE id = ? AND mesh_id = ?
491
+ `).run(nodeId, sessionId, now, JSON.stringify(entry), entry.id, meshId);
492
+
493
+ this.maybeCheckpointWal();
494
+ return entry;
495
+ });
496
+ }
497
+
498
+ getQueueStatsByStatus(meshId: string): { status: string; count: number }[] {
499
+ this.ensureLegacyQueueMigrated(meshId);
500
+ return this.db.prepare(
501
+ `SELECT status, COUNT(*) as count FROM mesh_queue WHERE mesh_id = ? GROUP BY status`
502
+ ).all(meshId) as { status: string; count: number }[];
503
+ }
504
+
505
+ getActiveAssignmentDetails(meshId: string): Array<{ id: string; nodeId?: string; sessionId?: string; message: string }> {
506
+ this.ensureLegacyQueueMigrated(meshId);
507
+ const rows = this.db.prepare(`
508
+ SELECT assigned_node_id, assigned_session_id, payload
509
+ FROM mesh_queue WHERE mesh_id = ? AND status = 'assigned'
510
+ `).all(meshId) as Array<{ assigned_node_id: string | null; assigned_session_id: string | null; payload: string }>;
511
+ return rows.map(r => {
512
+ let id = '', message = '';
513
+ try { const e = JSON.parse(r.payload) as MeshWorkQueueEntry; id = e.id; message = e.message; } catch { /* ignore */ }
514
+ return { id, nodeId: r.assigned_node_id ?? undefined, sessionId: r.assigned_session_id ?? undefined, message };
515
+ });
516
+ }
517
+
518
+ findAssignedBySession(meshId: string, sessionId: string, occurredAtIso?: string): MeshWorkQueueEntry | null {
519
+ this.ensureLegacyQueueMigrated(meshId);
520
+ // Use updated_at (≈ dispatchTimestamp when status='assigned') for the occurredAt filter.
521
+ const sql = occurredAtIso
522
+ ? `SELECT payload FROM mesh_queue WHERE mesh_id = ? AND assigned_session_id = ? AND status = 'assigned' AND updated_at <= ? ORDER BY updated_at DESC LIMIT 1`
523
+ : `SELECT payload FROM mesh_queue WHERE mesh_id = ? AND assigned_session_id = ? AND status = 'assigned' ORDER BY updated_at DESC LIMIT 1`;
524
+ const args: string[] = occurredAtIso ? [meshId, sessionId, occurredAtIso] : [meshId, sessionId];
525
+ const row = this.db.prepare(sql).get(...args as [string, string, string?]) as { payload: string } | undefined;
526
+ return row ? JSON.parse(row.payload) as MeshWorkQueueEntry : null;
527
+ }
528
+
529
+ private toRow(entry: MeshWorkQueueEntry): Record<string, unknown> {
530
+ return {
531
+ id: entry.id,
532
+ meshId: entry.meshId,
533
+ status: entry.status,
534
+ targetNodeId: entry.targetNodeId ?? null,
535
+ targetSessionId: entry.targetSessionId ?? null,
536
+ assignedNodeId: entry.assignedNodeId ?? null,
537
+ assignedSessionId: entry.assignedSessionId ?? null,
538
+ createdAt: entry.createdAt,
539
+ updatedAt: entry.updatedAt,
540
+ payload: JSON.stringify(entry),
541
+ };
542
+ }
543
+
544
+ // ── Direct Dispatch Tracking ─────────────────────────────────────────────
545
+
546
+ insertDirectDispatch(entry: {
547
+ taskId: string;
548
+ meshId: string;
549
+ nodeId?: string;
550
+ sessionId?: string;
551
+ providerType?: string;
552
+ message: string;
553
+ taskMode?: string;
554
+ via: string;
555
+ dispatchedToIdleSession?: boolean;
556
+ dispatchedAt: string;
557
+ }): void {
558
+ const now = new Date().toISOString();
559
+ this.db.prepare(`
560
+ INSERT OR REPLACE INTO mesh_direct_dispatches
561
+ (task_id, mesh_id, node_id, session_id, provider_type, message, task_mode, via,
562
+ status, dispatched_to_idle_session, dispatched_at, updated_at)
563
+ VALUES
564
+ (@taskId, @meshId, @nodeId, @sessionId, @providerType, @message, @taskMode, @via,
565
+ 'dispatched', @dispatchedToIdle, @dispatchedAt, @updatedAt)
566
+ `).run({
567
+ taskId: entry.taskId,
568
+ meshId: entry.meshId,
569
+ nodeId: entry.nodeId ?? null,
570
+ sessionId: entry.sessionId ?? null,
571
+ providerType: entry.providerType ?? null,
572
+ message: entry.message,
573
+ taskMode: entry.taskMode ?? null,
574
+ via: entry.via,
575
+ dispatchedToIdle: entry.dispatchedToIdleSession ? 1 : 0,
576
+ dispatchedAt: entry.dispatchedAt,
577
+ updatedAt: now,
578
+ });
579
+ }
580
+
581
+ getActiveDirectDispatches(meshId: string): Array<{
582
+ taskId: string;
583
+ meshId: string;
584
+ nodeId: string | null;
585
+ sessionId: string | null;
586
+ providerType: string | null;
587
+ message: string;
588
+ taskMode: string | null;
589
+ via: string;
590
+ status: string;
591
+ dispatchedToIdleSession: boolean;
592
+ dispatchedAt: string;
593
+ updatedAt: string;
594
+ }> {
595
+ const rows = this.db.prepare(`
596
+ SELECT task_id, mesh_id, node_id, session_id, provider_type, message, task_mode, via,
597
+ status, dispatched_to_idle_session, dispatched_at, updated_at
598
+ FROM mesh_direct_dispatches
599
+ WHERE mesh_id = ? AND status NOT IN ('completed', 'failed', 'stale')
600
+ ORDER BY dispatched_at ASC
601
+ `).all(meshId) as Array<Record<string, unknown>>;
602
+ return rows.map(r => ({
603
+ taskId: r.task_id as string,
604
+ meshId: r.mesh_id as string,
605
+ nodeId: r.node_id as string | null,
606
+ sessionId: r.session_id as string | null,
607
+ providerType: r.provider_type as string | null,
608
+ message: r.message as string,
609
+ taskMode: r.task_mode as string | null,
610
+ via: r.via as string,
611
+ status: r.status as string,
612
+ dispatchedToIdleSession: (r.dispatched_to_idle_session as number) === 1,
613
+ dispatchedAt: r.dispatched_at as string,
614
+ updatedAt: r.updated_at as string,
615
+ }));
616
+ }
617
+
618
+ updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void {
619
+ if (!sessionId) return; // never update rows without a session binding
620
+ const now = new Date().toISOString();
621
+ this.db.prepare(`
622
+ UPDATE mesh_direct_dispatches
623
+ SET status = @status, updated_at = @updatedAt
624
+ WHERE mesh_id = @meshId AND session_id = @sessionId
625
+ AND session_id IS NOT NULL
626
+ AND status NOT IN ('completed', 'failed')
627
+ `).run({ status, meshId, sessionId, updatedAt: now });
628
+ }
629
+
630
+ cleanupTerminalDirectDispatches(olderThanMs: number): void {
631
+ const cutoff = new Date(Date.now() - olderThanMs).toISOString();
632
+ this.db.prepare(`
633
+ DELETE FROM mesh_direct_dispatches
634
+ WHERE status IN ('completed', 'failed', 'stale') AND updated_at < ?
635
+ `).run(cutoff);
636
+ }
637
+
638
+ deleteDirectDispatches(meshId: string): void {
639
+ this.db.prepare(`DELETE FROM mesh_direct_dispatches WHERE mesh_id = ?`).run(meshId);
640
+ }
641
+
642
+ markStaleDirectDispatches(meshId: string, olderThanMs: number): void {
643
+ const cutoff = new Date(Date.now() - olderThanMs).toISOString();
644
+ const now = new Date().toISOString();
645
+ this.db.prepare(`
646
+ UPDATE mesh_direct_dispatches
647
+ SET status = 'stale', updated_at = ?
648
+ WHERE mesh_id = ? AND status = 'dispatched' AND dispatched_at < ?
649
+ `).run(now, meshId, cutoff);
650
+ }
651
+
652
+ // ── Remote Idle Sessions ─────────────────────────────────────────────────
653
+
654
+ setRemoteIdleSession(nodeId: string, sessionId: string, providerType: string, expiresAt: number, metadata?: any): void {
655
+ this.db.prepare(`
656
+ INSERT OR REPLACE INTO remote_idle_sessions (node_id, session_id, provider_type, expires_at, metadata)
657
+ VALUES (?, ?, ?, ?, ?)
658
+ `).run(nodeId, sessionId, providerType, expiresAt, metadata ? JSON.stringify(metadata) : null);
659
+ }
660
+
661
+ getRemoteIdleSessions(): Array<{ nodeId: string; sessionId: string; providerType: string; expiresAt: number; metadata?: any }> {
662
+ const rows = this.db.prepare('SELECT node_id, session_id, provider_type, expires_at, metadata FROM remote_idle_sessions').all() as Array<any>;
663
+ return rows.map(r => ({
664
+ nodeId: r.node_id,
665
+ sessionId: r.session_id,
666
+ providerType: r.provider_type,
667
+ expiresAt: r.expires_at,
668
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
669
+ }));
670
+ }
671
+
672
+ deleteRemoteIdleSession(nodeId: string, sessionId: string): void {
673
+ this.db.prepare('DELETE FROM remote_idle_sessions WHERE node_id = ? AND session_id = ?').run(nodeId, sessionId);
674
+ }
675
+
676
+ pruneExpiredRemoteIdleSessions(): void {
677
+ this.db.prepare('DELETE FROM remote_idle_sessions WHERE expires_at <= ?').run(Date.now());
678
+ }
679
+
680
+ // ── Session Delivery Queue ───────────────────────────────────────────────
681
+
682
+ insertSessionDelivery(entry: {
683
+ id: string;
684
+ meshId: string;
685
+ nodeId?: string;
686
+ sessionId?: string;
687
+ providerType?: string;
688
+ taskId?: string;
689
+ kind: string;
690
+ priority?: number;
691
+ message: string;
692
+ status: string;
693
+ deliverAfter?: string;
694
+ expiresAt?: string;
695
+ sourceCoordinatorSessionId?: string;
696
+ sourceCoordinatorDaemonId?: string;
697
+ createdAt: string;
698
+ updatedAt: string;
699
+ }): void {
700
+ this.db.prepare(`
701
+ INSERT OR REPLACE INTO mesh_session_delivery (
702
+ id, mesh_id, node_id, session_id, provider_type, task_id, kind, priority,
703
+ message, status, deliver_after, expires_at, attempt_count,
704
+ source_coordinator_session_id, source_coordinator_daemon_id,
705
+ last_error, created_at, updated_at
706
+ ) VALUES (
707
+ @id, @meshId, @nodeId, @sessionId, @providerType, @taskId, @kind, @priority,
708
+ @message, @status, @deliverAfter, @expiresAt, 0,
709
+ @sourceCoordinatorSessionId, @sourceCoordinatorDaemonId,
710
+ NULL, @createdAt, @updatedAt
711
+ )
712
+ `).run({
713
+ id: entry.id,
714
+ meshId: entry.meshId,
715
+ nodeId: entry.nodeId ?? null,
716
+ sessionId: entry.sessionId ?? null,
717
+ providerType: entry.providerType ?? null,
718
+ taskId: entry.taskId ?? null,
719
+ kind: entry.kind,
720
+ priority: entry.priority ?? 0,
721
+ message: entry.message,
722
+ status: entry.status,
723
+ deliverAfter: entry.deliverAfter ?? null,
724
+ expiresAt: entry.expiresAt ?? null,
725
+ sourceCoordinatorSessionId: entry.sourceCoordinatorSessionId ?? null,
726
+ sourceCoordinatorDaemonId: entry.sourceCoordinatorDaemonId ?? null,
727
+ createdAt: entry.createdAt,
728
+ updatedAt: entry.updatedAt,
729
+ });
730
+ this.maybeCheckpointWal();
731
+ }
732
+
733
+ updateSessionDeliveryStatus(id: string, status: string, opts?: { lastError?: string; incrementAttempt?: boolean }): void {
734
+ const now = new Date().toISOString();
735
+ if (opts?.incrementAttempt) {
736
+ this.db.prepare(`
737
+ UPDATE mesh_session_delivery
738
+ SET status = @status, last_error = @lastError, attempt_count = attempt_count + 1, updated_at = @updatedAt
739
+ WHERE id = @id
740
+ `).run({ id, status, lastError: opts?.lastError ?? null, updatedAt: now });
741
+ } else {
742
+ this.db.prepare(`
743
+ UPDATE mesh_session_delivery
744
+ SET status = @status, last_error = @lastError, updated_at = @updatedAt
745
+ WHERE id = @id
746
+ `).run({ id, status, lastError: opts?.lastError ?? null, updatedAt: now });
747
+ }
748
+ }
749
+
750
+ getActiveSessionDeliveries(meshId: string, sessionId?: string): Array<{
751
+ id: string; meshId: string; nodeId: string | null; sessionId: string | null;
752
+ providerType: string | null; taskId: string | null; kind: string; priority: number;
753
+ message: string; status: string; deliverAfter: string | null; expiresAt: string | null;
754
+ attemptCount: number; sourceCoordinatorSessionId: string | null;
755
+ sourceCoordinatorDaemonId: string | null; lastError: string | null;
756
+ createdAt: string; updatedAt: string;
757
+ }> {
758
+ const now = new Date().toISOString();
759
+ const sql = sessionId
760
+ ? `SELECT * FROM mesh_session_delivery WHERE mesh_id = ? AND session_id = ? AND status NOT IN ('delivered','completed','failed','expired','cancelled') AND (expires_at IS NULL OR expires_at > ?) ORDER BY priority DESC, created_at ASC`
761
+ : `SELECT * FROM mesh_session_delivery WHERE mesh_id = ? AND status NOT IN ('delivered','completed','failed','expired','cancelled') AND (expires_at IS NULL OR expires_at > ?) ORDER BY priority DESC, created_at ASC`;
762
+ const rows = sessionId
763
+ ? this.db.prepare(sql).all(meshId, sessionId, now) as Array<Record<string, unknown>>
764
+ : this.db.prepare(sql).all(meshId, now) as Array<Record<string, unknown>>;
765
+ return rows.map(r => ({
766
+ id: r.id as string,
767
+ meshId: r.mesh_id as string,
768
+ nodeId: r.node_id as string | null,
769
+ sessionId: r.session_id as string | null,
770
+ providerType: r.provider_type as string | null,
771
+ taskId: r.task_id as string | null,
772
+ kind: r.kind as string,
773
+ priority: r.priority as number,
774
+ message: r.message as string,
775
+ status: r.status as string,
776
+ deliverAfter: r.deliver_after as string | null,
777
+ expiresAt: r.expires_at as string | null,
778
+ attemptCount: r.attempt_count as number,
779
+ sourceCoordinatorSessionId: r.source_coordinator_session_id as string | null,
780
+ sourceCoordinatorDaemonId: r.source_coordinator_daemon_id as string | null,
781
+ lastError: r.last_error as string | null,
782
+ createdAt: r.created_at as string,
783
+ updatedAt: r.updated_at as string,
784
+ }));
785
+ }
786
+
787
+ expireStaleSessionDeliveries(meshId: string): void {
788
+ const now = new Date().toISOString();
789
+ this.db.prepare(`
790
+ UPDATE mesh_session_delivery
791
+ SET status = 'expired', updated_at = ?
792
+ WHERE mesh_id = ? AND expires_at IS NOT NULL AND expires_at <= ?
793
+ AND status NOT IN ('delivered','completed','failed','expired','cancelled')
794
+ `).run(now, meshId, now);
795
+ }
796
+
797
+ deleteSessionDeliveries(meshId: string): void {
798
+ this.db.prepare('DELETE FROM mesh_session_delivery WHERE mesh_id = ?').run(meshId);
799
+ }
800
+
801
+ // ── Completion Conflict Diagnostics ──────────────────────────────────────
802
+
803
+ recordCompletionConflict(entry: {
804
+ id: string;
805
+ meshId: string;
806
+ fingerprint: string;
807
+ conflictingTaskId?: string;
808
+ conflictingSessionId?: string;
809
+ originalTaskId?: string;
810
+ originalSessionId?: string;
811
+ event: string;
812
+ createdAt: string;
813
+ }): void {
814
+ this.db.prepare(`
815
+ INSERT OR IGNORE INTO mesh_completion_conflicts
816
+ (id, mesh_id, fingerprint, conflicting_task_id, conflicting_session_id,
817
+ original_task_id, original_session_id, event, created_at)
818
+ VALUES (@id, @meshId, @fingerprint, @conflictingTaskId, @conflictingSessionId,
819
+ @originalTaskId, @originalSessionId, @event, @createdAt)
820
+ `).run({
821
+ id: entry.id,
822
+ meshId: entry.meshId,
823
+ fingerprint: entry.fingerprint,
824
+ conflictingTaskId: entry.conflictingTaskId ?? null,
825
+ conflictingSessionId: entry.conflictingSessionId ?? null,
826
+ originalTaskId: entry.originalTaskId ?? null,
827
+ originalSessionId: entry.originalSessionId ?? null,
828
+ event: entry.event,
829
+ createdAt: entry.createdAt,
830
+ });
831
+ this.maybeCheckpointWal();
832
+ }
833
+
834
+ getRecentCompletionConflicts(meshId: string, limitMs: number = 60 * 60 * 1000): Array<{
835
+ id: string; meshId: string; fingerprint: string; conflictingTaskId: string | null;
836
+ conflictingSessionId: string | null; originalTaskId: string | null;
837
+ originalSessionId: string | null; event: string; createdAt: string;
838
+ }> {
839
+ const cutoff = new Date(Date.now() - limitMs).toISOString();
840
+ const rows = this.db.prepare(
841
+ 'SELECT * FROM mesh_completion_conflicts WHERE mesh_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT 50'
842
+ ).all(meshId, cutoff) as Array<Record<string, unknown>>;
843
+ return rows.map(r => ({
844
+ id: r.id as string,
845
+ meshId: r.mesh_id as string,
846
+ fingerprint: r.fingerprint as string,
847
+ conflictingTaskId: r.conflicting_task_id as string | null,
848
+ conflictingSessionId: r.conflicting_session_id as string | null,
849
+ originalTaskId: r.original_task_id as string | null,
850
+ originalSessionId: r.original_session_id as string | null,
851
+ event: r.event as string,
852
+ createdAt: r.created_at as string,
853
+ }));
854
+ }
855
+
856
+ /**
857
+ * Record a mesh tool call and check whether this mesh+tool combination is
858
+ * being called too rapidly (sliding window rate guard).
859
+ *
860
+ * Returns a rate-limit advisory string when the call rate is too high, null otherwise.
861
+ * windowMs: sliding window size in ms (default 10s)
862
+ * maxCalls: max allowed calls within the window (default 5)
863
+ */
864
+ recordMeshToolCall(opts: {
865
+ meshId: string;
866
+ tool: string;
867
+ sessionId?: string | null;
868
+ windowMs?: number;
869
+ maxCalls?: number;
870
+ }): { rateLimitExceeded: boolean; callsInWindow: number; advisory: string | null } {
871
+ const { meshId, tool, sessionId = null } = opts;
872
+ const windowMs = opts.windowMs ?? 10_000;
873
+ const maxCalls = opts.maxCalls ?? 5;
874
+ const now = Date.now();
875
+ const windowStart = now - windowMs;
876
+
877
+ this.db.prepare(
878
+ 'INSERT INTO mesh_tool_call_log (mesh_id, tool, session_id, called_at) VALUES (?, ?, ?, ?)'
879
+ ).run(meshId, tool, sessionId, now);
880
+
881
+ const row = this.db.prepare(
882
+ 'SELECT COUNT(*) as cnt FROM mesh_tool_call_log WHERE mesh_id = ? AND tool = ? AND called_at >= ?'
883
+ ).get(meshId, tool, windowStart) as { cnt: number };
884
+ const callsInWindow = row?.cnt ?? 0;
885
+
886
+ // Sweep old entries periodically to keep the table lean (every 200 calls across all tools).
887
+ if (++this.walWriteCounter % 200 === 0) {
888
+ this.db.prepare(
889
+ 'DELETE FROM mesh_tool_call_log WHERE called_at < ?'
890
+ ).run(now - Math.max(windowMs * 10, 60_000));
891
+ }
892
+
893
+ if (callsInWindow > maxCalls) {
894
+ const advisory = `Rate limit: ${tool} called ${callsInWindow} times in the last ${windowMs / 1000}s for mesh ${meshId}. `
895
+ + `Wait for pendingCoordinatorEvents or an explicit user status request before calling again.`;
896
+ return { rateLimitExceeded: true, callsInWindow, advisory };
897
+ }
898
+ return { rateLimitExceeded: false, callsInWindow, advisory: null };
899
+ }
900
+
901
+ /**
902
+ * Prune tool call log entries older than the given age in ms.
903
+ * Exposed for testing.
904
+ */
905
+ pruneToolCallLog(olderThanMs: number): void {
906
+ this.db.prepare('DELETE FROM mesh_tool_call_log WHERE called_at < ?').run(Date.now() - olderThanMs);
907
+ }
908
+
909
+ // ── G2: Event Ledger ────────────────────────────────────────────────────
910
+
911
+ appendLedgerEntry(entry: {
912
+ id: string;
913
+ meshId: string;
914
+ timestamp: string;
915
+ kind: string;
916
+ nodeId?: string | null;
917
+ sessionId?: string | null;
918
+ providerType?: string | null;
919
+ payload?: unknown;
920
+ }): void {
921
+ this.db.prepare(
922
+ `INSERT OR IGNORE INTO mesh_event_ledger
923
+ (id, mesh_id, timestamp, kind, node_id, session_id, provider_type, payload)
924
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
925
+ ).run(
926
+ entry.id,
927
+ entry.meshId,
928
+ entry.timestamp,
929
+ entry.kind,
930
+ entry.nodeId ?? null,
931
+ entry.sessionId ?? null,
932
+ entry.providerType ?? null,
933
+ JSON.stringify(entry.payload ?? {}),
934
+ );
935
+ this.maybeCheckpointWal();
936
+ }
937
+
938
+ readLedgerEntries(meshId: string, opts?: {
939
+ tail?: number;
940
+ since?: string;
941
+ kind?: string;
942
+ limit?: number;
943
+ }): Array<{ id: string; meshId: string; timestamp: string; kind: string; nodeId: string | null; sessionId: string | null; providerType: string | null; payload: unknown }> {
944
+ const limit = opts?.tail ?? opts?.limit ?? 200;
945
+ let query: string;
946
+ const params: unknown[] = [meshId];
947
+ if (opts?.kind && opts?.since) {
948
+ query = `SELECT * FROM mesh_event_ledger WHERE mesh_id = ? AND kind = ? AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?`;
949
+ params.push(opts.kind, opts.since, limit);
950
+ } else if (opts?.kind) {
951
+ query = `SELECT * FROM mesh_event_ledger WHERE mesh_id = ? AND kind = ? ORDER BY timestamp DESC LIMIT ?`;
952
+ params.push(opts.kind, limit);
953
+ } else if (opts?.since) {
954
+ query = `SELECT * FROM mesh_event_ledger WHERE mesh_id = ? AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?`;
955
+ params.push(opts.since, limit);
956
+ } else {
957
+ query = `SELECT * FROM mesh_event_ledger WHERE mesh_id = ? ORDER BY timestamp DESC LIMIT ?`;
958
+ params.push(limit);
959
+ }
960
+ const rows = this.db.prepare(query).all(...params) as Array<Record<string, unknown>>;
961
+ return rows.map(r => ({
962
+ id: r.id as string,
963
+ meshId: r.mesh_id as string,
964
+ timestamp: r.timestamp as string,
965
+ kind: r.kind as string,
966
+ nodeId: r.node_id as string | null,
967
+ sessionId: r.session_id as string | null,
968
+ providerType: r.provider_type as string | null,
969
+ payload: (() => { try { return JSON.parse(r.payload as string); } catch { return {}; } })(),
970
+ }));
971
+ }
972
+
973
+ /**
974
+ * G2 read cutover: read ledger entries in append order (oldest first),
975
+ * matching legacy JSONL file-order semantics. Ties on the same timestamp
976
+ * are broken by rowid (insertion order), preserving the positional
977
+ * guarantee that mesh-events relies on for same-millisecond entries.
978
+ */
979
+ readLedgerEntriesOrdered(meshId: string, opts?: {
980
+ since?: string;
981
+ kinds?: string[];
982
+ tail?: number;
983
+ }): Array<{ id: string; meshId: string; timestamp: string; kind: string; nodeId: string | null; sessionId: string | null; providerType: string | null; payload: unknown }> {
984
+ const params: unknown[] = [meshId];
985
+ let whereClause = 'mesh_id = ?';
986
+ if (opts?.since) {
987
+ whereClause += ' AND timestamp >= ?';
988
+ params.push(opts.since);
989
+ }
990
+ const kinds = Array.isArray(opts?.kinds) ? opts.kinds.filter(k => typeof k === 'string' && k.trim()) : [];
991
+ if (kinds.length > 0) {
992
+ whereClause += ` AND kind IN (${kinds.map(() => '?').join(', ')})`;
993
+ params.push(...kinds);
994
+ }
995
+ let query: string;
996
+ if (opts?.tail && opts.tail > 0) {
997
+ // Tail: newest N in append order — inner DESC limit, outer re-sort ASC.
998
+ query = `SELECT * FROM (
999
+ SELECT rowid AS rid, * FROM mesh_event_ledger WHERE ${whereClause}
1000
+ ORDER BY timestamp DESC, rowid DESC LIMIT ?
1001
+ ) ORDER BY timestamp ASC, rid ASC`;
1002
+ params.push(Math.floor(opts.tail));
1003
+ } else {
1004
+ query = `SELECT rowid AS rid, * FROM mesh_event_ledger WHERE ${whereClause} ORDER BY timestamp ASC, rowid ASC`;
1005
+ }
1006
+ const rows = this.db.prepare(query).all(...params) as Array<Record<string, unknown>>;
1007
+ return rows.map(r => ({
1008
+ id: r.id as string,
1009
+ meshId: r.mesh_id as string,
1010
+ timestamp: r.timestamp as string,
1011
+ kind: r.kind as string,
1012
+ nodeId: r.node_id as string | null,
1013
+ sessionId: r.session_id as string | null,
1014
+ providerType: r.provider_type as string | null,
1015
+ payload: (() => { try { return JSON.parse(r.payload as string); } catch { return {}; } })(),
1016
+ }));
1017
+ }
1018
+
1019
+ /** Remove all ledger entries for a mesh (mesh deletion / test cleanup). */
1020
+ clearLedgerForMesh(meshId: string): number {
1021
+ return this.db.prepare('DELETE FROM mesh_event_ledger WHERE mesh_id = ?').run(meshId).changes;
1022
+ }
1023
+
1024
+ /** G2: remove entries moved to the JSONL archive so the SQLite runtime set mirrors the active ledger. */
1025
+ deleteLedgerEntries(meshId: string, ids: string[]): number {
1026
+ if (!ids.length) return 0;
1027
+ let deleted = 0;
1028
+ const stmt = this.db.prepare('DELETE FROM mesh_event_ledger WHERE mesh_id = ? AND id = ?');
1029
+ this.db.transaction(() => {
1030
+ for (const id of ids) {
1031
+ deleted += stmt.run(meshId, id).changes;
1032
+ }
1033
+ })();
1034
+ return deleted;
1035
+ }
1036
+
1037
+ hasLedgerEntry(meshId: string, id: string): boolean {
1038
+ const row = this.db.prepare(
1039
+ 'SELECT 1 FROM mesh_event_ledger WHERE mesh_id = ? AND id = ? LIMIT 1'
1040
+ ).get(meshId, id);
1041
+ return row !== undefined;
1042
+ }
1043
+
1044
+ ledgerEntryCount(meshId: string): number {
1045
+ const row = this.db.prepare(
1046
+ 'SELECT COUNT(*) as cnt FROM mesh_event_ledger WHERE mesh_id = ?'
1047
+ ).get(meshId) as { cnt: number } | undefined;
1048
+ return row?.cnt ?? 0;
1049
+ }
1050
+
1051
+ importLedgerEntries(entries: Array<{
1052
+ id: string; meshId: string; timestamp: string; kind: string;
1053
+ nodeId?: string | null; sessionId?: string | null; providerType?: string | null; payload?: unknown;
1054
+ }>): number {
1055
+ let imported = 0;
1056
+ const stmt = this.db.prepare(
1057
+ `INSERT OR IGNORE INTO mesh_event_ledger
1058
+ (id, mesh_id, timestamp, kind, node_id, session_id, provider_type, payload)
1059
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1060
+ );
1061
+ this.db.transaction(() => {
1062
+ for (const e of entries) {
1063
+ const result = stmt.run(
1064
+ e.id, e.meshId, e.timestamp, e.kind,
1065
+ e.nodeId ?? null, e.sessionId ?? null, e.providerType ?? null,
1066
+ JSON.stringify(e.payload ?? {}),
1067
+ );
1068
+ if (result.changes > 0) imported++;
1069
+ }
1070
+ })();
1071
+ return imported;
1072
+ }
1073
+
1074
+ /**
1075
+ * G4: Read a bounded, cursor-addressable ledger slice directly from the SQLite
1076
+ * mesh_event_ledger table. This is the P2P reconcile read path; JSONL files are
1077
+ * retained as export/import/debug/legacy artifacts only.
1078
+ *
1079
+ * The return shape is structurally compatible with MeshLedgerSlice so callers
1080
+ * in mesh-tools.ts can pass it directly to buildMeshLedgerReplicaEvidence.
1081
+ */
1082
+ readLedgerSlice(meshId: string, opts?: {
1083
+ afterId?: string;
1084
+ since?: string;
1085
+ kind?: string;
1086
+ limit?: number;
1087
+ }): {
1088
+ protocol: 'adhdev.mesh.ledger.slice.v1';
1089
+ meshId: string;
1090
+ entries: Array<{ id: string; meshId: string; timestamp: string; kind: string; nodeId: string | null; sessionId: string | null; providerType: string | null; payload: unknown }>;
1091
+ cursor: { afterId: string | null; nextAfterId: string | null; limit: number; hasMore: boolean };
1092
+ sourceOfTruth: { kind: 'local_sqlite'; table: 'mesh_event_ledger'; bounded: true; maxLimit: number };
1093
+ } {
1094
+ // Protocol maximum of 500, default 100 — mirrors mesh-ledger.ts constants.
1095
+ const MAX_LIMIT = 500;
1096
+ const DEFAULT_LIMIT = 100;
1097
+ const limit = (typeof opts?.limit === 'number' && Number.isFinite(opts.limit))
1098
+ ? Math.max(1, Math.min(MAX_LIMIT, Math.floor(opts.limit)))
1099
+ : DEFAULT_LIMIT;
1100
+
1101
+ const afterId = typeof opts?.afterId === 'string' && opts.afterId.trim() ? opts.afterId.trim() : null;
1102
+
1103
+ // Build query: fetch limit+1 rows so we can detect hasMore without a COUNT(*).
1104
+ const params: unknown[] = [meshId];
1105
+ let whereClause = 'mesh_id = ?';
1106
+
1107
+ if (opts?.kind) {
1108
+ whereClause += ' AND kind = ?';
1109
+ params.push(opts.kind);
1110
+ }
1111
+ if (opts?.since) {
1112
+ whereClause += ' AND timestamp >= ?';
1113
+ params.push(opts.since);
1114
+ }
1115
+ if (afterId) {
1116
+ // afterId: return entries with timestamp strictly after the referenced entry's timestamp,
1117
+ // or with the same timestamp but id > afterId (stable pagination).
1118
+ whereClause += ` AND (timestamp > (SELECT timestamp FROM mesh_event_ledger WHERE id = ? AND mesh_id = ?) OR (timestamp = (SELECT timestamp FROM mesh_event_ledger WHERE id = ? AND mesh_id = ?) AND id > ?))`;
1119
+ params.push(afterId, meshId, afterId, meshId, afterId);
1120
+ }
1121
+
1122
+ // Fetch limit+1 to detect hasMore
1123
+ const query = `SELECT * FROM mesh_event_ledger WHERE ${whereClause} ORDER BY timestamp ASC, id ASC LIMIT ?`;
1124
+ params.push(limit + 1);
1125
+
1126
+ const rows = this.db.prepare(query).all(...params) as Array<Record<string, unknown>>;
1127
+ const hasMore = rows.length > limit;
1128
+ const bounded = hasMore ? rows.slice(0, limit) : rows;
1129
+
1130
+ const entries = bounded.map(r => ({
1131
+ id: r.id as string,
1132
+ meshId: r.mesh_id as string,
1133
+ timestamp: r.timestamp as string,
1134
+ kind: r.kind as string,
1135
+ nodeId: r.node_id as string | null,
1136
+ sessionId: r.session_id as string | null,
1137
+ providerType: r.provider_type as string | null,
1138
+ payload: (() => { try { return JSON.parse(r.payload as string); } catch { return {}; } })(),
1139
+ }));
1140
+
1141
+ return {
1142
+ protocol: 'adhdev.mesh.ledger.slice.v1',
1143
+ meshId,
1144
+ entries,
1145
+ cursor: {
1146
+ afterId,
1147
+ nextAfterId: entries.length ? entries[entries.length - 1].id : afterId,
1148
+ limit,
1149
+ hasMore,
1150
+ },
1151
+ sourceOfTruth: {
1152
+ kind: 'local_sqlite',
1153
+ table: 'mesh_event_ledger',
1154
+ bounded: true,
1155
+ maxLimit: MAX_LIMIT,
1156
+ },
1157
+ };
1158
+ }
1159
+
1160
+ // ── G3: Pending Coordinator Events ──────────────────────────────────────
1161
+
1162
+ insertPendingEvent(event: {
1163
+ id: string;
1164
+ meshId: string;
1165
+ coordinatorDaemonId?: string | null;
1166
+ event: string;
1167
+ payload?: unknown;
1168
+ fingerprint?: string | null;
1169
+ queuedAt: number;
1170
+ }): boolean {
1171
+ const result = this.db.prepare(
1172
+ `INSERT OR IGNORE INTO mesh_pending_events
1173
+ (id, mesh_id, coordinator_daemon_id, event, payload, fingerprint, queued_at)
1174
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
1175
+ ).run(
1176
+ event.id,
1177
+ event.meshId,
1178
+ event.coordinatorDaemonId ?? null,
1179
+ event.event,
1180
+ JSON.stringify(event.payload ?? {}),
1181
+ event.fingerprint ?? null,
1182
+ event.queuedAt,
1183
+ );
1184
+ this.maybeCheckpointWal();
1185
+ return result.changes > 0;
1186
+ }
1187
+
1188
+ drainPendingEvents(meshId: string, coordinatorDaemonId?: string | null): Array<{ id: string; event: string; payload: unknown }> {
1189
+ return this.transaction(() => {
1190
+ const whereClause = coordinatorDaemonId
1191
+ ? `WHERE mesh_id = ? AND drained = 0 AND (coordinator_daemon_id IS NULL OR coordinator_daemon_id = ?)`
1192
+ : `WHERE mesh_id = ? AND drained = 0`;
1193
+ const params: unknown[] = coordinatorDaemonId
1194
+ ? [meshId, coordinatorDaemonId]
1195
+ : [meshId];
1196
+ const rows = this.db.prepare(
1197
+ `SELECT id, event, payload FROM mesh_pending_events ${whereClause} ORDER BY queued_at ASC LIMIT 100`
1198
+ ).all(...params) as Array<{ id: string; event: string; payload: string }>;
1199
+ if (rows.length === 0) return [];
1200
+ const ids = rows.map(r => r.id);
1201
+ const now = Date.now();
1202
+ this.db.prepare(
1203
+ `UPDATE mesh_pending_events SET drained = 1, drained_at = ? WHERE id IN (${ids.map(() => '?').join(',')})`
1204
+ ).run(now, ...ids);
1205
+ return rows.map(r => ({
1206
+ id: r.id,
1207
+ event: r.event,
1208
+ payload: (() => { try { return JSON.parse(r.payload); } catch { return {}; } })(),
1209
+ }));
1210
+ });
1211
+ }
1212
+
1213
+ /** Non-destructive peek — returns undrained events without marking them drained. */
1214
+ peekPendingEvents(meshId: string, coordinatorDaemonId?: string | null): Array<{ id: string; event: string; payload: unknown }> {
1215
+ const whereClause = coordinatorDaemonId
1216
+ ? `WHERE mesh_id = ? AND drained = 0 AND (coordinator_daemon_id IS NULL OR coordinator_daemon_id = ?)`
1217
+ : `WHERE mesh_id = ? AND drained = 0`;
1218
+ const params: unknown[] = coordinatorDaemonId ? [meshId, coordinatorDaemonId] : [meshId];
1219
+ const rows = this.db.prepare(
1220
+ `SELECT id, event, payload FROM mesh_pending_events ${whereClause} ORDER BY queued_at ASC LIMIT 100`
1221
+ ).all(...params) as Array<{ id: string; event: string; payload: string }>;
1222
+ return rows.map(r => ({
1223
+ id: r.id,
1224
+ event: r.event,
1225
+ payload: (() => { try { return JSON.parse(r.payload); } catch { return {}; } })(),
1226
+ }));
1227
+ }
1228
+
1229
+ hasPendingEventFingerprint(meshId: string, fingerprint: string): boolean {
1230
+ const row = this.db.prepare(
1231
+ 'SELECT 1 FROM mesh_pending_events WHERE mesh_id = ? AND fingerprint = ? AND drained = 0 LIMIT 1'
1232
+ ).get(meshId, fingerprint);
1233
+ return row !== undefined;
1234
+ }
1235
+
1236
+ // ── M3: Mission Records ─────────────────────────────────────────────────
1237
+
1238
+ upsertMission(mission: {
1239
+ id: string;
1240
+ meshId: string;
1241
+ title: string;
1242
+ goal?: string;
1243
+ status?: string;
1244
+ }): void {
1245
+ const now = new Date().toISOString();
1246
+ this.db.prepare(
1247
+ `INSERT INTO mesh_missions (id, mesh_id, title, goal, status, created_at, updated_at)
1248
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1249
+ ON CONFLICT(id) DO UPDATE SET
1250
+ title = excluded.title,
1251
+ goal = excluded.goal,
1252
+ status = excluded.status,
1253
+ updated_at = excluded.updated_at`
1254
+ ).run(
1255
+ mission.id,
1256
+ mission.meshId,
1257
+ mission.title,
1258
+ mission.goal ?? '',
1259
+ mission.status ?? 'active',
1260
+ now,
1261
+ now,
1262
+ );
1263
+ this.maybeCheckpointWal();
1264
+ }
1265
+
1266
+ getMission(meshId: string, missionId: string): { id: string; meshId: string; title: string; goal: string; status: string; createdAt: string; updatedAt: string } | null {
1267
+ const row = this.db.prepare(
1268
+ 'SELECT * FROM mesh_missions WHERE mesh_id = ? AND id = ?'
1269
+ ).get(meshId, missionId) as Record<string, string> | undefined;
1270
+ if (!row) return null;
1271
+ return { id: row.id, meshId: row.mesh_id, title: row.title, goal: row.goal, status: row.status, createdAt: row.created_at, updatedAt: row.updated_at };
1272
+ }
1273
+
1274
+ getMissions(meshId: string, statuses?: string[]): Array<{ id: string; meshId: string; title: string; goal: string; status: string; createdAt: string; updatedAt: string }> {
1275
+ let rows: Array<Record<string, string>>;
1276
+ if (statuses?.length) {
1277
+ const placeholders = statuses.map(() => '?').join(', ');
1278
+ rows = this.db.prepare(
1279
+ `SELECT * FROM mesh_missions WHERE mesh_id = ? AND status IN (${placeholders}) ORDER BY updated_at DESC`
1280
+ ).all(meshId, ...statuses) as Array<Record<string, string>>;
1281
+ } else {
1282
+ rows = this.db.prepare(
1283
+ 'SELECT * FROM mesh_missions WHERE mesh_id = ? ORDER BY updated_at DESC'
1284
+ ).all(meshId) as Array<Record<string, string>>;
1285
+ }
1286
+ return rows.map(row => ({ id: row.id, meshId: row.mesh_id, title: row.title, goal: row.goal, status: row.status, createdAt: row.created_at, updatedAt: row.updated_at }));
1287
+ }
1288
+
1289
+ /** Remove all missions for a mesh — mesh deletion / test cleanup. */
1290
+ clearMissionsForMesh(meshId: string): number {
1291
+ return this.db.prepare('DELETE FROM mesh_missions WHERE mesh_id = ?').run(meshId).changes;
1292
+ }
1293
+
1294
+ /** Remove all pending-event rows (drained included) for a mesh — mesh deletion / test cleanup. */
1295
+ clearPendingEventsForMesh(meshId: string): number {
1296
+ return this.db.prepare('DELETE FROM mesh_pending_events WHERE mesh_id = ?').run(meshId).changes;
1297
+ }
1298
+
1299
+ pendingEventCount(meshId: string): number {
1300
+ const row = this.db.prepare(
1301
+ 'SELECT COUNT(*) as cnt FROM mesh_pending_events WHERE mesh_id = ? AND drained = 0'
1302
+ ).get(meshId) as { cnt: number } | undefined;
1303
+ return row?.cnt ?? 0;
1304
+ }
1305
+ }