@selesai/code 0.13.2 → 0.13.4
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 +20 -0
- package/dist/defaults/models.json +85 -13
- package/dist/extensions/cost-reconcile.test.ts +200 -4
- package/dist/extensions/cost-reconcile.ts +88 -102
- package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
- package/dist/extensions/pi-intercom/README.md +4 -5
- package/dist/extensions/pi-intercom/config.test.ts +3 -31
- package/dist/extensions/pi-intercom/config.ts +0 -15
- package/dist/extensions/pi-intercom/index.ts +9 -46
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
- package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
- package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
- package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
- package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
- package/dist/extensions/pi-web-agent/README.md +131 -0
- package/dist/extensions/pi-web-agent/package.json +4 -2
- package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
- package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
- package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
- package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
- package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
- package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
- package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
- package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
- package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
- package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
- package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
- package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
- package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
- package/dist/extensions/pi-web-agent/src/types.ts +19 -4
- package/dist/extensions/tokenin-onboarding.ts +302 -0
- package/package.json +1 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
import { PROJECT_SUBAGENTS_RELATIVE_DIR } from "../shared/artifacts.ts";
|
|
6
7
|
|
|
7
8
|
const IGNORED_CHANGE_PREFIXES = [`${PROJECT_SUBAGENTS_RELATIVE_DIR}/`, "tmp/", "node_modules/"];
|
|
8
9
|
const IGNORED_CHANGE_PATHS = new Set([PROJECT_SUBAGENTS_RELATIVE_DIR, "tmp", "node_modules"]);
|
|
9
10
|
const IGNORED_CHANGE_SEGMENTS = new Set([".git", "node_modules"]);
|
|
11
|
+
const TRACKED_ENTRY_PROBE_MAX_BYTES = 1_024;
|
|
10
12
|
|
|
11
13
|
const DEFAULT_MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
|
|
12
14
|
const DEFAULT_MAX_HASH_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
@@ -43,6 +45,37 @@ function git(cwd: string, args: string[]): string | undefined {
|
|
|
43
45
|
return result.stdout;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function comparablePath(value: string): string {
|
|
49
|
+
let resolved = path.resolve(value);
|
|
50
|
+
try {
|
|
51
|
+
resolved = fs.realpathSync.native(resolved);
|
|
52
|
+
} catch {
|
|
53
|
+
// Keep the lexical path when canonicalization is unavailable.
|
|
54
|
+
}
|
|
55
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isHomeRepoRoot(root: string): boolean {
|
|
59
|
+
return comparablePath(root) === comparablePath(os.homedir());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Probe the index without asking Git to inspect the working tree. The output
|
|
64
|
+
* cap keeps this check bounded even for repositories with a very large index;
|
|
65
|
+
* an ENOBUFS result still proves that at least one tracked path exists.
|
|
66
|
+
*/
|
|
67
|
+
function hasTrackedEntries(root: string): boolean | undefined {
|
|
68
|
+
const result = spawnSync("git", ["-C", root, "ls-files", "--cached", "--error-unmatch", "--", "."], {
|
|
69
|
+
encoding: "utf-8",
|
|
70
|
+
maxBuffer: TRACKED_ENTRY_PROBE_MAX_BYTES,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
|
74
|
+
if (result.status === 0 || errorCode === "ENOBUFS") return true;
|
|
75
|
+
if (result.status === 1) return false;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
46
79
|
function normalizeRelPath(value: string): string {
|
|
47
80
|
return value.replaceAll(path.sep, "/").replace(/^\.\//, "");
|
|
48
81
|
}
|
|
@@ -181,7 +214,13 @@ function buildRepoChangeSignature(root: string, statusOutput: string): WatchdogR
|
|
|
181
214
|
export function computeWatchdogRepoChangeSignature(cwd: string): WatchdogRepoChangeSignature | undefined {
|
|
182
215
|
const root = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
183
216
|
if (!root) return undefined;
|
|
184
|
-
const
|
|
217
|
+
const skipUntracked = isHomeRepoRoot(root) || hasTrackedEntries(root) === false;
|
|
218
|
+
const statusOutput = git(root, [
|
|
219
|
+
"status",
|
|
220
|
+
"--porcelain=v1",
|
|
221
|
+
"-z",
|
|
222
|
+
`--untracked-files=${skipUntracked ? "no" : "all"}`,
|
|
223
|
+
]);
|
|
185
224
|
if (statusOutput === undefined) return undefined;
|
|
186
225
|
try {
|
|
187
226
|
return buildRepoChangeSignature(root, statusOutput);
|
|
@@ -53,7 +53,12 @@ export function normalizeWorkflowHostCommandParams(value: unknown, label = "runs
|
|
|
53
53
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
54
54
|
const params = value as Record<string, unknown>;
|
|
55
55
|
const unknownFields = Object.keys(params).filter((field) => !["kind", "command", "timeoutMs", "output", "role", "provider"].includes(field));
|
|
56
|
-
if (unknownFields.length)
|
|
56
|
+
if (unknownFields.length) {
|
|
57
|
+
const cwdHint = unknownFields.includes("cwd")
|
|
58
|
+
? " The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')."
|
|
59
|
+
: "";
|
|
60
|
+
throw new Error(`${label} has unsupported fields: ${unknownFields.join(", ")}.${cwdHint}`);
|
|
61
|
+
}
|
|
57
62
|
if (params.kind !== "command") throw new Error(`${label}.kind must be 'command'.`);
|
|
58
63
|
if (typeof params.command !== "string" || !params.command.trim()) throw new Error(`${label}.command must be a non-empty string.`);
|
|
59
64
|
const command = params.command.trim();
|
|
@@ -500,8 +500,26 @@ function runLane(lane, firstResult, observe) {
|
|
|
500
500
|
return visit(0, firstResult);
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
function workflowPlanStringMetadata(params) {
|
|
504
|
+
return {
|
|
505
|
+
...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
|
|
506
|
+
...(typeof params.label === "string" && params.label.trim() ? { label: params.label.trim() } : {}),
|
|
507
|
+
...(typeof params.agent === "string" && params.agent.trim() ? { agent: params.agent.trim() } : {}),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
503
511
|
function runLanes(laneSpecs) {
|
|
504
512
|
const lanes = validateLaneSpecs(laneSpecs);
|
|
513
|
+
parentPort.postMessage({ type: "lanePlan", lanes: lanes.map((lane) => ({
|
|
514
|
+
key: lane.key,
|
|
515
|
+
stages: lane.stages.map((stage) => ({
|
|
516
|
+
key: stage.key,
|
|
517
|
+
generatedKey: stage.generatedKey,
|
|
518
|
+
...workflowPlanStringMetadata(stage.params),
|
|
519
|
+
...(typeof stage.params.as === "string" && stage.params.as.trim() ? { outputName: stage.params.as.trim() } : {}),
|
|
520
|
+
...(stage.params.outputSchema !== undefined ? { structured: true } : {}),
|
|
521
|
+
})),
|
|
522
|
+
})) });
|
|
505
523
|
const firstItems = lanes.map((lane) => {
|
|
506
524
|
const first = lane.stages[0];
|
|
507
525
|
return { key: first.generatedKey, ...first.params };
|
|
@@ -624,7 +642,12 @@ function validateHostCommand(key, params) {
|
|
|
624
642
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("runs.host('" + key + "') params must be an object.");
|
|
625
643
|
const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
|
|
626
644
|
const unknown = Object.keys(params).filter((field) => !allowed.has(field));
|
|
627
|
-
if (unknown.length)
|
|
645
|
+
if (unknown.length) {
|
|
646
|
+
const cwdHint = unknown.includes("cwd")
|
|
647
|
+
? " The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')."
|
|
648
|
+
: "";
|
|
649
|
+
throw new Error("runs.host('" + key + "') params have unsupported fields: " + unknown.join(", ") + "." + cwdHint);
|
|
650
|
+
}
|
|
628
651
|
if (params.kind !== "command") throw new Error("runs.host('" + key + "') kind must be 'command'.");
|
|
629
652
|
if (typeof params.command !== "string" || !params.command.trim() || params.command.includes("\u0000") || new TextEncoder().encode(params.command.trim()).byteLength > 16384) throw new Error("runs.host('" + key + "') command must be a non-empty string of at most 16384 bytes without NUL.");
|
|
630
653
|
if (!Number.isInteger(params.timeoutMs) || params.timeoutMs < 1 || params.timeoutMs > 86400000) throw new Error("runs.host('" + key + "') timeoutMs must be an integer from 1 to 86400000.");
|
|
@@ -977,6 +1000,22 @@ export interface WorkflowScriptTraceEntry {
|
|
|
977
1000
|
warning?: string;
|
|
978
1001
|
}
|
|
979
1002
|
|
|
1003
|
+
/** Bounded plan metadata emitted when a workflow materializes a runs.lanes graph. */
|
|
1004
|
+
export interface WorkflowLanePlanStage {
|
|
1005
|
+
key: string;
|
|
1006
|
+
generatedKey: string;
|
|
1007
|
+
agent?: string;
|
|
1008
|
+
phase?: string;
|
|
1009
|
+
label?: string;
|
|
1010
|
+
outputName?: string;
|
|
1011
|
+
structured?: boolean;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export interface WorkflowLanePlan {
|
|
1015
|
+
key: string;
|
|
1016
|
+
stages: WorkflowLanePlanStage[];
|
|
1017
|
+
}
|
|
1018
|
+
|
|
980
1019
|
export interface WorkflowSteerOptions {
|
|
981
1020
|
mode?: "steer" | "follow_up" | "auto";
|
|
982
1021
|
index?: number;
|
|
@@ -1044,6 +1083,7 @@ export interface RunWorkflowScriptOptions {
|
|
|
1044
1083
|
};
|
|
1045
1084
|
registerStopChild?: (stop: ((key: string, message?: string) => boolean) | undefined) => void;
|
|
1046
1085
|
onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
|
|
1086
|
+
onLanePlan?: (lanes: WorkflowLanePlan[]) => void;
|
|
1047
1087
|
onEmit?: (emits: unknown[]) => void;
|
|
1048
1088
|
}
|
|
1049
1089
|
|
|
@@ -1227,7 +1267,7 @@ function validateStaticHostCall(call: AstNode): WorkflowScriptValidationError[]
|
|
|
1227
1267
|
for (const property of params.properties) {
|
|
1228
1268
|
if (!astNode(property)) continue;
|
|
1229
1269
|
const name = staticPropertyKey(property);
|
|
1230
|
-
if (name !== undefined && !allowed.has(name)) errors.push({ message: `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
|
|
1270
|
+
if (name !== undefined && !allowed.has(name)) errors.push({ message: name === "cwd" ? "runs.host params contain unsupported field 'cwd'. The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')." : `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
|
|
1231
1271
|
}
|
|
1232
1272
|
const kindNode = directObjectPropertyValue(params, "kind");
|
|
1233
1273
|
const commandNode = directObjectPropertyValue(params, "command");
|
|
@@ -1524,6 +1564,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1524
1564
|
console.error("Workflow onTrace callback failed:", error);
|
|
1525
1565
|
}
|
|
1526
1566
|
};
|
|
1567
|
+
const lanePlanChanged = (lanes: WorkflowLanePlan[]) => {
|
|
1568
|
+
try {
|
|
1569
|
+
options.onLanePlan?.(lanes);
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
console.error("Workflow onLanePlan callback failed:", error);
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1527
1574
|
const hostStepChanged = (hostStep: HostStepNodeV1) => {
|
|
1528
1575
|
try {
|
|
1529
1576
|
options.onHostStep?.(hostStep);
|
|
@@ -1626,6 +1673,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1626
1673
|
});
|
|
1627
1674
|
worker.on("message", (message: Record<string, unknown>) => {
|
|
1628
1675
|
if (settled) return;
|
|
1676
|
+
if (message.type === "lanePlan" && Array.isArray(message.lanes)) {
|
|
1677
|
+
lanePlanChanged(message.lanes as WorkflowLanePlan[]);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1629
1680
|
if (message.type === "emit") {
|
|
1630
1681
|
try {
|
|
1631
1682
|
assertWorkflowJsonValue(message.value, "emit");
|
|
@@ -26,6 +26,7 @@ import { resolveSubagentLaunchContract } from "../../src/api/preflight.ts";
|
|
|
26
26
|
import { discoverAgents } from "../../src/agents/agents.ts";
|
|
27
27
|
import { runSync } from "../../src/runs/foreground/execution.ts";
|
|
28
28
|
import { ACTIVE_ASYNC_CAPACITY_DIR, acquireActiveAsyncCapacity, activeAsyncCapacitySessionKey } from "../../src/runs/background/active-async-capacity.ts";
|
|
29
|
+
import { deriveForkPromptCacheKey, SUBAGENT_FORK_CACHE_KEY_ENV } from "../../src/runs/shared/pi-args.ts";
|
|
29
30
|
|
|
30
31
|
interface LaunchResolvedExtensions {
|
|
31
32
|
version?: number;
|
|
@@ -80,7 +81,7 @@ interface AsyncResultPayload {
|
|
|
80
81
|
totalTokens?: { input: number; output: number; total: number };
|
|
81
82
|
totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
|
|
82
83
|
usageBudget?: UsageBudgetState;
|
|
83
|
-
results: Array<{ agent?: string; sessionName?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string }; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; turns: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean; message?: string }; settlementDiagnostic?: { finalTextPresent?: boolean; mutation?: { expected?: boolean; attempted?: boolean; observed?: boolean }; requiredOutput?: { kind?: string; path?: string; missing?: boolean }; afterCompactionSettlement?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string; transcriptPath?: string }; outputSaveError?: string; metadataSaveError?: string; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
|
|
84
|
+
results: Array<{ agent?: string; sessionName?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string; recoveryNeeded?: boolean; reason?: string; reportStatus?: string }; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; turns: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean; message?: string }; settlementDiagnostic?: { finalTextPresent?: boolean; mutation?: { expected?: boolean; attempted?: boolean; observed?: boolean }; requiredOutput?: { kind?: string; path?: string; missing?: boolean }; afterCompactionSettlement?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string; transcriptPath?: string }; outputSaveError?: string; metadataSaveError?: string; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
|
|
84
85
|
outputs?: Record<string, { text?: string; structured?: unknown }>;
|
|
85
86
|
workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> };
|
|
86
87
|
parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
|
|
@@ -128,7 +129,7 @@ interface AsyncStatusPayload {
|
|
|
128
129
|
status?: string;
|
|
129
130
|
exitCode?: number;
|
|
130
131
|
timedOut?: boolean;
|
|
131
|
-
timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string };
|
|
132
|
+
timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string; recoveryNeeded?: boolean; reason?: string; reportStatus?: string };
|
|
132
133
|
error?: string;
|
|
133
134
|
model?: string;
|
|
134
135
|
thinking?: string;
|
|
@@ -1551,14 +1552,15 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1551
1552
|
assert.equal(payload.results[0]?.error, "Subagent timed out after 150ms.");
|
|
1552
1553
|
});
|
|
1553
1554
|
|
|
1554
|
-
it("
|
|
1555
|
+
it("classifies a timed-out dirty child with a missing requested report as recovery-needed", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
|
|
1555
1556
|
mockPi.onCall({ delay: 5_000, output: "too late" });
|
|
1556
1557
|
const repo = createRepo("pi-subagents-timeout-recovery-");
|
|
1557
1558
|
try {
|
|
1558
1559
|
const id = `async-timeout-recovery-${Date.now().toString(36)}`;
|
|
1560
|
+
const outputPath = path.join(repo, "missing-report.md");
|
|
1559
1561
|
executeAsyncChain(id, {
|
|
1560
|
-
chain: [{ agent: "slow", task: "
|
|
1561
|
-
agents: [makeAgent("slow")],
|
|
1562
|
+
chain: [{ agent: "slow", task: "Write the requested report" }],
|
|
1563
|
+
agents: [makeAgent("slow", { output: outputPath, outputMode: "file-only" })],
|
|
1562
1564
|
ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
|
|
1563
1565
|
artifactConfig: {
|
|
1564
1566
|
enabled: true,
|
|
@@ -1578,12 +1580,27 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1578
1580
|
const payload = await readAsyncPayload(id);
|
|
1579
1581
|
const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
|
|
1580
1582
|
const result = payload.results[0];
|
|
1583
|
+
const recovery = result?.timeoutRecovery;
|
|
1584
|
+
const statusRecovery = status.steps?.[0]?.timeoutRecovery;
|
|
1585
|
+
assert.equal(payload.success, false);
|
|
1586
|
+
assert.equal(payload.state, "failed");
|
|
1581
1587
|
assert.equal(result?.timedOut, true);
|
|
1588
|
+
assert.equal(result?.success, false);
|
|
1589
|
+
assert.equal(fs.existsSync(outputPath), false);
|
|
1582
1590
|
assert.deepEqual(result?.timeoutRecovery?.changedFiles, ["input.md"]);
|
|
1591
|
+
assert.equal(recovery?.recoveryNeeded, true);
|
|
1592
|
+
assert.equal(recovery?.reason, "timed-out-with-dirty-worktree");
|
|
1593
|
+
assert.equal(recovery?.reportStatus, "missing");
|
|
1583
1594
|
assert.match(result?.timeoutRecovery?.message ?? "", /changed tracked files: input\.md/);
|
|
1595
|
+
assert.match(recovery?.message ?? "", /requested report: missing/i);
|
|
1596
|
+
assert.match(result?.output ?? "", /review (?:the )?diff and artifacts before resuming.*dependent stages/i);
|
|
1584
1597
|
assert.match(result?.output ?? "", /Recovery summary:/);
|
|
1585
1598
|
assert.match(result?.output ?? "", /Warning: Inspect partial changes before retrying/);
|
|
1586
|
-
assert.
|
|
1599
|
+
assert.equal(status.state, "failed");
|
|
1600
|
+
assert.equal(status.steps?.[0]?.status, "failed");
|
|
1601
|
+
assert.deepEqual(statusRecovery?.changedFiles, ["input.md"]);
|
|
1602
|
+
assert.equal(statusRecovery?.recoveryNeeded, true);
|
|
1603
|
+
assert.equal(statusRecovery?.reportStatus, "missing");
|
|
1587
1604
|
} finally {
|
|
1588
1605
|
fs.rmSync(repo, { recursive: true, force: true });
|
|
1589
1606
|
}
|
|
@@ -4652,6 +4669,38 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
4652
4669
|
assert.equal(payload.results[0]?.model, "gateway/parent-model");
|
|
4653
4670
|
});
|
|
4654
4671
|
|
|
4672
|
+
it("background forked runs receive the derived fork cache key", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
4673
|
+
mockPi.onCall({ echoEnv: [SUBAGENT_FORK_CACHE_KEY_ENV] });
|
|
4674
|
+
const parentSessionFile = path.join(tempDir, "parent-cache.jsonl");
|
|
4675
|
+
const forkedSessionFile = path.join(tempDir, "forked-cache.jsonl");
|
|
4676
|
+
const sessionHeader = JSON.stringify({ type: "session", cwd: fs.realpathSync(tempDir) });
|
|
4677
|
+
fs.writeFileSync(parentSessionFile, `${sessionHeader}\n`, "utf-8");
|
|
4678
|
+
fs.writeFileSync(forkedSessionFile, `${sessionHeader}\n`, "utf-8");
|
|
4679
|
+
const ctx = {
|
|
4680
|
+
...makeMinimalCtx(tempDir),
|
|
4681
|
+
sessionManager: {
|
|
4682
|
+
getSessionId: () => "session-cache-parent",
|
|
4683
|
+
getSessionFile: () => parentSessionFile,
|
|
4684
|
+
getLeafId: () => "leaf-current",
|
|
4685
|
+
openSession: () => ({ createBranchedSession: () => forkedSessionFile }),
|
|
4686
|
+
},
|
|
4687
|
+
};
|
|
4688
|
+
|
|
4689
|
+
const launch = await makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]).execute(
|
|
4690
|
+
"forked-cache-key",
|
|
4691
|
+
{ agent: "worker", task: "Inspect cache affinity", async: true, context: "fork" },
|
|
4692
|
+
new AbortController().signal,
|
|
4693
|
+
undefined,
|
|
4694
|
+
ctx,
|
|
4695
|
+
) as AsyncExecutionResult;
|
|
4696
|
+
assert.ok(!launch.isError, launch.content[0]?.text);
|
|
4697
|
+
assert.ok(launch.details.asyncId);
|
|
4698
|
+
|
|
4699
|
+
const payload = await readAsyncPayload(launch.details.asyncId);
|
|
4700
|
+
const childEnv = JSON.parse(payload.results[0]?.output ?? "{}") as Record<string, string | null>;
|
|
4701
|
+
assert.equal(childEnv[SUBAGENT_FORK_CACHE_KEY_ENV], deriveForkPromptCacheKey("session-cache-parent"));
|
|
4702
|
+
});
|
|
4703
|
+
|
|
4655
4704
|
it("revives an inherited parent model outside the current registry", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
4656
4705
|
mockPi.onCall({ output: "Initial async work" });
|
|
4657
4706
|
const sourceId = `async-revive-parent-model-${Date.now().toString(36)}`;
|
|
@@ -4,7 +4,7 @@ import { syncBuiltinESMExports } from "node:module";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { describe, it } from "node:test";
|
|
7
|
-
import { formatAsyncRunList, listAsyncRuns } from "../../src/runs/background/async-status.ts";
|
|
7
|
+
import { formatAsyncRunList, formatWorkflowStageLine, listAsyncRuns } from "../../src/runs/background/async-status.ts";
|
|
8
8
|
import { ACTIVE_RUN_INDEX_DIR, DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS, updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
|
|
9
9
|
import { encodeIndexSegment } from "../../src/runs/background/index-segment.ts";
|
|
10
10
|
import { TERMINAL_RUN_INDEX_DIR } from "../../src/runs/background/terminal-run-index.ts";
|
|
@@ -19,7 +19,43 @@ function createAsyncDir(root: string, id: string, status: Record<string, unknown
|
|
|
19
19
|
return dir;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const stagedLaneKeys = ["scope-scout", "red-tests", "label-helpers", "summary-title", "detail-row", "tiers-noise", "validation", "minimality-challenge", "fresh-review"];
|
|
23
|
+
|
|
24
|
+
function stagedLaneStatusGraph(runId: string): Record<string, unknown> {
|
|
25
|
+
const nodeIds = stagedLaneKeys.map((key) => `${runId}.${key}`);
|
|
26
|
+
return {
|
|
27
|
+
runId,
|
|
28
|
+
mode: "workflow",
|
|
29
|
+
phases: [{ title: `${runId} staged lane`, nodeIds }],
|
|
30
|
+
nodes: stagedLaneKeys.map((key, index) => ({
|
|
31
|
+
id: nodeIds[index],
|
|
32
|
+
kind: "step",
|
|
33
|
+
agent: index === 0 ? "scout" : index === 7 ? "simplifier" : "worker",
|
|
34
|
+
label: key,
|
|
35
|
+
status: index === 0 ? "running" : "pending",
|
|
36
|
+
flatIndex: index,
|
|
37
|
+
stepIndex: index,
|
|
38
|
+
})),
|
|
39
|
+
currentNodeId: nodeIds[0],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
describe("async status helpers", () => {
|
|
44
|
+
it("bounds workflow stage text from persisted status", () => {
|
|
45
|
+
const line = formatWorkflowStageLine({
|
|
46
|
+
id: `${"stage".repeat(80)}\u001b[31m`,
|
|
47
|
+
kind: "step",
|
|
48
|
+
label: `${"label".repeat(80)}\nsecond line`,
|
|
49
|
+
agent: "worker".repeat(40),
|
|
50
|
+
status: "failed",
|
|
51
|
+
error: `${"boom".repeat(100)}\nwith details`,
|
|
52
|
+
}, 0, 1);
|
|
53
|
+
|
|
54
|
+
assert.equal(line.includes("\u001b"), false);
|
|
55
|
+
assert.equal(line.includes("\n"), false);
|
|
56
|
+
assert.ok(line.length < 700, line);
|
|
57
|
+
});
|
|
58
|
+
|
|
23
59
|
it("lists only requested states and includes flattened step summaries", () => {
|
|
24
60
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-status-"));
|
|
25
61
|
let budgetDirectory: string | undefined;
|
|
@@ -73,6 +109,80 @@ describe("async status helpers", () => {
|
|
|
73
109
|
if (budgetDirectory) fs.rmSync(budgetDirectory, { recursive: true, force: true });
|
|
74
110
|
}
|
|
75
111
|
});
|
|
112
|
+
|
|
113
|
+
it("forwards bounded timeout recovery evidence and formats the recovery route", () => {
|
|
114
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-status-recovery-"));
|
|
115
|
+
try {
|
|
116
|
+
const changedFiles = Array.from({ length: 25 }, (_, index) => `src/file-${String(index + 1).padStart(2, "0")}.ts`);
|
|
117
|
+
createAsyncDir(root, "run-recovery", {
|
|
118
|
+
runId: "run-recovery",
|
|
119
|
+
mode: "single",
|
|
120
|
+
state: "failed",
|
|
121
|
+
startedAt: 100,
|
|
122
|
+
lastUpdate: 200,
|
|
123
|
+
steps: [{
|
|
124
|
+
agent: "worker",
|
|
125
|
+
status: "failed",
|
|
126
|
+
timedOut: true,
|
|
127
|
+
timeoutRecovery: {
|
|
128
|
+
termination: "timed-out",
|
|
129
|
+
changedFiles,
|
|
130
|
+
truncated: true,
|
|
131
|
+
recoveryNeeded: true,
|
|
132
|
+
reason: "timed-out-with-dirty-worktree",
|
|
133
|
+
reportStatus: "missing",
|
|
134
|
+
message: "raw recovery message must not cross the projection",
|
|
135
|
+
effects: { settlementDiagnostic: { finalTextPresent: true } },
|
|
136
|
+
},
|
|
137
|
+
}],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const run = listAsyncRuns(root, { states: ["failed"], reconcile: false })[0]!;
|
|
141
|
+
assert.deepEqual(run.steps[0]?.timeoutRecovery, {
|
|
142
|
+
termination: "timed-out",
|
|
143
|
+
changedFiles: changedFiles.slice(0, 20),
|
|
144
|
+
truncated: true,
|
|
145
|
+
recoveryNeeded: true,
|
|
146
|
+
reason: "timed-out-with-dirty-worktree",
|
|
147
|
+
reportStatus: "missing",
|
|
148
|
+
});
|
|
149
|
+
assert.doesNotMatch(JSON.stringify(run), /raw recovery message|settlementDiagnostic/);
|
|
150
|
+
const text = formatAsyncRunList([run], "Failed async runs");
|
|
151
|
+
assert.match(text, /Recovery needed: review the diff and artifacts before resuming or launching dependent stages\./);
|
|
152
|
+
assert.match(text, /requested report: missing/);
|
|
153
|
+
assert.match(text, /changed tracked files: src\/file-01\.ts, src\/file-02\.ts/);
|
|
154
|
+
assert.match(text, /file-20\.ts, …/);
|
|
155
|
+
} finally {
|
|
156
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("reports the planned runs.lanes stage count while the first child is running", () => {
|
|
161
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-status-staged-lane-"));
|
|
162
|
+
try {
|
|
163
|
+
const runId = "run-staged-lane";
|
|
164
|
+
createAsyncDir(root, runId, {
|
|
165
|
+
runId,
|
|
166
|
+
mode: "workflow",
|
|
167
|
+
state: "running",
|
|
168
|
+
startedAt: 100,
|
|
169
|
+
lastUpdate: 200,
|
|
170
|
+
currentStep: 0,
|
|
171
|
+
steps: [{ agent: "scout", workflowKey: `${runId}.scope-scout`, label: "scope-scout", status: "running" }],
|
|
172
|
+
workflowGraph: stagedLaneStatusGraph(runId),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const runs = listAsyncRuns(root, { states: ["running"], reconcile: false });
|
|
176
|
+
const text = formatAsyncRunList(runs);
|
|
177
|
+
assert.match(text, /stage\s+1\/9/i);
|
|
178
|
+
assert.doesNotMatch(text, /step\s+1\/1/i);
|
|
179
|
+
assert.match(text, /scope-scout/);
|
|
180
|
+
assert.match(text, /stage\s+9\/9:.*fresh-review.*pending/i);
|
|
181
|
+
} finally {
|
|
182
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
76
186
|
it("loads typed host monitor nodes from workflow status without child identity", () => {
|
|
77
187
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-host-step-"));
|
|
78
188
|
try {
|