@selesai/code 0.8.4 → 0.8.6
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 +22 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +46 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
- package/dist/extensions/pi-subagents/README.md +2 -0
- package/dist/extensions/pi-subagents/agents/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +54 -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/docs/workflows.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/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
- package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
- 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-job-tracker.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- 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/result-watcher.ts +19 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
- 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/background/wait-subscriptions.ts +2 -1
- 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/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
- 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/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +108 -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 +226 -13
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- 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/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -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/session-lineage.test.ts +73 -0
- 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/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
- package/dist/extensions/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +18 -0
- package/dist/extensions/question/question-list.ts +22 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/helpers.test.ts +68 -35
- package/dist/extensions/question/tests/question-list.test.ts +506 -204
- package/dist/extensions/question/tests/row-layout.test.ts +180 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
acquireActiveAsyncCapacity,
|
|
8
|
+
ActiveAsyncCapacityError,
|
|
9
|
+
getActiveAsyncCapacitySnapshot,
|
|
10
|
+
transferActiveAsyncCapacity,
|
|
11
|
+
} from "../../src/runs/background/active-async-capacity.ts";
|
|
12
|
+
|
|
13
|
+
function tempRoot(): string {
|
|
14
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "active-async-capacity-"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeJson(filePath: string, value: unknown): void {
|
|
18
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
19
|
+
fs.writeFileSync(filePath, JSON.stringify(value));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function observedProof(runId: string, runnerProcessInstanceId: string) {
|
|
23
|
+
return {
|
|
24
|
+
version: 1,
|
|
25
|
+
state: "observed",
|
|
26
|
+
runId,
|
|
27
|
+
runnerProcessInstanceId,
|
|
28
|
+
observedAt: 300,
|
|
29
|
+
instances: [{
|
|
30
|
+
kind: "runner",
|
|
31
|
+
processInstanceId: runnerProcessInstanceId,
|
|
32
|
+
closeObservedAt: 300,
|
|
33
|
+
exitCode: 0,
|
|
34
|
+
signal: null,
|
|
35
|
+
}],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("active async capacity", () => {
|
|
40
|
+
it("admits exactly to the limit and rolls back only its own unstarted slot", () => {
|
|
41
|
+
const rootDir = tempRoot();
|
|
42
|
+
try {
|
|
43
|
+
const first = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-a", kind: "runner", asyncDir: path.join(rootDir, "run-a") }, { rootDir, token: () => "token-a" });
|
|
44
|
+
assert.ok(first);
|
|
45
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
46
|
+
assert.throws(
|
|
47
|
+
() => acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-b", kind: "runner", asyncDir: path.join(rootDir, "run-b") }, { rootDir, token: () => "token-b" }),
|
|
48
|
+
(error: unknown) => error instanceof ActiveAsyncCapacityError && error.message === "Active async run capacity exhausted: 1/1 used.",
|
|
49
|
+
);
|
|
50
|
+
assert.equal(first.rollback(), true);
|
|
51
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
52
|
+
} finally {
|
|
53
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("counts corrupt slot metadata as occupied", () => {
|
|
58
|
+
const rootDir = tempRoot();
|
|
59
|
+
try {
|
|
60
|
+
// Create a valid occupied slot, then corrupt only its owner metadata.
|
|
61
|
+
const handle = acquireActiveAsyncCapacity({ sessionId: "session-corrupt", limit: 1, runId: "run-a", kind: "runner", asyncDir: path.join(rootDir, "run-a") }, { rootDir });
|
|
62
|
+
assert.ok(handle);
|
|
63
|
+
fs.writeFileSync(path.join(rootDir, handle.owner.ownerSessionKey, "slot-0", "owner.json"), "not json");
|
|
64
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-corrupt", 1, { rootDir }), { used: 1, limit: 1 });
|
|
65
|
+
assert.throws(() => acquireActiveAsyncCapacity({ sessionId: "session-corrupt", limit: 1, runId: "run-b", kind: "runner", asyncDir: path.join(rootDir, "run-b") }, { rootDir }), ActiveAsyncCapacityError);
|
|
66
|
+
} finally {
|
|
67
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("uses the injected clock for reservation and start timestamps", () => {
|
|
72
|
+
const rootDir = tempRoot();
|
|
73
|
+
let now = 100;
|
|
74
|
+
try {
|
|
75
|
+
const runner = acquireActiveAsyncCapacity({ sessionId: "session-clock", limit: 2, runId: "runner", kind: "runner", asyncDir: path.join(rootDir, "runner") }, { rootDir, now: () => now });
|
|
76
|
+
assert.ok(runner);
|
|
77
|
+
assert.equal(runner.owner.reservedAt, 100);
|
|
78
|
+
now = 200;
|
|
79
|
+
runner.markStarted("runner-process");
|
|
80
|
+
assert.equal(runner.owner.runnerStartedAt, 200);
|
|
81
|
+
|
|
82
|
+
now = 300;
|
|
83
|
+
const workflow = acquireActiveAsyncCapacity({ sessionId: "session-clock", limit: 2, runId: "workflow", kind: "workflow", asyncDir: path.join(rootDir, "workflow") }, { rootDir, now: () => now });
|
|
84
|
+
assert.ok(workflow);
|
|
85
|
+
workflow.markWorkflowStarted();
|
|
86
|
+
assert.equal(workflow.owner.runnerStartedAt, 300);
|
|
87
|
+
} finally {
|
|
88
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("does not roll back when runner start was recorded only in memory", () => {
|
|
93
|
+
const rootDir = tempRoot();
|
|
94
|
+
try {
|
|
95
|
+
const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-a", kind: "runner", asyncDir: path.join(rootDir, "runs", "run-a") }, { rootDir });
|
|
96
|
+
assert.ok(handle);
|
|
97
|
+
handle.owner.runnerProcessInstanceId = "runner-a";
|
|
98
|
+
handle.owner.runnerStartedAt = Date.now();
|
|
99
|
+
|
|
100
|
+
assert.equal(handle.rollback(), false);
|
|
101
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
102
|
+
} finally {
|
|
103
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("retains active and unknown runner proof, then releases matching observed proof", () => {
|
|
108
|
+
const rootDir = tempRoot();
|
|
109
|
+
const asyncDir = path.join(rootDir, "runs", "run-a");
|
|
110
|
+
try {
|
|
111
|
+
const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 2, runId: "run-a", kind: "runner", asyncDir }, { rootDir });
|
|
112
|
+
assert.ok(handle);
|
|
113
|
+
handle.markStarted("runner-a");
|
|
114
|
+
writeJson(path.join(asyncDir, "status.json"), { runId: "run-a", sessionId: "session-a", mode: "single", state: "running", startedAt: 100, processTerminal: { version: 1, state: "pending", runId: "run-a", runnerProcessInstanceId: "runner-a" } });
|
|
115
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 2, { rootDir }), { used: 1, limit: 2 });
|
|
116
|
+
writeJson(path.join(asyncDir, "status.json"), { runId: "run-a", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100, processTerminal: { version: 1, state: "unknown", runId: "run-a", runnerProcessInstanceId: "runner-a", reason: "process-tree-unverified" } });
|
|
117
|
+
writeJson(path.join(asyncDir, "process-terminal.json"), { version: 1, state: "unknown", runId: "run-a", runnerProcessInstanceId: "runner-a", reason: "process-tree-unverified" });
|
|
118
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 2, { rootDir }), { used: 1, limit: 2 });
|
|
119
|
+
writeJson(path.join(asyncDir, "process-terminal.json"), observedProof("run-a", "runner-a"));
|
|
120
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 2, { rootDir }), { used: 0, limit: 2 });
|
|
121
|
+
} finally {
|
|
122
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does not delete a slot recreated while a terminal owner is released", () => {
|
|
127
|
+
const rootDir = tempRoot();
|
|
128
|
+
const asyncDir = path.join(rootDir, "runs", "run-a");
|
|
129
|
+
try {
|
|
130
|
+
const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-a", kind: "runner", asyncDir }, { rootDir, token: () => "token-a" });
|
|
131
|
+
assert.ok(handle);
|
|
132
|
+
handle.markStarted("runner-a");
|
|
133
|
+
writeJson(path.join(asyncDir, "status.json"), { runId: "run-a", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100 });
|
|
134
|
+
writeJson(path.join(asyncDir, "process-terminal.json"), observedProof("run-a", "runner-a"));
|
|
135
|
+
|
|
136
|
+
let replacement: ReturnType<typeof acquireActiveAsyncCapacity>;
|
|
137
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, {
|
|
138
|
+
rootDir,
|
|
139
|
+
afterSlotRename: () => {
|
|
140
|
+
replacement = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-b", kind: "runner", asyncDir: path.join(rootDir, "runs", "run-b") }, { rootDir, token: () => "token-b" });
|
|
141
|
+
},
|
|
142
|
+
}), { used: 1, limit: 1 });
|
|
143
|
+
assert.equal(replacement!.owner.runId, "run-b");
|
|
144
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
145
|
+
} finally {
|
|
146
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("does not delete a slot recreated during pre-start rollback", () => {
|
|
151
|
+
const rootDir = tempRoot();
|
|
152
|
+
try {
|
|
153
|
+
const first = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-a", kind: "runner", asyncDir: path.join(rootDir, "runs", "run-a") }, {
|
|
154
|
+
rootDir,
|
|
155
|
+
token: () => "token-a",
|
|
156
|
+
afterSlotRename: () => {
|
|
157
|
+
const replacement = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "run-b", kind: "runner", asyncDir: path.join(rootDir, "runs", "run-b") }, { rootDir, token: () => "token-b" });
|
|
158
|
+
assert.ok(replacement);
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
assert.ok(first);
|
|
162
|
+
assert.equal(first.rollback(), true);
|
|
163
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
164
|
+
assert.equal(first.rollback(), false);
|
|
165
|
+
} finally {
|
|
166
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("releases a transferred failure that never started", () => {
|
|
171
|
+
const rootDir = tempRoot();
|
|
172
|
+
const sourceDir = path.join(rootDir, "runs", "source");
|
|
173
|
+
const failedDir = path.join(rootDir, "runs", "failed-revival");
|
|
174
|
+
try {
|
|
175
|
+
const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
|
|
176
|
+
assert.ok(source);
|
|
177
|
+
source.markStarted("source-runner");
|
|
178
|
+
writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "paused", startedAt: 100 });
|
|
179
|
+
const transferred = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "failed-revival", asyncDir: failedDir }, { rootDir });
|
|
180
|
+
assert.ok(transferred);
|
|
181
|
+
transferred.markStarted("failed-runner");
|
|
182
|
+
writeJson(path.join(failedDir, "status.json"), { runId: "failed-revival", sessionId: "session-a", mode: "single", state: "failed", startedAt: 200, error: "Failed before startup proceed", processTerminal: { version: 1, state: "not-started", runId: "failed-revival", runnerProcessInstanceId: "failed-runner" } });
|
|
183
|
+
|
|
184
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
185
|
+
} finally {
|
|
186
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("does not restore old source proof after a transferred runner starts", () => {
|
|
191
|
+
const rootDir = tempRoot();
|
|
192
|
+
const sourceDir = path.join(rootDir, "runs", "source");
|
|
193
|
+
const revivedDir = path.join(rootDir, "runs", "revived");
|
|
194
|
+
try {
|
|
195
|
+
const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
|
|
196
|
+
assert.ok(source);
|
|
197
|
+
source.markStarted("source-runner");
|
|
198
|
+
writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100 });
|
|
199
|
+
writeJson(path.join(sourceDir, "process-terminal.json"), observedProof("source", "source-runner"));
|
|
200
|
+
const transferred = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "revived", asyncDir: revivedDir }, { rootDir });
|
|
201
|
+
assert.ok(transferred);
|
|
202
|
+
writeJson(path.join(revivedDir, "status.json"), { runId: "revived", sessionId: "session-a", mode: "single", state: "failed", startedAt: 200, processTerminal: { version: 1, state: "not-started", runId: "revived", runnerProcessInstanceId: "revived-runner" } });
|
|
203
|
+
transferred.markStarted("revived-runner");
|
|
204
|
+
assert.equal(transferred.rollback(), false);
|
|
205
|
+
|
|
206
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
207
|
+
writeJson(path.join(revivedDir, "process-terminal.json"), observedProof("revived", "revived-runner"));
|
|
208
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
209
|
+
} finally {
|
|
210
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("transfers a paused reservation without changing usage", () => {
|
|
215
|
+
const rootDir = tempRoot();
|
|
216
|
+
const sourceDir = path.join(rootDir, "runs", "source");
|
|
217
|
+
try {
|
|
218
|
+
const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
|
|
219
|
+
assert.ok(source);
|
|
220
|
+
source.markStarted("runner-source");
|
|
221
|
+
writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "paused", startedAt: 100 });
|
|
222
|
+
const next = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "next", asyncDir: path.join(rootDir, "runs", "next") }, { rootDir });
|
|
223
|
+
assert.ok(next);
|
|
224
|
+
assert.equal(next.owner.sourceRunId, "source");
|
|
225
|
+
assert.equal(next.owner.generation, 1);
|
|
226
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
227
|
+
} finally {
|
|
228
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("transfers an existing reservation after the cap is disabled", () => {
|
|
233
|
+
const rootDir = tempRoot();
|
|
234
|
+
const sourceDir = path.join(rootDir, "runs", "source");
|
|
235
|
+
try {
|
|
236
|
+
const source = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "source", kind: "runner", asyncDir: sourceDir }, { rootDir });
|
|
237
|
+
assert.ok(source);
|
|
238
|
+
source.markStarted("runner-source");
|
|
239
|
+
writeJson(path.join(sourceDir, "status.json"), { runId: "source", sessionId: "session-a", mode: "single", state: "paused", startedAt: 100 });
|
|
240
|
+
const next = transferActiveAsyncCapacity({ sessionId: "session-a", limit: undefined, sourceRunId: "source", runId: "next", asyncDir: path.join(rootDir, "runs", "next") }, { rootDir });
|
|
241
|
+
assert.ok(next);
|
|
242
|
+
assert.equal(next.owner.runId, "next");
|
|
243
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", undefined, { rootDir }), { used: 1, limit: 0 });
|
|
244
|
+
} finally {
|
|
245
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("releases terminal workflows only after the controller is gone and async children have observed proof", () => {
|
|
250
|
+
const rootDir = tempRoot();
|
|
251
|
+
const asyncRoot = path.join(rootDir, "runs");
|
|
252
|
+
const workflowDir = path.join(asyncRoot, "workflow");
|
|
253
|
+
const childDir = path.join(asyncRoot, "child");
|
|
254
|
+
try {
|
|
255
|
+
const workflow = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow", kind: "workflow", asyncDir: workflowDir }, { rootDir });
|
|
256
|
+
assert.ok(workflow);
|
|
257
|
+
workflow.markWorkflowStarted();
|
|
258
|
+
writeJson(path.join(workflowDir, "status.json"), { runId: "workflow", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "foreground", async: false, status: "completed" }] });
|
|
259
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir, liveWorkflowRunIds: new Set(["workflow"]) }), { used: 1, limit: 1 });
|
|
260
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
261
|
+
|
|
262
|
+
const second = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow-2", kind: "workflow", asyncDir: workflowDir }, { rootDir });
|
|
263
|
+
assert.ok(second);
|
|
264
|
+
second.markWorkflowStarted();
|
|
265
|
+
writeJson(path.join(workflowDir, "status.json"), { runId: "workflow-2", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "async", runId: "child", async: true, status: "completed" }] });
|
|
266
|
+
writeJson(path.join(childDir, "status.json"), { runId: "child", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100, processTerminal: { version: 1, state: "unknown", runId: "child", runnerProcessInstanceId: "runner-child", reason: "process-tree-unverified" } });
|
|
267
|
+
writeJson(path.join(childDir, "process-terminal.json"), { version: 1, state: "unknown", runId: "child", runnerProcessInstanceId: "runner-child", reason: "process-tree-unverified" });
|
|
268
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
269
|
+
writeJson(path.join(childDir, "process-terminal.json"), observedProof("child", "runner-child"));
|
|
270
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
271
|
+
} finally {
|
|
272
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("retains terminal workflows while a resumed async child lacks observed proof", () => {
|
|
277
|
+
const rootDir = tempRoot();
|
|
278
|
+
const asyncRoot = path.join(rootDir, "runs");
|
|
279
|
+
const workflowDir = path.join(asyncRoot, "workflow");
|
|
280
|
+
const childDir = path.join(asyncRoot, "revived-child");
|
|
281
|
+
try {
|
|
282
|
+
const workflow = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow", kind: "workflow", asyncDir: workflowDir }, { rootDir });
|
|
283
|
+
assert.ok(workflow);
|
|
284
|
+
workflow.markWorkflowStarted();
|
|
285
|
+
writeJson(path.join(workflowDir, "status.json"), { runId: "workflow", sessionId: "session-a", mode: "workflow", state: "failed", startedAt: 100, steps: [{ agent: "worker", workflowKey: "resume", runId: "revived-child", async: true, status: "failed" }] });
|
|
286
|
+
writeJson(path.join(childDir, "status.json"), { runId: "revived-child", sessionId: "session-a", mode: "single", state: "failed", startedAt: 100, processTerminal: { version: 1, state: "not-started", runId: "revived-child", runnerProcessInstanceId: "runner-child" } });
|
|
287
|
+
|
|
288
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
289
|
+
writeJson(path.join(childDir, "process-terminal.json"), observedProof("revived-child", "runner-child"));
|
|
290
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
291
|
+
} finally {
|
|
292
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("releases terminal workflows when a child is marked failed before launch identity exists", () => {
|
|
297
|
+
const rootDir = tempRoot();
|
|
298
|
+
const workflowDir = path.join(rootDir, "runs", "workflow");
|
|
299
|
+
try {
|
|
300
|
+
const workflow = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow", kind: "workflow", asyncDir: workflowDir }, { rootDir });
|
|
301
|
+
assert.ok(workflow);
|
|
302
|
+
workflow.markWorkflowStarted();
|
|
303
|
+
writeJson(path.join(workflowDir, "status.json"), {
|
|
304
|
+
runId: "workflow",
|
|
305
|
+
sessionId: "session-a",
|
|
306
|
+
mode: "workflow",
|
|
307
|
+
state: "failed",
|
|
308
|
+
startedAt: 100,
|
|
309
|
+
steps: [{ agent: "worker", workflowKey: "prep-failure", status: "failed", async: false, error: "gate cannot be combined with acceptance" }],
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
313
|
+
} finally {
|
|
314
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -80,8 +80,27 @@ describe("builtin agent overrides", () => {
|
|
|
80
80
|
assert.equal(scout?.modelSource?.type, "subagents.defaultModel");
|
|
81
81
|
assert.equal(scout?.modelSource?.scope, "user");
|
|
82
82
|
assert.equal(builtins.find((agent) => agent.name === "worker")?.model, "deepseek-v4-flash");
|
|
83
|
-
|
|
84
|
-
assert.equal(
|
|
83
|
+
const oracle = builtins.find((agent) => agent.name === "oracle");
|
|
84
|
+
assert.equal(oracle?.model, "deepseek-v4-pro");
|
|
85
|
+
assert.equal(oracle?.modelSource, undefined);
|
|
86
|
+
const reviewer = builtins.find((agent) => agent.name === "reviewer");
|
|
87
|
+
assert.equal(reviewer?.model, undefined);
|
|
88
|
+
assert.equal(reviewer?.modelSource, undefined);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("clears subagents.defaultModel provenance for same-value agent model overrides", () => {
|
|
92
|
+
writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
|
|
93
|
+
subagents: {
|
|
94
|
+
defaultModel: "deepseek-v4-flash",
|
|
95
|
+
agentOverrides: {
|
|
96
|
+
worker: { model: "deepseek-v4-flash" },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const worker = discoverAgentsAll(tempProject).builtin.find((agent) => agent.name === "worker");
|
|
102
|
+
assert.equal(worker?.model, "deepseek-v4-flash");
|
|
103
|
+
assert.equal(worker?.modelSource, undefined);
|
|
85
104
|
});
|
|
86
105
|
|
|
87
106
|
it("prefers project subagents.defaultModel over user defaultModel", () => {
|
|
@@ -2,8 +2,21 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { describe, it } from "node:test";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
6
6
|
import { readAsyncRecoveryDescriptor } from "../../src/runs/background/async-resume.ts";
|
|
7
|
+
import { createRunFanoutBudget } from "../../src/runs/shared/run-fanout-budget.ts";
|
|
8
|
+
|
|
9
|
+
const budgetDirectories: string[] = [];
|
|
10
|
+
|
|
11
|
+
function runFanoutBudget(runId: string) {
|
|
12
|
+
const descriptor = createRunFanoutBudget(runId, 64);
|
|
13
|
+
budgetDirectories.push(descriptor.directory);
|
|
14
|
+
return descriptor;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const directory of budgetDirectories.splice(0)) fs.rmSync(directory, { recursive: true, force: true });
|
|
19
|
+
});
|
|
7
20
|
|
|
8
21
|
describe("async recovery descriptor", () => {
|
|
9
22
|
it("accepts launchContractDigest written by async execution", () => {
|
|
@@ -13,6 +26,7 @@ describe("async recovery descriptor", () => {
|
|
|
13
26
|
fs.writeFileSync(path.join(root, "recovery-descriptor.json"), JSON.stringify({
|
|
14
27
|
version: 1,
|
|
15
28
|
launchContractDigest: digest,
|
|
29
|
+
runFanoutBudget: runFanoutBudget("run-digest"),
|
|
16
30
|
sourceRunId: "run-digest",
|
|
17
31
|
agent: "worker",
|
|
18
32
|
cwd: root,
|
|
@@ -38,6 +52,7 @@ describe("async recovery descriptor", () => {
|
|
|
38
52
|
fs.writeFileSync(path.join(root, "recovery-descriptor.json"), JSON.stringify({
|
|
39
53
|
version: 1,
|
|
40
54
|
launchContractDigest: {},
|
|
55
|
+
runFanoutBudget: runFanoutBudget("run-bad-digest"),
|
|
41
56
|
sourceRunId: "run-digest",
|
|
42
57
|
agent: "worker",
|
|
43
58
|
cwd: root,
|
|
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../../src/runs/background/async-resume.ts";
|
|
7
|
+
import { createRunFanoutBudget } from "../../src/runs/shared/run-fanout-budget.ts";
|
|
7
8
|
|
|
8
9
|
function writeJson(filePath: string, value: object): void {
|
|
9
10
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -97,7 +98,7 @@ describe("async resume lookup", () => {
|
|
|
97
98
|
steps: [{ agent: "worker", status: "paused", sessionFile }],
|
|
98
99
|
});
|
|
99
100
|
const descriptor = {
|
|
100
|
-
version: 1, sourceRunId: "run-descriptor", agent: "worker", cwd: root, systemPromptMode: "replace",
|
|
101
|
+
version: 1, runFanoutBudget: createRunFanoutBudget("run-descriptor", 64), sourceRunId: "run-descriptor", agent: "worker", cwd: root, systemPromptMode: "replace",
|
|
101
102
|
inheritProjectContext: false, inheritSkills: false, outputMode: "inline", maxSubagentDepth: 2, share: false,
|
|
102
103
|
};
|
|
103
104
|
writeJson(path.join(asyncDir, "recovery-descriptor.json"), { ...descriptor, token: "must-not-be-accepted" });
|
|
@@ -141,6 +142,7 @@ describe("async resume lookup", () => {
|
|
|
141
142
|
});
|
|
142
143
|
const descriptor = {
|
|
143
144
|
version: 1,
|
|
145
|
+
runFanoutBudget: createRunFanoutBudget("run-turn-budget", 64),
|
|
144
146
|
sourceRunId: "run-turn-budget",
|
|
145
147
|
agent: "worker",
|
|
146
148
|
cwd: root,
|
|
@@ -195,6 +197,7 @@ describe("async resume lookup", () => {
|
|
|
195
197
|
});
|
|
196
198
|
writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
|
|
197
199
|
version: 1,
|
|
200
|
+
runFanoutBudget: createRunFanoutBudget("run-acceptance", 64),
|
|
198
201
|
sourceRunId: "run-acceptance",
|
|
199
202
|
agent: "worker",
|
|
200
203
|
cwd: root,
|
|
@@ -244,6 +247,7 @@ describe("async resume lookup", () => {
|
|
|
244
247
|
});
|
|
245
248
|
writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
|
|
246
249
|
version: 1,
|
|
250
|
+
runFanoutBudget: createRunFanoutBudget("run-reviewed-acceptance", 64),
|
|
247
251
|
sourceRunId: "run-reviewed-acceptance",
|
|
248
252
|
agent: "worker",
|
|
249
253
|
cwd: root,
|
|
@@ -288,6 +292,7 @@ describe("async resume lookup", () => {
|
|
|
288
292
|
});
|
|
289
293
|
writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
|
|
290
294
|
version: 1,
|
|
295
|
+
runFanoutBudget: createRunFanoutBudget("run-inferred-acceptance", 64),
|
|
291
296
|
sourceRunId: "run-inferred-acceptance",
|
|
292
297
|
agent: "worker",
|
|
293
298
|
cwd: root,
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from "../../src/runs/background/chain-append.ts";
|
|
13
13
|
import type { AsyncStatus } from "../../src/shared/types.ts";
|
|
14
14
|
import type { RunnerStep } from "../../src/runs/shared/parallel-utils.ts";
|
|
15
|
+
import { claimRunFanoutBatchWithCommit, createRunFanoutBudget, getRunFanoutBudgetSnapshot } from "../../src/runs/shared/run-fanout-budget.ts";
|
|
16
|
+
import { PROMPT_REDACTED } from "../../src/shared/utils.ts";
|
|
15
17
|
import { createTempDir, removeTempDir } from "../support/helpers.ts";
|
|
16
18
|
|
|
17
19
|
function writeStatus(asyncDir: string, status: Partial<AsyncStatus> & Pick<AsyncStatus, "runId" | "mode" | "state" | "startedAt">): void {
|
|
@@ -71,6 +73,75 @@ describe("chain append requests", () => {
|
|
|
71
73
|
}
|
|
72
74
|
});
|
|
73
75
|
|
|
76
|
+
it("persists the request inside the admission callback", () => {
|
|
77
|
+
const root = createTempDir("pi-chain-append-admission-");
|
|
78
|
+
try {
|
|
79
|
+
const asyncDir = path.join(root, "run-admission");
|
|
80
|
+
writeStatus(asyncDir, {
|
|
81
|
+
runId: "run-admission",
|
|
82
|
+
mode: "chain",
|
|
83
|
+
state: "running",
|
|
84
|
+
startedAt: 100,
|
|
85
|
+
steps: [{ agent: "scout", status: "running" }],
|
|
86
|
+
});
|
|
87
|
+
let admitted = false;
|
|
88
|
+
const result = enqueueChainAppendRequest({
|
|
89
|
+
asyncDir,
|
|
90
|
+
runId: "run-admission",
|
|
91
|
+
steps: [runnerStep("worker")],
|
|
92
|
+
now: 200,
|
|
93
|
+
admit: (persist) => {
|
|
94
|
+
assert.equal(countPendingChainAppendRequests(asyncDir), 0);
|
|
95
|
+
persist();
|
|
96
|
+
admitted = true;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.equal(admitted, true);
|
|
101
|
+
assert.equal(result.pendingCount, 1);
|
|
102
|
+
} finally {
|
|
103
|
+
removeTempDir(root);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("reports accepted appends and keeps admission claims when bookkeeping fails after persistence", () => {
|
|
108
|
+
const root = createTempDir("pi-chain-append-bookkeeping-");
|
|
109
|
+
const budget = createRunFanoutBudget("append-bookkeeping", 1);
|
|
110
|
+
try {
|
|
111
|
+
const asyncDir = path.join(root, "run-bookkeeping");
|
|
112
|
+
writeStatus(asyncDir, {
|
|
113
|
+
runId: "run-bookkeeping",
|
|
114
|
+
mode: "chain",
|
|
115
|
+
state: "running",
|
|
116
|
+
startedAt: 100,
|
|
117
|
+
steps: [{ agent: "scout", status: "running" }],
|
|
118
|
+
});
|
|
119
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
120
|
+
fs.mkdirSync(path.join(asyncDir, "events.jsonl"));
|
|
121
|
+
|
|
122
|
+
const result = enqueueChainAppendRequest({
|
|
123
|
+
asyncDir,
|
|
124
|
+
runId: "run-bookkeeping",
|
|
125
|
+
steps: [runnerStep("worker")],
|
|
126
|
+
now: 200,
|
|
127
|
+
admit: (persist) => claimRunFanoutBatchWithCommit(budget, ["chain[1]"], () => {
|
|
128
|
+
persist();
|
|
129
|
+
fs.rmSync(statusPath, { force: true });
|
|
130
|
+
fs.mkdirSync(statusPath);
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.equal(result.pendingCount, 1);
|
|
135
|
+
assert.match(result.bookkeepingError ?? "", /status update failed/);
|
|
136
|
+
assert.match(result.bookkeepingError ?? "", /event append failed/);
|
|
137
|
+
assert.equal(readPendingChainAppendRequests(asyncDir).length, 1);
|
|
138
|
+
assert.deepEqual(getRunFanoutBudgetSnapshot(budget), { used: 1, limit: 1, remaining: 0 });
|
|
139
|
+
} finally {
|
|
140
|
+
fs.rmSync(budget.directory, { recursive: true, force: true });
|
|
141
|
+
removeTempDir(root);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
74
145
|
it("reads pending append requests without consuming them and reports reserved output names", () => {
|
|
75
146
|
const root = createTempDir("pi-chain-append-pending-");
|
|
76
147
|
try {
|
|
@@ -198,11 +269,11 @@ describe("chain append requests", () => {
|
|
|
198
269
|
"reviewer:pending",
|
|
199
270
|
"auditor:pending",
|
|
200
271
|
]);
|
|
201
|
-
// Appended steps must carry
|
|
272
|
+
// Appended steps must not carry raw task text into durable status.
|
|
202
273
|
assert.deepEqual(status.steps?.slice(1).map((step) => step.description), [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
274
|
+
PROMPT_REDACTED,
|
|
275
|
+
PROMPT_REDACTED,
|
|
276
|
+
PROMPT_REDACTED,
|
|
206
277
|
]);
|
|
207
278
|
assert.deepEqual(status.parallelGroups, [{ start: 2, count: 2, stepIndex: 2 }]);
|
|
208
279
|
assert.equal(status.workflowGraph?.nodes[1]?.id, "step-1");
|
|
@@ -53,6 +53,24 @@ test("declared read-only builtin tools suppress implementation-word false positi
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test("hyphenated fix adjectives in review tasks do not trigger the completion guard", () => {
|
|
57
|
+
const result = evaluateCompletionMutationGuard({
|
|
58
|
+
agent: "worker",
|
|
59
|
+
task: "Return a review with the top 2-3 must-fix items.",
|
|
60
|
+
messages: [assistantText("Review: findings with severity labels.")],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(result, {
|
|
64
|
+
expectedMutation: false,
|
|
65
|
+
attemptedMutation: false,
|
|
66
|
+
triggered: false,
|
|
67
|
+
});
|
|
68
|
+
assert.equal(
|
|
69
|
+
expectsImplementationMutation("worker", "Return a review with the top 2-3 must-fix items."),
|
|
70
|
+
false,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
56
74
|
test("read-only issue drafting tasks do not trigger on suggested fix wording", () => {
|
|
57
75
|
const task = "Draft GitHub issue for pi-subagents bug from current conversation. Include title, environment/context, reproduction steps, actual/expected, logs excerpt, suspected cause, suggested fix. Terse but complete. No tools needed.";
|
|
58
76
|
const result = evaluateCompletionMutationGuard({
|
|
@@ -69,7 +69,7 @@ describe("buildDoctorReport", () => {
|
|
|
69
69
|
|
|
70
70
|
const report = buildDoctorReport({
|
|
71
71
|
cwd: root,
|
|
72
|
-
config: { defaultSessionDir: "~/subagent-sessions", intercomBridge: { mode: "always" }, maxSubagentSpawnsPerSession: 4 },
|
|
72
|
+
config: { defaultSessionDir: "~/subagent-sessions", intercomBridge: { mode: "always" }, maxSubagentSpawnsPerSession: 4, maxActiveAsyncRunsPerSession: 2 },
|
|
73
73
|
state,
|
|
74
74
|
currentSessionFile: path.join(root, "sessions", "parent.jsonl"),
|
|
75
75
|
currentSessionId: "session-abc123",
|
|
@@ -116,6 +116,10 @@ describe("buildDoctorReport", () => {
|
|
|
116
116
|
assert.match(report, /Spawn budget\n- usage: 3\/5 used, 2 remaining \(configured 4; granted 1; grant allowance 3\)/);
|
|
117
117
|
assert.match(report, /- recent grants: \+1 at 1970-01-01T00:00:00\.000Z \(4 → 5\)/);
|
|
118
118
|
assert.match(report, /new parent session resets usage and grants; compaction does not/);
|
|
119
|
+
assert.match(report, /Run fan-out budget\n- configured limit: 64 \(default\)/);
|
|
120
|
+
assert.match(report, /cumulative claims are never released; a new top-level run creates a new budget/);
|
|
121
|
+
assert.match(report, /Active async capacity\n- usage: 0\/2 used/);
|
|
122
|
+
assert.match(report, /missing or unknown cleanup proof retains capacity/);
|
|
119
123
|
assert.match(report, /- skills: total 2 \(project 1, user-package 1\)/);
|
|
120
124
|
assert.match(report, /- bridge: active/);
|
|
121
125
|
assert.match(report, /- supervisor channel: available \(native:pi-subagents-supervisor-channel\)/);
|
|
@@ -125,6 +129,30 @@ describe("buildDoctorReport", () => {
|
|
|
125
129
|
}
|
|
126
130
|
});
|
|
127
131
|
|
|
132
|
+
it("reports the effective source when the run fan-out environment value is invalid", () => {
|
|
133
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-doctor-fanout-source-"));
|
|
134
|
+
const previous = process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_RUN;
|
|
135
|
+
try {
|
|
136
|
+
process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_RUN = "invalid";
|
|
137
|
+
const report = buildDoctorReport({
|
|
138
|
+
cwd: root,
|
|
139
|
+
config: { maxSubagentSpawnsPerRun: 12 },
|
|
140
|
+
state: makeState(root),
|
|
141
|
+
deps: {
|
|
142
|
+
isAsyncAvailable: () => true,
|
|
143
|
+
discoverAgentsAll: () => ({ builtin: [], user: [], project: [], chains: [], userDir: root, projectDir: root, userChainDir: root, projectChainDir: root, userSettingsPath: path.join(root, "user.json"), projectSettingsPath: path.join(root, "project.json") }),
|
|
144
|
+
discoverAvailableSkills: () => [],
|
|
145
|
+
diagnoseIntercomBridge: () => ({ active: false, mode: "off", wantsIntercom: false, supervisorChannelAvailable: false, extensionDir: "none" }),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
assert.match(report, /Run fan-out budget\n- configured limit: 12 \(config\)/);
|
|
149
|
+
} finally {
|
|
150
|
+
if (previous === undefined) delete process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_RUN;
|
|
151
|
+
else process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_RUN = previous;
|
|
152
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
128
156
|
it("keeps reporting when a directory or discovery check fails", () => {
|
|
129
157
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-doctor-failure-"));
|
|
130
158
|
try {
|