@adhdev/daemon-core 0.9.82-rc.348 → 0.9.82-rc.349

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.
@@ -49,20 +49,6 @@ export declare function readWorkerResultMetadata(event: Record<string, unknown>)
49
49
  * and the held-event ledger audit record so both read the summary the same way.
50
50
  */
51
51
  export declare function readMeshCompletionSummary(metadataEvent: Record<string, unknown>): string;
52
- /**
53
- * A coordinator that surfaces a REMOTE worker's mesh session has no local instance for
54
- * it, so the status snapshot's getLastDisplayMessage has nothing to read and the only
55
- * preview the coordinator-mirrored copy could ever carry comes from the worker's
56
- * completion event. The worker's latest assistant reply rides on that event as
57
- * `finalSummary` (and as `workerResult.summary` / `result.summary` on some paths).
58
- *
59
- * This resolves that assistant text into a preview the coordinator can stamp onto its
60
- * mirror entry so the mobile inbox shows the worker's latest assistant response instead
61
- * of being stuck on the first dispatched user task (which is the only message the
62
- * coordinator-side transcript ever holds). Returns undefined when the event carries no
63
- * assistant text — non-completion lifecycle events (generating_started / ready without a
64
- * summary) must NOT clobber a previously surfaced preview.
65
- */
66
52
  export declare function resolveMeshSurfacedSessionPreview(metadataEvent: Record<string, unknown>): {
67
53
  preview: string;
68
54
  role: 'assistant';
@@ -62,6 +62,13 @@ export interface MeshMissionSlimSummary extends Omit<MeshMissionSummary, 'goal'>
62
62
  }
63
63
  /** Max chars of goal text retained in the slim (compact) mission summary. */
64
64
  export declare const GOAL_PREVIEW_MAX = 120;
65
+ /**
66
+ * Shorter goal preview used by the mesh_status compact (LLM coordinator) surface.
67
+ * The coordinator only needs to recognize a mission, not read its full goal, and
68
+ * mesh_status repeats every live mission on every poll — so the status preview is
69
+ * tighter than the dashboard / mesh_mission_list preview (GOAL_PREVIEW_MAX).
70
+ */
71
+ export declare const COMPACT_STATUS_GOAL_PREVIEW_MAX = 80;
65
72
  export declare function upsertMeshMission(meshId: string, input: {
66
73
  id?: string;
67
74
  title: string;
@@ -93,6 +100,47 @@ export declare function getMeshStatusMissionSummaries(meshId: string, options?:
93
100
  verbose?: boolean;
94
101
  withStats?: boolean;
95
102
  }): MeshMissionSummary[] | MeshMissionSlimSummary[];
103
+ /** Folded completed/abandoned history for the mesh_status compact surface. */
104
+ export interface MeshStatusMissionsHistoryFold {
105
+ /** Total completed + abandoned missions. */
106
+ count: number;
107
+ /** Count by lifecycle status (e.g. { completed, abandoned }). */
108
+ byStatus: Record<string, number>;
109
+ /** Newest-first id list (capped) so each folded mission stays addressable. */
110
+ missionIds: string[];
111
+ note: string;
112
+ }
113
+ /** Compact mesh_status mission projection: live detail + folded history. */
114
+ export interface MeshStatusMissionsCompact {
115
+ /**
116
+ * Active + paused missions, goal-elided to COMPACT_STATUS_GOAL_PREVIEW_MAX and
117
+ * WITHOUT the operational `stats` rollup — the `tasks` aggregate already carries
118
+ * progress, and stats (durations/retries) is a verbose/dashboard concern.
119
+ */
120
+ live: MeshMissionSlimSummary[];
121
+ /** Completed + abandoned missions folded to counts + ids; null when none. */
122
+ historyFold: MeshStatusMissionsHistoryFold | null;
123
+ }
124
+ /**
125
+ * Mission projection for the mesh_status COMPACT (LLM coordinator) surface.
126
+ *
127
+ * Unlike getMeshStatusMissionSummaries (which emits every live mission plus a
128
+ * capped slice of full-detail history, each carrying a stats rollup), this keeps
129
+ * per-mission detail ONLY for live (active/paused) missions and folds the whole
130
+ * completed/abandoned history into a counts + id-list summary. Combined with the
131
+ * tighter goal preview and dropped stats, this is what keeps the compact
132
+ * mesh_status payload bounded as a mesh accumulates missions — the missions
133
+ * section previously dominated the payload (full goalPreview + tasks + stats per
134
+ * mission, for every live mission and up to 10 history missions, on every poll).
135
+ *
136
+ * The stored missions are untouched; this is an output-only projection. Full
137
+ * mission detail (goal text + stats + history) stays available via
138
+ * mesh_status verbose=true or mesh_mission_list.
139
+ */
140
+ export declare function getMeshStatusMissionsCompact(meshId: string, options?: {
141
+ previewMax?: number;
142
+ historyIdLimit?: number;
143
+ }): MeshStatusMissionsCompact;
96
144
  /**
97
145
  * Read-only mission listing for the mesh_mission_list tool. Returns summaries
98
146
  * (record + live task aggregate) for missions matching `statuses` — or every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.348",
3
+ "version": "0.9.82-rc.349",
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.348",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.349",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
package/src/index.ts CHANGED
@@ -188,8 +188,8 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
188
188
 
189
189
  // ── Mesh Coordinator ──
190
190
  export { buildCoordinatorSystemPrompt } from './mesh/coordinator-prompt.js';
191
- export { upsertMeshMission, getMeshMissions, getMeshMission, summarizeMissionTasks, summarizeMeshMission, getActiveMeshMissionSummaries, getMeshStatusMissionSummaries, listMeshMissionSummaries, buildMissionPromptSection, GOAL_PREVIEW_MAX, MESH_MISSION_STATUSES } from './mesh/mesh-missions.js';
192
- export type { MeshMissionRecord, MeshMissionStatus, MeshMissionSummary, MeshMissionSlimSummary, MeshMissionTaskAggregate } from './mesh/mesh-missions.js';
191
+ export { upsertMeshMission, getMeshMissions, getMeshMission, summarizeMissionTasks, summarizeMeshMission, getActiveMeshMissionSummaries, getMeshStatusMissionSummaries, getMeshStatusMissionsCompact, listMeshMissionSummaries, buildMissionPromptSection, GOAL_PREVIEW_MAX, COMPACT_STATUS_GOAL_PREVIEW_MAX, MESH_MISSION_STATUSES } from './mesh/mesh-missions.js';
192
+ export type { MeshMissionRecord, MeshMissionStatus, MeshMissionSummary, MeshMissionSlimSummary, MeshMissionTaskAggregate, MeshStatusMissionsCompact, MeshStatusMissionsHistoryFold } from './mesh/mesh-missions.js';
193
193
  export { computeMeshTaskStats, computeMeshMissionStats } from './mesh/mesh-task-stats.js';
194
194
  export type { MeshTaskStats, MeshMissionStats } from './mesh/mesh-task-stats.js';
195
195
  export { deriveMeshReviewInboxItems } from './mesh/mesh-review-inbox.js';
@@ -14,6 +14,7 @@ import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents } f
14
14
  import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
15
15
  import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent, isUnroutableDelegateRejection } from './mesh-routing.js';
16
16
  import { enqueueUnresolvedDelegateForward, peekUnresolvedDelegateForwards, ackUnresolvedDelegateForward } from './mesh-unresolved-forward-outbox.js';
17
+ import { getLastDisplayMessage } from '../status/snapshot.js';
17
18
  import { resolveDelegatedWorkerAutoApprove, resolveProviderMaxParallel, resolveNodeSchedulingPriority, normalizeMeshSchedulingStrategy } from '../repo-mesh-types.js';
18
19
  import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
19
20
  import { normalizeMeshNodeId, meshNodeIdMatches, type MeshNodeIdentified } from '@adhdev/mesh-shared';
@@ -1424,6 +1425,26 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1424
1425
  (sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorSessionId,
1425
1426
  ) || readNonEmptyString(args.metadataEvent.meshCoordinatorSessionId);
1426
1427
 
1428
+ // T2: a summary-less completion (and any non-completion status-sync event) carries no
1429
+ // assistant text on the event, so resolveMeshSurfacedSessionPreview had nothing to surface
1430
+ // and the coordinator's inbox mirror stayed stuck on the first dispatched user task. When
1431
+ // THIS daemon hosts the live worker instance (sourceSession present), derive the worker's
1432
+ // latest display message straight from its transcript and attach it to the event as
1433
+ // lastMessagePreview/lastMessageRole/lastMessageAt. resolveMeshSurfacedSessionPreview reads
1434
+ // these as an assistant-only fallback; they also ride the pending-queue + P2P relay
1435
+ // (handleMeshForwardEvent whitelist) so a remote coordinator can surface them. A remote
1436
+ // coordinator has no local instance and keeps relying on the relayed fields — unchanged.
1437
+ const enrichedMetadataEvent = ((): Record<string, unknown> => {
1438
+ const last = sourceSession ? getLastDisplayMessage(sourceSession.getState()) : null;
1439
+ if (!last || !last.preview) return args.metadataEvent;
1440
+ return {
1441
+ ...args.metadataEvent,
1442
+ lastMessagePreview: last.preview,
1443
+ lastMessageRole: last.role,
1444
+ ...(last.receivedAt > 0 ? { lastMessageAt: last.receivedAt } : {}),
1445
+ };
1446
+ })();
1447
+
1427
1448
  // R2: cloud P2P dashboard metadata sync. The cloud daemon used to do this from its own
1428
1449
  // relay listener; now the single core forwarder invokes the injected hook (no-op on
1429
1450
  // standalone) so the event path stays single-listener and the local code path is identical
@@ -1435,15 +1456,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1435
1456
  // mirror would stay stuck on the first dispatched user task. Resolve the
1436
1457
  // worker's latest assistant reply (carried on the completion event's
1437
1458
  // finalSummary / workerResult) into a preview the mirror can stamp, so the
1438
- // mobile inbox reflects the assistant response. Only completion-style events
1439
- // carry assistant text; for everything else this is undefined and the prior
1440
- // surfaced preview is preserved downstream (no clobber).
1441
- const surfacedPreview = resolveMeshSurfacedSessionPreview(args.metadataEvent);
1459
+ // mobile inbox reflects the assistant response. Completion events carry assistant
1460
+ // text as finalSummary; a summary-less completion / status sync falls back to the
1461
+ // worker's latest assistant display message (enrichedMetadataEvent.lastMessage*).
1462
+ // For a mid-turn user-only event this is undefined and the prior surfaced preview
1463
+ // is preserved downstream (no clobber).
1464
+ const surfacedPreview = resolveMeshSurfacedSessionPreview(enrichedMetadataEvent);
1442
1465
  components.onMeshCoordinatorEventForwarded({
1443
1466
  event: args.event,
1444
1467
  meshId: args.meshId,
1445
1468
  nodeId: eventNodeId || undefined,
1446
- ...args.metadataEvent,
1469
+ ...enrichedMetadataEvent,
1447
1470
  // Ensure a `workspace` field reaches updateMeshOwnedSession even when the
1448
1471
  // worker provider event only carried `workspaceName`. The merge spread of
1449
1472
  // metadataEvent above wins when it already has a non-empty `workspace`.
@@ -1877,7 +1900,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1877
1900
  workspace: readNonEmptyString(args.metadataEvent.workspace)
1878
1901
  || readNonEmptyString(args.metadataEvent.workspaceName),
1879
1902
  metadataEvent: {
1880
- ...args.metadataEvent,
1903
+ ...enrichedMetadataEvent,
1881
1904
  ...(recoveryContext ? { recoveryContext } : {}),
1882
1905
  // Stash the coordinator session id INSIDE metadataEvent too, so it survives the
1883
1906
  // P2P relay serialization (buildForwardPayloadFromPending spreads metadata; the
@@ -1955,6 +1978,13 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1955
1978
  providerName: readNonEmptyString(payload.providerName),
1956
1979
  ...(payload.sessionSettings && typeof payload.sessionSettings === 'object' && !Array.isArray(payload.sessionSettings) ? { sessionSettings: payload.sessionSettings } : {}),
1957
1980
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
1981
+ // T2: carry the worker's status-snapshot last-message preview across the machine
1982
+ // boundary so a summary-less completion still surfaces the assistant reply in the
1983
+ // coordinator's inbox mirror. resolveMeshSurfacedSessionPreview reads these
1984
+ // (assistant-role only) when finalSummary is absent.
1985
+ lastMessagePreview: readNonEmptyString(payload.lastMessagePreview),
1986
+ lastMessageRole: readNonEmptyString(payload.lastMessageRole),
1987
+ ...(payload.lastMessageAt !== undefined ? { lastMessageAt: payload.lastMessageAt } : {}),
1958
1988
  jobId: readNonEmptyString(payload.jobId),
1959
1989
  interactionId: readNonEmptyString(payload.interactionId),
1960
1990
  status: readNonEmptyString(payload.status),
@@ -162,17 +162,46 @@ export function readMeshCompletionSummary(metadataEvent: Record<string, unknown>
162
162
  * assistant text — non-completion lifecycle events (generating_started / ready without a
163
163
  * summary) must NOT clobber a previously surfaced preview.
164
164
  */
165
+ function truncateSurfacedPreview(text: string): string {
166
+ const truncationSuffix = '...[truncated]';
167
+ return text.length > MESH_SURFACED_PREVIEW_MAX_CHARS
168
+ ? `${text.slice(0, MESH_SURFACED_PREVIEW_MAX_CHARS - truncationSuffix.length)}${truncationSuffix}`
169
+ : text;
170
+ }
171
+
165
172
  export function resolveMeshSurfacedSessionPreview(
166
173
  metadataEvent: Record<string, unknown>,
167
174
  ): { preview: string; role: 'assistant'; receivedAt: number } | undefined {
175
+ // 1) Completion summary — the worker's final assistant text rides a completion event
176
+ // as finalSummary / workerResult.summary / result.summary.
168
177
  const summaryText = readMeshCompletionSummary(metadataEvent);
169
- if (!summaryText) return undefined;
170
- const truncationSuffix = '...[truncated]';
171
- const preview = summaryText.length > MESH_SURFACED_PREVIEW_MAX_CHARS
172
- ? `${summaryText.slice(0, MESH_SURFACED_PREVIEW_MAX_CHARS - truncationSuffix.length)}${truncationSuffix}`
173
- : summaryText;
174
- const timestamp = readEventTimestampValue(metadataEvent.timestamp);
175
- return { preview, role: 'assistant', receivedAt: timestamp };
178
+ if (summaryText) {
179
+ return {
180
+ preview: truncateSurfacedPreview(summaryText),
181
+ role: 'assistant',
182
+ receivedAt: readEventTimestampValue(metadataEvent.timestamp),
183
+ };
184
+ }
185
+ // 2) Status-snapshot fallback — a completion that carried NO summary (and any later
186
+ // status-sync event) still ships the worker's latest display message on the event as
187
+ // lastMessagePreview/lastMessageRole/lastMessageAt (computed by getLastDisplayMessage
188
+ // on the worker, attached in injectMeshSystemMessage and carried over the relay).
189
+ // Surface it ONLY when it is an assistant reply: a user-role last message means the
190
+ // turn is still mid-flight (the dispatched task is the only message), and surfacing it
191
+ // would both re-introduce the "inbox stuck on the user task" bug and clobber a
192
+ // previously surfaced assistant preview. The web inbox guard renders only an
193
+ // assistant-role preview anyway, so a user-role one would be inert there.
194
+ const lastPreview = readNonEmptyString(metadataEvent.lastMessagePreview);
195
+ const lastRole = readNonEmptyString(metadataEvent.lastMessageRole);
196
+ if (lastPreview && lastRole === 'assistant') {
197
+ return {
198
+ preview: truncateSurfacedPreview(lastPreview),
199
+ role: 'assistant',
200
+ receivedAt: readEventTimestampValue(metadataEvent.lastMessageAt)
201
+ || readEventTimestampValue(metadataEvent.timestamp),
202
+ };
203
+ }
204
+ return undefined;
176
205
  }
177
206
 
178
207
  function readEventTimestampValue(value: unknown): number {
@@ -74,6 +74,14 @@ export interface MeshMissionSlimSummary extends Omit<MeshMissionSummary, 'goal'>
74
74
  /** Max chars of goal text retained in the slim (compact) mission summary. */
75
75
  export const GOAL_PREVIEW_MAX = 120;
76
76
 
77
+ /**
78
+ * Shorter goal preview used by the mesh_status compact (LLM coordinator) surface.
79
+ * The coordinator only needs to recognize a mission, not read its full goal, and
80
+ * mesh_status repeats every live mission on every poll — so the status preview is
81
+ * tighter than the dashboard / mesh_mission_list preview (GOAL_PREVIEW_MAX).
82
+ */
83
+ export const COMPACT_STATUS_GOAL_PREVIEW_MAX = 80;
84
+
77
85
  function normalizeMissionStatus(value: unknown): MeshMissionStatus {
78
86
  return MESH_MISSION_STATUSES.includes(value as MeshMissionStatus)
79
87
  ? value as MeshMissionStatus
@@ -153,13 +161,13 @@ export function getActiveMeshMissionSummaries(meshId: string): MeshMissionSummar
153
161
  }
154
162
 
155
163
  /** Project a full mission summary down to the slim (goal-elided) shape. */
156
- function slimMissionSummary(summary: MeshMissionSummary): MeshMissionSlimSummary {
164
+ function slimMissionSummary(summary: MeshMissionSummary, previewMax: number = GOAL_PREVIEW_MAX): MeshMissionSlimSummary {
157
165
  const goal = typeof summary.goal === 'string' ? summary.goal : '';
158
- const goalTruncated = goal.length > GOAL_PREVIEW_MAX;
166
+ const goalTruncated = goal.length > previewMax;
159
167
  const { goal: _omitGoal, ...rest } = summary;
160
168
  return {
161
169
  ...rest,
162
- goalPreview: goalTruncated ? goal.slice(0, GOAL_PREVIEW_MAX) : goal,
170
+ goalPreview: goalTruncated ? goal.slice(0, previewMax) : goal,
163
171
  goalTruncated,
164
172
  };
165
173
  }
@@ -197,7 +205,73 @@ export function getMeshStatusMissionSummaries(
197
205
  if (options?.withStats) {
198
206
  full = full.map(summary => ({ ...summary, stats: computeMeshMissionStats(meshId, summary.id) }));
199
207
  }
200
- return options?.verbose ? full : full.map(slimMissionSummary);
208
+ return options?.verbose ? full : full.map(summary => slimMissionSummary(summary));
209
+ }
210
+
211
+ /** Folded completed/abandoned history for the mesh_status compact surface. */
212
+ export interface MeshStatusMissionsHistoryFold {
213
+ /** Total completed + abandoned missions. */
214
+ count: number;
215
+ /** Count by lifecycle status (e.g. { completed, abandoned }). */
216
+ byStatus: Record<string, number>;
217
+ /** Newest-first id list (capped) so each folded mission stays addressable. */
218
+ missionIds: string[];
219
+ note: string;
220
+ }
221
+
222
+ /** Compact mesh_status mission projection: live detail + folded history. */
223
+ export interface MeshStatusMissionsCompact {
224
+ /**
225
+ * Active + paused missions, goal-elided to COMPACT_STATUS_GOAL_PREVIEW_MAX and
226
+ * WITHOUT the operational `stats` rollup — the `tasks` aggregate already carries
227
+ * progress, and stats (durations/retries) is a verbose/dashboard concern.
228
+ */
229
+ live: MeshMissionSlimSummary[];
230
+ /** Completed + abandoned missions folded to counts + ids; null when none. */
231
+ historyFold: MeshStatusMissionsHistoryFold | null;
232
+ }
233
+
234
+ /**
235
+ * Mission projection for the mesh_status COMPACT (LLM coordinator) surface.
236
+ *
237
+ * Unlike getMeshStatusMissionSummaries (which emits every live mission plus a
238
+ * capped slice of full-detail history, each carrying a stats rollup), this keeps
239
+ * per-mission detail ONLY for live (active/paused) missions and folds the whole
240
+ * completed/abandoned history into a counts + id-list summary. Combined with the
241
+ * tighter goal preview and dropped stats, this is what keeps the compact
242
+ * mesh_status payload bounded as a mesh accumulates missions — the missions
243
+ * section previously dominated the payload (full goalPreview + tasks + stats per
244
+ * mission, for every live mission and up to 10 history missions, on every poll).
245
+ *
246
+ * The stored missions are untouched; this is an output-only projection. Full
247
+ * mission detail (goal text + stats + history) stays available via
248
+ * mesh_status verbose=true or mesh_mission_list.
249
+ */
250
+ export function getMeshStatusMissionsCompact(
251
+ meshId: string,
252
+ options?: { previewMax?: number; historyIdLimit?: number },
253
+ ): MeshStatusMissionsCompact {
254
+ const previewMax = Math.max(0, options?.previewMax ?? COMPACT_STATUS_GOAL_PREVIEW_MAX);
255
+ const historyIdLimit = Math.max(0, options?.historyIdLimit ?? 20);
256
+ const all = getMeshMissions(meshId);
257
+ const live = all
258
+ .filter(m => m.status === 'active' || m.status === 'paused')
259
+ .map(mission => slimMissionSummary(summarizeMeshMission(meshId, mission), previewMax));
260
+ const history = all
261
+ .filter(m => m.status === 'completed' || m.status === 'abandoned')
262
+ .sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
263
+ let historyFold: MeshStatusMissionsHistoryFold | null = null;
264
+ if (history.length > 0) {
265
+ const byStatus: Record<string, number> = {};
266
+ for (const m of history) byStatus[m.status] = (byStatus[m.status] ?? 0) + 1;
267
+ historyFold = {
268
+ count: history.length,
269
+ byStatus,
270
+ missionIds: history.slice(0, historyIdLimit).map(m => m.id),
271
+ note: 'Completed/abandoned missions are folded to counts + ids in compact mesh_status. Use mesh_mission_list or mesh_status verbose=true for their goal/task detail.',
272
+ };
273
+ }
274
+ return { live, historyFold };
201
275
  }
202
276
 
203
277
  /**
@@ -219,7 +293,7 @@ export function listMeshMissionSummaries(
219
293
  const missions = getMeshMissions(meshId, statuses)
220
294
  .sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
221
295
  const full = missions.map(mission => summarizeMeshMission(meshId, mission));
222
- return options?.verbose ? full : full.map(slimMissionSummary);
296
+ return options?.verbose ? full : full.map(summary => slimMissionSummary(summary));
223
297
  }
224
298
 
225
299
  /**