@adhdev/daemon-core 0.9.82-rc.6 → 0.9.82-rc.61

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 (42) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +2 -0
  2. package/dist/commands/router.d.ts +24 -0
  3. package/dist/config/mesh-config.d.ts +66 -1
  4. package/dist/git/git-commands.d.ts +1 -0
  5. package/dist/git/git-status.d.ts +5 -0
  6. package/dist/git/git-types.d.ts +10 -0
  7. package/dist/index.d.ts +13 -6
  8. package/dist/index.js +3522 -593
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +3496 -587
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/mesh/mesh-active-work.d.ts +48 -0
  13. package/dist/mesh/mesh-events.d.ts +17 -5
  14. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  15. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  16. package/dist/mesh/mesh-ledger.d.ts +38 -1
  17. package/dist/mesh/mesh-work-queue.d.ts +23 -5
  18. package/dist/mesh/refine-config.d.ts +119 -0
  19. package/dist/providers/chat-message-normalization.d.ts +1 -0
  20. package/dist/providers/cli-provider-instance.d.ts +1 -0
  21. package/dist/repo-mesh-types.d.ts +160 -0
  22. package/package.json +1 -1
  23. package/src/boot/daemon-lifecycle.ts +4 -0
  24. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  25. package/src/commands/router.ts +2178 -419
  26. package/src/config/mesh-config.ts +244 -1
  27. package/src/git/git-commands.ts +3 -3
  28. package/src/git/git-status.ts +97 -6
  29. package/src/git/git-summary.ts +3 -0
  30. package/src/git/git-types.ts +11 -0
  31. package/src/index.ts +39 -5
  32. package/src/mesh/coordinator-prompt.ts +4 -2
  33. package/src/mesh/mesh-active-work.ts +205 -0
  34. package/src/mesh/mesh-events.ts +210 -38
  35. package/src/mesh/mesh-fast-forward.ts +430 -0
  36. package/src/mesh/mesh-host-ownership.ts +73 -0
  37. package/src/mesh/mesh-ledger.ts +137 -0
  38. package/src/mesh/mesh-work-queue.ts +202 -122
  39. package/src/mesh/refine-config.ts +306 -0
  40. package/src/providers/chat-message-normalization.ts +3 -1
  41. package/src/providers/cli-provider-instance.ts +66 -1
  42. package/src/repo-mesh-types.ts +174 -0
@@ -1,20 +1,70 @@
1
- import { existsSync, writeFileSync, readFileSync } from 'fs';
1
+ import { existsSync, writeFileSync, readFileSync, openSync, closeSync, unlinkSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { getLedgerDir } from './mesh-ledger.js';
5
+ import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
6
+ import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
5
7
 
6
8
  export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
7
9
  export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
8
10
  export type MeshHistoricalTaskStatus = Extract<MeshTaskStatus, 'completed' | 'failed' | 'cancelled'>;
11
+ export type MeshTaskMode = 'code_change' | 'validation' | 'live_debug_readonly' | 'launch_app' | 'convergence';
9
12
 
10
13
  export const ACTIVE_MESH_QUEUE_STATUSES: MeshActiveTaskStatus[] = ['pending', 'assigned'];
11
14
  export const HISTORICAL_MESH_QUEUE_STATUSES: MeshHistoricalTaskStatus[] = ['completed', 'failed', 'cancelled'];
15
+ export const MESH_TASK_MODES: MeshTaskMode[] = ['code_change', 'validation', 'live_debug_readonly', 'launch_app', 'convergence'];
16
+
17
+ export interface MeshTaskModeValidationResult {
18
+ valid: boolean;
19
+ taskMode?: MeshTaskMode;
20
+ violations: string[];
21
+ allowedOperations?: string[];
22
+ }
23
+
24
+ const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
25
+ { label: 'source_edit', pattern: /\b(edit|modify|patch|apply\s+patch|write\s+(?:to\s+)?(?:file|source)|overwrite|delete\s+file|remove\s+file|create\s+file|touch\s+file)\b/i },
26
+ { label: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv)|push\b)/i },
27
+ { label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
28
+ { label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release)\b/i },
29
+ { label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i)\b/i },
30
+ ];
31
+
32
+ export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
33
+ if (typeof value !== 'string') return undefined;
34
+ const normalized = value.trim() as MeshTaskMode;
35
+ return (MESH_TASK_MODES as string[]).includes(normalized) ? normalized : undefined;
36
+ }
37
+
38
+ export function validateMeshTaskModeRequest(mode: unknown, message: string): MeshTaskModeValidationResult {
39
+ const taskMode = normalizeMeshTaskMode(mode);
40
+ if (!taskMode) {
41
+ return { valid: true, violations: [] };
42
+ }
43
+ if (taskMode !== 'live_debug_readonly') {
44
+ return { valid: true, taskMode, violations: [] };
45
+ }
46
+ const violations = LIVE_DEBUG_READONLY_FORBIDDEN
47
+ .filter(rule => rule.pattern.test(message || ''))
48
+ .map(rule => rule.label);
49
+ return {
50
+ valid: violations.length === 0,
51
+ taskMode,
52
+ violations,
53
+ allowedOperations: [
54
+ 'process/log/window/port/session inspection',
55
+ 'read-only filesystem listing/reading',
56
+ 'status probes and keep-running handle reporting',
57
+ 'diagnostic summaries without source edits, commits, checkpoints, pushes, deploys, resets, rebases, or destructive cleanups',
58
+ ],
59
+ };
60
+ }
12
61
 
13
62
  export interface MeshWorkQueueEntry {
14
63
  id: string;
15
64
  meshId: string;
16
65
  message: string;
17
66
  status: MeshTaskStatus;
67
+ taskMode?: MeshTaskMode;
18
68
  /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
19
69
  targetNodeId?: string;
20
70
  /** If specified, only this runtime session can claim the task */
@@ -45,11 +95,40 @@ export interface MeshWorkQueueEntry {
45
95
  updatedAt: string;
46
96
  }
47
97
 
98
+ export interface MeshQueueMutationOptions {
99
+ ownerRole?: RepoMeshDaemonRole;
100
+ }
101
+
48
102
  function getQueuePath(meshId: string): string {
49
103
  const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
50
104
  return join(getLedgerDir(), `${safe}.queue.json`);
51
105
  }
52
106
 
107
+ function getLockPath(meshId: string): string {
108
+ const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
109
+ return join(getLedgerDir(), `${safe}.queue.lock`);
110
+ }
111
+
112
+ /**
113
+ * Simple advisory file lock using O_EXCL (atomic create) for queue mutations.
114
+ * Retries up to 10 times at 30 ms intervals; proceeds without lock on timeout
115
+ * to prevent deadlock (best-effort — far better than no locking at all).
116
+ */
117
+ function withQueueLock<T>(meshId: string, fn: () => T): T {
118
+ const lockPath = getLockPath(meshId);
119
+ let fd = -1;
120
+ for (let i = 0; i < 10; i++) {
121
+ try { fd = openSync(lockPath, 'wx'); break; } catch {
122
+ const deadline = Date.now() + 30;
123
+ while (Date.now() < deadline) { /* spin */ }
124
+ }
125
+ }
126
+ try { return fn(); } finally {
127
+ if (fd !== -1) try { closeSync(fd); } catch { /* noop */ }
128
+ try { unlinkSync(lockPath); } catch { /* already removed */ }
129
+ }
130
+ }
131
+
53
132
  function readQueue(meshId: string): MeshWorkQueueEntry[] {
54
133
  const path = getQueuePath(meshId);
55
134
  if (!existsSync(path)) return [];
@@ -72,22 +151,30 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
72
151
  export function enqueueTask(
73
152
  meshId: string,
74
153
  message: string,
75
- opts?: { targetNodeId?: string; targetSessionId?: string }
154
+ opts?: { targetNodeId?: string; targetSessionId?: string; taskMode?: MeshTaskMode | string } & MeshQueueMutationOptions,
76
155
  ): MeshWorkQueueEntry {
77
- const queue = readQueue(meshId);
78
- const entry: MeshWorkQueueEntry = {
79
- id: randomUUID(),
80
- meshId,
81
- message,
82
- status: 'pending',
83
- targetNodeId: opts?.targetNodeId,
84
- targetSessionId: opts?.targetSessionId,
85
- createdAt: new Date().toISOString(),
86
- updatedAt: new Date().toISOString(),
87
- };
88
- queue.push(entry);
89
- writeQueue(meshId, queue);
90
- return entry;
156
+ requireMeshHostQueueOwner(opts);
157
+ const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
158
+ if (!modeValidation.valid) {
159
+ throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
160
+ }
161
+ return withQueueLock(meshId, () => {
162
+ const queue = readQueue(meshId);
163
+ const entry: MeshWorkQueueEntry = {
164
+ id: randomUUID(),
165
+ meshId,
166
+ message,
167
+ status: 'pending',
168
+ taskMode: modeValidation.taskMode,
169
+ targetNodeId: opts?.targetNodeId,
170
+ targetSessionId: opts?.targetSessionId,
171
+ createdAt: new Date().toISOString(),
172
+ updatedAt: new Date().toISOString(),
173
+ };
174
+ queue.push(entry);
175
+ writeQueue(meshId, queue);
176
+ return entry;
177
+ });
91
178
  }
92
179
 
93
180
  /**
@@ -106,39 +193,29 @@ export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }):
106
193
  * Find the next pending task that this node is allowed to claim, and mark it as assigned.
107
194
  */
108
195
  export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
109
- const queue = readQueue(meshId);
110
-
111
- // A worker must finish or fail its current queued assignment before it can
112
- // claim another one. maxParallelTasks limits total mesh concurrency; it is
113
- // not permission for one node/session to accumulate multiple assigned items.
114
- const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
115
- q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
116
- ));
117
- if (hasActiveAssignment) return null;
118
-
119
- // Find highest priority task:
120
- // 1. Pending tasks explicitly targeted at this runtime session
121
- // 2. Pending tasks explicitly targeted at this node (but not another session)
122
- // 3. Pending tasks with no target node/session
123
- let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
124
- if (targetIdx === -1) {
125
- targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
126
- }
127
- if (targetIdx === -1) {
128
- targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
129
- }
130
-
131
- if (targetIdx === -1) return null;
132
-
133
- const entry = queue[targetIdx];
134
- entry.status = 'assigned';
135
- entry.assignedNodeId = nodeId;
136
- entry.assignedSessionId = sessionId;
137
- entry.dispatchTimestamp = new Date().toISOString();
138
- entry.updatedAt = new Date().toISOString();
139
-
140
- writeQueue(meshId, queue);
141
- return entry;
196
+ return withQueueLock(meshId, () => {
197
+ const queue = readQueue(meshId);
198
+ const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
199
+ q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
200
+ ));
201
+ if (hasActiveAssignment) return null;
202
+ let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
203
+ if (targetIdx === -1) {
204
+ targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
205
+ }
206
+ if (targetIdx === -1) {
207
+ targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
208
+ }
209
+ if (targetIdx === -1) return null;
210
+ const entry = queue[targetIdx];
211
+ entry.status = 'assigned';
212
+ entry.assignedNodeId = nodeId;
213
+ entry.assignedSessionId = sessionId;
214
+ entry.dispatchTimestamp = new Date().toISOString();
215
+ entry.updatedAt = new Date().toISOString();
216
+ writeQueue(meshId, queue);
217
+ return entry;
218
+ });
142
219
  }
143
220
 
144
221
  /**
@@ -149,15 +226,18 @@ export function updateTaskStatus(
149
226
  meshId: string,
150
227
  taskId: string,
151
228
  status: MeshTaskStatus,
229
+ opts?: MeshQueueMutationOptions,
152
230
  ): MeshWorkQueueEntry | null {
153
- const queue = readQueue(meshId);
154
- const idx = queue.findIndex(q => q.id === taskId);
155
- if (idx === -1) return null;
156
-
157
- queue[idx].status = status;
158
- queue[idx].updatedAt = new Date().toISOString();
159
- writeQueue(meshId, queue);
160
- return queue[idx];
231
+ requireMeshHostQueueOwner(opts);
232
+ return withQueueLock(meshId, () => {
233
+ const queue = readQueue(meshId);
234
+ const idx = queue.findIndex(q => q.id === taskId);
235
+ if (idx === -1) return null;
236
+ queue[idx].status = status;
237
+ queue[idx].updatedAt = new Date().toISOString();
238
+ writeQueue(meshId, queue);
239
+ return queue[idx];
240
+ });
161
241
  }
162
242
 
163
243
  export function recordTaskAutoLaunch(
@@ -165,17 +245,16 @@ export function recordTaskAutoLaunch(
165
245
  taskId: string,
166
246
  autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
167
247
  ): MeshWorkQueueEntry | null {
168
- const queue = readQueue(meshId);
169
- const idx = queue.findIndex(q => q.id === taskId);
170
- if (idx === -1) return null;
171
- const now = new Date().toISOString();
172
- queue[idx].autoLaunch = {
173
- ...autoLaunch,
174
- updatedAt: now,
175
- };
176
- queue[idx].updatedAt = now;
177
- writeQueue(meshId, queue);
178
- return queue[idx];
248
+ return withQueueLock(meshId, () => {
249
+ const queue = readQueue(meshId);
250
+ const idx = queue.findIndex(q => q.id === taskId);
251
+ if (idx === -1) return null;
252
+ const now = new Date().toISOString();
253
+ queue[idx].autoLaunch = { ...autoLaunch, updatedAt: now };
254
+ queue[idx].updatedAt = now;
255
+ writeQueue(meshId, queue);
256
+ return queue[idx];
257
+ });
179
258
  }
180
259
 
181
260
  /**
@@ -184,19 +263,21 @@ export function recordTaskAutoLaunch(
184
263
  export function cancelTask(
185
264
  meshId: string,
186
265
  taskId: string,
187
- opts?: { reason?: string },
266
+ opts?: { reason?: string } & MeshQueueMutationOptions,
188
267
  ): MeshWorkQueueEntry | null {
189
- const queue = readQueue(meshId);
190
- const idx = queue.findIndex(q => q.id === taskId);
191
- if (idx === -1) return null;
192
-
193
- const now = new Date().toISOString();
194
- queue[idx].status = 'cancelled';
195
- queue[idx].updatedAt = now;
196
- queue[idx].cancelledAt = now;
197
- if (opts?.reason) queue[idx].cancelReason = opts.reason;
198
- writeQueue(meshId, queue);
199
- return queue[idx];
268
+ requireMeshHostQueueOwner(opts);
269
+ return withQueueLock(meshId, () => {
270
+ const queue = readQueue(meshId);
271
+ const idx = queue.findIndex(q => q.id === taskId);
272
+ if (idx === -1) return null;
273
+ const now = new Date().toISOString();
274
+ queue[idx].status = 'cancelled';
275
+ queue[idx].updatedAt = now;
276
+ queue[idx].cancelledAt = now;
277
+ if (opts?.reason) queue[idx].cancelReason = opts.reason;
278
+ writeQueue(meshId, queue);
279
+ return queue[idx];
280
+ });
200
281
  }
201
282
 
202
283
  /**
@@ -212,29 +293,31 @@ export function requeueTask(
212
293
  targetSessionId?: string;
213
294
  clearTargetNode?: boolean;
214
295
  clearTargetSession?: boolean;
215
- },
296
+ } & MeshQueueMutationOptions,
216
297
  ): MeshWorkQueueEntry | null {
217
- const queue = readQueue(meshId);
218
- const idx = queue.findIndex(q => q.id === taskId);
219
- if (idx === -1) return null;
220
-
221
- const entry = queue[idx];
222
- const now = new Date().toISOString();
223
- entry.status = 'pending';
224
- delete entry.assignedNodeId;
225
- delete entry.assignedSessionId;
226
- delete entry.cancelledAt;
227
- delete entry.cancelReason;
228
- if (opts?.clearTargetNode) delete entry.targetNodeId;
229
- if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
230
- if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
231
- if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
232
- entry.updatedAt = now;
233
- entry.requeuedAt = now;
234
- entry.requeueCount = (entry.requeueCount || 0) + 1;
235
- if (opts?.reason) entry.requeueReason = opts.reason;
236
- writeQueue(meshId, queue);
237
- return entry;
298
+ requireMeshHostQueueOwner(opts);
299
+ return withQueueLock(meshId, () => {
300
+ const queue = readQueue(meshId);
301
+ const idx = queue.findIndex(q => q.id === taskId);
302
+ if (idx === -1) return null;
303
+ const entry = queue[idx];
304
+ const now = new Date().toISOString();
305
+ entry.status = 'pending';
306
+ delete entry.assignedNodeId;
307
+ delete entry.assignedSessionId;
308
+ delete entry.cancelledAt;
309
+ delete entry.cancelReason;
310
+ if (opts?.clearTargetNode) delete entry.targetNodeId;
311
+ if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
312
+ if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
313
+ if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
314
+ entry.updatedAt = now;
315
+ entry.requeuedAt = now;
316
+ entry.requeueCount = (entry.requeueCount || 0) + 1;
317
+ if (opts?.reason) entry.requeueReason = opts.reason;
318
+ writeQueue(meshId, queue);
319
+ return entry;
320
+ });
238
321
  }
239
322
 
240
323
  /**
@@ -244,29 +327,26 @@ export function updateSessionTaskStatus(
244
327
  meshId: string,
245
328
  sessionId: string,
246
329
  status: MeshTaskStatus,
330
+ opts?: { occurredAt?: string },
247
331
  ): MeshWorkQueueEntry | null {
248
- const queue = readQueue(meshId);
249
- // Collect all assigned tasks for this session, then pick the one with the
250
- // most recent dispatchTimestamp (or updatedAt fallback for legacy entries).
251
- // This prevents completing the wrong task when multiple tasks were assigned
252
- // to the same session in rapid succession.
253
- let bestIdx = -1;
254
- let bestTime = 0;
255
- for (let i = queue.length - 1; i >= 0; i--) {
256
- if (queue[i].assignedSessionId === sessionId && queue[i].status === 'assigned') {
332
+ return withQueueLock(meshId, () => {
333
+ const queue = readQueue(meshId);
334
+ const occurredAtTime = opts?.occurredAt ? new Date(opts.occurredAt).getTime() : Number.NaN;
335
+ const hasOccurredAt = Number.isFinite(occurredAtTime);
336
+ let bestIdx = -1;
337
+ let bestTime = 0;
338
+ for (let i = queue.length - 1; i >= 0; i--) {
339
+ if (queue[i].assignedSessionId !== sessionId || queue[i].status !== 'assigned') continue;
257
340
  const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
258
- if (time > bestTime) {
259
- bestTime = time;
260
- bestIdx = i;
261
- }
341
+ if (hasOccurredAt && Number.isFinite(time) && time > occurredAtTime) continue;
342
+ if (time > bestTime) { bestTime = time; bestIdx = i; }
262
343
  }
263
- }
264
- if (bestIdx === -1) return null;
265
-
266
- queue[bestIdx].status = status;
267
- queue[bestIdx].updatedAt = new Date().toISOString();
268
- writeQueue(meshId, queue);
269
- return queue[bestIdx];
344
+ if (bestIdx === -1) return null;
345
+ queue[bestIdx].status = status;
346
+ queue[bestIdx].updatedAt = new Date().toISOString();
347
+ writeQueue(meshId, queue);
348
+ return queue[bestIdx];
349
+ });
270
350
  }
271
351
 
272
352
  export interface MeshWorkQueueStats {