pi-crew 0.11.2 → 0.11.5
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 +83 -1
- package/README.md +1 -1
- package/dist/index.mjs +492 -493
- package/docs/actions-reference.md +22 -0
- package/index.ts +9 -5
- package/package.json +12 -9
- package/scripts/analyze-run.mjs +6 -3
- package/skills/real-test-pi-crew/SKILL.md +20 -12
- package/src/extension/async-notifier.ts +32 -8
- package/src/extension/crew-input-router.ts +9 -2
- package/src/extension/plan-orchestrate.ts +19 -3
- package/src/extension/registration/commands/shared.ts +11 -10
- package/src/extension/registration/foreground-run-controller.ts +2 -2
- package/src/extension/registration/lazy-configurers.ts +4 -1
- package/src/extension/registration/lifecycle-handlers.ts +16 -10
- package/src/extension/registration/lifecycle.ts +2 -0
- package/src/extension/registration/observability.ts +27 -18
- package/src/extension/registration/team-tool.ts +9 -2
- package/src/extension/registration/wire-cross-extension.ts +2 -0
- package/src/extension/team-manager-command.ts +3 -2
- package/src/extension/team-tool/dispatch/index.ts +4 -3
- package/src/extension/team-tool/dispatch/status.ts +7 -3
- package/src/extension/team-tool/goal-wrap.ts +7 -0
- package/src/extension/team-tool/goal.ts +12 -0
- package/src/extension/team-tool/inspect.ts +90 -0
- package/src/extension/team-tool/parallel-dispatch.ts +16 -1
- package/src/extension/team-tool/run-intent.ts +1 -1
- package/src/extension/team-tool/run.ts +1 -1
- package/src/runtime/async-runner.ts +36 -0
- package/src/runtime/broker/crew-broker.ts +61 -62
- package/src/runtime/broker/delegate/delegate-event.ts +12 -8
- package/src/runtime/broker/protocol/connection-state.ts +10 -0
- package/src/runtime/broker/protocol/lock-busy.ts +76 -0
- package/src/runtime/delegate-spawn.ts +16 -2
- package/src/runtime/deterministic-ast.ts +14 -2
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +78 -58
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +14 -2
- package/src/runtime/goal-workflow/goal-loop-runner.ts +32 -0
- package/src/runtime/group-join.ts +11 -3
- package/src/runtime/live-session/live-session-runtime.ts +1 -1
- package/src/runtime/model/runtime-warmup.ts +2 -0
- package/src/runtime/output/output-validator.ts +10 -5
- package/src/runtime/plan-approval.ts +14 -1
- package/src/runtime/recovery/crash-recovery.ts +80 -52
- package/src/runtime/stale-reconciler.ts +8 -0
- package/src/runtime/subagent-manager.ts +43 -13
- package/src/runtime/task-output-context.ts +11 -1
- package/src/runtime/task-runner/child-executor.ts +17 -6
- package/src/runtime/task-runner/post-execution.ts +46 -14
- package/src/runtime/task-runner/pre-execution.ts +29 -21
- package/src/runtime/task-runner/prompt-builder.ts +8 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +36 -14
- package/src/runtime/task-runner.ts +7 -0
- package/src/runtime/team-runner.ts +4 -1
- package/src/runtime/verification/verification-worktree.ts +3 -0
- package/src/schema/team-tool-schema.ts +2 -1
- package/src/state/coordination/mailbox.ts +7 -2
- package/src/state/stores/state-store.ts +8 -1
- package/src/ui/dashboard-panes/agents-pane.ts +11 -8
- package/src/ui/inline-panel/index.ts +1 -1
- package/src/ui/render-diff.ts +19 -6
- package/src/ui/run-dashboard.ts +11 -1
- package/src/ui/run-snapshot-cache.ts +9 -3
- package/src/ui/widget/task-list.ts +8 -5
- package/src/ui/widget/widget-model.ts +3 -1
- package/tsconfig.json +2 -0
- package/src/ui/card-colors.ts +0 -168
- package/src/utils/completion-dedupe.ts +0 -63
- package/src/utils/resolve-shell.ts +0 -36
|
@@ -71,7 +71,11 @@ export interface ObservabilityDeps {
|
|
|
71
71
|
* newer session. Reuse this counter — do not invent a parallel one.
|
|
72
72
|
*/
|
|
73
73
|
getSessionGeneration: () => number;
|
|
74
|
-
reconcileStaleRuns: (
|
|
74
|
+
reconcileStaleRuns: (
|
|
75
|
+
cwd: string,
|
|
76
|
+
cache: ReturnType<ObservabilityDeps["getManifestCache"]>,
|
|
77
|
+
currentSessionId?: string,
|
|
78
|
+
) => unknown[] | Promise<unknown[] | undefined>;
|
|
75
79
|
reconcileOrphanedTempWorkspaces: (now: number, opts: { cleanupOrphanedTempDirs?: boolean }) => unknown;
|
|
76
80
|
cleanupOrphanTempDirs: () => { cleaned: number; scanned: number; failed: number };
|
|
77
81
|
cleanupLegacyOrphanTempDirs: () => { cleaned: number; scanned: number; failed: number };
|
|
@@ -253,26 +257,31 @@ async function configureObservabilityInternal(ctx: ExtensionContext, state: Obse
|
|
|
253
257
|
if (autoRepairIntervalMs > 0) {
|
|
254
258
|
state.autoRepairTimer = setInterval(() => {
|
|
255
259
|
if (deps.isCleanedUp()) return;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
260
|
+
// RR-021 WI-1.5: reconcileStaleRuns may be async (mapConcurrent) —
|
|
261
|
+
// settle the result before inspecting, keep errors inside the log.
|
|
262
|
+
void (async () => {
|
|
263
|
+
try {
|
|
264
|
+
const settled = await deps.reconcileStaleRuns(ctx.cwd, deps.getManifestCache(ctx.cwd), extractSessionId(ctx));
|
|
265
|
+
const staleResults = Array.isArray(settled) ? settled : [];
|
|
266
|
+
if (staleResults.length > 0) {
|
|
267
|
+
for (const result of staleResults) {
|
|
268
|
+
const repaired = (result as { repaired?: boolean }).repaired;
|
|
269
|
+
if (repaired) {
|
|
270
|
+
deps.notifyOperator({
|
|
271
|
+
id: `auto_repair_${(result as { runId: string }).runId}`,
|
|
272
|
+
severity: "info",
|
|
273
|
+
source: "auto-repair",
|
|
274
|
+
runId: (result as { runId: string }).runId,
|
|
275
|
+
title: `Auto-repaired stale run`,
|
|
276
|
+
body: (result as { detail?: string }).detail ?? "",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
270
279
|
}
|
|
271
280
|
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logInternalError("register.autoRepair", error);
|
|
272
283
|
}
|
|
273
|
-
}
|
|
274
|
-
logInternalError("register.autoRepair", error);
|
|
275
|
-
}
|
|
284
|
+
})();
|
|
276
285
|
}, autoRepairIntervalMs);
|
|
277
286
|
state.autoRepairTimer.unref();
|
|
278
287
|
|
|
@@ -2,7 +2,7 @@ import { statSync } from "node:fs";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import type { TObject } from "@sinclair/typebox";
|
|
5
|
-
import {
|
|
5
|
+
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
6
6
|
import { loadConfig } from "../../config/config.ts";
|
|
7
7
|
import { findClosestKey } from "../../config/suggestions.ts";
|
|
8
8
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
@@ -36,6 +36,12 @@ import { withSessionId } from "../team-tool/context.ts";
|
|
|
36
36
|
import { formatTeamToolParamError } from "../team-tool/param-error.ts";
|
|
37
37
|
import { toolResult } from "../tool-result.ts";
|
|
38
38
|
|
|
39
|
+
// RR-021 WI-4.3g: compile the team-tool schema ONCE. Value.Check re-walks the
|
|
40
|
+
// whole (large) schema object on every tool call; TypeCompiler.Compile (STATIC
|
|
41
|
+
// method on @sinclair/typebox/compiler) builds a specialized checker at module
|
|
42
|
+
// load. The error formatter keeps using the schema + Value.Errors (cold path).
|
|
43
|
+
const compiledTeamToolParams = TypeCompiler.Compile(TeamToolParams);
|
|
44
|
+
|
|
39
45
|
/**
|
|
40
46
|
* Normalize model-generated tool params before schema validation.
|
|
41
47
|
*
|
|
@@ -228,7 +234,8 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
228
234
|
// validation so strict unions/patterns accept "unset" as omitted.
|
|
229
235
|
const normalized = normalizeTeamParams(params);
|
|
230
236
|
// Defense-in-depth: validate params at runtime even though Pi framework already does
|
|
231
|
-
|
|
237
|
+
// (RR-021 WI-4.3g: precompiled checker — same semantics as Value.Check.)
|
|
238
|
+
if (!compiledTeamToolParams.Check(normalized)) {
|
|
232
239
|
return toolResult(formatTeamToolParamError(TeamToolParams, normalized), { action: "list", status: "error" }, true);
|
|
233
240
|
}
|
|
234
241
|
// EXT-1: additionalProperties:true lets unknown (typo'd) keys slip past
|
|
@@ -28,6 +28,7 @@ export function installCrossExtensionWiring(pi: ExtensionAPI, ctx: RegistrationC
|
|
|
28
28
|
// in the entire runtime chain) out of the cold-start module graph. EXT-9:
|
|
29
29
|
// the registry now lives in module-scoped state, not globalThis.
|
|
30
30
|
void import("../team-tool.ts").then(({ installCrewGlobalRegistry }) => {
|
|
31
|
+
// LAZY: defer team-tool until cross-extension wiring fires
|
|
31
32
|
const manifestCacheForRegistry = ctx.getManifestCache(ctx.currentCtx?.cwd ?? process.cwd());
|
|
32
33
|
installCrewGlobalRegistry({
|
|
33
34
|
manifestCache: manifestCacheForRegistry,
|
|
@@ -57,6 +58,7 @@ export function refreshCrossExtensionWiringForSession(pi: ExtensionAPI, ctx: Reg
|
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
void import("../team-tool.ts").then(({ installCrewGlobalRegistry }) => {
|
|
61
|
+
// LAZY: defer team-tool until cross-extension wiring fires
|
|
60
62
|
installCrewGlobalRegistry({
|
|
61
63
|
manifestCache: ctx.getManifestCache(ctx.currentCtx?.cwd ?? process.cwd()),
|
|
62
64
|
cwdProvider: () => ctx.currentCtx?.cwd ?? process.cwd(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { listRecentRuns } from "./run-index.ts";
|
|
3
3
|
// Lazy-loaded: team-tool.ts pulls in entire runtime chain.
|
|
4
4
|
import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
|
|
5
5
|
|
|
@@ -122,7 +122,8 @@ export async function handleTeamManagerCommand(_args: string, ctx: ExtensionComm
|
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// RR-021 WI-4.1: bounded recent-runs read instead of a full index scan.
|
|
126
|
+
const runs = listRecentRuns(ctx.cwd, 20);
|
|
126
127
|
if (runs.length === 0) {
|
|
127
128
|
ctx.ui.notify("No pi-crew runs found.", "info");
|
|
128
129
|
return;
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 54→5 mapping:
|
|
9
9
|
* run (9): run, parallel, plan, orchestrate, resume, retry, wait, steer, goal
|
|
10
|
-
* status (
|
|
11
|
-
* health, worktrees, checkpoint, cache, explain, onboard, recommend, help
|
|
10
|
+
* status (17): status, list, get, events, artifacts, breakdown, summary, graph,
|
|
11
|
+
* search, health, worktrees, checkpoint, cache, explain, onboard, recommend, help
|
|
12
12
|
* control (7): cancel, invalidate, respond, cleanup, prune, forget, doctor
|
|
13
13
|
* manage (17): create, update, delete, init, config, validate, autonomy, settings,
|
|
14
14
|
* workflow-create/get/list/save/delete, import, imports, export, compare
|
|
@@ -36,12 +36,13 @@ const ACTION_TO_DOMAIN: Record<string, TeamDomain> = {
|
|
|
36
36
|
steer: "run",
|
|
37
37
|
goal: "run",
|
|
38
38
|
|
|
39
|
-
// status domain (
|
|
39
|
+
// status domain (17)
|
|
40
40
|
status: "status",
|
|
41
41
|
list: "status",
|
|
42
42
|
get: "status",
|
|
43
43
|
events: "status",
|
|
44
44
|
artifacts: "status",
|
|
45
|
+
breakdown: "status",
|
|
45
46
|
summary: "status",
|
|
46
47
|
graph: "status",
|
|
47
48
|
search: "status",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API-5 facade dispatch — Status domain router.
|
|
3
3
|
*
|
|
4
|
-
* Actions: status, list, get, events, artifacts, summary, graph,
|
|
5
|
-
* health, worktrees, checkpoint, cache, explain, onboard, recommend,
|
|
4
|
+
* Actions: status, list, get, events, artifacts, breakdown, summary, graph,
|
|
5
|
+
* search, health, worktrees, checkpoint, cache, explain, onboard, recommend,
|
|
6
|
+
* help.
|
|
6
7
|
*
|
|
7
8
|
* Inline cases (graph, search, onboard, explain, cache, checkpoint, recommend,
|
|
8
9
|
* help) are moved verbatim from the former handleTeamTool switch.
|
|
@@ -28,7 +29,7 @@ import type { PiTeamsToolResult } from "../../tool-result.ts";
|
|
|
28
29
|
import { result, type TeamContext } from "../context.ts";
|
|
29
30
|
import { handleExplain } from "../explain.ts";
|
|
30
31
|
import { handleHealthMonitor } from "../health-monitor.ts";
|
|
31
|
-
import { handleArtifacts, handleEvents, handleSummary } from "../inspect.ts";
|
|
32
|
+
import { handleArtifacts, handleBreakdown, handleEvents, handleSummary } from "../inspect.ts";
|
|
32
33
|
import { handleWorktrees } from "../lifecycle-actions.ts";
|
|
33
34
|
import { handleStatus } from "../status.ts";
|
|
34
35
|
|
|
@@ -45,6 +46,7 @@ export const STATUS_DOMAIN_ACTIONS = [
|
|
|
45
46
|
"get",
|
|
46
47
|
"events",
|
|
47
48
|
"artifacts",
|
|
49
|
+
"breakdown",
|
|
48
50
|
"summary",
|
|
49
51
|
"graph",
|
|
50
52
|
"search",
|
|
@@ -73,6 +75,8 @@ export async function handleStatusDomain(params: TeamToolParamsValue, ctx: TeamC
|
|
|
73
75
|
return handleEvents(params, ctx);
|
|
74
76
|
case "artifacts":
|
|
75
77
|
return handleArtifacts(params, ctx);
|
|
78
|
+
case "breakdown":
|
|
79
|
+
return handleBreakdown(params, ctx);
|
|
76
80
|
case "worktrees":
|
|
77
81
|
return handleWorktrees(params, ctx);
|
|
78
82
|
case "summary":
|
|
@@ -21,6 +21,7 @@ import { loadConfig } from "../../config/config.ts";
|
|
|
21
21
|
import type { GoalWrapWorkflowConfig } from "../../config/types.ts";
|
|
22
22
|
import { spawnBackgroundTeamRun } from "../../runtime/async-runner.ts";
|
|
23
23
|
import { GoalStore } from "../../runtime/goal-workflow/goal-state-store.ts";
|
|
24
|
+
import { captureRunModelContext } from "../../runtime/model/session-model.ts";
|
|
24
25
|
import { snapshotManifests } from "../../runtime/verification/verification-integrity.ts";
|
|
25
26
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
26
27
|
import { atomicWriteJson } from "../../state/atomic-write.ts";
|
|
@@ -248,6 +249,11 @@ export async function startGoalWrappedRun(
|
|
|
248
249
|
store.save(goalState);
|
|
249
250
|
|
|
250
251
|
const paths = createRunPaths(cwd, goalId);
|
|
252
|
+
// Finding #1 (battery 2026-09-29): persist the parent session's model
|
|
253
|
+
// routing on the goal-loop manifest — the DETACHED runner re-loads this
|
|
254
|
+
// manifest and has no ExtensionContext to re-resolve it. Without it,
|
|
255
|
+
// goal-wrap turns silently default-chain (minimax 402 family).
|
|
256
|
+
const modelContext = captureRunModelContext(ctx, typeof params.model === "string" ? params.model : undefined);
|
|
251
257
|
const goalLoopManifest: TeamRunManifest = {
|
|
252
258
|
schemaVersion: 1,
|
|
253
259
|
runId: goalId,
|
|
@@ -267,6 +273,7 @@ export async function startGoalWrappedRun(
|
|
|
267
273
|
artifacts: [],
|
|
268
274
|
ownerSessionId,
|
|
269
275
|
runKind: "goal-loop",
|
|
276
|
+
...(modelContext ? { modelContext } : {}),
|
|
270
277
|
};
|
|
271
278
|
await saveRunManifestAsync(goalLoopManifest);
|
|
272
279
|
await appendEventAsync(paths.eventsPath, {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { spawnBackgroundTeamRun } from "../../runtime/async-runner.ts";
|
|
18
18
|
import { GoalStore } from "../../runtime/goal-workflow/goal-state-store.ts";
|
|
19
|
+
import { captureRunModelContext } from "../../runtime/model/session-model.ts";
|
|
19
20
|
import { snapshotManifests } from "../../runtime/verification/verification-integrity.ts";
|
|
20
21
|
import { isWorkspaceBusy } from "../../runtime/workspace-lock.ts";
|
|
21
22
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
@@ -173,6 +174,12 @@ async function handleStart(input: GoalSubActionInput): Promise<ReturnType<typeof
|
|
|
173
174
|
// runs/<goalId>/ → "Run not found" → silent death. (Review #2 F1.)
|
|
174
175
|
const paths = createRunPaths(cwd, goalId);
|
|
175
176
|
const now2 = new Date().toISOString();
|
|
177
|
+
// Finding #1 (battery 2026-09-29): the goal-loop manifest is loaded by the
|
|
178
|
+
// DETACHED background-runner (no ExtensionContext). Persist the parent
|
|
179
|
+
// session's model routing so goal-loop turns restore it instead of falling
|
|
180
|
+
// back to the default chain (minimax 402 incident family). goalState keeps
|
|
181
|
+
// workerModel/evaluatorModel as the explicit per-role picks.
|
|
182
|
+
const modelContext = captureRunModelContext(ctx, typeof params.model === "string" ? params.model : undefined);
|
|
176
183
|
const goalLoopManifest: TeamRunManifest = {
|
|
177
184
|
schemaVersion: 1,
|
|
178
185
|
runId: goalId, // paths.runId === goalId by construction
|
|
@@ -192,6 +199,7 @@ async function handleStart(input: GoalSubActionInput): Promise<ReturnType<typeof
|
|
|
192
199
|
artifacts: [],
|
|
193
200
|
ownerSessionId,
|
|
194
201
|
runKind: "goal-loop",
|
|
202
|
+
...(modelContext ? { modelContext } : {}),
|
|
195
203
|
};
|
|
196
204
|
await saveRunManifestAsync(goalLoopManifest);
|
|
197
205
|
await appendEventAsync(paths.eventsPath, {
|
|
@@ -449,6 +457,9 @@ async function handleResume(input: GoalSubActionInput): Promise<ReturnType<typeo
|
|
|
449
457
|
// Re-spawn the background loop. The loop checks goal.state === "running" before each
|
|
450
458
|
// turn; since we just set it to running, it proceeds.
|
|
451
459
|
try {
|
|
460
|
+
// Finding #1: re-capture at resume time (same battery) — the resuming
|
|
461
|
+
// session's live model is authoritative for subsequent turns.
|
|
462
|
+
const modelContext = captureRunModelContext(ctx, typeof params.model === "string" ? params.model : undefined);
|
|
452
463
|
const manifest: TeamRunManifest = {
|
|
453
464
|
schemaVersion: 1,
|
|
454
465
|
runId: goalId,
|
|
@@ -468,6 +479,7 @@ async function handleResume(input: GoalSubActionInput): Promise<ReturnType<typeo
|
|
|
468
479
|
artifacts: [],
|
|
469
480
|
ownerSessionId: existing.ownerSessionId,
|
|
470
481
|
runKind: "goal-loop",
|
|
482
|
+
...(modelContext ? { modelContext } : {}),
|
|
471
483
|
};
|
|
472
484
|
const spawned = await spawnBackgroundTeamRun(manifest);
|
|
473
485
|
return result(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
4
|
import { readEventsCursor } from "../../state/event-log/event-log.ts";
|
|
3
5
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
@@ -110,3 +112,91 @@ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
110
112
|
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
111
113
|
});
|
|
112
114
|
}
|
|
115
|
+
|
|
116
|
+
// ─── G18 (SDD 2026-09-30 WI-3): prompt-token breakdown surface ──────────────
|
|
117
|
+
|
|
118
|
+
/** File suffix of the per-task breakdown artifact written by pre-execution
|
|
119
|
+
* (`metadata/<taskId>.prompt-breakdown.json`) when PI_CREW_PROMPT_BREAKDOWN=1. */
|
|
120
|
+
const BREAKDOWN_FILE_SUFFIX = ".prompt-breakdown.json";
|
|
121
|
+
|
|
122
|
+
/** Shape of one section entry in a prompt-breakdown JSON artifact
|
|
123
|
+
* (see writeArtifact call in src/runtime/task-runner/pre-execution.ts). */
|
|
124
|
+
interface PromptBreakdownSection {
|
|
125
|
+
chars: number;
|
|
126
|
+
estTokens: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* G18: read the opt-in per-task prompt-token breakdown artifacts of a run and
|
|
131
|
+
* surface per-task totals + the top-5 largest sections. Consumers: the leader
|
|
132
|
+
* prompt (future W-F token decisions, G15/G17) and humans debugging prompt
|
|
133
|
+
* bloat. The artifacts exist only when the host session was started with
|
|
134
|
+
* PI_CREW_PROMPT_BREAKDOWN=1 (default off — zero cost when off, SR-02).
|
|
135
|
+
* Mirrors the handleArtifacts pattern: resolve run → read the manifest
|
|
136
|
+
* artifacts index → filter kind=metadata breakdown entries → read files.
|
|
137
|
+
*/
|
|
138
|
+
export function handleBreakdown(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
139
|
+
if (!params.runId)
|
|
140
|
+
return result(
|
|
141
|
+
paramRequired("breakdown", "runId", "{ action: 'breakdown', runId: 'team_...' }"),
|
|
142
|
+
{ action: "breakdown", status: "error" },
|
|
143
|
+
true,
|
|
144
|
+
);
|
|
145
|
+
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
146
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "breakdown", status: "error" }, true);
|
|
147
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
148
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "breakdown", status: "error" }, true);
|
|
149
|
+
// Index-driven lookup (no disk scan): the manifest artifacts index carries
|
|
150
|
+
// one metadata entry per task with a prompt breakdown.
|
|
151
|
+
const breakdownArtifacts = loaded.manifest.artifacts.filter(
|
|
152
|
+
(artifact) => artifact.kind === "metadata" && path.basename(artifact.path).endsWith(BREAKDOWN_FILE_SUFFIX),
|
|
153
|
+
);
|
|
154
|
+
const perTask: Array<{
|
|
155
|
+
taskId: string;
|
|
156
|
+
totalEstTokens: number;
|
|
157
|
+
topSections: Array<{ section: string; estTokens: number; chars: number }>;
|
|
158
|
+
}> = [];
|
|
159
|
+
for (const artifact of breakdownArtifacts) {
|
|
160
|
+
const taskId = path.basename(artifact.path).slice(0, -BREAKDOWN_FILE_SUFFIX.length);
|
|
161
|
+
if (params.taskId && taskId !== params.taskId) continue;
|
|
162
|
+
let sections: Record<string, PromptBreakdownSection>;
|
|
163
|
+
try {
|
|
164
|
+
sections = JSON.parse(fs.readFileSync(artifact.path, "utf-8")) as Record<string, PromptBreakdownSection>;
|
|
165
|
+
} catch {
|
|
166
|
+
continue; // unreadable/corrupt breakdown JSON — skip rather than fail the whole action
|
|
167
|
+
}
|
|
168
|
+
const entries = Object.entries(sections).filter((entry): entry is [string, PromptBreakdownSection] => {
|
|
169
|
+
const value = entry[1];
|
|
170
|
+
return value !== null && typeof value === "object" && typeof value.estTokens === "number";
|
|
171
|
+
});
|
|
172
|
+
const totalEstTokens = entries.reduce((sum, [, value]) => sum + value.estTokens, 0);
|
|
173
|
+
const topSections = [...entries]
|
|
174
|
+
.sort((a, b) => b[1].estTokens - a[1].estTokens)
|
|
175
|
+
.slice(0, 5)
|
|
176
|
+
.map(([section, value]) => ({ section, estTokens: value.estTokens, chars: value.chars ?? 0 }));
|
|
177
|
+
perTask.push({ taskId, totalEstTokens, topSections });
|
|
178
|
+
}
|
|
179
|
+
if (perTask.length === 0) {
|
|
180
|
+
const scope = params.taskId ? ` for task '${params.taskId}'` : "";
|
|
181
|
+
return result(
|
|
182
|
+
`No prompt-breakdown artifacts${scope} for ${loaded.manifest.runId}. Breakdown capture is opt-in: start the host Pi session with PI_CREW_PROMPT_BREAKDOWN=1 and re-run to record per-section token estimates.`,
|
|
183
|
+
{ action: "breakdown", status: "ok", runId: loaded.manifest.runId, data: { runTotal: 0, perTask: [] } },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const runTotal = perTask.reduce((sum, task) => sum + task.totalEstTokens, 0);
|
|
187
|
+
const lines = [
|
|
188
|
+
`Prompt-token breakdown for ${loaded.manifest.runId}:`,
|
|
189
|
+
...perTask.flatMap((task) => [
|
|
190
|
+
`- ${task.taskId}: ~${task.totalEstTokens} tokens`,
|
|
191
|
+
...task.topSections.map((section) => ` - ${section.section}: ~${section.estTokens} tokens (${section.chars} chars)`),
|
|
192
|
+
]),
|
|
193
|
+
`Run total: ~${runTotal} tokens`,
|
|
194
|
+
];
|
|
195
|
+
return result(lines.join("\n"), {
|
|
196
|
+
action: "breakdown",
|
|
197
|
+
status: "ok",
|
|
198
|
+
runId: loaded.manifest.runId,
|
|
199
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
200
|
+
data: { runTotal, perTask },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
@@ -10,6 +10,7 @@ import { discoverAgents } from "../../agents/discover-agents.ts";
|
|
|
10
10
|
import { loadConfig } from "../../config/config.ts";
|
|
11
11
|
import { spawnBackgroundTeamRun } from "../../runtime/async-runner.ts";
|
|
12
12
|
import { resolveCrewRuntime } from "../../runtime/model/runtime-resolver.ts";
|
|
13
|
+
import { captureRunModelContext } from "../../runtime/model/session-model.ts";
|
|
13
14
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
14
15
|
import { appendEventAsync } from "../../state/event-log/event-log.ts";
|
|
15
16
|
import { createRunManifest } from "../../state/stores/state-store.ts";
|
|
@@ -66,13 +67,23 @@ export async function handleParallel(params: TeamToolParamsValue, ctx: TeamConte
|
|
|
66
67
|
|
|
67
68
|
const runtime = await resolveCrewRuntime(config.config);
|
|
68
69
|
|
|
70
|
+
// Finding #1 (full battery 2026-09-29): parallel-dispatched runs re-enter
|
|
71
|
+
// through the detached background-runner, which has no ExtensionContext.
|
|
72
|
+
// Without a persisted modelContext, restoredModelRouting() returns {} and
|
|
73
|
+
// every worker silently falls back to the DEFAULT chain (live: 3 runs died
|
|
74
|
+
// on minimax/MiniMax-M3 402 while the parent session ran zai/glm-5.3).
|
|
75
|
+
// Captured ONCE here (in the parent session's process) so resolveParentModel()
|
|
76
|
+
// sees the live session model; undefined when nothing is worth persisting
|
|
77
|
+
// (older manifests stay byte-identical).
|
|
78
|
+
const modelContext = captureRunModelContext(ctx, typeof params.model === "string" ? params.model : undefined);
|
|
79
|
+
|
|
69
80
|
const launched: Array<{ runId: string; goal: string; agent: string }> = [];
|
|
70
81
|
const errors: Array<{ goal: string; error: string }> = [];
|
|
71
82
|
|
|
72
83
|
// C1: Enforce concurrency limit with batched spawning
|
|
73
84
|
for (let batchStart = 0; batchStart < tasksParam.length; batchStart += concurrency) {
|
|
74
85
|
const batch = tasksParam.slice(batchStart, batchStart + concurrency);
|
|
75
|
-
const batchPromises = batch.map((task) => spawnSingleTask(task, ctx, allAgentsList, team, workflow, runtime));
|
|
86
|
+
const batchPromises = batch.map((task) => spawnSingleTask(task, ctx, allAgentsList, team, workflow, runtime, modelContext));
|
|
76
87
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
77
88
|
for (const res of batchResults) {
|
|
78
89
|
if (res.status === "fulfilled" && res.value.ok) {
|
|
@@ -121,6 +132,7 @@ async function spawnSingleTask(
|
|
|
121
132
|
team: TeamConfig,
|
|
122
133
|
workflow: WorkflowConfig,
|
|
123
134
|
runtime: { available: boolean; kind: string },
|
|
135
|
+
modelContext: ReturnType<typeof captureRunModelContext>,
|
|
124
136
|
): Promise<SpawnResult> {
|
|
125
137
|
try {
|
|
126
138
|
const taskRec = task as Record<string, unknown>;
|
|
@@ -163,6 +175,9 @@ async function spawnSingleTask(
|
|
|
163
175
|
// skipped and any session can cancel/retry parallel-dispatched runs.
|
|
164
176
|
ownerSessionId: ctx.sessionId,
|
|
165
177
|
runKind: "team-run",
|
|
178
|
+
// Finding #1: persist the parent session's model routing so the detached
|
|
179
|
+
// runner restores it instead of default-chaining (see handleParallel).
|
|
180
|
+
modelContext,
|
|
166
181
|
});
|
|
167
182
|
|
|
168
183
|
await appendEventAsync(created.manifest.eventsPath, {
|
|
@@ -73,7 +73,7 @@ const MAX_ANALYSIS_BYTES = 100_000;
|
|
|
73
73
|
var crewInitPromise: Promise<typeof import("../../state/crew-init.ts")> | undefined;
|
|
74
74
|
function loadCrewInit(): Promise<typeof import("../../state/crew-init.ts")> {
|
|
75
75
|
if (!crewInitPromise) {
|
|
76
|
-
crewInitPromise = import("../../state/crew-init.ts");
|
|
76
|
+
crewInitPromise = import("../../state/crew-init.ts"); // LAZY: defer crew-init until first run intent
|
|
77
77
|
}
|
|
78
78
|
return crewInitPromise;
|
|
79
79
|
}
|
|
@@ -61,7 +61,7 @@ import type { RuntimeResolutionState, TeamRunManifest, TeamTaskState } from "../
|
|
|
61
61
|
var crewInitPromise: Promise<typeof import("../../state/crew-init.ts")> | undefined;
|
|
62
62
|
function loadCrewInit(): Promise<typeof import("../../state/crew-init.ts")> {
|
|
63
63
|
if (!crewInitPromise) {
|
|
64
|
-
crewInitPromise = import("../../state/crew-init.ts");
|
|
64
|
+
crewInitPromise = import("../../state/crew-init.ts"); // LAZY: defer crew-init until first run intent
|
|
65
65
|
}
|
|
66
66
|
return crewInitPromise;
|
|
67
67
|
}
|
|
@@ -240,6 +240,16 @@ export const BACKGROUND_RUNNER_ENV_ALLOWLIST: string[] = [
|
|
|
240
240
|
"HERDR_SOCKET_PATH",
|
|
241
241
|
"HERDR_WORKSPACE_ID",
|
|
242
242
|
"HERDR_PING_TIMEOUT_MS",
|
|
243
|
+
// Model-selection control vars (Finding #1, full battery 2026-09-29): the
|
|
244
|
+
// detached runner resolves subagent model routing from env (PI_CREW_MODEL →
|
|
245
|
+
// resolveDefaultSubagentModel; PI_CREW_MODEL_FALLBACK_ORDER →
|
|
246
|
+
// resolveModelFallbackPolicy) for manifests without a persisted modelContext
|
|
247
|
+
// (older manifests) and as a user override channel. Stripping them made
|
|
248
|
+
// every async run silently fall back to the DEFAULT chain (live: 3 parallel
|
|
249
|
+
// runs died on minimax/MiniMax-M3 402 while the parent ran zai/glm-5.3).
|
|
250
|
+
// PI_CREW_MODEL_REQUIRE_CREDENTIALS is NOT here — see forwardModelControlEnv.
|
|
251
|
+
"PI_CREW_MODEL",
|
|
252
|
+
"PI_CREW_MODEL_FALLBACK_ORDER",
|
|
243
253
|
];
|
|
244
254
|
|
|
245
255
|
/**
|
|
@@ -256,6 +266,29 @@ export function buildBackgroundRunnerEnv(env: NodeJS.ProcessEnv): NodeJS.Process
|
|
|
256
266
|
return { ...env, PI_CREW_ASYNC_RUN: "1" };
|
|
257
267
|
}
|
|
258
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Model-selection control vars that CANNOT live on
|
|
271
|
+
* {@link BACKGROUND_RUNNER_ENV_ALLOWLIST}: isSecretKey() matches the
|
|
272
|
+
* "_CREDENTIALS" suffix of PI_CREW_MODEL_REQUIRE_CREDENTIALS (keyword
|
|
273
|
+
* "credential"), and sanitizeEnvSecrets THROWS on secret-looking allowlist
|
|
274
|
+
* entries that are absent from the spawning env — so allowlisting it would
|
|
275
|
+
* break EVERY async spawn whenever the flag is unset. It is a declared
|
|
276
|
+
* control flag (config/env-vars.ts, read by resolveModelFallbackPolicy),
|
|
277
|
+
* never a secret, so we forward it explicitly when set instead.
|
|
278
|
+
* Exported pure so tests pin the contract (async-runner.test.ts).
|
|
279
|
+
*/
|
|
280
|
+
export const MODEL_CONTROL_ENV_EXPLICIT: readonly string[] = ["PI_CREW_MODEL_REQUIRE_CREDENTIALS"];
|
|
281
|
+
|
|
282
|
+
/** Copy the explicitly-forwarded model control vars (set ones only). */
|
|
283
|
+
export function forwardModelControlEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
284
|
+
const forwarded: NodeJS.ProcessEnv = {};
|
|
285
|
+
for (const key of MODEL_CONTROL_ENV_EXPLICIT) {
|
|
286
|
+
const value = env[key];
|
|
287
|
+
if (value !== undefined) forwarded[key] = value;
|
|
288
|
+
}
|
|
289
|
+
return forwarded;
|
|
290
|
+
}
|
|
291
|
+
|
|
259
292
|
/** F4 v2: the ONE line written to the background-runner's stdin carrying
|
|
260
293
|
* PER-TASK compound broker tokens (see stdin-handshake.ts for why compound —
|
|
261
294
|
* ADR-0 item 6 rejects bare-runId tokens for wait.*). Kept pure + exported so
|
|
@@ -377,6 +410,9 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
|
|
|
377
410
|
const childEnv = buildBackgroundRunnerEnv({
|
|
378
411
|
...(peerDepDir ? { ...filteredEnv, [PEER_DEP_DIR_ENV]: peerDepDir } : filteredEnv),
|
|
379
412
|
PI_CREW_BACKGROUND_RUNNER_ENTRY: "1",
|
|
413
|
+
// Finding #1: model control vars that cannot ride the allowlist (see
|
|
414
|
+
// forwardModelControlEnv). Copied from the ORIGINAL env, post-sanitize.
|
|
415
|
+
...forwardModelControlEnv(process.env),
|
|
380
416
|
});
|
|
381
417
|
|
|
382
418
|
const loader = resolveTypeScriptLoader();
|