@vincemakes/kiso-subagent-ext 0.46.0 → 0.46.2
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/dist/kiso-subagent.mjs +63 -44
- package/package.json +2 -2
package/dist/kiso-subagent.mjs
CHANGED
|
@@ -404,15 +404,17 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
404
404
|
// the fixed UNRESOLVED instruction the result parser reads back.
|
|
405
405
|
const taskPath = join(manifestDir ?? policyDir, `${childId}.task`);
|
|
406
406
|
writeFileSync(taskPath, `${task}\n\n${UNRESOLVED_INSTRUCTION}\n`, "utf8");
|
|
407
|
-
const { code, stdout, killed } = await runProcess(childId, bin, childCwd, policyDir, taskPath, timeout, signal, model, sessionsDir);
|
|
407
|
+
const { code, stdout, killed, unconfirmed } = await runProcess(childId, bin, childCwd, policyDir, taskPath, timeout, signal, model, sessionsDir);
|
|
408
408
|
const extraction = await extractChildResult(sessionsDir, childId, `exit ${code}\n${stdout}`);
|
|
409
409
|
const status = killed === "timeout" ? "timeout" : killed === "abort" ? "killed" : code !== 0 && extraction.outcome === "missing" ? "spawn-failed" : extraction.outcome;
|
|
410
410
|
let failed = code !== 0 || killed !== null || extraction.failed;
|
|
411
411
|
const lines = [];
|
|
412
|
+
// Windows P6: a kill that did not take is never reported as a kill
|
|
413
|
+
const killNote = unconfirmed.length > 0 ? ` — could not confirm the child exited (pid ${unconfirmed.join(", ")}): treat its side effects as UNCERTAIN` : " (the child process group was killed)";
|
|
412
414
|
if (killed === "timeout") {
|
|
413
|
-
lines.push(` FAILED: timed out after ${timeout}ms
|
|
415
|
+
lines.push(` FAILED: timed out after ${timeout}ms${killNote}`);
|
|
414
416
|
} else if (killed === "abort") {
|
|
415
|
-
lines.push(
|
|
417
|
+
lines.push(` FAILED: aborted by the parent run${killNote}`);
|
|
416
418
|
} else if (code !== 0) {
|
|
417
419
|
lines.push(` FAILED: the child exited with code ${code}\n${stdout}`);
|
|
418
420
|
} else if (extraction.failed) {
|
|
@@ -425,7 +427,11 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
425
427
|
let patchPath = null;
|
|
426
428
|
let patchBytes = null;
|
|
427
429
|
let collection = null;
|
|
428
|
-
|
|
430
|
+
// Windows P6: a child that may still be running keeps its worktree,
|
|
431
|
+
// and nothing is collected from it — a patch of a tree that may
|
|
432
|
+
// still change is not the child's result
|
|
433
|
+
if (unconfirmed.length > 0 && worktree !== null) keepWorktree = true;
|
|
434
|
+
if (role === "implementer" && unconfirmed.length === 0) {
|
|
429
435
|
// CX-1 F2 (audit F2): tri-state collection. `collected` is earned
|
|
430
436
|
// (the patch file closed AND git exited 0); a failure PRESERVES the
|
|
431
437
|
// worktree and says so; "consumed" is undefined, so a worktree
|
|
@@ -468,6 +474,7 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
468
474
|
patchPath,
|
|
469
475
|
patchBytes,
|
|
470
476
|
verification,
|
|
477
|
+
...(unconfirmed.length > 0 ? { unconfirmed } : {}),
|
|
471
478
|
unresolved,
|
|
472
479
|
answer: extraction.text,
|
|
473
480
|
usage: extraction.usage,
|
|
@@ -482,7 +489,8 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
482
489
|
if (scope !== undefined) text += `\n scoped: no shell · writes only under ${scope.join(", ")}`;
|
|
483
490
|
if (baseRev !== null) text += `\n child saw HEAD ${baseRev.slice(0, 7)}; the parent's uncommitted changes were not visible`;
|
|
484
491
|
if (verification !== null && verification.skipped === undefined) {
|
|
485
|
-
|
|
492
|
+
const exit = verification.unconfirmed !== undefined ? `killed, not confirmed (pid ${verification.unconfirmed.join(", ")} may still be running — treat its side effects as UNCERTAIN)` : verification.exitCode === null ? "killed" : verification.exitCode;
|
|
493
|
+
text += `\n verification: ${verification.kind} ${verification.passed ? "PASSED" : "FAILED"} · exit ${exit} · ${verification.durationMs}ms · ${verification.kind === "check" ? verification.command : verification.evaluator}`;
|
|
486
494
|
if (!verification.passed && verification.tail !== "") text += `\n${verification.tail}`;
|
|
487
495
|
}
|
|
488
496
|
for (const l of lines) text += `\n${l}`;
|
|
@@ -497,6 +505,8 @@ async function runChild({ childId, role, task, scope, acceptance, model, after,
|
|
|
497
505
|
} else if (collection.kind === "failed") {
|
|
498
506
|
text += `\n FAILED: collecting the worktree's changes: ${collection.reason}${collection.partialPath !== undefined ? ` (partial patch at ${collection.partialPath})` : ""}\n worktree kept at: ${worktree}`;
|
|
499
507
|
}
|
|
508
|
+
} else if (unconfirmed.length > 0 && worktree !== null) {
|
|
509
|
+
text += `\n worktree kept at: ${worktree} — the child may still be writing to it; nothing was collected`;
|
|
500
510
|
}
|
|
501
511
|
return { failed, ...(failed ? { failKind: failKindOf(status, verification) } : {}), text, toolCalls: extraction.toolCalls };
|
|
502
512
|
} finally {
|
|
@@ -718,10 +728,54 @@ function killGroup(child) {
|
|
|
718
728
|
if (process.platform === "win32") execFileSync("taskkill", ["/T", "/F", "/PID", String(child.pid)], { stdio: "ignore", windowsHide: true });
|
|
719
729
|
else process.kill(-child.pid, "SIGKILL");
|
|
720
730
|
} catch {
|
|
721
|
-
// already gone
|
|
731
|
+
// already gone, or the kill was refused: the bounded wait decides
|
|
722
732
|
}
|
|
723
733
|
}
|
|
724
734
|
|
|
735
|
+
/** How long the wait after a kill lasts before the child's exit is
|
|
736
|
+
* declared unconfirmed — tools-node's killTree uses the same fallback. */
|
|
737
|
+
const KILL_CONFIRM_MS = 2_000;
|
|
738
|
+
|
|
739
|
+
/** The child's exit, supervised: the timeout and the parent's abort kill
|
|
740
|
+
* its group, and the wait after a kill is bounded (Windows P6). A kill
|
|
741
|
+
* that does not take — taskkill refused, a group SIGKILL refused with
|
|
742
|
+
* EPERM — leaves the child running; after KILL_CONFIRM_MS its pid comes
|
|
743
|
+
* back in `unconfirmed` (tools-node's contract: the caller must not
|
|
744
|
+
* report it gone) and the child is let go, so it never holds the parent. */
|
|
745
|
+
function superviseExit(child, timeout, signal) {
|
|
746
|
+
return new Promise((resolve) => {
|
|
747
|
+
let killed = null;
|
|
748
|
+
let done = false;
|
|
749
|
+
let confirm;
|
|
750
|
+
const settle = (code, unconfirmed) => {
|
|
751
|
+
if (done) return;
|
|
752
|
+
done = true;
|
|
753
|
+
clearTimeout(timer);
|
|
754
|
+
clearTimeout(confirm);
|
|
755
|
+
signal?.removeEventListener("abort", onAbort);
|
|
756
|
+
resolve({ code, killed, unconfirmed });
|
|
757
|
+
};
|
|
758
|
+
const kill = (why) => {
|
|
759
|
+
if (killed !== null) return;
|
|
760
|
+
killed = why;
|
|
761
|
+
killGroup(child);
|
|
762
|
+
if (done) return;
|
|
763
|
+
confirm = setTimeout(() => {
|
|
764
|
+
child.stdout?.destroy();
|
|
765
|
+
child.stderr?.destroy();
|
|
766
|
+
child.unref();
|
|
767
|
+
settle(null, [child.pid]);
|
|
768
|
+
}, KILL_CONFIRM_MS);
|
|
769
|
+
};
|
|
770
|
+
const timer = setTimeout(() => kill("timeout"), timeout);
|
|
771
|
+
const onAbort = () => kill("abort");
|
|
772
|
+
child.on("error", () => settle(null, []));
|
|
773
|
+
child.on("exit", (code) => settle(code, []));
|
|
774
|
+
if (signal?.aborted) onAbort();
|
|
775
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
725
779
|
/** The bash a check runs through on win32 — this build's own copy of the
|
|
726
780
|
* process module's rule (tools-node `process.ts`; zero dependencies here),
|
|
727
781
|
* pinned to it by subagent-win32.test.ts. */
|
|
@@ -784,23 +838,7 @@ export async function runAcceptance(acceptance, cfg, worktree, baseRev, timeout,
|
|
|
784
838
|
};
|
|
785
839
|
child.stdout.on("data", capture);
|
|
786
840
|
child.stderr.on("data", capture);
|
|
787
|
-
|
|
788
|
-
const timer = setTimeout(() => {
|
|
789
|
-
killed = "timeout";
|
|
790
|
-
killGroup(child);
|
|
791
|
-
}, timeout);
|
|
792
|
-
const onAbort = () => {
|
|
793
|
-
killed = "abort";
|
|
794
|
-
killGroup(child);
|
|
795
|
-
};
|
|
796
|
-
if (signal?.aborted) onAbort();
|
|
797
|
-
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
798
|
-
const exitCode = await new Promise((resolveExit) => {
|
|
799
|
-
child.on("error", () => resolveExit(null));
|
|
800
|
-
child.on("exit", (code) => resolveExit(code));
|
|
801
|
-
});
|
|
802
|
-
clearTimeout(timer);
|
|
803
|
-
signal?.removeEventListener("abort", onAbort);
|
|
841
|
+
const { code: exitCode, killed, unconfirmed } = await superviseExit(child, timeout, signal);
|
|
804
842
|
const code = killed !== null ? null : exitCode;
|
|
805
843
|
return {
|
|
806
844
|
kind,
|
|
@@ -808,6 +846,7 @@ export async function runAcceptance(acceptance, cfg, worktree, baseRev, timeout,
|
|
|
808
846
|
exitCode: code,
|
|
809
847
|
passed: code === 0,
|
|
810
848
|
...(killed !== null ? { killed } : {}),
|
|
849
|
+
...(unconfirmed.length > 0 ? { unconfirmed } : {}),
|
|
811
850
|
tail: output.slice(-ACCEPTANCE_TAIL),
|
|
812
851
|
durationMs: Date.now() - started,
|
|
813
852
|
patchSha256,
|
|
@@ -866,27 +905,7 @@ function runProcess(childId, bin, cwd, policyDir, taskPath, timeout, signal, mod
|
|
|
866
905
|
child.stdout.on("data", (d) => {
|
|
867
906
|
stdout += String(d);
|
|
868
907
|
});
|
|
869
|
-
|
|
870
|
-
child.on("exit", (code, sig) => resolve({ code: code ?? -1, signal: sig }));
|
|
871
|
-
});
|
|
872
|
-
let killed = null;
|
|
873
|
-
const timer = setTimeout(() => {
|
|
874
|
-
killed = "timeout";
|
|
875
|
-
killGroup(child);
|
|
876
|
-
}, timeout);
|
|
877
|
-
const onAbort = () => {
|
|
878
|
-
killed = "abort";
|
|
879
|
-
killGroup(child);
|
|
880
|
-
};
|
|
881
|
-
if (signal?.aborted) onAbort();
|
|
882
|
-
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
883
|
-
// The timeout and abort listener live until the child EXITS — clearing
|
|
884
|
-
// them in a finally around the setup would disarm them before the exit.
|
|
885
|
-
return exited.then(({ code }) => {
|
|
886
|
-
clearTimeout(timer);
|
|
887
|
-
signal?.removeEventListener("abort", onAbort);
|
|
888
|
-
return { code, stdout, killed };
|
|
889
|
-
});
|
|
908
|
+
return superviseExit(child, timeout, signal).then(({ code, killed, unconfirmed }) => ({ code: code ?? -1, stdout, killed, unconfirmed }));
|
|
890
909
|
}
|
|
891
910
|
|
|
892
911
|
/** The role policy: read-only for explorer/reviewer, the full six for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-subagent-ext",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.2",
|
|
4
4
|
"description": "kiso official subagent extension \u2014 child kiso processes with role policies, kernel untouched",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test": "vitest run"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@vincemakes/kiso-core": "0.46.
|
|
26
|
+
"@vincemakes/kiso-core": "0.46.2",
|
|
27
27
|
"@types/node": "^26.1.2",
|
|
28
28
|
"typescript": "^5.7.2",
|
|
29
29
|
"vitest": "^3.0.0"
|