@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.
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +2 -0
- package/dist/commands/router.d.ts +6 -0
- package/dist/git/git-commands.d.ts +2 -0
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +5699 -3486
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5679 -3483
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +6 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +5 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
- package/dist/mesh/mesh-events-pending.d.ts +33 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +14 -0
- package/dist/mesh/mesh-events.d.ts +5 -198
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +19 -0
- package/dist/mesh/mesh-missions.d.ts +58 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-runtime-store.d.ts +175 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +82 -0
- package/dist/mesh/refine-config.d.ts +24 -2
- package/dist/mesh/worktree-bootstrap-config.d.ts +22 -0
- package/dist/providers/acp-provider-instance.d.ts +2 -0
- package/dist/providers/spec/driver.d.ts +8 -0
- package/dist/providers/spec/evaluator.d.ts +4 -5
- package/dist/providers/spec/loader.d.ts +1 -0
- package/dist/providers/spec/schema.gen.d.ts +1409 -6
- package/dist/providers/spec/types.d.ts +188 -175
- package/dist/repo-mesh-types.d.ts +1 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +3 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +20 -7
- package/src/commands/router.ts +594 -66
- package/src/git/git-commands.ts +5 -5
- package/src/git/git-diff.ts +53 -0
- package/src/index.ts +11 -5
- package/src/mesh/coordinator-prompt.ts +14 -1
- package/src/mesh/mesh-delivery-policy.ts +17 -0
- package/src/mesh/mesh-events-coordinator.ts +1404 -0
- package/src/mesh/mesh-events-pending.ts +371 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +161 -0
- package/src/mesh/mesh-events.ts +27 -2143
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +134 -2
- package/src/mesh/mesh-missions.ts +151 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-runtime-store.ts +539 -3
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +233 -17
- package/src/mesh/refine-config.ts +42 -5
- package/src/mesh/worktree-bootstrap-config.ts +79 -0
- package/src/providers/acp-provider-instance.ts +15 -1
- package/src/providers/cli-provider-instance.ts +34 -13
- package/src/providers/spec/driver.ts +57 -29
- package/src/providers/spec/evaluator.ts +302 -112
- package/src/providers/spec/loader.ts +226 -37
- package/src/providers/spec/schema.gen.ts +450 -334
- package/src/providers/spec/schema.json +162 -75
- package/src/providers/spec/types.ts +234 -183
- 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
|
|
389
|
-
|
|
390
|
-
|
|
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
|
}
|