@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.137

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 (39) hide show
  1. package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
  2. package/dist/cli-adapters/cli-state-engine.d.ts +154 -0
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +73 -74
  4. package/dist/cli-adapters/provider-cli-shared.d.ts +4 -0
  5. package/dist/config/chat-history.d.ts +1 -0
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +2591 -1966
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2594 -1974
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/mesh/beads-db.d.ts +54 -0
  12. package/dist/mesh/mesh-active-work.d.ts +7 -1
  13. package/dist/mesh/mesh-events.d.ts +10 -4
  14. package/dist/mesh/mesh-ledger.d.ts +21 -1
  15. package/dist/mesh/mesh-refine-status.d.ts +2 -3
  16. package/dist/mesh/mesh-work-queue.d.ts +17 -0
  17. package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
  18. package/dist/repo-mesh-types.d.ts +5 -0
  19. package/package.json +1 -1
  20. package/src/cli-adapters/cli-script-runner.ts +145 -0
  21. package/src/cli-adapters/cli-state-engine.ts +957 -0
  22. package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
  23. package/src/cli-adapters/provider-cli-adapter.ts +365 -1397
  24. package/src/cli-adapters/provider-cli-shared.ts +4 -0
  25. package/src/commands/chat-commands.ts +17 -1
  26. package/src/commands/router.ts +8 -0
  27. package/src/config/chat-history.ts +7 -3
  28. package/src/git/git-worktree.ts +8 -1
  29. package/src/index.ts +3 -2
  30. package/src/mesh/beads-db.ts +305 -2
  31. package/src/mesh/coordinator-prompt.ts +12 -17
  32. package/src/mesh/mesh-active-work.ts +162 -59
  33. package/src/mesh/mesh-events.ts +198 -53
  34. package/src/mesh/mesh-ledger.ts +321 -105
  35. package/src/mesh/mesh-refine-status.ts +2 -3
  36. package/src/mesh/mesh-work-queue.ts +116 -120
  37. package/src/mesh/worktree-bootstrap-config.ts +17 -4
  38. package/src/providers/provider-schema.ts +2 -0
  39. package/src/repo-mesh-types.ts +10 -0
@@ -21,10 +21,12 @@ export interface MeshTaskModeValidationResult {
21
21
 
22
22
  const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
23
23
  { 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 },
24
- { label: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv)|push\b)/i },
24
+ { label: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv|stash|worktree\s+(?:add|remove|move))|push\b)/i },
25
25
  { label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
26
- { label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release)\b/i },
27
- { label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i)\b/i },
26
+ { label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release|npm\s+publish|yarn\s+publish|pnpm\s+publish)\b/i },
27
+ { label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i|shred\b)\b/i },
28
+ { label: 'package_install', pattern: /\b(npm\s+(?:install|i|add|link|uninstall|remove)|yarn\s+(?:add|remove|link)|pnpm\s+(?:add|remove|link)|pip\s+install|brew\s+install|apt\s+install|cargo\s+install)\b/i },
29
+ { label: 'container_mutation', pattern: /\b(docker\s+(?:build|run|exec|push|tag|rmi|rm|create|start|stop|kill)|kubectl\s+(?:apply|delete|patch|replace|create|scale))\b/i },
28
30
  ];
29
31
 
30
32
  export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
@@ -122,35 +124,26 @@ export function enqueueTask(
122
124
  if (!modeValidation.valid) {
123
125
  throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
124
126
  }
125
- return withQueueLock(meshId, () => {
126
- const queue = readQueue(meshId);
127
- const entry: MeshWorkQueueEntry = {
128
- id: randomUUID(),
129
- meshId,
130
- message,
131
- status: 'pending',
132
- taskMode: modeValidation.taskMode,
133
- targetNodeId: opts?.targetNodeId,
134
- targetSessionId: opts?.targetSessionId,
135
- createdAt: new Date().toISOString(),
136
- updatedAt: new Date().toISOString(),
137
- };
138
- queue.push(entry);
139
- writeQueue(meshId, queue);
140
- return entry;
141
- });
127
+ const entry: MeshWorkQueueEntry = {
128
+ id: randomUUID(),
129
+ meshId,
130
+ message,
131
+ status: 'pending',
132
+ taskMode: modeValidation.taskMode,
133
+ targetNodeId: opts?.targetNodeId,
134
+ targetSessionId: opts?.targetSessionId,
135
+ createdAt: new Date().toISOString(),
136
+ updatedAt: new Date().toISOString(),
137
+ };
138
+ BeadsDB.getInstance().insertQueueEntry(entry);
139
+ return entry;
142
140
  }
143
141
 
144
142
  /**
145
143
  * Get all tasks in the queue, optionally filtered by status.
146
144
  */
147
145
  export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): MeshWorkQueueEntry[] {
148
- let queue = readQueue(meshId);
149
- if (opts?.status?.length) {
150
- const statuses = new Set(opts.status);
151
- queue = queue.filter(q => statuses.has(q.status));
152
- }
153
- return queue;
146
+ return BeadsDB.getInstance().getQueueEntries(meshId, opts?.status?.length ? opts.status : undefined);
154
147
  }
155
148
 
156
149
  export function getMeshQueueRevision(meshId: string): string {
@@ -161,29 +154,7 @@ export function getMeshQueueRevision(meshId: string): string {
161
154
  * Find the next pending task that this node is allowed to claim, and mark it as assigned.
162
155
  */
163
156
  export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
164
- return withQueueLock(meshId, () => {
165
- const queue = readQueue(meshId);
166
- const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
167
- q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
168
- ));
169
- if (hasActiveAssignment) return null;
170
- let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
171
- if (targetIdx === -1) {
172
- targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
173
- }
174
- if (targetIdx === -1) {
175
- targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
176
- }
177
- if (targetIdx === -1) return null;
178
- const entry = queue[targetIdx];
179
- entry.status = 'assigned';
180
- entry.assignedNodeId = nodeId;
181
- entry.assignedSessionId = sessionId;
182
- entry.dispatchTimestamp = new Date().toISOString();
183
- entry.updatedAt = new Date().toISOString();
184
- writeQueue(meshId, queue);
185
- return entry;
186
- });
157
+ return BeadsDB.getInstance().claimNextQueueTask(meshId, nodeId, sessionId);
187
158
  }
188
159
 
189
160
  /**
@@ -198,13 +169,11 @@ export function updateTaskStatus(
198
169
  ): MeshWorkQueueEntry | null {
199
170
  requireMeshHostQueueOwner(opts);
200
171
  return withQueueLock(meshId, () => {
201
- const queue = readQueue(meshId);
202
- const idx = queue.findIndex(q => q.id === taskId);
203
- if (idx === -1) return null;
204
- queue[idx].status = status;
205
- queue[idx].updatedAt = new Date().toISOString();
206
- writeQueue(meshId, queue);
207
- return queue[idx];
172
+ const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
173
+ if (!entry) return null;
174
+ entry.status = status;
175
+ BeadsDB.getInstance().updateQueueEntry(entry);
176
+ return entry;
208
177
  });
209
178
  }
210
179
 
@@ -214,14 +183,12 @@ export function recordTaskAutoLaunch(
214
183
  autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
215
184
  ): MeshWorkQueueEntry | null {
216
185
  return withQueueLock(meshId, () => {
217
- const queue = readQueue(meshId);
218
- const idx = queue.findIndex(q => q.id === taskId);
219
- if (idx === -1) return null;
186
+ const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
187
+ if (!entry) return null;
220
188
  const now = new Date().toISOString();
221
- queue[idx].autoLaunch = { ...autoLaunch, updatedAt: now };
222
- queue[idx].updatedAt = now;
223
- writeQueue(meshId, queue);
224
- return queue[idx];
189
+ entry.autoLaunch = { ...autoLaunch, updatedAt: now };
190
+ BeadsDB.getInstance().updateQueueEntry(entry);
191
+ return entry;
225
192
  });
226
193
  }
227
194
 
@@ -235,16 +202,14 @@ export function cancelTask(
235
202
  ): MeshWorkQueueEntry | null {
236
203
  requireMeshHostQueueOwner(opts);
237
204
  return withQueueLock(meshId, () => {
238
- const queue = readQueue(meshId);
239
- const idx = queue.findIndex(q => q.id === taskId);
240
- if (idx === -1) return null;
205
+ const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
206
+ if (!entry) return null;
241
207
  const now = new Date().toISOString();
242
- queue[idx].status = 'cancelled';
243
- queue[idx].updatedAt = now;
244
- queue[idx].cancelledAt = now;
245
- if (opts?.reason) queue[idx].cancelReason = opts.reason;
246
- writeQueue(meshId, queue);
247
- return queue[idx];
208
+ entry.status = 'cancelled';
209
+ entry.cancelledAt = now;
210
+ if (opts?.reason) entry.cancelReason = opts.reason;
211
+ BeadsDB.getInstance().updateQueueEntry(entry);
212
+ return entry;
248
213
  });
249
214
  }
250
215
 
@@ -265,11 +230,8 @@ export function requeueTask(
265
230
  ): MeshWorkQueueEntry | null {
266
231
  requireMeshHostQueueOwner(opts);
267
232
  return withQueueLock(meshId, () => {
268
- const queue = readQueue(meshId);
269
- const idx = queue.findIndex(q => q.id === taskId);
270
- if (idx === -1) return null;
271
- const entry = queue[idx];
272
- const now = new Date().toISOString();
233
+ const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
234
+ if (!entry) return null;
273
235
  entry.status = 'pending';
274
236
  delete entry.assignedNodeId;
275
237
  delete entry.assignedSessionId;
@@ -279,11 +241,10 @@ export function requeueTask(
279
241
  if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
280
242
  if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
281
243
  if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
282
- entry.updatedAt = now;
283
- entry.requeuedAt = now;
244
+ entry.requeuedAt = new Date().toISOString();
284
245
  entry.requeueCount = (entry.requeueCount || 0) + 1;
285
246
  if (opts?.reason) entry.requeueReason = opts.reason;
286
- writeQueue(meshId, queue);
247
+ BeadsDB.getInstance().updateQueueEntry(entry);
287
248
  return entry;
288
249
  });
289
250
  }
@@ -298,22 +259,12 @@ export function updateSessionTaskStatus(
298
259
  opts?: { occurredAt?: string },
299
260
  ): MeshWorkQueueEntry | null {
300
261
  return withQueueLock(meshId, () => {
301
- const queue = readQueue(meshId);
302
- const occurredAtTime = opts?.occurredAt ? new Date(opts.occurredAt).getTime() : Number.NaN;
303
- const hasOccurredAt = Number.isFinite(occurredAtTime);
304
- let bestIdx = -1;
305
- let bestTime = 0;
306
- for (let i = queue.length - 1; i >= 0; i--) {
307
- if (queue[i].assignedSessionId !== sessionId || queue[i].status !== 'assigned') continue;
308
- const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
309
- if (hasOccurredAt && Number.isFinite(time) && time > occurredAtTime) continue;
310
- if (time > bestTime) { bestTime = time; bestIdx = i; }
311
- }
312
- if (bestIdx === -1) return null;
313
- queue[bestIdx].status = status;
314
- queue[bestIdx].updatedAt = new Date().toISOString();
315
- writeQueue(meshId, queue);
316
- return queue[bestIdx];
262
+ const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
263
+ const entry = BeadsDB.getInstance().findAssignedBySession(meshId, sessionId, occurredAtIso);
264
+ if (!entry) return null;
265
+ entry.status = status;
266
+ BeadsDB.getInstance().updateQueueEntry(entry);
267
+ return entry;
317
268
  });
318
269
  }
319
270
 
@@ -342,14 +293,16 @@ export interface MeshWorkQueueStats {
342
293
  * Return aggregate queue statistics for the given mesh.
343
294
  */
344
295
  export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
345
- const queue = readQueue(meshId);
346
- const pending = queue.filter(q => q.status === 'pending').length;
347
- const assigned = queue.filter(q => q.status === 'assigned').length;
348
- const completed = queue.filter(q => q.status === 'completed').length;
349
- const failed = queue.filter(q => q.status === 'failed').length;
350
- const cancelled = queue.filter(q => q.status === 'cancelled').length;
296
+ const rows = BeadsDB.getInstance().getQueueStatsByStatus(meshId);
297
+ const counts: Record<string, number> = {};
298
+ for (const r of rows) counts[r.status] = r.count;
299
+ const pending = counts['pending'] ?? 0;
300
+ const assigned = counts['assigned'] ?? 0;
301
+ const completed = counts['completed'] ?? 0;
302
+ const failed = counts['failed'] ?? 0;
303
+ const cancelled = counts['cancelled'] ?? 0;
351
304
  return {
352
- total: queue.length,
305
+ total: pending + assigned + completed + failed + cancelled,
353
306
  active: pending + assigned,
354
307
  historical: completed + failed + cancelled,
355
308
  pending,
@@ -357,23 +310,9 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
357
310
  completed,
358
311
  failed,
359
312
  cancelled,
360
- activeCounts: {
361
- pending,
362
- assigned,
363
- },
364
- historicalCounts: {
365
- completed,
366
- failed,
367
- cancelled,
368
- },
369
- activeAssignments: queue
370
- .filter(q => q.status === 'assigned')
371
- .map(q => ({
372
- id: q.id,
373
- nodeId: q.assignedNodeId,
374
- sessionId: q.assignedSessionId,
375
- message: q.message,
376
- })),
313
+ activeCounts: { pending, assigned },
314
+ historicalCounts: { completed, failed, cancelled },
315
+ activeAssignments: BeadsDB.getInstance().getActiveAssignmentDetails(meshId),
377
316
  };
378
317
  }
379
318
 
@@ -390,3 +329,60 @@ export function __clearMeshQueueForTests(meshId: string): void {
390
329
  export function __resetBeadsDBForTests(): void {
391
330
  BeadsDB.resetForTests();
392
331
  }
332
+
333
+ // ── Direct Dispatch Tracking ─────────────────────────────────────────────────
334
+ // Persists direct (non-queue) task dispatches so buildMeshActiveWork can read
335
+ // active work from BeadsDB instead of scanning ledger JSONL entries.
336
+
337
+ export type DirectDispatchRecord = ReturnType<BeadsDB['getActiveDirectDispatches']>[number];
338
+
339
+ export function insertDirectDispatch(
340
+ meshId: string,
341
+ data: {
342
+ taskId: string;
343
+ nodeId?: string;
344
+ sessionId?: string;
345
+ providerType?: string;
346
+ message: string;
347
+ taskMode?: string;
348
+ via: string;
349
+ dispatchedToIdleSession?: boolean;
350
+ dispatchedAt: string;
351
+ },
352
+ ): void {
353
+ try {
354
+ BeadsDB.getInstance().insertDirectDispatch({ ...data, meshId });
355
+ } catch (e: any) {
356
+ process.stderr.write(`[adhdev-mesh] insertDirectDispatch failed for task ${data.taskId}: ${e?.message || e}\n`);
357
+ }
358
+ }
359
+
360
+ export function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[] {
361
+ try {
362
+ return BeadsDB.getInstance().getActiveDirectDispatches(meshId);
363
+ } catch {
364
+ return [];
365
+ }
366
+ }
367
+
368
+ export function updateDirectDispatchStatus(
369
+ meshId: string,
370
+ sessionId: string,
371
+ status: 'acked' | 'completed' | 'failed' | 'stale',
372
+ ): void {
373
+ try {
374
+ BeadsDB.getInstance().updateDirectDispatchStatus(meshId, sessionId, status);
375
+ } catch { /* best-effort */ }
376
+ }
377
+
378
+ export function cleanupTerminalDirectDispatches(olderThanMs = 7 * 24 * 60 * 60_000): void {
379
+ try {
380
+ BeadsDB.getInstance().cleanupTerminalDirectDispatches(olderThanMs);
381
+ } catch { /* best-effort */ }
382
+ }
383
+
384
+ export function markStaleDirectDispatches(meshId: string, olderThanMs = 60 * 60_000): void {
385
+ try {
386
+ BeadsDB.getInstance().markStaleDirectDispatches(meshId, olderThanMs);
387
+ } catch { /* best-effort */ }
388
+ }
@@ -9,6 +9,7 @@ import {
9
9
  type MeshRefineValidationCommandPlan,
10
10
  type RepoMeshRefineValidationCommandConfig,
11
11
  } from './refine-config.js';
12
+ import type { MeshAsyncJobLifecycle } from '../repo-mesh-types.js';
12
13
 
13
14
  export type WorktreeBootstrapStatus = 'ready' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
14
15
 
@@ -21,16 +22,13 @@ export interface RepoMeshWorktreeBootstrapConfig {
21
22
  staleInputs?: string[];
22
23
  }
23
24
 
24
- export interface WorktreeBootstrapState {
25
+ export interface WorktreeBootstrapState extends MeshAsyncJobLifecycle {
25
26
  status: WorktreeBootstrapStatus;
26
27
  required: boolean;
27
28
  configSource?: string;
28
29
  configSourceType?: 'repo_file' | 'mesh_policy' | 'unavailable' | 'invalid';
29
- startedAt?: string;
30
- completedAt?: string;
31
30
  lastCommand?: string;
32
31
  exitCode?: number | null;
33
- error?: string;
34
32
  commandsRun?: Array<Record<string, unknown>>;
35
33
  staleInputs?: string[];
36
34
  }
@@ -177,7 +175,22 @@ export async function runMeshWorktreeBootstrap(mesh: any, workspace: string): Pr
177
175
  commandsRun: [],
178
176
  staleInputs: loaded.config.staleInputs,
179
177
  };
178
+ const staleInputPaths = loaded.config.staleInputs ?? [];
179
+ // Snapshot which staleInputs files are absent at bootstrap start.
180
+ // Files already present before we start are not interference signals.
181
+ const initiallyAbsent = staleInputPaths.filter(p => !existsSync(join(workspace, p)));
180
182
  for (const command of validation.commands) {
183
+ // Check if any initially-absent staleInputs files appeared since bootstrap started
184
+ // (indicates another process modified the environment mid-run).
185
+ if (initiallyAbsent.length > 0) {
186
+ const appearedNow = initiallyAbsent.filter(p => existsSync(join(workspace, p)));
187
+ if (appearedNow.length > 0) {
188
+ state.status = 'stale';
189
+ state.completedAt = new Date().toISOString();
190
+ state.error = `Bootstrap interrupted: staleInputs files appeared during run: ${appearedNow.join(', ')}`;
191
+ return state;
192
+ }
193
+ }
181
194
  const cwd = command.cwd ? pathResolve(workspace, command.cwd) : workspace;
182
195
  const startedAt = Date.now();
183
196
  state.lastCommand = command.displayCommand;
@@ -64,6 +64,8 @@ const KNOWN_PROVIDER_FIELDS = new Set<string>([
64
64
  'sendKey',
65
65
  'submitStrategy',
66
66
  'requirePromptEchoBeforeSubmit',
67
+ 'requiresFinalAssistantBeforeIdle',
68
+ 'augmentStaleSnapshot',
67
69
  'timeouts',
68
70
  'disableUpstream',
69
71
  ])
@@ -472,3 +472,13 @@ export interface RepoMeshLedgerStatus {
472
472
  entries: RepoMeshLedgerEntryStatus[];
473
473
  summary: RepoMeshLedgerSummaryStatus;
474
474
  }
475
+
476
+ // ─── Async Job Lifecycle ─────────────────────────
477
+ // Shared base for all mesh async job types (refine jobs, bootstrap runs, etc.)
478
+ // Each concrete type adds its own status enum and domain-specific fields.
479
+
480
+ export interface MeshAsyncJobLifecycle {
481
+ startedAt?: string;
482
+ completedAt?: string;
483
+ error?: string;
484
+ }