@adhdev/daemon-core 0.9.82-rc.473 → 0.9.82-rc.475

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.
@@ -91,6 +91,18 @@ export declare const GOAL_PREVIEW_MAX = 120;
91
91
  * tighter than the dashboard / mesh_mission_list preview (GOAL_PREVIEW_MAX).
92
92
  */
93
93
  export declare const COMPACT_STATUS_GOAL_PREVIEW_MAX = 80;
94
+ /**
95
+ * mesh_mission_list default fold sizes. Without an explicit `statuses` filter the
96
+ * tool returns non-terminal (active/paused) missions in full detail but folds the
97
+ * completed/abandoned history into counts + a capped newest-first id list — a mesh
98
+ * can accumulate hundreds of terminal missions and returning them all (each with a
99
+ * ledger-scanned stats rollup) blew past the tool payload / token budget and spilled
100
+ * to a file. When an explicit `statuses` filter IS given, the matching missions are
101
+ * returned in detail but still bounded by MESH_MISSION_LIST_STATUS_LIMIT so a
102
+ * status:["completed"] call on a huge history stays bounded (overflow → truncated).
103
+ */
104
+ export declare const MESH_MISSION_LIST_HISTORY_ID_LIMIT = 30;
105
+ export declare const MESH_MISSION_LIST_STATUS_LIMIT = 50;
94
106
  export declare function upsertMeshMission(meshId: string, input: {
95
107
  id?: string;
96
108
  title: string;
@@ -218,6 +230,49 @@ export declare function listMeshMissionSummaries(meshId: string, options?: {
218
230
  verbose?: boolean;
219
231
  includeMagi?: boolean;
220
232
  }): MeshMissionSummary[] | MeshMissionSlimSummary[];
233
+ /** Shaped mesh_mission_list result: bounded detail list + optional folded history. */
234
+ export interface MeshMissionListResult {
235
+ /** Detailed (slim or verbose) mission summaries — bounded, newest-first. */
236
+ missions: MeshMissionSummary[] | MeshMissionSlimSummary[];
237
+ /**
238
+ * Completed/abandoned missions folded to counts + ids. Present (default path)
239
+ * when no explicit status filter is given and terminal missions exist. Null when
240
+ * an explicit status filter is active (those missions go into `missions`).
241
+ */
242
+ historyFold: MeshStatusMissionsHistoryFold | null;
243
+ /** True when an explicit status filter matched more than `limit` missions. */
244
+ truncated: boolean;
245
+ /** Total missions matching the query BEFORE the detail-list cap was applied. */
246
+ matched: number;
247
+ /** Newest-first ids of missions dropped by the detail-list cap (truncated path). */
248
+ overflowIds?: string[];
249
+ }
250
+ /**
251
+ * mesh_mission_list projection: payload-bounded regardless of mesh mission count.
252
+ *
253
+ * Default (no explicit `statuses`): non-terminal missions (active/paused) return in
254
+ * detail; completed/abandoned missions are folded to counts + a capped id list
255
+ * (historyFold), NOT emitted one-by-one. This is what keeps the tool from returning
256
+ * hundreds of terminal missions (each with a ledger stats rollup) and overflowing the
257
+ * token budget.
258
+ *
259
+ * Explicit `statuses` (e.g. ["completed"]): the coordinator asked to see those
260
+ * missions, so they ARE returned in detail — but still bounded by `limit` (default
261
+ * MESH_MISSION_LIST_STATUS_LIMIT). Overflow beyond `limit` is reported via
262
+ * truncated:true + overflowIds rather than silently dropped.
263
+ *
264
+ * MAGI + verbose semantics match listMeshMissionSummaries. `withStats` opts each
265
+ * detailed mission into the ledger-scanned stats rollup (off by default — the tasks
266
+ * aggregate is enough for a list view).
267
+ */
268
+ export declare function listMeshMissionsForTool(meshId: string, options?: {
269
+ statuses?: MeshMissionStatus[];
270
+ verbose?: boolean;
271
+ includeMagi?: boolean;
272
+ withStats?: boolean;
273
+ limit?: number;
274
+ historyIdLimit?: number;
275
+ }): MeshMissionListResult;
221
276
  /**
222
277
  * M3-3: render active missions as a prompt section for {{mission}}.
223
278
  * Empty string when no active mission — the prompt stays byte-identical to
@@ -287,9 +287,21 @@ export declare class CliProviderInstance implements ProviderInstance {
287
287
  private pruneRecentUserInputAcks;
288
288
  /**
289
289
  * Owner token for this session in the antigravity conversation-claim
290
- * registry. Derived identically to the dispatcher's read-side token
291
- * (workspace + spawn time) so the claims the dispatcher records under this
292
- * session are the ones dispose() releases.
290
+ * registry. Keyed on the daemon instance id — the SAME value the session
291
+ * registry stores as this session's `sessionId` (see cli-manager
292
+ * `sessionRegistry.register({ sessionId: cliInstance.instanceId })`) and the
293
+ * read side hands the dispatcher as `instanceId`. Both sides therefore
294
+ * derive the identical `iid:<instanceId>` token, so the claims the
295
+ * dispatcher records under this session are exactly the ones dispose()
296
+ * releases.
297
+ *
298
+ * This must NOT be derived from a spawn timestamp: the instance's
299
+ * `startedAt`, the adapter's `spawnedAtMs`, and the session registry's
300
+ * `spawnedAtMs` are three INDEPENDENT `Date.now()` samples for the one
301
+ * session, so a workspace+spawn-time token computed here would never equal
302
+ * the read side's — the claim isolation then silently collapses and two
303
+ * concurrent antigravity sessions cross-bind each other's conversation .db
304
+ * (coordinator+worker chat crosswire).
293
305
  */
294
306
  private antigravityClaimOwner;
295
307
  dispose(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.473",
3
+ "version": "0.9.82-rc.475",
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",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "0.9.82-rc.473",
51
- "@adhdev/session-host-core": "0.9.82-rc.473",
50
+ "@adhdev/mesh-shared": "0.9.82-rc.475",
51
+ "@adhdev/session-host-core": "0.9.82-rc.475",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -1095,6 +1095,13 @@ function readCliProviderNativeHistory(agentStr: string, args: {
1095
1095
  // the downstream hasSafeNativeHistoryMapping workspace-overlap gate. Only the
1096
1096
  // read_chat path opts in, and only with a concrete workspace.
1097
1097
  allowWorkspaceLatestFallback?: boolean;
1098
+ // ADHDev session id of the reading session (== the session registry's
1099
+ // sessionId == the provider instance's instanceId). Threaded to the
1100
+ // native-history dispatcher so antigravity's conversation-claim owner token
1101
+ // is keyed on this stable identity and matches the instance-side token —
1102
+ // without it two concurrent antigravity sessions cross-bind each other's
1103
+ // conversation .db.
1104
+ instanceId?: string;
1098
1105
  }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
1099
1106
  const canBindFromLiveSession = !args.historySessionId
1100
1107
  && typeof args.sessionStartedAtMs === 'number'
@@ -1146,6 +1153,7 @@ function readCliProviderNativeHistory(agentStr: string, args: {
1146
1153
  excludeInProgressTurn: args.excludeInProgressTurn,
1147
1154
  sessionStartedAtMs: args.sessionStartedAtMs,
1148
1155
  envOverrides: args.envOverrides,
1156
+ instanceId: args.instanceId,
1149
1157
  });
1150
1158
  const boundProviderSessionId = typeof (sessionHistory as any)?.providerSessionId === 'string'
1151
1159
  ? (sessionHistory as any).providerSessionId.trim()
@@ -1541,6 +1549,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
1541
1549
  scripts: provider?.scripts as any,
1542
1550
  sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
1543
1551
  envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1552
+ instanceId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : undefined,
1544
1553
  pinnedProviderSessionId: getBoundProviderSessionIdPin(args?.targetSessionId),
1545
1554
  })
1546
1555
  : readProviderChatHistory(agentStr, {
@@ -1772,6 +1781,9 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1772
1781
  excludeInProgressTurn: returnedStatus === 'waiting_approval',
1773
1782
  sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
1774
1783
  envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1784
+ // Stable per-session identity for antigravity's conversation-claim
1785
+ // owner token (== session registry sessionId == instance instanceId).
1786
+ instanceId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : undefined,
1775
1787
  pinnedProviderSessionId: getBoundProviderSessionIdPin(targetSessionId),
1776
1788
  // Last-resort only when no pin was ever recorded for this
1777
1789
  // session; the downstream workspace-overlap safety gate
@@ -1847,6 +1859,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1847
1859
  excludeInProgressTurn: returnedStatus === 'waiting_approval',
1848
1860
  sessionStartedAtMs,
1849
1861
  envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1862
+ instanceId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : undefined,
1850
1863
  });
1851
1864
  nativeHistoryError = undefined;
1852
1865
  nativeMessages = nativeHistory && Array.isArray(nativeHistory.messages)
@@ -2145,6 +2158,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2145
2158
  scripts: provider?.scripts as any,
2146
2159
  sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
2147
2160
  envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2161
+ instanceId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : undefined,
2148
2162
  pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
2149
2163
  // Last-resort only when no pin was ever recorded AND the
2150
2164
  // runtime fallback did not resolve a real provider session.
@@ -1813,10 +1813,12 @@ function callProviderNativeHistoryRead(
1813
1813
  sessionStartedAtMs?: number,
1814
1814
  envOverrides?: Record<string, string>,
1815
1815
  forceRefresh?: boolean,
1816
+ instanceId?: string,
1816
1817
  ): ProviderNativeHistoryReadResult | null {
1817
1818
  const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'readSession');
1818
1819
  if (!fn) return null;
1819
1820
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1821
+ const normalizedInstanceId = typeof instanceId === 'string' ? instanceId.trim() : '';
1820
1822
  const result = fn({
1821
1823
  agentType,
1822
1824
  sessionId: normalizedSessionId,
@@ -1831,6 +1833,12 @@ function callProviderNativeHistoryRead(
1831
1833
  // which leaves the guard disarmed so discovery still works.
1832
1834
  providerSessionId: normalizedSessionId,
1833
1835
  historySessionId: normalizedSessionId,
1836
+ // Stable per-session owner key for the antigravity conversation-claim
1837
+ // registry (see dispatcher.resolveAntigravityPath / antigravityOwnerToken).
1838
+ // Equals the session registry's sessionId and the provider instance's
1839
+ // instanceId, so read side and instance side derive the identical claim
1840
+ // owner token and two concurrent antigravity sessions never cross-bind.
1841
+ instanceId: normalizedInstanceId || undefined,
1834
1842
  workspace,
1835
1843
  format: canonicalHistory?.format,
1836
1844
  watchPath: canonicalHistory?.watchPath,
@@ -1838,7 +1846,7 @@ function callProviderNativeHistoryRead(
1838
1846
  sessionStartedAtMs,
1839
1847
  envOverrides,
1840
1848
  forceRefresh: forceRefresh === true,
1841
- args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, workspace, excludeInProgressTurn: excludeInProgressTurn === true, sessionStartedAtMs, envOverrides, forceRefresh: forceRefresh === true },
1849
+ args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, instanceId: normalizedInstanceId || undefined, workspace, excludeInProgressTurn: excludeInProgressTurn === true, sessionStartedAtMs, envOverrides, forceRefresh: forceRefresh === true },
1842
1850
  });
1843
1851
  if (!result || typeof result !== 'object') return null;
1844
1852
  const records = normalizeProviderNativeHistoryRecords(agentType, normalizedSessionId, (result as any).messages || (result as any).records);
@@ -1865,11 +1873,12 @@ function buildNativeHistoryReadResult(
1865
1873
  sessionStartedAtMs?: number,
1866
1874
  envOverrides?: Record<string, string>,
1867
1875
  forceRefresh?: boolean,
1876
+ instanceId?: string,
1868
1877
  ): ProviderNativeHistoryReadResult | null {
1869
1878
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1870
1879
  const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
1871
1880
  if (!canonicalHistory || (!normalizedSessionId && !normalizedWorkspace) || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
1872
- return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace, excludeInProgressTurn, sessionStartedAtMs, envOverrides, forceRefresh);
1881
+ return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace, excludeInProgressTurn, sessionStartedAtMs, envOverrides, forceRefresh, instanceId);
1873
1882
  }
1874
1883
 
1875
1884
  function materializeNativeHistoryToMirror(
@@ -1929,6 +1938,13 @@ export function readProviderChatHistory(
1929
1938
  sessionStartedAtMs?: number;
1930
1939
  envOverrides?: Record<string, string>;
1931
1940
  forceRefresh?: boolean;
1941
+ // Daemon instance id of the reading session (== the session registry's
1942
+ // sessionId). Threaded to the native-history dispatcher so the
1943
+ // antigravity conversation-claim owner token is keyed on this stable
1944
+ // per-session identity — identical to the token the provider instance
1945
+ // derives — instead of a spawn timestamp that differs across sample
1946
+ // sites and silently breaks claim isolation.
1947
+ instanceId?: string;
1932
1948
  } = {},
1933
1949
  ): {
1934
1950
  messages: HistoryMessage[];
@@ -1943,7 +1959,7 @@ export function readProviderChatHistory(
1943
1959
  unavailableReason?: string;
1944
1960
  } {
1945
1961
  if (isNativeSourceCanonicalHistory(options.canonicalHistory) && (options.historySessionId || options.workspace)) {
1946
- const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace, options.excludeInProgressTurn, options.sessionStartedAtMs, options.envOverrides, options.forceRefresh);
1962
+ const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace, options.excludeInProgressTurn, options.sessionStartedAtMs, options.envOverrides, options.forceRefresh, options.instanceId);
1947
1963
  if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
1948
1964
  return {
1949
1965
  ...pageHistoryRecords(agentType, nativeResult.records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
package/src/index.ts CHANGED
@@ -228,8 +228,8 @@ export type { CanonicalMeshToolName } from '@adhdev/mesh-shared';
228
228
 
229
229
  // ── Mesh Coordinator ──
230
230
  export { buildCoordinatorSystemPrompt } from './mesh/coordinator-prompt.js';
231
- 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';
232
- export type { MeshMissionRecord, MeshMissionStatus, MeshMissionSummary, MeshMissionSlimSummary, MeshMissionTaskAggregate, MeshStatusMissionsCompact, MeshStatusMissionsHistoryFold } from './mesh/mesh-missions.js';
231
+ export { upsertMeshMission, getMeshMissions, getMeshMission, summarizeMissionTasks, summarizeMeshMission, getActiveMeshMissionSummaries, getMeshStatusMissionSummaries, getMeshStatusMissionsCompact, listMeshMissionSummaries, listMeshMissionsForTool, buildMissionPromptSection, GOAL_PREVIEW_MAX, COMPACT_STATUS_GOAL_PREVIEW_MAX, MESH_MISSION_LIST_HISTORY_ID_LIMIT, MESH_MISSION_LIST_STATUS_LIMIT, MESH_MISSION_STATUSES } from './mesh/mesh-missions.js';
232
+ export type { MeshMissionRecord, MeshMissionStatus, MeshMissionSummary, MeshMissionSlimSummary, MeshMissionTaskAggregate, MeshStatusMissionsCompact, MeshStatusMissionsHistoryFold, MeshMissionListResult } from './mesh/mesh-missions.js';
233
233
  export { computeMeshTaskStats, computeMeshMissionStats } from './mesh/mesh-task-stats.js';
234
234
  export type { MeshTaskStats, MeshMissionStats } from './mesh/mesh-task-stats.js';
235
235
  export { deriveMeshReviewInboxItems } from './mesh/mesh-review-inbox.js';
@@ -303,6 +303,11 @@ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHost
303
303
  // ── Mesh Events ──
304
304
  export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, requeueHeldMeshCoordinatorEvents, serializeV2EnvelopeToWire, readV2EnvelopeFromWire, reconcileDirectDispatchCompletionFromTranscript } from './mesh/mesh-events.js';
305
305
  export type { PendingMeshCoordinatorEvent, MeshHeldEventRequeueFilter, MeshHeldEventRequeueResult } from './mesh/mesh-events.js';
306
+ // COORD-EVENT-MISROUTE: coordinator-identity helper so the mcp-server drain path can build a
307
+ // session-scoped drainer identity (identityDeliversTo sibling-session filter) with the same
308
+ // canonical builder daemon-core uses internally, instead of hand-rolling the identity shape.
309
+ export { coordinatorIdentityFromEmitFields } from './mesh/contracts.js';
310
+ export type { CoordinatorIdentity } from './mesh/contracts.js';
306
311
  // The coordinator-side preview surfaced from a worker's completion/status event
307
312
  // (finalSummary / workerResult.summary / lastMessagePreview). Same data the mobile
308
313
  // inbox is fed; reused by mesh_read_chat's cache fallback when the live P2P read path
@@ -327,7 +327,23 @@ export async function reconcileUnterminatedDirectDispatches(
327
327
  }
328
328
 
329
329
  const providerSessionId = readNonEmptyString(payload.providerSessionId);
330
- const coordinatorDaemonId = selfIds.find(id => !!id);
330
+ // COORD-EVENT-MISROUTE (anchor preservation): the coordinator DAEMON anchor for the
331
+ // synthesized completion is the daemon that DISPATCHED the task, NOT this reconcile
332
+ // runner's own daemon. On a REMOTE worker the reconcile loop runs on the WORKER's daemon,
333
+ // so `selfIds` is the worker daemon — stamping it as targetCoordinatorDaemonId corrupts the
334
+ // anchor and (when the real coordinator is on another machine) downgrades the completion to
335
+ // a cross-machine broadcast deliverable to any coordinator. Recover the true anchor:
336
+ // 1. the live worker session's meshCoordinatorDaemonId relay stamp (set at dispatch,
337
+ // the same anchor the worker's own real-emit path reads in mesh-event-forwarding); then
338
+ // 2. leave it to reconcileDirectDispatchCompletionFromTranscript, which recovers the
339
+ // DISPATCHING coordinator daemon from the task_dispatched ledger (authoritative).
340
+ // Only when NEITHER is available do we fall back to selfIds — a genuinely local,
341
+ // single-daemon dispatch where self IS the coordinator daemon (unchanged behaviour).
342
+ const workerSession = components.instanceManager.getInstance(sessionId);
343
+ const workerCoordinatorDaemonId = readNonEmptyString(
344
+ (workerSession?.getState()?.settings as Record<string, unknown> | undefined)?.meshCoordinatorDaemonId,
345
+ );
346
+ const coordinatorDaemonId = workerCoordinatorDaemonId || selfIds.find(id => !!id);
331
347
  try {
332
348
  const result = reconcileDirectDispatchCompletionFromTranscript({
333
349
  meshId: mesh.id,
@@ -338,6 +354,8 @@ export async function reconcileUnterminatedDirectDispatches(
338
354
  taskId,
339
355
  finalSummary: evidence.finalSummary,
340
356
  ...(evidence.transcriptMessageAt ? { transcriptMessageAt: evidence.transcriptMessageAt } : {}),
357
+ // The ledger-recovered dispatching-coordinator daemon (inside the reconcile fn)
358
+ // takes PRIORITY over this arg; this remains the best-available fallback.
341
359
  ...(coordinatorDaemonId ? { targetCoordinatorDaemonId: coordinatorDaemonId } : {}),
342
360
  source: 'daemon_reconcile_transcript_completion',
343
361
  });
@@ -345,6 +345,18 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
345
345
  // Absent on legacy rows → undefined → daemon-level routing (unchanged, no regression).
346
346
  const targetCoordinatorSessionId = readNonEmptyString(args.targetCoordinatorSessionId)
347
347
  || readNonEmptyString(dispatch?.payload?.coordinatorSessionId);
348
+ // COORD-EVENT-MISROUTE (anchor preservation): the DISPATCHING coordinator's daemon anchor.
349
+ // Prefer the DISPATCH LEDGER's `coordinatorDaemonId` (the daemon that ISSUED the task) over the
350
+ // caller-supplied arg — the synth caller (mesh-completion-synthesis) resolves the arg from its
351
+ // OWN selfIds, which on a remote worker's reconcile is the WORKER daemon, not the coordinator.
352
+ // Using the worker's self-daemon as the anchor makes coordinatorIdentityFromEmitFields treat
353
+ // the completion as addressed to the worker daemon; when the coordinator lives on a DIFFERENT
354
+ // machine that anchor never matches, selfFallback fires and stampPendingEventV2 broadcasts the
355
+ // completion to ANY coordinator (contracts.ts shouldDeliverPendingEventToCoordinator → true for
356
+ // broadcast), the cross-machine misroute. The ledger value is the authoritative issuing-daemon
357
+ // anchor. Absent on legacy rows → fall back to the arg → daemon-level routing (no regression).
358
+ const targetCoordinatorDaemonId = readNonEmptyString(dispatch?.payload?.coordinatorDaemonId)
359
+ || readNonEmptyString(args.targetCoordinatorDaemonId);
348
360
  queuePendingMeshCoordinatorEvent({
349
361
  event: eventName,
350
362
  meshId: args.meshId,
@@ -353,7 +365,7 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
353
365
  metadataEvent,
354
366
  coordinatorMessage: buildMeshSystemMessage({ event: eventName, nodeLabel, metadataEvent }),
355
367
  queuedAt: Date.now(),
356
- ...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
368
+ ...(targetCoordinatorDaemonId ? { targetCoordinatorDaemonId } : {}),
357
369
  ...(targetCoordinatorSessionId ? { targetCoordinatorSessionId } : {}),
358
370
  });
359
371
 
@@ -120,6 +120,19 @@ export const GOAL_PREVIEW_MAX = 120;
120
120
  */
121
121
  export const COMPACT_STATUS_GOAL_PREVIEW_MAX = 80;
122
122
 
123
+ /**
124
+ * mesh_mission_list default fold sizes. Without an explicit `statuses` filter the
125
+ * tool returns non-terminal (active/paused) missions in full detail but folds the
126
+ * completed/abandoned history into counts + a capped newest-first id list — a mesh
127
+ * can accumulate hundreds of terminal missions and returning them all (each with a
128
+ * ledger-scanned stats rollup) blew past the tool payload / token budget and spilled
129
+ * to a file. When an explicit `statuses` filter IS given, the matching missions are
130
+ * returned in detail but still bounded by MESH_MISSION_LIST_STATUS_LIMIT so a
131
+ * status:["completed"] call on a huge history stays bounded (overflow → truncated).
132
+ */
133
+ export const MESH_MISSION_LIST_HISTORY_ID_LIMIT = 30;
134
+ export const MESH_MISSION_LIST_STATUS_LIMIT = 50;
135
+
123
136
  function normalizeMissionStatus(value: unknown): MeshMissionStatus {
124
137
  return MESH_MISSION_STATUSES.includes(value as MeshMissionStatus)
125
138
  ? value as MeshMissionStatus
@@ -542,6 +555,120 @@ export function listMeshMissionSummaries(
542
555
  return options?.verbose ? full : full.map(summary => slimMissionSummary(summary));
543
556
  }
544
557
 
558
+ /** Shaped mesh_mission_list result: bounded detail list + optional folded history. */
559
+ export interface MeshMissionListResult {
560
+ /** Detailed (slim or verbose) mission summaries — bounded, newest-first. */
561
+ missions: MeshMissionSummary[] | MeshMissionSlimSummary[];
562
+ /**
563
+ * Completed/abandoned missions folded to counts + ids. Present (default path)
564
+ * when no explicit status filter is given and terminal missions exist. Null when
565
+ * an explicit status filter is active (those missions go into `missions`).
566
+ */
567
+ historyFold: MeshStatusMissionsHistoryFold | null;
568
+ /** True when an explicit status filter matched more than `limit` missions. */
569
+ truncated: boolean;
570
+ /** Total missions matching the query BEFORE the detail-list cap was applied. */
571
+ matched: number;
572
+ /** Newest-first ids of missions dropped by the detail-list cap (truncated path). */
573
+ overflowIds?: string[];
574
+ }
575
+
576
+ /**
577
+ * mesh_mission_list projection: payload-bounded regardless of mesh mission count.
578
+ *
579
+ * Default (no explicit `statuses`): non-terminal missions (active/paused) return in
580
+ * detail; completed/abandoned missions are folded to counts + a capped id list
581
+ * (historyFold), NOT emitted one-by-one. This is what keeps the tool from returning
582
+ * hundreds of terminal missions (each with a ledger stats rollup) and overflowing the
583
+ * token budget.
584
+ *
585
+ * Explicit `statuses` (e.g. ["completed"]): the coordinator asked to see those
586
+ * missions, so they ARE returned in detail — but still bounded by `limit` (default
587
+ * MESH_MISSION_LIST_STATUS_LIMIT). Overflow beyond `limit` is reported via
588
+ * truncated:true + overflowIds rather than silently dropped.
589
+ *
590
+ * MAGI + verbose semantics match listMeshMissionSummaries. `withStats` opts each
591
+ * detailed mission into the ledger-scanned stats rollup (off by default — the tasks
592
+ * aggregate is enough for a list view).
593
+ */
594
+ export function listMeshMissionsForTool(
595
+ meshId: string,
596
+ options?: {
597
+ statuses?: MeshMissionStatus[];
598
+ verbose?: boolean;
599
+ includeMagi?: boolean;
600
+ withStats?: boolean;
601
+ limit?: number;
602
+ historyIdLimit?: number;
603
+ },
604
+ ): MeshMissionListResult {
605
+ const explicitStatuses = options?.statuses && options.statuses.length > 0 ? options.statuses : undefined;
606
+ const includeMagi = options?.includeMagi === true;
607
+ const verbose = options?.verbose === true;
608
+ const withStats = options?.withStats === true;
609
+ const limit = Math.max(1, options?.limit ?? MESH_MISSION_LIST_STATUS_LIMIT);
610
+ const historyIdLimit = Math.max(0, options?.historyIdLimit ?? MESH_MISSION_LIST_HISTORY_ID_LIMIT);
611
+
612
+ const passesMagi = (m: MeshMissionRecord) => includeMagi || !(m.source === 'magi' && m.status === 'completed');
613
+ const byUpdatedDesc = (a: MeshMissionRecord, b: MeshMissionRecord) => (b.updatedAt || '').localeCompare(a.updatedAt || '');
614
+
615
+ const project = (mission: MeshMissionRecord): MeshMissionSummary | MeshMissionSlimSummary => {
616
+ let summary = summarizeMeshMission(meshId, mission);
617
+ if (withStats) {
618
+ try {
619
+ summary = { ...summary, stats: computeMeshMissionStats(meshId, mission.id) };
620
+ } catch { /* stats optional — omit on failure */ }
621
+ }
622
+ return verbose ? summary : slimMissionSummary(summary);
623
+ };
624
+
625
+ const foldHistory = (history: MeshMissionRecord[]): MeshStatusMissionsHistoryFold | null => {
626
+ if (history.length === 0) return null;
627
+ const byStatus: Record<string, number> = {};
628
+ for (const m of history) byStatus[m.status] = (byStatus[m.status] ?? 0) + 1;
629
+ return {
630
+ count: history.length,
631
+ byStatus,
632
+ missionIds: history.slice(0, historyIdLimit).map(m => m.id),
633
+ note: 'Completed/abandoned missions are folded to counts + ids. Pass status (e.g. status:["completed"]) to list them in detail.',
634
+ };
635
+ };
636
+
637
+ if (explicitStatuses) {
638
+ // Explicit filter: return matching missions in detail, capped at `limit`.
639
+ const matched = getMeshMissions(meshId, explicitStatuses)
640
+ .filter(passesMagi)
641
+ .sort(byUpdatedDesc);
642
+ const shown = matched.slice(0, limit);
643
+ const overflow = matched.slice(limit);
644
+ return {
645
+ missions: shown.map(project) as MeshMissionSummary[] | MeshMissionSlimSummary[],
646
+ historyFold: null,
647
+ truncated: overflow.length > 0,
648
+ matched: matched.length,
649
+ ...(overflow.length > 0 ? { overflowIds: overflow.map(m => m.id) } : {}),
650
+ };
651
+ }
652
+
653
+ // Default: detail for non-terminal missions, fold terminal history.
654
+ const all = getMeshMissions(meshId).filter(passesMagi);
655
+ const live = all
656
+ .filter(m => m.status === 'active' || m.status === 'paused')
657
+ .sort(byUpdatedDesc);
658
+ const history = all
659
+ .filter(m => m.status === 'completed' || m.status === 'abandoned')
660
+ .sort(byUpdatedDesc);
661
+ const shown = live.slice(0, limit);
662
+ const overflow = live.slice(limit);
663
+ return {
664
+ missions: shown.map(project) as MeshMissionSummary[] | MeshMissionSlimSummary[],
665
+ historyFold: foldHistory(history),
666
+ truncated: overflow.length > 0,
667
+ matched: live.length,
668
+ ...(overflow.length > 0 ? { overflowIds: overflow.map(m => m.id) } : {}),
669
+ };
670
+ }
671
+
545
672
  /**
546
673
  * M3-3: render active missions as a prompt section for {{mission}}.
547
674
  * Empty string when no active mission — the prompt stays byte-identical to
@@ -1089,12 +1089,24 @@ export class CliProviderInstance implements ProviderInstance {
1089
1089
 
1090
1090
  /**
1091
1091
  * Owner token for this session in the antigravity conversation-claim
1092
- * registry. Derived identically to the dispatcher's read-side token
1093
- * (workspace + spawn time) so the claims the dispatcher records under this
1094
- * session are the ones dispose() releases.
1092
+ * registry. Keyed on the daemon instance id — the SAME value the session
1093
+ * registry stores as this session's `sessionId` (see cli-manager
1094
+ * `sessionRegistry.register({ sessionId: cliInstance.instanceId })`) and the
1095
+ * read side hands the dispatcher as `instanceId`. Both sides therefore
1096
+ * derive the identical `iid:<instanceId>` token, so the claims the
1097
+ * dispatcher records under this session are exactly the ones dispose()
1098
+ * releases.
1099
+ *
1100
+ * This must NOT be derived from a spawn timestamp: the instance's
1101
+ * `startedAt`, the adapter's `spawnedAtMs`, and the session registry's
1102
+ * `spawnedAtMs` are three INDEPENDENT `Date.now()` samples for the one
1103
+ * session, so a workspace+spawn-time token computed here would never equal
1104
+ * the read side's — the claim isolation then silently collapses and two
1105
+ * concurrent antigravity sessions cross-bind each other's conversation .db
1106
+ * (coordinator+worker chat crosswire).
1095
1107
  */
1096
1108
  private antigravityClaimOwner(): string {
1097
- return antigravityOwnerToken(this.workingDir, this.startedAt);
1109
+ return antigravityOwnerToken(this.workingDir, this.startedAt, this.instanceId);
1098
1110
  }
1099
1111
 
1100
1112
  dispose(): void {