@adhdev/daemon-core 0.9.77-rc.5 → 0.9.77-rc.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +3 -0
  2. package/dist/cli-adapters/provider-cli-adapter.d.ts +4 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +14 -4
  4. package/dist/commands/mesh-coordinator.d.ts +10 -0
  5. package/dist/commands/router.d.ts +4 -1
  6. package/dist/config/mesh-config.d.ts +1 -0
  7. package/dist/git/git-worktree.d.ts +15 -2
  8. package/dist/index.d.ts +8 -4
  9. package/dist/index.js +1959 -291
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +1947 -291
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/mesh/mesh-events.d.ts +10 -7
  14. package/dist/mesh/mesh-ledger-reconciliation.d.ts +55 -0
  15. package/dist/mesh/mesh-ledger.d.ts +84 -4
  16. package/dist/mesh/mesh-sync.d.ts +4 -12
  17. package/dist/mesh/mesh-work-queue.d.ts +56 -1
  18. package/dist/mesh/p2p-relay-failure.d.ts +35 -0
  19. package/dist/providers/cli-provider-instance.d.ts +6 -0
  20. package/dist/repo-mesh-types.d.ts +2 -0
  21. package/dist/shared-types.d.ts +38 -0
  22. package/package.json +1 -1
  23. package/src/boot/daemon-lifecycle.ts +5 -0
  24. package/src/cli-adapters/provider-cli-adapter.ts +35 -4
  25. package/src/cli-adapters/provider-cli-shared.ts +14 -4
  26. package/src/commands/cli-manager.ts +0 -4
  27. package/src/commands/mesh-coordinator.ts +55 -7
  28. package/src/commands/router.ts +847 -26
  29. package/src/commands/stream-commands.ts +8 -1
  30. package/src/config/config.ts +2 -1
  31. package/src/config/mesh-config.ts +2 -0
  32. package/src/config/workspaces.ts +1 -1
  33. package/src/git/git-worktree.ts +56 -4
  34. package/src/index.d.ts +3 -0
  35. package/src/index.ts +20 -4
  36. package/src/mesh/coordinator-prompt.ts +21 -10
  37. package/src/mesh/mesh-events.ts +522 -22
  38. package/src/mesh/mesh-ledger-reconciliation.ts +115 -0
  39. package/src/mesh/mesh-ledger.ts +209 -8
  40. package/src/mesh/mesh-sync.ts +4 -34
  41. package/src/mesh/mesh-work-queue.ts +163 -10
  42. package/src/mesh/p2p-relay-failure.ts +152 -0
  43. package/src/providers/cli-provider-instance.ts +153 -30
  44. package/src/repo-mesh-types.ts +2 -0
  45. package/src/shared-types.ts +38 -0
@@ -8,20 +8,23 @@ 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;
11
+ export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
14
12
  /**
15
13
  * Triggers a queue check for all nodes in the mesh.
16
14
  * Called when a new task is enqueued, in case nodes are already idle.
17
15
  */
18
- export declare function triggerMeshQueue(components: {
19
- instanceManager: any;
20
- cliManager: any;
21
- }, meshId: string): void;
16
+ export declare function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<void>;
22
17
  export declare function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>): {
23
18
  success: boolean;
24
19
  forwarded: number;
20
+ suppressed: boolean;
21
+ intentionalCleanupStop: boolean;
22
+ error?: undefined;
23
+ } | {
24
+ success: boolean;
25
+ forwarded: number;
26
+ suppressed?: undefined;
27
+ intentionalCleanupStop?: undefined;
25
28
  error?: undefined;
26
29
  } | {
27
30
  success: boolean;
@@ -0,0 +1,55 @@
1
+ import type { AppendRemoteLedgerResult, MeshLedgerSlice, MeshLedgerSummary } from './mesh-ledger.js';
2
+ export type MeshLedgerReplicaStatus = 'local' | 'queried' | 'imported' | 'failed';
3
+ export interface MeshLedgerReplicaEvidence {
4
+ nodeId: string;
5
+ daemonId?: string;
6
+ status: MeshLedgerReplicaStatus;
7
+ transport: 'local' | 'p2p_datachannel';
8
+ protocol: 'adhdev.mesh.ledger.slice.v1';
9
+ entriesReceived: number;
10
+ entriesImported: number;
11
+ skippedDuplicate: number;
12
+ rejectedInvalid: number;
13
+ hasMore: boolean;
14
+ nextAfterId: string | null;
15
+ lastTimestamp: string | null;
16
+ summary?: MeshLedgerSummary;
17
+ error?: string;
18
+ noFallbackReason?: string;
19
+ }
20
+ export interface MeshLedgerReconciliationEvidence {
21
+ protocol: 'adhdev.mesh.ledger.reconciliation.v1';
22
+ meshId: string;
23
+ generatedAt: string;
24
+ sourceOfTruth: {
25
+ kind: 'coordinator_local_jsonl';
26
+ p2pOnly: true;
27
+ cloudD1LedgerSync: false;
28
+ notes: string;
29
+ };
30
+ replicas: MeshLedgerReplicaEvidence[];
31
+ totals: {
32
+ replicas: number;
33
+ queried: number;
34
+ failed: number;
35
+ entriesReceived: number;
36
+ entriesImported: number;
37
+ skippedDuplicate: number;
38
+ rejectedInvalid: number;
39
+ };
40
+ convergence: {
41
+ complete: boolean;
42
+ pendingNodes: string[];
43
+ failedNodes: string[];
44
+ };
45
+ }
46
+ export declare function buildMeshLedgerReplicaEvidence(args: {
47
+ nodeId: string;
48
+ daemonId?: string;
49
+ transport: 'local' | 'p2p_datachannel';
50
+ slice?: MeshLedgerSlice;
51
+ importResult?: AppendRemoteLedgerResult;
52
+ status?: MeshLedgerReplicaStatus;
53
+ error?: string;
54
+ }): MeshLedgerReplicaEvidence;
55
+ export declare function buildMeshLedgerReconciliationEvidence(meshId: string, replicas: MeshLedgerReplicaEvidence[]): MeshLedgerReconciliationEvidence;
@@ -13,7 +13,7 @@
13
13
  * Safety: mode 0o600, atomic append via appendFileSync
14
14
  */
15
15
  import { EventEmitter } from 'events';
16
- export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'session_launched' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_removed' | 'coordinator_started' | 'recovery_attempted';
16
+ export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled';
17
17
  export interface MeshLedgerEntry {
18
18
  id: string;
19
19
  meshId: string;
@@ -24,6 +24,43 @@ export interface MeshLedgerEntry {
24
24
  providerType?: string;
25
25
  payload: Record<string, unknown>;
26
26
  }
27
+ export declare function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind' | 'payload'>): boolean;
28
+ export interface MeshTaskCompletionEvidence {
29
+ source: 'agent_status_event';
30
+ event: 'agent:generating_completed' | 'agent:ready';
31
+ nodeId: string;
32
+ sessionId: string;
33
+ providerType?: string;
34
+ completedAt: string;
35
+ transcriptHandle: {
36
+ kind: 'provider_session' | 'runtime_session';
37
+ sessionId: string;
38
+ providerSessionId?: string;
39
+ finalSummaryAvailable: boolean;
40
+ };
41
+ git: {
42
+ status: 'deferred';
43
+ reason: string;
44
+ };
45
+ validation: {
46
+ status: 'deferred';
47
+ commandsRun: string[];
48
+ reason: string;
49
+ };
50
+ checkpoint: {
51
+ attempted: false;
52
+ reason: 'not_attempted_for_ordinary_completion';
53
+ };
54
+ }
55
+ export interface BuildTaskCompletionEvidenceOptions {
56
+ event: MeshTaskCompletionEvidence['event'];
57
+ nodeId: string;
58
+ sessionId: string;
59
+ providerType?: string;
60
+ providerSessionId?: string;
61
+ finalSummary?: string;
62
+ completedAt?: string;
63
+ }
27
64
  export interface MeshLedgerSummary {
28
65
  meshId: string;
29
66
  totalEntries: number;
@@ -41,7 +78,44 @@ export interface ReadLedgerOptions {
41
78
  since?: string;
42
79
  kind?: MeshLedgerKind[];
43
80
  }
81
+ export interface ReadLedgerSliceOptions {
82
+ /** Return entries strictly after this entry id. If not found, starts from the beginning of the filtered set. */
83
+ afterId?: string;
84
+ /** Return entries at or after this timestamp. */
85
+ since?: string;
86
+ /** Optional event kind filter. */
87
+ kind?: MeshLedgerKind[];
88
+ /** Maximum entries to return. Clamped to a bounded protocol maximum. */
89
+ limit?: number;
90
+ }
91
+ export interface MeshLedgerCursor {
92
+ afterId: string | null;
93
+ nextAfterId: string | null;
94
+ limit: number;
95
+ hasMore: boolean;
96
+ }
97
+ export interface MeshLedgerSlice {
98
+ protocol: 'adhdev.mesh.ledger.slice.v1';
99
+ meshId: string;
100
+ entries: MeshLedgerEntry[];
101
+ cursor: MeshLedgerCursor;
102
+ summary: MeshLedgerSummary;
103
+ sourceOfTruth: {
104
+ kind: 'local_jsonl';
105
+ path: string;
106
+ bounded: true;
107
+ maxLimit: number;
108
+ };
109
+ }
110
+ export interface AppendRemoteLedgerResult {
111
+ accepted: number;
112
+ skippedDuplicate: number;
113
+ rejectedInvalid: number;
114
+ entries: MeshLedgerEntry[];
115
+ }
116
+ export declare const MAX_LEDGER_SLICE_LIMIT = 500;
44
117
  export declare function getLedgerDir(): string;
118
+ export declare function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence;
45
119
  /**
46
120
  * Append a new entry to the mesh ledger.
47
121
  * Handles file creation, rotation on size overflow, and atomic writes.
@@ -49,14 +123,20 @@ export declare function getLedgerDir(): string;
49
123
  export declare const meshLedgerEvents: EventEmitter<[never]>;
50
124
  export declare function appendLedgerEntry(meshId: string, partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>): MeshLedgerEntry;
51
125
  /**
52
- * Append entries received from the cloud to the local ledger.
53
- * This skips deduplicated entries and just writes new ones.
126
+ * Append entries received over local-first/P2P ledger replication to the local ledger.
127
+ * This skips deduplicated entries and rejects malformed/cross-mesh entries.
54
128
  */
55
- export declare function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]): void;
129
+ export declare function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]): AppendRemoteLedgerResult;
56
130
  /**
57
131
  * Read ledger entries with optional filtering.
58
132
  */
59
133
  export declare function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[];
134
+ /**
135
+ * Read a bounded, cursor-addressable ledger slice for local-first/P2P replication.
136
+ * The result is intentionally small and self-describing so coordinators can query
137
+ * remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
138
+ */
139
+ export declare function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice;
60
140
  /**
61
141
  * Get a summary of mesh activity from the ledger.
62
142
  */
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Mesh Sync — Sync local mesh config to/from cloud D1
2
+ * Mesh Sync — Sync local mesh metadata to/from cloud D1
3
3
  *
4
4
  * When cloud is available, this module pushes local mesh config
5
5
  * to the server and pulls remote meshes that were created from
6
6
  * other machines. The local ~/.adhdev/meshes.json remains the
7
- * canonical source; cloud is a persistence/relay layer.
7
+ * canonical source; cloud is a membership/metadata layer only.
8
+ * Task/chat/ledger evidence remains local-first and must not be
9
+ * synchronized through Cloud/D1.
8
10
  *
9
11
  * This is called lazily (not on daemon startup) — only when the
10
12
  * user explicitly opens the mesh page or runs `adhdev mesh sync`.
@@ -26,12 +28,6 @@ export interface MeshSyncTransport {
26
28
  }>;
27
29
  /** DELETE /api/v1/repo-meshes/:id */
28
30
  deleteRemoteMesh(meshId: string): Promise<void>;
29
- /** POST /api/v1/repo-meshes/:id/ledger/sync */
30
- syncMeshLedger?(meshId: string, data: {
31
- newEntries: any[];
32
- }): Promise<{
33
- missingEntries: any[];
34
- }>;
35
31
  }
36
32
  export interface RemoteMeshRecord {
37
33
  id: string;
@@ -55,7 +51,3 @@ export interface MeshSyncResult {
55
51
  * Pull remote meshes that don't exist locally.
56
52
  */
57
53
  export declare function syncMeshes(transport: MeshSyncTransport): Promise<MeshSyncResult>;
58
- /**
59
- * Sync the task ledger for a specific mesh.
60
- */
61
- export declare function syncMeshLedger(meshId: string, transport: MeshSyncTransport): Promise<void>;
@@ -1,4 +1,8 @@
1
- export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed';
1
+ export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
2
+ export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
3
+ export type MeshHistoricalTaskStatus = Extract<MeshTaskStatus, 'completed' | 'failed' | 'cancelled'>;
4
+ export declare const ACTIVE_MESH_QUEUE_STATUSES: MeshActiveTaskStatus[];
5
+ export declare const HISTORICAL_MESH_QUEUE_STATUSES: MeshHistoricalTaskStatus[];
2
6
  export interface MeshWorkQueueEntry {
3
7
  id: string;
4
8
  meshId: string;
@@ -6,10 +10,28 @@ export interface MeshWorkQueueEntry {
6
10
  status: MeshTaskStatus;
7
11
  /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
8
12
  targetNodeId?: string;
13
+ /** If specified, only this runtime session can claim the task */
14
+ targetSessionId?: string;
9
15
  /** The node that actually claimed and is executing the task */
10
16
  assignedNodeId?: string;
11
17
  /** The session currently executing the task */
12
18
  assignedSessionId?: string;
19
+ /** Human/operator reason for terminal cancellation. */
20
+ cancelReason?: string;
21
+ cancelledAt?: string;
22
+ /** Human/operator reason for manually requeueing a task. */
23
+ requeueReason?: string;
24
+ requeuedAt?: string;
25
+ requeueCount?: number;
26
+ /** Last automatic queue session spin-up attempt, for mesh_view_queue/debug visibility. */
27
+ autoLaunch?: {
28
+ status: 'skipped' | 'started' | 'failed' | 'completed';
29
+ reason?: string;
30
+ nodeId?: string;
31
+ providerType?: string;
32
+ sessionId?: string;
33
+ updatedAt: string;
34
+ };
13
35
  createdAt: string;
14
36
  updatedAt: string;
15
37
  }
@@ -18,6 +40,7 @@ export interface MeshWorkQueueEntry {
18
40
  */
19
41
  export declare function enqueueTask(meshId: string, message: string, opts?: {
20
42
  targetNodeId?: string;
43
+ targetSessionId?: string;
21
44
  }): MeshWorkQueueEntry;
22
45
  /**
23
46
  * Get all tasks in the queue, optionally filtered by status.
@@ -34,15 +57,47 @@ export declare function claimNextTask(meshId: string, nodeId: string, sessionId:
34
57
  * Used when a session completes, fails, or stalls.
35
58
  */
36
59
  export declare function updateTaskStatus(meshId: string, taskId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
60
+ export declare function recordTaskAutoLaunch(meshId: string, taskId: string, autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>): MeshWorkQueueEntry | null;
61
+ /**
62
+ * Mark a queue task as manually cancelled without deleting audit history.
63
+ */
64
+ export declare function cancelTask(meshId: string, taskId: string, opts?: {
65
+ reason?: string;
66
+ }): MeshWorkQueueEntry | null;
67
+ /**
68
+ * Return a queue task to pending for retry. By default, dead session targeting
69
+ * and assigned ownership are cleared so stale assignments do not strand again.
70
+ */
71
+ export declare function requeueTask(meshId: string, taskId: string, opts?: {
72
+ reason?: string;
73
+ targetNodeId?: string;
74
+ targetSessionId?: string;
75
+ clearTargetNode?: boolean;
76
+ clearTargetSession?: boolean;
77
+ }): MeshWorkQueueEntry | null;
37
78
  /**
38
79
  * Update the status of the task currently assigned to a specific session.
39
80
  */
40
81
  export declare function updateSessionTaskStatus(meshId: string, sessionId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
41
82
  export interface MeshWorkQueueStats {
83
+ total: number;
84
+ active: number;
85
+ historical: number;
42
86
  pending: number;
43
87
  assigned: number;
44
88
  completed: number;
45
89
  failed: number;
90
+ cancelled: number;
91
+ /** Source-of-truth active queue counters; only pending/assigned are live work. */
92
+ activeCounts: Record<MeshActiveTaskStatus, number>;
93
+ /** Terminal ledger records kept for audit/history; never count as active work. */
94
+ historicalCounts: Record<MeshHistoricalTaskStatus, number>;
95
+ activeAssignments: Array<{
96
+ id: string;
97
+ nodeId?: string;
98
+ sessionId?: string;
99
+ message: string;
100
+ }>;
46
101
  }
47
102
  /**
48
103
  * Return aggregate queue statistics for the given mesh.
@@ -0,0 +1,35 @@
1
+ export type P2pRelayFailureCode = 'p2p_unavailable' | 'p2p_timeout' | 'p2p_not_connected' | 'p2p_datachannel_closed' | 'p2p_no_route' | 'p2p_daemon_offline' | 'mesh_logic_or_provider_failure';
2
+ export interface P2pRelayFailureContext {
3
+ command?: string;
4
+ targetDaemonId?: string;
5
+ }
6
+ export interface P2pRelayFailureClassification {
7
+ code: P2pRelayFailureCode;
8
+ reason: string;
9
+ transport: 'p2p' | 'unknown';
10
+ recoverable: boolean;
11
+ retryRecommended: boolean;
12
+ nextAction: string;
13
+ noFallbackReason: string;
14
+ }
15
+ export interface P2pRelayFailurePayload extends P2pRelayFailureClassification {
16
+ success: false;
17
+ error: string;
18
+ command?: string;
19
+ targetDaemonId?: string;
20
+ }
21
+ export declare function classifyP2pRelayFailure(error: unknown, _context?: P2pRelayFailureContext): P2pRelayFailureClassification;
22
+ export declare function isP2pRelayTransportFailure(error: unknown): boolean;
23
+ export declare function buildP2pRelayFailurePayload(error: unknown, context?: P2pRelayFailureContext): P2pRelayFailurePayload;
24
+ export declare class P2pRelayFailureError extends Error {
25
+ code: P2pRelayFailureCode;
26
+ reason: string;
27
+ transport: 'p2p' | 'unknown';
28
+ recoverable: boolean;
29
+ retryRecommended: boolean;
30
+ nextAction: string;
31
+ noFallbackReason: string;
32
+ command?: string;
33
+ targetDaemonId?: string;
34
+ constructor(message: string, context?: P2pRelayFailureContext);
35
+ }
@@ -100,6 +100,12 @@ export declare class CliProviderInstance implements ProviderInstance {
100
100
  private completedDebounceTimer;
101
101
  private completedDebouncePending;
102
102
  private enforceFreshSessionLaunchIfNeeded;
103
+ private completionHasFinalAssistantMessage;
104
+ private hasAdapterPendingResponse;
105
+ private shouldSuppressStaleParsedBusyStatus;
106
+ private getCompletedFinalizationBlockReason;
107
+ private scheduleCompletedDebounceFlush;
108
+ private flushCompletedDebounceIfFinalized;
103
109
  private maybeAutoApproveStatus;
104
110
  private detectStatusTransition;
105
111
  private pushEvent;
@@ -179,6 +179,8 @@ export interface LocalMeshNodeEntry {
179
179
  workspace: string;
180
180
  repoRoot?: string;
181
181
  daemonId?: string;
182
+ /** Machine registry ID that owns this workspace, when known. */
183
+ machineId?: string;
182
184
  userOverrides: Partial<RepoMeshNodeCapabilities>;
183
185
  policy: RepoMeshNodePolicy;
184
186
  /** For single-machine mesh: same daemon, different worktree */
@@ -293,10 +293,29 @@ export interface SessionEntry {
293
293
  surfaceHidden?: boolean;
294
294
  settings?: Record<string, any>;
295
295
  meshQueueStats?: {
296
+ total?: number;
297
+ active?: number;
298
+ historical?: number;
296
299
  pending: number;
297
300
  assigned: number;
298
301
  completed: number;
299
302
  failed: number;
303
+ cancelled?: number;
304
+ activeCounts?: {
305
+ pending: number;
306
+ assigned: number;
307
+ };
308
+ historicalCounts?: {
309
+ completed: number;
310
+ failed: number;
311
+ cancelled: number;
312
+ };
313
+ activeAssignments?: Array<{
314
+ id: string;
315
+ nodeId?: string;
316
+ sessionId?: string;
317
+ message: string;
318
+ }>;
300
319
  };
301
320
  }
302
321
  /**
@@ -337,10 +356,29 @@ export interface CompactSessionEntry {
337
356
  summaryMetadata?: ProviderSummaryMetadata;
338
357
  settings?: Record<string, any>;
339
358
  meshQueueStats?: {
359
+ total?: number;
360
+ active?: number;
361
+ historical?: number;
340
362
  pending: number;
341
363
  assigned: number;
342
364
  completed: number;
343
365
  failed: number;
366
+ cancelled?: number;
367
+ activeCounts?: {
368
+ pending: number;
369
+ assigned: number;
370
+ };
371
+ historicalCounts?: {
372
+ completed: number;
373
+ failed: number;
374
+ cancelled: number;
375
+ };
376
+ activeAssignments?: Array<{
377
+ id: string;
378
+ nodeId?: string;
379
+ sessionId?: string;
380
+ message: string;
381
+ }>;
344
382
  };
345
383
  }
346
384
  export type VersionUpdateReason = 'force_update_below' | 'major_minor_mismatch' | 'patch_mismatch' | 'daemon_ahead';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.77-rc.5",
3
+ "version": "0.9.77-rc.50",
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",
@@ -83,6 +83,9 @@ export interface DaemonInitConfig {
83
83
 
84
84
  /** Fired before send_chat is dispatched — used for turn snapshot hooks */
85
85
  onBeforeSendChat?: (params: { workspace: string; sessionId: string }) => void;
86
+
87
+ /** Relays a command to a remote mesh node daemon */
88
+ dispatchMeshCommand?: (daemonId: string, command: string, args: Record<string, unknown>) => Promise<any>;
86
89
  }
87
90
 
88
91
  // ─── Result ───
@@ -100,6 +103,7 @@ export interface DaemonComponents {
100
103
  sessionRegistry: SessionRegistry;
101
104
  detectedIdes: { value: IDEInfo[] };
102
105
  refreshProviderAvailability: (providerType?: string) => Promise<void>;
106
+ dispatchMeshCommand?: (daemonId: string, command: string, args: Record<string, unknown>) => Promise<any>;
103
107
  }
104
108
 
105
109
  export interface DaemonDevSupportOptions {
@@ -331,6 +335,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
331
335
  sessionRegistry,
332
336
  detectedIdes: detectedIdesRef,
333
337
  refreshProviderAvailability,
338
+ dispatchMeshCommand: config.dispatchMeshCommand,
334
339
  };
335
340
 
336
341
  // 11. Setup Mesh Event Forwarding
@@ -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 = '';
@@ -223,6 +225,7 @@ export class ProviderCliAdapter implements CliAdapter {
223
225
  currentTurnScope: TurnParseScope | null;
224
226
  recentOutputBuffer: string;
225
227
  accumulatedBuffer: string;
228
+ accumulatedRawBufferKey: string;
226
229
  screenText: string;
227
230
  currentStatus: CliSessionStatus['status'];
228
231
  activeModal: { message: string; buttons: string[] } | null;
@@ -297,14 +300,23 @@ export class ProviderCliAdapter implements CliAdapter {
297
300
  this.lastScreenSnapshotReadAt = Number.NEGATIVE_INFINITY;
298
301
  }
299
302
 
303
+ private getAccumulatedRawBufferCacheKey(): string {
304
+ return this.accumulatedRawBuffer
305
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
306
+ .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
307
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
308
+ }
309
+
300
310
  private getFreshParsedStatusCache(): any | null {
301
311
  const cached = this.parsedStatusCache;
312
+ const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
302
313
  if (
303
314
  cached
304
315
  && cached.responseBuffer === this.responseBuffer
305
316
  && cached.currentTurnScope === this.currentTurnScope
306
317
  && cached.recentOutputBuffer === this.recentOutputBuffer
307
318
  && cached.accumulatedBuffer === this.accumulatedBuffer
319
+ && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
308
320
  && cached.screenText === this.lastScreenText
309
321
  && cached.currentStatus === this.currentStatus
310
322
  && cached.activeModal === this.activeModal
@@ -448,6 +460,7 @@ export class ProviderCliAdapter implements CliAdapter {
448
460
 
449
461
  // Scripts are required — loaded by ProviderLoader via compatibility array
450
462
  this.cliScripts = provider.scripts || {};
463
+ this.scriptState = typeof this.cliScripts.createState === 'function' ? (this.cliScripts.createState() ?? null) : null;
451
464
  const scriptNames = listCliScriptNames(this.cliScripts);
452
465
  if (scriptNames.length > 0) {
453
466
  LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
@@ -477,6 +490,9 @@ export class ProviderCliAdapter implements CliAdapter {
477
490
  this.cliScripts = scripts;
478
491
  this.parsedStatusCache = null;
479
492
  this.parseErrorMessage = null;
493
+ // Initialize per-session state: createState() is called once here and on script reload.
494
+ // The returned object lives until the PTY exits (scriptState = null on exit).
495
+ this.scriptState = typeof scripts.createState === 'function' ? (scripts.createState() ?? null) : null;
480
496
  const scriptNames = listCliScriptNames(scripts);
481
497
  LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
482
498
  }
@@ -610,6 +626,7 @@ export class ProviderCliAdapter implements CliAdapter {
610
626
  this.ready = false;
611
627
  this.startupParseGate = false;
612
628
  this.spawnAt = 0;
629
+ this.scriptState = null;
613
630
  this.onStatusChange?.();
614
631
  });
615
632
 
@@ -1450,6 +1467,14 @@ export class ProviderCliAdapter implements CliAdapter {
1450
1467
 
1451
1468
  // ─── Script Execution ──────────────────────────
1452
1469
 
1470
+ private invokeCliScript<T>(script: Function, input: any): T {
1471
+ const hasStateFactory = typeof this.cliScripts?.createState === 'function';
1472
+ const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
1473
+ return expectsStateArgument
1474
+ ? script(this.scriptState, input)
1475
+ : script(input);
1476
+ }
1477
+
1453
1478
  private runParseSession(): ParsedSession | null {
1454
1479
  if (typeof this.cliScripts?.parseSession !== 'function') {
1455
1480
  this.parseErrorMessage = `${this.cliType} parseSession unavailable`;
@@ -1470,7 +1495,10 @@ export class ProviderCliAdapter implements CliAdapter {
1470
1495
  scope: this.currentTurnScope,
1471
1496
  runtimeSettings: this.runtimeSettings,
1472
1497
  });
1473
- const session = this.cliScripts.parseSession({ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
1498
+ const session = this.invokeCliScript<ParsedSession | null>(
1499
+ this.cliScripts.parseSession,
1500
+ { ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
1501
+ );
1474
1502
  this.parseErrorMessage = null;
1475
1503
  return session && typeof session === 'object' ? session : null;
1476
1504
  } catch (e: any) {
@@ -1485,7 +1513,7 @@ export class ProviderCliAdapter implements CliAdapter {
1485
1513
  if (!this.cliScripts?.detectStatus) return null;
1486
1514
  try {
1487
1515
  const screenText = this.terminalScreen.getText();
1488
- const status = this.cliScripts.detectStatus({
1516
+ const status = this.invokeCliScript<string | null>(this.cliScripts.detectStatus, {
1489
1517
  tail: text.slice(-500),
1490
1518
  screenText,
1491
1519
  rawBuffer: this.accumulatedRawBuffer,
@@ -1505,7 +1533,7 @@ export class ProviderCliAdapter implements CliAdapter {
1505
1533
  try {
1506
1534
  const screenText = this.terminalScreen.getText();
1507
1535
  const buffer = screenText || this.accumulatedBuffer;
1508
- return this.cliScripts.parseApproval({
1536
+ return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
1509
1537
  buffer,
1510
1538
  screenText,
1511
1539
  rawBuffer: this.accumulatedRawBuffer,
@@ -1570,12 +1598,14 @@ export class ProviderCliAdapter implements CliAdapter {
1570
1598
  const screenText = this.readTerminalScreenText();
1571
1599
  const parseScreenText = this.getParseScreenText(screenText);
1572
1600
  const cached = this.parsedStatusCache;
1601
+ const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
1573
1602
  if (
1574
1603
  cached
1575
1604
  && cached.responseBuffer === this.responseBuffer
1576
1605
  && cached.currentTurnScope === this.currentTurnScope
1577
1606
  && cached.recentOutputBuffer === this.recentOutputBuffer
1578
1607
  && cached.accumulatedBuffer === this.accumulatedBuffer
1608
+ && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
1579
1609
  && cached.screenText === parseScreenText
1580
1610
  && cached.currentStatus === this.currentStatus
1581
1611
  && cached.activeModal === this.activeModal
@@ -1615,6 +1645,7 @@ export class ProviderCliAdapter implements CliAdapter {
1615
1645
  currentTurnScope: this.currentTurnScope,
1616
1646
  recentOutputBuffer: this.recentOutputBuffer,
1617
1647
  accumulatedBuffer: this.accumulatedBuffer,
1648
+ accumulatedRawBufferKey,
1618
1649
  screenText: parseScreenText,
1619
1650
  currentStatus: this.currentStatus,
1620
1651
  activeModal: this.activeModal,
@@ -1640,7 +1671,7 @@ export class ProviderCliAdapter implements CliAdapter {
1640
1671
  scope: this.currentTurnScope,
1641
1672
  runtimeSettings: this.runtimeSettings,
1642
1673
  });
1643
- return await Promise.resolve(fn({
1674
+ return await Promise.resolve(this.invokeCliScript(fn, {
1644
1675
  ...input,
1645
1676
  args: args && typeof args === 'object' ? { ...args } : {},
1646
1677
  }));
@@ -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 {
@@ -177,10 +177,6 @@ export function buildCoordinatorDelegatedCliLaunchOptions(
177
177
  const cliArgs = Array.isArray(input.cliArgs) ? [...input.cliArgs] : [];
178
178
  const env: Record<string, string> = { ...(input.env || {}), ...COORDINATOR_DELEGATED_ENV_UNSETS };
179
179
 
180
- if (cliType === 'hermes-cli' && !hasCliArg(cliArgs, '--ignore-user-config')) {
181
- cliArgs.unshift('--ignore-user-config');
182
- }
183
-
184
180
  if (cliType === 'claude-cli' && !hasCliArg(cliArgs, '--mcp-config')) {
185
181
  cliArgs.unshift('--mcp-config', ensureEmptyDelegatedMcpConfig(input.workspace));
186
182
  }