@adhdev/daemon-core 0.9.82-rc.15 → 0.9.82-rc.151
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/process-hardening.d.ts +50 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +5 -1
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +104 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +27 -0
- package/dist/commands/handler.d.ts +66 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +5 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +68 -1
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +20 -6
- package/dist/index.js +13153 -3031
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13099 -3023
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +72 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-registry.d.ts +25 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +77 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +44 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
- package/dist/providers/approval-utils.d.ts +9 -0
- package/dist/providers/cli-provider-instance.d.ts +6 -1
- package/dist/providers/contracts.d.ts +50 -5
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/index.d.ts +11 -0
- package/dist/providers/provider-loader.d.ts +19 -1
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/repo-mesh-types.d.ts +67 -0
- package/dist/shared-types.d.ts +12 -0
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +6 -2
- package/src/boot/daemon-lifecycle.ts +17 -10
- package/src/boot/process-hardening.ts +89 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +14 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +3 -1
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +664 -1137
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +57 -11
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +1451 -52
- package/src/commands/cli-manager.ts +184 -3
- package/src/commands/handler.ts +547 -2
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3273 -428
- package/src/config/chat-history.ts +79 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +249 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +17 -5
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +72 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +479 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +40 -22
- package/src/mesh/coordinator-registry.ts +75 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +820 -61
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +216 -158
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- package/src/providers/approval-utils.ts +39 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +380 -44
- package/src/providers/contracts.ts +52 -5
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/index.ts +23 -0
- package/src/providers/provider-loader.ts +294 -28
- package/src/providers/provider-schema.ts +31 -13
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +444 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +210 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +77 -0
- package/src/shared-types.ts +9 -0
- package/src/status/builders.ts +23 -6
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +35 -11
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Safety: mode 0o600, atomic append via appendFileSync
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, renameSync, writeFileSync } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { randomUUID } from 'crypto';
|
|
19
19
|
import { getConfigDir } from '../config/config.js';
|
|
@@ -26,16 +26,19 @@ export type MeshLedgerKind =
|
|
|
26
26
|
| 'task_failed'
|
|
27
27
|
| 'task_stalled'
|
|
28
28
|
| 'task_approval_needed'
|
|
29
|
+
| 'p2p_dispatch_failed'
|
|
29
30
|
| 'session_launched'
|
|
30
31
|
| 'session_auto_launch'
|
|
31
32
|
| 'session_stopped'
|
|
32
33
|
| 'checkpoint_created'
|
|
33
34
|
| 'node_cloned'
|
|
35
|
+
| 'node_joined'
|
|
34
36
|
| 'node_removed'
|
|
35
37
|
| 'coordinator_started'
|
|
36
38
|
| 'recovery_attempted'
|
|
37
39
|
| 'ledger_replicated'
|
|
38
40
|
| 'ledger_reconciled'
|
|
41
|
+
| 'direct_fast_forward'
|
|
39
42
|
;
|
|
40
43
|
|
|
41
44
|
export interface MeshLedgerEntry {
|
|
@@ -61,6 +64,44 @@ export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind
|
|
|
61
64
|
|| payload.source === 'mesh_remove_node');
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
export type MeshWorkerResultStatus = 'completed' | 'failed' | 'blocked' | 'partial' | 'unknown';
|
|
68
|
+
export type MeshProcessArtifactKind = 'process' | 'log' | 'port' | 'window' | 'session' | 'file' | 'url' | 'other';
|
|
69
|
+
|
|
70
|
+
export interface MeshValidationResultArtifact {
|
|
71
|
+
command?: string;
|
|
72
|
+
status: 'passed' | 'failed' | 'skipped' | 'unknown';
|
|
73
|
+
durationMs?: number;
|
|
74
|
+
outputPath?: string;
|
|
75
|
+
summary?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface MeshProcessArtifact {
|
|
79
|
+
kind: MeshProcessArtifactKind;
|
|
80
|
+
id?: string;
|
|
81
|
+
label?: string;
|
|
82
|
+
locator?: string;
|
|
83
|
+
pid?: number;
|
|
84
|
+
port?: number;
|
|
85
|
+
url?: string;
|
|
86
|
+
path?: string;
|
|
87
|
+
sessionId?: string;
|
|
88
|
+
keepRunning?: boolean;
|
|
89
|
+
metadata?: Record<string, unknown>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MeshWorkerResultArtifact {
|
|
93
|
+
status: MeshWorkerResultStatus;
|
|
94
|
+
classification?: string;
|
|
95
|
+
changedFiles: string[];
|
|
96
|
+
validationResults: MeshValidationResultArtifact[];
|
|
97
|
+
gitStatus?: Record<string, unknown>;
|
|
98
|
+
processArtifacts: MeshProcessArtifact[];
|
|
99
|
+
errors: string[];
|
|
100
|
+
nextAction?: string;
|
|
101
|
+
requiresUserAction: boolean;
|
|
102
|
+
source: 'explicit_metadata' | 'final_summary_json' | 'default';
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
export interface MeshTaskCompletionEvidence {
|
|
65
106
|
source: 'agent_status_event';
|
|
66
107
|
event: 'agent:generating_completed' | 'agent:ready';
|
|
@@ -74,6 +115,7 @@ export interface MeshTaskCompletionEvidence {
|
|
|
74
115
|
providerSessionId?: string;
|
|
75
116
|
finalSummaryAvailable: boolean;
|
|
76
117
|
};
|
|
118
|
+
workerResult: MeshWorkerResultArtifact;
|
|
77
119
|
git: {
|
|
78
120
|
status: 'deferred';
|
|
79
121
|
reason: string;
|
|
@@ -96,6 +138,7 @@ export interface BuildTaskCompletionEvidenceOptions {
|
|
|
96
138
|
providerType?: string;
|
|
97
139
|
providerSessionId?: string;
|
|
98
140
|
finalSummary?: string;
|
|
141
|
+
workerResult?: Record<string, unknown>;
|
|
99
142
|
completedAt?: string;
|
|
100
143
|
}
|
|
101
144
|
|
|
@@ -160,8 +203,19 @@ export interface AppendRemoteLedgerResult {
|
|
|
160
203
|
// ─── Constants ──────────────────────────────────
|
|
161
204
|
|
|
162
205
|
const LEDGER_DIR_NAME = 'mesh-ledger';
|
|
163
|
-
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
206
|
+
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB — full rotation threshold
|
|
207
|
+
const COMPACT_THRESHOLD_BYTES = 2 * 1024 * 1024; // 2 MB — compaction threshold
|
|
208
|
+
const ARCHIVE_TERMINAL_OLDER_THAN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
164
209
|
const RECENT_FAILURE_WINDOW_MS = 30 * 60 * 1000; // 30 minutes
|
|
210
|
+
|
|
211
|
+
// Kinds that accumulate indefinitely and are safe to archive after ARCHIVE_TERMINAL_OLDER_THAN_MS.
|
|
212
|
+
// Non-terminal kinds (dispatched, sessions, nodes, checkpoints) are always kept in the active file.
|
|
213
|
+
const ARCHIVABLE_KINDS: ReadonlySet<MeshLedgerKind> = new Set([
|
|
214
|
+
'task_completed',
|
|
215
|
+
'task_failed',
|
|
216
|
+
'task_stalled',
|
|
217
|
+
'recovery_attempted',
|
|
218
|
+
] as MeshLedgerKind[]);
|
|
165
219
|
const DEFAULT_LEDGER_SLICE_LIMIT = 100;
|
|
166
220
|
export const MAX_LEDGER_SLICE_LIMIT = 500;
|
|
167
221
|
|
|
@@ -186,8 +240,251 @@ function getRotatedPath(meshId: string, index: number): string {
|
|
|
186
240
|
return join(getLedgerDir(), `${safe}.${index}.jsonl`);
|
|
187
241
|
}
|
|
188
242
|
|
|
243
|
+
function getArchivePath(meshId: string): string {
|
|
244
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
245
|
+
return join(getLedgerDir(), `${safe}.archive.jsonl`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getRotatedArchivePath(meshId: string, index: number): string {
|
|
249
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
250
|
+
return join(getLedgerDir(), `${safe}.archive.${index}.jsonl`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getArchivedCountsPath(meshId: string): string {
|
|
254
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
255
|
+
return join(getLedgerDir(), `${safe}.archived-counts.json`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function rotateArchiveFile(meshId: string, archivePath: string): void {
|
|
259
|
+
let index = 1;
|
|
260
|
+
while (existsSync(getRotatedArchivePath(meshId, index))) {
|
|
261
|
+
index++;
|
|
262
|
+
if (index > 5) break;
|
|
263
|
+
}
|
|
264
|
+
if (index > 5) index = 5;
|
|
265
|
+
try {
|
|
266
|
+
renameSync(archivePath, getRotatedArchivePath(meshId, index));
|
|
267
|
+
} catch (e: any) {
|
|
268
|
+
process.stderr.write(`[adhdev-mesh] Archive rotation failed for mesh ${meshId}: ${e?.message || e}\n`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface LedgerArchivedCounts {
|
|
273
|
+
taskCompleted: number;
|
|
274
|
+
taskFailed: number;
|
|
275
|
+
taskStalled: number;
|
|
276
|
+
recoveryAttempted: number;
|
|
277
|
+
totalArchived: number;
|
|
278
|
+
lastArchivedAt: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function readArchivedCounts(meshId: string): LedgerArchivedCounts {
|
|
282
|
+
const path = getArchivedCountsPath(meshId);
|
|
283
|
+
if (!existsSync(path)) return { taskCompleted: 0, taskFailed: 0, taskStalled: 0, recoveryAttempted: 0, totalArchived: 0, lastArchivedAt: '' };
|
|
284
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')) as LedgerArchivedCounts; } catch { return { taskCompleted: 0, taskFailed: 0, taskStalled: 0, recoveryAttempted: 0, totalArchived: 0, lastArchivedAt: '' }; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function updateArchivedCounts(meshId: string, archived: MeshLedgerEntry[]): void {
|
|
288
|
+
const counts = readArchivedCounts(meshId);
|
|
289
|
+
for (const e of archived) {
|
|
290
|
+
if (e.kind === 'task_completed') counts.taskCompleted++;
|
|
291
|
+
else if (e.kind === 'task_failed') counts.taskFailed++;
|
|
292
|
+
else if (e.kind === 'task_stalled') counts.taskStalled++;
|
|
293
|
+
else if (e.kind === 'recovery_attempted') counts.recoveryAttempted++;
|
|
294
|
+
}
|
|
295
|
+
counts.totalArchived += archived.length;
|
|
296
|
+
counts.lastArchivedAt = new Date().toISOString();
|
|
297
|
+
try { writeFileSync(getArchivedCountsPath(meshId), JSON.stringify(counts), { encoding: 'utf-8', mode: 0o600 }); } catch { /* best-effort */ }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── Worker Result Footer ───────────────────────
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Footer to append to worker task messages so workers output structured results
|
|
304
|
+
* that the daemon parses via extractJsonObjectFromSummary / normalizeMeshWorkerResult.
|
|
305
|
+
*
|
|
306
|
+
* Usage: append buildWorkerTaskFooter() to the task message in mesh_send_task /
|
|
307
|
+
* mesh_enqueue_task. The coordinator prompt rules instruct coordinators to do this.
|
|
308
|
+
*/
|
|
309
|
+
export function buildWorkerTaskFooter(): string {
|
|
310
|
+
return `
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
When your task is done, end your final response with a JSON code block in this exact format (omit fields that don't apply):
|
|
314
|
+
\`\`\`json
|
|
315
|
+
{
|
|
316
|
+
"status": "completed",
|
|
317
|
+
"changedFiles": ["src/foo.ts", "tests/foo.test.ts"],
|
|
318
|
+
"gitStatus": { "branch": "feat/your-branch", "committed": true, "pushed": false },
|
|
319
|
+
"validationResults": [{ "command": "npm test", "status": "passed" }],
|
|
320
|
+
"errors": [],
|
|
321
|
+
"nextAction": "optional guidance for the coordinator"
|
|
322
|
+
}
|
|
323
|
+
\`\`\`
|
|
324
|
+
Valid status values: \`completed\` | \`failed\` | \`blocked\` | \`partial\`.`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Ledger Compaction ──────────────────────────
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Compact the active ledger file for a mesh by moving old terminal entries
|
|
331
|
+
* (task_completed, task_failed, task_stalled, recovery_attempted older than 7 days)
|
|
332
|
+
* to <meshId>.archive.jsonl, keeping the active file lean.
|
|
333
|
+
*
|
|
334
|
+
* Non-terminal entries (dispatch, sessions, node lifecycle) are always retained.
|
|
335
|
+
* Called automatically from appendLedgerEntry when the file exceeds COMPACT_THRESHOLD_BYTES.
|
|
336
|
+
*/
|
|
337
|
+
export function compactLedger(meshId: string): { archivedCount: number; retainedCount: number } {
|
|
338
|
+
const filePath = getLedgerPath(meshId);
|
|
339
|
+
if (!existsSync(filePath)) return { archivedCount: 0, retainedCount: 0 };
|
|
340
|
+
|
|
341
|
+
const cutoff = Date.now() - ARCHIVE_TERMINAL_OLDER_THAN_MS;
|
|
342
|
+
const entries = readLedgerEntries(meshId);
|
|
343
|
+
|
|
344
|
+
const keep: MeshLedgerEntry[] = [];
|
|
345
|
+
const archive: MeshLedgerEntry[] = [];
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
if (ARCHIVABLE_KINDS.has(entry.kind) && new Date(entry.timestamp).getTime() < cutoff) {
|
|
348
|
+
archive.push(entry);
|
|
349
|
+
} else {
|
|
350
|
+
keep.push(entry);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (archive.length === 0) return { archivedCount: 0, retainedCount: keep.length };
|
|
355
|
+
|
|
356
|
+
// Append archived entries to the archive file, rotate if it exceeds 50MB
|
|
357
|
+
const archivePath = getArchivePath(meshId);
|
|
358
|
+
try {
|
|
359
|
+
if (existsSync(archivePath) && statSync(archivePath).size > 50 * 1024 * 1024) {
|
|
360
|
+
rotateArchiveFile(meshId, archivePath);
|
|
361
|
+
}
|
|
362
|
+
const archiveLines = archive.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
363
|
+
appendFileSync(archivePath, archiveLines, { encoding: 'utf-8', mode: 0o600 });
|
|
364
|
+
updateArchivedCounts(meshId, archive);
|
|
365
|
+
} catch (e: any) {
|
|
366
|
+
process.stderr.write(`[adhdev-mesh] Ledger archive write failed for mesh ${meshId}: ${e?.message || e}\n`);
|
|
367
|
+
return { archivedCount: 0, retainedCount: entries.length };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Rewrite active file with retained entries only
|
|
371
|
+
try {
|
|
372
|
+
const keepLines = keep.length ? keep.map(e => JSON.stringify(e)).join('\n') + '\n' : '';
|
|
373
|
+
writeFileSync(filePath, keepLines, { encoding: 'utf-8', mode: 0o600 });
|
|
374
|
+
invalidateLedgerCache(meshId);
|
|
375
|
+
} catch (e: any) {
|
|
376
|
+
process.stderr.write(`[adhdev-mesh] Ledger compaction rewrite failed for mesh ${meshId}: ${e?.message || e}\n`);
|
|
377
|
+
return { archivedCount: archive.length, retainedCount: keep.length };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { archivedCount: archive.length, retainedCount: keep.length };
|
|
381
|
+
}
|
|
382
|
+
|
|
189
383
|
// ─── Core API ───────────────────────────────────
|
|
190
384
|
|
|
385
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
386
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function readStringArray(value: unknown): string[] {
|
|
390
|
+
if (!Array.isArray(value)) return [];
|
|
391
|
+
return value.map(item => readNonEmptyString(item)).filter(Boolean) as string[];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function extractJsonObjectFromSummary(summary?: string): Record<string, unknown> | undefined {
|
|
395
|
+
const text = readNonEmptyString(summary);
|
|
396
|
+
if (!text) return undefined;
|
|
397
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
398
|
+
const candidates = [fenced?.[1], text].filter(Boolean) as string[];
|
|
399
|
+
for (const candidate of candidates) {
|
|
400
|
+
const trimmed = candidate.trim();
|
|
401
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
|
|
402
|
+
try {
|
|
403
|
+
const parsed = JSON.parse(trimmed);
|
|
404
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
405
|
+
// Require at least one mesh worker result field to avoid false positives
|
|
406
|
+
// (e.g. JSON from tool call outputs or log lines in the final summary).
|
|
407
|
+
const hasWorkerShape = 'status' in parsed && (
|
|
408
|
+
'changedFiles' in parsed || 'errors' in parsed
|
|
409
|
+
|| 'gitStatus' in parsed || 'nextAction' in parsed
|
|
410
|
+
|| 'validationResults' in parsed
|
|
411
|
+
);
|
|
412
|
+
if (hasWorkerShape) return parsed;
|
|
413
|
+
}
|
|
414
|
+
} catch { /* try next candidate */ }
|
|
415
|
+
}
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function normalizeValidationResults(value: unknown): MeshValidationResultArtifact[] {
|
|
420
|
+
if (!Array.isArray(value)) return [];
|
|
421
|
+
return value
|
|
422
|
+
.filter(item => item && typeof item === 'object' && !Array.isArray(item))
|
|
423
|
+
.map((item: any) => {
|
|
424
|
+
const status = ['passed', 'failed', 'skipped', 'unknown'].includes(item.status) ? item.status : 'unknown';
|
|
425
|
+
return {
|
|
426
|
+
...(readNonEmptyString(item.command) ? { command: readNonEmptyString(item.command) } : {}),
|
|
427
|
+
status,
|
|
428
|
+
...(Number.isFinite(Number(item.durationMs)) ? { durationMs: Number(item.durationMs) } : {}),
|
|
429
|
+
...(readNonEmptyString(item.outputPath) ? { outputPath: readNonEmptyString(item.outputPath) } : {}),
|
|
430
|
+
...(readNonEmptyString(item.summary) ? { summary: readNonEmptyString(item.summary) } : {}),
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function normalizeProcessArtifacts(value: unknown): MeshProcessArtifact[] {
|
|
436
|
+
if (!Array.isArray(value)) return [];
|
|
437
|
+
const kinds = new Set(['process', 'log', 'port', 'window', 'session', 'file', 'url', 'other']);
|
|
438
|
+
return value
|
|
439
|
+
.filter(item => item && typeof item === 'object' && !Array.isArray(item))
|
|
440
|
+
.map((item: any) => ({
|
|
441
|
+
kind: kinds.has(item.kind) ? item.kind : 'other',
|
|
442
|
+
...(readNonEmptyString(item.id) ? { id: readNonEmptyString(item.id) } : {}),
|
|
443
|
+
...(readNonEmptyString(item.label) ? { label: readNonEmptyString(item.label) } : {}),
|
|
444
|
+
...(readNonEmptyString(item.locator) ? { locator: readNonEmptyString(item.locator) } : {}),
|
|
445
|
+
...(Number.isFinite(Number(item.pid)) ? { pid: Number(item.pid) } : {}),
|
|
446
|
+
...(Number.isFinite(Number(item.port)) ? { port: Number(item.port) } : {}),
|
|
447
|
+
...(readNonEmptyString(item.url) ? { url: readNonEmptyString(item.url) } : {}),
|
|
448
|
+
...(readNonEmptyString(item.path) ? { path: readNonEmptyString(item.path) } : {}),
|
|
449
|
+
...(readNonEmptyString(item.sessionId) ? { sessionId: readNonEmptyString(item.sessionId) } : {}),
|
|
450
|
+
...(typeof item.keepRunning === 'boolean' ? { keepRunning: item.keepRunning } : {}),
|
|
451
|
+
...(item.metadata && typeof item.metadata === 'object' && !Array.isArray(item.metadata) ? { metadata: item.metadata as Record<string, unknown> } : {}),
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function normalizeMeshWorkerResult(input?: Record<string, unknown>, source: MeshWorkerResultArtifact['source'] = 'explicit_metadata'): MeshWorkerResultArtifact {
|
|
456
|
+
const raw = input && typeof input === 'object' ? input : {};
|
|
457
|
+
const status = ['completed', 'failed', 'blocked', 'partial', 'unknown'].includes(String(raw.status))
|
|
458
|
+
? raw.status as MeshWorkerResultStatus
|
|
459
|
+
: 'unknown';
|
|
460
|
+
const gitStatus = raw.gitStatus && typeof raw.gitStatus === 'object' && !Array.isArray(raw.gitStatus)
|
|
461
|
+
? raw.gitStatus as Record<string, unknown>
|
|
462
|
+
: undefined;
|
|
463
|
+
return {
|
|
464
|
+
status,
|
|
465
|
+
...(readNonEmptyString(raw.classification) ? { classification: readNonEmptyString(raw.classification) } : {}),
|
|
466
|
+
changedFiles: readStringArray(raw.changedFiles),
|
|
467
|
+
validationResults: normalizeValidationResults(raw.validationResults),
|
|
468
|
+
...(gitStatus ? { gitStatus } : {}),
|
|
469
|
+
processArtifacts: normalizeProcessArtifacts(raw.processArtifacts),
|
|
470
|
+
errors: readStringArray(raw.errors),
|
|
471
|
+
...(readNonEmptyString(raw.nextAction) ? { nextAction: readNonEmptyString(raw.nextAction) } : {}),
|
|
472
|
+
requiresUserAction: raw.requiresUserAction === true,
|
|
473
|
+
source,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function resolveWorkerResult(opts: BuildTaskCompletionEvidenceOptions): MeshWorkerResultArtifact {
|
|
478
|
+
if (opts.workerResult && typeof opts.workerResult === 'object') {
|
|
479
|
+
return normalizeMeshWorkerResult(opts.workerResult, 'explicit_metadata');
|
|
480
|
+
}
|
|
481
|
+
const parsed = extractJsonObjectFromSummary(opts.finalSummary);
|
|
482
|
+
if (parsed) {
|
|
483
|
+
return normalizeMeshWorkerResult(parsed, 'final_summary_json');
|
|
484
|
+
}
|
|
485
|
+
return normalizeMeshWorkerResult(undefined, 'default');
|
|
486
|
+
}
|
|
487
|
+
|
|
191
488
|
export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence {
|
|
192
489
|
const providerSessionId = opts.providerSessionId?.trim() || undefined;
|
|
193
490
|
const providerType = opts.providerType?.trim() || undefined;
|
|
@@ -204,6 +501,7 @@ export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOpt
|
|
|
204
501
|
providerSessionId,
|
|
205
502
|
finalSummaryAvailable: typeof opts.finalSummary === 'string' && opts.finalSummary.trim().length > 0,
|
|
206
503
|
},
|
|
504
|
+
workerResult: resolveWorkerResult(opts),
|
|
207
505
|
git: {
|
|
208
506
|
status: 'deferred',
|
|
209
507
|
reason: 'ordinary_completion_git_status_not_checked',
|
|
@@ -239,12 +537,14 @@ export function appendLedgerEntry(
|
|
|
239
537
|
|
|
240
538
|
const filePath = getLedgerPath(meshId);
|
|
241
539
|
|
|
242
|
-
//
|
|
540
|
+
// Compact or rotate based on file size
|
|
243
541
|
if (existsSync(filePath)) {
|
|
244
542
|
try {
|
|
245
543
|
const stat = statSync(filePath);
|
|
246
544
|
if (stat.size >= MAX_FILE_SIZE_BYTES) {
|
|
247
545
|
rotateLedgerFile(meshId, filePath);
|
|
546
|
+
} else if (stat.size >= COMPACT_THRESHOLD_BYTES) {
|
|
547
|
+
compactLedger(meshId);
|
|
248
548
|
}
|
|
249
549
|
} catch {
|
|
250
550
|
// stat failed — proceed with append anyway
|
|
@@ -254,6 +554,7 @@ export function appendLedgerEntry(
|
|
|
254
554
|
try {
|
|
255
555
|
const line = JSON.stringify(entry) + '\n';
|
|
256
556
|
appendFileSync(filePath, line, { encoding: 'utf-8', mode: 0o600 });
|
|
557
|
+
invalidateLedgerCache(meshId);
|
|
257
558
|
meshLedgerEvents.emit('append', meshId, entry);
|
|
258
559
|
return entry;
|
|
259
560
|
} catch (e: any) {
|
|
@@ -285,8 +586,9 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
|
|
|
285
586
|
if (entries.length === 0) return { accepted: 0, skippedDuplicate: 0, rejectedInvalid: 0, entries: [] };
|
|
286
587
|
const ledgerPath = getLedgerPath(meshId);
|
|
287
588
|
|
|
288
|
-
//
|
|
289
|
-
|
|
589
|
+
// Dedup against recent entries only — P2P replication is incremental (cursor-based),
|
|
590
|
+
// so duplicates appear in the recent tail, not deep history.
|
|
591
|
+
const existing = new Set(readLedgerEntries(meshId, { tail: 1000 }).map(e => e.id));
|
|
290
592
|
const validEntries: MeshLedgerEntry[] = [];
|
|
291
593
|
let rejectedInvalid = 0;
|
|
292
594
|
let skippedDuplicate = 0;
|
|
@@ -310,6 +612,7 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
|
|
|
310
612
|
try {
|
|
311
613
|
const lines = validEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
312
614
|
appendFileSync(ledgerPath, lines, { encoding: 'utf-8', mode: 0o600 });
|
|
615
|
+
invalidateLedgerCache(meshId);
|
|
313
616
|
for (const entry of validEntries) {
|
|
314
617
|
meshLedgerEvents.emit('append', meshId, entry);
|
|
315
618
|
}
|
|
@@ -319,103 +622,81 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
|
|
|
319
622
|
}
|
|
320
623
|
}
|
|
321
624
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
625
|
+
// ─── Ledger Read Cache ─────────────────────────
|
|
626
|
+
// Absorbs repeated reads within a single event-processing burst (e.g. agent:stopped
|
|
627
|
+
// triggers shouldSuppressIntentionalCleanupStop, findRecentTerminalLedgerEvidence,
|
|
628
|
+
// hasDispatchAfterTerminal, and getSessionRecoveryContext — all reading the same file).
|
|
629
|
+
// TTL is 100ms: short enough to stay current, long enough to cover one event cycle.
|
|
630
|
+
// Cache is invalidated on every write (append, remote import, compaction).
|
|
631
|
+
|
|
632
|
+
const ledgerReadCache = new Map<string, { entries: MeshLedgerEntry[]; cachedAt: number }>();
|
|
633
|
+
const LEDGER_CACHE_TTL_MS = 100;
|
|
634
|
+
|
|
635
|
+
function readLedgerFile(meshId: string): MeshLedgerEntry[] {
|
|
326
636
|
const filePath = getLedgerPath(meshId);
|
|
327
637
|
if (!existsSync(filePath)) return [];
|
|
328
|
-
|
|
329
638
|
let content: string;
|
|
330
|
-
try {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
337
|
-
let entries: MeshLedgerEntry[] = [];
|
|
338
|
-
|
|
339
|
-
for (const line of lines) {
|
|
639
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch { return []; }
|
|
640
|
+
const entries: MeshLedgerEntry[] = [];
|
|
641
|
+
for (const line of content.split('\n')) {
|
|
642
|
+
if (!line.trim()) continue;
|
|
340
643
|
try {
|
|
341
644
|
const entry = JSON.parse(line) as MeshLedgerEntry;
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
} catch {
|
|
345
|
-
// Skip malformed lines
|
|
346
|
-
}
|
|
645
|
+
if (entry.id && entry.kind) entries.push(entry);
|
|
646
|
+
} catch { /* skip malformed lines */ }
|
|
347
647
|
}
|
|
648
|
+
return entries;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function getCachedRawEntries(meshId: string): MeshLedgerEntry[] {
|
|
652
|
+
const now = Date.now();
|
|
653
|
+
const cached = ledgerReadCache.get(meshId);
|
|
654
|
+
if (cached && now - cached.cachedAt < LEDGER_CACHE_TTL_MS) return cached.entries;
|
|
655
|
+
const entries = readLedgerFile(meshId);
|
|
656
|
+
ledgerReadCache.set(meshId, { entries, cachedAt: now });
|
|
657
|
+
return entries;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function invalidateLedgerCache(meshId: string): void {
|
|
661
|
+
ledgerReadCache.delete(meshId);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Read ledger entries with optional filtering.
|
|
666
|
+
*/
|
|
667
|
+
export function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[] {
|
|
668
|
+
let entries = getCachedRawEntries(meshId);
|
|
348
669
|
|
|
349
|
-
// Apply filters
|
|
350
670
|
if (opts?.since) {
|
|
351
671
|
const sinceDate = new Date(opts.since).getTime();
|
|
352
|
-
if (!isNaN(sinceDate))
|
|
353
|
-
entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
|
|
354
|
-
}
|
|
672
|
+
if (!isNaN(sinceDate)) entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
|
|
355
673
|
}
|
|
356
|
-
|
|
357
674
|
if (opts?.kind?.length) {
|
|
358
675
|
const kindSet = new Set(opts.kind);
|
|
359
676
|
entries = entries.filter(e => kindSet.has(e.kind));
|
|
360
677
|
}
|
|
361
|
-
|
|
362
|
-
// Apply tail (return last N entries)
|
|
363
678
|
if (opts?.tail && opts.tail > 0 && entries.length > opts.tail) {
|
|
364
679
|
entries = entries.slice(-opts.tail);
|
|
365
680
|
}
|
|
366
|
-
|
|
367
681
|
return entries;
|
|
368
682
|
}
|
|
369
683
|
|
|
370
684
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
* remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
|
|
685
|
+
* Build a ledger summary from pre-loaded entries. Used by both getLedgerSummary
|
|
686
|
+
* and readLedgerSlice so they share a single getCachedRawEntries() call.
|
|
374
687
|
*/
|
|
375
|
-
|
|
376
|
-
const
|
|
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
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Get a summary of mesh activity from the ledger.
|
|
406
|
-
*/
|
|
407
|
-
export function getLedgerSummary(meshId: string): MeshLedgerSummary {
|
|
408
|
-
const entries = readLedgerEntries(meshId);
|
|
688
|
+
function buildLedgerSummary(meshId: string, entries: MeshLedgerEntry[]): MeshLedgerSummary {
|
|
689
|
+
const archived = readArchivedCounts(meshId);
|
|
409
690
|
const now = Date.now();
|
|
410
691
|
const recentFailureCutoff = now - RECENT_FAILURE_WINDOW_MS;
|
|
411
692
|
|
|
412
693
|
const summary: MeshLedgerSummary = {
|
|
413
694
|
meshId,
|
|
414
|
-
totalEntries: entries.length,
|
|
695
|
+
totalEntries: entries.length + archived.totalArchived,
|
|
415
696
|
taskDispatched: 0,
|
|
416
|
-
taskCompleted:
|
|
417
|
-
taskFailed:
|
|
418
|
-
taskStalled:
|
|
697
|
+
taskCompleted: archived.taskCompleted,
|
|
698
|
+
taskFailed: archived.taskFailed,
|
|
699
|
+
taskStalled: archived.taskStalled,
|
|
419
700
|
sessionLaunched: 0,
|
|
420
701
|
checkpointCreated: 0,
|
|
421
702
|
lastActivityAt: null,
|
|
@@ -450,6 +731,59 @@ export function getLedgerSummary(meshId: string): MeshLedgerSummary {
|
|
|
450
731
|
return summary;
|
|
451
732
|
}
|
|
452
733
|
|
|
734
|
+
/**
|
|
735
|
+
* Read a bounded, cursor-addressable ledger slice for local-first/P2P replication.
|
|
736
|
+
* The result is intentionally small and self-describing so coordinators can query
|
|
737
|
+
* remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
|
|
738
|
+
*/
|
|
739
|
+
export function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice {
|
|
740
|
+
const limit = clampLedgerSliceLimit(opts?.limit);
|
|
741
|
+
// Load raw entries once and share between filtering, pagination, and summary.
|
|
742
|
+
const rawEntries = getCachedRawEntries(meshId);
|
|
743
|
+
|
|
744
|
+
let entries: MeshLedgerEntry[] = rawEntries;
|
|
745
|
+
if (opts?.since) {
|
|
746
|
+
const sinceDate = new Date(opts.since).getTime();
|
|
747
|
+
if (!isNaN(sinceDate)) entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
|
|
748
|
+
}
|
|
749
|
+
if (opts?.kind?.length) {
|
|
750
|
+
const kindSet = new Set(opts.kind);
|
|
751
|
+
entries = entries.filter(e => kindSet.has(e.kind));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const afterId = typeof opts?.afterId === 'string' && opts.afterId.trim() ? opts.afterId.trim() : null;
|
|
755
|
+
if (afterId) {
|
|
756
|
+
const index = entries.findIndex(entry => entry.id === afterId);
|
|
757
|
+
entries = index >= 0 ? entries.slice(index + 1) : entries;
|
|
758
|
+
}
|
|
759
|
+
const bounded = entries.slice(0, limit);
|
|
760
|
+
return {
|
|
761
|
+
protocol: 'adhdev.mesh.ledger.slice.v1',
|
|
762
|
+
meshId,
|
|
763
|
+
entries: bounded,
|
|
764
|
+
cursor: {
|
|
765
|
+
afterId,
|
|
766
|
+
nextAfterId: bounded.length ? bounded[bounded.length - 1].id : afterId,
|
|
767
|
+
limit,
|
|
768
|
+
hasMore: entries.length > bounded.length,
|
|
769
|
+
},
|
|
770
|
+
summary: buildLedgerSummary(meshId, rawEntries),
|
|
771
|
+
sourceOfTruth: {
|
|
772
|
+
kind: 'local_jsonl',
|
|
773
|
+
path: getLedgerPath(meshId),
|
|
774
|
+
bounded: true,
|
|
775
|
+
maxLimit: MAX_LEDGER_SLICE_LIMIT,
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Get a summary of mesh activity from the ledger.
|
|
782
|
+
*/
|
|
783
|
+
export function getLedgerSummary(meshId: string): MeshLedgerSummary {
|
|
784
|
+
return buildLedgerSummary(meshId, getCachedRawEntries(meshId));
|
|
785
|
+
}
|
|
786
|
+
|
|
453
787
|
// ─── Recovery Context ───────────────────────────
|
|
454
788
|
|
|
455
789
|
export interface SessionRecoveryContext {
|
|
@@ -484,46 +818,64 @@ export function getSessionRecoveryContext(
|
|
|
484
818
|
},
|
|
485
819
|
): SessionRecoveryContext {
|
|
486
820
|
const maxRetries = opts.maxRetries ?? 1;
|
|
487
|
-
|
|
821
|
+
// tail:500 is sufficient — task_dispatched is never archived (only terminal kinds are),
|
|
822
|
+
// so dispatch history is always present. The 30-min failure window means we never need
|
|
823
|
+
// more than a few dozen recent entries for consecutiveNodeFailures. Bounding to 500
|
|
824
|
+
// avoids a full O(n) scan for meshes with many historical entries.
|
|
825
|
+
const entries = readLedgerEntries(meshId, { tail: 500 });
|
|
488
826
|
|
|
489
|
-
//
|
|
827
|
+
// Single backward pass: find last task_dispatched AND count consecutive recent failures.
|
|
828
|
+
const now = Date.now();
|
|
829
|
+
const recentWindow = now - RECENT_FAILURE_WINDOW_MS;
|
|
490
830
|
let lastDispatch: MeshLedgerEntry | null = null;
|
|
831
|
+
let consecutiveNodeFailures = 0;
|
|
832
|
+
let failureCountDone = false;
|
|
491
833
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
492
834
|
const e = entries[i];
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
835
|
+
const ts = new Date(e.timestamp).getTime();
|
|
836
|
+
|
|
837
|
+
// Failure counting: scan until we exit the recent window or hit a chain-breaker
|
|
838
|
+
if (!failureCountDone) {
|
|
839
|
+
if (ts < recentWindow) {
|
|
840
|
+
failureCountDone = true;
|
|
841
|
+
} else if (opts.nodeId && e.nodeId !== opts.nodeId) {
|
|
842
|
+
// Entry for a different node — skip for failure counting but continue scanning for dispatch
|
|
843
|
+
} else if (e.kind === 'task_failed') {
|
|
844
|
+
if (!isIntentionalCleanupStopEntry(e)) consecutiveNodeFailures++;
|
|
845
|
+
} else if (e.kind === 'task_completed' || e.kind === 'task_dispatched') {
|
|
846
|
+
// A completion or new dispatch breaks the consecutive failure chain
|
|
847
|
+
failureCountDone = true;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Dispatch search: find the last dispatch matching this session or node
|
|
852
|
+
if (lastDispatch === null && e.kind === 'task_dispatched') {
|
|
853
|
+
if (opts.sessionId && e.sessionId === opts.sessionId) { lastDispatch = e; }
|
|
854
|
+
else if (!opts.sessionId && opts.nodeId && e.nodeId === opts.nodeId) { lastDispatch = e; }
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Stop once both tasks are done
|
|
858
|
+
if (lastDispatch !== null && failureCountDone) break;
|
|
496
859
|
}
|
|
497
860
|
|
|
498
861
|
const lastTaskMessage = typeof lastDispatch?.payload?.message === 'string'
|
|
499
862
|
? lastDispatch.payload.message
|
|
500
863
|
: null;
|
|
501
864
|
|
|
502
|
-
// Count
|
|
503
|
-
|
|
504
|
-
const recentWindow = now - RECENT_FAILURE_WINDOW_MS;
|
|
505
|
-
let consecutiveNodeFailures = 0;
|
|
506
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
507
|
-
const e = entries[i];
|
|
508
|
-
if (new Date(e.timestamp).getTime() < recentWindow) break;
|
|
509
|
-
if (opts.nodeId && e.nodeId !== opts.nodeId) continue;
|
|
510
|
-
if (e.kind === 'task_failed') {
|
|
511
|
-
if (isIntentionalCleanupStopEntry(e)) continue;
|
|
512
|
-
consecutiveNodeFailures++;
|
|
513
|
-
} else if (e.kind === 'task_completed' || e.kind === 'task_dispatched') {
|
|
514
|
-
// A completion or new dispatch breaks the consecutive failure chain
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Count how many times the same task was attempted (match by message prefix)
|
|
865
|
+
// Count how many times the same task was attempted.
|
|
866
|
+
// Prefer exact taskId match (payload.taskId) to avoid 200-char prefix collisions.
|
|
520
867
|
let taskAttemptCount = 0;
|
|
521
|
-
if (
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (e.payload
|
|
526
|
-
|
|
868
|
+
if (lastDispatch) {
|
|
869
|
+
const taskId = typeof lastDispatch.payload?.taskId === 'string' ? lastDispatch.payload.taskId : null;
|
|
870
|
+
if (taskId) {
|
|
871
|
+
for (const e of entries) {
|
|
872
|
+
if (e.kind === 'task_dispatched' && e.payload?.taskId === taskId) taskAttemptCount++;
|
|
873
|
+
}
|
|
874
|
+
} else if (lastTaskMessage) {
|
|
875
|
+
const prefix = lastTaskMessage.slice(0, 200);
|
|
876
|
+
for (const e of entries) {
|
|
877
|
+
if (e.kind === 'task_dispatched' && typeof e.payload?.message === 'string') {
|
|
878
|
+
if (e.payload.message.startsWith(prefix)) taskAttemptCount++;
|
|
527
879
|
}
|
|
528
880
|
}
|
|
529
881
|
}
|
|
@@ -573,7 +925,8 @@ function rotateLedgerFile(meshId: string, currentPath: string): void {
|
|
|
573
925
|
|
|
574
926
|
try {
|
|
575
927
|
renameSync(currentPath, getRotatedPath(meshId, index));
|
|
576
|
-
} catch {
|
|
928
|
+
} catch (e: any) {
|
|
577
929
|
// Rotation failed — the next append will just grow the file
|
|
930
|
+
process.stderr.write(`[adhdev-mesh] Ledger rotation failed for mesh ${meshId}: ${e?.message || e}. File will continue to grow.\n`);
|
|
578
931
|
}
|
|
579
932
|
}
|