pi-crew 0.6.1 → 0.6.3
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 +160 -0
- package/README.md +70 -33
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -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 +194 -34
- package/src/extension/registration/commands.ts +2 -2
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/registration/team-tool.ts +1 -1
- 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 +60 -11
- package/src/extension/team-tool/status.ts +1 -1
- 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/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/run-action-dispatcher.ts +2 -2
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- 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
|
|
|
@@ -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,
|
|
@@ -108,6 +109,7 @@ import {
|
|
|
108
109
|
import { registerSubagentTools } from "./registration/subagent-tools.ts";
|
|
109
110
|
import { registerTeamTool } from "./registration/team-tool.ts";
|
|
110
111
|
import { handleTeamTool } from "./team-tool.ts";
|
|
112
|
+
import { persistScheduledJobUpdate } from "./team-tool/handle-schedule.ts";
|
|
111
113
|
|
|
112
114
|
let _cachedOTLPExporter: typeof OTLPExporterType | undefined;
|
|
113
115
|
async function importOTLPExporter(): Promise<typeof OTLPExporterType> {
|
|
@@ -132,6 +134,8 @@ import {
|
|
|
132
134
|
} from "../runtime/crash-recovery.ts";
|
|
133
135
|
import { appendDeadletter } from "../runtime/deadletter.ts";
|
|
134
136
|
import { HeartbeatWatcher } from "../runtime/heartbeat-watcher.ts";
|
|
137
|
+
import { cleanupOrphanTempDirs, cleanupLegacyOrphanTempDirs } from "../runtime/pi-args.ts";
|
|
138
|
+
import { cleanupOrphanWorkers } from "../runtime/orphan-worker-registry.ts";
|
|
135
139
|
import { reconcileOrphanedTempWorkspaces } from "../runtime/stale-reconciler.ts";
|
|
136
140
|
|
|
137
141
|
let _cachedCrashRecovery:
|
|
@@ -183,10 +187,14 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
183
187
|
const disposeI18n = initI18n(pi);
|
|
184
188
|
resetTimings();
|
|
185
189
|
time("register:start");
|
|
186
|
-
const globalStore = globalThis as Record<string, unknown>;
|
|
187
|
-
const runtimeCleanupStoreKey = "__piCrewRuntimeCleanup";
|
|
190
|
+
const globalStore = globalThis as Record<string | symbol, unknown>;
|
|
191
|
+
const runtimeCleanupStoreKey = Symbol("__piCrewRuntimeCleanup");
|
|
188
192
|
const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
|
|
189
193
|
time("register:init");
|
|
194
|
+
// Best-effort cleanup of the previous runtime instance. Errors are logged but
|
|
195
|
+
// do not halt new registration — a failing cleanup from a prior instance is
|
|
196
|
+
// preferable to leaving pi-crew unregistered, and any stale state from the
|
|
197
|
+
// previous instance will be reconciled when the new instance initializes.
|
|
190
198
|
if (typeof previousRuntimeCleanup === "function") {
|
|
191
199
|
try {
|
|
192
200
|
previousRuntimeCleanup();
|
|
@@ -443,6 +451,34 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
443
451
|
cleanupOrphanedTempDirs:
|
|
444
452
|
config.reliability?.cleanupOrphanedTempDirs,
|
|
445
453
|
});
|
|
454
|
+
// Layer 4: also clean orphan temp dirs under
|
|
455
|
+
// ~/.pi/agent/pi-crew/tmp/ that the SIGKILL'd parent
|
|
456
|
+
// processes left behind. Catches anything Layers 1-3 missed.
|
|
457
|
+
const orphanResult = cleanupOrphanTempDirs();
|
|
458
|
+
if (orphanResult.cleaned > 0) {
|
|
459
|
+
notifyOperator({
|
|
460
|
+
id: `layer4_temp_cleanup_${Date.now()}`,
|
|
461
|
+
severity: "info",
|
|
462
|
+
source: "temp-cleanup",
|
|
463
|
+
title: `Layer 4: cleaned ${orphanResult.cleaned} orphan temp dir(s)`,
|
|
464
|
+
body: `~/.pi/agent/pi-crew/tmp/ orphans older than 24h removed (scanned ${orphanResult.scanned}, failed ${orphanResult.failed}).`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
// Layer 5: clean legacy /tmp/pi-crew-* prompt/task orphans
|
|
468
|
+
// from before commit 8ba270d moved temp dirs out of /tmp.
|
|
469
|
+
// The existing reconcileOrphanedTempWorkspaces only cleans
|
|
470
|
+
// dirs containing .crew/state/runs/ (run-state dirs), so
|
|
471
|
+
// prompt/task orphans are never touched by Layer 3.
|
|
472
|
+
const legacyResult = cleanupLegacyOrphanTempDirs();
|
|
473
|
+
if (legacyResult.cleaned > 0) {
|
|
474
|
+
notifyOperator({
|
|
475
|
+
id: `layer5_legacy_temp_cleanup_${Date.now()}`,
|
|
476
|
+
severity: "info",
|
|
477
|
+
source: "temp-cleanup",
|
|
478
|
+
title: `Layer 5: cleaned ${legacyResult.cleaned} legacy /tmp/pi-crew-* orphan(s)`,
|
|
479
|
+
body: `Pre-fix /tmp/pi-crew-* prompt/task orphans (no .crew/state/runs/, >24h) removed (scanned ${legacyResult.scanned}, failed ${legacyResult.failed}).`,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
446
482
|
} catch (error) {
|
|
447
483
|
logInternalError("register.tempAutoRepair", error);
|
|
448
484
|
}
|
|
@@ -478,13 +514,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
478
514
|
);
|
|
479
515
|
}
|
|
480
516
|
};
|
|
481
|
-
const autoRecoveryLast = new Map<string, number>();
|
|
517
|
+
const autoRecoveryLast = new Map<string, { insertedAt: number; lastAccessAt: number }>();
|
|
482
518
|
// FIX (Round 22, defensive cap): Bound the cooldown-gate Map. Each run
|
|
483
519
|
// contributes up to 4 keys (one per maybeNotifyHealth kind). Without a cap,
|
|
484
520
|
// a long-running pi session that runs thousands of teams accumulates
|
|
485
|
-
// thousands of entries. Eviction: oldest
|
|
486
|
-
//
|
|
487
|
-
//
|
|
521
|
+
// thousands of entries. Eviction: oldest lastAccessAt first — uses LRU-like
|
|
522
|
+
// semantics so entries that are still being actively accessed (re-accessed
|
|
523
|
+
// before eviction) survive longer. This is fairer than insertion-order
|
|
524
|
+
// eviction under high churn where many entries expire naturally before
|
|
525
|
+
// being re-accessed.
|
|
488
526
|
const AUTO_RECOVERY_LAST_MAX_ENTRIES = 1000;
|
|
489
527
|
const configureDeliveryCoordinator = (): void => {
|
|
490
528
|
deliveryCoordinator?.dispose();
|
|
@@ -812,8 +850,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
812
850
|
.then(({ startForegroundWatchdog }) => {
|
|
813
851
|
startForegroundWatchdog({ pi, cwd: ctx.cwd, runId });
|
|
814
852
|
})
|
|
815
|
-
.catch(() => {
|
|
816
|
-
|
|
853
|
+
.catch((error) => {
|
|
854
|
+
logInternalError("register.foreground-watchdog-import", error);
|
|
817
855
|
});
|
|
818
856
|
}
|
|
819
857
|
setImmediate(() => {
|
|
@@ -823,7 +861,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
823
861
|
error instanceof Error ? error.message : String(error);
|
|
824
862
|
if (runId) {
|
|
825
863
|
try {
|
|
826
|
-
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
864
|
+
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
865
|
if (
|
|
828
866
|
loaded &&
|
|
829
867
|
loaded.manifest.status !== "completed" &&
|
|
@@ -877,7 +915,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
877
915
|
}
|
|
878
916
|
}
|
|
879
917
|
if (ownerCurrent && runId) {
|
|
880
|
-
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
918
|
+
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
919
|
const status = loaded?.manifest.status ?? "finished";
|
|
882
920
|
const level =
|
|
883
921
|
status === "failed" || status === "blocked"
|
|
@@ -971,9 +1009,25 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
971
1009
|
const registry = (globalThis as Record<symbol | string, unknown>)[
|
|
972
1010
|
CREW_REGISTRY_KEY
|
|
973
1011
|
] as Record<string, unknown>;
|
|
974
|
-
|
|
1012
|
+
// Phase 3b (defensive): Validate registry structure before patching methods.
|
|
1013
|
+
// If a previous occupant left a non-conforming object, replace it entirely.
|
|
1014
|
+
// This prevents runtime failures if getRecord/listRuns/etc. are called on a
|
|
1015
|
+
// malformed predecessor value.
|
|
1016
|
+
if (
|
|
1017
|
+
registry === null ||
|
|
1018
|
+
typeof registry !== "object" ||
|
|
1019
|
+
Array.isArray(registry)
|
|
1020
|
+
) {
|
|
1021
|
+
(globalThis as Record<symbol | string, unknown>)[
|
|
1022
|
+
CREW_REGISTRY_KEY
|
|
1023
|
+
] = {};
|
|
1024
|
+
}
|
|
1025
|
+
const validatedRegistry = (globalThis as Record<symbol | string, unknown>)[
|
|
1026
|
+
CREW_REGISTRY_KEY
|
|
1027
|
+
] as Record<string, unknown>;
|
|
1028
|
+
validatedRegistry.getRecord = (runId: string) =>
|
|
975
1029
|
manifestCacheForRegistry.get(runId);
|
|
976
|
-
|
|
1030
|
+
validatedRegistry.listRuns = () =>
|
|
977
1031
|
manifestCacheForRegistry
|
|
978
1032
|
.list(100)
|
|
979
1033
|
.map(
|
|
@@ -987,7 +1041,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
987
1041
|
goal: m.goal,
|
|
988
1042
|
}),
|
|
989
1043
|
);
|
|
990
|
-
|
|
1044
|
+
validatedRegistry.appendEvent = (
|
|
991
1045
|
runId: string,
|
|
992
1046
|
event: Record<string, unknown>,
|
|
993
1047
|
) => {
|
|
@@ -1003,7 +1057,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1003
1057
|
),
|
|
1004
1058
|
);
|
|
1005
1059
|
};
|
|
1006
|
-
|
|
1060
|
+
validatedRegistry.waitForAll = async (runId: string) => {
|
|
1007
1061
|
// LAZY: state-store only needed for post-completion polling (waitForAll) and sync hasRunning check; avoid at startup.
|
|
1008
1062
|
const { loadRunManifestById } = await import(
|
|
1009
1063
|
"../state/state-store.ts"
|
|
@@ -1022,7 +1076,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1022
1076
|
while (!check())
|
|
1023
1077
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1024
1078
|
};
|
|
1025
|
-
|
|
1079
|
+
validatedRegistry.hasRunning = async (runId: string) => {
|
|
1026
1080
|
const manifest = manifestCacheForRegistry.get(runId);
|
|
1027
1081
|
if (!manifest) return false;
|
|
1028
1082
|
// LAZY: state-store only needed in hasRunning; avoid at startup.
|
|
@@ -1112,7 +1166,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1112
1166
|
otlpExporter = undefined;
|
|
1113
1167
|
metricRegistry = undefined;
|
|
1114
1168
|
deliveryCoordinator?.dispose();
|
|
1115
|
-
|
|
1169
|
+
clearHooksScoped();
|
|
1116
1170
|
uninstallCrewGlobalRegistry();
|
|
1117
1171
|
overflowTracker?.dispose();
|
|
1118
1172
|
deliveryCoordinator = undefined;
|
|
@@ -1150,10 +1204,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1150
1204
|
notifyActiveRuns(ctx);
|
|
1151
1205
|
|
|
1152
1206
|
// Auto-cancel orphaned runs from dead sessions
|
|
1153
|
-
// Extract sessionId from context —
|
|
1207
|
+
// Extract sessionId from context — use Object.getOwnPropertyDescriptor
|
|
1208
|
+
// to safely access property without triggering Proxy traps, then validate.
|
|
1154
1209
|
const rawSessionId =
|
|
1155
|
-
typeof ctx === "object" && ctx !== null
|
|
1156
|
-
? (ctx
|
|
1210
|
+
typeof ctx === "object" && ctx !== null
|
|
1211
|
+
? Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value
|
|
1157
1212
|
: undefined;
|
|
1158
1213
|
const currentSessionId =
|
|
1159
1214
|
typeof rawSessionId === "string" && rawSessionId.length > 0
|
|
@@ -1219,6 +1274,54 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1219
1274
|
}
|
|
1220
1275
|
}
|
|
1221
1276
|
|
|
1277
|
+
// Startup cleanup (Fix A): run orphan-temp-dir cleanup
|
|
1278
|
+
// immediately on session_start so we don't wait 5 minutes
|
|
1279
|
+
// for the first timer tick. Especially important after
|
|
1280
|
+
// a SIGKILL'd previous session that left thousands of
|
|
1281
|
+
// orphan temp dirs behind.
|
|
1282
|
+
try {
|
|
1283
|
+
const orphanTmp = cleanupOrphanTempDirs();
|
|
1284
|
+
const legacyTmp = cleanupLegacyOrphanTempDirs();
|
|
1285
|
+
if (orphanTmp.cleaned > 0 || legacyTmp.cleaned > 0) {
|
|
1286
|
+
notifyOperator({
|
|
1287
|
+
id: `startup_temp_cleanup_${Date.now()}`,
|
|
1288
|
+
severity: "info",
|
|
1289
|
+
source: "temp-cleanup",
|
|
1290
|
+
title: `Startup cleanup: removed ${orphanTmp.cleaned + legacyTmp.cleaned} orphan temp dir(s)`,
|
|
1291
|
+
body: `${orphanTmp.cleaned} from ~/.pi/agent/pi-crew/tmp/ + ${legacyTmp.cleaned} legacy /tmp/pi-crew-*`,
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
logInternalError(
|
|
1296
|
+
"register.sessionStart.startupTempCleanup",
|
|
1297
|
+
error,
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Orphan worker cleanup (Fix B): kill stale background-runner
|
|
1302
|
+
// processes from previous (SIGKILL'd) sessions. Workers
|
|
1303
|
+
// detached via setsid+unref outlive the spawning pi
|
|
1304
|
+
// session, and the per-worker parent-guard is intentionally
|
|
1305
|
+
// disabled for background-runner (BUG #17 design). So
|
|
1306
|
+
// orphans can only be cleaned from the next session_start.
|
|
1307
|
+
try {
|
|
1308
|
+
const orphanWorkers = cleanupOrphanWorkers(currentSessionId);
|
|
1309
|
+
if (orphanWorkers.killed > 0) {
|
|
1310
|
+
notifyOperator({
|
|
1311
|
+
id: `orphan_workers_cleanup`,
|
|
1312
|
+
severity: "info",
|
|
1313
|
+
source: "worker-cleanup",
|
|
1314
|
+
title: `Cleaned up ${orphanWorkers.killed} orphan worker(s)`,
|
|
1315
|
+
body: `Background workers from previous (SIGKILL'd) sessions were terminated (pruned ${orphanWorkers.pruned} dead, kept ${orphanWorkers.kept}).`,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
logInternalError(
|
|
1320
|
+
"register.sessionStart.orphanWorkers",
|
|
1321
|
+
error,
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1222
1325
|
// Global purge of stale active-run-index entries
|
|
1223
1326
|
try {
|
|
1224
1327
|
const { purged } = purgeStaleActiveRunIndexFn();
|
|
@@ -1324,19 +1427,60 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1324
1427
|
runParams = { action: "run", team: "default", goal: job.prompt };
|
|
1325
1428
|
}
|
|
1326
1429
|
if (runParams.action !== "run") return `scheduled-${job.id}-${Date.now()}`;
|
|
1430
|
+
const agentId = `scheduled-${job.id}-${Date.now()}`;
|
|
1327
1431
|
setImmediate(async () => {
|
|
1328
1432
|
try {
|
|
1329
|
-
await handleTeamTool(
|
|
1433
|
+
const runResult = await handleTeamTool(
|
|
1330
1434
|
{ action: "run", team: runParams.team, goal: runParams.goal, async: true },
|
|
1331
1435
|
{ cwd: ctx.cwd, sessionId },
|
|
1332
1436
|
);
|
|
1437
|
+
// Track the runId so remove() can cancel spawned runs
|
|
1438
|
+
const runId = runResult?.details?.runId;
|
|
1439
|
+
if (runId && typeof runId === "string") {
|
|
1440
|
+
crewScheduler?.recordSpawnedRun(job.id, runId);
|
|
1441
|
+
// Update run manifest with scheduler provenance for traceability
|
|
1442
|
+
try {
|
|
1443
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1444
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
1445
|
+
if (loaded) {
|
|
1446
|
+
const { atomicWriteJson } = await import("../state/atomic-write.ts");
|
|
1447
|
+
atomicWriteJson(loaded.manifest.stateRoot + "/manifest.json", {
|
|
1448
|
+
...loaded.manifest,
|
|
1449
|
+
schedulerJobId: job.id,
|
|
1450
|
+
schedulerName: job.name,
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
} catch { /* best-effort provenance tracking */ }
|
|
1454
|
+
}
|
|
1455
|
+
// Persist updated job with spawnedRunIds to settings
|
|
1456
|
+
try {
|
|
1457
|
+
const updatedJob = crewScheduler?.list().find((j) => j.id === job.id);
|
|
1458
|
+
if (updatedJob) persistScheduledJobUpdate(ctx.cwd, updatedJob);
|
|
1459
|
+
} catch { /* best-effort */ }
|
|
1460
|
+
// Update run count
|
|
1461
|
+
crewScheduler?.update(job.id, {
|
|
1462
|
+
runCount: job.runCount + 1,
|
|
1463
|
+
lastRun: new Date().toISOString(),
|
|
1464
|
+
lastStatus: "success",
|
|
1465
|
+
});
|
|
1333
1466
|
} catch (err) {
|
|
1334
1467
|
logInternalError("scheduler.execute", err);
|
|
1468
|
+
crewScheduler?.update(job.id, { lastStatus: "error" });
|
|
1335
1469
|
}
|
|
1336
1470
|
});
|
|
1337
|
-
return
|
|
1471
|
+
return agentId;
|
|
1338
1472
|
},
|
|
1339
1473
|
finalizer: () => {},
|
|
1474
|
+
runCancelFn: (runId: string) => {
|
|
1475
|
+
try {
|
|
1476
|
+
handleTeamTool(
|
|
1477
|
+
{ action: "cancel", runId, confirm: true },
|
|
1478
|
+
{ cwd: ctx.cwd, sessionId },
|
|
1479
|
+
).catch((err) => logInternalError("scheduler.runCancelFn", err, `runId=${runId}`));
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
logInternalError("scheduler.runCancelFn.sync", err, `runId=${runId}`);
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1340
1484
|
});
|
|
1341
1485
|
// Wire scheduler into handle-schedule.ts so handlers can add/list jobs.
|
|
1342
1486
|
// Uses a global symbol so the module doesn't need a direct circular import.
|
|
@@ -1534,18 +1678,26 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1534
1678
|
const previous = autoRecoveryLast.get(key);
|
|
1535
1679
|
if (
|
|
1536
1680
|
previous !== undefined &&
|
|
1537
|
-
now - previous < 5 * 60_000
|
|
1681
|
+
now - previous.lastAccessAt < 5 * 60_000
|
|
1538
1682
|
)
|
|
1539
1683
|
return;
|
|
1540
|
-
// Defensive cap: evict oldest
|
|
1541
|
-
// when size exceeds the limit.
|
|
1542
|
-
//
|
|
1684
|
+
// Defensive cap: evict entry with oldest lastAccessAt before
|
|
1685
|
+
// inserting/updating when size exceeds the limit. Uses LRU
|
|
1686
|
+
// semantics so entries that are still being actively
|
|
1687
|
+
// accessed survive longer than insertion-order eviction.
|
|
1543
1688
|
while (autoRecoveryLast.size >= AUTO_RECOVERY_LAST_MAX_ENTRIES) {
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
autoRecoveryLast
|
|
1689
|
+
let oldestKey: string | undefined;
|
|
1690
|
+
let oldestAccess = Infinity;
|
|
1691
|
+
for (const [k, v] of autoRecoveryLast) {
|
|
1692
|
+
if (v.lastAccessAt < oldestAccess) {
|
|
1693
|
+
oldestAccess = v.lastAccessAt;
|
|
1694
|
+
oldestKey = k;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (oldestKey === undefined) break;
|
|
1698
|
+
autoRecoveryLast.delete(oldestKey);
|
|
1547
1699
|
}
|
|
1548
|
-
autoRecoveryLast.set(key, now);
|
|
1700
|
+
autoRecoveryLast.set(key, { insertedAt: now, lastAccessAt: now });
|
|
1549
1701
|
notifyOperator({
|
|
1550
1702
|
id: key,
|
|
1551
1703
|
severity: "warning",
|
|
@@ -1752,8 +1904,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1752
1904
|
);
|
|
1753
1905
|
const paths: string[] = [];
|
|
1754
1906
|
if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
|
|
1755
|
-
if (skillDir !== extSkillDir && fs.existsSync(skillDir))
|
|
1756
|
-
|
|
1907
|
+
if (skillDir !== extSkillDir && fs.existsSync(skillDir)) {
|
|
1908
|
+
// Validate skillDir is within sessionCwd to prevent path traversal
|
|
1909
|
+
try {
|
|
1910
|
+
resolveContainedPath(sessionCwd, "skills");
|
|
1911
|
+
paths.push(skillDir);
|
|
1912
|
+
} catch {
|
|
1913
|
+
// skillDir outside sessionCwd boundary — skip
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1757
1916
|
return paths.length > 0 ? { skillPaths: paths } : {};
|
|
1758
1917
|
});
|
|
1759
1918
|
} catch {
|
|
@@ -1777,7 +1936,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1777
1936
|
if (event.toolName !== "team") return;
|
|
1778
1937
|
const rawInput = event.input;
|
|
1779
1938
|
if (!rawInput || typeof rawInput !== "object") return;
|
|
1780
|
-
const input = rawInput
|
|
1939
|
+
const input = asRecord(rawInput);
|
|
1940
|
+
if (!input) return;
|
|
1781
1941
|
const action = typeof input.action === "string" ? input.action : undefined;
|
|
1782
1942
|
const destructiveActions = new Set(["delete", "forget", "prune", "cleanup"]);
|
|
1783
1943
|
if (!action || !destructiveActions.has(action)) return;
|
|
@@ -154,7 +154,7 @@ function teamCommandContext(ctx: ExtensionCommandContext): ExtensionCommandConte
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
async function handleHealthDashboardAction(ctx: ExtensionCommandContext, selection: RunDashboardSelection): Promise<void> {
|
|
157
|
-
const loaded = loadRunManifestById(ctx.cwd, selection.runId);
|
|
157
|
+
const loaded = loadRunManifestById(ctx.cwd, selection.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
158
158
|
if (!loaded) {
|
|
159
159
|
depsNotify(ctx, `Run '${selection.runId}' not found.`, "error");
|
|
160
160
|
return;
|
|
@@ -379,7 +379,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
379
379
|
pi.registerCommand("team-result", { description: "Open a pi-crew agent result viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
380
380
|
const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
|
|
381
381
|
const selected = await selectAgentTask(ctx, runId, rawTaskId);
|
|
382
|
-
const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
|
|
382
|
+
const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined; // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
383
383
|
if (ctx.hasUI && loaded) {
|
|
384
384
|
const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
|
|
385
385
|
const resultText = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected?.runId ?? "", config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, teamCommandContext(ctx))) : "(no result)";
|
|
@@ -34,7 +34,7 @@ export function sendAgentWakeUp(pi: ExtensionAPI, content: string): boolean {
|
|
|
34
34
|
|
|
35
35
|
export function refreshPersistedSubagentRecord(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): SubagentRecord {
|
|
36
36
|
if (!record.runId) return record;
|
|
37
|
-
const loaded = loadRunManifestById(ctx.cwd, record.runId);
|
|
37
|
+
const loaded = loadRunManifestById(ctx.cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
38
38
|
if (!loaded) return record;
|
|
39
39
|
if (loaded.manifest.status === "completed" || loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled" || loaded.manifest.status === "blocked") {
|
|
40
40
|
const refreshed = {
|
|
@@ -65,7 +65,7 @@ export function formatSubagentRecord(record: SubagentRecord): string {
|
|
|
65
65
|
|
|
66
66
|
export function readSubagentRunResult(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): string | undefined {
|
|
67
67
|
if (!record.runId) return record.result;
|
|
68
|
-
const loaded = loadRunManifestById(ctx.cwd, record.runId);
|
|
68
|
+
const loaded = loadRunManifestById(ctx.cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
69
69
|
const task = loaded?.tasks.find((item) => item.resultArtifact) ?? loaded?.tasks[0];
|
|
70
70
|
const artifactPath = task?.resultArtifact?.path;
|
|
71
71
|
if (!artifactPath || !loaded) return undefined;
|
|
@@ -204,7 +204,7 @@ function startAgentToolProgress(cwd: string, agentRecordId: string, onUpdate: On
|
|
|
204
204
|
let tasks;
|
|
205
205
|
let agents;
|
|
206
206
|
if (record.runId) {
|
|
207
|
-
const loaded = loadRunManifestById(cwd, record.runId);
|
|
207
|
+
const loaded = loadRunManifestById(cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
208
208
|
if (loaded) {
|
|
209
209
|
manifest = loaded.manifest;
|
|
210
210
|
tasks = loaded.tasks;
|
|
@@ -136,7 +136,7 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
|
|
|
136
136
|
onUpdate({ content: [{ type: "text", text: `team status=starting elapsed=${elapsed}s` }] });
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
|
-
const loaded = loadRunManifestById(cwd, runId);
|
|
139
|
+
const loaded = loadRunManifestById(cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
140
140
|
if (!loaded) {
|
|
141
141
|
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
142
142
|
onUpdate({ content: [{ type: "text", text: `team run=${runId} elapsed=${elapsed}s (manifest pending)` }] });
|
|
@@ -23,7 +23,7 @@ async function getViewer(): Promise<typeof DurableTranscriptViewerType> {
|
|
|
23
23
|
export async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
|
|
24
24
|
if (!runId) return undefined;
|
|
25
25
|
if (taskId) return { runId, taskId };
|
|
26
|
-
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
26
|
+
const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
27
27
|
if (!loaded) return { runId };
|
|
28
28
|
const agents = readCrewAgents(loaded.manifest);
|
|
29
29
|
if (ctx.hasUI && agents.length > 1) {
|
|
@@ -39,7 +39,7 @@ export async function openTranscriptViewer(ctx: ExtensionCommandContext, initial
|
|
|
39
39
|
const runId = selected.runId;
|
|
40
40
|
const taskId = selected.taskId;
|
|
41
41
|
if (!runId || !ctx.hasUI) return false;
|
|
42
|
-
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
42
|
+
const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
43
43
|
if (!loaded) return false;
|
|
44
44
|
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
45
45
|
const DurableTranscriptViewer = await getViewer();
|
|
@@ -92,7 +92,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
92
92
|
if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
|
|
93
93
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
94
94
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
95
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
95
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
96
96
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
97
97
|
if (operation === "read-manifest") {
|
|
98
98
|
return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -102,7 +102,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
102
102
|
if (!permission.allowed) return result(permission.reason ?? "Plan approval is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
103
103
|
try {
|
|
104
104
|
return withRunLockSync(loaded.manifest, () => {
|
|
105
|
-
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
|
|
105
|
+
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded; // NOTE: inside withRunLockSync - consistent read
|
|
106
106
|
const approval = current.manifest.planApproval;
|
|
107
107
|
if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
108
108
|
const now = new Date().toISOString();
|
|
@@ -121,7 +121,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
121
121
|
if (!permission.allowed) return result(permission.reason ?? "Plan approval cancellation is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
122
122
|
try {
|
|
123
123
|
return withRunLockSync(loaded.manifest, () => {
|
|
124
|
-
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
|
|
124
|
+
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded; // NOTE: inside withRunLockSync - consistent read
|
|
125
125
|
const approval = current.manifest.planApproval;
|
|
126
126
|
if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
127
127
|
const now = new Date().toISOString();
|
|
@@ -41,7 +41,7 @@ export function abortOwned(
|
|
|
41
41
|
): AbortOwnedResult {
|
|
42
42
|
const runCwd = locateRunCwd(runId, ctx.cwd);
|
|
43
43
|
if (!runCwd) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
|
|
44
|
-
const loaded = loadRunManifestById(runCwd, runId);
|
|
44
|
+
const loaded = loadRunManifestById(runCwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
45
45
|
if (!loaded) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
|
|
46
46
|
|
|
47
47
|
const result: AbortOwnedResult = { abortedIds: [], missingIds: [], foreignIds: [] };
|
|
@@ -81,7 +81,7 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext,
|
|
|
81
81
|
if (!params.runId) return result("Retry requires runId.", { action: "retry", status: "error" }, true);
|
|
82
82
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
83
83
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
|
|
84
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
84
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
85
85
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
|
|
86
86
|
|
|
87
87
|
// Pre-lock ownership check: reject foreign-owned runs unless force is set
|
|
@@ -146,7 +146,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
146
146
|
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
147
147
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
148
148
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
149
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
149
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
150
150
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
151
151
|
|
|
152
152
|
// Pre-lock ownership check: reject foreign-owned runs unless force is set
|
|
@@ -209,7 +209,7 @@ export function handleExplain(params: {
|
|
|
209
209
|
return result("explain requires runId", { action: "explain", status: "error" }, true);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
const loaded = loadRunManifestById(cwd, params.runId);
|
|
212
|
+
const loaded = loadRunManifestById(cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
213
213
|
if (!loaded) {
|
|
214
214
|
return result(`Run '${params.runId}' not found.`, { action: "explain", status: "error" }, true);
|
|
215
215
|
}
|
|
@@ -163,6 +163,43 @@ function persistScheduledJob(cwd: string, job: import("../../runtime/scheduler.t
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/** Update an existing scheduled job in persistent settings. */
|
|
167
|
+
export function persistScheduledJobUpdate(cwd: string, job: import("../../runtime/scheduler.ts").ScheduledJob): void {
|
|
168
|
+
try {
|
|
169
|
+
const settings = loadCrewSettings(cwd);
|
|
170
|
+
const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
|
|
171
|
+
(settings as Record<string, unknown>).scheduledJobs,
|
|
172
|
+
)
|
|
173
|
+
? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
|
|
174
|
+
: [];
|
|
175
|
+
const updated = existingJobs.map((j) => j.id === job.id ? job : j);
|
|
176
|
+
saveCrewSettings(
|
|
177
|
+
{ ...settings, scheduledJobs: updated } as Parameters<typeof saveCrewSettings>[0],
|
|
178
|
+
cwd,
|
|
179
|
+
);
|
|
180
|
+
} catch {
|
|
181
|
+
/* best-effort persistence */
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Remove a scheduled job from persistent settings. */
|
|
186
|
+
function persistScheduledJobRemove(cwd: string, jobId: string): void {
|
|
187
|
+
try {
|
|
188
|
+
const settings = loadCrewSettings(cwd);
|
|
189
|
+
const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
|
|
190
|
+
(settings as Record<string, unknown>).scheduledJobs,
|
|
191
|
+
)
|
|
192
|
+
? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
|
|
193
|
+
: [];
|
|
194
|
+
saveCrewSettings(
|
|
195
|
+
{ ...settings, scheduledJobs: existingJobs.filter((j) => j.id !== jobId) } as Parameters<typeof saveCrewSettings>[0],
|
|
196
|
+
cwd,
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
/* best-effort persistence */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
166
203
|
export function handleListScheduled(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
167
204
|
const scheduler = getCrewScheduler();
|
|
168
205
|
if (!scheduler) return result("Scheduler not running.", { action: "scheduled", status: "error" }, true);
|
|
@@ -177,6 +214,9 @@ export function handleListScheduled(_params: TeamToolParamsValue, ctx: TeamConte
|
|
|
177
214
|
` Next run: ${job.nextRun ?? "(unscheduled)"}`,
|
|
178
215
|
` Runs: ${job.runCount}, Last: ${job.lastRun ?? "(never)"} [${job.lastStatus ?? "?"}]`,
|
|
179
216
|
);
|
|
217
|
+
if (job.spawnedRunIds && job.spawnedRunIds.length > 0) {
|
|
218
|
+
lines.push(` Spawned runs: ${job.spawnedRunIds.join(", ")}`);
|
|
219
|
+
}
|
|
180
220
|
}
|
|
181
221
|
return result(lines.join("\n"), { action: "scheduled", status: "ok" });
|
|
182
222
|
}
|
|
@@ -10,7 +10,7 @@ export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
10
10
|
if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
|
|
11
11
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
12
12
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
|
|
13
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
13
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
14
14
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
|
|
15
15
|
const events = readEvents(loaded.manifest.eventsPath);
|
|
16
16
|
const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
|
|
@@ -21,7 +21,7 @@ export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
21
21
|
if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
|
|
22
22
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
23
23
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
|
|
24
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
24
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
25
25
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
|
|
26
26
|
const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
|
|
27
27
|
return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -31,7 +31,7 @@ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
31
31
|
if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
|
|
32
32
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
33
33
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
|
|
34
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
34
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
35
35
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
|
|
36
36
|
const usage = aggregateUsage(loaded.tasks);
|
|
37
37
|
const lines = [
|