pi-crew 0.9.44 → 0.9.47
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 +136 -0
- package/README.md +38 -3
- package/dist/build-meta.json +349 -203
- package/dist/index.mjs +2229 -2968
- package/dist/index.mjs.map +4 -4
- package/docs/decisions/2026-07-21-broker-phase4-default-on.md +77 -0
- package/docs/decisions/2026-07-21-broker-windows-perms.md +91 -0
- package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +99 -0
- package/docs/decisions/README.md +3 -0
- package/docs/publishing.md +26 -0
- package/package.json +3 -1
- package/scripts/build-bundle.mjs +7 -0
- package/scripts/postinstall.mjs +35 -1
- package/scripts/pty_probe.py +174 -0
- package/skills/real-test-pi-crew/SKILL.md +659 -0
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config.ts +42 -1
- package/src/config/defaults.ts +45 -1
- package/src/config/types.ts +19 -0
- package/src/extension/register.ts +6 -1
- package/src/extension/registration/context-builder.ts +4 -0
- package/src/extension/registration/lifecycle-handlers.ts +200 -6
- package/src/extension/registration/registration-types.ts +9 -0
- package/src/extension/registration/subagent-manager-setup.ts +178 -59
- package/src/extension/run-import.ts +21 -1
- package/src/extension/team-tool/api.ts +4 -2
- package/src/prompt/prompt-runtime.ts +108 -0
- package/src/runtime/async-runner.ts +9 -1
- package/src/runtime/broker-issuer.ts +37 -0
- package/src/runtime/child-pi-spawn.ts +53 -0
- package/src/runtime/child-pi.ts +42 -11
- package/src/runtime/crew-broker-child.ts +88 -0
- package/src/runtime/crew-broker-client.ts +673 -0
- package/src/runtime/crew-broker-tokens.ts +84 -0
- package/src/runtime/crew-broker.ts +1276 -0
- package/src/runtime/dynamic-workflow-context.ts +7 -3
- package/src/runtime/dynamic-workflow-runner.ts +1 -1
- package/src/runtime/manifest-cache.ts +30 -0
- package/src/runtime/plan-templates.ts +8 -6
- package/src/runtime/resilient-edit.ts +16 -15
- package/src/runtime/role-permission.ts +27 -2
- package/src/runtime/run-coalesced-task-group.ts +72 -15
- package/src/runtime/task-packet.ts +1 -1
- package/src/schema/config-schema.ts +14 -0
- package/src/state/event-log.ts +88 -34
- package/src/state/locks.ts +53 -0
- package/src/state/mailbox.ts +208 -4
- package/src/state/run-metrics.ts +40 -12
- package/src/ui/key-utils.ts +42 -0
- package/src/ui/keybinding-map.ts +29 -3
- package/src/ui/live-run-sidebar.ts +1 -9
- package/src/ui/run-dashboard.ts +29 -9
- package/src/ui/settings-overlay.ts +42 -22
- package/src/utils/incremental-reader.ts +105 -0
- package/src/utils/ndjson.ts +115 -0
- package/src/utils/session-utils.ts +30 -0
- package/src/utils/socket-path.ts +127 -0
- package/src/utils/visual.ts +27 -91
- package/workflows/default.workflow.md +1 -1
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/src/runtime/auto-resume.ts +0 -100
- package/src/runtime/notebook-helpers.ts +0 -88
- package/src/runtime/orphan-sentinel.ts +0 -7
package/src/config/config.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
|
8
8
|
import { withFileLockSync } from "../state/locks.ts";
|
|
9
9
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
10
10
|
import { projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
|
|
11
|
+
import { DEFAULT_BROKER, resolveBrokerEnvOverride } from "./defaults.ts";
|
|
11
12
|
import { suggestConfigKey } from "./suggestions.ts";
|
|
12
13
|
|
|
13
14
|
// 2.9: interface types extracted to ./types.ts; re-export for back-compat.
|
|
@@ -44,6 +45,7 @@ import type {
|
|
|
44
45
|
AgentOverrideConfig,
|
|
45
46
|
ConfigValidationResult,
|
|
46
47
|
CrewAgentsConfig,
|
|
48
|
+
CrewBrokerConfig,
|
|
47
49
|
CrewControlConfig,
|
|
48
50
|
CrewLimitsConfig,
|
|
49
51
|
CrewNotificationsConfig,
|
|
@@ -744,6 +746,38 @@ function parseControlConfig(value: unknown): CrewControlConfig | undefined {
|
|
|
744
746
|
return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
|
|
745
747
|
}
|
|
746
748
|
|
|
749
|
+
/**
|
|
750
|
+
* Phase 0 broker parser. Returns `undefined` only when input is not an object;
|
|
751
|
+
* otherwise returns the broker config (with only defined fields populated)
|
|
752
|
+
* so the caller can layer defaults on top.
|
|
753
|
+
*/
|
|
754
|
+
function parseBrokerConfig(value: unknown): CrewBrokerConfig | undefined {
|
|
755
|
+
const obj = asRecord(value);
|
|
756
|
+
if (!obj) return undefined;
|
|
757
|
+
// Use the exact schema bounds (4..32 / 1024..1048576 / 32..4096). The
|
|
758
|
+
// previous version used parsePositiveInteger(value, default) which clamps
|
|
759
|
+
// the UPPER bound to the default — effectively pathHashLen was capped
|
|
760
|
+
// at 8, much narrower than the schema advertises.
|
|
761
|
+
const broker: CrewBrokerConfig = {
|
|
762
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
763
|
+
pathHashLen: parseIntegerInRange(obj.pathHashLen, 4, 32),
|
|
764
|
+
maxFrameBytes: parseIntegerInRange(obj.maxFrameBytes, 1024, 1_048_576),
|
|
765
|
+
outboundQueueCap: parseIntegerInRange(obj.outboundQueueCap, 32, 4096),
|
|
766
|
+
};
|
|
767
|
+
return Object.values(broker).some((entry) => entry !== undefined) ? broker : undefined;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Apply PI_CREW_BROKER env override to the parsed broker config, then
|
|
772
|
+
* layer in DEFAULT_BROKER for any field the user did not set. Keeps the
|
|
773
|
+
* kill switch (enabled:false) reachable in three independent ways: env,
|
|
774
|
+
* config block, or default.
|
|
775
|
+
*/
|
|
776
|
+
function applyBrokerEnvOverrideAndDefaults(parsed: CrewBrokerConfig | undefined): CrewBrokerConfig {
|
|
777
|
+
const envOverridden = resolveBrokerEnvOverride(parsed);
|
|
778
|
+
return { ...DEFAULT_BROKER, ...envOverridden };
|
|
779
|
+
}
|
|
780
|
+
|
|
747
781
|
function parseWorktreeConfig(value: unknown): CrewWorktreeConfig | undefined {
|
|
748
782
|
const obj = asRecord(value);
|
|
749
783
|
if (!obj) return undefined;
|
|
@@ -1019,6 +1053,7 @@ export function parseConfig(raw: unknown): PiTeamsConfig {
|
|
|
1019
1053
|
reliability: parseReliabilityConfig(obj.reliability),
|
|
1020
1054
|
otlp: parseOtlpConfig(obj.otlp),
|
|
1021
1055
|
ui: parseUiConfig(obj.ui),
|
|
1056
|
+
broker: parseBrokerConfig(obj.broker),
|
|
1022
1057
|
};
|
|
1023
1058
|
}
|
|
1024
1059
|
|
|
@@ -1164,7 +1199,13 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
|
1164
1199
|
const result: LoadedPiTeamsConfig = {
|
|
1165
1200
|
path: filePath,
|
|
1166
1201
|
paths,
|
|
1167
|
-
config
|
|
1202
|
+
config: {
|
|
1203
|
+
...config,
|
|
1204
|
+
// Phase 0 broker: layer in env override + defaults. Env wins over
|
|
1205
|
+
// config; defaults fill any missing field. Env `"1"`/`"0"` forces
|
|
1206
|
+
// the enabled flag even when no broker block is configured.
|
|
1207
|
+
broker: applyBrokerEnvOverrideAndDefaults(config.broker),
|
|
1208
|
+
},
|
|
1168
1209
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
1169
1210
|
};
|
|
1170
1211
|
// Only cache when at least one of the watched paths exists — this avoids
|
package/src/config/defaults.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CrewBrokerConfig } from "./types.ts";
|
|
2
|
+
|
|
1
3
|
export const DEFAULT_CHILD_PI: Readonly<{
|
|
2
4
|
postExitStdioGuardMs: number;
|
|
3
5
|
finalDrainMs: number;
|
|
@@ -20,7 +22,7 @@ export const DEFAULT_CHILD_PI: Readonly<{
|
|
|
20
22
|
hardKillMs: 3000,
|
|
21
23
|
// Child workers can spend more than a few seconds in provider calls or long-running tools without emitting stdout.
|
|
22
24
|
// Keep this as a coarse stuck-worker guard rather than a short per-message latency budget.
|
|
23
|
-
responseTimeoutMs:
|
|
25
|
+
responseTimeoutMs: 10 * 60_000,
|
|
24
26
|
// #3 unresponsive worker hardening: increased from 256KB to 512KB so critical
|
|
25
27
|
// diagnostic stderr is less likely to be silently truncated during hang analysis.
|
|
26
28
|
maxCaptureBytes: 512 * 1024,
|
|
@@ -147,3 +149,45 @@ export const DEFAULT_MAILBOX = {
|
|
|
147
149
|
export const DEFAULT_SUBAGENT = {
|
|
148
150
|
stuckBlockedNotifyMs: 5 * 60_000,
|
|
149
151
|
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Phase 0 inter-pi broker defaults.
|
|
155
|
+
* Phase 4 (v0.9.47): default is ON (enabled:true). The broker runs
|
|
156
|
+
* automatically for users on supported platforms (Linux + macOS).
|
|
157
|
+
* Three independent ways to disable:
|
|
158
|
+
* 1. `broker.enabled: false` in user config
|
|
159
|
+
* 2. env `PI_CREW_BROKER=0` (beats config=true)
|
|
160
|
+
* 3. (Windows) auto-disabled — broker requires unix socket which Windows
|
|
161
|
+
* supports only via WSL1/2; native Windows perm model lacks the
|
|
162
|
+
* abstract-socket guarantees the broker relies on. See
|
|
163
|
+
* docs/decisions/2026-07-21-broker-windows-perms.md.
|
|
164
|
+
* Limits are bounded by `src/schema/config-schema.ts` CrewBrokerConfigSchema:
|
|
165
|
+
* pathHashLen 4..32 (default 8)
|
|
166
|
+
* maxFrameBytes 1024..1048576 (default 262144 = 256 KiB)
|
|
167
|
+
* outboundQueueCap 32..4096 (default 256)
|
|
168
|
+
*/
|
|
169
|
+
export const DEFAULT_BROKER = {
|
|
170
|
+
enabled: true,
|
|
171
|
+
pathHashLen: 8,
|
|
172
|
+
maxFrameBytes: 262144,
|
|
173
|
+
outboundQueueCap: 256,
|
|
174
|
+
} as const;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Apply `PI_CREW_BROKER` env override to a parsed broker config.
|
|
178
|
+
* - `"1"` forces `enabled: true` (beats a config of `false` AND the
|
|
179
|
+
* default of `false` when no broker block is configured).
|
|
180
|
+
* - `"0"` forces `enabled: false` (beats a config of `true`).
|
|
181
|
+
* - unset / any other value falls through to the parsed value (which
|
|
182
|
+
* may itself be `undefined`; the loadConfig merge layer fills defaults).
|
|
183
|
+
* Phase 0 scope: only the `enabled` flag is overridable via env; numeric
|
|
184
|
+
* bounds must go through the schema + parser path, not env.
|
|
185
|
+
*/
|
|
186
|
+
export function resolveBrokerEnvOverride(parsed: CrewBrokerConfig | undefined): CrewBrokerConfig | undefined {
|
|
187
|
+
const override = process.env.PI_CREW_BROKER;
|
|
188
|
+
if (override === "1" || override === "0") {
|
|
189
|
+
const base: CrewBrokerConfig = parsed ?? {};
|
|
190
|
+
return { ...base, enabled: override === "1" };
|
|
191
|
+
}
|
|
192
|
+
return parsed;
|
|
193
|
+
}
|
package/src/config/types.ts
CHANGED
|
@@ -224,6 +224,25 @@ export interface PiTeamsConfig {
|
|
|
224
224
|
reliability?: CrewReliabilityConfig;
|
|
225
225
|
otlp?: CrewOtlpConfig;
|
|
226
226
|
ui?: CrewUiConfig;
|
|
227
|
+
/**
|
|
228
|
+
* Inter-pi broker (Phase 0). Local-only socket transport between parent
|
|
229
|
+
* and child pi workers. Default is OFF (`enabled:false`) — keep it that
|
|
230
|
+
* way until Phase 4 soak completes. Numeric limits are bounded by the
|
|
231
|
+
* schema (`src/schema/config-schema.ts`).
|
|
232
|
+
*/
|
|
233
|
+
broker?: CrewBrokerConfig;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** CrewBroker config (Phase 0 inter-pi broker). */
|
|
237
|
+
export interface CrewBrokerConfig {
|
|
238
|
+
/** Master switch. `false` keeps the broker fully dormant. */
|
|
239
|
+
enabled?: boolean;
|
|
240
|
+
/** Length of the SHA-256 hex prefix used in the socket filename. 4..32. */
|
|
241
|
+
pathHashLen?: number;
|
|
242
|
+
/** Maximum NDJSON frame size in UTF-8 bytes (default 262144 = 256 KiB). 1024..1048576. */
|
|
243
|
+
maxFrameBytes?: number;
|
|
244
|
+
/** Per-connection outbound queue cap. 32..4096 (default 256). */
|
|
245
|
+
outboundQueueCap?: number;
|
|
227
246
|
}
|
|
228
247
|
|
|
229
248
|
export interface LoadedPiTeamsConfig {
|
|
@@ -36,7 +36,7 @@ import { importCrashRecovery, purgeStaleActiveRunIndexSyncIfLoaded } from "./reg
|
|
|
36
36
|
import { installForegroundRunController } from "./registration/foreground-run-controller.ts";
|
|
37
37
|
import { installPiHooks } from "./registration/hook-registration.ts";
|
|
38
38
|
import { installLazyConfigurers } from "./registration/lazy-configurers.ts";
|
|
39
|
-
import { installSessionLifecycleHandlers } from "./registration/lifecycle-handlers.ts";
|
|
39
|
+
import { installCrewBrokerLifecycleController, installSessionLifecycleHandlers } from "./registration/lifecycle-handlers.ts";
|
|
40
40
|
import { installRuntimeCleanup } from "./registration/runtime-cleanup.ts";
|
|
41
41
|
import { __test__subagentSpawnParams } from "./registration/subagent-helpers.ts";
|
|
42
42
|
import { installSubagentManager } from "./registration/subagent-manager-setup.ts";
|
|
@@ -73,6 +73,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
73
73
|
registerPiCommands(pi, ctx);
|
|
74
74
|
installPiHooks(pi, ctx);
|
|
75
75
|
installSessionLifecycleHandlers(pi, ctx);
|
|
76
|
+
// Phase 0 inter-pi broker: install the lifecycle controller immediately
|
|
77
|
+
// after the session handlers. The controller's gate (broker.enabled AND
|
|
78
|
+
// root-session only) decides whether anything is actually done; for
|
|
79
|
+
// subagents or when the flag is off, it returns a no-op controller.
|
|
80
|
+
ctx.brokerController = installCrewBrokerLifecycleController(pi, ctx);
|
|
76
81
|
|
|
77
82
|
registerCleanupHandler(pi);
|
|
78
83
|
registerCompactionGuard(pi, {
|
|
@@ -102,6 +102,10 @@ export function buildRegistrationContext(pi: ExtensionAPI): RegistrationContext
|
|
|
102
102
|
startForegroundRun: undefined as never,
|
|
103
103
|
abortForegroundRun: () => false,
|
|
104
104
|
openLiveSidebar: () => {},
|
|
105
|
+
// Phase 0 inter-pi broker controller. Initially undefined; register.ts
|
|
106
|
+
// replaces this with a real controller (or no-op) after lifecycle
|
|
107
|
+
// handlers are installed.
|
|
108
|
+
brokerController: undefined,
|
|
105
109
|
};
|
|
106
110
|
|
|
107
111
|
ctx.notifyOperator = (notification: NotificationDescriptor): void => {
|
|
@@ -20,11 +20,13 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
20
20
|
import { loadConfig } from "../../config/config.ts";
|
|
21
21
|
import { DEFAULT_UI } from "../../config/defaults.ts";
|
|
22
22
|
import { pruneFinishedRuns, pruneUserLevelRuns } from "../../extension/run-maintenance.ts";
|
|
23
|
+
import { type BrokerSpawnCredentials, setActiveBrokerIssuer } from "../../runtime/broker-issuer.ts";
|
|
23
24
|
import { reconcileAllStaleRuns } from "../../runtime/crash-recovery.ts";
|
|
25
|
+
import { CrewBroker } from "../../runtime/crew-broker.ts";
|
|
24
26
|
import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
|
|
25
27
|
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
26
28
|
import { cleanupOrphanWorkers } from "../../runtime/orphan-worker-registry.ts";
|
|
27
|
-
import { cleanupLegacyOrphanTempDirs, cleanupOrphanTempDirs } from "../../runtime/pi-args.ts";
|
|
29
|
+
import { cleanupLegacyOrphanTempDirs, cleanupOrphanTempDirs, currentCrewDepth } from "../../runtime/pi-args.ts";
|
|
28
30
|
import { CrewScheduler, type ScheduledJob } from "../../runtime/scheduler.ts";
|
|
29
31
|
import { tryRegisterSessionCleanup } from "../../runtime/session-resources.ts";
|
|
30
32
|
import { createSessionSnapshot } from "../../runtime/session-snapshot.ts";
|
|
@@ -47,7 +49,8 @@ import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
|
47
49
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
48
50
|
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
49
51
|
import { RunWatcherRegistry } from "../../utils/run-watcher-registry.ts";
|
|
50
|
-
import {
|
|
52
|
+
import { extractBrokerSessionId } from "../../utils/session-utils.ts";
|
|
53
|
+
import { getBrokerSocketPath } from "../../utils/socket-path.ts";
|
|
51
54
|
import { startAsyncRunNotifier, stopAsyncRunNotifier } from "../async-notifier.ts";
|
|
52
55
|
import { registerCrewAutocomplete } from "../crew-autocomplete.ts";
|
|
53
56
|
import { notifyActiveRuns } from "../session-summary.ts";
|
|
@@ -169,7 +172,11 @@ function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext):
|
|
|
169
172
|
ctx.widgetState.interval = undefined;
|
|
170
173
|
notifyActiveRuns(extensionCtx);
|
|
171
174
|
|
|
172
|
-
const currentSessionId =
|
|
175
|
+
const currentSessionId = extractBrokerSessionId(extensionCtx);
|
|
176
|
+
// Phase 0 broker: feed the captured session_id to the controller so
|
|
177
|
+
// it can issue tokens for child runs in this session. The controller
|
|
178
|
+
// already gates by flag + root-session; this is a no-op when disabled.
|
|
179
|
+
ctx.brokerController?.setSessionId(currentSessionId);
|
|
173
180
|
|
|
174
181
|
// Defer ALL heavy cleanup to after the session_start handler returns.
|
|
175
182
|
// These operations involve synchronous directory scanning (readdirSync, readFileSync)
|
|
@@ -521,7 +528,15 @@ function setupRenderLoop(
|
|
|
521
528
|
{
|
|
522
529
|
const onRunChange = (runId: string): void => {
|
|
523
530
|
if (ctx.cleanedUp || ctx.sessionGeneration !== ownerGeneration) return;
|
|
524
|
-
|
|
531
|
+
// FLICKER FIX: rebuild-in-place instead of deleting the entry. The
|
|
532
|
+
// file just changed on disk, so force a fresh snapshot while keeping
|
|
533
|
+
// the entry populated — deleting it left a window where the widget's
|
|
534
|
+
// `get()` returned undefined and dropped the run to "(loading…)".
|
|
535
|
+
try {
|
|
536
|
+
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).refresh(runId);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
logInternalError("register.runWatcher.refresh", error, runId);
|
|
539
|
+
}
|
|
525
540
|
ctx.renderScheduler?.schedule({ runId });
|
|
526
541
|
};
|
|
527
542
|
const onWatchErr = (error: unknown): void => {
|
|
@@ -678,7 +693,23 @@ function setupRenderLoop(
|
|
|
678
693
|
typeof (payload as { runId: unknown }).runId === "string"
|
|
679
694
|
? (payload as { runId: string }).runId
|
|
680
695
|
: undefined;
|
|
681
|
-
|
|
696
|
+
// FLICKER FIX: never hard-delete snapshot entries from a render-scheduler
|
|
697
|
+
// invalidate. A no-runId payload — emitted by EVERY fallback tick
|
|
698
|
+
// (~every 160ms while a run is active) — previously ran
|
|
699
|
+
// `invalidate(undefined)` → `entries.clear()`, wiping ALL snapshots.
|
|
700
|
+
// The next `renderTick` then saw `get() === undefined` for every run,
|
|
701
|
+
// so `activeWidgetRuns` dropped them to "(loading…)" until the async
|
|
702
|
+
// preload rebuilt the cache — an endless visible flicker. For a
|
|
703
|
+
// specific runId we now refresh-if-stale (stale-while-revalidate) so
|
|
704
|
+
// the widget always sees a populated snapshot; a no-runId tick does
|
|
705
|
+
// nothing (renderTick itself repaints; the cache's own
|
|
706
|
+
// run:state/worker:lifecycle subscription refreshes affected runs).
|
|
707
|
+
if (!runId) return;
|
|
708
|
+
try {
|
|
709
|
+
ctx.getRunSnapshotCache(extensionCtx.cwd).refreshIfStale(runId);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
logInternalError("register.renderScheduler.refresh", error, runId);
|
|
712
|
+
}
|
|
682
713
|
},
|
|
683
714
|
});
|
|
684
715
|
// Fix D: bridge internal runEventBus events to renderScheduler so the UI
|
|
@@ -697,7 +728,14 @@ function setupRenderLoop(
|
|
|
697
728
|
// Bounded run watcher setup (pts/2 hang fix 2026-06-16).
|
|
698
729
|
const crewRunWatcherOnChange = (runId: string): void => {
|
|
699
730
|
if (ctx.cleanedUp || ctx.sessionGeneration !== ownerGeneration) return;
|
|
700
|
-
|
|
731
|
+
// FLICKER FIX: rebuild-in-place instead of deleting the entry (see
|
|
732
|
+
// onRunChange above). A hard delete left `get()` returning undefined for
|
|
733
|
+
// a frame, dropping the run to "(loading…)" and causing visible flicker.
|
|
734
|
+
try {
|
|
735
|
+
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).refresh(runId);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
logInternalError("register.crewRunWatcher.refresh", error, runId);
|
|
738
|
+
}
|
|
701
739
|
ctx.renderScheduler?.schedule({ runId });
|
|
702
740
|
};
|
|
703
741
|
const crewRunWatcherOnError = (error: unknown): void => {
|
|
@@ -729,3 +767,159 @@ function setupRenderLoop(
|
|
|
729
767
|
// watchers for any runs that are already active on session start.
|
|
730
768
|
backgroundPreload();
|
|
731
769
|
}
|
|
770
|
+
|
|
771
|
+
// =============================================================================
|
|
772
|
+
// Phase 0 inter-pi broker lifecycle controller (sub-task 0.5)
|
|
773
|
+
// =============================================================================
|
|
774
|
+
//
|
|
775
|
+
// `installCrewBrokerLifecycleController` wires the per-session broker into
|
|
776
|
+
// the existing extension lifecycle. The controller:
|
|
777
|
+
//
|
|
778
|
+
// - is a no-op unless broker.enabled is true AND the current process is the
|
|
779
|
+
// root pi session (PI_CREW_KIND !== "subagent" AND currentCrewDepth === 0).
|
|
780
|
+
// Children NEVER install a broker.
|
|
781
|
+
// - lazily constructs a single CrewBroker instance per session_id. listen()
|
|
782
|
+
// is deferred until the first child run actually requests broker credentials.
|
|
783
|
+
// - issues a heap-only token per child run via `issueForChild`. The token
|
|
784
|
+
// NEVER leaves the parent's heap and the child's env (PI_CREW_BROKER_TOKEN).
|
|
785
|
+
// It is never written to disk.
|
|
786
|
+
// - retains the broker across session switches when the session_id is
|
|
787
|
+
// unchanged; on a session_id change, stops the old broker and binds a new
|
|
788
|
+
// one on the next acquire.
|
|
789
|
+
// - stops the broker during session_shutdown BEFORE the runtime cleanup path.
|
|
790
|
+
//
|
|
791
|
+
// The gate is re-evaluated on every `issueForChild` call (cheap env+depth
|
|
792
|
+
// check) so the kill switch (PI_CREW_BROKER=0) takes effect immediately.
|
|
793
|
+
|
|
794
|
+
export interface CrewBrokerLifecycleController {
|
|
795
|
+
/** Issue credentials for a child run. Returns undefined when the broker
|
|
796
|
+
* is disabled, this process is a subagent, or no session_id is known. */
|
|
797
|
+
issueForChild(runId: string): Promise<BrokerSpawnCredentials | undefined>;
|
|
798
|
+
/** Stop the broker (idempotent). Called on session_shutdown. */
|
|
799
|
+
stop(): Promise<void>;
|
|
800
|
+
/** Test/lifecycle seam: remember the most recent session_id for token issuance. */
|
|
801
|
+
setSessionId(sessionId: string | undefined): void;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function isRootSession(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
805
|
+
if (env.PI_CREW_KIND === "subagent") return false;
|
|
806
|
+
try {
|
|
807
|
+
return currentCrewDepth(env) === 0;
|
|
808
|
+
} catch {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: RegistrationContext): CrewBrokerLifecycleController {
|
|
814
|
+
let broker: CrewBroker | null = null;
|
|
815
|
+
let brokerSessionId: string | undefined;
|
|
816
|
+
let starting: Promise<CrewBroker> | null = null;
|
|
817
|
+
let cachedSessionId: string | undefined;
|
|
818
|
+
|
|
819
|
+
function effectiveEnabled(): boolean {
|
|
820
|
+
// Env wins over config. PI_CREW_BROKER=1 forces on, =0 forces off.
|
|
821
|
+
const envOverride = process.env.PI_CREW_BROKER;
|
|
822
|
+
if (envOverride === "0") return false;
|
|
823
|
+
// Config block: read fresh so a runtime config update takes effect.
|
|
824
|
+
try {
|
|
825
|
+
const cfg = loadConfig().config.broker;
|
|
826
|
+
if (envOverride === "1") return cfg !== undefined ? cfg.enabled !== false : true;
|
|
827
|
+
// Phase 4 (v0.9.47) default-on: enabled unless explicitly disabled.
|
|
828
|
+
return cfg?.enabled !== false;
|
|
829
|
+
} catch {
|
|
830
|
+
// Fail-safe: config load failed → keep broker disabled.
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function getOrStartBroker(sessionId: string): Promise<CrewBroker> {
|
|
836
|
+
if (broker && brokerSessionId === sessionId) return broker;
|
|
837
|
+
if (broker && brokerSessionId !== sessionId) {
|
|
838
|
+
try {
|
|
839
|
+
await broker.stop();
|
|
840
|
+
} catch {
|
|
841
|
+
/* ignore */
|
|
842
|
+
}
|
|
843
|
+
broker = null;
|
|
844
|
+
brokerSessionId = undefined;
|
|
845
|
+
}
|
|
846
|
+
if (!broker && !starting) {
|
|
847
|
+
starting = (async () => {
|
|
848
|
+
const cfg = (() => {
|
|
849
|
+
try {
|
|
850
|
+
return loadConfig().config.broker;
|
|
851
|
+
} catch {
|
|
852
|
+
return undefined;
|
|
853
|
+
}
|
|
854
|
+
})();
|
|
855
|
+
const b = new CrewBroker({
|
|
856
|
+
sessionId,
|
|
857
|
+
socketPath: getBrokerSocketPath(sessionId),
|
|
858
|
+
maxFrameBytes: cfg?.maxFrameBytes ?? 262144,
|
|
859
|
+
outboundQueueCap: cfg?.outboundQueueCap ?? 256,
|
|
860
|
+
enabled: true,
|
|
861
|
+
cwd: process.cwd(),
|
|
862
|
+
});
|
|
863
|
+
try {
|
|
864
|
+
await b.start();
|
|
865
|
+
broker = b;
|
|
866
|
+
brokerSessionId = sessionId;
|
|
867
|
+
return b;
|
|
868
|
+
} finally {
|
|
869
|
+
starting = null;
|
|
870
|
+
}
|
|
871
|
+
})();
|
|
872
|
+
}
|
|
873
|
+
return starting!;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const issueForChild = async (runId: string): Promise<BrokerSpawnCredentials | undefined> => {
|
|
877
|
+
if (!runId || typeof runId !== "string") return undefined;
|
|
878
|
+
if (!isRootSession(process.env)) return undefined;
|
|
879
|
+
if (!effectiveEnabled()) return undefined;
|
|
880
|
+
const sessionId = cachedSessionId;
|
|
881
|
+
if (!sessionId) return undefined;
|
|
882
|
+
try {
|
|
883
|
+
const b = await getOrStartBroker(sessionId);
|
|
884
|
+
const token = b.issueRunToken(runId);
|
|
885
|
+
return { socketPath: b.socketPath, token };
|
|
886
|
+
} catch {
|
|
887
|
+
return undefined;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
// Publish this issuer as the process-local active issuer so runChildPi can
|
|
892
|
+
// default `brokerIssuer` without the registration context being threaded
|
|
893
|
+
// through every runner call site. The issuer self-gates (root + flag), so
|
|
894
|
+
// publishing it unconditionally is safe even when the broker is disabled.
|
|
895
|
+
setActiveBrokerIssuer(issueForChild);
|
|
896
|
+
|
|
897
|
+
return {
|
|
898
|
+
issueForChild,
|
|
899
|
+
stop: async () => {
|
|
900
|
+
setActiveBrokerIssuer(undefined);
|
|
901
|
+
if (broker) {
|
|
902
|
+
try {
|
|
903
|
+
await broker.stop();
|
|
904
|
+
} catch {
|
|
905
|
+
/* ignore */
|
|
906
|
+
}
|
|
907
|
+
broker = null;
|
|
908
|
+
brokerSessionId = undefined;
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
/** Test seam: remember the most recent session_id for token issuance. */
|
|
912
|
+
setSessionId: (sessionId: string | undefined) => {
|
|
913
|
+
// Runtime validation: cap the length and charset to defend against
|
|
914
|
+
// a hostile extension supplying a huge or pathological id. The
|
|
915
|
+
// socket path is already hash-derived (4..32 hex), so a longer
|
|
916
|
+
// sessionId is harmless on disk but wastes heap.
|
|
917
|
+
if (typeof sessionId !== "string") return;
|
|
918
|
+
if (sessionId.length === 0 || sessionId.length > 256) return;
|
|
919
|
+
cachedSessionId = sessionId;
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Marker used by tests to confirm the controller object identity. */
|
|
925
|
+
export const __test__brokerControllerMarker = true;
|
|
@@ -146,4 +146,13 @@ export interface RegistrationContext {
|
|
|
146
146
|
startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
147
147
|
abortForegroundRun: (runId: string) => boolean;
|
|
148
148
|
openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
|
|
149
|
+
/**
|
|
150
|
+
* Phase 0 inter-pi broker lifecycle controller. Set by `register.ts`
|
|
151
|
+
* after `installSessionLifecycleHandlers`. The controller is a no-op
|
|
152
|
+
* when the broker is disabled or the current process is a subagent;
|
|
153
|
+
* callers (e.g. child-pi-spawn consumers) MUST handle the case where
|
|
154
|
+
* `issueForChild` returns undefined. The controller is stopped
|
|
155
|
+
* during session_shutdown via `stop()`.
|
|
156
|
+
*/
|
|
157
|
+
brokerController: import("./lifecycle-handlers.ts").CrewBrokerLifecycleController | undefined;
|
|
149
158
|
}
|