@adhdev/daemon-core 0.9.82-rc.271 → 0.9.82-rc.273

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.
@@ -50,6 +50,16 @@ export declare function summarizeMissionTasks(meshId: string, missionId: string)
50
50
  export declare function summarizeMeshMission(meshId: string, mission: MeshMissionRecord): MeshMissionSummary;
51
51
  /** Active mission summaries for mesh_status / coordinator prompt injection. */
52
52
  export declare function getActiveMeshMissionSummaries(meshId: string): MeshMissionSummary[];
53
+ /**
54
+ * Mission summaries for the mesh_status dashboard surface: every active/paused
55
+ * mission plus a capped, newest-first slice of completed/abandoned history so
56
+ * the dashboard can render a collapsible "history" section without unbounded
57
+ * payload growth. Returned newest-first within each group (active/paused first,
58
+ * then history), so the frontend can split on `status` directly.
59
+ */
60
+ export declare function getMeshStatusMissionSummaries(meshId: string, options?: {
61
+ historyLimit?: number;
62
+ }): MeshMissionSummary[];
53
63
  /**
54
64
  * M3-3: render active missions as a prompt section for {{mission}}.
55
65
  * Empty string when no active mission — the prompt stays byte-identical to
@@ -11,6 +11,7 @@
11
11
  * IMPORTANT: This file must remain runtime-free (types only).
12
12
  */
13
13
  import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
14
+ import type { MeshMissionSummary } from './mesh/mesh-missions.js';
14
15
  export interface RepoMesh {
15
16
  id: string;
16
17
  name: string;
@@ -337,6 +338,13 @@ export interface RepoMeshStatus {
337
338
  nodes: RepoMeshNodeStatus[];
338
339
  queue?: RepoMeshQueueStatus;
339
340
  ledger?: RepoMeshLedgerStatus;
341
+ /**
342
+ * Mission summaries for the dashboard overview. Active/paused missions plus a
343
+ * capped, newest-first slice of completed/abandoned history. Omitted by older
344
+ * daemons — the dashboard must treat this as optional and render an empty
345
+ * state when absent. Split on each entry's `status` for live vs. history.
346
+ */
347
+ missions?: MeshMissionSummary[];
340
348
  }
341
349
  export type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
342
350
  import type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.271",
3
+ "version": "0.9.82-rc.273",
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",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.271",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.273",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -5259,27 +5259,17 @@ export class DaemonCommandRouter {
5259
5259
  }
5260
5260
 
5261
5261
  case 'launch_cli': {
5262
- // Worker launch envelope hardening: a mesh worker session must carry
5263
- // meshCoordinatorDaemonId so completion events route back to the
5264
- // coordinator (the cloud relay and the daemon-core forwarder both key on
5265
- // it). mesh_launch_session resolves coordinatorNode.daemonId || ctx.localDaemonId
5266
- // upstream, but for a freshly-cloned local worktree both can be empty. When the
5267
- // launch is a coordinator-driven worker on this machine and the id is missing,
5268
- // stamp this daemon's own id — worker and coordinator are co-located here.
5269
- {
5270
- const launchSettings = (args?.settings && typeof args.settings === 'object')
5271
- ? args.settings as Record<string, unknown>
5272
- : undefined;
5273
- const isMeshWorkerLaunch = !!launchSettings
5274
- && (readStringValue(launchSettings.meshNodeFor) || launchSettings.launchedByCoordinator === true);
5275
- const hasCoordinatorDaemonId = !!launchSettings && !!readStringValue(launchSettings.meshCoordinatorDaemonId);
5276
- if (launchSettings && isMeshWorkerLaunch && !hasCoordinatorDaemonId) {
5277
- try {
5278
- const localDaemonId = readStringValue(loadConfig().machineId);
5279
- if (localDaemonId) launchSettings.meshCoordinatorDaemonId = localDaemonId;
5280
- } catch { /* best-effort — launch proceeds without the stamp */ }
5281
- }
5282
- }
5262
+ // The coordinator routing anchor (meshCoordinatorDaemonId) is stamped
5263
+ // upstream by mesh_launch_session, which resolves
5264
+ // coordinatorNode.daemonId || ctx.localDaemonId || ctx.localMachineId and
5265
+ // fail-closes for a remote node when none resolve. We deliberately do NOT
5266
+ // self-stamp this daemon's own id when the field is missing: for a
5267
+ // P2P-relayed remote worker launch, stamping the worker's own id would make
5268
+ // the self-forward gate (mesh-events-coordinator: sameDaemonId) treat the
5269
+ // worker as its own coordinator, suppressing the spontaneous completion-event
5270
+ // forward and leaving the event in the pending inbox until a read_chat
5271
+ // reconcile drains it. If the anchor is genuinely absent here, leave it
5272
+ // absent rather than poison the routing.
5283
5273
  const launchResult = await this.deps.cliManager.handleCliCommand(cmd, args);
5284
5274
  // Bug C fix (part 1): when launching a mesh node worker session, surface
5285
5275
  // bootstrapPending:true if the node's worktree bootstrap is still running.
@@ -8428,6 +8418,8 @@ export class DaemonCommandRouter {
8428
8418
  nodes: mesh.nodes || [],
8429
8419
  liveSessionRecords: liveMeshSessions,
8430
8420
  });
8421
+ const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
8422
+ const missions = getMeshStatusMissionSummaries(meshId);
8431
8423
  const statusResult = {
8432
8424
  success: true,
8433
8425
  meshId: mesh.id,
@@ -8470,6 +8462,7 @@ export class DaemonCommandRouter {
8470
8462
  nodes: nodeStatuses,
8471
8463
  queue: { tasks: queue, summary: queueSummary },
8472
8464
  ledger: { entries: ledgerEntries, summary: ledgerSummary },
8465
+ ...(missions.length > 0 ? { missions } : {}),
8473
8466
  ...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
8474
8467
  ...(historicalSessions ? { historicalSessions } : {}),
8475
8468
  ...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
@@ -125,6 +125,27 @@ export function getActiveMeshMissionSummaries(meshId: string): MeshMissionSummar
125
125
  return getMeshMissions(meshId, ['active']).map(mission => summarizeMeshMission(meshId, mission));
126
126
  }
127
127
 
128
+ /**
129
+ * Mission summaries for the mesh_status dashboard surface: every active/paused
130
+ * mission plus a capped, newest-first slice of completed/abandoned history so
131
+ * the dashboard can render a collapsible "history" section without unbounded
132
+ * payload growth. Returned newest-first within each group (active/paused first,
133
+ * then history), so the frontend can split on `status` directly.
134
+ */
135
+ export function getMeshStatusMissionSummaries(
136
+ meshId: string,
137
+ options?: { historyLimit?: number },
138
+ ): MeshMissionSummary[] {
139
+ const historyLimit = Math.max(0, options?.historyLimit ?? 10);
140
+ const all = getMeshMissions(meshId);
141
+ const live = all.filter(m => m.status === 'active' || m.status === 'paused');
142
+ const history = all
143
+ .filter(m => m.status === 'completed' || m.status === 'abandoned')
144
+ .sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))
145
+ .slice(0, historyLimit);
146
+ return [...live, ...history].map(mission => summarizeMeshMission(meshId, mission));
147
+ }
148
+
128
149
  /**
129
150
  * M3-3: render active missions as a prompt section for {{mission}}.
130
151
  * Empty string when no active mission — the prompt stays byte-identical to
@@ -1578,9 +1578,23 @@ export class CliProviderInstance implements ProviderInstance {
1578
1578
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
1579
1579
  const modal = adapterStatus.activeModal;
1580
1580
  LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
1581
+ // Include the FSM's approval entry seq, mirroring the auto-approve
1582
+ // path (maybeAutoApproveStatus) and resolveModal's sameEntryReResolve
1583
+ // guard. Two distinct back-to-back approvals can carry identical
1584
+ // message/buttons (very common with claude-cli's "Allow Bash
1585
+ // command?"). Without the seq their fingerprints collide and the dedup
1586
+ // below silently drops the second waiting_approval event — it is never
1587
+ // emitted, so it cannot even land in the pending inbox for a later
1588
+ // read_chat reconcile to recover. The seq is bumped by the FSM on every
1589
+ // fresh waiting_approval entry, so a new approval always yields a new
1590
+ // fingerprint and emits.
1591
+ const approvalEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
1592
+ ? adapterStatus.approvalEntrySeq
1593
+ : 0;
1581
1594
  const approvalFingerprint = JSON.stringify({
1582
1595
  message: typeof modal?.message === 'string' ? modal.message.trim() : '',
1583
1596
  buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
1597
+ seq: approvalEntrySeq,
1584
1598
  });
1585
1599
  // PTY redraws repeat the same modal content; fingerprint dedup prevents duplicate events.
1586
1600
  // Do NOT also gate on lastStatus: consecutive approvals can arrive waiting_approval→waiting_approval
@@ -1598,6 +1612,15 @@ export class CliProviderInstance implements ProviderInstance {
1598
1612
  modalButtons: modal?.buttons,
1599
1613
  });
1600
1614
  }
1615
+ } else if (newStatus === 'generating' && this.lastStatus === 'waiting_approval') {
1616
+ // Approval resolved and the agent resumed work. Defense-in-depth:
1617
+ // clear the approval emit fingerprint here too (not only on
1618
+ // completion at scheduleCompletedDebounceFlush). A subsequent
1619
+ // waiting_approval with the same modal content as the one just
1620
+ // resolved would otherwise collide with the stale fingerprint and be
1621
+ // dropped. The seq in the fingerprint already separates entries; this
1622
+ // reset is a belt-and-suspenders guard for the re-entry case.
1623
+ this.lastApprovalEventFingerprint = '';
1601
1624
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
1602
1625
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
1603
1626
  // Guard: if generatingStartedAt===0 and no debounce pending, the generating phase
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
15
+ import type { MeshMissionSummary } from './mesh/mesh-missions.js';
15
16
 
16
17
  // ─── Core Mesh Types ────────────────────────────
17
18
 
@@ -405,6 +406,13 @@ export interface RepoMeshStatus {
405
406
  nodes: RepoMeshNodeStatus[];
406
407
  queue?: RepoMeshQueueStatus;
407
408
  ledger?: RepoMeshLedgerStatus;
409
+ /**
410
+ * Mission summaries for the dashboard overview. Active/paused missions plus a
411
+ * capped, newest-first slice of completed/abandoned history. Omitted by older
412
+ * daemons — the dashboard must treat this as optional and render an empty
413
+ * state when absent. Split on each entry's `status` for live vs. history.
414
+ */
415
+ missions?: MeshMissionSummary[];
408
416
  }
409
417
 
410
418
  // RepoMeshSessionStatus shape now lives in @adhdev/mesh-shared (shared with