@adhdev/daemon-core 0.9.82-rc.364 → 0.9.82-rc.366

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 (41) hide show
  1. package/dist/commands/high-family/index.d.ts +3 -0
  2. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  3. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  4. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  5. package/dist/commands/high-family/types.d.ts +60 -0
  6. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  7. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  8. package/dist/commands/med-family/ide.d.ts +10 -0
  9. package/dist/commands/med-family/index.d.ts +3 -0
  10. package/dist/commands/med-family/mesh-crud.d.ts +2 -0
  11. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  12. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  13. package/dist/commands/med-family/types.d.ts +116 -0
  14. package/dist/commands/router.d.ts +291 -0
  15. package/dist/index.js +3824 -3565
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +3811 -3553
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/mesh/mesh-events-coordinator.d.ts +8 -0
  20. package/dist/system/hash.d.ts +8 -0
  21. package/package.json +2 -2
  22. package/src/commands/cli-manager.ts +30 -3
  23. package/src/commands/high-family/index.ts +28 -0
  24. package/src/commands/high-family/mesh-coordinator-launch.ts +592 -0
  25. package/src/commands/high-family/mesh-events.ts +47 -0
  26. package/src/commands/high-family/mesh-status.ts +639 -0
  27. package/src/commands/high-family/types.ts +76 -0
  28. package/src/commands/med-family/cli-agent.ts +218 -0
  29. package/src/commands/med-family/fast-forward.ts +198 -0
  30. package/src/commands/med-family/ide.ts +163 -0
  31. package/src/commands/med-family/index.ts +35 -0
  32. package/src/commands/med-family/mesh-crud.ts +788 -0
  33. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  34. package/src/commands/med-family/mesh-queue.ts +131 -0
  35. package/src/commands/med-family/types.ts +120 -0
  36. package/src/commands/mesh-coordinator.ts +2 -2
  37. package/src/commands/router.ts +328 -2847
  38. package/src/config/mesh-config.ts +3 -2
  39. package/src/mesh/mesh-active-work.ts +59 -81
  40. package/src/mesh/mesh-events-coordinator.ts +35 -1
  41. package/src/system/hash.ts +23 -0
@@ -8,7 +8,8 @@
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync } from 'fs';
10
10
  import { join } from 'path';
11
- import { createHash, randomBytes, randomUUID } from 'crypto';
11
+ import { randomBytes, randomUUID } from 'crypto';
12
+ import { shortHash } from '../system/hash.js';
12
13
  import { getConfigDir } from './config.js';
13
14
  import type {
14
15
  LocalMeshConfig,
@@ -309,7 +310,7 @@ function normalizeManualHostAddress(hostAddress: string): string {
309
310
  }
310
311
 
311
312
  export function tokenIdForManualPairing(token: string): string {
312
- return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
313
+ return `tok_${shortHash(token)}`;
313
314
  }
314
315
 
315
316
  function normalizeTokenExpiry(value: unknown): string | undefined {
@@ -208,6 +208,60 @@ function classifyDirectDispatch(params: {
208
208
  return { ledgerOnlyStaleReason, isFreshUnacknowledged };
209
209
  }
210
210
 
211
+ /**
212
+ * Build a direct-dispatch MeshActiveWorkRecord from a `task_dispatched` ledger entry,
213
+ * matching it against the terminal ledger entries and live mesh nodes. Shared by the
214
+ * remote-ledger scan (inside the MeshRuntimeStore branch) and the full-ledger scan
215
+ * (standalone branch) — those two loops were previously byte-identical except for a
216
+ * single `dbTaskIds.has(taskId)` skip guard that stays in the caller. Returns the record
217
+ * plus `terminalRow` so the caller can route it into terminal/stale/active buckets.
218
+ */
219
+ function buildLedgerDirectDispatchRecord(
220
+ dispatch: MeshLedgerEntry,
221
+ ctx: { terminals: MeshLedgerEntry[]; nodes: any[] | undefined; now: number },
222
+ ): { record: MeshActiveWorkRecord; terminalRow: boolean } {
223
+ const taskId = directDispatchTaskId(dispatch);
224
+ const terminal = ctx.terminals
225
+ .filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
226
+ .find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
227
+ const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
228
+ const live = sessionStatusFromNodes(ctx.nodes, dispatch.nodeId, dispatch.sessionId);
229
+ const status = terminalStatus || live.status || 'assigned';
230
+ const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
231
+ const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
232
+ status,
233
+ isTerminalRow: terminalRow,
234
+ hasTerminalStatus: Boolean(terminalStatus),
235
+ liveStatus: live.status,
236
+ liveStaleReason: live.staleReason,
237
+ dispatchedToIdleSession: dispatch.payload?.dispatchedToIdleSession === true,
238
+ });
239
+ const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
240
+ const { title, summary } = summarizeMessage(message);
241
+ const record: MeshActiveWorkRecord = {
242
+ taskId,
243
+ source: 'direct',
244
+ status,
245
+ nodeId: dispatch.nodeId,
246
+ sessionId: dispatch.sessionId,
247
+ providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
248
+ taskTitle: readString(dispatch.payload?.taskTitle) || title,
249
+ taskSummary: readString(dispatch.payload?.taskSummary) || summary,
250
+ message,
251
+ taskMode: readString(dispatch.payload?.taskMode),
252
+ createdAt: dispatch.timestamp,
253
+ updatedAt: terminal?.timestamp || dispatch.timestamp,
254
+ dispatchedAt: dispatch.timestamp,
255
+ elapsedMs: elapsedSince(dispatch.timestamp, ctx.now),
256
+ terminal: terminalRow,
257
+ terminalKind: terminal?.kind,
258
+ terminalAt: terminal?.timestamp,
259
+ staleReason: live.staleReason || ledgerOnlyStaleReason,
260
+ ...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
261
+ };
262
+ return { record, terminalRow };
263
+ }
264
+
211
265
  export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary {
212
266
  const statusCounts: Record<MeshActiveWorkStatus, number> = {
213
267
  pending: 0,
@@ -324,51 +378,13 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
324
378
  const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
325
379
  const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
326
380
  for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
327
- const taskId = directDispatchTaskId(dispatch);
328
- if (dbTaskIds.has(taskId)) continue; // already covered by MeshRuntimeStore path above
329
- const terminal = terminals
330
- .filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
331
- .find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
332
- const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
333
- const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
334
- const status = terminalStatus || live.status || 'assigned';
335
- const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
336
- const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
337
- status,
338
- isTerminalRow: terminalRow,
339
- hasTerminalStatus: Boolean(terminalStatus),
340
- liveStatus: live.status,
341
- liveStaleReason: live.staleReason,
342
- dispatchedToIdleSession: dispatch.payload?.dispatchedToIdleSession === true,
343
- });
344
- const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
345
- const { title, summary } = summarizeMessage(message);
346
- const record: MeshActiveWorkRecord = {
347
- taskId,
348
- source: 'direct',
349
- status,
350
- nodeId: dispatch.nodeId,
351
- sessionId: dispatch.sessionId,
352
- providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
353
- taskTitle: readString(dispatch.payload?.taskTitle) || title,
354
- taskSummary: readString(dispatch.payload?.taskSummary) || summary,
355
- message,
356
- taskMode: readString(dispatch.payload?.taskMode),
357
- createdAt: dispatch.timestamp,
358
- updatedAt: terminal?.timestamp || dispatch.timestamp,
359
- dispatchedAt: dispatch.timestamp,
360
- elapsedMs: elapsedSince(dispatch.timestamp, now),
361
- terminal: terminalRow,
362
- terminalKind: terminal?.kind,
363
- terminalAt: terminal?.timestamp,
364
- staleReason: live.staleReason || ledgerOnlyStaleReason,
365
- ...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
366
- };
381
+ if (dbTaskIds.has(directDispatchTaskId(dispatch))) continue; // already covered by MeshRuntimeStore path above
382
+ const { record, terminalRow } = buildLedgerDirectDispatchRecord(dispatch, { terminals, nodes: opts.nodes, now });
367
383
  if (terminalRow) {
368
384
  terminalDirectWork.push(record);
369
385
  if (opts.includeTerminalDirect !== true) continue;
370
386
  }
371
- if ((live.staleReason || ledgerOnlyStaleReason) && !terminalRow) {
387
+ if (record.staleReason && !terminalRow) {
372
388
  staleDirectWork.push(record);
373
389
  continue;
374
390
  }
@@ -379,50 +395,12 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
379
395
  const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
380
396
  const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
381
397
  for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
382
- const taskId = directDispatchTaskId(dispatch);
383
- const terminal = terminals
384
- .filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
385
- .find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
386
- const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
387
- const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
388
- const status = terminalStatus || live.status || 'assigned';
389
- const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
390
- const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
391
- status,
392
- isTerminalRow: terminalRow,
393
- hasTerminalStatus: Boolean(terminalStatus),
394
- liveStatus: live.status,
395
- liveStaleReason: live.staleReason,
396
- dispatchedToIdleSession: dispatch.payload?.dispatchedToIdleSession === true,
397
- });
398
- const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
399
- const { title, summary } = summarizeMessage(message);
400
- const record: MeshActiveWorkRecord = {
401
- taskId,
402
- source: 'direct',
403
- status,
404
- nodeId: dispatch.nodeId,
405
- sessionId: dispatch.sessionId,
406
- providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
407
- taskTitle: readString(dispatch.payload?.taskTitle) || title,
408
- taskSummary: readString(dispatch.payload?.taskSummary) || summary,
409
- message,
410
- taskMode: readString(dispatch.payload?.taskMode),
411
- createdAt: dispatch.timestamp,
412
- updatedAt: terminal?.timestamp || dispatch.timestamp,
413
- dispatchedAt: dispatch.timestamp,
414
- elapsedMs: elapsedSince(dispatch.timestamp, now),
415
- terminal: terminalRow,
416
- terminalKind: terminal?.kind,
417
- terminalAt: terminal?.timestamp,
418
- staleReason: live.staleReason || ledgerOnlyStaleReason,
419
- ...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
420
- };
398
+ const { record, terminalRow } = buildLedgerDirectDispatchRecord(dispatch, { terminals, nodes: opts.nodes, now });
421
399
  if (terminalRow) {
422
400
  terminalDirectWork.push(record);
423
401
  if (opts.includeTerminalDirect !== true) continue;
424
402
  }
425
- if ((live.staleReason || ledgerOnlyStaleReason) && !terminalRow) {
403
+ if (record.staleReason && !terminalRow) {
426
404
  staleDirectWork.push(record);
427
405
  continue;
428
406
  }
@@ -1325,6 +1325,14 @@ export interface MeshQueueTriggerResult {
1325
1325
  status?: string;
1326
1326
  }>;
1327
1327
  autoLaunchStarted: boolean;
1328
+ /**
1329
+ * True when a worker session is already on its way to claim a still-pending task —
1330
+ * either launched this tick (autoLaunchStarted) or launched on a prior tick and still
1331
+ * booting/awaiting-claim. Callers MUST treat this as "wait, do not launch another
1332
+ * session": a second launch double-edits the worktree. Mutually informative with
1333
+ * `noIdleMeshSessionAvailable`, which is suppressed whenever this is true.
1334
+ */
1335
+ autoLaunchPending?: boolean;
1328
1336
  noIdleMeshSessionAvailable?: boolean;
1329
1337
  }
1330
1338
 
@@ -1485,6 +1493,28 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
1485
1493
  nodeId: task.assignedNodeId,
1486
1494
  sessionId: task.assignedSessionId,
1487
1495
  }));
1496
+
1497
+ // An auto-launch is "pending" when the coordinator has already spun a session up
1498
+ // for a still-pending task and is waiting on that session's idle→claim. This covers
1499
+ // two ticks:
1500
+ // - THIS tick fired the launch (autoLaunchStarted), or
1501
+ // - a PRIOR tick launched a session that is still booting/awaiting-claim — the
1502
+ // per-task await-claim guard (maybeAutoLaunchOneQueueSession) deliberately
1503
+ // declines to launch again, so autoLaunchStarted is false even though a session
1504
+ // is on its way to claim this task.
1505
+ // Without this signal, the second tick reports `noIdleMeshSessionAvailable` and the
1506
+ // MCP guidance tells the coordinator to launch ANOTHER worker — producing a duplicate
1507
+ // session that double-edits the worktree. The claim itself is fine; only the wording
1508
+ // was wrong, so we surface `autoLaunchPending` to suppress the bad "launch one more"
1509
+ // advice while the just-launched session converges.
1510
+ const autoLaunchPending = autoLaunchStarted || afterQueue.some(task => {
1511
+ if (task.status !== 'pending') return false;
1512
+ const al = task.autoLaunch;
1513
+ if (!al || (al.status !== 'started' && al.status !== 'completed')) return false;
1514
+ const launchedAtMs = Date.parse(al.updatedAt);
1515
+ return Number.isFinite(launchedAtMs) && Date.now() - launchedAtMs < AUTO_LAUNCH_AWAIT_CLAIM_MS;
1516
+ });
1517
+
1488
1518
  return {
1489
1519
  success: true,
1490
1520
  meshId,
@@ -1498,7 +1528,11 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
1498
1528
  remoteIdleSessionsChecked,
1499
1529
  skippedSessions,
1500
1530
  autoLaunchStarted,
1501
- ...(pendingAfter > 0 && newlyAssignedTasks.length === 0 && localIdleSessionsChecked === 0 && remoteIdleSessionsChecked === 0 && !autoLaunchStarted
1531
+ ...(autoLaunchPending ? { autoLaunchPending: true } : {}),
1532
+ // Only report "no idle session, go launch one" when nothing is already on its way.
1533
+ // A pending auto-launch (this tick or a prior still-converging one) means a session
1534
+ // WILL claim shortly, so it is not a no-session-available situation.
1535
+ ...(pendingAfter > 0 && newlyAssignedTasks.length === 0 && localIdleSessionsChecked === 0 && remoteIdleSessionsChecked === 0 && !autoLaunchPending
1502
1536
  ? { noIdleMeshSessionAvailable: true }
1503
1537
  : {}),
1504
1538
  };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared SHA-256 hashing helpers for daemon-core (Node runtime).
3
+ *
4
+ * daemon-core runs under Node, so it uses `node:crypto` directly and must NOT
5
+ * import the server's WebCrypto-based `utils/crypto.ts` (different runtime +
6
+ * cross-package dependency direction). Output is byte-identical to that helper
7
+ * for the same input — lowercase hex SHA-256.
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+
11
+ /** Full lowercase hex SHA-256 digest of `input`. */
12
+ export function sha256Hex(input: string): string {
13
+ return createHash('sha256').update(input).digest('hex');
14
+ }
15
+
16
+ /**
17
+ * Truncated SHA-256 hex digest — the first `length` hex chars (default 16).
18
+ * Used for stable short identifiers (workspace hashes, token ids, coordinator
19
+ * home dirs) where collision risk at 16 hex chars (64 bits) is negligible.
20
+ */
21
+ export function shortHash(input: string, length = 16): string {
22
+ return sha256Hex(input).slice(0, length);
23
+ }