pi-subagents 0.56.0 → 0.57.0
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 +53 -0
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +111 -15
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +2 -2
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +11 -1
- package/docs/workflows.md +20 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/src/agents/agent-management.ts +13 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agents.ts +219 -48
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-registry.ts +8 -2
- package/src/api/preflight.ts +2 -2
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +18 -4
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +20 -5
- package/src/extension/schemas.ts +4 -3
- package/src/extension/tool-description.ts +11 -9
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +46 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +100 -9
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +5 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +9 -5
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +177 -44
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +72 -10
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +270 -81
- package/src/runs/foreground/workflow-detach-reconcile.ts +16 -1
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +348 -55
- package/src/runs/shared/model-exclusions.ts +57 -7
- package/src/runs/shared/model-fallback.ts +23 -1
- package/src/runs/shared/mutation-evidence.ts +7 -2
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +7 -1
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +118 -9
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +47 -21
- package/src/workflows/scripted-workflow.ts +248 -27
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +119 -4
|
@@ -20,14 +20,30 @@ type RecordModelFailureOptions = ModelExclusionTarget & {
|
|
|
20
20
|
|
|
21
21
|
let exclusions: ModelExclusion[] = [];
|
|
22
22
|
let loaded = false;
|
|
23
|
-
|
|
23
|
+
/** Default duration for a new model exclusion when no per-record TTL is supplied. */
|
|
24
|
+
export const DEFAULT_MODEL_EXCLUSION_TTL_MS = 24 * 60 * 60_000;
|
|
25
|
+
/** Keeps a new expiry safely below JavaScript's maximum Date timestamp. */
|
|
26
|
+
export const MAX_MODEL_EXCLUSION_TTL_MS = 8_000_000_000_000_000;
|
|
27
|
+
const MAX_DATE_TIMESTAMP_MS = 8_640_000_000_000_000;
|
|
28
|
+
let defaultTTLMs = DEFAULT_MODEL_EXCLUSION_TTL_MS;
|
|
29
|
+
let loadedTTLCeilingMs: number | undefined;
|
|
24
30
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
25
31
|
let persistSeq = 0;
|
|
26
32
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Override the default TTL applied to newly recorded model exclusions.
|
|
35
|
+
*
|
|
36
|
+
* @param ms Duration in milliseconds. Must be finite and positive.
|
|
37
|
+
* @returns Nothing.
|
|
38
|
+
*/
|
|
39
|
+
// TEST:test/unit/model-exclusions.test.ts[model exclusions — TTL expiry]
|
|
40
|
+
export function setDefaultTTL(ms: number, options?: { shortenExisting?: boolean }): void {
|
|
41
|
+
if (!Number.isFinite(ms) || ms <= 0 || ms > MAX_MODEL_EXCLUSION_TTL_MS) {
|
|
42
|
+
throw new Error(`Default model exclusion TTL must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}.`);
|
|
43
|
+
}
|
|
30
44
|
defaultTTLMs = ms;
|
|
45
|
+
loadedTTLCeilingMs = options?.shortenExisting ? ms : undefined;
|
|
46
|
+
if (loaded && loadedTTLCeilingMs !== undefined && shortenExclusionsToTTL(exclusions, loadedTTLCeilingMs, Date.now())) schedulePersist();
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
/**
|
|
@@ -78,9 +94,23 @@ function ensureLoaded(): void {
|
|
|
78
94
|
const raw = fs.readFileSync(getExclusionsFilePath(), "utf-8");
|
|
79
95
|
const data = JSON.parse(raw);
|
|
80
96
|
if (data.version === 1) {
|
|
97
|
+
if (!Array.isArray(data.exclusions)) throw new Error("Model exclusion store version 1 must contain an exclusions array.");
|
|
98
|
+
for (let index = 0; index < data.exclusions.length; index++) {
|
|
99
|
+
const entry = data.exclusions[index];
|
|
100
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`Model exclusion store entry ${index} must be an object.`);
|
|
101
|
+
if (entry.reason !== undefined && typeof entry.reason !== "string") throw new Error(`Model exclusion store entry ${index} has an invalid reason.`);
|
|
102
|
+
for (const field of ["recordedAt", "expiresAt"] as const) {
|
|
103
|
+
const timestamp = entry[field];
|
|
104
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp) || timestamp <= 0 || timestamp > MAX_DATE_TIMESTAMP_MS) {
|
|
105
|
+
throw new Error(`Model exclusion store entry ${index} has an invalid ${field}.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
81
109
|
const now = Date.now();
|
|
82
110
|
exclusions = (data.exclusions ?? []).filter((e: ModelExclusion) => e.expiresAt > now);
|
|
111
|
+
const shortened = loadedTTLCeilingMs !== undefined && shortenExclusionsToTTL(exclusions, loadedTTLCeilingMs, now);
|
|
83
112
|
exclusions = deduplicate(exclusions);
|
|
113
|
+
if (shortened) schedulePersist();
|
|
84
114
|
}
|
|
85
115
|
} catch (error) {
|
|
86
116
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
@@ -205,7 +235,10 @@ export function parseModelKey(fullId: string): { provider?: string; modelId: str
|
|
|
205
235
|
* Filter a list of candidate fullIds, removing excluded models/providers and
|
|
206
236
|
* duplicates while preserving order.
|
|
207
237
|
*/
|
|
208
|
-
export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
238
|
+
export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
239
|
+
now?: number;
|
|
240
|
+
onExcluded?: (candidate: string, exclusion: Readonly<ModelExclusion>) => void;
|
|
241
|
+
}): string[] {
|
|
209
242
|
ensureLoaded();
|
|
210
243
|
const timestamp = opts?.now ?? Date.now();
|
|
211
244
|
const seen = new Set<string>();
|
|
@@ -213,8 +246,11 @@ export function filterFallbackCandidates(candidates: string[], opts?: { now?: nu
|
|
|
213
246
|
for (const raw of candidates) {
|
|
214
247
|
if (!raw || seen.has(raw)) continue;
|
|
215
248
|
const { provider: candidateProvider, modelId: candidateModelId } = parseModelKey(raw);
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
249
|
+
const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp));
|
|
250
|
+
if (exclusion) {
|
|
251
|
+
opts?.onExcluded?.(raw, exclusion);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
218
254
|
seen.add(raw);
|
|
219
255
|
filtered.push(raw);
|
|
220
256
|
}
|
|
@@ -241,3 +277,17 @@ function prune(items: ModelExclusion[], now: number): void {
|
|
|
241
277
|
}
|
|
242
278
|
items.length = write;
|
|
243
279
|
}
|
|
280
|
+
|
|
281
|
+
function shortenExclusionsToTTL(items: ModelExclusion[], ttlMs: number, now: number): boolean {
|
|
282
|
+
let changed = false;
|
|
283
|
+
for (const entry of items) {
|
|
284
|
+
const configuredExpiry = entry.recordedAt + ttlMs;
|
|
285
|
+
if (entry.expiresAt > configuredExpiry) {
|
|
286
|
+
entry.expiresAt = configuredExpiry;
|
|
287
|
+
changed = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const previousLength = items.length;
|
|
291
|
+
prune(items, now);
|
|
292
|
+
return changed || items.length !== previousLength;
|
|
293
|
+
}
|
|
@@ -2,6 +2,7 @@ import { splitKnownThinkingSuffix, type ModelInfo as AvailableModelInfo } from "
|
|
|
2
2
|
import type { Usage } from "../../shared/types.ts";
|
|
3
3
|
import { filterFallbackCandidates, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
|
|
4
4
|
import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
|
|
5
|
+
import { redactSecretValues } from "./permissions.ts";
|
|
5
6
|
|
|
6
7
|
export type { AvailableModelInfo };
|
|
7
8
|
|
|
@@ -394,7 +395,12 @@ export function buildModelCandidates(
|
|
|
394
395
|
seen.add(normalized);
|
|
395
396
|
candidates.push(normalized);
|
|
396
397
|
}
|
|
397
|
-
return filterFallbackCandidates(candidates
|
|
398
|
+
return filterFallbackCandidates(candidates, {
|
|
399
|
+
onExcluded(candidate, exclusion) {
|
|
400
|
+
const reason = redactSecretValues((exclusion.reason ?? "runtime-failure").replace(/[\u0000-\u001f\u007f]+/g, " ")).slice(0, 240);
|
|
401
|
+
console.warn(`[pi-subagents] Skipping model '${candidate}' due to a cached exclusion (reason: ${reason}; expires: ${new Date(exclusion.expiresAt).toISOString()}).`);
|
|
402
|
+
},
|
|
403
|
+
});
|
|
398
404
|
}
|
|
399
405
|
|
|
400
406
|
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
@@ -419,6 +425,7 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
|
419
425
|
/overloaded/i,
|
|
420
426
|
/service unavailable/i,
|
|
421
427
|
/temporar(?:ily)? unavailable/i,
|
|
428
|
+
/connection\s+(?:error|reset|closed|aborted)/i,
|
|
422
429
|
/connection refused/i,
|
|
423
430
|
/fetch failed/i,
|
|
424
431
|
/network error/i,
|
|
@@ -451,6 +458,21 @@ export function isRetryableModelFailure(error: string | undefined): boolean {
|
|
|
451
458
|
return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
|
|
452
459
|
}
|
|
453
460
|
|
|
461
|
+
function messageError(message: unknown): string | undefined {
|
|
462
|
+
if (!message || typeof message !== "object") return undefined;
|
|
463
|
+
const value = (message as { errorMessage?: unknown }).errorMessage;
|
|
464
|
+
return typeof value === "string" ? value : undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function isRetryableModelFailureAttempt(input: { error: string | undefined; messages?: readonly unknown[]; toolCount?: number }): boolean {
|
|
468
|
+
if (!isRetryableModelFailure(input.error)) return false;
|
|
469
|
+
if ((input.toolCount ?? 0) > 0) return false;
|
|
470
|
+
if (input.error === "Subagent produced no output (possible model cold-start or empty response).") return true;
|
|
471
|
+
if ((input.toolCount ?? 0) === 0 && (input.messages?.length ?? 0) === 0) return true;
|
|
472
|
+
const error = input.error?.trim();
|
|
473
|
+
return Boolean(error && input.messages?.some((message) => messageError(message)?.trim() === error));
|
|
474
|
+
}
|
|
475
|
+
|
|
454
476
|
export function recordRetryableModelFailure(model: string | undefined, error: string | undefined): void {
|
|
455
477
|
if (!model || !isRetryableModelFailure(error)) return;
|
|
456
478
|
const { provider, modelId } = parseModelKey(model);
|
|
@@ -9,8 +9,13 @@ const MAX_TRACKED_PATHS = 500;
|
|
|
9
9
|
const MAX_HASH_BYTES = 1024 * 1024;
|
|
10
10
|
const MAX_TIMEOUT_FILES = 20;
|
|
11
11
|
|
|
12
|
+
function gitArguments(args: string[]): string[] {
|
|
13
|
+
// Mutation evidence must not block child startup on a stale fsmonitor daemon.
|
|
14
|
+
return ["-c", "core.fsmonitor=false", ...args];
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
function gitOutput(cwd: string, args: string[], maxBuffer = MAX_HASH_BYTES): string {
|
|
13
|
-
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
|
|
18
|
+
return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
function splitNul(output: string): string[] {
|
|
@@ -21,7 +26,7 @@ function hashLargeDiff(cwd: string, relativePath: string): string {
|
|
|
21
26
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tracked-diff-"));
|
|
22
27
|
const diffPath = path.join(tempDir, "diff.patch");
|
|
23
28
|
try {
|
|
24
|
-
execFileSync("git", ["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath], { cwd, stdio: "ignore" });
|
|
29
|
+
execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore" });
|
|
25
30
|
const hash = createHash("sha256");
|
|
26
31
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
27
32
|
const fd = fs.openSync(diffPath, "r");
|
|
@@ -199,8 +199,10 @@ function sanitizeTokenUsage(value: unknown): NestedRunSummary["totalTokens"] | u
|
|
|
199
199
|
const input = clampNumber(raw.input);
|
|
200
200
|
const output = clampNumber(raw.output);
|
|
201
201
|
const total = clampNumber(raw.total);
|
|
202
|
+
const window = clampNumber(raw.window);
|
|
203
|
+
const windowPeak = clampNumber(raw.windowPeak);
|
|
202
204
|
return input !== undefined && output !== undefined && total !== undefined
|
|
203
|
-
? { input, output, total }
|
|
205
|
+
? { input, output, total, ...(window !== undefined ? { window } : {}), ...(windowPeak !== undefined ? { windowPeak } : {}) }
|
|
204
206
|
: undefined;
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatDuration, formatModelThinking,
|
|
1
|
+
import { formatDuration, formatModelThinking, formatTokenUsage, shortenPath } from "../../shared/formatters.ts";
|
|
2
2
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
3
3
|
import type { ActivityState, NestedRunSummary, NestedStepSummary } from "../../shared/types.ts";
|
|
4
4
|
|
|
@@ -68,7 +68,7 @@ function formatNestedActivity(input: {
|
|
|
68
68
|
if (input.currentPath) facts.push(shortenPath(input.currentPath));
|
|
69
69
|
if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
|
|
70
70
|
if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
|
|
71
|
-
if (input.totalTokens) facts.push(
|
|
71
|
+
if (input.totalTokens) facts.push(formatTokenUsage(input.totalTokens));
|
|
72
72
|
const activity = formatActivityLabel(input.lastActivityAt, input.activityState as ActivityState | undefined);
|
|
73
73
|
return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
|
|
74
74
|
}
|
|
@@ -2,6 +2,19 @@ export function formatProcessSignalError(signal: string): string {
|
|
|
2
2
|
return `Subagent process terminated by signal ${signal}.`;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
export function formatMidToolExitError(input: {
|
|
6
|
+
toolName: string;
|
|
7
|
+
exitCode: number | null;
|
|
8
|
+
processSignal?: string | null;
|
|
9
|
+
}): string {
|
|
10
|
+
const terminal = [`exit ${input.exitCode ?? "unknown"}`, ...(input.processSignal ? [`signal ${input.processSignal}`] : [])].join(", ");
|
|
11
|
+
return `Subagent process exited during '${input.toolName}' tool execution (${terminal}) before the tool completed. Earlier assistant output is not a terminal result.`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isOrdinaryToolForMidToolExit(toolName: string): boolean {
|
|
15
|
+
return toolName !== "intercom" && toolName !== "contact_supervisor";
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
export function isUnexplainedProcessSignal(input: {
|
|
6
19
|
processSignal?: string | null;
|
|
7
20
|
interrupted?: boolean;
|
|
@@ -2,6 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
5
|
+
import { isUnexplainedProcessSignal } from "./process-signal.ts";
|
|
6
|
+
|
|
7
|
+
export type RunOutcome = "completed" | "failed" | "timed_out" | "stopped" | "interrupted";
|
|
5
8
|
|
|
6
9
|
export interface RunEntry {
|
|
7
10
|
agent: string;
|
|
@@ -9,6 +12,7 @@ export interface RunEntry {
|
|
|
9
12
|
taskHash?: string;
|
|
10
13
|
ts: number;
|
|
11
14
|
status: "ok" | "error";
|
|
15
|
+
outcome?: RunOutcome;
|
|
12
16
|
duration: number;
|
|
13
17
|
exit?: number;
|
|
14
18
|
}
|
|
@@ -129,14 +133,30 @@ function appendPrivateHistoryLine(historyPath: string, line: string): void {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
export function recordRun(
|
|
136
|
+
export function recordRun(
|
|
137
|
+
agent: string,
|
|
138
|
+
task: string,
|
|
139
|
+
exitCode: number,
|
|
140
|
+
durationMs: number,
|
|
141
|
+
terminal: { interrupted?: boolean; processSignal?: string | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean } = {},
|
|
142
|
+
): void {
|
|
133
143
|
try {
|
|
144
|
+
const outcome: RunOutcome = terminal.stopped
|
|
145
|
+
? "stopped"
|
|
146
|
+
: terminal.interrupted
|
|
147
|
+
? "interrupted"
|
|
148
|
+
: terminal.timedOut
|
|
149
|
+
? "timed_out"
|
|
150
|
+
: exitCode !== 0 && isUnexplainedProcessSignal(terminal)
|
|
151
|
+
? "stopped"
|
|
152
|
+
: exitCode === 0 ? "completed" : "failed";
|
|
134
153
|
const entry: RunEntry = {
|
|
135
154
|
agent,
|
|
136
155
|
task: REDACTED_TASK,
|
|
137
156
|
taskHash: hashTask(task),
|
|
138
157
|
ts: Math.floor(Date.now() / 1000),
|
|
139
158
|
status: exitCode === 0 ? "ok" : "error",
|
|
159
|
+
outcome,
|
|
140
160
|
duration: durationMs,
|
|
141
161
|
...(exitCode !== 0 ? { exit: exitCode } : {}),
|
|
142
162
|
};
|
|
@@ -37,6 +37,8 @@ interface ForkableSessionManager {
|
|
|
37
37
|
|
|
38
38
|
interface ForkContextResolverOptions {
|
|
39
39
|
openSession?: (path: string, sessionDir?: string) => BranchSessionManager;
|
|
40
|
+
/** Rewrite a created fork before its path can be used to spawn a child. */
|
|
41
|
+
pruneSession?: (sessionFile: string) => Promise<void>;
|
|
40
42
|
/** Decide per child index whether a sanitized transcript must also disable the child's
|
|
41
43
|
* thinking. Defaults to true (the pre-existing conservative behavior) when omitted. */
|
|
42
44
|
forceThinkingOffForIndex?: (index: number) => boolean;
|
|
@@ -48,6 +50,7 @@ interface ForkContextResolution {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
interface ForkContextResolver {
|
|
53
|
+
prepareSessionForIndex(index?: number): Promise<void>;
|
|
51
54
|
sessionFileForIndex(index?: number): string | undefined;
|
|
52
55
|
thinkingOverrideForIndex(index?: number): "off" | undefined;
|
|
53
56
|
}
|
|
@@ -190,6 +193,7 @@ export function createForkContextResolver(
|
|
|
190
193
|
): ForkContextResolver {
|
|
191
194
|
if (resolveSubagentContext(requestedContext) !== "fork") {
|
|
192
195
|
return {
|
|
196
|
+
prepareSessionForIndex: async () => {},
|
|
193
197
|
sessionFileForIndex: () => undefined,
|
|
194
198
|
thinkingOverrideForIndex: () => undefined,
|
|
195
199
|
};
|
|
@@ -226,6 +230,8 @@ export function createForkContextResolver(
|
|
|
226
230
|
"forks",
|
|
227
231
|
);
|
|
228
232
|
const cachedResolutions = new Map<number, ForkContextResolution>();
|
|
233
|
+
const preparedIndexes = new Set<number>();
|
|
234
|
+
const preparationPromises = new Map<number, Promise<void>>();
|
|
229
235
|
|
|
230
236
|
const resolveFork = (index = 0): ForkContextResolution => {
|
|
231
237
|
const cached = cachedResolutions.get(index);
|
|
@@ -274,7 +280,22 @@ export function createForkContextResolver(
|
|
|
274
280
|
};
|
|
275
281
|
|
|
276
282
|
return {
|
|
283
|
+
async prepareSessionForIndex(index = 0): Promise<void> {
|
|
284
|
+
const resolution = resolveFork(index);
|
|
285
|
+
if (!options.pruneSession || preparedIndexes.has(index)) return;
|
|
286
|
+
let preparation = preparationPromises.get(index);
|
|
287
|
+
if (!preparation) {
|
|
288
|
+
preparation = options.pruneSession(resolution.sessionFile).then(() => {
|
|
289
|
+
preparedIndexes.add(index);
|
|
290
|
+
});
|
|
291
|
+
preparationPromises.set(index, preparation);
|
|
292
|
+
}
|
|
293
|
+
await preparation;
|
|
294
|
+
},
|
|
277
295
|
sessionFileForIndex(index = 0): string | undefined {
|
|
296
|
+
if (options.pruneSession && !preparedIndexes.has(index)) {
|
|
297
|
+
throw new Error(`Pruned fork session ${index} was used before pruning completed.`);
|
|
298
|
+
}
|
|
278
299
|
return resolveFork(index).sessionFile;
|
|
279
300
|
},
|
|
280
301
|
thinkingOverrideForIndex(index = 0): "off" | undefined {
|
package/src/shared/formatters.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import type { Usage, SingleResult } from "./types.ts";
|
|
7
|
+
import type { Usage, SingleResult, TokenUsage } from "./types.ts";
|
|
8
8
|
import type { ChainStep } from "./settings.ts";
|
|
9
9
|
import { isDynamicParallelStep, isParallelStep } from "./settings.ts";
|
|
10
10
|
import { previewDisplayText, sanitizeDisplayText } from "./display-text.ts";
|
|
@@ -17,6 +17,12 @@ export function formatTokens(n: number): string {
|
|
|
17
17
|
return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function formatTokenUsage(usage: TokenUsage, legacyLabel = "tok"): string {
|
|
21
|
+
return usage.window !== undefined
|
|
22
|
+
? `${formatTokens(usage.window)} window · ${formatTokens(usage.total)} spent`
|
|
23
|
+
: `${formatTokens(usage.total)} ${legacyLabel}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export function formatModelThinking(model?: string, thinking?: string): string {
|
|
21
27
|
const parsed = model ? splitKnownThinkingSuffix(model) : undefined;
|
|
22
28
|
let displayModel = parsed?.baseModel ?? model;
|