@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.
@@ -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 from the cloud to the local ledger.
53
- * This skips deduplicated entries and just writes new ones.
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[]): void;
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
  */
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Mesh Sync — Sync local mesh config to/from cloud D1
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 persistence/relay layer.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.77-rc.47",
3
+ "version": "0.9.77-rc.49",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
+ }
@@ -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 from the cloud to the local ledger.
141
- * This skips deduplicated entries and just writes new ones.
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[]): void {
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 newEntries = entries.filter(e => !existing.has(e.id));
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 (newEntries.length === 0) return;
224
+ if (validEntries.length === 0) {
225
+ return { accepted: 0, skippedDuplicate, rejectedInvalid, entries: [] };
226
+ }
152
227
 
153
228
  try {
154
- const lines = newEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
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
  */
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Mesh Sync — Sync local mesh config to/from cloud D1
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 persistence/relay layer.
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
- }