pi-crew 0.10.1 → 0.10.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 +347 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/dist/index.mjs +2555 -932
- package/package.json +2 -1
- package/schema.json +503 -94
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +6 -2
- package/skills/real-test-pi-crew/SKILL.md +278 -79
- package/src/config/config-merge.ts +11 -1
- package/src/config/config-validation.ts +47 -2
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +43 -11
- package/src/config/env-vars.ts +27 -2
- package/src/config/role-tools.ts +4 -2
- package/src/config/types.ts +55 -1
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/run.ts +15 -1
- package/src/extension/registration/commands/shared.ts +8 -0
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +92 -11
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/team-tool.ts +58 -6
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/team-tool/doctor.ts +364 -7
- package/src/extension/team-tool/handle-settings.ts +19 -0
- package/src/extension/team-tool/inspect.ts +10 -2
- package/src/extension/team-tool/run-deadline.ts +20 -3
- package/src/extension/team-tool/run.ts +26 -2
- package/src/extension/team-tool/status.ts +7 -0
- package/src/extension/team-tool.ts +35 -2
- package/src/hooks/registry.ts +59 -56
- package/src/prompt/inbox-poll.ts +90 -0
- package/src/prompt/message-tool.ts +166 -0
- package/src/prompt/prompt-runtime.ts +201 -18
- package/src/prompt/surface-worker.ts +720 -0
- package/src/prompt/worker-events-channel.ts +49 -3
- package/src/runtime/async-runner.ts +29 -1
- package/src/runtime/background-runner.ts +13 -7
- package/src/runtime/broker/broker-issuer.ts +27 -2
- package/src/runtime/broker/crew-broker-tokens.ts +56 -4
- package/src/runtime/broker/crew-broker.ts +261 -41
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +30 -2
- package/src/runtime/child-pi/child-pi.ts +353 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/dispatch-batch.ts +12 -1
- package/src/runtime/event-log-tail-source.ts +374 -0
- package/src/runtime/finalize-run.ts +4 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +30 -3
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +5 -1
- package/src/runtime/live-session/live-agent-manager.ts +34 -1
- package/src/runtime/live-session/live-control-realtime.ts +10 -0
- package/src/runtime/live-session/live-session-runtime.ts +47 -27
- package/src/runtime/manifest-cache.ts +128 -17
- package/src/runtime/merge-gate.ts +25 -9
- package/src/runtime/model/model-fallback.ts +7 -3
- package/src/runtime/model/pi-args.ts +54 -65
- package/src/runtime/output/sidechain-output.ts +61 -6
- package/src/runtime/process/proc-stat.ts +46 -0
- package/src/runtime/process/zombie-scanner.ts +32 -19
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/crash-recovery.ts +16 -0
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/spawn-policy.ts +27 -41
- package/src/runtime/surface/degrade.ts +776 -0
- package/src/runtime/surface/herdr-provider.ts +546 -0
- package/src/runtime/surface/launch-script.ts +172 -0
- package/src/runtime/surface/resolve-surface.ts +274 -0
- package/src/runtime/surface/surface-provider.ts +129 -0
- package/src/runtime/surface/surface-spawn.ts +475 -0
- package/src/runtime/surface/tmux-provider.ts +400 -0
- package/src/runtime/task-runner/child-executor.ts +47 -0
- package/src/runtime/task-runner/post-execution.ts +57 -2
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
- package/src/runtime/task-runner/state-helpers.ts +54 -30
- package/src/runtime/task-runner.ts +4 -2
- package/src/runtime/team-runner.ts +104 -2
- package/src/schema/config-schema.ts +25 -1
- package/src/state/atomic-write.ts +219 -40
- package/src/state/coordination/locks.ts +7 -5
- package/src/state/coordination/mailbox.ts +56 -10
- package/src/state/event-log/cursor.ts +413 -23
- package/src/state/event-log/event-log.ts +120 -113
- package/src/state/event-log/sequence-cache.ts +21 -3
- package/src/state/stores/plan-store.ts +1 -1
- package/src/state/stores/state-store.ts +171 -9
- package/src/state/types.ts +53 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/inline-panel/agent-pane.ts +378 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/render-diff.ts +16 -8
- package/src/ui/run-dashboard.ts +87 -42
- package/src/ui/run-event-bus.ts +10 -1
- package/src/ui/run-snapshot-cache.ts +83 -35
- package/src/ui/transcript-cache.ts +101 -13
- package/src/ui/transcript-viewer.ts +92 -24
- package/src/ui/widget/index.ts +203 -25
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +234 -37
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/visual.ts +43 -0
- package/src/workflows/discover-workflows.ts +1 -0
- package/src/workflows/workflow-config.ts +7 -0
- package/src/worktree/worktree-manager.ts +65 -4
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
|
@@ -21,9 +21,10 @@ import { loadConfig } from "../../config/config.ts";
|
|
|
21
21
|
import { DEFAULT_UI } from "../../config/defaults.ts";
|
|
22
22
|
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
23
23
|
import { pruneFinishedRuns, pruneUserLevelRuns } from "../../extension/run-maintenance.ts";
|
|
24
|
-
import { type BrokerSpawnCredentials, setActiveBrokerIssuer } from "../../runtime/broker/broker-issuer.ts";
|
|
24
|
+
import { type BrokerSpawnCredentials, setActiveBrokerIssuer, setActiveBrokerRevoker } from "../../runtime/broker/broker-issuer.ts";
|
|
25
25
|
import { CrewBroker } from "../../runtime/broker/crew-broker.ts";
|
|
26
26
|
import { terminateActiveChildPiProcesses } from "../../runtime/child-pi/child-pi.ts";
|
|
27
|
+
import { forgetDetachedRun, hasDetachedRuns, peekFinishedDetachedRunResults } from "../../runtime/detached-run-results.ts";
|
|
27
28
|
import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
28
29
|
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
29
30
|
import { configuredModelInfosFromPiConfig } from "../../runtime/model/model-fallback.ts";
|
|
@@ -39,7 +40,9 @@ import { applyCrewSettingsTiersToConfig, loadCrewSettingsTiers } from "../../run
|
|
|
39
40
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
40
41
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
41
42
|
import { summarizeHeartbeats } from "../../ui/heartbeat-aggregator.ts";
|
|
42
|
-
import {
|
|
43
|
+
import { installInlinePanel } from "../../ui/inline-panel/index.ts";
|
|
44
|
+
import { clearSessionSwitchInFlight, markSessionSwitchInFlight } from "../../ui/inline-panel/view-session-store.ts";
|
|
45
|
+
import { requestRender, setExtensionWidget, toPiWidgetPlacement } from "../../ui/pi-ui-compat.ts";
|
|
43
46
|
import {
|
|
44
47
|
registerPiCrewPowerbarSegments,
|
|
45
48
|
requestPowerbarUpdate,
|
|
@@ -75,6 +78,36 @@ export function installSessionLifecycleHandlers(pi: ExtensionAPI, ctx: Registrat
|
|
|
75
78
|
installModelTrackingHandlers(pi);
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Hand over the outcome of runs detached by an agent-view switch.
|
|
83
|
+
*
|
|
84
|
+
* Delivered as a DISPLAYED session entry (not a follow-up queue item): an idle
|
|
85
|
+
* session never flushes a follow-up, so the queued variant left the result
|
|
86
|
+
* invisible. Each result is dropped from the registry only after its send
|
|
87
|
+
* succeeded, and only while the owning session is current — a worker's view
|
|
88
|
+
* session must never receive the parent run's report.
|
|
89
|
+
*/
|
|
90
|
+
function deliverDetachedRunResults(pi: ExtensionAPI, extensionCtx: ExtensionContext): void {
|
|
91
|
+
if (!hasDetachedRuns()) return;
|
|
92
|
+
try {
|
|
93
|
+
// Agent views are in-document panes now, never sessions: the current
|
|
94
|
+
// session is always the main one, so detached results always deliver.
|
|
95
|
+
const inViewSession = false;
|
|
96
|
+
for (const { runId, text } of peekFinishedDetachedRunResults({ inViewSession })) {
|
|
97
|
+
pi.sendMessage({ customType: "pi-crew-run-result", content: text, display: true });
|
|
98
|
+
forgetDetachedRun(runId);
|
|
99
|
+
try {
|
|
100
|
+
extensionCtx.ui.notify(text.split("\n")[0] ?? `pi-crew run finished: ${runId}`, "info");
|
|
101
|
+
} catch {
|
|
102
|
+
/* toast is secondary to the session entry */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Keep the entry: the next tick retries rather than losing the outcome.
|
|
107
|
+
logInternalError("register.detachedRunResults", error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
78
111
|
/**
|
|
79
112
|
* model_select / thinking_level_select:
|
|
80
113
|
* Track what the MAIN session is *actually* running so subagents that
|
|
@@ -125,6 +158,12 @@ function installSessionShutdownHandler(pi: ExtensionAPI, ctx: RegistrationContex
|
|
|
125
158
|
*/
|
|
126
159
|
function installSessionBeforeSwitchHandler(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
127
160
|
pi.on("session_before_switch", () => {
|
|
161
|
+
// The switch tears the current turn down right after this handler
|
|
162
|
+
// (teardownCurrent → session.abort()). That turn-abort must NOT cancel
|
|
163
|
+
// a foreground team run that is still forming (see run-deadline.ts):
|
|
164
|
+
// foreground runs survive session switches (P0). Cleared on the next
|
|
165
|
+
// session_start.
|
|
166
|
+
markSessionSwitchInFlight();
|
|
128
167
|
ctx.sessionGeneration++;
|
|
129
168
|
const pendingCount = ctx.lifecycleState.deliveryCoordinator?.getPendingCount() ?? 0;
|
|
130
169
|
try {
|
|
@@ -167,6 +206,9 @@ function installSessionBeforeSwitchHandler(pi: ExtensionAPI, ctx: RegistrationCo
|
|
|
167
206
|
*/
|
|
168
207
|
function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
169
208
|
pi.on("session_start", (_event, extensionCtx) => {
|
|
209
|
+
// Any session start means a pending switch landed — the turn-abort
|
|
210
|
+
// suppression window for that switch is over.
|
|
211
|
+
clearSessionSwitchInFlight();
|
|
170
212
|
runArtifactCleanup(extensionCtx.cwd);
|
|
171
213
|
|
|
172
214
|
// Restore brief mode state from session entries (best-effort).
|
|
@@ -273,6 +315,13 @@ function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext):
|
|
|
273
315
|
});
|
|
274
316
|
const cache = ctx.getManifestCache(extensionCtx.cwd);
|
|
275
317
|
updateCrewWidget(extensionCtx, ctx.widgetState, loadedConfig.config.ui, cache, ctx.getRunSnapshotCache(extensionCtx.cwd));
|
|
318
|
+
// Inline agent panel: keyboard-navigable rows under the prompt + per-agent
|
|
319
|
+
// transcript pane. Installed after the widget so the row projection sees
|
|
320
|
+
// the same caches the widget paint uses.
|
|
321
|
+
installInlinePanel(pi, extensionCtx, loadedConfig.config.ui);
|
|
322
|
+
// Returning from an agent view lands here: report any detached run that
|
|
323
|
+
// finished while the view was open, without waiting for a render tick.
|
|
324
|
+
deliverDetachedRunResults(pi, extensionCtx);
|
|
276
325
|
updatePiCrewPowerbar(
|
|
277
326
|
pi.events,
|
|
278
327
|
extensionCtx.cwd,
|
|
@@ -632,8 +681,13 @@ function setupRenderLoop(
|
|
|
632
681
|
// file just changed on disk, so force a fresh snapshot while keeping
|
|
633
682
|
// the entry populated — deleting it left a window where the widget's
|
|
634
683
|
// `get()` returned undefined and dropped the run to "(loading…)".
|
|
684
|
+
// PERF (2026-08-24): route through the coalesced ASYNC refresh —
|
|
685
|
+
// fs.watch can fire many times per second and the sync rebuild
|
|
686
|
+
// blocked the UI event loop. The entry stays populated until the
|
|
687
|
+
// async rebuild re-sets it in place; the render schedule below
|
|
688
|
+
// repaints while it lands.
|
|
635
689
|
try {
|
|
636
|
-
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).
|
|
690
|
+
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).scheduleRefresh(runId);
|
|
637
691
|
} catch (error) {
|
|
638
692
|
logInternalError("register.runWatcher.refresh", error, runId);
|
|
639
693
|
}
|
|
@@ -681,13 +735,14 @@ function setupRenderLoop(
|
|
|
681
735
|
|
|
682
736
|
const renderTick = (): void => {
|
|
683
737
|
if (!ctx.currentCtx) return;
|
|
738
|
+
deliverDetachedRunResults(pi, ctx.currentCtx);
|
|
684
739
|
const config = lastPreloadedConfig?.config.ui;
|
|
685
740
|
const activeCache = lastFrameManifestCache ?? ctx.getManifestCache(ctx.currentCtx.cwd);
|
|
686
741
|
const snapshotCache = lastFrameSnapshotCache ?? ctx.getRunSnapshotCache(ctx.currentCtx.cwd);
|
|
687
742
|
const manifests = lastPreloadedManifests;
|
|
688
743
|
if (!lastPreloadedConfig) backgroundPreload();
|
|
689
744
|
if (ctx.uiState.liveSidebarRunId || ctx.uiState.dashboardOpen) {
|
|
690
|
-
const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
745
|
+
const placement = toPiWidgetPlacement(config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement);
|
|
691
746
|
if (ctx.widgetState.lastVisibility !== "hidden" || ctx.widgetState.lastPlacement !== placement) {
|
|
692
747
|
setExtensionWidget(ctx.currentCtx, "pi-crew", undefined, { placement });
|
|
693
748
|
setExtensionWidget(ctx.currentCtx, "pi-crew-active", undefined, { placement });
|
|
@@ -880,8 +935,11 @@ function setupRenderLoop(
|
|
|
880
935
|
// FLICKER FIX: rebuild-in-place instead of deleting the entry (see
|
|
881
936
|
// onRunChange above). A hard delete left `get()` returning undefined for
|
|
882
937
|
// a frame, dropping the run to "(loading…)" and causing visible flicker.
|
|
938
|
+
// PERF (2026-08-24): coalesced ASYNC refresh — fs.watch can fire many
|
|
939
|
+
// times per second and the sync rebuild blocked the UI event loop; the
|
|
940
|
+
// render schedule below repaints while the rebuild lands.
|
|
883
941
|
try {
|
|
884
|
-
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).
|
|
942
|
+
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).scheduleRefresh(runId);
|
|
885
943
|
} catch (error) {
|
|
886
944
|
logInternalError("register.crewRunWatcher.refresh", error, runId);
|
|
887
945
|
}
|
|
@@ -1027,10 +1085,11 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
1027
1085
|
// config.broker.waitMethodsEnabled a dead knob and the ADR-0
|
|
1028
1086
|
// "then true" flip a silent no-op. Fail-closed when unset.
|
|
1029
1087
|
waitMethodsEnabled: cfg?.waitMethodsEnabled ?? false,
|
|
1030
|
-
// T3/R5 (ADR-5 §10): governed-nesting capability gate —
|
|
1031
|
-
//
|
|
1032
|
-
// only). Nested-slot sizing +
|
|
1033
|
-
//
|
|
1088
|
+
// T3/R5 (ADR-5 §10): governed-nesting capability gate — default-on
|
|
1089
|
+
// since D8 (loadConfig layers DEFAULT_NESTING.enabled=true; sensitive
|
|
1090
|
+
// flag, so only USER config may flip it). Nested-slot sizing +
|
|
1091
|
+
// admission-time model catalog (ADR-5 §7 — the production wiring
|
|
1092
|
+
// MUST supply it) + workspace gate mirror.
|
|
1034
1093
|
nestingEnabled: nestingCfg?.nesting?.enabled ?? false,
|
|
1035
1094
|
...(nestingCfg?.nesting?.maxSlots !== undefined ? { nestingMaxSlots: nestingCfg.nesting.maxSlots } : {}),
|
|
1036
1095
|
...(nestingCfg?.nesting?.maxDepth !== undefined ? { nestingMaxDepth: nestingCfg.nesting.maxDepth } : {}),
|
|
@@ -1070,8 +1129,8 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
1070
1129
|
if (!effectiveEnabled()) return undefined;
|
|
1071
1130
|
// ADR-5 §4 (governed nesting): tokens are minted ONLY for children that
|
|
1072
1131
|
// may themselves delegate — childDepth < resolved maxDepth. At the default
|
|
1073
|
-
// maxDepth=
|
|
1074
|
-
// (env containment: no PI_CREW_BROKER_SOCKET/TOKEN at depth
|
|
1132
|
+
// maxDepth=4 a delegate-spawned depth-4 grandchild gets NO credentials
|
|
1133
|
+
// (env containment: no PI_CREW_BROKER_SOCKET/TOKEN at the cap depth; identity
|
|
1075
1134
|
// routing via PI_CREW_BROKER_RUN_ID/TASK_ID is threaded unconditionally
|
|
1076
1135
|
// elsewhere). Undefined childDepth = legacy worker spawn (depth 1).
|
|
1077
1136
|
if (childDepth !== undefined && childDepth >= resolveCrewMaxDepth(undefined)) return undefined;
|
|
@@ -1086,6 +1145,27 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
1086
1145
|
}
|
|
1087
1146
|
};
|
|
1088
1147
|
|
|
1148
|
+
// MuxSurface A1 (spec §7 D3 step 2): publish the revoker alongside the
|
|
1149
|
+
// issuer — team-runner's degrade controller calls it when a pane is lost.
|
|
1150
|
+
// Best-effort and self-gated: no broker bound (never started / session
|
|
1151
|
+
// switched) means nothing to revoke; re-issue after respawn mints fresh (T10).
|
|
1152
|
+
const revokeForTask = (taskId: string): void => {
|
|
1153
|
+
if (!taskId || typeof taskId !== "string") return;
|
|
1154
|
+
if (!isRootSession(process.env)) return;
|
|
1155
|
+
if (!broker || brokerSessionId !== cachedSessionId) return;
|
|
1156
|
+
try {
|
|
1157
|
+
broker.revokeTaskToken(taskId);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
logInternalError(
|
|
1160
|
+
"broker.revoke-for-task",
|
|
1161
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1162
|
+
`taskId=${taskId}`,
|
|
1163
|
+
"warn",
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
setActiveBrokerRevoker(revokeForTask);
|
|
1168
|
+
|
|
1089
1169
|
// Publish this issuer as the process-local active issuer so runChildPi can
|
|
1090
1170
|
// default `brokerIssuer` without the registration context being threaded
|
|
1091
1171
|
// through every runner call site. The issuer self-gates (root + flag), so
|
|
@@ -1096,6 +1176,7 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
1096
1176
|
issueForChild,
|
|
1097
1177
|
stop: async () => {
|
|
1098
1178
|
setActiveBrokerIssuer(undefined);
|
|
1179
|
+
setActiveBrokerRevoker(undefined);
|
|
1099
1180
|
if (broker) {
|
|
1100
1181
|
try {
|
|
1101
1182
|
await broker.stop();
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { loadConfig } from "../../config/config.ts";
|
|
22
22
|
import { clearHooksScoped } from "../../hooks/registry.ts";
|
|
23
|
-
import { terminateActiveChildPiProcesses } from "../../runtime/child-pi/child-pi.ts";
|
|
24
23
|
import { stopAllWatchdogs } from "../../runtime/foreground-watchdog.ts";
|
|
25
24
|
import { clearPiCrewPowerbar, disposePowerbarCoalescer } from "../../ui/powerbar-publisher.ts";
|
|
26
25
|
import { stopCrewWidget } from "../../ui/widget/index.ts";
|
|
27
26
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
28
27
|
import { clearProjectRootCache } from "../../utils/paths.ts";
|
|
28
|
+
import { safeAbortAll } from "../../utils/safe-abort.ts";
|
|
29
29
|
import { extractSessionId } from "../../utils/session-utils.ts";
|
|
30
30
|
import { stopAsyncRunNotifier } from "../async-notifier.ts";
|
|
31
31
|
import { uninstallCrewGlobalRegistry } from "../team-tool.ts";
|
|
@@ -135,7 +135,9 @@ function buildCleanupRuntime(ctx: RegistrationContext): () => void {
|
|
|
135
135
|
ctx.stopSessionBoundSubagents();
|
|
136
136
|
// P0 fix: also abort foreground team runs on session shutdown (not on session switch).
|
|
137
137
|
// This is the only place where foreground team run controllers should be aborted.
|
|
138
|
-
|
|
138
|
+
// safe-abort: shutdown races completed runs whose spawned children already
|
|
139
|
+
// exited — abort() would throw AbortError from Node's child_process listener.
|
|
140
|
+
safeAbortAll(ctx.foregroundTeamRunControllers.values(), "cleanup-runtime.foreground-team");
|
|
139
141
|
ctx.foregroundTeamRunControllers.clear();
|
|
140
142
|
// RC-01: stop the foreground-run watchdog timers on full shutdown — they were
|
|
141
143
|
// never cleared, so an active (non-terminal) foreground run left its watchdog
|
|
@@ -219,10 +221,26 @@ function buildStopSessionBoundSubagents(ctx: RegistrationContext): () => void {
|
|
|
219
221
|
// Only abort subagent controllers — NOT foreground team runs.
|
|
220
222
|
// Foreground team runs are bound to the session lifecycle; they will be aborted
|
|
221
223
|
// by cleanupRuntime during session_shutdown.
|
|
222
|
-
|
|
224
|
+
// safe-abort: these signals may be linked to spawned children (pipelines pass
|
|
225
|
+
// them into spawn options.signal); aborting after a child exited makes Node's
|
|
226
|
+
// child_process listener throw AbortError synchronously — inside a session
|
|
227
|
+
// event handler that would surface as uncaughtException and kill pi.
|
|
228
|
+
safeAbortAll(ctx.foregroundControllers.values(), "stop-session-bound.foreground");
|
|
223
229
|
ctx.foregroundControllers.clear();
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
try {
|
|
231
|
+
ctx.subagentManager.abortAll("Session switching — foreground subagents cancelled.");
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logInternalError("stop-session-bound.abortAll", error);
|
|
234
|
+
}
|
|
235
|
+
// Deliberately NOT terminateActiveChildPiProcesses() here: the active
|
|
236
|
+
// child registry holds EVERY child pi spawned in this process —
|
|
237
|
+
// including foreground team-run workers. Killing them on a session
|
|
238
|
+
// switch (resume/new/fork) is exactly the regression "jumping into any
|
|
239
|
+
// session stops the run" (exit 143). Session-bound subagents are
|
|
240
|
+
// cancelled above via their own controllers; foreground runs must
|
|
241
|
+
// survive switches (P0) and are aborted only by cleanupRuntime on real
|
|
242
|
+
// shutdown; the process-exit hook (tryRegisterSessionCleanup) drains
|
|
243
|
+
// the registry when pi actually exits.
|
|
226
244
|
ctx.disposeRenderSchedulerSubscriptions();
|
|
227
245
|
ctx.renderScheduler?.dispose();
|
|
228
246
|
ctx.renderScheduler = undefined;
|
|
@@ -13,6 +13,7 @@ import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
|
13
13
|
import { statusIcon, type ToolRenderContext, teamToolRenderer } from "../../ui/tool-renderers/index.ts";
|
|
14
14
|
import type { CrewWidgetState } from "../../ui/widget/index.ts";
|
|
15
15
|
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
16
|
+
import { safeAbort } from "../../utils/safe-abort.ts";
|
|
16
17
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
17
18
|
// Team tool handler — lazy-loaded because team-tool.ts imports many modules
|
|
18
19
|
import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
|
|
@@ -146,6 +147,52 @@ export function detectUnrecognizedParams(schema: TObject, params: unknown): stri
|
|
|
146
147
|
].join("\n");
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// PERF (2026-08-24): cheap read actions (status/summary/events/get/list) re-rendered
|
|
151
|
+
// the widget + powerbar after every call; the event bus and render tick already
|
|
152
|
+
// reflect run-state changes. Refresh only on actions that mutate run state
|
|
153
|
+
// outside the watched files. Names mirror the five domain dispatcher tables in
|
|
154
|
+
// src/schema/team-tool-schema.ts (RUN/STATUS/CONTROL/MANAGE/AUTOMATE). Actions
|
|
155
|
+
// whose effect depends on sub-params (api: approve-plan/agent-control/mailbox;
|
|
156
|
+
// plans: approve/reject; anchor/auto-summarize/auto_boomerang: set/clear/toggle;
|
|
157
|
+
// config/autonomy: show vs write) are included — a redundant refresh on their
|
|
158
|
+
// read sub-actions is cheap, a missed refresh after a write leaves stale UI.
|
|
159
|
+
const MUTATING_ACTIONS = new Set([
|
|
160
|
+
// run domain
|
|
161
|
+
"run",
|
|
162
|
+
"parallel",
|
|
163
|
+
"orchestrate",
|
|
164
|
+
"resume",
|
|
165
|
+
"retry",
|
|
166
|
+
"steer",
|
|
167
|
+
"goal",
|
|
168
|
+
"plans",
|
|
169
|
+
// control domain
|
|
170
|
+
"cancel",
|
|
171
|
+
"invalidate",
|
|
172
|
+
"respond",
|
|
173
|
+
"cleanup",
|
|
174
|
+
"prune",
|
|
175
|
+
"forget",
|
|
176
|
+
// manage domain
|
|
177
|
+
"create",
|
|
178
|
+
"update",
|
|
179
|
+
"delete",
|
|
180
|
+
"init",
|
|
181
|
+
"config",
|
|
182
|
+
"autonomy",
|
|
183
|
+
"settings",
|
|
184
|
+
"workflow-create",
|
|
185
|
+
"workflow-save",
|
|
186
|
+
"workflow-delete",
|
|
187
|
+
"import",
|
|
188
|
+
// automate domain
|
|
189
|
+
"schedule",
|
|
190
|
+
"anchor",
|
|
191
|
+
"auto-summarize",
|
|
192
|
+
"auto_boomerang",
|
|
193
|
+
"api",
|
|
194
|
+
]);
|
|
195
|
+
|
|
149
196
|
export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
|
|
150
197
|
const tool: ToolDefinition = {
|
|
151
198
|
name: "team",
|
|
@@ -174,7 +221,10 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
174
221
|
const controller = new AbortController();
|
|
175
222
|
const toolKey = Symbol();
|
|
176
223
|
deps.foregroundControllers.set(toolKey, controller);
|
|
177
|
-
|
|
224
|
+
// safe-abort: pi's tool-call abort can race a completed run whose
|
|
225
|
+
// spawned children already exited — Node's child_process signal
|
|
226
|
+
// listener throws AbortError out of abort() and kills the host.
|
|
227
|
+
const abort = (): void => safeAbort(controller, "team-tool.caller-abort");
|
|
178
228
|
signal?.addEventListener("abort", abort, { once: true });
|
|
179
229
|
const stopProgress = startTeamToolProgressBinder(onUpdate as OnUpdate | undefined);
|
|
180
230
|
try {
|
|
@@ -225,11 +275,13 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
225
275
|
timestamp: Date.now(),
|
|
226
276
|
});
|
|
227
277
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
278
|
+
if (MUTATING_ACTIONS.has(resolved.action ?? "list")) {
|
|
279
|
+
const config = loadConfig(toolCtx.cwd).config.ui;
|
|
280
|
+
const cache = deps.getManifestCache(toolCtx.cwd);
|
|
281
|
+
const snapshotCache = deps.getRunSnapshotCache?.(toolCtx.cwd);
|
|
282
|
+
updateCrewWidget(toolCtx, deps.widgetState, config, cache, snapshotCache);
|
|
283
|
+
updatePiCrewPowerbar(pi.events, toolCtx.cwd, config, cache, snapshotCache, toolCtx);
|
|
284
|
+
}
|
|
233
285
|
return output;
|
|
234
286
|
} finally {
|
|
235
287
|
signal?.removeEventListener("abort", abort);
|
|
@@ -15,7 +15,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
15
15
|
import { loadConfig } from "../../config/config.ts";
|
|
16
16
|
import { DEFAULT_UI } from "../../config/defaults.ts";
|
|
17
17
|
import type { LiveRunSidebar as LiveRunSidebarType } from "../../ui/live-run-sidebar.ts";
|
|
18
|
-
import { requestRender, setExtensionWidget, showCustom } from "../../ui/pi-ui-compat.ts";
|
|
18
|
+
import { requestRender, setExtensionWidget, showCustom, toPiWidgetPlacement } from "../../ui/pi-ui-compat.ts";
|
|
19
19
|
import {
|
|
20
20
|
clearPiCrewPowerbar,
|
|
21
21
|
registerPiCrewPowerbarSegments,
|
|
@@ -76,10 +76,11 @@ export async function installLiveSidebar(ctx: ExtensionContext, runId: string, s
|
|
|
76
76
|
state.liveSidebarRunId = runId;
|
|
77
77
|
state.dashboardOpened = true;
|
|
78
78
|
const widgetPlacement = uiConfig?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
79
|
-
|
|
80
|
-
setExtensionWidget(ctx, "pi-crew
|
|
79
|
+
const placement = toPiWidgetPlacement(widgetPlacement);
|
|
80
|
+
setExtensionWidget(ctx, "pi-crew", undefined, { placement });
|
|
81
|
+
setExtensionWidget(ctx, "pi-crew-active", undefined, { placement });
|
|
81
82
|
deps.widgetState.lastVisibility = "hidden";
|
|
82
|
-
deps.widgetState.lastPlacement =
|
|
83
|
+
deps.widgetState.lastPlacement = placement;
|
|
83
84
|
deps.widgetState.lastKey = "pi-crew-active";
|
|
84
85
|
deps.widgetState.model = undefined;
|
|
85
86
|
const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth));
|