pi-crew 0.6.1 → 0.6.4
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 +194 -0
- package/README.md +81 -33
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/docs/ui-optimization-plan.md +447 -0
- package/package.json +2 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +213 -35
- package/src/extension/registration/brief-tool-overrides.ts +400 -0
- package/src/extension/registration/commands.ts +27 -2
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +9 -4
- package/src/extension/registration/team-tool.ts +17 -10
- package/src/extension/registration/viewers.ts +2 -2
- package/src/extension/team-tool/api.ts +3 -3
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +64 -14
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +173 -46
- package/src/hooks/registry.ts +77 -13
- package/src/hooks/types.ts +9 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/async-runner.ts +35 -7
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +9 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +164 -37
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +2 -0
- package/src/runtime/diagnostic-export.ts +1 -1
- package/src/runtime/dynamic-script-runner.ts +10 -7
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +1 -1
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +17 -2
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/skill-effectiveness.ts +105 -62
- package/src/runtime/skill-instructions.ts +110 -32
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +251 -57
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +92 -70
- package/src/runtime/team-runner.ts +186 -20
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +254 -70
- package/src/state/health-store.ts +71 -0
- package/src/state/locks.ts +102 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/session-state-map.ts +51 -0
- package/src/state/state-store.ts +330 -43
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +2 -2
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/ui/status-colors.ts +5 -1
- package/src/ui/theme-adapter.ts +80 -1
- package/src/ui/tool-progress-formatter.ts +9 -5
- package/src/ui/tool-render.ts +4 -0
- package/src/ui/tool-renderers/brief-mode.ts +207 -0
- package/src/ui/tool-renderers/index.ts +627 -0
- package/src/ui/widget/index.ts +224 -0
- package/src/ui/widget/widget-formatters.ts +148 -0
- package/src/ui/widget/widget-model.ts +90 -0
- package/src/ui/widget/widget-renderer.ts +130 -0
- package/src/ui/widget/widget-types.ts +37 -0
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/guards.ts +110 -0
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +281 -10
- package/src/worktree/cleanup.ts +112 -24
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- package/test-integration-check.ts +4 -0
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
ExtensionAPI,
|
|
6
6
|
ExtensionContext,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { loadConfig } from "../config/config.ts";
|
|
8
|
+
import { asRecord, loadConfig } from "../config/config.ts";
|
|
9
9
|
import { applyCrewSettingsToConfig, loadCrewSettings } from "../runtime/settings-store.ts";
|
|
10
10
|
// 2.7: Lazy-load LiveRunSidebar — only constructed when the user actually opens
|
|
11
11
|
// a live run sidebar overlay. The class pulls in transcript-viewer and other
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
20
20
|
import { registerCleanupHandler } from "./crew-cleanup.ts";
|
|
21
21
|
import type { ScheduledJob } from "../runtime/scheduler.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { clearHooksScoped } from "../hooks/registry.ts";
|
|
23
23
|
import { uninstallCrewGlobalRegistry } from "./team-tool.ts";
|
|
24
24
|
import { notifyActiveRuns } from "./session-summary.ts";
|
|
25
25
|
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
type CrewWidgetState,
|
|
62
62
|
stopCrewWidget,
|
|
63
63
|
updateCrewWidget,
|
|
64
|
-
} from "../ui/
|
|
64
|
+
} from "../ui/widget/index.ts";
|
|
65
65
|
import { summarizeHeartbeats } from "../ui/heartbeat-aggregator.ts";
|
|
66
66
|
import {
|
|
67
67
|
requestRender,
|
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
projectCrewRoot,
|
|
88
88
|
userCrewRoot,
|
|
89
89
|
} from "../utils/paths.ts";
|
|
90
|
+
import { resolveContainedPath } from "../utils/safe-paths.ts";
|
|
90
91
|
import { resetTimings, time } from "../utils/timings.ts";
|
|
91
92
|
import {
|
|
92
93
|
type PiCrewRpcHandle,
|
|
@@ -99,6 +100,7 @@ import {
|
|
|
99
100
|
import { createJsonlSink, type NotificationSink } from "./notification-sink.ts";
|
|
100
101
|
import { runArtifactCleanup } from "./registration/artifact-cleanup.ts";
|
|
101
102
|
import { registerTeamCommands } from "./registration/commands.ts";
|
|
103
|
+
import { registerBriefToolOverrides } from "./registration/brief-tool-overrides.ts";
|
|
102
104
|
import { registerCompactionGuard } from "./registration/compaction-guard.ts";
|
|
103
105
|
import {
|
|
104
106
|
__test__subagentSpawnParams,
|
|
@@ -108,6 +110,7 @@ import {
|
|
|
108
110
|
import { registerSubagentTools } from "./registration/subagent-tools.ts";
|
|
109
111
|
import { registerTeamTool } from "./registration/team-tool.ts";
|
|
110
112
|
import { handleTeamTool } from "./team-tool.ts";
|
|
113
|
+
import { persistScheduledJobUpdate } from "./team-tool/handle-schedule.ts";
|
|
111
114
|
|
|
112
115
|
let _cachedOTLPExporter: typeof OTLPExporterType | undefined;
|
|
113
116
|
async function importOTLPExporter(): Promise<typeof OTLPExporterType> {
|
|
@@ -132,6 +135,8 @@ import {
|
|
|
132
135
|
} from "../runtime/crash-recovery.ts";
|
|
133
136
|
import { appendDeadletter } from "../runtime/deadletter.ts";
|
|
134
137
|
import { HeartbeatWatcher } from "../runtime/heartbeat-watcher.ts";
|
|
138
|
+
import { cleanupOrphanTempDirs, cleanupLegacyOrphanTempDirs } from "../runtime/pi-args.ts";
|
|
139
|
+
import { cleanupOrphanWorkers } from "../runtime/orphan-worker-registry.ts";
|
|
135
140
|
import { reconcileOrphanedTempWorkspaces } from "../runtime/stale-reconciler.ts";
|
|
136
141
|
|
|
137
142
|
let _cachedCrashRecovery:
|
|
@@ -183,10 +188,14 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
183
188
|
const disposeI18n = initI18n(pi);
|
|
184
189
|
resetTimings();
|
|
185
190
|
time("register:start");
|
|
186
|
-
const globalStore = globalThis as Record<string, unknown>;
|
|
187
|
-
const runtimeCleanupStoreKey = "__piCrewRuntimeCleanup";
|
|
191
|
+
const globalStore = globalThis as Record<string | symbol, unknown>;
|
|
192
|
+
const runtimeCleanupStoreKey = Symbol("__piCrewRuntimeCleanup");
|
|
188
193
|
const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
|
|
189
194
|
time("register:init");
|
|
195
|
+
// Best-effort cleanup of the previous runtime instance. Errors are logged but
|
|
196
|
+
// do not halt new registration — a failing cleanup from a prior instance is
|
|
197
|
+
// preferable to leaving pi-crew unregistered, and any stale state from the
|
|
198
|
+
// previous instance will be reconciled when the new instance initializes.
|
|
190
199
|
if (typeof previousRuntimeCleanup === "function") {
|
|
191
200
|
try {
|
|
192
201
|
previousRuntimeCleanup();
|
|
@@ -443,6 +452,34 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
443
452
|
cleanupOrphanedTempDirs:
|
|
444
453
|
config.reliability?.cleanupOrphanedTempDirs,
|
|
445
454
|
});
|
|
455
|
+
// Layer 4: also clean orphan temp dirs under
|
|
456
|
+
// ~/.pi/agent/pi-crew/tmp/ that the SIGKILL'd parent
|
|
457
|
+
// processes left behind. Catches anything Layers 1-3 missed.
|
|
458
|
+
const orphanResult = cleanupOrphanTempDirs();
|
|
459
|
+
if (orphanResult.cleaned > 0) {
|
|
460
|
+
notifyOperator({
|
|
461
|
+
id: `layer4_temp_cleanup_${Date.now()}`,
|
|
462
|
+
severity: "info",
|
|
463
|
+
source: "temp-cleanup",
|
|
464
|
+
title: `Layer 4: cleaned ${orphanResult.cleaned} orphan temp dir(s)`,
|
|
465
|
+
body: `~/.pi/agent/pi-crew/tmp/ orphans older than 24h removed (scanned ${orphanResult.scanned}, failed ${orphanResult.failed}).`,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// Layer 5: clean legacy /tmp/pi-crew-* prompt/task orphans
|
|
469
|
+
// from before commit 8ba270d moved temp dirs out of /tmp.
|
|
470
|
+
// The existing reconcileOrphanedTempWorkspaces only cleans
|
|
471
|
+
// dirs containing .crew/state/runs/ (run-state dirs), so
|
|
472
|
+
// prompt/task orphans are never touched by Layer 3.
|
|
473
|
+
const legacyResult = cleanupLegacyOrphanTempDirs();
|
|
474
|
+
if (legacyResult.cleaned > 0) {
|
|
475
|
+
notifyOperator({
|
|
476
|
+
id: `layer5_legacy_temp_cleanup_${Date.now()}`,
|
|
477
|
+
severity: "info",
|
|
478
|
+
source: "temp-cleanup",
|
|
479
|
+
title: `Layer 5: cleaned ${legacyResult.cleaned} legacy /tmp/pi-crew-* orphan(s)`,
|
|
480
|
+
body: `Pre-fix /tmp/pi-crew-* prompt/task orphans (no .crew/state/runs/, >24h) removed (scanned ${legacyResult.scanned}, failed ${legacyResult.failed}).`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
446
483
|
} catch (error) {
|
|
447
484
|
logInternalError("register.tempAutoRepair", error);
|
|
448
485
|
}
|
|
@@ -478,13 +515,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
478
515
|
);
|
|
479
516
|
}
|
|
480
517
|
};
|
|
481
|
-
const autoRecoveryLast = new Map<string, number>();
|
|
518
|
+
const autoRecoveryLast = new Map<string, { insertedAt: number; lastAccessAt: number }>();
|
|
482
519
|
// FIX (Round 22, defensive cap): Bound the cooldown-gate Map. Each run
|
|
483
520
|
// contributes up to 4 keys (one per maybeNotifyHealth kind). Without a cap,
|
|
484
521
|
// a long-running pi session that runs thousands of teams accumulates
|
|
485
|
-
// thousands of entries. Eviction: oldest
|
|
486
|
-
//
|
|
487
|
-
//
|
|
522
|
+
// thousands of entries. Eviction: oldest lastAccessAt first — uses LRU-like
|
|
523
|
+
// semantics so entries that are still being actively accessed (re-accessed
|
|
524
|
+
// before eviction) survive longer. This is fairer than insertion-order
|
|
525
|
+
// eviction under high churn where many entries expire naturally before
|
|
526
|
+
// being re-accessed.
|
|
488
527
|
const AUTO_RECOVERY_LAST_MAX_ENTRIES = 1000;
|
|
489
528
|
const configureDeliveryCoordinator = (): void => {
|
|
490
529
|
deliveryCoordinator?.dispose();
|
|
@@ -812,8 +851,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
812
851
|
.then(({ startForegroundWatchdog }) => {
|
|
813
852
|
startForegroundWatchdog({ pi, cwd: ctx.cwd, runId });
|
|
814
853
|
})
|
|
815
|
-
.catch(() => {
|
|
816
|
-
|
|
854
|
+
.catch((error) => {
|
|
855
|
+
logInternalError("register.foreground-watchdog-import", error);
|
|
817
856
|
});
|
|
818
857
|
}
|
|
819
858
|
setImmediate(() => {
|
|
@@ -823,7 +862,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
823
862
|
error instanceof Error ? error.message : String(error);
|
|
824
863
|
if (runId) {
|
|
825
864
|
try {
|
|
826
|
-
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
865
|
+
const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency. Post-run status updates tolerate slight staleness.
|
|
827
866
|
if (
|
|
828
867
|
loaded &&
|
|
829
868
|
loaded.manifest.status !== "completed" &&
|
|
@@ -877,7 +916,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
877
916
|
}
|
|
878
917
|
}
|
|
879
918
|
if (ownerCurrent && runId) {
|
|
880
|
-
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
919
|
+
const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency. Post-run status updates tolerate slight staleness.
|
|
881
920
|
const status = loaded?.manifest.status ?? "finished";
|
|
882
921
|
const level =
|
|
883
922
|
status === "failed" || status === "blocked"
|
|
@@ -971,9 +1010,25 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
971
1010
|
const registry = (globalThis as Record<symbol | string, unknown>)[
|
|
972
1011
|
CREW_REGISTRY_KEY
|
|
973
1012
|
] as Record<string, unknown>;
|
|
974
|
-
|
|
1013
|
+
// Phase 3b (defensive): Validate registry structure before patching methods.
|
|
1014
|
+
// If a previous occupant left a non-conforming object, replace it entirely.
|
|
1015
|
+
// This prevents runtime failures if getRecord/listRuns/etc. are called on a
|
|
1016
|
+
// malformed predecessor value.
|
|
1017
|
+
if (
|
|
1018
|
+
registry === null ||
|
|
1019
|
+
typeof registry !== "object" ||
|
|
1020
|
+
Array.isArray(registry)
|
|
1021
|
+
) {
|
|
1022
|
+
(globalThis as Record<symbol | string, unknown>)[
|
|
1023
|
+
CREW_REGISTRY_KEY
|
|
1024
|
+
] = {};
|
|
1025
|
+
}
|
|
1026
|
+
const validatedRegistry = (globalThis as Record<symbol | string, unknown>)[
|
|
1027
|
+
CREW_REGISTRY_KEY
|
|
1028
|
+
] as Record<string, unknown>;
|
|
1029
|
+
validatedRegistry.getRecord = (runId: string) =>
|
|
975
1030
|
manifestCacheForRegistry.get(runId);
|
|
976
|
-
|
|
1031
|
+
validatedRegistry.listRuns = () =>
|
|
977
1032
|
manifestCacheForRegistry
|
|
978
1033
|
.list(100)
|
|
979
1034
|
.map(
|
|
@@ -987,7 +1042,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
987
1042
|
goal: m.goal,
|
|
988
1043
|
}),
|
|
989
1044
|
);
|
|
990
|
-
|
|
1045
|
+
validatedRegistry.appendEvent = (
|
|
991
1046
|
runId: string,
|
|
992
1047
|
event: Record<string, unknown>,
|
|
993
1048
|
) => {
|
|
@@ -1003,7 +1058,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1003
1058
|
),
|
|
1004
1059
|
);
|
|
1005
1060
|
};
|
|
1006
|
-
|
|
1061
|
+
validatedRegistry.waitForAll = async (runId: string) => {
|
|
1007
1062
|
// LAZY: state-store only needed for post-completion polling (waitForAll) and sync hasRunning check; avoid at startup.
|
|
1008
1063
|
const { loadRunManifestById } = await import(
|
|
1009
1064
|
"../state/state-store.ts"
|
|
@@ -1022,7 +1077,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1022
1077
|
while (!check())
|
|
1023
1078
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1024
1079
|
};
|
|
1025
|
-
|
|
1080
|
+
validatedRegistry.hasRunning = async (runId: string) => {
|
|
1026
1081
|
const manifest = manifestCacheForRegistry.get(runId);
|
|
1027
1082
|
if (!manifest) return false;
|
|
1028
1083
|
// LAZY: state-store only needed in hasRunning; avoid at startup.
|
|
@@ -1112,7 +1167,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1112
1167
|
otlpExporter = undefined;
|
|
1113
1168
|
metricRegistry = undefined;
|
|
1114
1169
|
deliveryCoordinator?.dispose();
|
|
1115
|
-
|
|
1170
|
+
clearHooksScoped();
|
|
1116
1171
|
uninstallCrewGlobalRegistry();
|
|
1117
1172
|
overflowTracker?.dispose();
|
|
1118
1173
|
deliveryCoordinator = undefined;
|
|
@@ -1140,6 +1195,17 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1140
1195
|
|
|
1141
1196
|
pi.on("session_start", (_event, ctx) => {
|
|
1142
1197
|
runArtifactCleanup(ctx.cwd);
|
|
1198
|
+
|
|
1199
|
+
// Restore brief mode state from session entries
|
|
1200
|
+
try {
|
|
1201
|
+
const entries = ctx.sessionManager?.getEntries?.();
|
|
1202
|
+
if (entries) {
|
|
1203
|
+
import("../ui/tool-renderers/brief-mode.ts").then(({ restoreBriefState }) => {
|
|
1204
|
+
restoreBriefState(entries);
|
|
1205
|
+
}).catch(() => {/* non-critical */});
|
|
1206
|
+
}
|
|
1207
|
+
} catch { /* non-critical */ }
|
|
1208
|
+
|
|
1143
1209
|
time("register.session-start");
|
|
1144
1210
|
cleanedUp = false;
|
|
1145
1211
|
sessionGeneration++;
|
|
@@ -1150,10 +1216,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1150
1216
|
notifyActiveRuns(ctx);
|
|
1151
1217
|
|
|
1152
1218
|
// Auto-cancel orphaned runs from dead sessions
|
|
1153
|
-
// Extract sessionId from context —
|
|
1219
|
+
// Extract sessionId from context — use Object.getOwnPropertyDescriptor
|
|
1220
|
+
// to safely access property without triggering Proxy traps, then validate.
|
|
1154
1221
|
const rawSessionId =
|
|
1155
|
-
typeof ctx === "object" && ctx !== null
|
|
1156
|
-
? (ctx
|
|
1222
|
+
typeof ctx === "object" && ctx !== null
|
|
1223
|
+
? Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value
|
|
1157
1224
|
: undefined;
|
|
1158
1225
|
const currentSessionId =
|
|
1159
1226
|
typeof rawSessionId === "string" && rawSessionId.length > 0
|
|
@@ -1219,6 +1286,54 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1219
1286
|
}
|
|
1220
1287
|
}
|
|
1221
1288
|
|
|
1289
|
+
// Startup cleanup (Fix A): run orphan-temp-dir cleanup
|
|
1290
|
+
// immediately on session_start so we don't wait 5 minutes
|
|
1291
|
+
// for the first timer tick. Especially important after
|
|
1292
|
+
// a SIGKILL'd previous session that left thousands of
|
|
1293
|
+
// orphan temp dirs behind.
|
|
1294
|
+
try {
|
|
1295
|
+
const orphanTmp = cleanupOrphanTempDirs();
|
|
1296
|
+
const legacyTmp = cleanupLegacyOrphanTempDirs();
|
|
1297
|
+
if (orphanTmp.cleaned > 0 || legacyTmp.cleaned > 0) {
|
|
1298
|
+
notifyOperator({
|
|
1299
|
+
id: `startup_temp_cleanup_${Date.now()}`,
|
|
1300
|
+
severity: "info",
|
|
1301
|
+
source: "temp-cleanup",
|
|
1302
|
+
title: `Startup cleanup: removed ${orphanTmp.cleaned + legacyTmp.cleaned} orphan temp dir(s)`,
|
|
1303
|
+
body: `${orphanTmp.cleaned} from ~/.pi/agent/pi-crew/tmp/ + ${legacyTmp.cleaned} legacy /tmp/pi-crew-*`,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
logInternalError(
|
|
1308
|
+
"register.sessionStart.startupTempCleanup",
|
|
1309
|
+
error,
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Orphan worker cleanup (Fix B): kill stale background-runner
|
|
1314
|
+
// processes from previous (SIGKILL'd) sessions. Workers
|
|
1315
|
+
// detached via setsid+unref outlive the spawning pi
|
|
1316
|
+
// session, and the per-worker parent-guard is intentionally
|
|
1317
|
+
// disabled for background-runner (BUG #17 design). So
|
|
1318
|
+
// orphans can only be cleaned from the next session_start.
|
|
1319
|
+
try {
|
|
1320
|
+
const orphanWorkers = cleanupOrphanWorkers(currentSessionId);
|
|
1321
|
+
if (orphanWorkers.killed > 0) {
|
|
1322
|
+
notifyOperator({
|
|
1323
|
+
id: `orphan_workers_cleanup`,
|
|
1324
|
+
severity: "info",
|
|
1325
|
+
source: "worker-cleanup",
|
|
1326
|
+
title: `Cleaned up ${orphanWorkers.killed} orphan worker(s)`,
|
|
1327
|
+
body: `Background workers from previous (SIGKILL'd) sessions were terminated (pruned ${orphanWorkers.pruned} dead, kept ${orphanWorkers.kept}).`,
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
logInternalError(
|
|
1332
|
+
"register.sessionStart.orphanWorkers",
|
|
1333
|
+
error,
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1222
1337
|
// Global purge of stale active-run-index entries
|
|
1223
1338
|
try {
|
|
1224
1339
|
const { purged } = purgeStaleActiveRunIndexFn();
|
|
@@ -1324,19 +1439,60 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1324
1439
|
runParams = { action: "run", team: "default", goal: job.prompt };
|
|
1325
1440
|
}
|
|
1326
1441
|
if (runParams.action !== "run") return `scheduled-${job.id}-${Date.now()}`;
|
|
1442
|
+
const agentId = `scheduled-${job.id}-${Date.now()}`;
|
|
1327
1443
|
setImmediate(async () => {
|
|
1328
1444
|
try {
|
|
1329
|
-
await handleTeamTool(
|
|
1445
|
+
const runResult = await handleTeamTool(
|
|
1330
1446
|
{ action: "run", team: runParams.team, goal: runParams.goal, async: true },
|
|
1331
1447
|
{ cwd: ctx.cwd, sessionId },
|
|
1332
1448
|
);
|
|
1449
|
+
// Track the runId so remove() can cancel spawned runs
|
|
1450
|
+
const runId = runResult?.details?.runId;
|
|
1451
|
+
if (runId && typeof runId === "string") {
|
|
1452
|
+
crewScheduler?.recordSpawnedRun(job.id, runId);
|
|
1453
|
+
// Update run manifest with scheduler provenance for traceability
|
|
1454
|
+
try {
|
|
1455
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1456
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
1457
|
+
if (loaded) {
|
|
1458
|
+
const { atomicWriteJson } = await import("../state/atomic-write.ts");
|
|
1459
|
+
atomicWriteJson(loaded.manifest.stateRoot + "/manifest.json", {
|
|
1460
|
+
...loaded.manifest,
|
|
1461
|
+
schedulerJobId: job.id,
|
|
1462
|
+
schedulerName: job.name,
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
} catch { /* best-effort provenance tracking */ }
|
|
1466
|
+
}
|
|
1467
|
+
// Persist updated job with spawnedRunIds to settings
|
|
1468
|
+
try {
|
|
1469
|
+
const updatedJob = crewScheduler?.list().find((j) => j.id === job.id);
|
|
1470
|
+
if (updatedJob) persistScheduledJobUpdate(ctx.cwd, updatedJob);
|
|
1471
|
+
} catch { /* best-effort */ }
|
|
1472
|
+
// Update run count
|
|
1473
|
+
crewScheduler?.update(job.id, {
|
|
1474
|
+
runCount: job.runCount + 1,
|
|
1475
|
+
lastRun: new Date().toISOString(),
|
|
1476
|
+
lastStatus: "success",
|
|
1477
|
+
});
|
|
1333
1478
|
} catch (err) {
|
|
1334
1479
|
logInternalError("scheduler.execute", err);
|
|
1480
|
+
crewScheduler?.update(job.id, { lastStatus: "error" });
|
|
1335
1481
|
}
|
|
1336
1482
|
});
|
|
1337
|
-
return
|
|
1483
|
+
return agentId;
|
|
1338
1484
|
},
|
|
1339
1485
|
finalizer: () => {},
|
|
1486
|
+
runCancelFn: (runId: string) => {
|
|
1487
|
+
try {
|
|
1488
|
+
handleTeamTool(
|
|
1489
|
+
{ action: "cancel", runId, confirm: true },
|
|
1490
|
+
{ cwd: ctx.cwd, sessionId },
|
|
1491
|
+
).catch((err) => logInternalError("scheduler.runCancelFn", err, `runId=${runId}`));
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
logInternalError("scheduler.runCancelFn.sync", err, `runId=${runId}`);
|
|
1494
|
+
}
|
|
1495
|
+
},
|
|
1340
1496
|
});
|
|
1341
1497
|
// Wire scheduler into handle-schedule.ts so handlers can add/list jobs.
|
|
1342
1498
|
// Uses a global symbol so the module doesn't need a direct circular import.
|
|
@@ -1534,18 +1690,26 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1534
1690
|
const previous = autoRecoveryLast.get(key);
|
|
1535
1691
|
if (
|
|
1536
1692
|
previous !== undefined &&
|
|
1537
|
-
now - previous < 5 * 60_000
|
|
1693
|
+
now - previous.lastAccessAt < 5 * 60_000
|
|
1538
1694
|
)
|
|
1539
1695
|
return;
|
|
1540
|
-
// Defensive cap: evict oldest
|
|
1541
|
-
// when size exceeds the limit.
|
|
1542
|
-
//
|
|
1696
|
+
// Defensive cap: evict entry with oldest lastAccessAt before
|
|
1697
|
+
// inserting/updating when size exceeds the limit. Uses LRU
|
|
1698
|
+
// semantics so entries that are still being actively
|
|
1699
|
+
// accessed survive longer than insertion-order eviction.
|
|
1543
1700
|
while (autoRecoveryLast.size >= AUTO_RECOVERY_LAST_MAX_ENTRIES) {
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
autoRecoveryLast
|
|
1701
|
+
let oldestKey: string | undefined;
|
|
1702
|
+
let oldestAccess = Infinity;
|
|
1703
|
+
for (const [k, v] of autoRecoveryLast) {
|
|
1704
|
+
if (v.lastAccessAt < oldestAccess) {
|
|
1705
|
+
oldestAccess = v.lastAccessAt;
|
|
1706
|
+
oldestKey = k;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (oldestKey === undefined) break;
|
|
1710
|
+
autoRecoveryLast.delete(oldestKey);
|
|
1547
1711
|
}
|
|
1548
|
-
autoRecoveryLast.set(key, now);
|
|
1712
|
+
autoRecoveryLast.set(key, { insertedAt: now, lastAccessAt: now });
|
|
1549
1713
|
notifyOperator({
|
|
1550
1714
|
id: key,
|
|
1551
1715
|
severity: "warning",
|
|
@@ -1752,8 +1916,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1752
1916
|
);
|
|
1753
1917
|
const paths: string[] = [];
|
|
1754
1918
|
if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
|
|
1755
|
-
if (skillDir !== extSkillDir && fs.existsSync(skillDir))
|
|
1756
|
-
|
|
1919
|
+
if (skillDir !== extSkillDir && fs.existsSync(skillDir)) {
|
|
1920
|
+
// Validate skillDir is within sessionCwd to prevent path traversal
|
|
1921
|
+
try {
|
|
1922
|
+
resolveContainedPath(sessionCwd, "skills");
|
|
1923
|
+
paths.push(skillDir);
|
|
1924
|
+
} catch {
|
|
1925
|
+
// skillDir outside sessionCwd boundary — skip
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1757
1928
|
return paths.length > 0 ? { skillPaths: paths } : {};
|
|
1758
1929
|
});
|
|
1759
1930
|
} catch {
|
|
@@ -1777,7 +1948,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1777
1948
|
if (event.toolName !== "team") return;
|
|
1778
1949
|
const rawInput = event.input;
|
|
1779
1950
|
if (!rawInput || typeof rawInput !== "object") return;
|
|
1780
|
-
const input = rawInput
|
|
1951
|
+
const input = asRecord(rawInput);
|
|
1952
|
+
if (!input) return;
|
|
1781
1953
|
const action = typeof input.action === "string" ? input.action : undefined;
|
|
1782
1954
|
const destructiveActions = new Set(["delete", "forget", "prune", "cleanup"]);
|
|
1783
1955
|
if (!action || !destructiveActions.has(action)) return;
|
|
@@ -1814,6 +1986,12 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1814
1986
|
|
|
1815
1987
|
registerCleanupHandler(pi);
|
|
1816
1988
|
|
|
1989
|
+
// Brief tool overrides DISABLED: re-registering built-in tools replaced Pi's
|
|
1990
|
+
// superior native renderers (syntax highlighting, diff views, full file
|
|
1991
|
+
// content) with inferior custom ones, and caused renderCall/renderResult
|
|
1992
|
+
// to duplicate path/command info. Pi's native rendering is better.
|
|
1993
|
+
// To re-enable, uncomment: registerBriefToolOverrides(pi, process.cwd());
|
|
1994
|
+
|
|
1817
1995
|
registerTeamCommands(pi, {
|
|
1818
1996
|
startForegroundRun,
|
|
1819
1997
|
abortForegroundRun,
|