pi-subagents 0.47.0 → 0.47.1
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/CHANGELOG.md +14 -0
- package/agents/reviewer.md +3 -4
- package/package.json +1 -1
- package/src/extension/config.ts +10 -0
- package/src/extension/index.ts +9 -11
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +102 -4
- package/src/runs/background/active-run-index.ts +42 -0
- package/src/runs/background/async-job-tracker.ts +279 -134
- package/src/runs/background/async-status.ts +27 -4
- package/src/runs/background/control-channel.ts +55 -17
- package/src/runs/background/result-watcher.ts +87 -6
- package/src/runs/background/run-status.ts +18 -0
- package/src/runs/background/scheduled-runs.ts +23 -0
- package/src/runs/background/stale-run-reconciler.ts +10 -3
- package/src/runs/background/subagent-runner.ts +21 -4
- package/src/runs/foreground/async-dismiss-action.ts +85 -0
- package/src/runs/foreground/async-steering-action.ts +6 -7
- package/src/runs/foreground/subagent-executor.ts +34 -8
- package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
- package/src/shared/artifacts.ts +1 -1
- package/src/shared/file-coalescer.ts +9 -0
- package/src/shared/types.ts +5 -1
- package/src/workflows/scripted-workflow.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,15 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.47.1] - 2026-08-12
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Honor configured artifact cleanup retention days and let `0` disable artifact cleanup. Thanks to @elecnix for #1012.
|
|
9
|
+
- Add a display-only dismiss action for reload-recovered running workflows without claiming or attempting to stop their work (#1010).
|
|
10
|
+
- Stop the bundled reviewer from inheriting chain-only plan/progress reads in ad-hoc review runs. Thanks to @Ostii for #1000.
|
|
11
|
+
- Remove mutation-capable tools from the bundled reviewer so read-only review lanes have a hard launch-time tool boundary (#1007).
|
|
12
|
+
- Show the requested child agent in workflow started trace entries. Thanks to @albertgwo for #1001.
|
|
13
|
+
|
|
5
14
|
## [0.47.0] - 2026-08-11
|
|
6
15
|
|
|
7
16
|
### Changed
|
|
17
|
+
- Avoid fully parsing stale cross-session result files during watcher recovery and reduce healthy watcher safety scans.
|
|
18
|
+
- Index active async runs so status restoration no longer scans all historical run directories.
|
|
19
|
+
- Refresh active async job state from filesystem events while reserving polling for slow liveness repair.
|
|
8
20
|
- Add optional strict model-scope enforcement that rejects inherited and fallback models outside the configured allowlist. Thanks to @antonioc-cl for #995.
|
|
9
21
|
- Trim legacy chain-control schema fields and guidance by default, saving 1,319 `o200k_base` tokens from the serialized default tool schema plus description versus `legacyChainControls: true`. Thanks to @tajquitgenius for #977.
|
|
10
22
|
- Move project-scoped pi-subagents storage from `.pi-subagents/` to `.pi/subagents/` for cleaner project roots. Thanks to @yceachan for #971.
|
|
11
23
|
- Clarify the accepted mission launch object contract for tool callers.
|
|
12
24
|
- Reduce repeated async status parsing, workflow trace projection, and constrained widget rendering work.
|
|
25
|
+
- Coalesce rapid running-status writes while keeping terminal and attention status changes durable immediately.
|
|
13
26
|
- Keep parsed async statuses cached beyond 50 runs while preserving per-read freshness checks. Thanks to @bcanvural for #982.
|
|
27
|
+
- Prefer native control inbox watchers over per-process 250 ms polling, with polling retained as a fallback.
|
|
14
28
|
|
|
15
29
|
### Fixed
|
|
16
30
|
- Omit missing configured read files from child task instructions.
|
package/agents/reviewer.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Versatile review specialist for code diffs, plans, proposed solutions, codebase health, and PR/issue validation
|
|
4
|
-
tools: read, grep, find, ls,
|
|
4
|
+
tools: read, grep, find, ls, intercom
|
|
5
5
|
thinking: high
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: true
|
|
8
8
|
inheritSkills: false
|
|
9
|
-
defaultReads: plan.md, progress.md
|
|
10
9
|
---
|
|
11
10
|
|
|
12
11
|
You are a disciplined review subagent. Your job is to inspect, evaluate, and report findings with evidence. You do not guess; you verify from the code, tests, docs, or requirements.
|
|
@@ -51,9 +50,9 @@ Review a PR or issue by understanding the context, then verifying:
|
|
|
51
50
|
- Tests and docs are updated as needed.
|
|
52
51
|
|
|
53
52
|
## Working rules
|
|
54
|
-
- Read the plan
|
|
53
|
+
- Read the relevant files first. Read plan and progress when the task supplies them.
|
|
55
54
|
- Repo-local `progress.md` files are allowed scratch/memory files. Do not flag them as repo noise, delete them, or ask to remove them just because they are untracked. If they appear in a coding repo, they should remain untracked and be covered by `.gitignore`.
|
|
56
|
-
-
|
|
55
|
+
- Do not use shell commands or write files. Report any test or Git command that a supervisor must run.
|
|
57
56
|
- Do not invent issues. Only report problems you can justify from evidence.
|
|
58
57
|
- Prefer small corrective edits over broad rewrites.
|
|
59
58
|
- If everything looks good, say so plainly.
|
package/package.json
CHANGED
package/src/extension/config.ts
CHANGED
|
@@ -37,6 +37,15 @@ function validateFleetKeybindingsConfig(value: unknown): void {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function validateArtifactConfig(value: unknown): void {
|
|
41
|
+
if (value === undefined) return;
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.artifactConfig must be a JSON object");
|
|
43
|
+
const cleanupDays = (value as Record<string, unknown>).cleanupDays;
|
|
44
|
+
if (cleanupDays !== undefined && (typeof cleanupDays !== "number" || !Number.isInteger(cleanupDays) || cleanupDays < 0)) {
|
|
45
|
+
throw new Error("config.artifactConfig.cleanupDays must be a non-negative integer");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
function validateConfig(config: Record<string, unknown>): void {
|
|
41
50
|
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
42
51
|
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
@@ -49,6 +58,7 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
49
58
|
validatePermissionConfig(config.permissions);
|
|
50
59
|
validateScheduledRunsConfig(config.scheduledRuns);
|
|
51
60
|
validateFleetKeybindingsConfig(config.fleetKeybindings);
|
|
61
|
+
validateArtifactConfig(config.artifactConfig);
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export function getConfigPath(): string {
|
package/src/extension/index.ts
CHANGED
|
@@ -366,7 +366,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
366
366
|
const asyncWidgetEnabled = config.asyncWidget !== false;
|
|
367
367
|
const summaryInlineToolDisplay = config.inlineToolDisplay === "summary";
|
|
368
368
|
const tempArtifactsDir = getArtifactsDir(null);
|
|
369
|
-
|
|
369
|
+
const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
|
|
370
|
+
cleanupAllArtifactDirs(artifactCleanupDays);
|
|
370
371
|
|
|
371
372
|
const state: SubagentState = {
|
|
372
373
|
baseCwd: "",
|
|
@@ -430,6 +431,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
430
431
|
},
|
|
431
432
|
resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
|
|
432
433
|
});
|
|
434
|
+
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
|
|
435
|
+
widgetEnabled: asyncWidgetEnabled,
|
|
436
|
+
});
|
|
433
437
|
const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
|
|
434
438
|
pi,
|
|
435
439
|
state,
|
|
@@ -438,6 +442,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
438
442
|
{
|
|
439
443
|
notifier: completionNotifier,
|
|
440
444
|
observeCompletion: (result) => scheduledRunManager.handleAsyncCompletion(result),
|
|
445
|
+
observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
|
|
441
446
|
deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
|
|
442
447
|
},
|
|
443
448
|
);
|
|
@@ -451,16 +456,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
451
456
|
supervisorChannel.dispose();
|
|
452
457
|
waitSubscriptionManager.dispose();
|
|
453
458
|
fleetStatus?.dispose();
|
|
454
|
-
|
|
455
|
-
clearInterval(state.poller);
|
|
456
|
-
state.poller = null;
|
|
457
|
-
}
|
|
459
|
+
disposeAsyncJobTracker();
|
|
458
460
|
};
|
|
459
461
|
globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
|
|
460
462
|
|
|
461
|
-
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state, DIRS.async, {
|
|
462
|
-
widgetEnabled: asyncWidgetEnabled,
|
|
463
|
-
});
|
|
464
463
|
const executor = createSubagentExecutor({
|
|
465
464
|
pi,
|
|
466
465
|
state,
|
|
@@ -708,7 +707,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
708
707
|
try {
|
|
709
708
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
710
709
|
if (sessionFile) {
|
|
711
|
-
cleanupOldArtifacts(getArtifactsDir(sessionFile),
|
|
710
|
+
cleanupOldArtifacts(getArtifactsDir(sessionFile), artifactCleanupDays);
|
|
712
711
|
}
|
|
713
712
|
} catch {
|
|
714
713
|
// Cleanup failures should not block session lifecycle events.
|
|
@@ -823,8 +822,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
823
822
|
delete globalStore[eventUnsubscribeStoreKey];
|
|
824
823
|
}
|
|
825
824
|
scheduledRunManager.stop();
|
|
826
|
-
|
|
827
|
-
state.poller = null;
|
|
825
|
+
disposeAsyncJobTracker();
|
|
828
826
|
for (const timer of state.cleanupTimers.values()) {
|
|
829
827
|
clearTimeout(timer);
|
|
830
828
|
}
|
package/src/extension/rpc.ts
CHANGED
|
@@ -490,6 +490,10 @@ function stopAsyncRun(
|
|
|
490
490
|
throw new SubagentRpcError("not_found", `Async run '${initialRunId}' was not found in the active session.`);
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
if (initialStatus.mode === "workflow" && initialStatus.state === "running") {
|
|
494
|
+
throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; reload recovery cannot stop it safely.`);
|
|
495
|
+
}
|
|
496
|
+
|
|
493
497
|
let status;
|
|
494
498
|
try {
|
|
495
499
|
status = reconcileAsyncRun(location.asyncDir, { resultsDir, kill: options.kill, now: options.now }).status;
|
package/src/extension/schemas.ts
CHANGED
|
@@ -263,10 +263,10 @@ const SubagentParamProperties = {
|
|
|
263
263
|
})),
|
|
264
264
|
name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
|
|
265
265
|
id: Type.Optional(Type.String({
|
|
266
|
-
description: "Run id or prefix for status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, mission.attach-run, or the decision id for mission.resolve-decision."
|
|
266
|
+
description: "Run id or prefix for status, interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, mission.attach-run, or the decision id for mission.resolve-decision."
|
|
267
267
|
})),
|
|
268
268
|
runId: Type.Optional(Type.String({
|
|
269
|
-
description: "Target run ID for interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
|
|
269
|
+
description: "Target run ID for interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
|
|
270
270
|
})),
|
|
271
271
|
dir: Type.Optional(Type.String({
|
|
272
272
|
description: "Async run directory for action='status', action='stop', action='resume', or action='steer'."
|
|
@@ -22,6 +22,7 @@ export const NATIVE_SUPERVISOR_TOOL_NAME = "subagent_supervisor";
|
|
|
22
22
|
const MAX_MESSAGE_BYTES = 64 * 1024;
|
|
23
23
|
const DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
24
24
|
const CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);
|
|
25
|
+
const CHANNEL_SAFETY_POLL_MS = 5000;
|
|
25
26
|
const STALE_EMPTY_CHANNEL_AGE_MS = 60 * 1000;
|
|
26
27
|
const STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
27
28
|
|
|
@@ -69,6 +70,13 @@ interface IntercomParams {
|
|
|
69
70
|
replyTo?: string;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string>) => fs.FSWatcher;
|
|
74
|
+
|
|
75
|
+
interface NativeSupervisorChannelDeps {
|
|
76
|
+
platform?: NodeJS.Platform;
|
|
77
|
+
watch?: SupervisorWatch;
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
const ContactSupervisorParamsSchema = Type.Object({
|
|
73
81
|
reason: Type.String({ enum: ["need_decision", "interview_request", "progress_update"] }),
|
|
74
82
|
message: Type.Optional(Type.String()),
|
|
@@ -626,10 +634,16 @@ function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>,
|
|
|
626
634
|
};
|
|
627
635
|
}
|
|
628
636
|
|
|
629
|
-
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
|
|
637
|
+
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
|
|
638
|
+
const watch = deps.watch ?? fs.watch;
|
|
630
639
|
const pending = new Map<string, PendingSupervisorRequest>();
|
|
631
640
|
const seenFiles = new Set<string>();
|
|
641
|
+
const requestWatchers = new Map<string, fs.FSWatcher>();
|
|
642
|
+
let rootWatcher: fs.FSWatcher | undefined;
|
|
632
643
|
let poller: ReturnType<typeof setInterval> | undefined;
|
|
644
|
+
let safetyPoller: ReturnType<typeof setInterval> | undefined;
|
|
645
|
+
let deferredWatcherRefresh: ReturnType<typeof setImmediate> | undefined;
|
|
646
|
+
let started = false;
|
|
633
647
|
let lastStaleCleanupAt = 0;
|
|
634
648
|
|
|
635
649
|
const registerParentTools = (): void => {
|
|
@@ -696,17 +710,101 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
696
710
|
}
|
|
697
711
|
};
|
|
698
712
|
|
|
713
|
+
const startPolling = (): void => {
|
|
714
|
+
if (poller) return;
|
|
715
|
+
poller = setInterval(poll, CHANNEL_POLL_MS);
|
|
716
|
+
poller.unref?.();
|
|
717
|
+
};
|
|
718
|
+
const startSafetyPolling = (): void => {
|
|
719
|
+
if (safetyPoller) return;
|
|
720
|
+
safetyPoller = setInterval(() => {
|
|
721
|
+
watchExistingRequestDirs();
|
|
722
|
+
poll();
|
|
723
|
+
}, CHANNEL_SAFETY_POLL_MS);
|
|
724
|
+
safetyPoller.unref?.();
|
|
725
|
+
};
|
|
726
|
+
const watchRequestDir = (requestsDir: string): void => {
|
|
727
|
+
if (requestWatchers.has(requestsDir)) return;
|
|
728
|
+
try {
|
|
729
|
+
const watcher = watch(requestsDir, () => poll());
|
|
730
|
+
watcher.on("error", () => {
|
|
731
|
+
try { watcher.close(); } catch {}
|
|
732
|
+
requestWatchers.delete(requestsDir);
|
|
733
|
+
startPolling();
|
|
734
|
+
});
|
|
735
|
+
watcher.unref?.();
|
|
736
|
+
requestWatchers.set(requestsDir, watcher);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") startPolling();
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const watchExistingRequestDirs = (): void => {
|
|
742
|
+
let channelEntries: fs.Dirent[];
|
|
743
|
+
try {
|
|
744
|
+
channelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true });
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
747
|
+
startPolling();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
for (const entry of channelEntries) {
|
|
751
|
+
if (entry.isDirectory()) watchRequestDir(path.join(SUPERVISOR_CHANNEL_ROOT, entry.name, REQUESTS_DIR));
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const scheduleWatcherRefresh = (): void => {
|
|
755
|
+
if (deferredWatcherRefresh) return;
|
|
756
|
+
deferredWatcherRefresh = setImmediate(() => {
|
|
757
|
+
deferredWatcherRefresh = undefined;
|
|
758
|
+
if (!started) return;
|
|
759
|
+
watchExistingRequestDirs();
|
|
760
|
+
poll();
|
|
761
|
+
});
|
|
762
|
+
deferredWatcherRefresh.unref?.();
|
|
763
|
+
};
|
|
764
|
+
|
|
699
765
|
return {
|
|
700
766
|
start: () => {
|
|
701
|
-
if (
|
|
767
|
+
if (started) return;
|
|
768
|
+
started = true;
|
|
702
769
|
registerParentTools();
|
|
703
770
|
poll();
|
|
704
|
-
|
|
705
|
-
|
|
771
|
+
try {
|
|
772
|
+
fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
|
|
773
|
+
if ((deps.platform ?? process.platform) === "win32") {
|
|
774
|
+
startPolling();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
watchExistingRequestDirs();
|
|
778
|
+
rootWatcher = watch(SUPERVISOR_CHANNEL_ROOT, () => {
|
|
779
|
+
watchExistingRequestDirs();
|
|
780
|
+
poll();
|
|
781
|
+
scheduleWatcherRefresh();
|
|
782
|
+
});
|
|
783
|
+
rootWatcher.on("error", startPolling);
|
|
784
|
+
startSafetyPolling();
|
|
785
|
+
scheduleWatcherRefresh();
|
|
786
|
+
} catch {
|
|
787
|
+
startPolling();
|
|
788
|
+
}
|
|
706
789
|
},
|
|
707
790
|
dispose: () => {
|
|
791
|
+
started = false;
|
|
792
|
+
try {
|
|
793
|
+
rootWatcher?.close();
|
|
794
|
+
} catch {
|
|
795
|
+
// Best effort during shutdown.
|
|
796
|
+
}
|
|
797
|
+
rootWatcher = undefined;
|
|
798
|
+
for (const watcher of requestWatchers.values()) {
|
|
799
|
+
try { watcher.close(); } catch {}
|
|
800
|
+
}
|
|
801
|
+
requestWatchers.clear();
|
|
708
802
|
if (poller) clearInterval(poller);
|
|
709
803
|
poller = undefined;
|
|
804
|
+
if (safetyPoller) clearInterval(safetyPoller);
|
|
805
|
+
safetyPoller = undefined;
|
|
806
|
+
if (deferredWatcherRefresh) clearImmediate(deferredWatcherRefresh);
|
|
807
|
+
deferredWatcherRefresh = undefined;
|
|
710
808
|
pending.clear();
|
|
711
809
|
seenFiles.clear();
|
|
712
810
|
},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AsyncStatus } from "../../shared/types.ts";
|
|
4
|
+
|
|
5
|
+
export const ACTIVE_RUN_INDEX_DIR = ".active-runs";
|
|
6
|
+
|
|
7
|
+
function indexDir(asyncDirRoot: string): string {
|
|
8
|
+
return path.join(asyncDirRoot, ACTIVE_RUN_INDEX_DIR);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function markerPath(asyncDir: string): string {
|
|
12
|
+
return path.join(indexDir(path.dirname(asyncDir)), path.basename(asyncDir));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isActiveAsyncState(state: AsyncStatus["state"]): boolean {
|
|
16
|
+
return state === "queued" || state === "running";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"]): void {
|
|
20
|
+
const marker = markerPath(asyncDir);
|
|
21
|
+
if (isActiveAsyncState(state)) {
|
|
22
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
23
|
+
fs.writeFileSync(marker, "", { flag: "a" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
fs.rmSync(marker);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readActiveRunIndex(asyncDirRoot: string): string[] | undefined {
|
|
34
|
+
try {
|
|
35
|
+
return fs.readdirSync(indexDir(asyncDirRoot), { withFileTypes: true })
|
|
36
|
+
.filter((entry) => entry.isFile())
|
|
37
|
+
.map((entry) => entry.name);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|