@selesai/code 0.8.4 → 0.8.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 +10 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +41 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
- package/dist/extensions/pi-subagents/README.md +2 -0
- package/dist/extensions/pi-subagents/docs/configuration.md +46 -2
- package/dist/extensions/pi-subagents/docs/observability.md +1 -1
- package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
- package/dist/extensions/pi-subagents/src/extension/config.ts +6 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +21 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
- package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -5
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +614 -187
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
- package/dist/extensions/pi-subagents/src/shared/types.ts +92 -0
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +225 -12
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
- package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +2 -1
|
@@ -25,6 +25,7 @@ import { registerSubagentCapabilityCeiling } from "../../src/api/capability-ceil
|
|
|
25
25
|
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
|
+
import { ACTIVE_ASYNC_CAPACITY_DIR, acquireActiveAsyncCapacity, activeAsyncCapacitySessionKey } from "../../src/runs/background/active-async-capacity.ts";
|
|
28
29
|
|
|
29
30
|
interface LaunchResolvedExtensions {
|
|
30
31
|
version?: number;
|
|
@@ -80,7 +81,7 @@ interface AsyncResultPayload {
|
|
|
80
81
|
totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
|
|
81
82
|
usageBudget?: UsageBudgetState;
|
|
82
83
|
checkpoint?: { name?: string; status?: string };
|
|
83
|
-
results: Array<{ agent?: 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; 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 }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: 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 }; 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; 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; 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 }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: 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 };
|
|
@@ -572,6 +573,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
572
573
|
acceptance: false,
|
|
573
574
|
});
|
|
574
575
|
assert.match(launch.details.launchContractDigest ?? "", /^[a-f0-9]{64}$/);
|
|
576
|
+
const recovery = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "recovery-descriptor.json"), "utf-8")) as { runFanoutBudget?: { rootRunId?: string; limit?: number } };
|
|
577
|
+
assert.deepEqual(recovery.runFanoutBudget && { rootRunId: recovery.runFanoutBudget.rootRunId, limit: recovery.runFanoutBudget.limit }, { rootRunId: id, limit: 64 });
|
|
575
578
|
const payload = await readAsyncPayload(id);
|
|
576
579
|
const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete" && candidate.runtimeAcknowledgedExtensions !== undefined);
|
|
577
580
|
assert.equal(payload.launchContractDigest, launch.details.launchContractDigest);
|
|
@@ -796,6 +799,44 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
796
799
|
}
|
|
797
800
|
});
|
|
798
801
|
|
|
802
|
+
it("redacts async task text from durable artifacts, transcripts, and metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
803
|
+
mockPi.onCall({ output: "async redaction complete" });
|
|
804
|
+
const id = `async-prompt-redaction-${Date.now().toString(36)}`;
|
|
805
|
+
const sentinel = "ASYNC_RAW_PROMPT_SENTINEL_1021";
|
|
806
|
+
executeAsyncSingle(id, {
|
|
807
|
+
agent: "worker",
|
|
808
|
+
task: `Handle ${sentinel} without persisting it`,
|
|
809
|
+
agentConfig: makeAgent("worker", { completionGuard: false }),
|
|
810
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
811
|
+
artifactConfig: { enabled: true, includeInput: true, includeOutput: true, includeJsonl: false, includeTranscript: true, includeMetadata: true, cleanupDays: 7 },
|
|
812
|
+
artifactsDir: path.join(tempDir, ".pi-subagents", "artifacts"),
|
|
813
|
+
shareEnabled: false,
|
|
814
|
+
maxSubagentDepth: 2,
|
|
815
|
+
acceptance: false,
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
await waitForMockPiCall(mockPi, 0);
|
|
819
|
+
const callFile = fs.readdirSync(mockPi.dir).find((name) => name.endsWith(".json"));
|
|
820
|
+
assert.ok(callFile);
|
|
821
|
+
const call = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as { args?: string[] };
|
|
822
|
+
assert.match(call.args?.join("\n") ?? "", new RegExp(sentinel));
|
|
823
|
+
|
|
824
|
+
const payload = await readAsyncPayload(id);
|
|
825
|
+
const artifactPaths = payload.results[0]?.artifactPaths;
|
|
826
|
+
assert.ok(artifactPaths?.inputPath);
|
|
827
|
+
assert.ok(artifactPaths.metadataPath);
|
|
828
|
+
assert.ok(artifactPaths.transcriptPath);
|
|
829
|
+
const inputText = fs.readFileSync(artifactPaths.inputPath, "utf-8");
|
|
830
|
+
const transcriptText = fs.readFileSync(artifactPaths.transcriptPath, "utf-8");
|
|
831
|
+
const metadataText = fs.readFileSync(artifactPaths.metadataPath, "utf-8");
|
|
832
|
+
assert.doesNotMatch(inputText, new RegExp(sentinel));
|
|
833
|
+
assert.doesNotMatch(transcriptText, new RegExp(sentinel));
|
|
834
|
+
assert.doesNotMatch(metadataText, new RegExp(sentinel));
|
|
835
|
+
assert.match(inputText, /\[prompt redacted\]/);
|
|
836
|
+
assert.match(transcriptText, /\[prompt redacted\]/);
|
|
837
|
+
assert.equal((JSON.parse(metadataText) as { task?: string }).task, "[prompt redacted]");
|
|
838
|
+
});
|
|
839
|
+
|
|
799
840
|
it("background writes a failure stub to output artifacts when no output was produced", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
800
841
|
mockPi.onCall({ output: "", stderr: "model unavailable", exitCode: 1 });
|
|
801
842
|
const id = `async-empty-failure-artifact-${Date.now().toString(36)}`;
|
|
@@ -1531,8 +1572,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1531
1572
|
assert.match(singleResult.content[0]?.text ?? "", /Do not run sleep timers or polling loops/);
|
|
1532
1573
|
assert.match(singleResult.content[0]?.text ?? "", /call subagent_wait\(\)/i);
|
|
1533
1574
|
assert.match(singleResult.content[0]?.text ?? "", /non-interactive run: Pi auto-drains current-session background work at agent_end/);
|
|
1534
|
-
assert.equal(startedEvent(singleId).task,
|
|
1535
|
-
assert.equal(startedEvent(singleId).goal,
|
|
1575
|
+
assert.equal(startedEvent(singleId).task, "[prompt redacted]");
|
|
1576
|
+
assert.equal(startedEvent(singleId).goal, "[prompt redacted]");
|
|
1536
1577
|
await waitForAsyncResultFile(singleId, 30_000);
|
|
1537
1578
|
|
|
1538
1579
|
mockPi.onCall({ output: "interactive done" });
|
|
@@ -1562,7 +1603,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1562
1603
|
assert.match(parallelResult.content[0]?.text ?? "", /Async parallel:/);
|
|
1563
1604
|
assert.match(parallelResult.content[0]?.text ?? "", /Do not run sleep timers or polling loops/);
|
|
1564
1605
|
assert.match(parallelResult.content[0]?.text ?? "", /call subagent_wait\(\)/i);
|
|
1565
|
-
assert.equal(startedEvent(parallelId).goal, "
|
|
1606
|
+
assert.equal(startedEvent(parallelId).goal, "[prompt redacted]");
|
|
1566
1607
|
const parallelResultPath = await waitForAsyncResultFile(parallelId, 10_000);
|
|
1567
1608
|
const parallelPayload = JSON.parse(fs.readFileSync(parallelResultPath, "utf-8")) as { agent?: string; mode?: string };
|
|
1568
1609
|
assert.equal(parallelPayload.mode, "parallel");
|
|
@@ -1581,8 +1622,8 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
1581
1622
|
assert.match(chainResult.content[0]?.text ?? "", /Async chain:/);
|
|
1582
1623
|
assert.match(chainResult.content[0]?.text ?? "", /Do not run sleep timers or polling loops/);
|
|
1583
1624
|
const chainEvent = startedEvent(chainId);
|
|
1584
|
-
assert.equal(chainEvent.task,
|
|
1585
|
-
assert.equal(chainEvent.goal,
|
|
1625
|
+
assert.equal(chainEvent.task, "[prompt redacted]");
|
|
1626
|
+
assert.equal(chainEvent.goal, "[prompt redacted]");
|
|
1586
1627
|
await waitForAsyncResultFile(chainId, 10_000);
|
|
1587
1628
|
});
|
|
1588
1629
|
|
|
@@ -3565,6 +3606,55 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
|
|
|
3565
3606
|
assert.deepEqual(payload.results[0].attemptedModels, ["github-copilot/gpt-5-mini"]);
|
|
3566
3607
|
});
|
|
3567
3608
|
|
|
3609
|
+
it("rejects an over-cap top-level async launch before creating run artifacts", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
3610
|
+
fs.rmSync(path.join(ACTIVE_ASYNC_CAPACITY_DIR, activeAsyncCapacitySessionKey("session-cap")), { recursive: true, force: true });
|
|
3611
|
+
const state = {
|
|
3612
|
+
baseCwd: tempDir,
|
|
3613
|
+
currentSessionId: "session-cap",
|
|
3614
|
+
asyncJobs: new Map(),
|
|
3615
|
+
fleetJobs: new Map(),
|
|
3616
|
+
foregroundControls: new Map(),
|
|
3617
|
+
lastForegroundControlId: null,
|
|
3618
|
+
};
|
|
3619
|
+
const occupied = acquireActiveAsyncCapacity({
|
|
3620
|
+
sessionId: "session-cap",
|
|
3621
|
+
limit: 1,
|
|
3622
|
+
runId: "held-run",
|
|
3623
|
+
kind: "runner",
|
|
3624
|
+
asyncDir: path.join(tempDir, "held-run"),
|
|
3625
|
+
});
|
|
3626
|
+
assert.ok(occupied);
|
|
3627
|
+
occupied.markStarted("held-runner");
|
|
3628
|
+
const rejectedAsyncDir = path.join(ASYNC_DIR, "cap-rejected");
|
|
3629
|
+
const rejectedResultPath = path.join(RESULTS_DIR, "cap-rejected.json");
|
|
3630
|
+
fs.rmSync(rejectedAsyncDir, { recursive: true, force: true });
|
|
3631
|
+
fs.rmSync(rejectedResultPath, { force: true });
|
|
3632
|
+
const executor = createSubagentExecutor!({
|
|
3633
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
3634
|
+
state,
|
|
3635
|
+
config: { maxActiveAsyncRunsPerSession: 1, artifactDir: "project" },
|
|
3636
|
+
asyncByDefault: false,
|
|
3637
|
+
tempArtifactsDir: tempDir,
|
|
3638
|
+
getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
|
|
3639
|
+
expandTilde: (p: string) => p,
|
|
3640
|
+
discoverAgents: () => ({ agents: [makeAgent("worker")] }),
|
|
3641
|
+
});
|
|
3642
|
+
const context = makeMinimalCtx(tempDir);
|
|
3643
|
+
context.sessionManager.getSessionFile = () => null;
|
|
3644
|
+
context.sessionManager.getSessionId = () => "session-cap";
|
|
3645
|
+
const previousDepth = process.env.SELESAI_SUBAGENT_DEPTH;
|
|
3646
|
+
process.env.SELESAI_SUBAGENT_DEPTH = "0";
|
|
3647
|
+
const result = await executor.execute("cap-rejected", { agent: "worker", task: "Must not start", async: true }, new AbortController().signal, undefined, context);
|
|
3648
|
+
if (previousDepth === undefined) delete process.env.SELESAI_SUBAGENT_DEPTH;
|
|
3649
|
+
else process.env.SELESAI_SUBAGENT_DEPTH = previousDepth;
|
|
3650
|
+
assert.equal(result.isError, true);
|
|
3651
|
+
assert.match(result.content[0]?.type === "text" ? result.content[0].text : "", /Active async run capacity exhausted: 1\/1 used/);
|
|
3652
|
+
assert.equal(fs.existsSync(rejectedAsyncDir), false);
|
|
3653
|
+
assert.equal(fs.existsSync(rejectedResultPath), false);
|
|
3654
|
+
assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents")), false);
|
|
3655
|
+
fs.rmSync(path.join(ACTIVE_ASYNC_CAPACITY_DIR, activeAsyncCapacitySessionKey("session-cap")), { recursive: true, force: true });
|
|
3656
|
+
});
|
|
3657
|
+
|
|
3568
3658
|
it("scheduled executor launches retain the live active session ownership", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
|
|
3569
3659
|
mockPi.onCall({ output: "Scheduled work completed" });
|
|
3570
3660
|
const liveCwd = path.join(tempDir, "live-project");
|
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import { formatAsyncRunList, listAsyncRuns } from "../../src/runs/background/async-status.ts";
|
|
7
7
|
import { ACTIVE_RUN_INDEX_DIR, updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
|
|
8
|
+
import { claimRunFanoutBatch, createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../../src/runs/shared/run-fanout-budget.ts";
|
|
8
9
|
|
|
9
10
|
function createAsyncDir(root: string, id: string, status: Record<string, unknown>): string {
|
|
10
11
|
const dir = path.join(root, id);
|
|
@@ -17,9 +18,10 @@ function createAsyncDir(root: string, id: string, status: Record<string, unknown
|
|
|
17
18
|
describe("async status helpers", () => {
|
|
18
19
|
it("lists only requested states and includes flattened step summaries", () => {
|
|
19
20
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-status-"));
|
|
21
|
+
let budgetDirectory: string | undefined;
|
|
20
22
|
try {
|
|
21
23
|
const outputFile = path.join(root, "run-a", "output-1.log");
|
|
22
|
-
createAsyncDir(root, "run-a", {
|
|
24
|
+
const runDir = createAsyncDir(root, "run-a", {
|
|
23
25
|
runId: "run-a",
|
|
24
26
|
mode: "chain",
|
|
25
27
|
state: "running",
|
|
@@ -27,12 +29,17 @@ describe("async status helpers", () => {
|
|
|
27
29
|
lastUpdate: 200,
|
|
28
30
|
cwd: "/repo-a",
|
|
29
31
|
currentStep: 1,
|
|
32
|
+
runFanoutBudget: { used: 2, limit: 64, remaining: 62 },
|
|
30
33
|
outputFile,
|
|
31
34
|
steps: [
|
|
32
35
|
{ agent: "scout", status: "complete", durationMs: 10, description: "Inspect auth only" },
|
|
33
36
|
{ agent: "worker", status: "running", durationMs: 20, description: "Patch billing only" },
|
|
34
37
|
],
|
|
35
38
|
});
|
|
39
|
+
const descriptor = createRunFanoutBudget("run-a", 64);
|
|
40
|
+
budgetDirectory = descriptor.directory;
|
|
41
|
+
writeRunFanoutBudgetDescriptor(runDir, descriptor);
|
|
42
|
+
claimRunFanoutBatch(descriptor, ["chain[0]", "chain[1]", "chain[1]/single"]);
|
|
36
43
|
createAsyncDir(root, "run-b", {
|
|
37
44
|
runId: "run-b",
|
|
38
45
|
mode: "single",
|
|
@@ -51,9 +58,12 @@ describe("async status helpers", () => {
|
|
|
51
58
|
assert.equal(runs[0]?.steps[1]?.status, "running");
|
|
52
59
|
assert.equal(runs[0]?.steps[0]?.description, "Inspect auth only");
|
|
53
60
|
assert.equal(runs[0]?.steps[1]?.description, "Patch billing only");
|
|
54
|
-
|
|
61
|
+
const text = formatAsyncRunList(runs);
|
|
62
|
+
assert.match(text, /Run fan-out: 3\/64 used, 61 remaining/);
|
|
63
|
+
assert.match(text, /output: .*output-1\.log/);
|
|
55
64
|
} finally {
|
|
56
65
|
fs.rmSync(root, { recursive: true, force: true });
|
|
66
|
+
if (budgetDirectory) fs.rmSync(budgetDirectory, { recursive: true, force: true });
|
|
57
67
|
}
|
|
58
68
|
});
|
|
59
69
|
|
|
@@ -597,7 +607,7 @@ describe("async status helpers", () => {
|
|
|
597
607
|
}
|
|
598
608
|
});
|
|
599
609
|
|
|
600
|
-
it("
|
|
610
|
+
it("keeps terminal active markers until observed process-terminal proof releases them", () => {
|
|
601
611
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-status-index-prune-"));
|
|
602
612
|
try {
|
|
603
613
|
const asyncDir = createAsyncDir(root, "finished", {
|
|
@@ -606,13 +616,26 @@ describe("async status helpers", () => {
|
|
|
606
616
|
state: "complete",
|
|
607
617
|
startedAt: 100,
|
|
608
618
|
lastUpdate: 200,
|
|
619
|
+
displayDismissedAt: 250,
|
|
620
|
+
processTerminal: { version: 1, state: "unknown", runId: "finished", runnerProcessInstanceId: "runner", reason: "process-tree-unverified" },
|
|
609
621
|
steps: [{ agent: "worker", status: "complete" }],
|
|
610
622
|
});
|
|
611
623
|
updateActiveRunIndex(asyncDir, "running");
|
|
624
|
+
const markerPath = path.join(root, ACTIVE_RUN_INDEX_DIR, "finished");
|
|
625
|
+
|
|
626
|
+
assert.deepEqual(listAsyncRuns(root, { states: ["running"], reconcile: false }), []);
|
|
627
|
+
assert.equal(fs.existsSync(markerPath), true);
|
|
612
628
|
|
|
629
|
+
fs.writeFileSync(path.join(asyncDir, "process-terminal.json"), JSON.stringify({
|
|
630
|
+
version: 1,
|
|
631
|
+
state: "observed",
|
|
632
|
+
runId: "finished",
|
|
633
|
+
runnerProcessInstanceId: "runner",
|
|
634
|
+
observedAt: 300,
|
|
635
|
+
instances: [{ kind: "runner", processInstanceId: "runner", closeObservedAt: 300, exitCode: 0, signal: null }],
|
|
636
|
+
}), "utf-8");
|
|
613
637
|
assert.deepEqual(listAsyncRuns(root, { states: ["running"], reconcile: false }), []);
|
|
614
|
-
assert.equal(fs.existsSync(
|
|
615
|
-
assert.deepEqual(listAsyncRuns(root, { states: ["complete"], reconcile: false }).map((run) => run.id), ["finished"]);
|
|
638
|
+
assert.equal(fs.existsSync(markerPath), false);
|
|
616
639
|
} finally {
|
|
617
640
|
fs.rmSync(root, { recursive: true, force: true });
|
|
618
641
|
}
|
|
@@ -667,10 +667,11 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
667
667
|
);
|
|
668
668
|
|
|
669
669
|
assert.ok(!result.isError);
|
|
670
|
-
|
|
670
|
+
assert.equal(result.details.results[1].task, "[prompt redacted]");
|
|
671
|
+
const step2Task = readCallArgs(1).at(-1) ?? "";
|
|
671
672
|
assert.ok(
|
|
672
673
|
step2Task.includes("MARKER_ABC_123"),
|
|
673
|
-
`step 2
|
|
674
|
+
`step 2 invocation should contain step 1 output via {previous}: ${step2Task.slice(0, 200)}`,
|
|
674
675
|
);
|
|
675
676
|
});
|
|
676
677
|
|
|
@@ -1164,10 +1165,11 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
|
|
|
1164
1165
|
);
|
|
1165
1166
|
|
|
1166
1167
|
assert.ok(!result.isError);
|
|
1167
|
-
|
|
1168
|
+
assert.equal(result.details.results[0].task, "[prompt redacted]");
|
|
1169
|
+
const workerTask = readCallArgs(0).at(-1) ?? "";
|
|
1168
1170
|
assert.ok(
|
|
1169
1171
|
workerTask.includes("the authentication module"),
|
|
1170
|
-
`should substitute {task}: ${workerTask.slice(0, 200)}`,
|
|
1172
|
+
`should substitute {task} in the child invocation: ${workerTask.slice(0, 200)}`,
|
|
1171
1173
|
);
|
|
1172
1174
|
});
|
|
1173
1175
|
|
|
@@ -1566,7 +1568,8 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
|
|
|
1566
1568
|
|
|
1567
1569
|
assert.ok(!result.isError);
|
|
1568
1570
|
assert.equal(result.details.results.length, 3);
|
|
1569
|
-
|
|
1571
|
+
assert.equal(result.details.results[2].task, "[prompt redacted]");
|
|
1572
|
+
const synthTask = readCallArgs(2).at(-1) ?? "";
|
|
1570
1573
|
assert.ok(
|
|
1571
1574
|
synthTask.includes("=== Parallel Task 1 (reviewer-a) ==="),
|
|
1572
1575
|
"synthesizer should include reviewer-a output block",
|
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import type { MockPi } from "../support/helpers.ts";
|
|
6
6
|
import { createEventBus, createMockPi, createTempDir, events, removeTempDir, tryImport } from "../support/helpers.ts";
|
|
7
7
|
import { discoverAgents } from "../../src/agents/agents.ts";
|
|
8
|
+
import { ACTIVE_ASYNC_CAPACITY_DIR, acquireActiveAsyncCapacity, activeAsyncCapacitySessionKey } from "../../src/runs/background/active-async-capacity.ts";
|
|
8
9
|
import { DEFAULT_FORK_PREAMBLE, INTERCOM_DETACH_REQUEST_EVENT, SUBAGENT_ASYNC_STARTED_EVENT } from "../../src/shared/types.ts";
|
|
9
10
|
|
|
10
11
|
interface ExecutorModule {
|
|
@@ -1523,6 +1524,7 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
|
|
|
1523
1524
|
const rawParallelChainGoal = "Parallel chain first child raw goal";
|
|
1524
1525
|
const paddedWorkflowGoal = " preserve this workflow padding ";
|
|
1525
1526
|
const literalPreambleGoal = `${DEFAULT_FORK_PREAMBLE}\n\nTask:\nliteral fresh-context text`;
|
|
1527
|
+
const redactedGoal = "[prompt redacted]";
|
|
1526
1528
|
const executor = makeExecutorWithDiscoverAgents(() => ({
|
|
1527
1529
|
agents: [
|
|
1528
1530
|
{ name: "echo", description: "Echo", defaultContext: "fork" },
|
|
@@ -1576,13 +1578,91 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
|
|
|
1576
1578
|
assert.ok(result.details?.asyncId, `${testCase.name}: expected an async id`);
|
|
1577
1579
|
const event = started.find((entry) => entry.id === result.details?.asyncId);
|
|
1578
1580
|
assert.ok(event, `${testCase.name}: missing async-started event for ${result.details?.asyncId}`);
|
|
1579
|
-
assert.equal(event.goal,
|
|
1581
|
+
assert.equal(event.goal, redactedGoal, testCase.name);
|
|
1580
1582
|
if (!("allowsLiteralPreamble" in testCase)) {
|
|
1581
1583
|
assert.doesNotMatch(event.goal ?? "", /delegated subagent running from a fork/, testCase.name);
|
|
1584
|
+
assert.notEqual(event.goal, testCase.goal, testCase.name);
|
|
1582
1585
|
}
|
|
1583
1586
|
}
|
|
1584
1587
|
});
|
|
1585
1588
|
|
|
1589
|
+
it("rejects Clarify background switches over the active async capacity cap", { skip: !asyncAvailable ? "jiti not available" : undefined }, async () => {
|
|
1590
|
+
const previousDepth = process.env.SELESAI_SUBAGENT_DEPTH;
|
|
1591
|
+
process.env.SELESAI_SUBAGENT_DEPTH = "0";
|
|
1592
|
+
try {
|
|
1593
|
+
for (const testCase of [
|
|
1594
|
+
{
|
|
1595
|
+
name: "single",
|
|
1596
|
+
mode: "single",
|
|
1597
|
+
params: { agent: "echo", task: "Original single", async: true, clarify: true },
|
|
1598
|
+
templates: ["Edited single"],
|
|
1599
|
+
},
|
|
1600
|
+
{
|
|
1601
|
+
name: "parallel",
|
|
1602
|
+
mode: "parallel",
|
|
1603
|
+
params: { tasks: [{ agent: "echo", task: "Original parallel" }, { agent: "second", task: "Second child" }], async: true, clarify: true },
|
|
1604
|
+
templates: ["Edited parallel", "Second child"],
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
name: "chain",
|
|
1608
|
+
mode: "chain",
|
|
1609
|
+
params: { chain: [{ agent: "echo", task: "Original chain" }], async: true, clarify: true },
|
|
1610
|
+
templates: ["Edited chain"],
|
|
1611
|
+
},
|
|
1612
|
+
] as const) {
|
|
1613
|
+
const sessionId = `session-clarify-cap-${testCase.name}-${Date.now()}`;
|
|
1614
|
+
const sessionCapacityDir = path.join(ACTIVE_ASYNC_CAPACITY_DIR, activeAsyncCapacitySessionKey(sessionId));
|
|
1615
|
+
fs.rmSync(sessionCapacityDir, { recursive: true, force: true });
|
|
1616
|
+
const held = acquireActiveAsyncCapacity({
|
|
1617
|
+
sessionId,
|
|
1618
|
+
limit: 1,
|
|
1619
|
+
runId: `held-${testCase.name}`,
|
|
1620
|
+
kind: "runner",
|
|
1621
|
+
asyncDir: path.join(tempDir, `held-${testCase.name}`),
|
|
1622
|
+
});
|
|
1623
|
+
held?.markStarted(`held-runner-${testCase.name}`);
|
|
1624
|
+
try {
|
|
1625
|
+
const executor = makeExecutorWithConfig({ maxActiveAsyncRunsPerSession: 1 });
|
|
1626
|
+
const manager = {
|
|
1627
|
+
...makeSessionManagerRecorder().manager,
|
|
1628
|
+
getSessionId: () => sessionId,
|
|
1629
|
+
};
|
|
1630
|
+
const ctx = {
|
|
1631
|
+
...makeCtx(manager),
|
|
1632
|
+
hasUI: true,
|
|
1633
|
+
ui: {
|
|
1634
|
+
custom: async () => ({
|
|
1635
|
+
confirmed: true,
|
|
1636
|
+
templates: testCase.templates,
|
|
1637
|
+
behaviorOverrides: testCase.templates.map(() => undefined),
|
|
1638
|
+
runInBackground: true,
|
|
1639
|
+
}),
|
|
1640
|
+
},
|
|
1641
|
+
};
|
|
1642
|
+
|
|
1643
|
+
const result = await executor.execute(
|
|
1644
|
+
`clarify-cap-${testCase.name}`,
|
|
1645
|
+
testCase.params,
|
|
1646
|
+
new AbortController().signal,
|
|
1647
|
+
undefined,
|
|
1648
|
+
ctx,
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1651
|
+
assert.equal(result.isError, true, testCase.name);
|
|
1652
|
+
assert.equal(result.details?.mode, testCase.mode, testCase.name);
|
|
1653
|
+
assert.equal(result.details?.asyncId, undefined, testCase.name);
|
|
1654
|
+
assert.match(result.content[0]?.text ?? "", /Active async run capacity exhausted: 1\/1 used/, testCase.name);
|
|
1655
|
+
assert.equal(mockPi.callCount(), 0, testCase.name);
|
|
1656
|
+
} finally {
|
|
1657
|
+
fs.rmSync(sessionCapacityDir, { recursive: true, force: true });
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
} finally {
|
|
1661
|
+
if (previousDepth === undefined) delete process.env.SELESAI_SUBAGENT_DEPTH;
|
|
1662
|
+
else process.env.SELESAI_SUBAGENT_DEPTH = previousDepth;
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1586
1666
|
it("uses edited raw goals when clarification switches each mode to background", { skip: !asyncAvailable ? "jiti not available" : undefined }, async () => {
|
|
1587
1667
|
const executor = makeExecutorWithDiscoverAgents(() => ({
|
|
1588
1668
|
agents: [
|
|
@@ -1640,7 +1720,8 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
|
|
|
1640
1720
|
assert.equal(result.isError, undefined, testCase.name);
|
|
1641
1721
|
assert.ok(result.details?.asyncId, `${testCase.name}: expected an async id`);
|
|
1642
1722
|
const event = started.find((entry) => entry.id === result.details?.asyncId);
|
|
1643
|
-
assert.equal(event?.goal,
|
|
1723
|
+
assert.equal(event?.goal, "[prompt redacted]", testCase.name);
|
|
1724
|
+
assert.notEqual(event?.goal, testCase.goal, testCase.name);
|
|
1644
1725
|
assert.doesNotMatch(event?.goal ?? "", /delegated subagent running from a fork/, testCase.name);
|
|
1645
1726
|
}
|
|
1646
1727
|
});
|