sortie-dogs 0.5.2 → 0.5.3
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/README.md +1 -1
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/worktree-commit-artifact.d.ts +1 -0
- package/dist/core/worktree-commit-artifact.js +89 -23
- package/dist/core/worktree-lifecycle.js +3 -3
- package/dist/plugin/fast-lane.d.ts +1 -0
- package/dist/plugin/fast-lane.js +13 -3
- package/dist/plugin/index.js +152 -17
- package/dist/runtime-assets.d.ts +7 -7
- package/dist/runtime-assets.js +21 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
20
20
|
|
|
21
21
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
22
22
|
|
|
23
|
-
Release: [v0.5.
|
|
23
|
+
Release: [v0.5.3](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.5.3)
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.34-parallel-host-contract-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export const RUNTIME_ASSET_VERSION = "0.3.34-parallel-host-contract-v1";
|
|
@@ -4,6 +4,7 @@ export declare class WorktreeCommitArtifactError extends Error {
|
|
|
4
4
|
readonly code: WorktreeCommitArtifactErrorCode;
|
|
5
5
|
constructor(code: WorktreeCommitArtifactErrorCode, message: string);
|
|
6
6
|
}
|
|
7
|
+
export declare function resolveValidationExecutable(executable: string): Promise<string | undefined>;
|
|
7
8
|
/** Runs one bounded command without exposing output or performing Git mutations. */
|
|
8
9
|
export declare function runContainedValidation(request: ContainedValidationRequest): Promise<ContainedValidationResult>;
|
|
9
10
|
export declare function produceWorktreeCommitArtifact(request: WorktreeCommitProduceRequest): Promise<WorktreeCommitArtifact>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { lstat, realpath } from "node:fs/promises";
|
|
4
|
-
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
4
|
+
import { delimiter, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
5
|
import { normalizeWorktreeScopePath } from "./worktree-scope.js";
|
|
6
6
|
const SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u;
|
|
7
7
|
const HASH = /^[0-9a-f]{64}$/u;
|
|
@@ -368,6 +368,26 @@ function exactKeys(value, keys) {
|
|
|
368
368
|
function validText(value, max = 256) {
|
|
369
369
|
return typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/u.test(value);
|
|
370
370
|
}
|
|
371
|
+
export async function resolveValidationExecutable(executable) {
|
|
372
|
+
if (isAbsolute(executable))
|
|
373
|
+
return realpath(executable).catch(() => undefined);
|
|
374
|
+
if (!validText(executable, MAX_COMMAND_TEXT) || executable.startsWith("-") || /[\\/]/u.test(executable))
|
|
375
|
+
return undefined;
|
|
376
|
+
const path = process.env.PATH ?? process.env.Path;
|
|
377
|
+
if (path === undefined)
|
|
378
|
+
return undefined;
|
|
379
|
+
const extensions = process.platform === "win32" && extname(executable).length === 0
|
|
380
|
+
? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((value) => value.length > 0)
|
|
381
|
+
: [""];
|
|
382
|
+
for (const directory of path.split(delimiter)) {
|
|
383
|
+
for (const extension of extensions) {
|
|
384
|
+
const candidate = await realpath(resolve(directory || ".", `${executable}${extension}`)).catch(() => undefined);
|
|
385
|
+
if (candidate !== undefined && isAbsolute(candidate))
|
|
386
|
+
return candidate;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
371
391
|
function pathIdentity(value) {
|
|
372
392
|
const normalized = resolve(value).split(sep).join("/");
|
|
373
393
|
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
@@ -550,6 +570,10 @@ async function runBounded(executable, args, cwd, timeout, kind) {
|
|
|
550
570
|
clearTimeout(timer);
|
|
551
571
|
}
|
|
552
572
|
}
|
|
573
|
+
function boundedCommandKind(executable, args) {
|
|
574
|
+
return /(?:^|[\\/])git(?:\.exe)?$/iu.test(executable) && args.length === 2 &&
|
|
575
|
+
args[0] === "diff" && args[1] === "--check" ? "git" : "validation";
|
|
576
|
+
}
|
|
553
577
|
/** Runs one bounded command without exposing output or performing Git mutations. */
|
|
554
578
|
export async function runContainedValidation(request) {
|
|
555
579
|
const suppliedArgs = isRecord(request) && Array.isArray(request.args) ? request.args : [];
|
|
@@ -560,21 +584,23 @@ export async function runContainedValidation(request) {
|
|
|
560
584
|
const failed = (command, exitCode, error) => Object.freeze({ ok: false, command, exit_code: exitCode,
|
|
561
585
|
fingerprint: createHash("sha256").update(JSON.stringify([command, exitCode, error])).digest("hex"), error });
|
|
562
586
|
if (!isRecord(request) || !exactKeys(request, ["cwd", "executable", "timeout_ms", ...(request.args === undefined ? [] : ["args"])]) ||
|
|
563
|
-
!validText(request.executable, MAX_COMMAND_TEXT) ||
|
|
587
|
+
!validText(request.executable, MAX_COMMAND_TEXT) ||
|
|
588
|
+
(!isAbsolute(request.executable) && (request.executable.startsWith("-") || /[\\/]/u.test(request.executable))) ||
|
|
564
589
|
!validText(request.cwd, MAX_PATH) || !isAbsolute(request.cwd) ||
|
|
565
590
|
!Number.isInteger(request.timeout_ms) || request.timeout_ms < 1 || request.timeout_ms > MAX_TIMEOUT ||
|
|
566
591
|
!Array.isArray(suppliedArgs) || suppliedArgs.length > MAX_ARGUMENTS ||
|
|
567
592
|
!suppliedArgs.every((arg) => validText(arg, MAX_COMMAND_TEXT)))
|
|
568
593
|
return failed(fallbackCommand, null, "invalid-request");
|
|
569
594
|
const [executable, cwd] = await Promise.all([
|
|
570
|
-
|
|
595
|
+
resolveValidationExecutable(request.executable),
|
|
596
|
+
realpath(request.cwd).catch(() => undefined),
|
|
571
597
|
]);
|
|
572
|
-
if (executable === undefined || cwd === undefined || !isAbsolute(
|
|
598
|
+
if (executable === undefined || cwd === undefined || !isAbsolute(cwd)) {
|
|
573
599
|
return failed(fallbackCommand, null, "invalid-request");
|
|
574
600
|
}
|
|
575
601
|
const command = Object.freeze([executable, ...suppliedArgs]);
|
|
576
602
|
try {
|
|
577
|
-
const result = await runBounded(executable, suppliedArgs, cwd, request.timeout_ms,
|
|
603
|
+
const result = await runBounded(executable, suppliedArgs, cwd, request.timeout_ms, boundedCommandKind(executable, suppliedArgs));
|
|
578
604
|
const fingerprint = createHash("sha256").update(JSON.stringify([command, result.code])).digest("hex");
|
|
579
605
|
return result.code === 0
|
|
580
606
|
? Object.freeze({ ok: true, command, exit_code: 0, fingerprint, error: null })
|
|
@@ -679,12 +705,15 @@ function parseStatus(source, staged) {
|
|
|
679
705
|
throw new WorktreeCommitArtifactError("invalid-state", "Git status is ambiguous.");
|
|
680
706
|
const x = field[0];
|
|
681
707
|
const y = field[1];
|
|
682
|
-
const untracked =
|
|
708
|
+
const untracked = x === "?" && y === "?";
|
|
683
709
|
if (!untracked && (x === "?" || y === "?" || x === "!" || y === "!" || "RCUT".includes(x) || "RCUT".includes(y))) {
|
|
684
710
|
throw new WorktreeCommitArtifactError("invalid-state", "Untracked, renamed, copied, or unsupported changes are forbidden.");
|
|
685
711
|
}
|
|
686
|
-
|
|
687
|
-
|
|
712
|
+
const unstagedEdit = x === " " && (y === "M" || y === "D");
|
|
713
|
+
const stagedEdit = (x === "A" || x === "M" || x === "D") && y === " ";
|
|
714
|
+
if ((!untracked && staged === "forbid" && !unstagedEdit) ||
|
|
715
|
+
(!untracked && staged === "require" && !stagedEdit) ||
|
|
716
|
+
(!untracked && staged === "either" && !unstagedEdit && !stagedEdit)) {
|
|
688
717
|
throw new WorktreeCommitArtifactError("invalid-state", "Index or worktree status is not an accepted implementation edit.");
|
|
689
718
|
}
|
|
690
719
|
const path = field.slice(3);
|
|
@@ -695,7 +724,7 @@ function parseStatus(source, staged) {
|
|
|
695
724
|
catch {
|
|
696
725
|
throw new WorktreeCommitArtifactError("invalid-state", "Changed path is not canonical.");
|
|
697
726
|
}
|
|
698
|
-
entries.push({ code: untracked ? "A" :
|
|
727
|
+
entries.push({ code: untracked ? "A" : stagedEdit ? x : y, path });
|
|
699
728
|
if (folded.length === 0)
|
|
700
729
|
throw new WorktreeCommitArtifactError("invalid-state", "Changed path is invalid.");
|
|
701
730
|
}
|
|
@@ -714,9 +743,34 @@ function assertScope(entries, scope) {
|
|
|
714
743
|
}
|
|
715
744
|
}
|
|
716
745
|
async function status(context, staged) {
|
|
717
|
-
|
|
746
|
+
const entries = parseStatus(await context.git([
|
|
718
747
|
"status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none",
|
|
719
748
|
]), staged);
|
|
749
|
+
const controls = new Set([
|
|
750
|
+
`handoff.${context.descriptor.task_id}.json`,
|
|
751
|
+
`${context.descriptor.task_id}.operation-manifest.json`,
|
|
752
|
+
]);
|
|
753
|
+
return entries.filter(({ code, path }) => code !== "A" || !controls.has(path));
|
|
754
|
+
}
|
|
755
|
+
async function stagedPaths(context) {
|
|
756
|
+
if ((await context.git(["ls-files", "-u", "-z"])).length !== 0) {
|
|
757
|
+
throw new WorktreeCommitArtifactError("invalid-state", "An unmerged index is forbidden.");
|
|
758
|
+
}
|
|
759
|
+
const fields = decodeGitOutput(await context.git([
|
|
760
|
+
"diff", "--cached", "--name-only", "--no-renames", "-z", "--",
|
|
761
|
+
]), "Staged path encoding is invalid.").split("\0");
|
|
762
|
+
if (fields.at(-1) !== "")
|
|
763
|
+
throw new WorktreeCommitArtifactError("invalid-state", "Staged paths are ambiguous.");
|
|
764
|
+
fields.pop();
|
|
765
|
+
for (const path of fields) {
|
|
766
|
+
if (normalizeWorktreeScopePath(path) !== path) {
|
|
767
|
+
throw new WorktreeCommitArtifactError("invalid-state", "A staged path is not canonical.");
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (new Set(fields.map((path) => path.toLowerCase())).size !== fields.length) {
|
|
771
|
+
throw new WorktreeCommitArtifactError("invalid-state", "Staged path identities are ambiguous.");
|
|
772
|
+
}
|
|
773
|
+
return new Set(fields);
|
|
720
774
|
}
|
|
721
775
|
async function assertNoSubmodules(context, entries) {
|
|
722
776
|
for (const entry of entries) {
|
|
@@ -884,9 +938,7 @@ async function committedEntries(context, base, commit) {
|
|
|
884
938
|
}
|
|
885
939
|
async function validateCommit(context, artifact) {
|
|
886
940
|
await assertBaseState(context, artifact.commit_sha);
|
|
887
|
-
if ((await context.
|
|
888
|
-
"status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none",
|
|
889
|
-
])).length !== 0) {
|
|
941
|
+
if ((await status(context, "forbid")).length !== 0) {
|
|
890
942
|
throw new WorktreeCommitArtifactError("verification-failed", "Committed checkout is not clean.");
|
|
891
943
|
}
|
|
892
944
|
const parentLine = (await context.git(["rev-list", "--parents", "-n", "1", artifact.commit_sha])).toString("utf8").trim().split(" ");
|
|
@@ -934,7 +986,9 @@ export async function produceWorktreeCommitArtifact(request) {
|
|
|
934
986
|
if (!isRecord(request) || !exactKeys(request, ["descriptor", "managed_path", "validation", ...(request.git_path === undefined ? [] : ["git_path"])]) ||
|
|
935
987
|
!isRecord(request.validation) || !exactKeys(request.validation, ["executable", ...(request.validation.args === undefined ? [] : ["args"]),
|
|
936
988
|
...(request.validation.timeout_ms === undefined ? [] : ["timeout_ms"])]) ||
|
|
937
|
-
!validText(request.validation.executable, MAX_COMMAND_TEXT) ||
|
|
989
|
+
!validText(request.validation.executable, MAX_COMMAND_TEXT) ||
|
|
990
|
+
(!isAbsolute(request.validation.executable) &&
|
|
991
|
+
(request.validation.executable.startsWith("-") || /[\\/]/u.test(request.validation.executable))) ||
|
|
938
992
|
(request.validation.args !== undefined && (!Array.isArray(request.validation.args) || request.validation.args.length > MAX_ARGUMENTS ||
|
|
939
993
|
!request.validation.args.every((arg) => validText(arg, MAX_COMMAND_TEXT)))) ||
|
|
940
994
|
(request.validation.timeout_ms !== undefined && (!Number.isInteger(request.validation.timeout_ms) ||
|
|
@@ -942,26 +996,38 @@ export async function produceWorktreeCommitArtifact(request) {
|
|
|
942
996
|
throw new WorktreeCommitArtifactError("invalid-request", "Commit producer request is invalid.");
|
|
943
997
|
}
|
|
944
998
|
const context = await makeContext(request);
|
|
945
|
-
const executable = await
|
|
999
|
+
const executable = await resolveValidationExecutable(request.validation.executable);
|
|
946
1000
|
if (executable === undefined || !isAbsolute(executable)) {
|
|
947
1001
|
throw new WorktreeCommitArtifactError("invalid-request", "Validation executable does not exist.");
|
|
948
1002
|
}
|
|
949
1003
|
const command = Object.freeze([executable, ...(request.validation.args ?? [])]);
|
|
950
1004
|
await assertBaseState(context, context.descriptor.base_sha);
|
|
951
|
-
const before = await status(context, "
|
|
1005
|
+
const before = await status(context, "either");
|
|
952
1006
|
if (before.length === 0)
|
|
953
1007
|
throw new WorktreeCommitArtifactError("invalid-state", "No implementation changes exist.");
|
|
954
1008
|
assertScope(before, context.descriptor.scope_write);
|
|
955
1009
|
await assertNoSubmodules(context, before);
|
|
1010
|
+
const staged = await stagedPaths(context);
|
|
1011
|
+
const hostStaged = staged.size > 0;
|
|
1012
|
+
if (hostStaged && (boundedCommandKind(executable, request.validation.args ?? []) !== "git" ||
|
|
1013
|
+
staged.size !== before.length || !before.every(({ path }) => staged.has(path)))) {
|
|
1014
|
+
throw new WorktreeCommitArtifactError("invalid-state", "Staged changes are not an exact host snapshot of the implementation.");
|
|
1015
|
+
}
|
|
956
1016
|
const beforeFingerprint = await changeFingerprint(context, before, "worktree");
|
|
957
|
-
const validation = await runBounded(executable, request.validation.args ?? [], context.managedPath, request.validation.timeout_ms ?? GIT_TIMEOUT,
|
|
1017
|
+
const validation = await runBounded(executable, request.validation.args ?? [], context.managedPath, request.validation.timeout_ms ?? GIT_TIMEOUT, boundedCommandKind(executable, request.validation.args ?? []));
|
|
958
1018
|
if (validation.code !== 0)
|
|
959
1019
|
throw new WorktreeCommitArtifactError("validation-failed", validation.code === 240 ? "Validation containment setup failed."
|
|
960
1020
|
: validation.code === 241 ? "Validation left a descendant process."
|
|
961
1021
|
: validation.code === 238 ? "Validation exceeded its resource bound."
|
|
962
1022
|
: "Validation exited unsuccessfully.");
|
|
1023
|
+
if (hostStaged) {
|
|
1024
|
+
const stagedValidation = await runBounded(executable, ["diff", "--cached", "--check"], context.managedPath, request.validation.timeout_ms ?? GIT_TIMEOUT, "git");
|
|
1025
|
+
if (stagedValidation.code !== 0) {
|
|
1026
|
+
throw new WorktreeCommitArtifactError("validation-failed", "Staged implementation validation exited unsuccessfully.");
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
963
1029
|
await assertBaseState(context, context.descriptor.base_sha);
|
|
964
|
-
const after = await status(context, "
|
|
1030
|
+
const after = await status(context, "either");
|
|
965
1031
|
assertScope(after, context.descriptor.scope_write);
|
|
966
1032
|
await assertNoSubmodules(context, after);
|
|
967
1033
|
if (JSON.stringify(after) !== JSON.stringify(before) || await changeFingerprint(context, after, "worktree") !== beforeFingerprint) {
|
|
@@ -1000,7 +1066,9 @@ export async function recoverWorktreeCommitArtifact(request) {
|
|
|
1000
1066
|
if (!isRecord(request) || !exactKeys(request, ["descriptor", "managed_path", "validation", ...(request.git_path === undefined ? [] : ["git_path"])]) ||
|
|
1001
1067
|
!isRecord(request.validation) || !exactKeys(request.validation, ["executable", ...(request.validation.args === undefined ? [] : ["args"]),
|
|
1002
1068
|
...(request.validation.timeout_ms === undefined ? [] : ["timeout_ms"])]) ||
|
|
1003
|
-
!validText(request.validation.executable, MAX_COMMAND_TEXT) ||
|
|
1069
|
+
!validText(request.validation.executable, MAX_COMMAND_TEXT) ||
|
|
1070
|
+
(!isAbsolute(request.validation.executable) &&
|
|
1071
|
+
(request.validation.executable.startsWith("-") || /[\\/]/u.test(request.validation.executable))) ||
|
|
1004
1072
|
(request.validation.args !== undefined && (!Array.isArray(request.validation.args) || request.validation.args.length > MAX_ARGUMENTS ||
|
|
1005
1073
|
!request.validation.args.every((arg) => validText(arg, MAX_COMMAND_TEXT)))) ||
|
|
1006
1074
|
(request.validation.timeout_ms !== undefined && (!Number.isInteger(request.validation.timeout_ms) ||
|
|
@@ -1008,7 +1076,7 @@ export async function recoverWorktreeCommitArtifact(request) {
|
|
|
1008
1076
|
throw new WorktreeCommitArtifactError("invalid-request", "Commit recovery request is invalid.");
|
|
1009
1077
|
}
|
|
1010
1078
|
const context = await makeContext(request);
|
|
1011
|
-
const executable = await
|
|
1079
|
+
const executable = await resolveValidationExecutable(request.validation.executable);
|
|
1012
1080
|
if (executable === undefined || !isAbsolute(executable)) {
|
|
1013
1081
|
throw new WorktreeCommitArtifactError("invalid-request", "Validation executable does not exist.");
|
|
1014
1082
|
}
|
|
@@ -1019,9 +1087,7 @@ export async function recoverWorktreeCommitArtifact(request) {
|
|
|
1019
1087
|
}
|
|
1020
1088
|
if (state.head === context.descriptor.base_sha)
|
|
1021
1089
|
return undefined;
|
|
1022
|
-
if ((await context.
|
|
1023
|
-
"status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none",
|
|
1024
|
-
])).length !== 0) {
|
|
1090
|
+
if ((await status(context, "forbid")).length !== 0) {
|
|
1025
1091
|
throw new WorktreeCommitArtifactError("invalid-state", "Managed checkout is not clean.");
|
|
1026
1092
|
}
|
|
1027
1093
|
const parentLine = (await context.git(["rev-list", "--parents", "-n", "1", state.head])).toString("utf8").trim().split(" ");
|
|
@@ -20,7 +20,7 @@ const SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u;
|
|
|
20
20
|
const HASH = /^[0-9a-f]{64}$/u;
|
|
21
21
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
22
22
|
const DEVICE_ID = /^[1-9][0-9]*$/u;
|
|
23
|
-
const
|
|
23
|
+
const MAX_FILE_ID = (1n << 64n) - 1n;
|
|
24
24
|
const ACTIVE_PATH = /^wt-([0-9a-f]{16})-([0-9a-f]{32})$/u;
|
|
25
25
|
const QUARANTINE_PATH = /^rm-([0-9a-f]{16})-([0-9a-f]{32})$/u;
|
|
26
26
|
const PHASES = new Set(["creating", "setting-up", "ready", "removing", "orphaned"]);
|
|
@@ -53,10 +53,10 @@ function pathWithin(path, parent) {
|
|
|
53
53
|
return childID === parentID || childID.startsWith(`${parentID}/`);
|
|
54
54
|
}
|
|
55
55
|
function safeStoredFileID(value) {
|
|
56
|
-
return typeof value === "string" && DEVICE_ID.test(value) && BigInt(value) <=
|
|
56
|
+
return typeof value === "string" && DEVICE_ID.test(value) && BigInt(value) <= MAX_FILE_ID;
|
|
57
57
|
}
|
|
58
58
|
function fileID(path, value, kind) {
|
|
59
|
-
const native = value > 0n && value <=
|
|
59
|
+
const native = value > 0n && value <= MAX_FILE_ID ? value.toString() : undefined;
|
|
60
60
|
if (native !== undefined)
|
|
61
61
|
return native;
|
|
62
62
|
if (process.platform !== "win32" || (kind === "inode" && value === 0n))
|
|
@@ -6,6 +6,7 @@ export declare class FastLaneDeniedError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
export interface FastLaneToolOptions {
|
|
8
8
|
readonly consultationFallbackAuthorized?: boolean;
|
|
9
|
+
readonly parallelWorkerAlreadyBound?: boolean;
|
|
9
10
|
readonly parallelWorkerAuthorized?: boolean;
|
|
10
11
|
}
|
|
11
12
|
export declare class FastLaneController {
|
package/dist/plugin/fast-lane.js
CHANGED
|
@@ -165,7 +165,7 @@ export class FastLaneController {
|
|
|
165
165
|
state.parallelMode = true;
|
|
166
166
|
state.workerLimit = totalTasks;
|
|
167
167
|
state.workerDispatches = running;
|
|
168
|
-
state.totalWorkerDispatches =
|
|
168
|
+
state.totalWorkerDispatches = dispatched;
|
|
169
169
|
}
|
|
170
170
|
continuationQueued(sessionID) {
|
|
171
171
|
const state = this.sessions.get(sessionID);
|
|
@@ -224,9 +224,19 @@ export class FastLaneController {
|
|
|
224
224
|
throw new FastLaneDeniedError("WORKER_RESUME_INVALID");
|
|
225
225
|
}
|
|
226
226
|
if (state.parallelMode) {
|
|
227
|
-
if (options.parallelWorkerAuthorized !== true
|
|
228
|
-
state.totalWorkerDispatches >= state.workerLimit)
|
|
227
|
+
if (options.parallelWorkerAuthorized !== true)
|
|
229
228
|
throw new FastLaneDeniedError("WORKER_LIMIT");
|
|
229
|
+
if (options.parallelWorkerAlreadyBound === true) {
|
|
230
|
+
if (state.workerDispatches < 1 || state.workerDispatches > state.parallelWorkerLimit ||
|
|
231
|
+
state.totalWorkerDispatches < 1 || state.totalWorkerDispatches > state.workerLimit) {
|
|
232
|
+
throw new FastLaneDeniedError("WORKER_LIMIT");
|
|
233
|
+
}
|
|
234
|
+
state.workerInFlight = true;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (state.workerDispatches >= state.parallelWorkerLimit || state.totalWorkerDispatches >= state.workerLimit) {
|
|
238
|
+
throw new FastLaneDeniedError("WORKER_LIMIT");
|
|
239
|
+
}
|
|
230
240
|
}
|
|
231
241
|
else if (state.backlogDrain &&
|
|
232
242
|
(state.workerDispatches >= 1 || state.totalWorkerDispatches >= state.workerLimit)) {
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { lstat, open, readFile, realpath, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
5
5
|
import { normalizeRelativePath, RelativePathError } from "../core/path.js";
|
|
6
6
|
import { ScopeLeaseError, ScopeLeaseRegistry } from "../core/scope-lease-registry.js";
|
|
7
|
-
import { produceWorktreeCommitArtifact, recoverWorktreeCommitArtifact, WorktreeCommitArtifactError, } from "../core/worktree-commit-artifact.js";
|
|
7
|
+
import { produceWorktreeCommitArtifact, recoverWorktreeCommitArtifact, resolveValidationExecutable, WorktreeCommitArtifactError, } from "../core/worktree-commit-artifact.js";
|
|
8
8
|
import { normalizeWorktreeScope } from "../core/worktree-scope.js";
|
|
9
9
|
import { ParallelDispatchCoordinator, ParallelDispatchError, } from "../core/worktree-parallel-dispatch.js";
|
|
10
10
|
import { IntegrationQueueError, WorktreeIntegrationQueue } from "../core/worktree-integration-queue.js";
|
|
@@ -640,12 +640,12 @@ function parseStringArray(value) {
|
|
|
640
640
|
function parallelDescriptor(text) {
|
|
641
641
|
const unique = (key) => {
|
|
642
642
|
const values = taskValues(text, [key]);
|
|
643
|
-
return values.length === 1 ? values[0] : undefined;
|
|
643
|
+
return values.length > 0 && new Set(values).size === 1 ? values[0] : undefined;
|
|
644
644
|
};
|
|
645
645
|
const runID = unique("run_id");
|
|
646
646
|
const dispatchID = unique("dispatch_id");
|
|
647
647
|
const taskID = unique("task_id");
|
|
648
|
-
const managedPath = unique("managed_path");
|
|
648
|
+
const managedPath = unique("managed_path") ?? unique("project_root");
|
|
649
649
|
const branch = unique("branch");
|
|
650
650
|
const baseSHA = unique("base_sha");
|
|
651
651
|
const dependsOn = parseStringArray(unique("depends_on"));
|
|
@@ -673,9 +673,11 @@ function sameParallelDescriptor(left, right) {
|
|
|
673
673
|
function parallelValidationRequest(args) {
|
|
674
674
|
const keys = Object.keys(args).sort();
|
|
675
675
|
const allowed = new Set(["dispatch_id", "run_id", "timeout_ms", "validation_args_json", "validation_executable"]);
|
|
676
|
+
const executable = args.validation_executable;
|
|
676
677
|
if (!keys.every((key) => allowed.has(key)) ||
|
|
677
678
|
!["dispatch_id", "run_id", "validation_executable"].every((key) => typeof args[key] === "string") ||
|
|
678
|
-
|
|
679
|
+
/[\u0000-\u001f\u007f]/u.test(executable) ||
|
|
680
|
+
(!isAbsolute(executable) && (executable.startsWith("-") || /[\\/]/u.test(executable))))
|
|
679
681
|
return undefined;
|
|
680
682
|
let validationArgs = [];
|
|
681
683
|
if (args.validation_args_json !== undefined) {
|
|
@@ -700,7 +702,7 @@ function parallelValidationRequest(args) {
|
|
|
700
702
|
return undefined;
|
|
701
703
|
}
|
|
702
704
|
const validation = {
|
|
703
|
-
executable:
|
|
705
|
+
executable: executable,
|
|
704
706
|
args: validationArgs,
|
|
705
707
|
...(timeout === undefined ? {} : { timeout_ms: timeout }),
|
|
706
708
|
};
|
|
@@ -1180,6 +1182,103 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1180
1182
|
validation: artifact.validation,
|
|
1181
1183
|
};
|
|
1182
1184
|
}
|
|
1185
|
+
function parallelControlPaths(descriptor) {
|
|
1186
|
+
return {
|
|
1187
|
+
handoff_path: join(descriptor.managed_path, `handoff.${descriptor.task_id}.json`),
|
|
1188
|
+
operation_manifest: join(descriptor.managed_path, `${descriptor.task_id}.operation-manifest.json`),
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
async function createParallelControlFiles(descriptor, validationCommands) {
|
|
1192
|
+
const canonicalRoot = await realpath(descriptor.managed_path);
|
|
1193
|
+
if (!samePath(canonicalRoot, descriptor.managed_path)) {
|
|
1194
|
+
throw new ParallelDispatchError("lifecycle-failed", "Managed worktree identity changed before contract creation.");
|
|
1195
|
+
}
|
|
1196
|
+
const paths = parallelControlPaths(descriptor);
|
|
1197
|
+
const manifestValue = {
|
|
1198
|
+
version: "0.1.0",
|
|
1199
|
+
task_id: descriptor.task_id,
|
|
1200
|
+
read: [...descriptor.scope_read],
|
|
1201
|
+
write: [...descriptor.scope_write],
|
|
1202
|
+
validation: [...validationCommands],
|
|
1203
|
+
};
|
|
1204
|
+
const handoffValue = {
|
|
1205
|
+
version: "0.1.0",
|
|
1206
|
+
profile: "minimal",
|
|
1207
|
+
id: descriptor.task_id,
|
|
1208
|
+
created_at: new Date().toISOString(),
|
|
1209
|
+
ext: { "sortie-dogs/write-gate": {
|
|
1210
|
+
operation_manifest: basename(paths.operation_manifest),
|
|
1211
|
+
project_root: descriptor.managed_path,
|
|
1212
|
+
} },
|
|
1213
|
+
task: {
|
|
1214
|
+
title: `Parallel task ${descriptor.task_id}`,
|
|
1215
|
+
objective: "Complete the prepared parallel descriptor within its declared scope.",
|
|
1216
|
+
},
|
|
1217
|
+
state: { done: [], next: ["Implement the prepared parallel descriptor."], blocked: [] },
|
|
1218
|
+
risks: [],
|
|
1219
|
+
verification: validationCommands.map((check) => ({
|
|
1220
|
+
check, status: "not_run", exit_code: null, summary: "Delegated to the parallel artifact capability.",
|
|
1221
|
+
})),
|
|
1222
|
+
};
|
|
1223
|
+
const manifest = validateOperationManifestSchema(manifestValue);
|
|
1224
|
+
const handoff = validateHandoffSchema(handoffValue);
|
|
1225
|
+
if (!manifest.ok || !handoff.ok ||
|
|
1226
|
+
validateManifest(handoff.value, manifest.value, undefined, false, { requirePassedValidation: false })
|
|
1227
|
+
.some(({ severity }) => severity === "error")) {
|
|
1228
|
+
throw new ParallelDispatchError("invalid-contract", "Generated parallel worker contract is invalid.");
|
|
1229
|
+
}
|
|
1230
|
+
const created = [];
|
|
1231
|
+
try {
|
|
1232
|
+
for (const [path, value] of [[paths.operation_manifest, manifestValue], [paths.handoff_path, handoffValue]]) {
|
|
1233
|
+
const content = JSON.stringify(value);
|
|
1234
|
+
try {
|
|
1235
|
+
const handle = await open(path, "wx", 0o600);
|
|
1236
|
+
try {
|
|
1237
|
+
await handle.writeFile(content, "utf8");
|
|
1238
|
+
await handle.sync();
|
|
1239
|
+
}
|
|
1240
|
+
finally {
|
|
1241
|
+
await handle.close();
|
|
1242
|
+
}
|
|
1243
|
+
created.push(path);
|
|
1244
|
+
}
|
|
1245
|
+
catch (error) {
|
|
1246
|
+
if (!isRecord(error) || error.code !== "EEXIST")
|
|
1247
|
+
throw error;
|
|
1248
|
+
const info = await lstat(path);
|
|
1249
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
1250
|
+
throw error;
|
|
1251
|
+
const existing = await readFile(path, "utf8");
|
|
1252
|
+
if (existing === content)
|
|
1253
|
+
continue;
|
|
1254
|
+
if (path !== paths.handoff_path)
|
|
1255
|
+
throw error;
|
|
1256
|
+
const existingValue = JSON.parse(existing);
|
|
1257
|
+
if (!isRecord(existingValue) || !validateHandoffSchema(existingValue).ok ||
|
|
1258
|
+
JSON.stringify({ ...existingValue, created_at: handoffValue.created_at }) !== content)
|
|
1259
|
+
throw error;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
await Promise.all(created.map((path) => rm(path, { force: true }).catch(() => undefined)));
|
|
1265
|
+
throw error;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async function removeParallelControlFiles(descriptor) {
|
|
1269
|
+
const paths = parallelControlPaths(descriptor);
|
|
1270
|
+
await Promise.all([
|
|
1271
|
+
rm(paths.handoff_path, { force: true }),
|
|
1272
|
+
rm(paths.operation_manifest, { force: true }),
|
|
1273
|
+
]);
|
|
1274
|
+
}
|
|
1275
|
+
async function ensureParallelReadyControls(snapshot) {
|
|
1276
|
+
if (snapshot.archived || snapshot.cancelled || snapshot.ready.length === 0)
|
|
1277
|
+
return;
|
|
1278
|
+
await ensureLoaded();
|
|
1279
|
+
const validationCommands = loaded?.manifest?.validation ?? [];
|
|
1280
|
+
await Promise.all(snapshot.ready.map((descriptor) => createParallelControlFiles(descriptor, validationCommands)));
|
|
1281
|
+
}
|
|
1183
1282
|
function boundedParallelSnapshot(snapshot) {
|
|
1184
1283
|
return {
|
|
1185
1284
|
run_id: snapshot.run_id,
|
|
@@ -1187,7 +1286,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1187
1286
|
cancelled: snapshot.cancelled,
|
|
1188
1287
|
archived: snapshot.archived,
|
|
1189
1288
|
terminal_reason: snapshot.terminal_reason,
|
|
1190
|
-
ready: snapshot.ready,
|
|
1289
|
+
ready: snapshot.ready.map((descriptor) => ({ ...descriptor, ...parallelControlPaths(descriptor) })),
|
|
1191
1290
|
tasks: snapshot.tasks.map(({ descriptor, worktree_id, phase, call_id, child_session_id, outcome, artifact }) => ({
|
|
1192
1291
|
task_id: descriptor.task_id,
|
|
1193
1292
|
worktree_id,
|
|
@@ -1257,7 +1356,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1257
1356
|
if (running === undefined)
|
|
1258
1357
|
return deny("dispatch-inactive");
|
|
1259
1358
|
if (running.artifact !== null) {
|
|
1260
|
-
const executable = await
|
|
1359
|
+
const executable = await resolveValidationExecutable(request.validation.executable);
|
|
1261
1360
|
if (executable === undefined)
|
|
1262
1361
|
return deny("invalid-request");
|
|
1263
1362
|
const requestedCommand = [executable, ...request.validation.args];
|
|
@@ -1265,6 +1364,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1265
1364
|
return deny("artifact-replay");
|
|
1266
1365
|
}
|
|
1267
1366
|
await (await getParallelCoordinator()).acceptArtifact(binding.ownerRoot, binding.completionCallID, sessionID, binding.descriptor, running.artifact);
|
|
1367
|
+
await removeParallelControlFiles(binding.descriptor);
|
|
1268
1368
|
parallelArtifacts.set(sessionID, { requestFingerprint: request.fingerprint, artifact: running.artifact });
|
|
1269
1369
|
pruneParallelChildMap(parallelArtifacts);
|
|
1270
1370
|
return JSON.stringify({ status: "created", replay: true, artifact: boundedParallelArtifact(running.artifact) });
|
|
@@ -1278,6 +1378,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1278
1378
|
const recovered = await recoverWorktreeCommitArtifact(produceRequest);
|
|
1279
1379
|
const artifact = recovered ?? await produceWorktreeCommitArtifact(produceRequest);
|
|
1280
1380
|
await (await getParallelCoordinator()).acceptArtifact(binding.ownerRoot, binding.completionCallID, sessionID, binding.descriptor, artifact);
|
|
1381
|
+
await removeParallelControlFiles(binding.descriptor);
|
|
1281
1382
|
parallelArtifacts.set(sessionID, { requestFingerprint: request.fingerprint, artifact });
|
|
1282
1383
|
pruneParallelChildMap(parallelArtifacts);
|
|
1283
1384
|
return JSON.stringify({ status: "created", ...(recovered === undefined ? {} : { replay: true }),
|
|
@@ -1298,9 +1399,18 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1298
1399
|
!await project.contains(contractPath))
|
|
1299
1400
|
return JSON.stringify({ status: "denied", reason: "project-boundary" });
|
|
1300
1401
|
const contract = await readJson(resolve(contractPath), INPUT_LIMITS.parallel);
|
|
1301
|
-
const
|
|
1402
|
+
const coordinator = await getParallelCoordinator();
|
|
1403
|
+
const result = await coordinator.prepare(contract, ownerRoot);
|
|
1302
1404
|
if (result.status === "serial-fallback")
|
|
1303
1405
|
return JSON.stringify(result);
|
|
1406
|
+
try {
|
|
1407
|
+
await ensureParallelReadyControls(result.snapshot);
|
|
1408
|
+
}
|
|
1409
|
+
catch (error) {
|
|
1410
|
+
await Promise.all(result.snapshot.ready.map((descriptor) => removeParallelControlFiles(descriptor).catch(() => undefined)));
|
|
1411
|
+
await coordinator.cancel(ownerRoot, result.snapshot.run_id).catch(() => undefined);
|
|
1412
|
+
throw error;
|
|
1413
|
+
}
|
|
1304
1414
|
const dispatched = result.snapshot.tasks.filter(({ phase }) => phase === "running" || phase === "completed" || phase === "failed" || phase === "abandoned").length;
|
|
1305
1415
|
const running = result.snapshot.tasks.filter(({ phase }) => phase === "running").length;
|
|
1306
1416
|
fastLane.enableParallelDispatch(ownerRoot, result.snapshot.max_workers, dispatched, running, result.snapshot.tasks.length);
|
|
@@ -1322,6 +1432,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1322
1432
|
const snapshot = reconcile === "true"
|
|
1323
1433
|
? await coordinator.reconcile(ownerRoot, coordinatorTaskCalls.get(ownerRoot) ?? new Set(), runID || undefined)
|
|
1324
1434
|
: await coordinator.snapshot(ownerRoot, runID || undefined);
|
|
1435
|
+
if (snapshot !== undefined)
|
|
1436
|
+
await ensureParallelReadyControls(snapshot);
|
|
1325
1437
|
if (snapshot === undefined && !runID) {
|
|
1326
1438
|
const archived = await coordinator.archives(ownerRoot);
|
|
1327
1439
|
return JSON.stringify({ status: "ok", active: null, archived: archived.map(boundedParallelArchive) });
|
|
@@ -1351,7 +1463,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1351
1463
|
const ownerRoot = await parallelToolOwner(sessionID);
|
|
1352
1464
|
if (ownerRoot === undefined)
|
|
1353
1465
|
return JSON.stringify({ status: "denied", reason: "coordinator-root-required" });
|
|
1354
|
-
const
|
|
1466
|
+
const coordinator = await getParallelCoordinator();
|
|
1467
|
+
const active = await coordinator.snapshot(ownerRoot, runID || undefined);
|
|
1468
|
+
const snapshot = await coordinator.cancel(ownerRoot, runID || undefined);
|
|
1469
|
+
if (active !== undefined && snapshot !== undefined) {
|
|
1470
|
+
await Promise.all(active.tasks.filter(({ phase }) => phase === "pending" || phase === "reserved")
|
|
1471
|
+
.map(({ descriptor }) => removeParallelControlFiles(descriptor).catch(() => undefined)));
|
|
1472
|
+
}
|
|
1355
1473
|
return snapshot === undefined
|
|
1356
1474
|
? JSON.stringify({ status: "absent" })
|
|
1357
1475
|
: JSON.stringify({ status: "cancelled", ...boundedParallelSnapshot(snapshot) });
|
|
@@ -2994,6 +3112,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2994
3112
|
parallelChildBindings.delete(completedChildSessionID);
|
|
2995
3113
|
}
|
|
2996
3114
|
if (snapshot !== undefined) {
|
|
3115
|
+
await ensureParallelReadyControls(snapshot);
|
|
3116
|
+
if (snapshot.cancelled) {
|
|
3117
|
+
await removeParallelControlFiles(parallel.descriptor).catch(() => undefined);
|
|
3118
|
+
}
|
|
2997
3119
|
const dispatched = snapshot.tasks.filter(({ phase }) => phase === "running" || phase === "completed" || phase === "failed" || phase === "abandoned").length;
|
|
2998
3120
|
const running = snapshot.tasks.filter(({ phase }) => phase === "running").length;
|
|
2999
3121
|
fastLane.enableParallelDispatch(parallel.ownerRoot, snapshot.max_workers, dispatched, running, snapshot.tasks.length);
|
|
@@ -3056,13 +3178,14 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3056
3178
|
const consultationFallbackAuthorized = role !== undefined &&
|
|
3057
3179
|
consultationRetries.get(consultationRetryKey(toolInput.sessionID, role))?.phase === "pending";
|
|
3058
3180
|
let parallelWorkerAuthorized = false;
|
|
3181
|
+
let parallelWorkerAlreadyBound = false;
|
|
3059
3182
|
let reservedParallelDescriptor;
|
|
3060
3183
|
if (toolInput.tool === "task" && taskRole === "dog-worker" && isRecord(output.args)) {
|
|
3061
3184
|
const prompt = typeof output.args.prompt === "string" ? output.args.prompt : "";
|
|
3062
3185
|
const contractPrompt = taskContractText(prompt);
|
|
3063
3186
|
reservedParallelDescriptor = parallelDescriptor(prompt);
|
|
3064
3187
|
if (reservedParallelDescriptor !== undefined) {
|
|
3065
|
-
const roots = taskValues(contractPrompt, ["project_root", "projectroot"]);
|
|
3188
|
+
const roots = [...new Set(taskValues(contractPrompt, ["project_root", "projectroot"]))];
|
|
3066
3189
|
if (roots.length !== 1 || !samePath(roots[0], reservedParallelDescriptor.managed_path)) {
|
|
3067
3190
|
throw new HandoffDeniedError("contract-invalid", "<worker-dispatch>", {
|
|
3068
3191
|
defects: [contractDefect("contract", "/managed_path", "parallel_descriptor_project_mismatch")],
|
|
@@ -3073,7 +3196,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3073
3196
|
const resume = modes.length === 1 && modes[0] === "same-task-resume";
|
|
3074
3197
|
const handoffPaths = taskValues(contractPrompt, ["handoff_path", "handoffpath"]);
|
|
3075
3198
|
const operationManifests = taskValues(contractPrompt, ["operation_manifest", "operationmanifest"]);
|
|
3076
|
-
const projectRoots = taskValues(contractPrompt, ["project_root", "projectroot"]);
|
|
3199
|
+
const projectRoots = [...new Set(taskValues(contractPrompt, ["project_root", "projectroot"]))];
|
|
3077
3200
|
const sourceManifests = taskValues(contractPrompt, ["source_manifest", "sourcemanifest"]);
|
|
3078
3201
|
const acceptanceValues = taskValues(contractPrompt, ["acceptance"]);
|
|
3079
3202
|
const validationValues = taskValues(contractPrompt, ["validation"]);
|
|
@@ -3193,14 +3316,21 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3193
3316
|
const snapshot = await coordinator.snapshot(toolInput.sessionID, reservedParallelDescriptor.run_id);
|
|
3194
3317
|
if (snapshot === undefined)
|
|
3195
3318
|
throw new ParallelDispatchError("descriptor-mismatch", "Parallel run is absent.");
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
fastLane.enableParallelDispatch(toolInput.sessionID, snapshot.max_workers, dispatched, running, snapshot.tasks.length);
|
|
3319
|
+
parallelWorkerAlreadyBound = snapshot.tasks.some(({ phase, call_id, descriptor }) => phase === "running" && call_id === toolInput.callID &&
|
|
3320
|
+
sameParallelDescriptor(descriptor, reservedParallelDescriptor));
|
|
3199
3321
|
await coordinator.bindDispatch(toolInput.sessionID, toolInput.callID, reservedParallelDescriptor);
|
|
3322
|
+
const boundSnapshot = await coordinator.snapshot(toolInput.sessionID, reservedParallelDescriptor.run_id);
|
|
3323
|
+
if (boundSnapshot === undefined)
|
|
3324
|
+
throw new ParallelDispatchError("descriptor-mismatch", "Parallel run is absent.");
|
|
3325
|
+
const currentContribution = parallelWorkerAlreadyBound ? 0 : 1;
|
|
3326
|
+
const dispatched = boundSnapshot.tasks.filter(({ phase }) => phase === "running" || phase === "completed" || phase === "failed" || phase === "abandoned").length;
|
|
3327
|
+
const running = boundSnapshot.tasks.filter(({ phase }) => phase === "running").length;
|
|
3328
|
+
fastLane.enableParallelDispatch(toolInput.sessionID, boundSnapshot.max_workers, dispatched - currentContribution, running - currentContribution, boundSnapshot.tasks.length);
|
|
3200
3329
|
parallelWorkerAuthorized = true;
|
|
3201
3330
|
}
|
|
3202
3331
|
const resumedWorkerSessionID = fastLane.beforeTool(toolInput.sessionID, toolInput.tool, output.args, {
|
|
3203
3332
|
consultationFallbackAuthorized,
|
|
3333
|
+
parallelWorkerAlreadyBound,
|
|
3204
3334
|
parallelWorkerAuthorized,
|
|
3205
3335
|
});
|
|
3206
3336
|
if (resumedWorkerSessionID !== undefined) {
|
|
@@ -3413,8 +3543,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3413
3543
|
if (eventSessionID !== undefined) {
|
|
3414
3544
|
const coordinator = await getParallelCoordinator().catch(() => undefined);
|
|
3415
3545
|
const owned = await coordinator?.snapshot(eventSessionID).catch(() => undefined);
|
|
3416
|
-
if (owned !== undefined)
|
|
3417
|
-
await coordinator?.cancel(eventSessionID, owned.run_id).catch(() => undefined);
|
|
3546
|
+
if (owned !== undefined) {
|
|
3547
|
+
const cancelled = await coordinator?.cancel(eventSessionID, owned.run_id).catch(() => undefined);
|
|
3548
|
+
if (cancelled !== undefined) {
|
|
3549
|
+
await Promise.all(owned.tasks.filter(({ phase }) => phase === "pending" || phase === "reserved")
|
|
3550
|
+
.map(({ descriptor }) => removeParallelControlFiles(descriptor).catch(() => undefined)));
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3418
3553
|
}
|
|
3419
3554
|
evictSession(eventSessionID);
|
|
3420
3555
|
for (const role of [REVIEWER_AGENT, ADVISOR_AGENT]) {
|