@adhdev/daemon-core 0.9.82-rc.190 → 0.9.82-rc.192

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.
@@ -70,4 +70,14 @@ export declare class BeadsDB {
70
70
  cleanupTerminalDirectDispatches(olderThanMs: number): void;
71
71
  deleteDirectDispatches(meshId: string): void;
72
72
  markStaleDirectDispatches(meshId: string, olderThanMs: number): void;
73
+ setRemoteIdleSession(nodeId: string, sessionId: string, providerType: string, expiresAt: number, metadata?: any): void;
74
+ getRemoteIdleSessions(): Array<{
75
+ nodeId: string;
76
+ sessionId: string;
77
+ providerType: string;
78
+ expiresAt: number;
79
+ metadata?: any;
80
+ }>;
81
+ deleteRemoteIdleSession(nodeId: string, sessionId: string): void;
82
+ pruneExpiredRemoteIdleSessions(): void;
73
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.190",
3
+ "version": "0.9.82-rc.192",
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",
@@ -111,6 +111,15 @@ export class BeadsDB {
111
111
 
112
112
  CREATE INDEX IF NOT EXISTS idx_direct_dispatches_mesh_session
113
113
  ON mesh_direct_dispatches(mesh_id, session_id, status);
114
+
115
+ CREATE TABLE IF NOT EXISTS remote_idle_sessions (
116
+ node_id TEXT NOT NULL,
117
+ session_id TEXT NOT NULL,
118
+ provider_type TEXT NOT NULL,
119
+ expires_at INTEGER NOT NULL,
120
+ metadata TEXT,
121
+ PRIMARY KEY (node_id, session_id)
122
+ );
114
123
  `);
115
124
  }
116
125
 
@@ -487,4 +496,32 @@ export class BeadsDB {
487
496
  WHERE mesh_id = ? AND status = 'dispatched' AND dispatched_at < ?
488
497
  `).run(now, meshId, cutoff);
489
498
  }
499
+
500
+ // ── Remote Idle Sessions ─────────────────────────────────────────────────
501
+
502
+ setRemoteIdleSession(nodeId: string, sessionId: string, providerType: string, expiresAt: number, metadata?: any): void {
503
+ this.db.prepare(`
504
+ INSERT OR REPLACE INTO remote_idle_sessions (node_id, session_id, provider_type, expires_at, metadata)
505
+ VALUES (?, ?, ?, ?, ?)
506
+ `).run(nodeId, sessionId, providerType, expiresAt, metadata ? JSON.stringify(metadata) : null);
507
+ }
508
+
509
+ getRemoteIdleSessions(): Array<{ nodeId: string; sessionId: string; providerType: string; expiresAt: number; metadata?: any }> {
510
+ const rows = this.db.prepare('SELECT node_id, session_id, provider_type, expires_at, metadata FROM remote_idle_sessions').all() as Array<any>;
511
+ return rows.map(r => ({
512
+ nodeId: r.node_id,
513
+ sessionId: r.session_id,
514
+ providerType: r.provider_type,
515
+ expiresAt: r.expires_at,
516
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
517
+ }));
518
+ }
519
+
520
+ deleteRemoteIdleSession(nodeId: string, sessionId: string): void {
521
+ this.db.prepare('DELETE FROM remote_idle_sessions WHERE node_id = ? AND session_id = ?').run(nodeId, sessionId);
522
+ }
523
+
524
+ pruneExpiredRemoteIdleSessions(): void {
525
+ this.db.prepare('DELETE FROM remote_idle_sessions WHERE expires_at <= ?').run(Date.now());
526
+ }
490
527
  }
@@ -18,14 +18,7 @@ import { fastForwardMeshNode } from './mesh-fast-forward.js';
18
18
  // can assign tasks to them. Each entry carries an expiresAt timestamp;
19
19
  // entries are swept on insertion to prevent unbounded growth.
20
20
  // ---------------------------------------------------------------------------
21
- interface RemoteIdleSession {
22
- nodeId: string;
23
- sessionId: string;
24
- providerType: string;
25
- expiresAt: number;
26
- }
27
21
  const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
28
- const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
29
22
 
30
23
  // ---------------------------------------------------------------------------
31
24
  // Workspace-to-mesh lookup cache
@@ -55,10 +48,9 @@ export function __resetIdleAutoFastForwardForTests(): void {
55
48
  }
56
49
 
57
50
  function sweepExpiredRemoteIdleSessions(): void {
58
- const now = Date.now();
59
- for (const [key, session] of remoteIdleSessions) {
60
- if (session.expiresAt <= now) remoteIdleSessions.delete(key);
61
- }
51
+ try {
52
+ BeadsDB.getInstance().pruneExpiredRemoteIdleSessions();
53
+ } catch { /* best-effort */ }
62
54
  }
63
55
 
64
56
  // ---------------------------------------------------------------------------
@@ -1286,14 +1278,21 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
1286
1278
  }
1287
1279
 
1288
1280
  // Also check known idle remote sessions
1289
- for (const [key, idle] of remoteIdleSessions.entries()) {
1281
+ let remoteSessions: Array<{ nodeId: string; sessionId: string; providerType: string }> = [];
1282
+ try {
1283
+ remoteSessions = BeadsDB.getInstance().getRemoteIdleSessions();
1284
+ } catch { /* best-effort */ }
1285
+
1286
+ for (const idle of remoteSessions) {
1290
1287
  // Find if this node is in the same mesh
1291
1288
  const node = mesh.nodes.find((n: any) => n.id === idle.nodeId);
1292
1289
  if (node) {
1293
1290
  remoteIdleSessionsChecked += 1;
1294
1291
  const assigned = tryAssignQueueTask(components, meshId, idle.nodeId, idle.sessionId, idle.providerType);
1295
1292
  if (assigned) {
1296
- remoteIdleSessions.delete(key);
1293
+ try {
1294
+ BeadsDB.getInstance().deleteRemoteIdleSession(idle.nodeId, idle.sessionId);
1295
+ } catch { /* best-effort */ }
1297
1296
  }
1298
1297
  }
1299
1298
  }
@@ -1534,7 +1533,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1534
1533
  });
1535
1534
  if (intentionalCleanupStop) {
1536
1535
  if (eventSessionId && eventNodeId) {
1537
- remoteIdleSessions.delete(`${eventNodeId}:${eventSessionId}`);
1536
+ try {
1537
+ BeadsDB.getInstance().deleteRemoteIdleSession(eventNodeId, eventSessionId);
1538
+ } catch { /* best-effort */ }
1538
1539
  }
1539
1540
  LOG.info('MeshEvents', `Suppressed ${args.event} for intentionally cleanup-stopped session ${eventSessionId || '(unknown session)'}`);
1540
1541
  return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
@@ -1632,19 +1633,26 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1632
1633
  }
1633
1634
 
1634
1635
  // ── Task Queue & Ledger ──
1636
+ // Helpers that keep queue and direct-dispatch status transitions symmetric.
1637
+ // Both paths must move together so buildMeshActiveWork and the coordinator
1638
+ // view stay consistent regardless of which dispatch path was used.
1639
+ function markSessionTerminal(sessionId: string, outcome: 'completed' | 'failed', occurredAtMs?: number | null): { id?: string } | null {
1640
+ const task = updateSessionTaskStatus(args.meshId, sessionId, outcome, {
1641
+ occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
1642
+ });
1643
+ updateDirectDispatchStatus(args.meshId, sessionId, outcome);
1644
+ setImmediate(() => cleanupTerminalDirectDispatches());
1645
+ return task ? { id: task.id } : null;
1646
+ }
1647
+
1635
1648
  let completedTaskForLedger: { id?: string } | null = null;
1636
1649
  if (args.event === 'agent:generating_completed') {
1637
1650
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1638
1651
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1639
1652
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
1640
-
1653
+
1641
1654
  if (sessionId) {
1642
- const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
1643
- occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
1644
- });
1645
- completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
1646
- updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
1647
- setImmediate(() => cleanupTerminalDirectDispatches());
1655
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed', eventTimestamp);
1648
1656
  if (nodeId && providerType) {
1649
1657
  runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
1650
1658
  }
@@ -1657,55 +1665,51 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1657
1665
  const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
1658
1666
  const workerResult = readWorkerResultMetadata(args.metadataEvent);
1659
1667
  const hasCompletionEvidence = !!finalSummary || !!workerResult;
1660
- const completedTask = sessionId && hasCompletionEvidence
1661
- ? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
1662
- : null;
1663
- if (completedTask && sessionId) {
1664
- completedTaskForLedger = { id: completedTask.id };
1665
- updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
1666
- setImmediate(() => cleanupTerminalDirectDispatches());
1667
- try {
1668
- appendLedgerEntry(args.meshId, {
1669
- kind: 'task_completed',
1670
- nodeId: nodeId || undefined,
1671
- sessionId,
1672
- providerType: providerType || undefined,
1673
- payload: {
1674
- event: args.event,
1675
- nodeLabel: args.nodeLabel,
1676
- taskId: completedTask.id,
1677
- completedViaReady: true,
1678
- providerSessionId,
1679
- finalSummary,
1680
- workerResult,
1681
- evidence: buildTaskCompletionEvidence({
1682
- event: 'agent:ready',
1683
- nodeId,
1684
- sessionId,
1685
- providerType: providerType || undefined,
1668
+ if (sessionId && hasCompletionEvidence) {
1669
+ completedTaskForLedger = markSessionTerminal(sessionId, 'completed');
1670
+ if (completedTaskForLedger) {
1671
+ try {
1672
+ appendLedgerEntry(args.meshId, {
1673
+ kind: 'task_completed',
1674
+ nodeId: nodeId || undefined,
1675
+ sessionId,
1676
+ providerType: providerType || undefined,
1677
+ payload: {
1678
+ event: args.event,
1679
+ nodeLabel: args.nodeLabel,
1680
+ taskId: completedTaskForLedger.id,
1681
+ completedViaReady: true,
1686
1682
  providerSessionId,
1687
1683
  finalSummary,
1688
1684
  workerResult,
1689
- }),
1690
- },
1691
- });
1692
- } catch (e: any) {
1693
- LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
1685
+ evidence: buildTaskCompletionEvidence({
1686
+ event: 'agent:ready',
1687
+ nodeId,
1688
+ sessionId,
1689
+ providerType: providerType || undefined,
1690
+ providerSessionId,
1691
+ finalSummary,
1692
+ workerResult,
1693
+ }),
1694
+ },
1695
+ });
1696
+ } catch (e: any) {
1697
+ LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
1698
+ }
1694
1699
  }
1695
1700
  }
1696
-
1701
+
1697
1702
  if (sessionId && nodeId && providerType) {
1698
1703
  sweepExpiredRemoteIdleSessions();
1699
- remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
1700
- nodeId, sessionId, providerType,
1701
- expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
1702
- });
1704
+ try {
1705
+ BeadsDB.getInstance().setRemoteIdleSession(nodeId, sessionId, providerType, Date.now() + REMOTE_IDLE_SESSION_TTL_MS);
1706
+ } catch { /* best-effort */ }
1703
1707
  setImmediate(() => {
1704
1708
  maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
1705
1709
  .finally(() => {
1706
1710
  try {
1707
1711
  const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
1708
- if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
1712
+ if (assigned) BeadsDB.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1709
1713
  } catch (e: any) {
1710
1714
  LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
1711
1715
  }
@@ -1716,22 +1720,23 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1716
1720
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1717
1721
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1718
1722
  if (sessionId && nodeId) {
1719
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
1723
+ try {
1724
+ BeadsDB.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1725
+ } catch { /* best-effort */ }
1720
1726
  }
1721
1727
  if (sessionId) {
1722
- // Mark direct dispatch as acknowledged — the session started generating.
1723
1728
  updateDirectDispatchStatus(args.meshId, sessionId, 'acked');
1724
1729
  }
1725
1730
  } else if (args.event === 'agent:stopped') {
1726
1731
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
1727
1732
  const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
1728
1733
  if (sessionId && nodeId) {
1729
- remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
1734
+ try {
1735
+ BeadsDB.getInstance().deleteRemoteIdleSession(nodeId, sessionId);
1736
+ } catch { /* best-effort */ }
1730
1737
  }
1731
1738
  if (sessionId) {
1732
- const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
1733
- completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
1734
- updateDirectDispatchStatus(args.meshId, sessionId, 'failed');
1739
+ completedTaskForLedger = markSessionTerminal(sessionId, 'failed');
1735
1740
  }
1736
1741
  }
1737
1742
 
@@ -2019,21 +2024,20 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
2019
2024
  if (!workspace) return;
2020
2025
  const settings = state.settings && typeof state.settings === 'object' ? state.settings as Record<string, unknown> : {};
2021
2026
 
2022
- // Coordinator sessions normally must not inject events into themselves, but a
2023
- // coordinator session can also be the direct-dispatch target of mesh_send_task
2024
- // issued by another coordinator (e.g. a remote orchestrator delegated a task to
2025
- // this local coordinator). In that case the completion event must still flow into
2026
- // injectMeshSystemMessage so the ledger records task_completed and the *other*
2027
- // coordinator's pendingCoordinatorEvents queue is populated. Skip only when this
2028
- // session is purely a coordinator with no in-flight direct dispatch waiting on it.
2027
+ // A coordinator session normally must not inject events into itself. However,
2028
+ // a coordinator can also be the direct-dispatch target of mesh_send_task from
2029
+ // another coordinator. In that case the completion event must flow through
2030
+ // injectMeshSystemMessage so the ledger records task_completed and the other
2031
+ // coordinator's pendingCoordinatorEvents queue is populated. Skip only when
2032
+ // this session has no in-flight direct dispatch.
2029
2033
  const coordinatorMeshId = readNonEmptyString(settings.meshCoordinatorFor);
2030
2034
  let meshIdFromDirectDispatch = '';
2031
2035
  if (coordinatorMeshId) {
2032
2036
  try {
2033
- const activeDispatches = getActiveDirectDispatches(coordinatorMeshId);
2034
- if (activeDispatches.some(d => d.sessionId === instanceId) || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId)) {
2035
- meshIdFromDirectDispatch = coordinatorMeshId;
2036
- }
2037
+ const hasActiveDispatch =
2038
+ getActiveDirectDispatches(coordinatorMeshId).some(d => d.sessionId === instanceId)
2039
+ || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId);
2040
+ if (hasActiveDispatch) meshIdFromDirectDispatch = coordinatorMeshId;
2037
2041
  } catch { /* best-effort */ }
2038
2042
  if (!meshIdFromDirectDispatch) return;
2039
2043
  }