pi-better-harness 0.3.0 → 0.3.2

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.
@@ -162,6 +162,7 @@ function formatSnapshot(snapshot: ActivitySnapshot): string {
162
162
 
163
163
  export default function (pi: ExtensionAPI): void {
164
164
  const providers = new Map<string, BackgroundActivityProvider>();
165
+ const providerUnsubscribers = new Map<string, () => void>();
165
166
  providers.set("subagents", {
166
167
  id: "subagents",
167
168
  label: "Subagents",
@@ -172,6 +173,7 @@ export default function (pi: ExtensionAPI): void {
172
173
  let foregroundRunning = false;
173
174
  let pollTimer: ReturnType<typeof setInterval> | undefined;
174
175
  let collecting = false;
176
+ let collectionPending = false;
175
177
  let latestSnapshot: ActivitySnapshot | null = null;
176
178
  let backgroundDrainTracker: BackgroundDrainTracker | null = null;
177
179
  let lastWakeSignature = "";
@@ -230,6 +232,7 @@ export default function (pi: ExtensionAPI): void {
230
232
  continuationQueuedFor = null;
231
233
  backgroundDrainTracker = null;
232
234
  clearIdleContinuation();
235
+ syncPollingState();
233
236
  // Force a full redraw when the dock height changes (absent ↔ visible clock).
234
237
  refreshGoalWidget?.(!wasVisible || !isGoalClockVisible(goal));
235
238
  };
@@ -241,6 +244,7 @@ export default function (pi: ExtensionAPI): void {
241
244
  continuationQueuedFor = null;
242
245
  backgroundDrainTracker = null;
243
246
  clearIdleContinuation();
247
+ syncPollingState();
244
248
  refreshGoalWidget?.(wasVisible);
245
249
  };
246
250
 
@@ -409,12 +413,22 @@ export default function (pi: ExtensionAPI): void {
409
413
 
410
414
  const collectIfPossible = (): void => {
411
415
  const ctx = currentCtx;
412
- if (!ctx || collecting) {
416
+ if (!ctx) {
417
+ return;
418
+ }
419
+ if (collecting) {
420
+ collectionPending = true;
413
421
  return;
414
422
  }
415
423
  collecting = true;
416
424
  void publishSnapshot(ctx).finally(() => {
417
425
  collecting = false;
426
+ if (collectionPending) {
427
+ collectionPending = false;
428
+ collectIfPossible();
429
+ } else {
430
+ syncPollingState();
431
+ }
418
432
  });
419
433
  };
420
434
 
@@ -424,7 +438,6 @@ export default function (pi: ExtensionAPI): void {
424
438
  }
425
439
  pollTimer = setInterval(collectIfPossible, POLL_INTERVAL_MS);
426
440
  pollTimer.unref?.();
427
- collectIfPossible();
428
441
  };
429
442
 
430
443
  const stopPolling = (): void => {
@@ -435,6 +448,16 @@ export default function (pi: ExtensionAPI): void {
435
448
  pollTimer = undefined;
436
449
  };
437
450
 
451
+ const syncPollingState = (): void => {
452
+ const ctx = currentCtx;
453
+ const goal = ctx ? currentGoalSnapshot(ctx) : null;
454
+ if (ctx && (isPokeable(goal) || latestSnapshot?.backgroundRunning === true)) {
455
+ startPolling();
456
+ } else {
457
+ stopPolling();
458
+ }
459
+ };
460
+
438
461
  const installGoalWidget = (ctx: ExtensionContext): void => {
439
462
  if (!ctx.hasUI) {
440
463
  return;
@@ -494,8 +517,14 @@ export default function (pi: ExtensionAPI): void {
494
517
  if (!candidate || typeof candidate.id !== "string" || typeof candidate.getActivity !== "function") {
495
518
  return;
496
519
  }
497
- providers.set(candidate.id, candidate as BackgroundActivityProvider);
498
- collectIfPossible();
520
+ const accepted = candidate as BackgroundActivityProvider;
521
+ providerUnsubscribers.get(accepted.id)?.();
522
+ providerUnsubscribers.delete(accepted.id);
523
+ providers.set(accepted.id, accepted);
524
+ if (typeof accepted.onActivityChanged === "function") {
525
+ providerUnsubscribers.set(accepted.id, accepted.onActivityChanged(collectIfPossible));
526
+ }
527
+ if (latestSnapshot) collectIfPossible();
499
528
  });
500
529
 
501
530
  pi.registerCommand("goal", {
@@ -661,7 +690,8 @@ export default function (pi: ExtensionAPI): void {
661
690
  foregroundRunning = !ctx.isIdle();
662
691
  pi.events.emit(EVENT_READY, { version: EXTENSION_VERSION });
663
692
  installGoalWidget(ctx);
664
- startPolling();
693
+ latestSnapshot = await publishSnapshot(ctx);
694
+ syncPollingState();
665
695
  });
666
696
 
667
697
  pi.on("input", async (event, ctx) => {
@@ -762,6 +792,8 @@ export default function (pi: ExtensionAPI): void {
762
792
  foregroundRunning = false;
763
793
  clearIdleContinuation();
764
794
  currentCtx = undefined;
795
+ latestSnapshot = null;
796
+ collectionPending = false;
765
797
  try {
766
798
  ctx.ui.setStatus(EXTENSION_NAME, undefined);
767
799
  ctx.ui.setWidget(EXTENSION_NAME, undefined);
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -82,6 +82,46 @@ export function readSubagentMetas(baseDir = defaultSubagentBaseDir()): SubagentR
82
82
  return metas.sort((left, right) => right.startedAt - left.startedAt);
83
83
  }
84
84
 
85
+ export function readParentSubagentMetas(
86
+ baseDir = defaultSubagentBaseDir(),
87
+ parentPid = process.pid,
88
+ ): SubagentRunMeta[] {
89
+ const directory = join(baseDir, "by-parent", String(parentPid));
90
+ ensureParentIndex(baseDir, directory, parentPid);
91
+ let ids: string[];
92
+ try {
93
+ ids = readdirSync(directory).filter((id) => id !== ".initialized");
94
+ } catch {
95
+ return [];
96
+ }
97
+ return ids
98
+ .map((id) => readSubagentMeta(baseDir, id))
99
+ .filter((meta): meta is SubagentRunMeta => meta !== undefined && meta.spawnPid === parentPid)
100
+ .sort((left, right) => right.startedAt - left.startedAt);
101
+ }
102
+
103
+ function readSubagentMeta(baseDir: string, id: string): SubagentRunMeta | undefined {
104
+ try {
105
+ const meta = JSON.parse(readFileSync(join(baseDir, "runs", id, "meta.json"), "utf8")) as SubagentRunMeta;
106
+ return typeof meta.id === "string" && typeof meta.status === "string" ? meta : undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ function ensureParentIndex(baseDir: string, directory: string, parentPid: number): void {
113
+ try {
114
+ readFileSync(join(directory, ".initialized"));
115
+ return;
116
+ } catch {
117
+ // Existing registries are backfilled once for this parent process.
118
+ }
119
+ const owned = readSubagentMetas(baseDir).filter((meta) => meta.spawnPid === parentPid);
120
+ mkdirSync(directory, { recursive: true });
121
+ for (const meta of owned) writeFileSync(join(directory, meta.id), "");
122
+ writeFileSync(join(directory, ".initialized"), "1");
123
+ }
124
+
85
125
  export function subagentStatusToItem(
86
126
  meta: SubagentRunMeta,
87
127
  status: SubagentEffectiveStatus,
@@ -122,9 +162,7 @@ export function collectSubagentActivity(
122
162
  ): BackgroundProviderSnapshot {
123
163
  const parentPid = options.parentPid ?? process.pid;
124
164
  const processExists = options.processExists ?? defaultProcessExists;
125
- const metas = readSubagentMetas(options.baseDir).filter((meta) =>
126
- isCurrentParentSubagent(meta, parentPid),
127
- );
165
+ const metas = readParentSubagentMetas(options.baseDir, parentPid);
128
166
 
129
167
  return {
130
168
  providerId: "subagents",
@@ -47,6 +47,7 @@ export interface BackgroundActivityProvider {
47
47
  id: string;
48
48
  label?: string;
49
49
  getActivity(ctx: ExtensionContext): BackgroundProviderSnapshot | Promise<BackgroundProviderSnapshot>;
50
+ onActivityChanged?(notify: () => void): () => void;
50
51
  }
51
52
 
52
53
  export interface ActivitySnapshot {
@@ -13,6 +13,7 @@ import {
13
13
  listMetas,
14
14
  ownedByThisParent,
15
15
  readMeta,
16
+ removeMetaArtifacts,
16
17
  runDir,
17
18
  sessionsDir,
18
19
  type RunMeta,
@@ -190,7 +191,8 @@ function cleanRunDirs(cutoff: number, errors: string[]): number {
190
191
  if (meta) {
191
192
  if (!TERMINAL_CLEANUP_STATUSES.has(meta.status)) continue;
192
193
  if (terminalTimestamp(meta) >= cutoff) continue;
193
- if (removePath(dir, errors)) removed += 1;
194
+ if (removeMetaArtifacts(meta)) removed += 1;
195
+ else errors.push(`${dir}: could not remove run artifacts`);
194
196
  continue;
195
197
  }
196
198
 
@@ -379,7 +381,9 @@ export function enforceRegistrySizeCapOnce(options: SizeCapOptions = {}): SizeCa
379
381
  const plan = planRegistrySizeCap(collectSizeCapEntries(), { maxBytes, protectedIds });
380
382
  const removed: string[] = [];
381
383
  for (const id of plan.remove) {
382
- if (removePath(runDir(id), errors)) removed.push(id);
384
+ const meta = readMeta(id);
385
+ if (meta ? removeMetaArtifacts(meta) : removePath(runDir(id), errors)) removed.push(id);
386
+ else if (meta) errors.push(`${runDir(id)}: could not remove run artifacts`);
383
387
  }
384
388
 
385
389
  try {
@@ -49,6 +49,9 @@ import {
49
49
  writeMeta,
50
50
  readMeta,
51
51
  listMetas,
52
+ listActiveMetasForParent,
53
+ listMetasForOrigin,
54
+ listMetasForParent,
52
55
  onMetaChanged,
53
56
  effectiveStatus,
54
57
  ownedByThisParent,
@@ -124,6 +127,44 @@ const SUBAGENT_TOOLS = [
124
127
  "subagent_result",
125
128
  ];
126
129
 
130
+ const GOAL_READY_EVENT = "pi-better-goal:ready";
131
+ const GOAL_REGISTER_PROVIDER_EVENT = "pi-better-goal:register-provider";
132
+ const goalReadySubscriptions = new WeakSet<ExtensionAPI>();
133
+
134
+ function registerSubagentsGoalProvider(pi: ExtensionAPI): void {
135
+ const emitProvider = (): void => {
136
+ pi.events?.emit(GOAL_REGISTER_PROVIDER_EVENT, {
137
+ id: "subagents",
138
+ label: "Subagents",
139
+ getActivity: () => ({
140
+ providerId: "subagents",
141
+ label: "Subagents",
142
+ items: listMetasForParent(process.pid).map((meta) => {
143
+ const status = effectiveStatus(meta);
144
+ const active = status === "running" || status === "orphaned";
145
+ return {
146
+ id: meta.id,
147
+ ...(meta.name ? { label: meta.name } : {}),
148
+ status,
149
+ active,
150
+ unhealthy: status === "orphaned",
151
+ terminal: !active,
152
+ attention: status === "orphaned" || status === "failed" || status === "killed" || status === "lost" || status === "exited",
153
+ startedAt: meta.startedAt,
154
+ ...(meta.endedAt !== undefined ? { endedAt: meta.endedAt } : {}),
155
+ };
156
+ }),
157
+ }),
158
+ onActivityChanged: onMetaChanged,
159
+ });
160
+ };
161
+ if (!goalReadySubscriptions.has(pi)) {
162
+ pi.events?.on?.(GOAL_READY_EVENT, emitProvider);
163
+ goalReadySubscriptions.add(pi);
164
+ }
165
+ emitProvider();
166
+ }
167
+
127
168
  // ---- retired live status widget ------------------------------------------
128
169
  //
129
170
  // The shared background-work navigator owns the active subagent list. This
@@ -224,7 +265,7 @@ function hasSelfProcessIdentity(meta: RunMeta): boolean {
224
265
 
225
266
  function stopCurrentSessionSubagents(ctx: ExtensionContext): void {
226
267
  const origin = callbackOriginFromContext(ctx);
227
- for (const summary of listMetas()) {
268
+ for (const summary of listMetasForParent(process.pid)) {
228
269
  if (!ownedByThisParent(summary)) continue;
229
270
  if (summary.status !== "running" && summary.status !== "orphaned") continue;
230
271
  if (!belongsToOrigin(summary, origin)) continue;
@@ -286,7 +327,7 @@ function enqueueCompletionCallback(pi: ExtensionAPI, id: string): void {
286
327
 
287
328
  /** Recover only records explicitly marked pending; legacy terminal runs never replay. */
288
329
  function recoverCompletionCallbacks(pi: ExtensionAPI): void {
289
- for (const meta of listMetas()) {
330
+ for (const meta of listMetasForParent(process.pid)) {
290
331
  if (!ownedByThisParent(meta)) continue;
291
332
  enqueueCompletionCallback(pi, meta.id);
292
333
  }
@@ -504,15 +545,12 @@ function deliverHealthCallback(pi: ExtensionAPI | undefined, meta: RunMeta, stat
504
545
  function reconcileHealth(): void {
505
546
  const ctx = uiCtx;
506
547
  const pi = healthPi;
507
- // Free work while the loop is already running: nothing else will ever
508
- // reconcile a record whose parent is gone.
509
- reconcileAbandonedRuns();
510
- for (const summary of listMetas()) {
548
+ for (const summary of listMetasForParent(process.pid)) {
511
549
  if (!ownedByThisParent(summary)) continue;
512
550
  // running/orphaned: process reconcile. lost: durable callback recovery only.
513
551
  if (summary.status !== "running" && summary.status !== "orphaned" && summary.status !== "lost") continue;
514
552
  // Re-read under the id: finalizeRun / subagent_stop may have written a
515
- // terminal status since listMetas() snapshotted.
553
+ // terminal status since the owned index was read.
516
554
  const meta = readMeta(summary.id);
517
555
  if (!meta) continue;
518
556
  if (meta.status !== "running" && meta.status !== "orphaned" && meta.status !== "lost") continue;
@@ -544,7 +582,7 @@ function reconcileHealth(): void {
544
582
  }
545
583
  }
546
584
  // Stop existing the moment nothing current-parent needs monitoring/recovery.
547
- if (!needsMonitoring(listMetas())) stopHealthTicker();
585
+ if (!needsMonitoring(listMetasForParent(process.pid))) stopHealthTicker();
548
586
  }
549
587
 
550
588
  /**
@@ -691,17 +729,14 @@ function navigatorRunningCount(): number {
691
729
  }
692
730
 
693
731
  /**
694
- * The visible run set. `listMetas` reads and parses one `meta.json` per run in
695
- * the registry, so a caller that needs this twice should scan once and pass the
696
- * snapshot down (see `subagentWorkRows`) rather than ask again.
697
- *
698
- * Deliberately NOT cached across calls. A time-based memo here was tried and
699
- * reverted: it made a run created outside this process — another pi session's
700
- * spawn, or a test seeding one — invisible until the window expired, and the
701
- * navigator then acted on a set that no longer matched the registry.
732
+ * The visible run set. The durable origin index limits reads to this session;
733
+ * each rebuild still reads owned non-terminal metadata so external status
734
+ * changes are visible immediately without a time-based memo.
702
735
  */
703
736
  function sessionVisibleNavigatorRuns(now: number = Date.now()): RunMeta[] {
704
- return navigatorVisibleRuns(listMetas())
737
+ const origin = activeCallbackOrigin;
738
+ if (!origin) return [];
739
+ return navigatorVisibleRuns(listMetasForOrigin(origin))
705
740
  .filter(belongsToActiveNavigatorSession)
706
741
  .filter((m) => !isExpiredTerminalNavigatorRun(m, now));
707
742
  }
@@ -1055,6 +1090,7 @@ export default function (pi: ExtensionAPI) {
1055
1090
  // coordinator follow-ups that fire outside a tool-call stack (#65).
1056
1091
  healthPi = pi;
1057
1092
  ensureSubagentProvider();
1093
+ registerSubagentsGoalProvider(pi);
1058
1094
 
1059
1095
  type SpawnParams = {
1060
1096
  prompt: string; name?: string; model?: string; thinking?: ThinkingLevel; tools?: string;
@@ -1260,7 +1296,7 @@ export default function (pi: ExtensionAPI) {
1260
1296
  const cfg = loadConfig();
1261
1297
  const maxConcurrent = cfg.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
1262
1298
  const countRunning = () =>
1263
- listMetas().filter((m) => ownedByThisParent(m) && effectiveStatus(m) === "running").length;
1299
+ listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
1264
1300
  // Shared with batch-spawn: reserve before any async work so an interleaved
1265
1301
  // batch cannot oversubscribe after this check and before writeMeta.
1266
1302
  const gate = getSharedCapacityGate(countRunning);
@@ -1351,7 +1387,7 @@ export default function (pi: ExtensionAPI) {
1351
1387
  const cfg = loadConfig();
1352
1388
  const maxConcurrent = cfg.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
1353
1389
  const countRunning = () =>
1354
- listMetas().filter((m) => ownedByThisParent(m) && effectiveStatus(m) === "running").length;
1390
+ listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
1355
1391
  const launchAvailable = p.onCapacity === "launch-available";
1356
1392
  // Shared with single-spawn. Reservations count against maxConcurrent so a
1357
1393
  // concurrent single spawn cannot take a slot the batch already admitted.
@@ -1530,7 +1566,7 @@ export default function (pi: ExtensionAPI) {
1530
1566
  // Resume supervision reconciliation + durable health-callback recovery
1531
1567
  // across /reload while current-parent work still needs the ticker
1532
1568
  // (running/orphaned, or unmarked lost); it stops itself when idle.
1533
- if (needsMonitoring(listMetas())) ensureHealthTicker();
1569
+ if (needsMonitoring(listMetasForParent(process.pid))) ensureHealthTicker();
1534
1570
  });
1535
1571
 
1536
1572
  pi.on("session_before_switch", () => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -7,7 +7,8 @@
7
7
  * for runs this process spawned.
8
8
  */
9
9
 
10
- import { mkdirSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
10
+ import { createHash } from "node:crypto";
11
+ import { mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync, unlinkSync } from "node:fs";
11
12
  import { tmpdir } from "node:os";
12
13
  import { join } from "node:path";
13
14
  import { processExists } from "./spawn.ts";
@@ -151,7 +152,31 @@ function metaPathFor(id: string): string {
151
152
  }
152
153
 
153
154
  let seq = 0;
155
+ const metaCache = new Map<string, RunMeta>();
156
+ // Owned snapshots are process-resident. A cheap directory signature catches
157
+ // cross-process index changes before any cached IDs are reused.
158
+ const indexIdsCache = new Map<string, { ids: Set<string>; signature: string }>();
159
+ const initializedIndexes = new Set<string>();
154
160
  const metaChangedListeners = new Set<() => void>();
161
+ const registryIo = { fullDirectoryReads: 0, indexDirectoryReads: 0, metadataFileReads: 0, indexRevisionChecks: 0 };
162
+
163
+ export interface RegistryIoMetrics {
164
+ fullDirectoryReads: number;
165
+ indexDirectoryReads: number;
166
+ metadataFileReads: number;
167
+ indexRevisionChecks: number;
168
+ }
169
+
170
+ export function getRegistryIoMetrics(): RegistryIoMetrics {
171
+ return { ...registryIo };
172
+ }
173
+
174
+ export function resetRegistryIoMetrics(): void {
175
+ registryIo.fullDirectoryReads = 0;
176
+ registryIo.indexDirectoryReads = 0;
177
+ registryIo.metadataFileReads = 0;
178
+ registryIo.indexRevisionChecks = 0;
179
+ }
155
180
  /** Monotonic, readable, collision-free run id: `sa_<base36-time>_<seq>`. */
156
181
  export function nextRunId(): string {
157
182
  seq += 1;
@@ -161,6 +186,8 @@ export function nextRunId(): string {
161
186
  export function writeMeta(meta: RunMeta): void {
162
187
  mkdirSync(runDir(meta.id), { recursive: true });
163
188
  writeFileSync(metaPathFor(meta.id), JSON.stringify(meta, null, 2));
189
+ metaCache.set(meta.id, meta);
190
+ indexMeta(meta);
164
191
  for (const listener of metaChangedListeners) {
165
192
  try { listener(); } catch { /* best effort */ }
166
193
  }
@@ -173,26 +200,203 @@ export function onMetaChanged(listener: () => void): () => void {
173
200
 
174
201
  export function readMeta(id: string): RunMeta | undefined {
175
202
  try {
176
- return JSON.parse(readFileSync(metaPathFor(id), "utf-8")) as RunMeta;
203
+ registryIo.metadataFileReads += 1;
204
+ const meta = JSON.parse(readFileSync(metaPathFor(id), "utf-8")) as RunMeta;
205
+ metaCache.set(id, meta);
206
+ return meta;
177
207
  } catch {
208
+ metaCache.delete(id);
178
209
  return undefined;
179
210
  }
180
211
  }
181
212
 
213
+ export function removeMetaArtifacts(meta: RunMeta): boolean {
214
+ try {
215
+ rmSync(runDir(meta.id), { recursive: true, force: true });
216
+ metaCache.delete(meta.id);
217
+ removeIndexEntry(join(baseDir(), "by-parent", String(meta.spawnPid)), meta.id);
218
+ removeIndexEntry(join(baseDir(), "by-parent-active", String(meta.spawnPid)), meta.id);
219
+ removeIndexEntry(join(baseDir(), "by-origin", originKey(originOf(meta))), meta.id);
220
+ for (const listener of metaChangedListeners) {
221
+ try { listener(); } catch { /* best effort */ }
222
+ }
223
+ return true;
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+
182
229
  /** All runs, newest first. */
183
230
  export function listMetas(): RunMeta[] {
184
231
  let ids: string[];
185
232
  try {
233
+ registryIo.fullDirectoryReads += 1;
186
234
  ids = readdirSync(join(baseDir(), "runs"));
187
235
  } catch {
188
236
  return [];
189
237
  }
190
238
  return ids
191
- .map(readMeta)
239
+ .map(readMetaForSweep)
192
240
  .filter((m): m is RunMeta => m !== undefined)
193
241
  .sort((a, b) => b.startedAt - a.startedAt);
194
242
  }
195
243
 
244
+ export function listMetasForParent(parentPid: number): RunMeta[] {
245
+ const directory = join(baseDir(), "by-parent", String(parentPid));
246
+ ensureIndex(directory, (meta) => meta.spawnPid === parentPid);
247
+ return readIndexedMetas(directory).filter((meta) => meta.spawnPid === parentPid);
248
+ }
249
+
250
+ export function listActiveMetasForParent(parentPid: number): RunMeta[] {
251
+ const directory = join(baseDir(), "by-parent-active", String(parentPid));
252
+ ensureActiveParentIndex(directory, parentPid);
253
+ return readIndexedMetas(directory)
254
+ .filter((meta) => meta.spawnPid === parentPid && isActiveStatus(meta.status));
255
+ }
256
+
257
+ export function listMetasForOrigin(origin: RunCallbackOrigin): RunMeta[] {
258
+ const directory = join(baseDir(), "by-origin", originKey(origin));
259
+ ensureIndex(directory, (meta) => belongsToOrigin(meta, origin));
260
+ return readIndexedMetas(directory).filter((meta) => belongsToOrigin(meta, origin));
261
+ }
262
+
263
+ function readMetaForSweep(id: string): RunMeta | undefined {
264
+ const cached = metaCache.get(id);
265
+ if (cached && cached.status !== "running" && cached.status !== "orphaned") return cached;
266
+ return readMeta(id);
267
+ }
268
+
269
+ function readIndexedMetas(directory: string): RunMeta[] {
270
+ return readIndexIds(directory)
271
+ .map((id) => metaCache.get(id) ?? readMeta(id))
272
+ .filter((meta): meta is RunMeta => meta !== undefined)
273
+ .sort((a, b) => b.startedAt - a.startedAt);
274
+ }
275
+
276
+ function indexMeta(meta: RunMeta): void {
277
+ try {
278
+ writeIndexEntry(join(baseDir(), "by-parent", String(meta.spawnPid)), meta.id);
279
+ const activeDirectory = join(baseDir(), "by-parent-active", String(meta.spawnPid));
280
+ if (isActiveStatus(meta.status)) writeIndexEntry(activeDirectory, meta.id);
281
+ else removeIndexEntry(activeDirectory, meta.id);
282
+ writeIndexEntry(join(baseDir(), "by-origin", originKey(originOf(meta))), meta.id);
283
+ } catch {
284
+ // Indexes are accelerators; meta.json remains authoritative.
285
+ }
286
+ }
287
+
288
+ function writeIndexEntry(directory: string, id: string): void {
289
+ mkdirSync(directory, { recursive: true });
290
+ const cached = indexIdsCache.get(directory);
291
+ const cacheWasCurrent = cached ? cached.signature === indexDirectorySignature(directory) : false;
292
+ try {
293
+ writeFileSync(join(directory, id), "", { flag: "wx" });
294
+ } catch (error) {
295
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
296
+ }
297
+ if (cached && cacheWasCurrent) {
298
+ cached.ids.add(id);
299
+ cached.signature = indexDirectorySignature(directory);
300
+ } else if (cached) {
301
+ indexIdsCache.delete(directory);
302
+ }
303
+ }
304
+
305
+ function removeIndexEntry(directory: string, id: string): void {
306
+ const cached = indexIdsCache.get(directory);
307
+ const cacheWasCurrent = cached ? cached.signature === indexDirectorySignature(directory) : false;
308
+ try { unlinkSync(join(directory, id)); } catch { /* stale index entries are harmless */ }
309
+ if (cached && cacheWasCurrent) {
310
+ cached.ids.delete(id);
311
+ cached.signature = indexDirectorySignature(directory);
312
+ } else if (cached) {
313
+ indexIdsCache.delete(directory);
314
+ }
315
+ }
316
+
317
+ function readIndexIds(directory: string): string[] {
318
+ const signature = indexDirectorySignature(directory);
319
+ const cached = indexIdsCache.get(directory);
320
+ if (cached && cached.signature === signature) return [...cached.ids];
321
+ try {
322
+ registryIo.indexDirectoryReads += 1;
323
+ const ids = new Set(readdirSync(directory).filter((id) => id !== ".initialized"));
324
+ indexIdsCache.set(directory, { ids, signature: indexDirectorySignature(directory) });
325
+ return [...ids];
326
+ } catch {
327
+ indexIdsCache.delete(directory);
328
+ return [];
329
+ }
330
+ }
331
+
332
+ function isActiveStatus(status: RunStatus): boolean {
333
+ return status === "running" || status === "orphaned";
334
+ }
335
+
336
+ function indexDirectorySignature(directory: string): string {
337
+ registryIo.indexRevisionChecks += 1;
338
+ try {
339
+ const stat = statSync(directory);
340
+ return `${stat.dev}:${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`;
341
+ } catch {
342
+ return "missing";
343
+ }
344
+ }
345
+
346
+ function ensureIndex(directory: string, matches: (meta: RunMeta) => boolean): void {
347
+ if (initializedIndexes.has(directory)) return;
348
+ try {
349
+ readFileSync(join(directory, ".initialized"));
350
+ initializedIndexes.add(directory);
351
+ return;
352
+ } catch {
353
+ // Existing registries are backfilled once for each owner.
354
+ }
355
+ indexIdsCache.delete(directory);
356
+ const owned = listMetas().filter(matches);
357
+ mkdirSync(directory, { recursive: true });
358
+ for (const meta of owned) writeIndexEntry(directory, meta.id);
359
+ writeFileSync(join(directory, ".initialized"), "1");
360
+ initializedIndexes.add(directory);
361
+ }
362
+
363
+ function ensureActiveParentIndex(directory: string, parentPid: number): void {
364
+ if (initializedIndexes.has(directory)) return;
365
+ try {
366
+ readFileSync(join(directory, ".initialized"));
367
+ initializedIndexes.add(directory);
368
+ return;
369
+ } catch {
370
+ // Build the active set from the already owner-scoped parent index.
371
+ }
372
+ indexIdsCache.delete(directory);
373
+ const active = listMetasForParent(parentPid).filter((meta) => isActiveStatus(meta.status));
374
+ mkdirSync(directory, { recursive: true });
375
+ for (const meta of active) writeIndexEntry(directory, meta.id);
376
+ writeFileSync(join(directory, ".initialized"), "1");
377
+ initializedIndexes.add(directory);
378
+ }
379
+
380
+ function originOf(meta: RunMeta): RunCallbackOrigin {
381
+ return meta.callbackOrigin ?? { cwd: meta.cwd };
382
+ }
383
+
384
+ function belongsToOrigin(meta: RunMeta, origin: RunCallbackOrigin): boolean {
385
+ const candidate = originOf(meta);
386
+ if (candidate.cwd !== origin.cwd) return false;
387
+ if (candidate.sessionId || origin.sessionId) return candidate.sessionId === origin.sessionId;
388
+ return true;
389
+ }
390
+
391
+ function originKey(origin: RunCallbackOrigin): string {
392
+ return createHash("sha256")
393
+ .update(origin.cwd)
394
+ .update("\0")
395
+ .update(origin.sessionId ?? "")
396
+ .digest("hex")
397
+ .slice(0, 24);
398
+ }
399
+
196
400
  /**
197
401
  * Reconcile the recorded status with reality for display. A run marked
198
402
  * "running" whose PID is no longer alive exited without our handler firing