@adhdev/daemon-core 0.9.82-rc.209 → 0.9.82-rc.210

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 (64) hide show
  1. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +2 -0
  2. package/dist/commands/router.d.ts +6 -0
  3. package/dist/git/git-commands.d.ts +2 -0
  4. package/dist/git/git-diff.d.ts +6 -0
  5. package/dist/index.d.ts +11 -5
  6. package/dist/index.js +5699 -3486
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5679 -3483
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/mesh/coordinator-prompt.d.ts +6 -0
  11. package/dist/mesh/mesh-delivery-policy.d.ts +5 -0
  12. package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
  13. package/dist/mesh/mesh-events-pending.d.ts +33 -0
  14. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  15. package/dist/mesh/mesh-events-utils.d.ts +14 -0
  16. package/dist/mesh/mesh-events.d.ts +5 -198
  17. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  18. package/dist/mesh/mesh-ledger.d.ts +19 -0
  19. package/dist/mesh/mesh-missions.d.ts +58 -0
  20. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  21. package/dist/mesh/mesh-runtime-store.d.ts +175 -0
  22. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  23. package/dist/mesh/mesh-work-queue.d.ts +82 -0
  24. package/dist/mesh/refine-config.d.ts +24 -2
  25. package/dist/mesh/worktree-bootstrap-config.d.ts +22 -0
  26. package/dist/providers/acp-provider-instance.d.ts +2 -0
  27. package/dist/providers/spec/driver.d.ts +8 -0
  28. package/dist/providers/spec/evaluator.d.ts +4 -5
  29. package/dist/providers/spec/loader.d.ts +1 -0
  30. package/dist/providers/spec/schema.gen.d.ts +1409 -6
  31. package/dist/providers/spec/types.d.ts +188 -175
  32. package/dist/repo-mesh-types.d.ts +1 -0
  33. package/package.json +1 -1
  34. package/src/boot/daemon-lifecycle.ts +3 -0
  35. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +20 -7
  36. package/src/commands/router.ts +594 -66
  37. package/src/git/git-commands.ts +5 -5
  38. package/src/git/git-diff.ts +53 -0
  39. package/src/index.ts +11 -5
  40. package/src/mesh/coordinator-prompt.ts +14 -1
  41. package/src/mesh/mesh-delivery-policy.ts +17 -0
  42. package/src/mesh/mesh-events-coordinator.ts +1404 -0
  43. package/src/mesh/mesh-events-pending.ts +371 -0
  44. package/src/mesh/mesh-events-stale.ts +283 -0
  45. package/src/mesh/mesh-events-utils.ts +161 -0
  46. package/src/mesh/mesh-events.ts +27 -2143
  47. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  48. package/src/mesh/mesh-ledger.ts +134 -2
  49. package/src/mesh/mesh-missions.ts +151 -0
  50. package/src/mesh/mesh-review-inbox.ts +307 -0
  51. package/src/mesh/mesh-runtime-store.ts +539 -3
  52. package/src/mesh/mesh-task-stats.ts +154 -0
  53. package/src/mesh/mesh-work-queue.ts +233 -17
  54. package/src/mesh/refine-config.ts +42 -5
  55. package/src/mesh/worktree-bootstrap-config.ts +79 -0
  56. package/src/providers/acp-provider-instance.ts +15 -1
  57. package/src/providers/cli-provider-instance.ts +34 -13
  58. package/src/providers/spec/driver.ts +57 -29
  59. package/src/providers/spec/evaluator.ts +302 -112
  60. package/src/providers/spec/loader.ts +226 -37
  61. package/src/providers/spec/schema.gen.ts +450 -334
  62. package/src/providers/spec/schema.json +162 -75
  63. package/src/providers/spec/types.ts +234 -183
  64. package/src/repo-mesh-types.ts +1 -0
@@ -186,6 +186,73 @@ export class MeshRuntimeStore {
186
186
 
187
187
  CREATE INDEX IF NOT EXISTS idx_mesh_completion_conflicts_mesh
188
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);
189
256
  `);
190
257
  }
191
258
 
@@ -385,9 +452,28 @@ export class MeshRuntimeStore {
385
452
  `).all(meshId) as Array<{ payload: string }>
386
453
  ),
387
454
  ];
388
- const entry = rows
389
- .map(row => JSON.parse(row.payload) as MeshWorkQueueEntry)
390
- .find(candidate => nodeSatisfiesRequiredTags(candidate.requiredTags, capabilityTags));
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));
391
477
  if (!entry) return null;
392
478
 
393
479
  const now = new Date().toISOString();
@@ -766,4 +852,454 @@ export class MeshRuntimeStore {
766
852
  createdAt: r.created_at as string,
767
853
  }));
768
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
+ }
769
1305
  }