@adhdev/daemon-core 0.9.82-rc.442 → 0.9.82-rc.444

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.
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { EventEmitter } from 'events';
16
16
  import { MeshRuntimeStore } from './mesh-runtime-store.js';
17
- export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'task_reclaimed' | 'coordinator_operating_note' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis';
17
+ export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis';
18
18
  export interface MeshLedgerEntry {
19
19
  id: string;
20
20
  meshId: string;
@@ -151,6 +151,10 @@ export interface AppendRemoteLedgerResult {
151
151
  entries: MeshLedgerEntry[];
152
152
  }
153
153
  export declare const MAX_LEDGER_SLICE_LIMIT = 500;
154
+ export declare const OPERATING_NOTE_KIND: MeshLedgerKind;
155
+ export declare const OPERATING_NOTE_TOMBSTONE_KIND: MeshLedgerKind;
156
+ export declare const OPERATING_NOTE_DEDUPE_WINDOW = 40;
157
+ export declare const OPERATING_NOTE_KEEP_LATEST = 100;
154
158
  export declare function getLedgerDir(): string;
155
159
  /**
156
160
  * Footer to append to worker task messages so workers output structured results
@@ -180,6 +184,40 @@ export declare function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvi
180
184
  */
181
185
  export declare const meshLedgerEvents: EventEmitter<[never]>;
182
186
  export declare function appendLedgerEntry(meshId: string, partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>): MeshLedgerEntry;
187
+ /** True if the operating note is retracted by any tombstone in `tombstones`. */
188
+ export declare function isOperatingNoteTombstoned(entry: Pick<MeshLedgerEntry, 'id' | 'payload'>, tombstones: {
189
+ ids: Set<string>;
190
+ fingerprints: Set<string>;
191
+ }): boolean;
192
+ /**
193
+ * Fix (2) supersede/remove: append a tombstone that retracts a coordinator
194
+ * operating note. Targets by note id and/or by exact trimmed text (a text target
195
+ * retracts every note with that text). History is preserved — the notes stay in
196
+ * the ledger but readers filter them out. Returns how many currently-live notes
197
+ * the tombstone will hide.
198
+ */
199
+ export declare function tombstoneOperatingNote(meshId: string, target: {
200
+ noteId?: string;
201
+ text?: string;
202
+ reason?: string;
203
+ }): {
204
+ tombstone: MeshLedgerEntry;
205
+ matched: number;
206
+ };
207
+ /**
208
+ * Read live operating notes (tombstoned notes filtered out), oldest→newest.
209
+ * `tail` bounds the number of live notes returned (the freshest N).
210
+ */
211
+ export declare function readOperatingNotes(meshId: string, opts?: {
212
+ tail?: number;
213
+ }): MeshLedgerEntry[];
214
+ /**
215
+ * Fix (3) keep-latest-N prune for coordinator_operating_note. Removes, from the
216
+ * store, (a) every note retracted by a tombstone, and (b) the oldest live notes
217
+ * beyond `keepLatest`. Tombstone entries themselves are retained as an audit trail
218
+ * of what was forgotten. Returns the number of note entries removed.
219
+ */
220
+ export declare function pruneOperatingNotes(meshId: string, keepLatest?: number): number;
183
221
  /**
184
222
  * Append entries received over local-first/P2P ledger replication to the local ledger.
185
223
  * This skips deduplicated entries and rejects malformed/cross-mesh entries.
@@ -199,6 +199,21 @@ export declare class MeshGitProbeCache {
199
199
  private recent;
200
200
  constructor(reuseMs: number, now?: () => number);
201
201
  private key;
202
+ /**
203
+ * Local (same-machine) git_status dedup. The bootstrap direct-truth hydrate
204
+ * and the per-node render loop both call getGitRepoStatus(refreshUpstream:true)
205
+ * for the same local workspace within one mesh_status call. Each such probe
206
+ * fans out ~13-15 git subprocesses, and because the two passes are separated by
207
+ * the render/hydrate work of every OTHER node they routinely straddle the
208
+ * getGitRepoStatus 1.5s TTL, so the second pass re-shells the whole ~14-process
209
+ * collection. Routing both through this cache (namespaced under a reserved
210
+ * daemon id so it never collides with a remote-peer key) collapses them to one
211
+ * collection per workspace per request, and reuses it across the reuse window
212
+ * so the dashboard auto-retry loop can't restart a fresh local probe seconds
213
+ * apart either.
214
+ */
215
+ private static readonly LOCAL_PROBE_DAEMON_ID;
216
+ probeLocal(workspace: string, probe: () => Promise<Record<string, unknown> | null>): Promise<Record<string, unknown> | null>;
202
217
  /**
203
218
  * Run `probe` for this peer, but reuse a fresh recent result or an in-flight
204
219
  * probe for the same key when one is available. `probe` is only invoked when
@@ -52,10 +52,24 @@ export declare function resolveCoordinatorDrainDeliverability(components: Pick<D
52
52
  * remote pull is never blocked by our local coordinator's busy state. A pure
53
53
  * stdio MCP coordinator (no live CLI session) never satisfies (1), so its tool
54
54
  * result remains the surface and the drain proceeds. No regression to either.
55
+ *
56
+ * SELF-COORDINATOR INBOX LEVEL-DRAIN (Defect 2): the hold above assumes the ONLY
57
+ * surface for a busy local coordinator's events is a future PTY inject on its idle
58
+ * edge, so it defers to the reconcile loop. But when the drain caller IS the local
59
+ * coordinator reading its OWN inbox (the `get_pending_mesh_events` call whose events
60
+ * are returned in the caller's tool RESULT — a data queue the self-coordinating LLM
61
+ * consumes directly), the events ARE surfaced losslessly the moment the tool returns,
62
+ * with NO PTY write. A busy self-coordinating LLM that calls a mesh tool mid-turn would
63
+ * otherwise get an empty inbox (held) and only see the completion on its NEXT busy→idle
64
+ * edge — the measured ~59s strand. `callerIsSelfCoordinatorInboxRead` marks that safe
65
+ * caller: the hold is relaxed for it (return the events), while every OTHER drain (a
66
+ * backfill relay, a broadcast poll, a DIFFERENT coordinator that genuinely needs its PTY)
67
+ * still defers to the reconcile loop. This relaxes delivery INTO the coordinator's own
68
+ * inbox only — it never changes how events are injected into a live PTY prompt.
55
69
  */
56
70
  export declare function shouldHoldPendingDrainForBusyLocalCoordinator(components: Pick<DaemonComponents, 'instanceManager'> & {
57
71
  statusInstanceId?: string;
58
- }, meshId: string, requestedCoordinatorDaemonId?: string | null): boolean;
72
+ }, meshId: string, requestedCoordinatorDaemonId?: string | null, callerIsSelfCoordinatorInboxRead?: boolean): boolean;
59
73
  export declare function runMeshReconcileTick(components: DaemonComponents): Promise<void>;
60
74
  export declare function __resetUnresolvedForwardRejectionCountsForTests(): void;
61
75
  interface ReconcileLoopHandle {
@@ -1,6 +1,7 @@
1
1
  import type { ChatMessage } from '../types.js';
2
2
  export declare const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 16000;
3
3
  export declare function extractFinalSummaryFromMessages(messages: ChatMessage[] | null | undefined, maxChars?: number): string;
4
+ export declare function readChatMessageTimestampMs(message: ChatMessage | null | undefined): number | undefined;
4
5
  /**
5
6
  * Turn-scoped variant of extractFinalSummaryFromMessages. Selects the last
6
7
  * user-facing assistant/model bubble whose own timestamp is at/after the
@@ -99,6 +99,7 @@ export declare class CliProviderInstance implements ProviderInstance {
99
99
  private lastStatus;
100
100
  private agentReadyEmitted;
101
101
  private generatingStartedAt;
102
+ private busyEpoch;
102
103
  private fastCollapseSynthesizedTaskId;
103
104
  private startupGraceCollapseAt;
104
105
  private settings;
@@ -285,6 +286,7 @@ export declare class CliProviderInstance implements ProviderInstance {
285
286
  private approvalResolutionFinalizationBlock;
286
287
  private scheduleCompletedDebounceFlush;
287
288
  private isMeshWorkerSession;
289
+ private isAutonomousMeshSession;
288
290
  /**
289
291
  * ARCH-REFACTOR R1: the taskId to attribute the CURRENTLY-completing turn to.
290
292
  * Prefers the per-turn binding (engine.currentTurnTaskId, set when the turn was
@@ -12,11 +12,35 @@
12
12
  * { source: 'USER_EXPLICIT'|'MODEL', type: string, content: string, status: 'DONE'|..., created_at: number }
13
13
  *
14
14
  * 3. ~/.gemini/antigravity-cli/conversations/<uuid>.pb
15
- * Protobuf binary — schema not publicly documented. Adapter extracts
15
+ * Legacy protobuf binary — schema not publicly documented. Adapter extracts
16
16
  * printable UTF-8 text runs as best-effort content (no proto library needed).
17
17
  *
18
+ * 4. ~/.gemini/antigravity-cli/conversations/<uuid>.db ← current format
19
+ * Per-session SQLite database. Recent antigravity migrated conversation
20
+ * storage from .pb (+ brain/*.jsonl) to a per-session SQLite db. The
21
+ * schema is a trajectory of `steps`, NOT a simple messages(role,content)
22
+ * table:
23
+ * steps(idx INTEGER PK, step_type INTEGER, status INTEGER,
24
+ * step_payload BLOB [protobuf], ...)
25
+ * Each `step_payload` is a protobuf message. Empirically (introspected
26
+ * from real stores):
27
+ * - step_type 14 → a USER turn. The prompt text is the largest
28
+ * contiguous UTF-8 run inside the payload (field 19 subtree).
29
+ * - step_type 15 → a MODEL/assistant turn. The assistant's final
30
+ * natural-language answer lives at payload field 20 → field 1
31
+ * (identical to field 8). Field 20 → field 3 is the internal
32
+ * reasoning summary and is intentionally NOT surfaced.
33
+ * - other step types are tool calls / ephemeral system context.
34
+ * We read the blobs with a tiny dependency-free protobuf field walker
35
+ * (no proto schema / codegen needed) and map the two message step types.
36
+ * Because the daemon does NOT read this db, native history previously
37
+ * returned 0 rows for these sessions and read_chat fell back to the
38
+ * pty parser (which only echoes the user's own input) — assistant
39
+ * answers appeared lost even though they were on disk.
40
+ *
18
41
  * This adapter provides:
19
- * - Full coverage when a brain transcript exists (authoritative source).
42
+ * - Full coverage from a per-session .db (current format) — preferred.
43
+ * - Full coverage when a brain transcript exists (legacy authoritative source).
20
44
  * - Partial coverage (user prompts only) from history.jsonl as fallback.
21
45
  * - Best-effort raw-string extraction from .pb files when no other source exists.
22
46
  *
@@ -24,6 +48,7 @@
24
48
  * ~/.gemini/antigravity-cli/history.jsonl
25
49
  * ~/.gemini/antigravity-cli/brain/{uuid}/.system_generated/logs/transcript*.jsonl
26
50
  * ~/.gemini/antigravity-cli/conversations/{uuid}.pb
51
+ * ~/.gemini/antigravity-cli/conversations/{uuid}.db
27
52
  *
28
53
  * OSS code (AGPL-3.0). Must not import from packages/ (proprietary).
29
54
  */
@@ -79,6 +104,7 @@ export interface NativeHistorySessionMeta {
79
104
  * `sessionPath` is the absolute path to one of:
80
105
  * - A brain transcript JSONL: ~/.gemini/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript*.jsonl
81
106
  * - The shared history.jsonl: ~/.gemini/antigravity-cli/history.jsonl
107
+ * - A conversation SQLite db: ~/.gemini/antigravity-cli/conversations/<uuid>.db
82
108
  * - A conversation protobuf: ~/.gemini/antigravity-cli/conversations/<uuid>.pb
83
109
  *
84
110
  * The session UUID is inferred from the directory name (brain), filename (pb), or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.442",
3
+ "version": "0.9.82-rc.444",
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.442",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.444",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -2210,6 +2210,47 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2210
2210
  });
2211
2211
 
2212
2212
  if (supportsNative && !decision.nativeSelected) {
2213
+ // Native-only content preservation (hermes chat_tail gap).
2214
+ // The history-only path has NO PTY transcript (native-only
2215
+ // providers suppress PTY bodies), so args.ptyMessages is empty
2216
+ // and the machine's pty-parser selection returns NOTHING. But a
2217
+ // post-turn / cold read routinely lands here with a REAL,
2218
+ // safely-mapped native slice that the source FSM declined only
2219
+ // because coverage came back 'partial' (missing sessionStartedAtMs
2220
+ // → Booting→Recovering→pty-parser) or a transient shrink looked
2221
+ // like a regression. Dropping those rows deletes the assistant
2222
+ // answer from chat_tail / read_chat entirely. When the native
2223
+ // read actually resolved rows for THIS session identity
2224
+ // (safeMapping proves ownership: matching historySessionId /
2225
+ // providerSessionId + workspace), return them instead of an empty
2226
+ // array. This never loosens identity safety — it is gated on the
2227
+ // same hasSafeNativeHistoryMapping used everywhere else — and it
2228
+ // is scoped to the native-only history path (no PTY to prefer).
2229
+ // Truly-empty native reads (historyMessages.length === 0) and
2230
+ // unsafe/workspace-aliasing reads (safeMapping === false) still
2231
+ // fall through to the soft-pending dead-end below.
2232
+ if (safeMapping && historyMessages.length > 0) {
2233
+ LOG.debug('Command', `[read_chat] native-only content preserved despite pty-parser selection target=${String(args?.targetSessionId || '')} provider=${agentStr} rows=${historyMessages.length} cause=${decision.decision.transition.cause}`);
2234
+ return buildReadChatCommandResult({
2235
+ messages: historyMessages,
2236
+ status: 'idle',
2237
+ messageSource: {
2238
+ ...decision.messageSource,
2239
+ nativeOnlyContentPreserved: true,
2240
+ returnedMessageCount: historyMessages.length,
2241
+ },
2242
+ transcriptProvenance: {
2243
+ ...decision.messageSource,
2244
+ nativeOnlyContentPreserved: true,
2245
+ },
2246
+ ...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
2247
+ ...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
2248
+ ...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')
2249
+ ? { transcriptAuthority: (provider?.historyBehavior as any).transcriptAuthority }
2250
+ : {}),
2251
+ coverage: 'tail',
2252
+ }, args, h);
2253
+ }
2213
2254
  // Dead-end: we are in the history-only path (no live PTY/ACP
2214
2255
  // adapter was found for this target session) AND provider-native
2215
2256
  // history is not safely mappable to the requested session
@@ -101,8 +101,10 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
101
101
  // Best-effort: a read failure just omits the section.
102
102
  const buildOperatingNotesBestEffort = async (id: string) => {
103
103
  try {
104
- const { readLedgerEntries } = await import('../../mesh/mesh-ledger.js');
105
- const noteEntries = readLedgerEntries(id, { kind: ['coordinator_operating_note'], tail: 20 });
104
+ const { readOperatingNotes } = await import('../../mesh/mesh-ledger.js');
105
+ // readOperatingNotes filters out tombstoned (forgotten) notes so a
106
+ // retracted lesson never rides into the prompt. Newest last; tail 20.
107
+ const noteEntries = readOperatingNotes(id, { tail: 20 });
106
108
  const notes = noteEntries
107
109
  .map((e) => {
108
110
  const p = (e.payload || {}) as Record<string, unknown>;
@@ -29,6 +29,11 @@ export const meshEventsHandlers: Record<string, HighFamilyHandler> = {
29
29
  const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
30
30
  ? args.coordinatorDaemonId.trim()
31
31
  : undefined;
32
+ // SELF-COORDINATOR INBOX LEVEL-DRAIN (Defect 2): the MCP self-coordinator inbox read
33
+ // (drainCoordinatorPendingEvents) sets this so its own drain is not held while its CLI
34
+ // is busy — the events return in ITS tool result (a lossless data-queue surface), never
35
+ // a PTY inject. Every other drain leaves it unset and keeps the busy-coordinator hold.
36
+ const selfCoordinatorInboxRead = args?.selfCoordinatorInboxRead === true;
32
37
  // DRAIN-WITHOUT-INJECT guard: when a LOCAL live CLI coordinator for this mesh is
33
38
  // busy (generating / modal-parked), the reconcile loop is HOLDING its terminal
34
39
  // events (drained=0) for the coordinator's next idle tick. Draining here would
@@ -57,11 +62,15 @@ export const meshEventsHandlers: Record<string, HighFamilyHandler> = {
57
62
  // loop: return nothing, leaving the rows undrained for its idle-tick delivery. A
58
63
  // remote pull (foreign coordinatorDaemonId) or a pure stdio MCP coordinator (no live
59
64
  // CLI session) is NOT held — see shouldHoldPendingDrainForBusyLocalCoordinator.
60
- if (meshId && shouldHoldPendingDrainForBusyLocalCoordinator(ctx.deps, meshId, coordinatorDaemonId)) {
65
+ if (meshId && shouldHoldPendingDrainForBusyLocalCoordinator(ctx.deps, meshId, coordinatorDaemonId, selfCoordinatorInboxRead)) {
61
66
  return { success: true, events: [], heldForBusyLocalCoordinator: true, hasLiveCliCoordinator };
62
67
  }
63
68
  const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
64
- return { success: true, events, hasLiveCliCoordinator };
69
+ // SELF-COORDINATOR INBOX LEVEL-DRAIN: when the busy local coordinator drained its OWN
70
+ // inbox (selfCoordinatorInboxRead), tell the puller these events were surfaced through
71
+ // the caller's tool result — it must NOT re-forward them into the (busy) PTY (that is the
72
+ // lossy path). Absent the flag, delivery is unchanged (reconcile-owned PTY / remote pull).
73
+ return { success: true, events, hasLiveCliCoordinator, ...(selfCoordinatorInboxRead ? { surfacedForSelfCoordinator: true } : {}) };
65
74
  },
66
75
 
67
76
  interactive_prompt_response: async (ctx: HighFamilyContext, args: any) => {
@@ -61,6 +61,11 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
61
61
  mesh_status: async (ctx: HighFamilyContext, args: any) => {
62
62
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
63
63
  if (!meshId) return { success: false, error: 'meshId required' };
64
+ // Latency telemetry: stamp handler entry so every return path can
65
+ // report durationMs. Makes the detail-open cost measurable in the
66
+ // repo-mesh-status debug log (cache hit ≈ single-digit ms; a full
67
+ // live rebuild is the ~seconds path this change is reducing).
68
+ const startedAtMs = Date.now();
64
69
  try {
65
70
  const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
66
71
  const mesh = meshRecord?.mesh;
@@ -94,10 +99,51 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
94
99
  meshId,
95
100
  command: 'mesh_status',
96
101
  refreshRequested,
102
+ durationMs: Date.now() - startedAtMs,
97
103
  summary: summarizeRepoMeshStatusDebug(cachedStatus),
98
104
  });
99
105
  return cachedStatus;
100
106
  }
107
+ // SWR stale-serve: the strict serve above missed only because
108
+ // some node still has a pending peer-git probe (the
109
+ // shouldRefreshStalePendingAggregate gate). Rather than block
110
+ // this interactive detail-open on a full synchronous live
111
+ // rebuild, serve the held (slightly stale) snapshot instantly
112
+ // and kick ONE coalesced background freshen whose result
113
+ // repopulates the cache for the next poll/subscription push.
114
+ // allowStalePending relaxes ONLY the pending-git freshness gate;
115
+ // getCachedAggregateMeshStatus still enforces the queueRevision
116
+ // guard, so a genuine queue/identity mutation is never stale-served.
117
+ const staleStatus = ctx.getCachedAggregateMeshStatus(meshId, mesh, {
118
+ requireDirectPeerTruth: args?.requireDirectPeerTruth === true,
119
+ allowStalePending: true,
120
+ });
121
+ if (staleStatus) {
122
+ if (!ctx.swrRefreshInFlight.has(meshId)) {
123
+ ctx.swrRefreshInFlight.add(meshId);
124
+ // Fire-and-forget: a full refresh (peer fan-out) that
125
+ // rewrites the aggregate cache. Errors are swallowed —
126
+ // the stale snapshot was already returned to the caller.
127
+ void Promise.resolve()
128
+ .then(() => ctx.execute('mesh_status', {
129
+ meshId,
130
+ inlineMesh: args?.inlineMesh,
131
+ coordinatorDaemonId: args?.coordinatorDaemonId,
132
+ requireDirectPeerTruth: args?.requireDirectPeerTruth === true,
133
+ refresh: true,
134
+ }, 'mesh_status_swr_freshen'))
135
+ .catch(() => {})
136
+ .finally(() => { ctx.swrRefreshInFlight.delete(meshId); });
137
+ }
138
+ logRepoMeshStatusDebug('return_stale_swr', {
139
+ meshId,
140
+ command: 'mesh_status',
141
+ refreshRequested,
142
+ durationMs: Date.now() - startedAtMs,
143
+ summary: summarizeRepoMeshStatusDebug(staleStatus),
144
+ });
145
+ return staleStatus;
146
+ }
101
147
  }
102
148
  const refreshReason = refreshRequested
103
149
  ? 'explicit_refresh'
@@ -229,8 +275,15 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
229
275
  ? selectedCoordinatorNodeId
230
276
  : undefined;
231
277
  const refreshedAt = new Date().toISOString();
232
- const nodeStatuses = [];
233
- for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
278
+ // Per-node render is parallelized (Promise.allSettled below):
279
+ // each node's git probe (local getGitRepoStatus or the remote
280
+ // P2P fan-out) is independent, so serializing them stacked one
281
+ // slow node's latency onto every other node. Each node has its
282
+ // own bounded timeout; a rejected/timed-out node degrades to a
283
+ // partial/last-known status for THAT node only and never rejects
284
+ // the whole aggregate. Order is preserved by mapping over the
285
+ // original entries() index and re-assembling in order.
286
+ const renderMeshNode = async (nodeIndex: number, node: any): Promise<Record<string, unknown>> => {
234
287
  const nodeId = normalizeMeshNodeId(node) ?? '';
235
288
  const daemonId = readStringValue(node.daemonId);
236
289
  const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
@@ -425,19 +478,23 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
425
478
  )) {
426
479
  applyInlineMeshBranchConvergence(mesh, node, status);
427
480
  finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode, directTruthUnavailable: directTruthUnavailableNodeIds.has(nodeId) });
428
- nodeStatuses.push(status);
429
- continue;
481
+ return status;
430
482
  }
431
483
  if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
432
484
  applyInlineMeshBranchConvergence(mesh, node, status);
433
485
  finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode, directTruthUnavailable: directTruthUnavailableNodeIds.has(nodeId) });
434
- nodeStatuses.push(status);
435
- continue;
486
+ return status;
436
487
  }
437
488
  }
438
489
  } else {
439
490
  try {
440
- const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
491
+ // Route through the shared per-request cache so this
492
+ // local probe reuses the bootstrap hydrate's probe for
493
+ // the same workspace (and vice versa) instead of
494
+ // re-shelling ~14 git processes across the 1.5s TTL.
495
+ const runLocalProbe = () => getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true }) as unknown as Promise<Record<string, unknown> | null>;
496
+ const gitStatus = (await meshGitProbeCache.probeLocal(workspace, runLocalProbe)) as any;
497
+ if (!gitStatus) throw new Error('local_git_probe_unavailable');
441
498
  status.git = gitStatus;
442
499
  status[MESH_NODE_LIVE_TRUTH_MARKER] = true;
443
500
  const reporter = recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
@@ -459,8 +516,43 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
459
516
  }
460
517
  applyInlineMeshBranchConvergence(mesh, node, status);
461
518
  finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode, directTruthUnavailable: directTruthUnavailableNodeIds.has(nodeId) });
462
- nodeStatuses.push(status);
463
- }
519
+ return status;
520
+ };
521
+ const meshNodeEntries = [...(mesh.nodes || []).entries()];
522
+ const settledNodeStatuses = await Promise.allSettled(
523
+ meshNodeEntries.map(([nodeIndex, node]) => renderMeshNode(nodeIndex, node)),
524
+ );
525
+ const nodeStatuses = settledNodeStatuses.map((settled, i) => {
526
+ if (settled.status === 'fulfilled') return settled.value;
527
+ // A per-node render should never reject (every git path is
528
+ // caught internally), but if one does, degrade to a minimal
529
+ // last-known/unknown entry for THAT node so a single failure
530
+ // can't drop the whole aggregate. Best-effort recovery of the
531
+ // node's cached inline truth, else an unknown-health stub.
532
+ const [nodeIndex, node] = meshNodeEntries[i];
533
+ const nodeId = normalizeMeshNodeId(node) ?? '';
534
+ const daemonId = readStringValue(node.daemonId);
535
+ const fallback: Record<string, unknown> = {
536
+ nodeId,
537
+ machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, readProviderPriorityFromPolicy(node.policy)),
538
+ workspace: node.workspace,
539
+ repoRoot: node.repoRoot,
540
+ isLocalWorktree: node.isLocalWorktree,
541
+ worktreeBranch: node.worktreeBranch,
542
+ daemonId,
543
+ machineId: readMeshNodeMachineId(node as Record<string, unknown>) || node.machineId,
544
+ health: 'unknown',
545
+ providers: node.providers || [],
546
+ activeSessions: [],
547
+ activeSessionDetails: [],
548
+ launchReady: false,
549
+ error: settled.reason instanceof Error ? settled.reason.message : 'node render failed',
550
+ };
551
+ applyCachedInlineMeshNodeStatus(fallback, node);
552
+ applyInlineMeshBranchConvergence(mesh, node, fallback);
553
+ finalizeMeshNodeStatus({ status: fallback, node, daemonId, isSelfNode: false, directTruthUnavailable: directTruthUnavailableNodeIds.has(nodeId) });
554
+ return fallback;
555
+ });
464
556
 
465
557
  // (B3) Resolve the coordinator daemon scope for the peek.
466
558
  // mesh_status is a read-only status query — it must not consume
@@ -620,6 +712,7 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
620
712
  refreshReason,
621
713
  meshSource: meshRecord.source,
622
714
  directTruth,
715
+ durationMs: Date.now() - startedAtMs,
623
716
  summary: summarizeRepoMeshStatusDebug(returnedStatus),
624
717
  });
625
718
  return returnedStatus;
@@ -45,7 +45,7 @@ export interface HighFamilyContext {
45
45
  getCachedAggregateMeshStatus: (
46
46
  meshId: string,
47
47
  mesh?: any,
48
- options?: { requireDirectPeerTruth?: boolean },
48
+ options?: { requireDirectPeerTruth?: boolean; allowStalePending?: boolean },
49
49
  ) => any | null;
50
50
 
51
51
  /** Bound `DaemonCommandRouter.rememberAggregateMeshStatus`. */
@@ -61,6 +61,10 @@ export interface HighFamilyContext {
61
61
  /** Router's aggregate-status memory cache (`.has()` probe in mesh_status). */
62
62
  aggregateMeshStatusCache: Map<string, { builtAt: number; snapshot: any; queueRevision: string }>;
63
63
 
64
+ /** Meshes with a background SWR freshen already in flight — coalesces the
65
+ * async refresh a stale-serve interactive open kicks off. */
66
+ swrRefreshInFlight: Set<string>;
67
+
64
68
  /** Router's running-refine-job table (surfaced as activeRefineJobs in mesh_status). */
65
69
  runningRefineJobs: Map<string, MeshRefineJobHandle>;
66
70
 
@@ -286,6 +286,10 @@ export class DaemonCommandRouter {
286
286
  * Spans separate mesh_status/get_mesh calls so the dashboard auto-retry
287
287
  * loop cannot storm a slow peer with back-to-back refreshUpstream probes. */
288
288
  private meshGitProbeCache = new MeshGitProbeCache(MESH_DIRECT_PROBE_REUSE_MS);
289
+ /** Meshes with a background SWR freshen (async mesh_status refresh) already in
290
+ * flight — so a burst of interactive detail-opens serves the cached snapshot
291
+ * and coalesces onto ONE background refresh instead of storming the peers. */
292
+ private swrRefreshInFlight = new Set<string>();
289
293
  /** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
290
294
  private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
291
295
  /** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
@@ -403,13 +407,25 @@ export class DaemonCommandRouter {
403
407
  };
404
408
  }
405
409
 
406
- private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
410
+ private getCachedAggregateMeshStatus(
411
+ meshId: string,
412
+ mesh?: any,
413
+ options?: { requireDirectPeerTruth?: boolean; allowStalePending?: boolean },
414
+ ): any | null {
407
415
  const cached = this.aggregateMeshStatusCache.get(meshId);
408
416
  if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
417
+ // Genuine invalidation still forces truth: a queue mutation bumps the
418
+ // revision, so a stale-revision snapshot is never served (even under the
419
+ // SWR allowStalePending path below).
409
420
  if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
410
421
  let snapshot = this.cloneJsonValue(cached.snapshot);
411
422
  snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
412
- if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
423
+ // SWR: allowStalePending lets the interactive detail-open serve a snapshot
424
+ // that still has pending peer-git nodes (would otherwise miss here) so the
425
+ // graph paints instantly; the caller fires a background freshen. The
426
+ // queueRevision guard above is NOT relaxed — only the pending-git freshness
427
+ // gate is, so a genuine queue/identity mutation still forces a live rebuild.
428
+ if (!options?.allowStalePending && shouldRefreshStalePendingAggregate(snapshot, options)) return null;
413
429
  const ageMs = Math.max(0, Date.now() - cached.builtAt);
414
430
  const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
415
431
  ? snapshot.sourceOfTruth
@@ -677,6 +693,7 @@ export class DaemonCommandRouter {
677
693
  rememberAggregateMeshStatus: this.rememberAggregateMeshStatus.bind(this),
678
694
  execute: this.execute.bind(this),
679
695
  aggregateMeshStatusCache: this.aggregateMeshStatusCache,
696
+ swrRefreshInFlight: this.swrRefreshInFlight,
680
697
  runningRefineJobs: this.runningRefineJobs,
681
698
  inlineMeshCache: this.inlineMeshCache,
682
699
  meshGitProbeCache: this.meshGitProbeCache,
package/src/index.ts CHANGED
@@ -262,7 +262,7 @@ export { loadRepoSettings } from './config/repo-settings.js';
262
262
  export type { RepoSettings, LoadRepoSettingsOptions } from './config/repo-settings.js';
263
263
 
264
264
  // ── Mesh Task Ledger ──
265
- export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT } from './mesh/mesh-ledger.js';
265
+ export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT, tombstoneOperatingNote, readOperatingNotes, pruneOperatingNotes, isOperatingNoteTombstoned, OPERATING_NOTE_KIND, OPERATING_NOTE_TOMBSTONE_KIND, OPERATING_NOTE_DEDUPE_WINDOW, OPERATING_NOTE_KEEP_LATEST } from './mesh/mesh-ledger.js';
266
266
  export type { AppendRemoteLedgerResult, MeshLedgerEntry, MeshLedgerKind, MeshLedgerSlice, MeshLedgerSummary, ReadLedgerOptions, ReadLedgerSliceOptions, SessionRecoveryContext, MeshTaskCompletionEvidence, MeshWorkerResultArtifact, MeshProcessArtifact, MeshValidationResultArtifact } from './mesh/mesh-ledger.js';
267
267
  export { fastForwardMeshNode } from './mesh/mesh-fast-forward.js';
268
268
  export type { MeshFastForwardNodeArgs, MeshFastForwardPlannedStep, MeshFastForwardResult } from './mesh/mesh-fast-forward.js';
@@ -490,6 +490,7 @@ const TOOLS_SECTION = `## Available Tools
490
490
  | \`mesh_read_debug\` | Collect a daemon-side chat/parser debug bundle for a session |
491
491
  | \`mesh_task_history\` | Read the task ledger — dispatches, completions, failures. Use to understand what has been done before deciding next steps |
492
492
  | \`mesh_record_note\` | Record a durable, provider-neutral operating note (provider quirk / pattern to avoid / recovery lesson). Future coordinators see it under "## Operating Notes" at launch |
493
+ | \`mesh_forget_note\` | Retract a stale/wrong operating note by note_id or exact text so it stops riding into future coordinators' prompts (append-only tombstone; history preserved) |
493
494
  | \`mesh_git_status\` | Check git status on a specific node |
494
495
  | \`mesh_read_node_logs\` | Fetch a remote node's daemon log tail directly over P2P (grep/since/byte-bounded, secrets redacted) — no session/PowerShell needed to debug a node's daemon |
495
496
  | \`mesh_fast_forward_node\` | Safely dry-run or explicitly execute an obvious clean fast-forward without launching an agent session |