@adhdev/daemon-core 0.9.76 → 0.9.77-rc.10

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.
@@ -8,6 +8,17 @@ export interface PendingMeshCoordinatorEvent {
8
8
  }
9
9
  /** Drain and return all pending coordinator events, clearing the queue. */
10
10
  export declare function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[];
11
+ export declare function tryAssignQueueTask(components: {
12
+ cliManager: any;
13
+ }, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
14
+ /**
15
+ * Triggers a queue check for all nodes in the mesh.
16
+ * Called when a new task is enqueued, in case nodes are already idle.
17
+ */
18
+ export declare function triggerMeshQueue(components: {
19
+ instanceManager: any;
20
+ cliManager: any;
21
+ }, meshId: string): void;
11
22
  export declare function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>): {
12
23
  success: boolean;
13
24
  forwarded: number;
@@ -15,6 +26,5 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
15
26
  } | {
16
27
  success: boolean;
17
28
  error: string;
18
- forwarded?: undefined;
19
29
  };
20
30
  export declare function setupMeshEventForwarding(components: DaemonComponents): void;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Mesh Task Ledger — GasTown-inspired append-only JSONL task history
3
+ *
4
+ * Records all mesh orchestration events (task dispatch, completion, failure,
5
+ * checkpoint, node lifecycle) as an append-only JSONL file per mesh.
6
+ *
7
+ * Inspired by GasTown's "Beads" pattern: every action is a versioned record
8
+ * that persists across agent sessions, enabling recovery, auditing, and
9
+ * continuity when individual sessions fail or context windows are exhausted.
10
+ *
11
+ * Storage: ~/.adhdev/mesh-ledger/<meshId>.jsonl
12
+ * Format: One JSON object per line, newest entries appended at end
13
+ * Safety: mode 0o600, atomic append via appendFileSync
14
+ */
15
+ import { EventEmitter } from 'events';
16
+ export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'session_launched' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_removed' | 'coordinator_started' | 'recovery_attempted';
17
+ export interface MeshLedgerEntry {
18
+ id: string;
19
+ meshId: string;
20
+ timestamp: string;
21
+ kind: MeshLedgerKind;
22
+ nodeId?: string;
23
+ sessionId?: string;
24
+ providerType?: string;
25
+ payload: Record<string, unknown>;
26
+ }
27
+ export interface MeshLedgerSummary {
28
+ meshId: string;
29
+ totalEntries: number;
30
+ taskDispatched: number;
31
+ taskCompleted: number;
32
+ taskFailed: number;
33
+ taskStalled: number;
34
+ sessionLaunched: number;
35
+ checkpointCreated: number;
36
+ lastActivityAt: string | null;
37
+ recentFailures: number;
38
+ }
39
+ export interface ReadLedgerOptions {
40
+ tail?: number;
41
+ since?: string;
42
+ kind?: MeshLedgerKind[];
43
+ }
44
+ export declare function getLedgerDir(): string;
45
+ /**
46
+ * Append a new entry to the mesh ledger.
47
+ * Handles file creation, rotation on size overflow, and atomic writes.
48
+ */
49
+ export declare const meshLedgerEvents: EventEmitter<[never]>;
50
+ export declare function appendLedgerEntry(meshId: string, partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>): MeshLedgerEntry;
51
+ /**
52
+ * Append entries received from the cloud to the local ledger.
53
+ * This skips deduplicated entries and just writes new ones.
54
+ */
55
+ export declare function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]): void;
56
+ /**
57
+ * Read ledger entries with optional filtering.
58
+ */
59
+ export declare function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[];
60
+ /**
61
+ * Get a summary of mesh activity from the ledger.
62
+ */
63
+ export declare function getLedgerSummary(meshId: string): MeshLedgerSummary;
64
+ export interface SessionRecoveryContext {
65
+ /** The original task message that was dispatched to this session/node */
66
+ lastTaskMessage: string | null;
67
+ /** The node that was running the failed task */
68
+ failedNodeId: string | null;
69
+ /** Session ID of the failed session */
70
+ failedSessionId: string | null;
71
+ /** Provider used for the failed session */
72
+ failedProviderType: string | null;
73
+ /** Number of consecutive failures for this node (within recent window) */
74
+ consecutiveNodeFailures: number;
75
+ /** Number of times this specific task was attempted (matched by truncated message prefix) */
76
+ taskAttemptCount: number;
77
+ /** Whether a retry is recommended based on maxRetries policy */
78
+ retryRecommended: boolean;
79
+ /** Human-readable recovery advice for the coordinator */
80
+ advice: string;
81
+ }
82
+ /**
83
+ * Build recovery context for a failed session.
84
+ * Looks up the ledger to find the original task, count failures, and advise on retry.
85
+ */
86
+ export declare function getSessionRecoveryContext(meshId: string, opts: {
87
+ sessionId?: string;
88
+ nodeId?: string;
89
+ maxRetries?: number;
90
+ }): SessionRecoveryContext;
@@ -26,6 +26,12 @@ export interface MeshSyncTransport {
26
26
  }>;
27
27
  /** DELETE /api/v1/repo-meshes/:id */
28
28
  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
+ }>;
29
35
  }
30
36
  export interface RemoteMeshRecord {
31
37
  id: string;
@@ -49,3 +55,7 @@ export interface MeshSyncResult {
49
55
  * Pull remote meshes that don't exist locally.
50
56
  */
51
57
  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>;
@@ -0,0 +1,50 @@
1
+ export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed';
2
+ export interface MeshWorkQueueEntry {
3
+ id: string;
4
+ meshId: string;
5
+ message: string;
6
+ status: MeshTaskStatus;
7
+ /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
8
+ targetNodeId?: string;
9
+ /** The node that actually claimed and is executing the task */
10
+ assignedNodeId?: string;
11
+ /** The session currently executing the task */
12
+ assignedSessionId?: string;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ }
16
+ /**
17
+ * Add a new task to the mesh queue.
18
+ */
19
+ export declare function enqueueTask(meshId: string, message: string, opts?: {
20
+ targetNodeId?: string;
21
+ }): MeshWorkQueueEntry;
22
+ /**
23
+ * Get all tasks in the queue, optionally filtered by status.
24
+ */
25
+ export declare function getQueue(meshId: string, opts?: {
26
+ status?: MeshTaskStatus[];
27
+ }): MeshWorkQueueEntry[];
28
+ /**
29
+ * Find the next pending task that this node is allowed to claim, and mark it as assigned.
30
+ */
31
+ export declare function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null;
32
+ /**
33
+ * Update the status of a specific task.
34
+ * Used when a session completes, fails, or stalls.
35
+ */
36
+ export declare function updateTaskStatus(meshId: string, taskId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
37
+ /**
38
+ * Update the status of the task currently assigned to a specific session.
39
+ */
40
+ export declare function updateSessionTaskStatus(meshId: string, sessionId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
41
+ export interface MeshWorkQueueStats {
42
+ pending: number;
43
+ assigned: number;
44
+ completed: number;
45
+ failed: number;
46
+ }
47
+ /**
48
+ * Return aggregate queue statistics for the given mesh.
49
+ */
50
+ export declare function getMeshQueueStats(meshId: string): MeshWorkQueueStats;
@@ -62,6 +62,12 @@ export interface RepoMeshPolicy {
62
62
  * runtimes are never stopped/deleted unless the mesh owner opts in.
63
63
  */
64
64
  sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
65
+ /**
66
+ * Maximum number of automatic retry recommendations for a failed task on the
67
+ * same node before the daemon advises the coordinator to escalate or reassign.
68
+ * Defaults to 1 (allow one retry). Set to 0 to disable auto-recovery advice.
69
+ */
70
+ maxTaskRetries?: number;
65
71
  }
66
72
  export interface RepoMeshRelatedRepo {
67
73
  /** Stable display label for an explicitly configured associated checkout. */
@@ -292,6 +292,12 @@ export interface SessionEntry {
292
292
  seenCompletionMarker?: string;
293
293
  surfaceHidden?: boolean;
294
294
  settings?: Record<string, any>;
295
+ meshQueueStats?: {
296
+ pending: number;
297
+ assigned: number;
298
+ completed: number;
299
+ failed: number;
300
+ };
295
301
  }
296
302
  /**
297
303
  * Compact session metadata stored in UserSessionDO and reused by server-side
@@ -330,6 +336,12 @@ export interface CompactSessionEntry {
330
336
  providerControls?: ProviderControlSchema[];
331
337
  summaryMetadata?: ProviderSummaryMetadata;
332
338
  settings?: Record<string, any>;
339
+ meshQueueStats?: {
340
+ pending: number;
341
+ assigned: number;
342
+ completed: number;
343
+ failed: number;
344
+ };
333
345
  }
334
346
  export type VersionUpdateReason = 'force_update_below' | 'major_minor_mismatch' | 'patch_mismatch' | 'daemon_ahead';
335
347
  export type ReleaseChannel = 'stable' | 'preview';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.76",
3
+ "version": "0.9.77-rc.10",
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",
@@ -195,6 +195,8 @@ export class ProviderCliAdapter implements CliAdapter {
195
195
 
196
196
  // ─── CLI Scripts (script-based parsing) ───
197
197
  private cliScripts: CliScripts;
198
+ /** Per-session opaque state object created by cliScripts.createState(), reset on stop. */
199
+ private scriptState: unknown = null;
198
200
  private runtimeSettings: Record<string, any> = {};
199
201
  /** Full accumulated rendered PTY transcript for parser/readback use */
200
202
  private accumulatedBuffer: string = '';
@@ -477,6 +479,9 @@ export class ProviderCliAdapter implements CliAdapter {
477
479
  this.cliScripts = scripts;
478
480
  this.parsedStatusCache = null;
479
481
  this.parseErrorMessage = null;
482
+ // Initialize per-session state: createState() is called once here and on script reload.
483
+ // The returned object lives until the PTY exits (scriptState = null on exit).
484
+ this.scriptState = typeof scripts.createState === 'function' ? scripts.createState() : null;
480
485
  const scriptNames = listCliScriptNames(scripts);
481
486
  LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
482
487
  }
@@ -610,6 +615,7 @@ export class ProviderCliAdapter implements CliAdapter {
610
615
  this.ready = false;
611
616
  this.startupParseGate = false;
612
617
  this.spawnAt = 0;
618
+ this.scriptState = null;
613
619
  this.onStatusChange?.();
614
620
  });
615
621
 
@@ -1470,7 +1476,7 @@ export class ProviderCliAdapter implements CliAdapter {
1470
1476
  scope: this.currentTurnScope,
1471
1477
  runtimeSettings: this.runtimeSettings,
1472
1478
  });
1473
- const session = this.cliScripts.parseSession({ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
1479
+ const session = this.cliScripts.parseSession(this.scriptState, { ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
1474
1480
  this.parseErrorMessage = null;
1475
1481
  return session && typeof session === 'object' ? session : null;
1476
1482
  } catch (e: any) {
@@ -1485,7 +1491,7 @@ export class ProviderCliAdapter implements CliAdapter {
1485
1491
  if (!this.cliScripts?.detectStatus) return null;
1486
1492
  try {
1487
1493
  const screenText = this.terminalScreen.getText();
1488
- const status = this.cliScripts.detectStatus({
1494
+ const status = this.cliScripts.detectStatus(this.scriptState, {
1489
1495
  tail: text.slice(-500),
1490
1496
  screenText,
1491
1497
  rawBuffer: this.accumulatedRawBuffer,
@@ -1505,7 +1511,7 @@ export class ProviderCliAdapter implements CliAdapter {
1505
1511
  try {
1506
1512
  const screenText = this.terminalScreen.getText();
1507
1513
  const buffer = screenText || this.accumulatedBuffer;
1508
- return this.cliScripts.parseApproval({
1514
+ return this.cliScripts.parseApproval(this.scriptState, {
1509
1515
  buffer,
1510
1516
  screenText,
1511
1517
  rawBuffer: this.accumulatedRawBuffer,
@@ -1640,7 +1646,7 @@ export class ProviderCliAdapter implements CliAdapter {
1640
1646
  scope: this.currentTurnScope,
1641
1647
  runtimeSettings: this.runtimeSettings,
1642
1648
  });
1643
- return await Promise.resolve(fn({
1649
+ return await Promise.resolve(fn(this.scriptState, {
1644
1650
  ...input,
1645
1651
  args: args && typeof args === 'object' ? { ...args } : {},
1646
1652
  }));
@@ -48,11 +48,21 @@ export interface ParsedSession {
48
48
  }
49
49
 
50
50
  export interface CliScripts {
51
- parseSession?: (input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
52
- detectStatus?: (input: CliStatusInput) => string | null;
53
- parseApproval?: (input: CliApprovalInput) => { message: string; buttons: string[] } | null;
51
+ /**
52
+ * Optional state factory. Called once per CLI session start (or script reload).
53
+ * The returned object is passed as the first argument to detectStatus, parseApproval,
54
+ * and parseSession on every invocation, allowing scripts to maintain per-session state
55
+ * (e.g. last-seen status, approval fingerprints, stability counters).
56
+ *
57
+ * Scripts that don't define createState() receive null as the state argument,
58
+ * making this change fully backward compatible.
59
+ */
60
+ createState?: () => unknown;
61
+ parseSession?: (state: unknown, input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
62
+ detectStatus?: (state: unknown, input: CliStatusInput) => string | null;
63
+ parseApproval?: (state: unknown, input: CliApprovalInput) => { message: string; buttons: string[] } | null;
54
64
  resolveAction?: (data: any) => string;
55
- [name: string]: ((input: any) => any) | undefined;
65
+ [name: string]: ((state: unknown, input: any) => any) | ((data: any) => any) | (() => unknown) | undefined;
56
66
  }
57
67
 
58
68
  export interface CliScreenLine {
@@ -1315,6 +1315,22 @@ export class DaemonCommandRouter {
1315
1315
  }
1316
1316
  }
1317
1317
 
1318
+ case 'get_mesh_ledger': {
1319
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1320
+ if (!meshId) return { success: false, error: 'meshId required' };
1321
+ try {
1322
+ const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
1323
+ const tail = typeof args?.tail === 'number' ? args.tail : 20;
1324
+ const since = typeof args?.since === 'string' ? args.since : undefined;
1325
+ const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
1326
+ const entries = readLedgerEntries(meshId, { tail, since, kind });
1327
+ const summary = getLedgerSummary(meshId);
1328
+ return { success: true, entries, summary };
1329
+ } catch (e: any) {
1330
+ return { success: false, error: e.message };
1331
+ }
1332
+ }
1333
+
1318
1334
  case 'add_mesh_node': {
1319
1335
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1320
1336
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
@@ -1394,6 +1410,65 @@ export class DaemonCommandRouter {
1394
1410
  }
1395
1411
  }
1396
1412
 
1413
+ case 'refine_mesh_node': {
1414
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1415
+ const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
1416
+ if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
1417
+ try {
1418
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
1419
+ const mesh = meshRecord?.mesh;
1420
+ const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
1421
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
1422
+
1423
+ if (!node.isLocalWorktree || !node.workspace) {
1424
+ return { success: false, error: `Refinery requires a local worktree node` };
1425
+ }
1426
+
1427
+ const sourceNode = node.clonedFromNodeId
1428
+ ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
1429
+ : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
1430
+ const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
1431
+ if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
1432
+
1433
+ const { execFile } = await import('node:child_process');
1434
+ const { promisify } = await import('node:util');
1435
+ const execFileAsync = promisify(execFile);
1436
+
1437
+ const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
1438
+ const branch = branchStdout.trim();
1439
+ if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
1440
+
1441
+ const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
1442
+ const baseBranch = baseBranchStdout.trim();
1443
+
1444
+ try {
1445
+ await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
1446
+ } catch (e: any) {
1447
+ return { success: false, error: `Merge failed (conflicts?): ${e.message}` };
1448
+ }
1449
+
1450
+ const removeResult = await this.execute('remove_mesh_node', {
1451
+ meshId,
1452
+ nodeId,
1453
+ sessionCleanupMode: 'kill',
1454
+ inlineMesh: args?.inlineMesh,
1455
+ });
1456
+
1457
+ try {
1458
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1459
+ appendLedgerEntry(meshId, {
1460
+ kind: 'node_removed',
1461
+ nodeId,
1462
+ payload: { refined: true, mergedBranch: branch, into: baseBranch },
1463
+ });
1464
+ } catch {}
1465
+
1466
+ return { success: true, merged: true, branch, into: baseBranch, removeResult };
1467
+ } catch (e: any) {
1468
+ return { success: false, error: e.message };
1469
+ }
1470
+ }
1471
+
1397
1472
  case 'remove_mesh_node': {
1398
1473
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1399
1474
  const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
@@ -1436,6 +1511,19 @@ export class DaemonCommandRouter {
1436
1511
  const { removeNode } = await import('../config/mesh-config.js');
1437
1512
  removed = removeNode(meshId, nodeId);
1438
1513
  }
1514
+
1515
+ // Record in task ledger
1516
+ if (removed) {
1517
+ try {
1518
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1519
+ appendLedgerEntry(meshId, {
1520
+ kind: 'node_removed',
1521
+ nodeId,
1522
+ payload: { worktree: !!node?.isLocalWorktree, sessionCleanupMode },
1523
+ });
1524
+ } catch { /* ledger append is best-effort */ }
1525
+ }
1526
+
1439
1527
  return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}) };
1440
1528
  } catch (e: any) {
1441
1529
  return { success: false, error: e.message };
@@ -1498,6 +1586,16 @@ export class DaemonCommandRouter {
1498
1586
  if (!node) return { success: false, error: 'Failed to register worktree node' };
1499
1587
  }
1500
1588
 
1589
+ // Record in task ledger
1590
+ try {
1591
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1592
+ appendLedgerEntry(meshId, {
1593
+ kind: 'node_cloned',
1594
+ nodeId: node.id,
1595
+ payload: { sourceNodeId, branch: result.branch, worktreePath: result.worktreePath },
1596
+ });
1597
+ } catch { /* ledger append is best-effort */ }
1598
+
1501
1599
  return {
1502
1600
  success: true,
1503
1601
  node,
@@ -1508,6 +1606,19 @@ export class DaemonCommandRouter {
1508
1606
  return { success: false, error: e.message };
1509
1607
  }
1510
1608
  }
1609
+ case 'trigger_mesh_queue': {
1610
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1611
+ if (!meshId) return { success: false, error: 'meshId required' };
1612
+ try {
1613
+ const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
1614
+ if (meshId) {
1615
+ triggerMeshQueue(this.deps as any, meshId);
1616
+ }
1617
+ return { success: true };
1618
+ } catch (e: any) {
1619
+ return { success: false, error: e.message };
1620
+ }
1621
+ }
1511
1622
 
1512
1623
  // ─── Mesh Coordinator Launch ───
1513
1624
  case 'launch_mesh_coordinator': {
@@ -1756,6 +1867,18 @@ export class DaemonCommandRouter {
1756
1867
  }
1757
1868
 
1758
1869
  LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
1870
+
1871
+ // Record coordinator launch in task ledger
1872
+ try {
1873
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1874
+ appendLedgerEntry(meshId, {
1875
+ kind: 'coordinator_started',
1876
+ sessionId: launchResult.sessionId || launchResult.id,
1877
+ providerType: cliType,
1878
+ payload: { workspace },
1879
+ });
1880
+ } catch { /* ledger append is best-effort */ }
1881
+
1759
1882
  return {
1760
1883
  success: true,
1761
1884
  meshId,
package/src/index.ts CHANGED
@@ -149,6 +149,17 @@ export type { CoordinatorPromptContext } from './mesh/coordinator-prompt.js';
149
149
  export { syncMeshes } from './mesh/mesh-sync.js';
150
150
  export type { MeshSyncTransport, MeshSyncResult, RemoteMeshRecord } from './mesh/mesh-sync.js';
151
151
 
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';
155
+
156
+ // ── Mesh Work Queue (GUPP) ──
157
+ export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus } from './mesh/mesh-work-queue.js';
158
+ export type { MeshWorkQueueEntry, MeshTaskStatus } from './mesh/mesh-work-queue.js';
159
+
160
+ // ── Mesh Events ──
161
+ export { triggerMeshQueue } from './mesh/mesh-events.js';
162
+
152
163
  // ── State Store ──
153
164
  export { loadState, saveState, resetState } from './config/state-store.js';
154
165
  export type { DaemonState } from './config/state-store.js';
@@ -133,6 +133,7 @@ const TOOLS_SECTION = `## Available Tools
133
133
  | \`mesh_launch_session\` | Start a new agent session on a node |
134
134
  | \`mesh_send_task\` | Send a task (natural language) to a running agent |
135
135
  | \`mesh_read_chat\` | Read an agent's recent messages to check progress |
136
+ | \`mesh_task_history\` | Read the task ledger — dispatches, completions, failures. Use to understand what has been done before deciding next steps |
136
137
  | \`mesh_git_status\` | Check git status on a specific node |
137
138
  | \`mesh_checkpoint\` | Create a git checkpoint on a node |
138
139
  | \`mesh_approve\` | Approve/reject a pending agent action |
@@ -145,18 +146,31 @@ Before doing any coordinator work, confirm that the actual callable tool list in
145
146
 
146
147
  const WORKFLOW_SECTION = `## Orchestration Workflow
147
148
 
148
- 1. **Assess** — Call \`mesh_status\` to see which nodes are healthy and available.
149
- 2. **Plan** — Decompose the user's request into independent tasks for parallel execution, or sequential tasks when dependencies exist.
150
- 3. **Delegate** — For each task:
151
- a. Pick the best node (consider: health, dirty state, current workload).
152
- b. If you need branch isolation for parallel work, call \`mesh_clone_node\` to create a worktree node first.
153
- c. If no session exists, call \`mesh_launch_session\` to start one.
154
- d. Call \`mesh_send_task\` with a **complete, self-contained** instruction that includes all context the agent needs (file paths, line numbers, what to change, why). Do not send partial instructions expecting future follow-up.
155
- 4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly just because the delegated session has not produced a final assistant message yet; tool/terminal activity means work may still be in progress. Do not call \`mesh_read_chat\` again within a few seconds for the same generating session; wait for the completion callback/status event instead unless you are debugging a real stall. Use at most one compact \`mesh_read_chat\` check after a completion/approval signal, an explicit user status request, or a real timeout/stall. Handle approvals via \`mesh_approve\`.
149
+ 1. **Assess** — Call \`mesh_status\` to see which nodes are healthy and available. Check \`mesh_task_history\` to understand what has already been done in this mesh — previous delegations, completions, and failures.
150
+ 2. **Plan** — Decompose the user's request into independent tasks for parallel execution, or sequential tasks when dependencies exist. If \`mesh_task_history\` shows a recent failure for a task, decide whether to retry or reassign.
151
+ 3. **Queue / Delegate** — The Mesh uses an autonomous pull-based Work Queue:
152
+ a. **General Tasks**: Enqueue tasks using \`mesh_enqueue_task\`. Idle node agents will automatically pull tasks from the queue and begin working.
153
+ b. **Node Preparation**: Call \`mesh_launch_session\` to ensure enough agent sessions are active to handle the queue. If you need branch isolation for parallel work, call \`mesh_clone_node\` to create a worktree node first.
154
+ c. **Targeted Tasks**: Use \`mesh_send_task\` only when you need to bypass the queue and force a specific node to execute a task immediately.
155
+ d. Always provide a **complete, self-contained** instruction that includes all context the agent needs (file paths, line numbers, what to change, why). Do not send partial instructions expecting future follow-up.
156
+ 4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly. Use \`mesh_view_queue\` to see the status of all pending, assigned, completed, and failed tasks. Do not call \`mesh_read_chat\` again within a few seconds for the same generating session. Use at most one compact \`mesh_read_chat\` check after a completion/approval signal. Handle approvals via \`mesh_approve\`.
156
157
  5. **Verify** — When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
157
158
  6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
158
159
  7. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
159
- 8. **Report** — Summarize what was done, what changed, and any issues.`;
160
+ 8. **Report** — Summarize what was done, what changed, and any issues.
161
+
162
+ ## Failure Recovery
163
+
164
+ When a node agent stops unexpectedly, the daemon automatically enriches the system message with **Recovery Context** that includes:
165
+ - The number of consecutive failures on that node
166
+ - The original task message (if recorded in the ledger)
167
+ - A recommendation: **retry**, **reassign**, or **escalate**
168
+
169
+ Follow these recovery rules:
170
+ 1. **If "Retry recommended"**: Re-launch the session on the same node (\`mesh_launch_session\`), then resend the original task (\`mesh_send_task\`). The system message includes the original task text.
171
+ 2. **If "Max retries exceeded"**: Do NOT retry on the same node. Either reassign the task to a different node, or inform the user that the task requires manual intervention.
172
+ 3. **If no recovery context**: The stop may be intentional (normal completion). Use \`mesh_read_chat\` once to verify, then move on.
173
+ 4. **Always record what happened**: After handling a failure, briefly note the outcome in your report to the user.`;
160
174
 
161
175
  function buildRulesSection(coordinatorCliType?: string): string {
162
176
  const coordinatorNote = coordinatorCliType
@@ -166,12 +180,13 @@ function buildRulesSection(coordinatorCliType?: string): string {
166
180
  return `## Rules
167
181
 
168
182
  - **Minimize coordinator context.** The coordinator's job is routing, not implementing. Do not read source files, run commands, or analyze code directly — delegate all of that to node agents. Your context should stay lean.
169
- - **Delegate analysis too.** If you need to understand a bug or explore the codebase, send that investigation as a task to a node. Do not do it yourself.
183
+ - **Delegate analysis too.** If you need to understand a bug or explore the codebase, send that investigation as a task to the queue or a node. Do not do it yourself.
170
184
  - **Respect explicit provider requests.** If the user names an agent/provider, pass the matching provider type to \`mesh_launch_session\`: Hermes → \`hermes-cli\`, Claude Code/Claude → \`claude-cli\`, Codex → \`codex-cli\`, Gemini → \`gemini-cli\`. Never substitute \`claude-cli\` just because the coordinator itself is Claude Code.
171
- - **Front-load the task message.** When calling \`mesh_send_task\`, include everything the agent needs: what files to touch, what the problem is, what the fix should look like. The agent won't ask follow-up questions.
185
+ - **Front-load the task message.** When calling \`mesh_enqueue_task\` or \`mesh_send_task\`, include everything the agent needs: what files to touch, what the problem is, what the fix should look like. The agent won't ask follow-up questions.
172
186
  - **Don't inspect code.** Treat delegated agent summaries as self-reports, not verification. Verify side effects via \`mesh_git_status\` (including related repo freshness when configured), not by reading source files.
173
187
  - **Don't over-parallelize.** Start with 1-2 concurrent tasks. Scale up if they succeed. Never launch a duplicate session or second worker solely because \`mesh_read_chat\` has no final assistant message while the delegated session is still showing tool/terminal activity.
174
- - **Handle failures gracefully.** If a task fails, read the chat to understand why, then retry or reassign.
188
+ - **Handle failures with context.** If a task fails, check \`mesh_task_history\` first to see if this task was attempted before and how it failed. Read the chat to understand why, then decide: retry on the same node, reassign to a different node, or escalate to the user.
189
+ - **Check history before starting.** At the beginning of a coordination session, call \`mesh_task_history\` to understand what was previously delegated and its outcomes. This prevents duplicate work and informs recovery decisions.
175
190
  - **Keep the user informed.** Report progress after each delegation round — one or two sentences, not a narration.
176
191
  - **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
177
192
  - **Never fabricate tool results.** Always call the actual tool; never pretend you did.