pi-better-subagents 0.1.24 → 0.1.26
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.
- package/cleanup.ts +6 -2
- package/index.ts +56 -20
- package/package.json +1 -1
- package/registry.ts +207 -3
- package/shared-navigator.ts +4 -2
package/cleanup.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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 {
|
package/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
695
|
-
*
|
|
696
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1569
|
+
if (needsMonitoring(listMetasForParent(process.pid))) ensureHealthTicker();
|
|
1534
1570
|
});
|
|
1535
1571
|
|
|
1536
1572
|
pi.on("session_before_switch", () => {
|
package/package.json
CHANGED
package/registry.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* for runs this process spawned.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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
|
package/shared-navigator.ts
CHANGED
|
@@ -161,7 +161,7 @@ export function isNavigatorUiAvailable(ctx: ExtensionContext | undefined): boole
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
export function navigatorFooterHint(count: number): string | null {
|
|
164
|
-
return count > 0 ? `←
|
|
164
|
+
return count > 0 ? `← work · ${count}` : null;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
export function applyNavigatorFooter(ui: { setStatus(key: string, value: string | undefined): void }, count: number): string | null {
|
|
@@ -460,7 +460,9 @@ function buildMainListLines(
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
function shortcutsLine(focused: boolean, fg: (color: string, value: string) => string): string {
|
|
463
|
-
const keys = focused
|
|
463
|
+
const keys = focused
|
|
464
|
+
? "↑↓ switch · Enter detail · x stop · Esc unfocus"
|
|
465
|
+
: "← work navigator";
|
|
464
466
|
return dim(keys, fg);
|
|
465
467
|
}
|
|
466
468
|
|