@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.
- package/dist/boot/daemon-lifecycle.d.ts +3 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/commands/mesh-coordinator.d.ts +10 -0
- package/dist/commands/router.d.ts +4 -1
- package/dist/config/mesh-config.d.ts +1 -0
- package/dist/git/git-worktree.d.ts +15 -2
- package/dist/index.d.ts +11 -6
- package/dist/index.js +2117 -300
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2102 -300
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +14 -7
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +55 -0
- package/dist/mesh/mesh-ledger.d.ts +84 -4
- package/dist/mesh/mesh-sync.d.ts +4 -12
- package/dist/mesh/mesh-visualization.d.ts +70 -0
- package/dist/mesh/mesh-work-queue.d.ts +58 -1
- package/dist/mesh/p2p-relay-failure.d.ts +35 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +6 -0
- package/dist/repo-mesh-types.d.ts +2 -0
- package/dist/shared-types.d.ts +38 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +5 -0
- package/src/cli-adapters/provider-cli-adapter.ts +30 -5
- package/src/commands/cli-manager.ts +0 -4
- package/src/commands/mesh-coordinator.ts +55 -7
- package/src/commands/router.ts +964 -26
- package/src/commands/stream-commands.ts +8 -1
- package/src/config/config.ts +2 -1
- package/src/config/mesh-config.ts +2 -0
- package/src/config/workspaces.ts +1 -1
- package/src/git/git-worktree.ts +56 -4
- package/src/index.d.ts +3 -0
- package/src/index.ts +30 -6
- package/src/mesh/coordinator-prompt.ts +21 -10
- package/src/mesh/mesh-events.ts +532 -22
- package/src/mesh/mesh-ledger-reconciliation.ts +115 -0
- package/src/mesh/mesh-ledger.ts +209 -8
- package/src/mesh/mesh-sync.ts +4 -34
- package/src/mesh/mesh-visualization.ts +341 -0
- package/src/mesh/mesh-work-queue.ts +183 -17
- package/src/mesh/p2p-relay-failure.ts +152 -0
- package/src/providers/acp-provider-instance.ts +2 -1
- package/src/providers/chat-message-normalization.ts +33 -1
- package/src/providers/cli-provider-instance.ts +155 -31
- package/src/providers/extension-provider-instance.ts +2 -1
- package/src/providers/ide-provider-instance.ts +2 -2
- package/src/repo-mesh-types.ts +2 -0
- 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
|
+
}
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -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
|
|
140
|
-
* This skips deduplicated entries and
|
|
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[]):
|
|
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
|
|
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 (
|
|
306
|
+
if (validEntries.length === 0) {
|
|
307
|
+
return { accepted: 0, skippedDuplicate, rejectedInvalid, entries: [] };
|
|
308
|
+
}
|
|
151
309
|
|
|
152
310
|
try {
|
|
153
|
-
const lines =
|
|
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':
|
|
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
|
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
|
-
}
|