@adhdev/daemon-core 0.9.77-rc.9 → 0.9.78

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 (50) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +3 -0
  2. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  3. package/dist/commands/mesh-coordinator.d.ts +10 -0
  4. package/dist/commands/router.d.ts +4 -1
  5. package/dist/config/mesh-config.d.ts +1 -0
  6. package/dist/git/git-worktree.d.ts +15 -2
  7. package/dist/index.d.ts +11 -6
  8. package/dist/index.js +2117 -300
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +2102 -300
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/mesh/mesh-events.d.ts +14 -7
  13. package/dist/mesh/mesh-ledger-reconciliation.d.ts +55 -0
  14. package/dist/mesh/mesh-ledger.d.ts +84 -4
  15. package/dist/mesh/mesh-sync.d.ts +4 -12
  16. package/dist/mesh/mesh-visualization.d.ts +70 -0
  17. package/dist/mesh/mesh-work-queue.d.ts +58 -1
  18. package/dist/mesh/p2p-relay-failure.d.ts +35 -0
  19. package/dist/providers/chat-message-normalization.d.ts +1 -0
  20. package/dist/providers/cli-provider-instance.d.ts +6 -0
  21. package/dist/repo-mesh-types.d.ts +2 -0
  22. package/dist/shared-types.d.ts +38 -0
  23. package/package.json +1 -1
  24. package/src/boot/daemon-lifecycle.ts +5 -0
  25. package/src/cli-adapters/provider-cli-adapter.ts +30 -5
  26. package/src/commands/cli-manager.ts +0 -4
  27. package/src/commands/mesh-coordinator.ts +55 -7
  28. package/src/commands/router.ts +964 -26
  29. package/src/commands/stream-commands.ts +8 -1
  30. package/src/config/config.ts +2 -1
  31. package/src/config/mesh-config.ts +2 -0
  32. package/src/config/workspaces.ts +1 -1
  33. package/src/git/git-worktree.ts +56 -4
  34. package/src/index.d.ts +3 -0
  35. package/src/index.ts +30 -6
  36. package/src/mesh/coordinator-prompt.ts +21 -10
  37. package/src/mesh/mesh-events.ts +532 -22
  38. package/src/mesh/mesh-ledger-reconciliation.ts +115 -0
  39. package/src/mesh/mesh-ledger.ts +209 -8
  40. package/src/mesh/mesh-sync.ts +4 -34
  41. package/src/mesh/mesh-visualization.ts +341 -0
  42. package/src/mesh/mesh-work-queue.ts +183 -17
  43. package/src/mesh/p2p-relay-failure.ts +152 -0
  44. package/src/providers/acp-provider-instance.ts +2 -1
  45. package/src/providers/chat-message-normalization.ts +33 -1
  46. package/src/providers/cli-provider-instance.ts +155 -31
  47. package/src/providers/extension-provider-instance.ts +2 -1
  48. package/src/providers/ide-provider-instance.ts +2 -2
  49. package/src/repo-mesh-types.ts +2 -0
  50. package/src/shared-types.ts +38 -0
@@ -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
+ }
@@ -27,12 +27,15 @@ export type MeshLedgerKind =
27
27
  | 'task_stalled'
28
28
  | 'task_approval_needed'
29
29
  | 'session_launched'
30
+ | 'session_auto_launch'
30
31
  | 'session_stopped'
31
32
  | 'checkpoint_created'
32
33
  | 'node_cloned'
33
34
  | 'node_removed'
34
35
  | 'coordinator_started'
35
36
  | 'recovery_attempted'
37
+ | 'ledger_replicated'
38
+ | 'ledger_reconciled'
36
39
  ;
37
40
 
38
41
  export interface MeshLedgerEntry {
@@ -46,6 +49,56 @@ export interface MeshLedgerEntry {
46
49
  payload: Record<string, unknown>;
47
50
  }
48
51
 
52
+ export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind' | 'payload'>): boolean {
53
+ if (entry.kind !== 'session_stopped' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') return false;
54
+ const payload = entry.payload && typeof entry.payload === 'object' && !Array.isArray(entry.payload)
55
+ ? entry.payload as Record<string, unknown>
56
+ : {};
57
+ return payload.intentional === true
58
+ && (payload.reason === 'operator_cleanup'
59
+ || payload.intentionalStopReason === 'operator_cleanup'
60
+ || payload.source === 'mesh_cleanup_sessions'
61
+ || payload.source === 'mesh_remove_node');
62
+ }
63
+
64
+ export interface MeshTaskCompletionEvidence {
65
+ source: 'agent_status_event';
66
+ event: 'agent:generating_completed' | 'agent:ready';
67
+ nodeId: string;
68
+ sessionId: string;
69
+ providerType?: string;
70
+ completedAt: string;
71
+ transcriptHandle: {
72
+ kind: 'provider_session' | 'runtime_session';
73
+ sessionId: string;
74
+ providerSessionId?: string;
75
+ finalSummaryAvailable: boolean;
76
+ };
77
+ git: {
78
+ status: 'deferred';
79
+ reason: string;
80
+ };
81
+ validation: {
82
+ status: 'deferred';
83
+ commandsRun: string[];
84
+ reason: string;
85
+ };
86
+ checkpoint: {
87
+ attempted: false;
88
+ reason: 'not_attempted_for_ordinary_completion';
89
+ };
90
+ }
91
+
92
+ export interface BuildTaskCompletionEvidenceOptions {
93
+ event: MeshTaskCompletionEvidence['event'];
94
+ nodeId: string;
95
+ sessionId: string;
96
+ providerType?: string;
97
+ providerSessionId?: string;
98
+ finalSummary?: string;
99
+ completedAt?: string;
100
+ }
101
+
49
102
  export interface MeshLedgerSummary {
50
103
  meshId: string;
51
104
  totalEntries: number;
@@ -65,11 +118,52 @@ export interface ReadLedgerOptions {
65
118
  kind?: MeshLedgerKind[];
66
119
  }
67
120
 
121
+ export interface ReadLedgerSliceOptions {
122
+ /** Return entries strictly after this entry id. If not found, starts from the beginning of the filtered set. */
123
+ afterId?: string;
124
+ /** Return entries at or after this timestamp. */
125
+ since?: string;
126
+ /** Optional event kind filter. */
127
+ kind?: MeshLedgerKind[];
128
+ /** Maximum entries to return. Clamped to a bounded protocol maximum. */
129
+ limit?: number;
130
+ }
131
+
132
+ export interface MeshLedgerCursor {
133
+ afterId: string | null;
134
+ nextAfterId: string | null;
135
+ limit: number;
136
+ hasMore: boolean;
137
+ }
138
+
139
+ export interface MeshLedgerSlice {
140
+ protocol: 'adhdev.mesh.ledger.slice.v1';
141
+ meshId: string;
142
+ entries: MeshLedgerEntry[];
143
+ cursor: MeshLedgerCursor;
144
+ summary: MeshLedgerSummary;
145
+ sourceOfTruth: {
146
+ kind: 'local_jsonl';
147
+ path: string;
148
+ bounded: true;
149
+ maxLimit: number;
150
+ };
151
+ }
152
+
153
+ export interface AppendRemoteLedgerResult {
154
+ accepted: number;
155
+ skippedDuplicate: number;
156
+ rejectedInvalid: number;
157
+ entries: MeshLedgerEntry[];
158
+ }
159
+
68
160
  // ─── Constants ──────────────────────────────────
69
161
 
70
162
  const LEDGER_DIR_NAME = 'mesh-ledger';
71
163
  const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
72
164
  const RECENT_FAILURE_WINDOW_MS = 30 * 60 * 1000; // 30 minutes
165
+ const DEFAULT_LEDGER_SLICE_LIMIT = 100;
166
+ export const MAX_LEDGER_SLICE_LIMIT = 500;
73
167
 
74
168
  // ─── Path Helpers ───────────────────────────────
75
169
 
@@ -94,6 +188,38 @@ function getRotatedPath(meshId: string, index: number): string {
94
188
 
95
189
  // ─── Core API ───────────────────────────────────
96
190
 
191
+ export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence {
192
+ const providerSessionId = opts.providerSessionId?.trim() || undefined;
193
+ const providerType = opts.providerType?.trim() || undefined;
194
+ return {
195
+ source: 'agent_status_event',
196
+ event: opts.event,
197
+ nodeId: opts.nodeId,
198
+ sessionId: opts.sessionId,
199
+ providerType,
200
+ completedAt: opts.completedAt || new Date().toISOString(),
201
+ transcriptHandle: {
202
+ kind: providerSessionId ? 'provider_session' : 'runtime_session',
203
+ sessionId: opts.sessionId,
204
+ providerSessionId,
205
+ finalSummaryAvailable: typeof opts.finalSummary === 'string' && opts.finalSummary.trim().length > 0,
206
+ },
207
+ git: {
208
+ status: 'deferred',
209
+ reason: 'ordinary_completion_git_status_not_checked',
210
+ },
211
+ validation: {
212
+ status: 'deferred',
213
+ commandsRun: [],
214
+ reason: 'ordinary_completion_validation_not_run',
215
+ },
216
+ checkpoint: {
217
+ attempted: false,
218
+ reason: 'not_attempted_for_ordinary_completion',
219
+ },
220
+ };
221
+ }
222
+
97
223
  /**
98
224
  * Append a new entry to the mesh ledger.
99
225
  * Handles file creation, rotation on size overflow, and atomic writes.
@@ -135,23 +261,59 @@ export function appendLedgerEntry(
135
261
  }
136
262
  }
137
263
 
264
+ function clampLedgerSliceLimit(limit: unknown): number {
265
+ if (typeof limit !== 'number' || !Number.isFinite(limit)) return DEFAULT_LEDGER_SLICE_LIMIT;
266
+ return Math.max(1, Math.min(MAX_LEDGER_SLICE_LIMIT, Math.floor(limit)));
267
+ }
268
+
269
+ function isValidRemoteLedgerEntry(meshId: string, value: unknown): value is MeshLedgerEntry {
270
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
271
+ const entry = value as Partial<MeshLedgerEntry>;
272
+ if (typeof entry.id !== 'string' || !entry.id.trim()) return false;
273
+ if (entry.meshId !== meshId) return false;
274
+ if (typeof entry.timestamp !== 'string' || Number.isNaN(new Date(entry.timestamp).getTime())) return false;
275
+ if (typeof entry.kind !== 'string' || !entry.kind.trim()) return false;
276
+ if (!entry.payload || typeof entry.payload !== 'object' || Array.isArray(entry.payload)) return false;
277
+ return true;
278
+ }
279
+
138
280
  /**
139
- * Append entries received from the cloud to the local ledger.
140
- * This skips deduplicated entries and just writes new ones.
281
+ * Append entries received over local-first/P2P ledger replication to the local ledger.
282
+ * This skips deduplicated entries and rejects malformed/cross-mesh entries.
141
283
  */
142
- export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]): void {
143
- if (entries.length === 0) return;
284
+ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]): AppendRemoteLedgerResult {
285
+ if (entries.length === 0) return { accepted: 0, skippedDuplicate: 0, rejectedInvalid: 0, entries: [] };
144
286
  const ledgerPath = getLedgerPath(meshId);
145
287
 
146
288
  // Read existing to deduplicate by ID
147
289
  const existing = new Set(readLedgerEntries(meshId).map(e => e.id));
148
- const newEntries = entries.filter(e => !existing.has(e.id));
290
+ const validEntries: MeshLedgerEntry[] = [];
291
+ let rejectedInvalid = 0;
292
+ let skippedDuplicate = 0;
293
+ for (const entry of entries) {
294
+ if (!isValidRemoteLedgerEntry(meshId, entry)) {
295
+ rejectedInvalid++;
296
+ continue;
297
+ }
298
+ if (existing.has(entry.id)) {
299
+ skippedDuplicate++;
300
+ continue;
301
+ }
302
+ existing.add(entry.id);
303
+ validEntries.push(entry);
304
+ }
149
305
 
150
- if (newEntries.length === 0) return;
306
+ if (validEntries.length === 0) {
307
+ return { accepted: 0, skippedDuplicate, rejectedInvalid, entries: [] };
308
+ }
151
309
 
152
310
  try {
153
- const lines = newEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
311
+ const lines = validEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
154
312
  appendFileSync(ledgerPath, lines, { encoding: 'utf-8', mode: 0o600 });
313
+ for (const entry of validEntries) {
314
+ meshLedgerEvents.emit('append', meshId, entry);
315
+ }
316
+ return { accepted: validEntries.length, skippedDuplicate, rejectedInvalid, entries: validEntries };
155
317
  } catch (e: any) {
156
318
  throw new Error(`Failed to append remote ledger entries for mesh ${meshId}: ${e.message}`);
157
319
  }
@@ -205,6 +367,40 @@ export function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): Mes
205
367
  return entries;
206
368
  }
207
369
 
370
+ /**
371
+ * Read a bounded, cursor-addressable ledger slice for local-first/P2P replication.
372
+ * The result is intentionally small and self-describing so coordinators can query
373
+ * remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
374
+ */
375
+ export function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice {
376
+ const limit = clampLedgerSliceLimit(opts?.limit);
377
+ let entries = readLedgerEntries(meshId, { since: opts?.since, kind: opts?.kind });
378
+ const afterId = typeof opts?.afterId === 'string' && opts.afterId.trim() ? opts.afterId.trim() : null;
379
+ if (afterId) {
380
+ const index = entries.findIndex(entry => entry.id === afterId);
381
+ entries = index >= 0 ? entries.slice(index + 1) : entries;
382
+ }
383
+ const bounded = entries.slice(0, limit);
384
+ return {
385
+ protocol: 'adhdev.mesh.ledger.slice.v1',
386
+ meshId,
387
+ entries: bounded,
388
+ cursor: {
389
+ afterId,
390
+ nextAfterId: bounded.length ? bounded[bounded.length - 1].id : afterId,
391
+ limit,
392
+ hasMore: entries.length > bounded.length,
393
+ },
394
+ summary: getLedgerSummary(meshId),
395
+ sourceOfTruth: {
396
+ kind: 'local_jsonl',
397
+ path: getLedgerPath(meshId),
398
+ bounded: true,
399
+ maxLimit: MAX_LEDGER_SLICE_LIMIT,
400
+ },
401
+ };
402
+ }
403
+
208
404
  /**
209
405
  * Get a summary of mesh activity from the ledger.
210
406
  */
@@ -231,13 +427,17 @@ export function getLedgerSummary(meshId: string): MeshLedgerSummary {
231
427
  case 'task_dispatched': summary.taskDispatched++; break;
232
428
  case 'task_completed': summary.taskCompleted++; break;
233
429
  case 'task_failed': {
430
+ if (isIntentionalCleanupStopEntry(entry)) break;
234
431
  summary.taskFailed++;
235
432
  if (new Date(entry.timestamp).getTime() >= recentFailureCutoff) {
236
433
  summary.recentFailures++;
237
434
  }
238
435
  break;
239
436
  }
240
- case 'task_stalled': summary.taskStalled++; break;
437
+ case 'task_stalled': {
438
+ if (!isIntentionalCleanupStopEntry(entry)) summary.taskStalled++;
439
+ break;
440
+ }
241
441
  case 'session_launched': summary.sessionLaunched++; break;
242
442
  case 'checkpoint_created': summary.checkpointCreated++; break;
243
443
  }
@@ -308,6 +508,7 @@ export function getSessionRecoveryContext(
308
508
  if (new Date(e.timestamp).getTime() < recentWindow) break;
309
509
  if (opts.nodeId && e.nodeId !== opts.nodeId) continue;
310
510
  if (e.kind === 'task_failed') {
511
+ if (isIntentionalCleanupStopEntry(e)) continue;
311
512
  consecutiveNodeFailures++;
312
513
  } else if (e.kind === 'task_completed' || e.kind === 'task_dispatched') {
313
514
  // A completion or new dispatch breaks the consecutive failure chain
@@ -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
- }