@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.
@@ -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 (the child process group was killed)`);
415
+ lines.push(` FAILED: timed out after ${timeout}ms${killNote}`);
414
416
  } else if (killed === "abort") {
415
- lines.push(" FAILED: aborted by the parent run (the child process group was killed)");
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
- if (role === "implementer") {
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
- text += `\n verification: ${verification.kind} ${verification.passed ? "PASSED" : "FAILED"} · exit ${verification.exitCode === null ? "killed" : verification.exitCode} · ${verification.durationMs}ms · ${verification.kind === "check" ? verification.command : verification.evaluator}`;
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
- let killed = null;
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
- const exited = new Promise((resolve) => {
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.0",
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.0",
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"