@diegopetrucci/pi-extensions 0.1.57 → 0.1.59
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/.pi-fleet-tested-version +1 -1
- package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
- package/extensions/agent-workflow-audit/index.ts +8 -1
- package/extensions/agent-workflow-audit/package.json +5 -1
- package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-git-diff/package.json +5 -2
- package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-last-message/package.json +5 -2
- package/extensions/brrr/.pi-fleet-tested-version +1 -1
- package/extensions/brrr/README.md +2 -2
- package/extensions/brrr/package.json +5 -1
- package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
- package/extensions/claude-fast/package.json +5 -1
- package/extensions/code-reviewer/.pi-fleet-tested-version +1 -1
- package/extensions/code-reviewer/index.ts +129 -39
- package/extensions/code-reviewer/package.json +5 -1
- package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +20 -5
- package/extensions/confirm-destructive/package.json +5 -1
- package/extensions/context-cap/.pi-fleet-tested-version +1 -1
- package/extensions/context-cap/package.json +5 -1
- package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
- package/extensions/context-inspector/package.json +5 -1
- package/extensions/contrarian/.pi-fleet-tested-version +1 -1
- package/extensions/contrarian/README.md +1 -1
- package/extensions/contrarian/index.ts +199 -40
- package/extensions/contrarian/package.json +5 -1
- package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
- package/extensions/dirty-repo-guard/README.md +1 -1
- package/extensions/dirty-repo-guard/package.json +5 -1
- package/extensions/dynamic-context-pruning/.pi-fleet-tested-version +1 -1
- package/extensions/dynamic-context-pruning/package.json +4 -1
- package/extensions/git-footer/.pi-fleet-tested-version +1 -1
- package/extensions/git-footer/package.json +5 -1
- package/extensions/gnosis/.pi-fleet-tested-version +1 -1
- package/extensions/gnosis/package.json +5 -1
- package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
- package/extensions/illustrations-to-explain-things/package.json +4 -1
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.webp +0 -0
- package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
- package/extensions/inline-bash/README.md +1 -1
- package/extensions/inline-bash/package.json +5 -1
- package/extensions/librarian/.pi-fleet-tested-version +1 -1
- package/extensions/librarian/index.ts +393 -107
- package/extensions/librarian/package.json +5 -1
- package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
- package/extensions/minimal-footer/index.ts +2 -5
- package/extensions/minimal-footer/openai-usage.ts +58 -11
- package/extensions/minimal-footer/package.json +5 -1
- package/extensions/notify/.pi-fleet-tested-version +1 -1
- package/extensions/notify/README.md +3 -3
- package/extensions/notify/package.json +5 -1
- package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
- package/extensions/openai-fast/package.json +5 -1
- package/extensions/oracle/.pi-fleet-tested-version +1 -1
- package/extensions/oracle/README.md +1 -1
- package/extensions/oracle/index.ts +200 -41
- package/extensions/oracle/package.json +5 -1
- package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
- package/extensions/permission-gate/README.md +1 -1
- package/extensions/permission-gate/index.ts +453 -2
- package/extensions/permission-gate/package.json +5 -1
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/package.json +5 -1
- package/extensions/review/.pi-fleet-tested-version +1 -1
- package/extensions/review/index.ts +53 -19
- package/extensions/review/package.json +5 -1
- package/extensions/todo/.pi-fleet-tested-version +1 -1
- package/extensions/todo/README.md +1 -1
- package/extensions/todo/package.json +5 -1
- package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
- package/extensions/triage-comments/index.ts +115 -34
- package/extensions/triage-comments/package.json +5 -1
- package/package.json +10 -6
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.png +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Dirent, Stats } from "node:fs";
|
|
2
3
|
import type { FileHandle } from "node:fs/promises";
|
|
3
4
|
import * as fs from "node:fs/promises";
|
|
4
5
|
import * as os from "node:os";
|
|
5
6
|
import * as path from "node:path";
|
|
6
7
|
|
|
7
8
|
import type { ExtensionAPI, ExtensionContext, ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
8
10
|
import {
|
|
9
11
|
DefaultResourceLoader,
|
|
10
12
|
SessionManager,
|
|
@@ -26,6 +28,11 @@ const CACHE_TTL_MS = CACHE_TTL_DAYS * 24 * 60 * 60 * 1000;
|
|
|
26
28
|
const CACHE_METADATA_FILE = ".pi-librarian-cache.json";
|
|
27
29
|
const CACHE_MARKER_FILE = ".pi-librarian-cache-used";
|
|
28
30
|
const CACHE_CONFIG_FILE = "librarian.json";
|
|
31
|
+
const CLEANUP_LOCK_FILE = ".cleanup.lock";
|
|
32
|
+
const CLEANUP_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
33
|
+
const CLEANUP_LOCK_HEARTBEAT_MS = 60 * 1000;
|
|
34
|
+
// Absolute owner lease prevents PID reuse from keeping an abandoned lock forever.
|
|
35
|
+
const CLEANUP_LOCK_MAX_LEASE_MS = 24 * 60 * 60 * 1000;
|
|
29
36
|
|
|
30
37
|
type LibrarianStatus = "running" | "done" | "error" | "aborted";
|
|
31
38
|
|
|
@@ -39,6 +46,13 @@ type PiModel = {
|
|
|
39
46
|
thinkingLevelMap?: ThinkingLevelMap;
|
|
40
47
|
};
|
|
41
48
|
|
|
49
|
+
type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
|
|
50
|
+
|
|
51
|
+
function getModelRuntimeOption(ctx: { modelRegistry?: unknown }): Pick<CreateAgentSessionOptions, "modelRuntime"> {
|
|
52
|
+
const modelRuntime = (ctx.modelRegistry as { runtime?: CreateAgentSessionOptions["modelRuntime"] } | undefined)?.runtime;
|
|
53
|
+
return modelRuntime ? { modelRuntime } : {};
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
const DEFAULT_CACHE_MODE: CacheMode = "disabled";
|
|
43
57
|
const DEFAULT_THINKING_LEVEL: ThinkingLevel = "low";
|
|
44
58
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
@@ -245,54 +259,236 @@ async function isManagedRepoCache(repoDir: string): Promise<boolean> {
|
|
|
245
259
|
(await pathExists(path.join(repoDir, CACHE_MARKER_FILE))));
|
|
246
260
|
}
|
|
247
261
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
type CleanupLockOwner = {
|
|
263
|
+
pid: number;
|
|
264
|
+
hostname: string;
|
|
265
|
+
token: string;
|
|
266
|
+
acquiredAt: string;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
type CleanupLock = {
|
|
270
|
+
handle: FileHandle;
|
|
271
|
+
path: string;
|
|
272
|
+
owner: CleanupLockOwner;
|
|
273
|
+
identity: Pick<Stats, "dev" | "ino">;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
function sameFileIdentity(left: Pick<Stats, "dev" | "ino">, right: Pick<Stats, "dev" | "ino">): boolean {
|
|
277
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
278
|
+
}
|
|
255
279
|
|
|
280
|
+
function parseCleanupLockOwner(value: string): CleanupLockOwner | undefined {
|
|
256
281
|
try {
|
|
257
|
-
|
|
282
|
+
const owner = JSON.parse(value) as Partial<CleanupLockOwner>;
|
|
283
|
+
if (!Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0) return undefined;
|
|
284
|
+
if (typeof owner.hostname !== "string" || !owner.hostname) return undefined;
|
|
285
|
+
if (typeof owner.token !== "string" || !owner.token) return undefined;
|
|
286
|
+
if (typeof owner.acquiredAt !== "string" || !owner.acquiredAt) return undefined;
|
|
287
|
+
if (!Number.isFinite(Date.parse(owner.acquiredAt))) return undefined;
|
|
288
|
+
return owner as CleanupLockOwner;
|
|
289
|
+
} catch {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isCleanupLockLeaseExpired(owner: CleanupLockOwner | undefined, now = Date.now()): boolean {
|
|
295
|
+
if (!owner) return true;
|
|
296
|
+
const acquiredAt = Date.parse(owner.acquiredAt);
|
|
297
|
+
if (!Number.isFinite(acquiredAt)) return true;
|
|
298
|
+
return Math.max(0, now - acquiredAt) > CLEANUP_LOCK_MAX_LEASE_MS;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isRecordedOwnerAlive(owner: CleanupLockOwner | undefined): boolean {
|
|
302
|
+
if (!owner || owner.hostname !== os.hostname()) return false;
|
|
303
|
+
try {
|
|
304
|
+
process.kill(owner.pid, 0);
|
|
305
|
+
return true;
|
|
258
306
|
} catch (error) {
|
|
259
|
-
|
|
307
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function restoreMovedLock(movedPath: string, lockPath: string): Promise<void> {
|
|
312
|
+
try {
|
|
313
|
+
// link() restores only when lockPath is still absent; unlike rename(), it cannot overwrite a newer owner.
|
|
314
|
+
await fs.link(movedPath, lockPath);
|
|
315
|
+
await fs.rm(movedPath, { force: true });
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
318
|
+
// Preserve both owners rather than deleting the moved lock when another owner won the canonical path.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function removeLockPathIfOwned(lockPath: string, identity: Pick<Stats, "dev" | "ino">): Promise<void> {
|
|
323
|
+
const currentStat = await fs.stat(lockPath).catch(() => undefined);
|
|
324
|
+
if (!currentStat || !sameFileIdentity(identity, currentStat)) return;
|
|
325
|
+
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function getCleanupLockPathIdentity(lockPath: string): Promise<Pick<Stats, "dev" | "ino"> | undefined> {
|
|
329
|
+
const canonicalPath = await fs.realpath(lockPath).catch((error: NodeJS.ErrnoException) => {
|
|
330
|
+
if (error.code === "ENOENT") return undefined;
|
|
331
|
+
throw error;
|
|
332
|
+
});
|
|
333
|
+
if (!canonicalPath) return undefined;
|
|
334
|
+
const currentStat = await fs.stat(canonicalPath).catch((error: NodeJS.ErrnoException) => {
|
|
335
|
+
if (error.code === "ENOENT") return undefined;
|
|
260
336
|
throw error;
|
|
337
|
+
});
|
|
338
|
+
return currentStat ? { dev: currentStat.dev, ino: currentStat.ino } : undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function cleanupLockOwnershipError(lock: CleanupLock, stage: string): Promise<string | undefined> {
|
|
342
|
+
const currentIdentity = await getCleanupLockPathIdentity(lock.path);
|
|
343
|
+
if (currentIdentity && sameFileIdentity(lock.identity, currentIdentity)) return undefined;
|
|
344
|
+
return `cleanup lock ownership lost before ${stage}; stopping cleanup.`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function tryAcquireCleanupLock(root: string): Promise<CleanupLock | undefined> {
|
|
348
|
+
const lockPath = path.join(root, CLEANUP_LOCK_FILE);
|
|
349
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
350
|
+
const owner: CleanupLockOwner = {
|
|
351
|
+
pid: process.pid,
|
|
352
|
+
hostname: os.hostname(),
|
|
353
|
+
token: randomUUID(),
|
|
354
|
+
acquiredAt: new Date().toISOString(),
|
|
355
|
+
};
|
|
356
|
+
try {
|
|
357
|
+
const handle = await fs.open(lockPath, "wx");
|
|
358
|
+
const identity = await handle.stat();
|
|
359
|
+
try {
|
|
360
|
+
await handle.writeFile(JSON.stringify(owner), "utf8");
|
|
361
|
+
await handle.sync();
|
|
362
|
+
return { handle, path: lockPath, owner, identity };
|
|
363
|
+
} catch (error) {
|
|
364
|
+
await handle.close().catch(() => undefined);
|
|
365
|
+
await removeLockPathIfOwned(lockPath, identity);
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let existing: FileHandle | undefined;
|
|
373
|
+
try {
|
|
374
|
+
existing = await fs.open(lockPath, "r");
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const stat = await existing.stat();
|
|
381
|
+
const recordedOwner = parseCleanupLockOwner(await existing.readFile("utf8"));
|
|
382
|
+
const now = Date.now();
|
|
383
|
+
const ageMs = Math.max(0, now - stat.mtimeMs);
|
|
384
|
+
if (!recordedOwner) {
|
|
385
|
+
if (ageMs <= CLEANUP_LOCK_STALE_MS) return undefined;
|
|
386
|
+
} else {
|
|
387
|
+
const leaseExpired = isCleanupLockLeaseExpired(recordedOwner, now);
|
|
388
|
+
if (isRecordedOwnerAlive(recordedOwner) && !leaseExpired) return undefined;
|
|
389
|
+
if (!leaseExpired && ageMs <= CLEANUP_LOCK_STALE_MS) return undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const stalePath = path.join(root, `${CLEANUP_LOCK_FILE}.stale-${process.pid}-${randomUUID()}`);
|
|
393
|
+
try {
|
|
394
|
+
await fs.rename(lockPath, stalePath);
|
|
395
|
+
} catch (renameError) {
|
|
396
|
+
if ((renameError as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
397
|
+
throw renameError;
|
|
398
|
+
}
|
|
399
|
+
const movedStat = await fs.stat(stalePath).catch(() => undefined);
|
|
400
|
+
if (!movedStat || !sameFileIdentity(stat, movedStat)) {
|
|
401
|
+
await restoreMovedLock(stalePath, lockPath);
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
await fs.rm(stalePath, { force: true });
|
|
405
|
+
} finally {
|
|
406
|
+
await existing.close().catch(() => undefined);
|
|
407
|
+
}
|
|
261
408
|
}
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
262
411
|
|
|
412
|
+
async function heartbeatCleanupLock(lock: CleanupLock): Promise<void> {
|
|
413
|
+
const now = new Date();
|
|
414
|
+
await lock.handle.utimes(now, now);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function releaseCleanupLock(lock: CleanupLock): Promise<void> {
|
|
418
|
+
await lock.handle.close().catch(() => undefined);
|
|
419
|
+
const currentStat = await fs.stat(lock.path).catch(() => undefined);
|
|
420
|
+
if (!currentStat || !sameFileIdentity(lock.identity, currentStat)) return;
|
|
421
|
+
const releasePath = `${lock.path}.release-${process.pid}-${lock.owner.token}`;
|
|
263
422
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (!isInside(root, repoDir)) continue;
|
|
423
|
+
await fs.rename(lock.path, releasePath);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
const movedStat = await fs.stat(releasePath).catch(() => undefined);
|
|
429
|
+
if (!movedStat || !sameFileIdentity(lock.identity, movedStat)) {
|
|
430
|
+
await restoreMovedLock(releasePath, lock.path);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
await fs.rm(releasePath, { force: true });
|
|
434
|
+
}
|
|
277
435
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
436
|
+
async function cleanupExpiredRepos(root: string, now: number, lock: CleanupLock): Promise<{ deleted: number; errors: string[] }> {
|
|
437
|
+
const errors: string[] = [];
|
|
438
|
+
let deleted = 0;
|
|
439
|
+
const ownershipBeforeScan = await cleanupLockOwnershipError(lock, "cache scan");
|
|
440
|
+
if (ownershipBeforeScan) return { deleted, errors: [ownershipBeforeScan] };
|
|
441
|
+
|
|
442
|
+
const hosts = await safeReadDir(root);
|
|
443
|
+
for (const host of hosts) {
|
|
444
|
+
if (!host.isDirectory()) continue;
|
|
445
|
+
const hostDir = path.join(root, host.name);
|
|
446
|
+
const owners = await safeReadDir(hostDir);
|
|
447
|
+
for (const owner of owners) {
|
|
448
|
+
if (!owner.isDirectory()) continue;
|
|
449
|
+
const ownerDir = path.join(hostDir, owner.name);
|
|
450
|
+
const repos = await safeReadDir(ownerDir);
|
|
451
|
+
for (const repo of repos) {
|
|
452
|
+
if (!repo.isDirectory()) continue;
|
|
453
|
+
const repoDir = path.join(ownerDir, repo.name);
|
|
454
|
+
if (!isInside(root, repoDir)) continue;
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
if (!(await isManagedRepoCache(repoDir))) continue;
|
|
458
|
+
const lastUsedAt = await getRepoLastUsedAt(repoDir);
|
|
459
|
+
if (lastUsedAt === undefined || now - lastUsedAt <= CACHE_TTL_MS) continue;
|
|
460
|
+
const ownershipBeforeDelete = await cleanupLockOwnershipError(lock, `deleting ${repoDir}`);
|
|
461
|
+
if (ownershipBeforeDelete) return { deleted, errors: [...errors, ownershipBeforeDelete] };
|
|
462
|
+
await fs.rm(repoDir, { recursive: true, force: true });
|
|
463
|
+
deleted += 1;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
466
|
+
errors.push(`${repoDir}: ${message}`);
|
|
288
467
|
}
|
|
289
468
|
}
|
|
290
469
|
}
|
|
470
|
+
}
|
|
291
471
|
|
|
292
|
-
|
|
472
|
+
return { deleted, errors };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function cleanupExpiredCache(cacheRoot: string): Promise<{ deleted: number; errors: string[] }> {
|
|
476
|
+
const now = Date.now();
|
|
477
|
+
const root = path.resolve(cacheRoot);
|
|
478
|
+
let lock: CleanupLock | undefined;
|
|
479
|
+
let heartbeat: NodeJS.Timeout | undefined;
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
lock = await tryAcquireCleanupLock(root);
|
|
483
|
+
if (!lock) return { deleted: 0, errors: [] };
|
|
484
|
+
heartbeat = setInterval(() => {
|
|
485
|
+
if (lock) void heartbeatCleanupLock(lock).catch(() => undefined);
|
|
486
|
+
}, CLEANUP_LOCK_HEARTBEAT_MS);
|
|
487
|
+
heartbeat.unref?.();
|
|
488
|
+
return await cleanupExpiredRepos(root, now, lock);
|
|
293
489
|
} finally {
|
|
294
|
-
|
|
295
|
-
|
|
490
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
491
|
+
if (lock) await releaseCleanupLock(lock).catch(() => undefined);
|
|
296
492
|
}
|
|
297
493
|
}
|
|
298
494
|
|
|
@@ -728,12 +924,44 @@ function buildUserPrompt(query: string, repos: string[], owners: string[], maxSe
|
|
|
728
924
|
return `Task: locate and cite exact GitHub code locations that answer the query.\n\nQuery: ${query}\nRepository filters: ${repos.length ? repos.join(", ") : "(none)"}\nOwner filters: ${owners.length ? owners.join(", ") : "(none)"}\nMax search results per gh search call: ${maxSearchResults}\nLocal checkout cache: ${cache.mode === "enabled" ? `enabled at ${cache.root}` : "disabled"}\nCache decision: ${cache.decisionReason}\n\nRespond directly with concise, citation-heavy findings. Always pass --limit ${maxSearchResults} to gh search code unless a narrower command is clearly better.`;
|
|
729
925
|
}
|
|
730
926
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
927
|
+
function createEmptyUsage(): Usage {
|
|
928
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function addUsage(total: Usage, usage: Usage): void {
|
|
932
|
+
total.input += usage.input || 0;
|
|
933
|
+
total.output += usage.output || 0;
|
|
934
|
+
total.cacheRead += usage.cacheRead || 0;
|
|
935
|
+
total.cacheWrite += usage.cacheWrite || 0;
|
|
936
|
+
if (usage.cacheWrite1h !== undefined) total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
|
|
937
|
+
if (usage.reasoning !== undefined) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
|
|
938
|
+
total.totalTokens += usage.totalTokens || 0;
|
|
939
|
+
total.cost.input += usage.cost?.input || 0;
|
|
940
|
+
total.cost.output += usage.cost?.output || 0;
|
|
941
|
+
total.cost.cacheRead += usage.cost?.cacheRead || 0;
|
|
942
|
+
total.cost.cacheWrite += usage.cost?.cacheWrite || 0;
|
|
943
|
+
total.cost.total += usage.cost?.total || 0;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
type AssistantLikeMessage = { role?: string; content?: unknown; stopReason?: unknown; errorMessage?: unknown; usage?: unknown };
|
|
947
|
+
|
|
948
|
+
function addAssistantMessageUsage(total: Usage, message: unknown): void {
|
|
949
|
+
const assistant = message as AssistantLikeMessage;
|
|
950
|
+
if (assistant?.role !== "assistant" || !assistant.usage || typeof assistant.usage !== "object") return;
|
|
951
|
+
addUsage(total, assistant.usage as Usage);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function aggregateAssistantUsage(messages: unknown[]): Usage {
|
|
955
|
+
const total = createEmptyUsage();
|
|
956
|
+
for (const message of messages) addAssistantMessageUsage(total, message);
|
|
957
|
+
return total;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function addSessionEventUsage(total: Usage, event: unknown): void {
|
|
961
|
+
const sessionEvent = event as { type?: string; message?: unknown; result?: { usage?: Usage } };
|
|
962
|
+
if (sessionEvent?.type === "message_end") return void addAssistantMessageUsage(total, sessionEvent.message);
|
|
963
|
+
if (sessionEvent?.type === "compaction_end" && sessionEvent.result?.usage) addUsage(total, sessionEvent.result.usage);
|
|
964
|
+
}
|
|
737
965
|
|
|
738
966
|
function getLastAssistantMessage(messages: unknown[]): AssistantLikeMessage | undefined {
|
|
739
967
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -743,17 +971,69 @@ function getLastAssistantMessage(messages: unknown[]): AssistantLikeMessage | un
|
|
|
743
971
|
return undefined;
|
|
744
972
|
}
|
|
745
973
|
|
|
746
|
-
function
|
|
974
|
+
function inspectFinalAssistant(messages: unknown[]):
|
|
975
|
+
| { ok: true; answer: string; message: AssistantLikeMessage; stopReason?: string }
|
|
976
|
+
| { ok: false; reason: string; message?: AssistantLikeMessage; stopReason?: string; errorMessage?: string } {
|
|
747
977
|
const message = getLastAssistantMessage(messages);
|
|
748
|
-
if (!message
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
978
|
+
if (!message) return { ok: false, reason: "the internal subagent produced no assistant message" };
|
|
979
|
+
const stopReason = typeof message.stopReason === "string" ? message.stopReason.trim() : "";
|
|
980
|
+
const errorMessage = typeof message.errorMessage === "string" ? message.errorMessage.trim() : "";
|
|
981
|
+
const answer = Array.isArray(message.content)
|
|
982
|
+
? message.content
|
|
983
|
+
.map((part) => (part && typeof part === "object" && (part as { type?: string }).type === "text" && typeof (part as { text?: unknown }).text === "string"
|
|
984
|
+
? (part as { text: string }).text
|
|
985
|
+
: ""))
|
|
986
|
+
.join("")
|
|
987
|
+
.trim()
|
|
988
|
+
: "";
|
|
989
|
+
if (stopReason === "error") {
|
|
990
|
+
return {
|
|
991
|
+
ok: false,
|
|
992
|
+
reason: `the internal subagent stopped with an error: ${errorMessage || "provider/model error"}`,
|
|
993
|
+
message,
|
|
994
|
+
stopReason,
|
|
995
|
+
errorMessage: errorMessage || undefined,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (stopReason === "aborted") {
|
|
999
|
+
return { ok: false, reason: "the internal subagent was aborted before producing an answer", message, stopReason };
|
|
755
1000
|
}
|
|
756
|
-
|
|
1001
|
+
if (answer) return { ok: true, answer, message, stopReason: stopReason || undefined };
|
|
1002
|
+
return { ok: false, reason: describeNoAnswerReason(message, 0), message, stopReason: stopReason || undefined };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function classifyRunFailure(error: unknown, callerAborted: boolean): { status: "aborted" | "error"; message: string; error?: string } {
|
|
1006
|
+
const message = callerAborted ? "Aborted" : error instanceof Error ? error.message : String(error);
|
|
1007
|
+
return { status: callerAborted ? "aborted" : "error", message, error: callerAborted ? undefined : message };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function resolveLibrarianTerminalOutcome(
|
|
1011
|
+
outcome: ReturnType<typeof inspectFinalAssistant> | undefined,
|
|
1012
|
+
callerAborted: boolean,
|
|
1013
|
+
turns: number,
|
|
1014
|
+
fallbackReason?: string,
|
|
1015
|
+
): { status: "done"; content: string } | { status: "aborted"; content: "Aborted" } | { status: "error"; reason: string } {
|
|
1016
|
+
if (callerAborted) return { status: "aborted", content: "Aborted" };
|
|
1017
|
+
if (outcome?.ok) return { status: "done", content: outcome.answer };
|
|
1018
|
+
const reason = fallbackReason ?? (outcome?.stopReason ? outcome.reason : describeNoAnswerReason(outcome?.message, turns));
|
|
1019
|
+
return { status: "error", reason };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function createLibrarianAbortController(abortChild: () => void, onCallerAbort: () => void) {
|
|
1023
|
+
let callerAborted = false;
|
|
1024
|
+
return {
|
|
1025
|
+
get callerAborted() {
|
|
1026
|
+
return callerAborted;
|
|
1027
|
+
},
|
|
1028
|
+
abortForCleanup() {
|
|
1029
|
+
abortChild();
|
|
1030
|
+
},
|
|
1031
|
+
abortFromCaller() {
|
|
1032
|
+
callerAborted = true;
|
|
1033
|
+
onCallerAbort();
|
|
1034
|
+
abortChild();
|
|
1035
|
+
},
|
|
1036
|
+
};
|
|
757
1037
|
}
|
|
758
1038
|
|
|
759
1039
|
function describeNoAnswerReason(message: AssistantLikeMessage | undefined, turns: number): string {
|
|
@@ -801,18 +1081,29 @@ function renderAnswer(details: LibrarianDetails): string {
|
|
|
801
1081
|
return details.status === "running" ? "(searching GitHub...)" : "(no output)";
|
|
802
1082
|
}
|
|
803
1083
|
|
|
804
|
-
function isAbortLikeError(error: unknown): boolean {
|
|
805
|
-
if (error && typeof error === "object" && (error as { name?: unknown }).name === "AbortError") return true;
|
|
806
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
807
|
-
return /aborted|cancelled|canceled/i.test(message);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
1084
|
export const __test__ = {
|
|
811
1085
|
buildLibrarianCandidates,
|
|
812
1086
|
findAvailableModel,
|
|
813
1087
|
isModelAvailabilityError,
|
|
814
1088
|
parseModelPreference,
|
|
815
1089
|
resolveThinkingLevel,
|
|
1090
|
+
aggregateAssistantUsage,
|
|
1091
|
+
addAssistantMessageUsage,
|
|
1092
|
+
addSessionEventUsage,
|
|
1093
|
+
inspectFinalAssistant,
|
|
1094
|
+
classifyRunFailure,
|
|
1095
|
+
resolveLibrarianTerminalOutcome,
|
|
1096
|
+
createLibrarianAbortController,
|
|
1097
|
+
cleanupExpiredCache,
|
|
1098
|
+
cleanupExpiredRepos,
|
|
1099
|
+
cleanupLockOwnershipError,
|
|
1100
|
+
tryAcquireCleanupLock,
|
|
1101
|
+
heartbeatCleanupLock,
|
|
1102
|
+
releaseCleanupLock,
|
|
1103
|
+
CLEANUP_LOCK_FILE,
|
|
1104
|
+
CLEANUP_LOCK_STALE_MS,
|
|
1105
|
+
CLEANUP_LOCK_MAX_LEASE_MS,
|
|
1106
|
+
isCleanupLockLeaseExpired,
|
|
816
1107
|
};
|
|
817
1108
|
|
|
818
1109
|
export default function librarianExtension(pi: ExtensionAPI) {
|
|
@@ -1080,25 +1371,27 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1080
1371
|
let unsubscribe: (() => void) | undefined;
|
|
1081
1372
|
let runTimeout: NodeJS.Timeout | undefined;
|
|
1082
1373
|
let abortListenerAdded = false;
|
|
1083
|
-
|
|
1374
|
+
const usage = createEmptyUsage();
|
|
1084
1375
|
|
|
1085
1376
|
const emit = (force = false) => {
|
|
1086
1377
|
void force;
|
|
1087
1378
|
onUpdate?.({ content: [{ type: "text", text: lastContent }], details });
|
|
1088
1379
|
};
|
|
1089
1380
|
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1381
|
+
const abortController = createLibrarianAbortController(
|
|
1382
|
+
() => void session?.abort(),
|
|
1383
|
+
() => {
|
|
1384
|
+
details.status = "aborted";
|
|
1385
|
+
details.endedAt = Date.now();
|
|
1386
|
+
lastContent = "Aborted";
|
|
1387
|
+
emit(true);
|
|
1388
|
+
},
|
|
1389
|
+
);
|
|
1390
|
+
const abortFromCaller = () => abortController.abortFromCaller();
|
|
1098
1391
|
|
|
1099
|
-
if (signal?.aborted)
|
|
1392
|
+
if (signal?.aborted) abortFromCaller();
|
|
1100
1393
|
if (signal && !signal.aborted) {
|
|
1101
|
-
signal.addEventListener("abort",
|
|
1394
|
+
signal.addEventListener("abort", abortFromCaller);
|
|
1102
1395
|
abortListenerAdded = true;
|
|
1103
1396
|
}
|
|
1104
1397
|
|
|
@@ -1137,7 +1430,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1137
1430
|
|
|
1138
1431
|
const runAttempt = async (
|
|
1139
1432
|
candidate: LibrarianCandidate,
|
|
1140
|
-
): Promise<
|
|
1433
|
+
): Promise<ReturnType<typeof inspectFinalAssistant>> => {
|
|
1141
1434
|
details.model = candidate.details;
|
|
1142
1435
|
details.turns = 0;
|
|
1143
1436
|
details.toolCalls = [];
|
|
@@ -1145,7 +1438,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1145
1438
|
|
|
1146
1439
|
const created = await createAgentSession({
|
|
1147
1440
|
cwd: workspace,
|
|
1148
|
-
|
|
1441
|
+
...getModelRuntimeOption(ctx),
|
|
1149
1442
|
resourceLoader,
|
|
1150
1443
|
sessionManager: SessionManager.inMemory(workspace),
|
|
1151
1444
|
model: candidate.model,
|
|
@@ -1154,7 +1447,9 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1154
1447
|
});
|
|
1155
1448
|
|
|
1156
1449
|
session = created.session as typeof session;
|
|
1450
|
+
if (abortController.callerAborted) abortController.abortForCleanup();
|
|
1157
1451
|
unsubscribe = (created.session as any).subscribe((event: any) => {
|
|
1452
|
+
addSessionEventUsage(usage, event);
|
|
1158
1453
|
switch (event.type) {
|
|
1159
1454
|
case "turn_end":
|
|
1160
1455
|
details.turns += 1;
|
|
@@ -1185,22 +1480,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1185
1480
|
});
|
|
1186
1481
|
|
|
1187
1482
|
try {
|
|
1188
|
-
if (!
|
|
1483
|
+
if (!abortController.callerAborted) {
|
|
1189
1484
|
const promptPromise = created.session.prompt(buildUserPrompt(query, repos, owners, maxSearchResults, details.cache), {
|
|
1190
1485
|
expandPromptTemplates: false,
|
|
1191
1486
|
});
|
|
1192
1487
|
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
1193
1488
|
runTimeout = setTimeout(() => {
|
|
1194
|
-
|
|
1489
|
+
abortController.abortForCleanup();
|
|
1195
1490
|
reject(new Error(`Librarian timed out after ${Math.round(MAX_RUN_MS / 1000)} seconds.`));
|
|
1196
1491
|
}, MAX_RUN_MS);
|
|
1197
1492
|
});
|
|
1198
1493
|
await Promise.race([promptPromise, timeoutPromise]);
|
|
1199
1494
|
}
|
|
1200
1495
|
|
|
1201
|
-
|
|
1202
|
-
const answer = session ? extractLastAssistantText(session.state.messages) : "";
|
|
1203
|
-
return { answer, lastAssistant };
|
|
1496
|
+
return inspectFinalAssistant(session?.state.messages ?? []);
|
|
1204
1497
|
} finally {
|
|
1205
1498
|
if (runTimeout) {
|
|
1206
1499
|
clearTimeout(runTimeout);
|
|
@@ -1213,57 +1506,48 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1213
1506
|
}
|
|
1214
1507
|
};
|
|
1215
1508
|
|
|
1216
|
-
let
|
|
1217
|
-
let lastAssistant: AssistantLikeMessage | undefined;
|
|
1509
|
+
let lastOutcome: ReturnType<typeof inspectFinalAssistant> | undefined;
|
|
1218
1510
|
let attemptErrorReason: string | undefined;
|
|
1219
1511
|
for (let index = 0; index < candidates.length; index++) {
|
|
1220
|
-
if (
|
|
1512
|
+
if (abortController.callerAborted) break;
|
|
1221
1513
|
const candidate =
|
|
1222
1514
|
index === 0 ? candidates[index] : withLibrarianFallbackReason(candidates[index], candidates[index - 1]);
|
|
1223
1515
|
|
|
1224
|
-
let attempt:
|
|
1516
|
+
let attempt: ReturnType<typeof inspectFinalAssistant>;
|
|
1225
1517
|
try {
|
|
1226
1518
|
attempt = await runAttempt(candidate);
|
|
1227
1519
|
} catch (error) {
|
|
1228
|
-
|
|
1229
|
-
if (aborted || isAbortLikeError(error)) throw error;
|
|
1520
|
+
if (abortController.callerAborted) throw error;
|
|
1230
1521
|
// Some providers throw (rather than carry the error on the assistant
|
|
1231
1522
|
// message) when a model is unavailable; treat that like a
|
|
1232
1523
|
// message-carried availability error and fall back to the next model.
|
|
1233
1524
|
const message = error instanceof Error ? error.message : String(error);
|
|
1234
1525
|
if (index < candidates.length - 1 && isModelAvailabilityError(message)) {
|
|
1235
|
-
|
|
1236
|
-
lastAssistant = undefined;
|
|
1526
|
+
lastOutcome = undefined;
|
|
1237
1527
|
attemptErrorReason = `the internal subagent stopped with an error: ${message}`;
|
|
1238
1528
|
continue;
|
|
1239
1529
|
}
|
|
1240
1530
|
throw error;
|
|
1241
1531
|
}
|
|
1242
1532
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
if (answer || aborted) break;
|
|
1533
|
+
lastOutcome = attempt;
|
|
1534
|
+
if (abortController.callerAborted || attempt.ok) break;
|
|
1246
1535
|
|
|
1247
|
-
attemptErrorReason = describeNoAnswerReason(
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const canFallBack = index < candidates.length - 1 && isModelAvailabilityError(errorMessage);
|
|
1536
|
+
attemptErrorReason = attempt.stopReason ? attempt.reason : describeNoAnswerReason(attempt.message, details.turns);
|
|
1537
|
+
const canFallBack =
|
|
1538
|
+
index < candidates.length - 1 &&
|
|
1539
|
+
attempt.stopReason === "error" &&
|
|
1540
|
+
isModelAvailabilityError(attempt.errorMessage);
|
|
1253
1541
|
if (!canFallBack) break;
|
|
1254
1542
|
}
|
|
1255
1543
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
details.status = "aborted";
|
|
1544
|
+
const terminal = resolveLibrarianTerminalOutcome(lastOutcome, abortController.callerAborted, details.turns, attemptErrorReason);
|
|
1545
|
+
details.status = terminal.status;
|
|
1546
|
+
if (terminal.status === "error") {
|
|
1547
|
+
lastContent = formatInternalFailure(details, terminal.reason);
|
|
1548
|
+
details.error = terminal.reason;
|
|
1262
1549
|
} else {
|
|
1263
|
-
|
|
1264
|
-
const reason = attemptErrorReason ?? describeNoAnswerReason(lastAssistant, details.turns);
|
|
1265
|
-
lastContent = formatInternalFailure(details, reason);
|
|
1266
|
-
details.error = reason;
|
|
1550
|
+
lastContent = terminal.content;
|
|
1267
1551
|
}
|
|
1268
1552
|
details.endedAt = Date.now();
|
|
1269
1553
|
emit(true);
|
|
@@ -1271,12 +1555,13 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1271
1555
|
return {
|
|
1272
1556
|
content: [{ type: "text", text: lastContent }],
|
|
1273
1557
|
details,
|
|
1558
|
+
usage,
|
|
1274
1559
|
};
|
|
1275
1560
|
} catch (error) {
|
|
1276
|
-
const
|
|
1277
|
-
const message =
|
|
1278
|
-
details.status =
|
|
1279
|
-
details.error =
|
|
1561
|
+
const failure = classifyRunFailure(error, abortController.callerAborted);
|
|
1562
|
+
const { message } = failure;
|
|
1563
|
+
details.status = failure.status;
|
|
1564
|
+
details.error = failure.error;
|
|
1280
1565
|
details.endedAt = Date.now();
|
|
1281
1566
|
lastContent = message;
|
|
1282
1567
|
emit(true);
|
|
@@ -1284,10 +1569,11 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1284
1569
|
return {
|
|
1285
1570
|
content: [{ type: "text", text: message }],
|
|
1286
1571
|
details,
|
|
1572
|
+
usage,
|
|
1287
1573
|
};
|
|
1288
1574
|
} finally {
|
|
1289
1575
|
if (runTimeout) clearTimeout(runTimeout);
|
|
1290
|
-
if (signal && abortListenerAdded) signal.removeEventListener("abort",
|
|
1576
|
+
if (signal && abortListenerAdded) signal.removeEventListener("abort", abortFromCaller);
|
|
1291
1577
|
unsubscribe?.();
|
|
1292
1578
|
session?.dispose();
|
|
1293
1579
|
}
|