@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.
@@ -0,0 +1,164 @@
1
+ import { existsSync, writeFileSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { randomUUID } from 'crypto';
4
+ import { getLedgerDir } from './mesh-ledger.js';
5
+
6
+ export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed';
7
+
8
+ export interface MeshWorkQueueEntry {
9
+ id: string;
10
+ meshId: string;
11
+ message: string;
12
+ status: MeshTaskStatus;
13
+ /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
14
+ targetNodeId?: string;
15
+ /** The node that actually claimed and is executing the task */
16
+ assignedNodeId?: string;
17
+ /** The session currently executing the task */
18
+ assignedSessionId?: string;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ }
22
+
23
+ function getQueuePath(meshId: string): string {
24
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
25
+ return join(getLedgerDir(), `${safe}.queue.json`);
26
+ }
27
+
28
+ function readQueue(meshId: string): MeshWorkQueueEntry[] {
29
+ const path = getQueuePath(meshId);
30
+ if (!existsSync(path)) return [];
31
+ try {
32
+ const content = readFileSync(path, 'utf-8');
33
+ return JSON.parse(content) as MeshWorkQueueEntry[];
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
40
+ const path = getQueuePath(meshId);
41
+ writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8');
42
+ }
43
+
44
+ /**
45
+ * Add a new task to the mesh queue.
46
+ */
47
+ export function enqueueTask(
48
+ meshId: string,
49
+ message: string,
50
+ opts?: { targetNodeId?: string }
51
+ ): MeshWorkQueueEntry {
52
+ const queue = readQueue(meshId);
53
+ const entry: MeshWorkQueueEntry = {
54
+ id: randomUUID(),
55
+ meshId,
56
+ message,
57
+ status: 'pending',
58
+ targetNodeId: opts?.targetNodeId,
59
+ createdAt: new Date().toISOString(),
60
+ updatedAt: new Date().toISOString(),
61
+ };
62
+ queue.push(entry);
63
+ writeQueue(meshId, queue);
64
+ return entry;
65
+ }
66
+
67
+ /**
68
+ * Get all tasks in the queue, optionally filtered by status.
69
+ */
70
+ export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): MeshWorkQueueEntry[] {
71
+ let queue = readQueue(meshId);
72
+ if (opts?.status?.length) {
73
+ const statuses = new Set(opts.status);
74
+ queue = queue.filter(q => statuses.has(q.status));
75
+ }
76
+ return queue;
77
+ }
78
+
79
+ /**
80
+ * Find the next pending task that this node is allowed to claim, and mark it as assigned.
81
+ */
82
+ export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
83
+ const queue = readQueue(meshId);
84
+
85
+ // Find highest priority task:
86
+ // 1. Pending tasks explicitly targeted at this node
87
+ // 2. Pending tasks with no target node
88
+ let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId);
89
+ if (targetIdx === -1) {
90
+ targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId);
91
+ }
92
+
93
+ if (targetIdx === -1) return null;
94
+
95
+ const entry = queue[targetIdx];
96
+ entry.status = 'assigned';
97
+ entry.assignedNodeId = nodeId;
98
+ entry.assignedSessionId = sessionId;
99
+ entry.updatedAt = new Date().toISOString();
100
+
101
+ writeQueue(meshId, queue);
102
+ return entry;
103
+ }
104
+
105
+ /**
106
+ * Update the status of a specific task.
107
+ * Used when a session completes, fails, or stalls.
108
+ */
109
+ export function updateTaskStatus(
110
+ meshId: string,
111
+ taskId: string,
112
+ status: MeshTaskStatus,
113
+ ): MeshWorkQueueEntry | null {
114
+ const queue = readQueue(meshId);
115
+ const idx = queue.findIndex(q => q.id === taskId);
116
+ if (idx === -1) return null;
117
+
118
+ queue[idx].status = status;
119
+ queue[idx].updatedAt = new Date().toISOString();
120
+ writeQueue(meshId, queue);
121
+ return queue[idx];
122
+ }
123
+
124
+ /**
125
+ * Update the status of the task currently assigned to a specific session.
126
+ */
127
+ export function updateSessionTaskStatus(
128
+ meshId: string,
129
+ sessionId: string,
130
+ status: MeshTaskStatus,
131
+ ): MeshWorkQueueEntry | null {
132
+ const queue = readQueue(meshId);
133
+ // Find the most recently assigned task for this session that isn't already terminal
134
+ // (In case multiple tasks were assigned to the same session over time, though rare)
135
+ for (let i = queue.length - 1; i >= 0; i--) {
136
+ if (queue[i].assignedSessionId === sessionId && queue[i].status === 'assigned') {
137
+ queue[i].status = status;
138
+ queue[i].updatedAt = new Date().toISOString();
139
+ writeQueue(meshId, queue);
140
+ return queue[i];
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+
146
+ export interface MeshWorkQueueStats {
147
+ pending: number;
148
+ assigned: number;
149
+ completed: number;
150
+ failed: number;
151
+ }
152
+
153
+ /**
154
+ * Return aggregate queue statistics for the given mesh.
155
+ */
156
+ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
157
+ const queue = readQueue(meshId);
158
+ return {
159
+ pending: queue.filter(q => q.status === 'pending').length,
160
+ assigned: queue.filter(q => q.status === 'assigned').length,
161
+ completed: queue.filter(q => q.status === 'completed').length,
162
+ failed: queue.filter(q => q.status === 'failed').length,
163
+ };
164
+ }
@@ -78,6 +78,12 @@ export interface RepoMeshPolicy {
78
78
  * runtimes are never stopped/deleted unless the mesh owner opts in.
79
79
  */
80
80
  sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
81
+ /**
82
+ * Maximum number of automatic retry recommendations for a failed task on the
83
+ * same node before the daemon advises the coordinator to escalate or reassign.
84
+ * Defaults to 1 (allow one retry). Set to 0 to disable auto-recovery advice.
85
+ */
86
+ maxTaskRetries?: number;
81
87
  }
82
88
 
83
89
  export interface RepoMeshRelatedRepo {
@@ -110,6 +116,7 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
110
116
  maxParallelTasks: 2,
111
117
  spawnedSessionVisibility: 'visible',
112
118
  sessionCleanupOnNodeRemove: 'preserve',
119
+ maxTaskRetries: 1,
113
120
  };
114
121
 
115
122
  // ─── Capabilities ───────────────────────────────
@@ -389,6 +389,12 @@ export interface SessionEntry {
389
389
  seenCompletionMarker?: string;
390
390
  surfaceHidden?: boolean;
391
391
  settings?: Record<string, any>;
392
+ meshQueueStats?: {
393
+ pending: number;
394
+ assigned: number;
395
+ completed: number;
396
+ failed: number;
397
+ };
392
398
  }
393
399
 
394
400
  /**
@@ -428,6 +434,12 @@ export interface CompactSessionEntry {
428
434
  providerControls?: ProviderControlSchema[];
429
435
  summaryMetadata?: ProviderSummaryMetadata;
430
436
  settings?: Record<string, any>;
437
+ meshQueueStats?: {
438
+ pending: number;
439
+ assigned: number;
440
+ completed: number;
441
+ failed: number;
442
+ };
431
443
  }
432
444
 
433
445
  export type VersionUpdateReason =
@@ -24,6 +24,7 @@ import {
24
24
  normalizeManagedStatus,
25
25
  type NormalizeActiveChatOptions,
26
26
  } from './normalize.js';
27
+ import { getMeshQueueStats } from '../mesh/mesh-work-queue.js';
27
28
  import { normalizeProviderStateControlValues } from '../providers/provider-patch-state.js';
28
29
  import { normalizeProviderSummaryMetadata } from '../providers/summary-metadata.js';
29
30
  import {
@@ -177,6 +178,8 @@ function buildIdeWorkspaceSession(
177
178
  const workspace = state.workspace || null;
178
179
  const git = getGitSummaryForWorkspace(workspace, options);
179
180
  const title = activeChat?.title || state.name;
181
+ const meshCoordinatorFor = state.settings?.meshCoordinatorFor as string | undefined;
182
+ const meshQueueStats = meshCoordinatorFor ? getMeshQueueStats(meshCoordinatorFor) : undefined;
180
183
  return {
181
184
  id: state.instanceId || state.type,
182
185
  parentId: null,
@@ -200,6 +203,7 @@ function buildIdeWorkspaceSession(
200
203
  errorReason: state.errorReason,
201
204
  lastUpdated: state.lastUpdated,
202
205
  settings: state.settings,
206
+ ...(meshQueueStats && { meshQueueStats }),
203
207
  };
204
208
  }
205
209
 
@@ -216,6 +220,8 @@ function buildExtensionAgentSession(
216
220
  const includeSessionControls = shouldIncludeSessionControls(profile);
217
221
  const workspace = parent.workspace || null;
218
222
  const git = getGitSummaryForWorkspace(workspace, options);
223
+ const meshCoordinatorFor = ext.settings?.meshCoordinatorFor as string | undefined;
224
+ const meshQueueStats = meshCoordinatorFor ? getMeshQueueStats(meshCoordinatorFor) : undefined;
219
225
  return {
220
226
  id: ext.instanceId || `${parent.instanceId}:${ext.type}`,
221
227
  parentId: parent.instanceId || parent.type,
@@ -239,6 +245,7 @@ function buildExtensionAgentSession(
239
245
  errorReason: ext.errorReason,
240
246
  lastUpdated: ext.lastUpdated,
241
247
  settings: ext.settings,
248
+ ...(meshQueueStats && { meshQueueStats }),
242
249
  };
243
250
  }
244
251
 
@@ -279,6 +286,8 @@ function buildCliSession(state: CliProviderState, options: SessionEntryBuildOpti
279
286
  const includeSessionControls = shouldIncludeSessionControls(profile);
280
287
  const workspace = state.workspace || null;
281
288
  const git = getGitSummaryForWorkspace(workspace, options);
289
+ const meshCoordinatorFor = state.settings?.meshCoordinatorFor as string | undefined;
290
+ const meshQueueStats = meshCoordinatorFor ? getMeshQueueStats(meshCoordinatorFor) : undefined;
282
291
  return {
283
292
  id: state.instanceId,
284
293
  parentId: null,
@@ -318,6 +327,7 @@ function buildCliSession(state: CliProviderState, options: SessionEntryBuildOpti
318
327
  errorReason: state.errorReason,
319
328
  lastUpdated: state.lastUpdated,
320
329
  settings: state.settings,
330
+ ...(meshQueueStats && { meshQueueStats }),
321
331
  };
322
332
  }
323
333
 
@@ -330,6 +340,8 @@ function buildAcpSession(state: AcpProviderState, options: SessionEntryBuildOpti
330
340
  const includeSessionControls = shouldIncludeSessionControls(profile);
331
341
  const workspace = state.workspace || null;
332
342
  const git = getGitSummaryForWorkspace(workspace, options);
343
+ const meshCoordinatorFor = state.settings?.meshCoordinatorFor as string | undefined;
344
+ const meshQueueStats = meshCoordinatorFor ? getMeshQueueStats(meshCoordinatorFor) : undefined;
333
345
  return {
334
346
  id: state.instanceId,
335
347
  parentId: null,
@@ -352,6 +364,7 @@ function buildAcpSession(state: AcpProviderState, options: SessionEntryBuildOpti
352
364
  errorReason: state.errorReason,
353
365
  lastUpdated: state.lastUpdated,
354
366
  settings: state.settings,
367
+ ...(meshQueueStats && { meshQueueStats }),
355
368
  };
356
369
  }
357
370