@adhdev/daemon-core 0.9.77-rc.47 → 0.9.77-rc.49
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/index.d.ts +4 -2
- package/dist/index.js +171 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +166 -24
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +55 -0
- package/dist/mesh/mesh-ledger.d.ts +46 -4
- package/dist/mesh/mesh-sync.d.ts +4 -12
- package/package.json +1 -1
- package/src/commands/router.ts +35 -0
- package/src/index.ts +4 -2
- package/src/mesh/mesh-ledger-reconciliation.ts +115 -0
- package/src/mesh/mesh-ledger.ts +120 -7
- package/src/mesh/mesh-sync.ts +4 -34
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AppendRemoteLedgerResult, MeshLedgerSlice, MeshLedgerSummary } from './mesh-ledger.js';
|
|
2
|
+
export type MeshLedgerReplicaStatus = 'local' | 'queried' | 'imported' | 'failed';
|
|
3
|
+
export interface MeshLedgerReplicaEvidence {
|
|
4
|
+
nodeId: string;
|
|
5
|
+
daemonId?: string;
|
|
6
|
+
status: MeshLedgerReplicaStatus;
|
|
7
|
+
transport: 'local' | 'p2p_datachannel';
|
|
8
|
+
protocol: 'adhdev.mesh.ledger.slice.v1';
|
|
9
|
+
entriesReceived: number;
|
|
10
|
+
entriesImported: number;
|
|
11
|
+
skippedDuplicate: number;
|
|
12
|
+
rejectedInvalid: number;
|
|
13
|
+
hasMore: boolean;
|
|
14
|
+
nextAfterId: string | null;
|
|
15
|
+
lastTimestamp: string | null;
|
|
16
|
+
summary?: MeshLedgerSummary;
|
|
17
|
+
error?: string;
|
|
18
|
+
noFallbackReason?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface MeshLedgerReconciliationEvidence {
|
|
21
|
+
protocol: 'adhdev.mesh.ledger.reconciliation.v1';
|
|
22
|
+
meshId: string;
|
|
23
|
+
generatedAt: string;
|
|
24
|
+
sourceOfTruth: {
|
|
25
|
+
kind: 'coordinator_local_jsonl';
|
|
26
|
+
p2pOnly: true;
|
|
27
|
+
cloudD1LedgerSync: false;
|
|
28
|
+
notes: string;
|
|
29
|
+
};
|
|
30
|
+
replicas: MeshLedgerReplicaEvidence[];
|
|
31
|
+
totals: {
|
|
32
|
+
replicas: number;
|
|
33
|
+
queried: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
entriesReceived: number;
|
|
36
|
+
entriesImported: number;
|
|
37
|
+
skippedDuplicate: number;
|
|
38
|
+
rejectedInvalid: number;
|
|
39
|
+
};
|
|
40
|
+
convergence: {
|
|
41
|
+
complete: boolean;
|
|
42
|
+
pendingNodes: string[];
|
|
43
|
+
failedNodes: string[];
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export declare function buildMeshLedgerReplicaEvidence(args: {
|
|
47
|
+
nodeId: string;
|
|
48
|
+
daemonId?: string;
|
|
49
|
+
transport: 'local' | 'p2p_datachannel';
|
|
50
|
+
slice?: MeshLedgerSlice;
|
|
51
|
+
importResult?: AppendRemoteLedgerResult;
|
|
52
|
+
status?: MeshLedgerReplicaStatus;
|
|
53
|
+
error?: string;
|
|
54
|
+
}): MeshLedgerReplicaEvidence;
|
|
55
|
+
export declare function buildMeshLedgerReconciliationEvidence(meshId: string, replicas: MeshLedgerReplicaEvidence[]): MeshLedgerReconciliationEvidence;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Safety: mode 0o600, atomic append via appendFileSync
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
|
-
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_removed' | 'coordinator_started' | 'recovery_attempted';
|
|
16
|
+
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled';
|
|
17
17
|
export interface MeshLedgerEntry {
|
|
18
18
|
id: string;
|
|
19
19
|
meshId: string;
|
|
@@ -41,6 +41,42 @@ export interface ReadLedgerOptions {
|
|
|
41
41
|
since?: string;
|
|
42
42
|
kind?: MeshLedgerKind[];
|
|
43
43
|
}
|
|
44
|
+
export interface ReadLedgerSliceOptions {
|
|
45
|
+
/** Return entries strictly after this entry id. If not found, starts from the beginning of the filtered set. */
|
|
46
|
+
afterId?: string;
|
|
47
|
+
/** Return entries at or after this timestamp. */
|
|
48
|
+
since?: string;
|
|
49
|
+
/** Optional event kind filter. */
|
|
50
|
+
kind?: MeshLedgerKind[];
|
|
51
|
+
/** Maximum entries to return. Clamped to a bounded protocol maximum. */
|
|
52
|
+
limit?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface MeshLedgerCursor {
|
|
55
|
+
afterId: string | null;
|
|
56
|
+
nextAfterId: string | null;
|
|
57
|
+
limit: number;
|
|
58
|
+
hasMore: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface MeshLedgerSlice {
|
|
61
|
+
protocol: 'adhdev.mesh.ledger.slice.v1';
|
|
62
|
+
meshId: string;
|
|
63
|
+
entries: MeshLedgerEntry[];
|
|
64
|
+
cursor: MeshLedgerCursor;
|
|
65
|
+
summary: MeshLedgerSummary;
|
|
66
|
+
sourceOfTruth: {
|
|
67
|
+
kind: 'local_jsonl';
|
|
68
|
+
path: string;
|
|
69
|
+
bounded: true;
|
|
70
|
+
maxLimit: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface AppendRemoteLedgerResult {
|
|
74
|
+
accepted: number;
|
|
75
|
+
skippedDuplicate: number;
|
|
76
|
+
rejectedInvalid: number;
|
|
77
|
+
entries: MeshLedgerEntry[];
|
|
78
|
+
}
|
|
79
|
+
export declare const MAX_LEDGER_SLICE_LIMIT = 500;
|
|
44
80
|
export declare function getLedgerDir(): string;
|
|
45
81
|
/**
|
|
46
82
|
* Append a new entry to the mesh ledger.
|
|
@@ -49,14 +85,20 @@ export declare function getLedgerDir(): string;
|
|
|
49
85
|
export declare const meshLedgerEvents: EventEmitter<[never]>;
|
|
50
86
|
export declare function appendLedgerEntry(meshId: string, partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>): MeshLedgerEntry;
|
|
51
87
|
/**
|
|
52
|
-
* Append entries received
|
|
53
|
-
* This skips deduplicated entries and
|
|
88
|
+
* Append entries received over local-first/P2P ledger replication to the local ledger.
|
|
89
|
+
* This skips deduplicated entries and rejects malformed/cross-mesh entries.
|
|
54
90
|
*/
|
|
55
|
-
export declare function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]):
|
|
91
|
+
export declare function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]): AppendRemoteLedgerResult;
|
|
56
92
|
/**
|
|
57
93
|
* Read ledger entries with optional filtering.
|
|
58
94
|
*/
|
|
59
95
|
export declare function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[];
|
|
96
|
+
/**
|
|
97
|
+
* Read a bounded, cursor-addressable ledger slice for local-first/P2P replication.
|
|
98
|
+
* The result is intentionally small and self-describing so coordinators can query
|
|
99
|
+
* remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
|
|
100
|
+
*/
|
|
101
|
+
export declare function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice;
|
|
60
102
|
/**
|
|
61
103
|
* Get a summary of mesh activity from the ledger.
|
|
62
104
|
*/
|
package/dist/mesh/mesh-sync.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mesh Sync — Sync local mesh
|
|
2
|
+
* Mesh Sync — Sync local mesh metadata to/from cloud D1
|
|
3
3
|
*
|
|
4
4
|
* When cloud is available, this module pushes local mesh config
|
|
5
5
|
* to the server and pulls remote meshes that were created from
|
|
6
6
|
* other machines. The local ~/.adhdev/meshes.json remains the
|
|
7
|
-
* canonical source; cloud is a
|
|
7
|
+
* canonical source; cloud is a membership/metadata layer only.
|
|
8
|
+
* Task/chat/ledger evidence remains local-first and must not be
|
|
9
|
+
* synchronized through Cloud/D1.
|
|
8
10
|
*
|
|
9
11
|
* This is called lazily (not on daemon startup) — only when the
|
|
10
12
|
* user explicitly opens the mesh page or runs `adhdev mesh sync`.
|
|
@@ -26,12 +28,6 @@ export interface MeshSyncTransport {
|
|
|
26
28
|
}>;
|
|
27
29
|
/** DELETE /api/v1/repo-meshes/:id */
|
|
28
30
|
deleteRemoteMesh(meshId: string): Promise<void>;
|
|
29
|
-
/** POST /api/v1/repo-meshes/:id/ledger/sync */
|
|
30
|
-
syncMeshLedger?(meshId: string, data: {
|
|
31
|
-
newEntries: any[];
|
|
32
|
-
}): Promise<{
|
|
33
|
-
missingEntries: any[];
|
|
34
|
-
}>;
|
|
35
31
|
}
|
|
36
32
|
export interface RemoteMeshRecord {
|
|
37
33
|
id: string;
|
|
@@ -55,7 +51,3 @@ export interface MeshSyncResult {
|
|
|
55
51
|
* Pull remote meshes that don't exist locally.
|
|
56
52
|
*/
|
|
57
53
|
export declare function syncMeshes(transport: MeshSyncTransport): Promise<MeshSyncResult>;
|
|
58
|
-
/**
|
|
59
|
-
* Sync the task ledger for a specific mesh.
|
|
60
|
-
*/
|
|
61
|
-
export declare function syncMeshLedger(meshId: string, transport: MeshSyncTransport): Promise<void>;
|
package/package.json
CHANGED
package/src/commands/router.ts
CHANGED
|
@@ -1821,6 +1821,41 @@ export class DaemonCommandRouter {
|
|
|
1821
1821
|
}
|
|
1822
1822
|
}
|
|
1823
1823
|
|
|
1824
|
+
case 'get_mesh_ledger_slice': {
|
|
1825
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1826
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1827
|
+
try {
|
|
1828
|
+
const { readLedgerSlice } = await import('../mesh/mesh-ledger.js');
|
|
1829
|
+
const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
|
|
1830
|
+
const slice = readLedgerSlice(meshId, {
|
|
1831
|
+
afterId: typeof args?.afterId === 'string' ? args.afterId : undefined,
|
|
1832
|
+
since: typeof args?.since === 'string' ? args.since : undefined,
|
|
1833
|
+
kind,
|
|
1834
|
+
limit: typeof args?.limit === 'number' ? args.limit : undefined,
|
|
1835
|
+
});
|
|
1836
|
+
return { success: true, slice };
|
|
1837
|
+
} catch (e: any) {
|
|
1838
|
+
return { success: false, error: e.message };
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
case 'import_mesh_ledger_slice': {
|
|
1843
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1844
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1845
|
+
try {
|
|
1846
|
+
const { appendRemoteLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
1847
|
+
const entries = Array.isArray(args?.entries)
|
|
1848
|
+
? args.entries as any[]
|
|
1849
|
+
: Array.isArray(args?.slice?.entries)
|
|
1850
|
+
? args.slice.entries as any[]
|
|
1851
|
+
: [];
|
|
1852
|
+
const result = appendRemoteLedgerEntries(meshId, entries as any);
|
|
1853
|
+
return { success: true, result, summary: getLedgerSummary(meshId) };
|
|
1854
|
+
} catch (e: any) {
|
|
1855
|
+
return { success: false, error: e.message };
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1824
1859
|
case 'get_mesh_queue': {
|
|
1825
1860
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1826
1861
|
if (!meshId) return { success: false, error: 'meshId required' };
|
package/src/index.ts
CHANGED
|
@@ -150,8 +150,10 @@ export { syncMeshes } from './mesh/mesh-sync.js';
|
|
|
150
150
|
export type { MeshSyncTransport, MeshSyncResult, RemoteMeshRecord } from './mesh/mesh-sync.js';
|
|
151
151
|
|
|
152
152
|
// ── Mesh Task Ledger ──
|
|
153
|
-
export { appendLedgerEntry, readLedgerEntries, getLedgerSummary, getLedgerDir, getSessionRecoveryContext } from './mesh/mesh-ledger.js';
|
|
154
|
-
export type { MeshLedgerEntry, MeshLedgerKind, MeshLedgerSummary, ReadLedgerOptions, SessionRecoveryContext } from './mesh/mesh-ledger.js';
|
|
153
|
+
export { appendLedgerEntry, appendRemoteLedgerEntries, readLedgerEntries, readLedgerSlice, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT } from './mesh/mesh-ledger.js';
|
|
154
|
+
export type { AppendRemoteLedgerResult, MeshLedgerEntry, MeshLedgerKind, MeshLedgerSlice, MeshLedgerSummary, ReadLedgerOptions, ReadLedgerSliceOptions, SessionRecoveryContext } from './mesh/mesh-ledger.js';
|
|
155
|
+
export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence } from './mesh/mesh-ledger-reconciliation.js';
|
|
156
|
+
export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
|
|
155
157
|
|
|
156
158
|
// ── Mesh Work Queue (GUPP) ──
|
|
157
159
|
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats } from './mesh/mesh-work-queue.js';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AppendRemoteLedgerResult, MeshLedgerSlice, MeshLedgerSummary } from './mesh-ledger.js';
|
|
2
|
+
|
|
3
|
+
export type MeshLedgerReplicaStatus = 'local' | 'queried' | 'imported' | 'failed';
|
|
4
|
+
|
|
5
|
+
export interface MeshLedgerReplicaEvidence {
|
|
6
|
+
nodeId: string;
|
|
7
|
+
daemonId?: string;
|
|
8
|
+
status: MeshLedgerReplicaStatus;
|
|
9
|
+
transport: 'local' | 'p2p_datachannel';
|
|
10
|
+
protocol: 'adhdev.mesh.ledger.slice.v1';
|
|
11
|
+
entriesReceived: number;
|
|
12
|
+
entriesImported: number;
|
|
13
|
+
skippedDuplicate: number;
|
|
14
|
+
rejectedInvalid: number;
|
|
15
|
+
hasMore: boolean;
|
|
16
|
+
nextAfterId: string | null;
|
|
17
|
+
lastTimestamp: string | null;
|
|
18
|
+
summary?: MeshLedgerSummary;
|
|
19
|
+
error?: string;
|
|
20
|
+
noFallbackReason?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MeshLedgerReconciliationEvidence {
|
|
24
|
+
protocol: 'adhdev.mesh.ledger.reconciliation.v1';
|
|
25
|
+
meshId: string;
|
|
26
|
+
generatedAt: string;
|
|
27
|
+
sourceOfTruth: {
|
|
28
|
+
kind: 'coordinator_local_jsonl';
|
|
29
|
+
p2pOnly: true;
|
|
30
|
+
cloudD1LedgerSync: false;
|
|
31
|
+
notes: string;
|
|
32
|
+
};
|
|
33
|
+
replicas: MeshLedgerReplicaEvidence[];
|
|
34
|
+
totals: {
|
|
35
|
+
replicas: number;
|
|
36
|
+
queried: number;
|
|
37
|
+
failed: number;
|
|
38
|
+
entriesReceived: number;
|
|
39
|
+
entriesImported: number;
|
|
40
|
+
skippedDuplicate: number;
|
|
41
|
+
rejectedInvalid: number;
|
|
42
|
+
};
|
|
43
|
+
convergence: {
|
|
44
|
+
complete: boolean;
|
|
45
|
+
pendingNodes: string[];
|
|
46
|
+
failedNodes: string[];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function lastTimestamp(slice?: MeshLedgerSlice): string | null {
|
|
51
|
+
const entries = Array.isArray(slice?.entries) ? slice!.entries : [];
|
|
52
|
+
return entries.length ? entries[entries.length - 1].timestamp : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildMeshLedgerReplicaEvidence(args: {
|
|
56
|
+
nodeId: string;
|
|
57
|
+
daemonId?: string;
|
|
58
|
+
transport: 'local' | 'p2p_datachannel';
|
|
59
|
+
slice?: MeshLedgerSlice;
|
|
60
|
+
importResult?: AppendRemoteLedgerResult;
|
|
61
|
+
status?: MeshLedgerReplicaStatus;
|
|
62
|
+
error?: string;
|
|
63
|
+
}): MeshLedgerReplicaEvidence {
|
|
64
|
+
const entriesReceived = Array.isArray(args.slice?.entries) ? args.slice!.entries.length : 0;
|
|
65
|
+
return {
|
|
66
|
+
nodeId: args.nodeId,
|
|
67
|
+
...(args.daemonId ? { daemonId: args.daemonId } : {}),
|
|
68
|
+
status: args.status ?? (args.importResult && args.importResult.accepted > 0 ? 'imported' : 'queried'),
|
|
69
|
+
transport: args.transport,
|
|
70
|
+
protocol: 'adhdev.mesh.ledger.slice.v1',
|
|
71
|
+
entriesReceived,
|
|
72
|
+
entriesImported: args.importResult?.accepted ?? 0,
|
|
73
|
+
skippedDuplicate: args.importResult?.skippedDuplicate ?? 0,
|
|
74
|
+
rejectedInvalid: args.importResult?.rejectedInvalid ?? 0,
|
|
75
|
+
hasMore: args.slice?.cursor?.hasMore === true,
|
|
76
|
+
nextAfterId: args.slice?.cursor?.nextAfterId ?? null,
|
|
77
|
+
lastTimestamp: lastTimestamp(args.slice),
|
|
78
|
+
...(args.slice?.summary ? { summary: args.slice.summary } : {}),
|
|
79
|
+
...(args.error ? {
|
|
80
|
+
error: args.error,
|
|
81
|
+
noFallbackReason: 'Ledger reconciliation is P2P/local-first only; Cloud/D1 ledger sync is intentionally disabled.',
|
|
82
|
+
} : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildMeshLedgerReconciliationEvidence(meshId: string, replicas: MeshLedgerReplicaEvidence[]): MeshLedgerReconciliationEvidence {
|
|
87
|
+
const failedNodes = replicas.filter(replica => replica.status === 'failed').map(replica => replica.nodeId);
|
|
88
|
+
const pendingNodes = replicas.filter(replica => replica.hasMore && replica.status !== 'failed').map(replica => replica.nodeId);
|
|
89
|
+
return {
|
|
90
|
+
protocol: 'adhdev.mesh.ledger.reconciliation.v1',
|
|
91
|
+
meshId,
|
|
92
|
+
generatedAt: new Date().toISOString(),
|
|
93
|
+
sourceOfTruth: {
|
|
94
|
+
kind: 'coordinator_local_jsonl',
|
|
95
|
+
p2pOnly: true,
|
|
96
|
+
cloudD1LedgerSync: false,
|
|
97
|
+
notes: 'Coordinator reconciles bounded slices from daemon-local JSONL ledgers over P2P DataChannel; Cloud/D1 is not a ledger source of truth.',
|
|
98
|
+
},
|
|
99
|
+
replicas,
|
|
100
|
+
totals: {
|
|
101
|
+
replicas: replicas.length,
|
|
102
|
+
queried: replicas.filter(replica => replica.status !== 'failed').length,
|
|
103
|
+
failed: failedNodes.length,
|
|
104
|
+
entriesReceived: replicas.reduce((sum, replica) => sum + replica.entriesReceived, 0),
|
|
105
|
+
entriesImported: replicas.reduce((sum, replica) => sum + replica.entriesImported, 0),
|
|
106
|
+
skippedDuplicate: replicas.reduce((sum, replica) => sum + replica.skippedDuplicate, 0),
|
|
107
|
+
rejectedInvalid: replicas.reduce((sum, replica) => sum + replica.rejectedInvalid, 0),
|
|
108
|
+
},
|
|
109
|
+
convergence: {
|
|
110
|
+
complete: failedNodes.length === 0 && pendingNodes.length === 0,
|
|
111
|
+
pendingNodes,
|
|
112
|
+
failedNodes,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type MeshLedgerKind =
|
|
|
34
34
|
| 'node_removed'
|
|
35
35
|
| 'coordinator_started'
|
|
36
36
|
| 'recovery_attempted'
|
|
37
|
+
| 'ledger_replicated'
|
|
38
|
+
| 'ledger_reconciled'
|
|
37
39
|
;
|
|
38
40
|
|
|
39
41
|
export interface MeshLedgerEntry {
|
|
@@ -66,11 +68,52 @@ export interface ReadLedgerOptions {
|
|
|
66
68
|
kind?: MeshLedgerKind[];
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
export interface ReadLedgerSliceOptions {
|
|
72
|
+
/** Return entries strictly after this entry id. If not found, starts from the beginning of the filtered set. */
|
|
73
|
+
afterId?: string;
|
|
74
|
+
/** Return entries at or after this timestamp. */
|
|
75
|
+
since?: string;
|
|
76
|
+
/** Optional event kind filter. */
|
|
77
|
+
kind?: MeshLedgerKind[];
|
|
78
|
+
/** Maximum entries to return. Clamped to a bounded protocol maximum. */
|
|
79
|
+
limit?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface MeshLedgerCursor {
|
|
83
|
+
afterId: string | null;
|
|
84
|
+
nextAfterId: string | null;
|
|
85
|
+
limit: number;
|
|
86
|
+
hasMore: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface MeshLedgerSlice {
|
|
90
|
+
protocol: 'adhdev.mesh.ledger.slice.v1';
|
|
91
|
+
meshId: string;
|
|
92
|
+
entries: MeshLedgerEntry[];
|
|
93
|
+
cursor: MeshLedgerCursor;
|
|
94
|
+
summary: MeshLedgerSummary;
|
|
95
|
+
sourceOfTruth: {
|
|
96
|
+
kind: 'local_jsonl';
|
|
97
|
+
path: string;
|
|
98
|
+
bounded: true;
|
|
99
|
+
maxLimit: number;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface AppendRemoteLedgerResult {
|
|
104
|
+
accepted: number;
|
|
105
|
+
skippedDuplicate: number;
|
|
106
|
+
rejectedInvalid: number;
|
|
107
|
+
entries: MeshLedgerEntry[];
|
|
108
|
+
}
|
|
109
|
+
|
|
69
110
|
// ─── Constants ──────────────────────────────────
|
|
70
111
|
|
|
71
112
|
const LEDGER_DIR_NAME = 'mesh-ledger';
|
|
72
113
|
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
73
114
|
const RECENT_FAILURE_WINDOW_MS = 30 * 60 * 1000; // 30 minutes
|
|
115
|
+
const DEFAULT_LEDGER_SLICE_LIMIT = 100;
|
|
116
|
+
export const MAX_LEDGER_SLICE_LIMIT = 500;
|
|
74
117
|
|
|
75
118
|
// ─── Path Helpers ───────────────────────────────
|
|
76
119
|
|
|
@@ -136,23 +179,59 @@ export function appendLedgerEntry(
|
|
|
136
179
|
}
|
|
137
180
|
}
|
|
138
181
|
|
|
182
|
+
function clampLedgerSliceLimit(limit: unknown): number {
|
|
183
|
+
if (typeof limit !== 'number' || !Number.isFinite(limit)) return DEFAULT_LEDGER_SLICE_LIMIT;
|
|
184
|
+
return Math.max(1, Math.min(MAX_LEDGER_SLICE_LIMIT, Math.floor(limit)));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isValidRemoteLedgerEntry(meshId: string, value: unknown): value is MeshLedgerEntry {
|
|
188
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
189
|
+
const entry = value as Partial<MeshLedgerEntry>;
|
|
190
|
+
if (typeof entry.id !== 'string' || !entry.id.trim()) return false;
|
|
191
|
+
if (entry.meshId !== meshId) return false;
|
|
192
|
+
if (typeof entry.timestamp !== 'string' || Number.isNaN(new Date(entry.timestamp).getTime())) return false;
|
|
193
|
+
if (typeof entry.kind !== 'string' || !entry.kind.trim()) return false;
|
|
194
|
+
if (!entry.payload || typeof entry.payload !== 'object' || Array.isArray(entry.payload)) return false;
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
139
198
|
/**
|
|
140
|
-
* Append entries received
|
|
141
|
-
* This skips deduplicated entries and
|
|
199
|
+
* Append entries received over local-first/P2P ledger replication to the local ledger.
|
|
200
|
+
* This skips deduplicated entries and rejects malformed/cross-mesh entries.
|
|
142
201
|
*/
|
|
143
|
-
export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]):
|
|
144
|
-
if (entries.length === 0) return;
|
|
202
|
+
export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]): AppendRemoteLedgerResult {
|
|
203
|
+
if (entries.length === 0) return { accepted: 0, skippedDuplicate: 0, rejectedInvalid: 0, entries: [] };
|
|
145
204
|
const ledgerPath = getLedgerPath(meshId);
|
|
146
205
|
|
|
147
206
|
// Read existing to deduplicate by ID
|
|
148
207
|
const existing = new Set(readLedgerEntries(meshId).map(e => e.id));
|
|
149
|
-
const
|
|
208
|
+
const validEntries: MeshLedgerEntry[] = [];
|
|
209
|
+
let rejectedInvalid = 0;
|
|
210
|
+
let skippedDuplicate = 0;
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (!isValidRemoteLedgerEntry(meshId, entry)) {
|
|
213
|
+
rejectedInvalid++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (existing.has(entry.id)) {
|
|
217
|
+
skippedDuplicate++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
existing.add(entry.id);
|
|
221
|
+
validEntries.push(entry);
|
|
222
|
+
}
|
|
150
223
|
|
|
151
|
-
if (
|
|
224
|
+
if (validEntries.length === 0) {
|
|
225
|
+
return { accepted: 0, skippedDuplicate, rejectedInvalid, entries: [] };
|
|
226
|
+
}
|
|
152
227
|
|
|
153
228
|
try {
|
|
154
|
-
const lines =
|
|
229
|
+
const lines = validEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
155
230
|
appendFileSync(ledgerPath, lines, { encoding: 'utf-8', mode: 0o600 });
|
|
231
|
+
for (const entry of validEntries) {
|
|
232
|
+
meshLedgerEvents.emit('append', meshId, entry);
|
|
233
|
+
}
|
|
234
|
+
return { accepted: validEntries.length, skippedDuplicate, rejectedInvalid, entries: validEntries };
|
|
156
235
|
} catch (e: any) {
|
|
157
236
|
throw new Error(`Failed to append remote ledger entries for mesh ${meshId}: ${e.message}`);
|
|
158
237
|
}
|
|
@@ -206,6 +285,40 @@ export function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): Mes
|
|
|
206
285
|
return entries;
|
|
207
286
|
}
|
|
208
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Read a bounded, cursor-addressable ledger slice for local-first/P2P replication.
|
|
290
|
+
* The result is intentionally small and self-describing so coordinators can query
|
|
291
|
+
* remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
|
|
292
|
+
*/
|
|
293
|
+
export function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice {
|
|
294
|
+
const limit = clampLedgerSliceLimit(opts?.limit);
|
|
295
|
+
let entries = readLedgerEntries(meshId, { since: opts?.since, kind: opts?.kind });
|
|
296
|
+
const afterId = typeof opts?.afterId === 'string' && opts.afterId.trim() ? opts.afterId.trim() : null;
|
|
297
|
+
if (afterId) {
|
|
298
|
+
const index = entries.findIndex(entry => entry.id === afterId);
|
|
299
|
+
entries = index >= 0 ? entries.slice(index + 1) : entries;
|
|
300
|
+
}
|
|
301
|
+
const bounded = entries.slice(0, limit);
|
|
302
|
+
return {
|
|
303
|
+
protocol: 'adhdev.mesh.ledger.slice.v1',
|
|
304
|
+
meshId,
|
|
305
|
+
entries: bounded,
|
|
306
|
+
cursor: {
|
|
307
|
+
afterId,
|
|
308
|
+
nextAfterId: bounded.length ? bounded[bounded.length - 1].id : afterId,
|
|
309
|
+
limit,
|
|
310
|
+
hasMore: entries.length > bounded.length,
|
|
311
|
+
},
|
|
312
|
+
summary: getLedgerSummary(meshId),
|
|
313
|
+
sourceOfTruth: {
|
|
314
|
+
kind: 'local_jsonl',
|
|
315
|
+
path: getLedgerPath(meshId),
|
|
316
|
+
bounded: true,
|
|
317
|
+
maxLimit: MAX_LEDGER_SLICE_LIMIT,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
209
322
|
/**
|
|
210
323
|
* Get a summary of mesh activity from the ledger.
|
|
211
324
|
*/
|
package/src/mesh/mesh-sync.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mesh Sync — Sync local mesh
|
|
2
|
+
* Mesh Sync — Sync local mesh metadata to/from cloud D1
|
|
3
3
|
*
|
|
4
4
|
* When cloud is available, this module pushes local mesh config
|
|
5
5
|
* to the server and pulls remote meshes that were created from
|
|
6
6
|
* other machines. The local ~/.adhdev/meshes.json remains the
|
|
7
|
-
* canonical source; cloud is a
|
|
7
|
+
* canonical source; cloud is a membership/metadata layer only.
|
|
8
|
+
* Task/chat/ledger evidence remains local-first and must not be
|
|
9
|
+
* synchronized through Cloud/D1.
|
|
8
10
|
*
|
|
9
11
|
* This is called lazily (not on daemon startup) — only when the
|
|
10
12
|
* user explicitly opens the mesh page or runs `adhdev mesh sync`.
|
|
@@ -26,8 +28,6 @@ export interface MeshSyncTransport {
|
|
|
26
28
|
}): Promise<{ mesh: RemoteMeshRecord }>;
|
|
27
29
|
/** DELETE /api/v1/repo-meshes/:id */
|
|
28
30
|
deleteRemoteMesh(meshId: string): Promise<void>;
|
|
29
|
-
/** POST /api/v1/repo-meshes/:id/ledger/sync */
|
|
30
|
-
syncMeshLedger?(meshId: string, data: { newEntries: any[] }): Promise<{ missingEntries: any[] }>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface RemoteMeshRecord {
|
|
@@ -107,35 +107,5 @@ export async function syncMeshes(transport: MeshSyncTransport): Promise<MeshSync
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// Sync ledgers for all local meshes if the transport supports it
|
|
111
|
-
if (transport.syncMeshLedger) {
|
|
112
|
-
for (const local of localMeshes) {
|
|
113
|
-
try {
|
|
114
|
-
await syncMeshLedger(local.id, transport);
|
|
115
|
-
} catch (e: any) {
|
|
116
|
-
result.errors.push(`Ledger sync failed for "${local.name}": ${e.message}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
110
|
return result;
|
|
122
111
|
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Sync the task ledger for a specific mesh.
|
|
126
|
-
*/
|
|
127
|
-
export async function syncMeshLedger(meshId: string, transport: MeshSyncTransport): Promise<void> {
|
|
128
|
-
if (!transport.syncMeshLedger) return;
|
|
129
|
-
const { readLedgerEntries, appendRemoteLedgerEntries } = await import('./mesh-ledger.js');
|
|
130
|
-
|
|
131
|
-
// Read all local entries (no tail)
|
|
132
|
-
const localEntries = readLedgerEntries(meshId);
|
|
133
|
-
|
|
134
|
-
// Send to cloud and get missing entries back
|
|
135
|
-
const res = await transport.syncMeshLedger(meshId, { newEntries: localEntries });
|
|
136
|
-
|
|
137
|
-
// Append any missing entries from the cloud
|
|
138
|
-
if (res.missingEntries && res.missingEntries.length > 0) {
|
|
139
|
-
appendRemoteLedgerEntries(meshId, res.missingEntries);
|
|
140
|
-
}
|
|
141
|
-
}
|