fix: sub-agent hang/crash stranding runs mid-phase with no save
Some checks failed
port-to-omp / port (push) Failing after 2s

A stalled sub-agent (provider stream never settling after the final tool
call, or a throwing session-event listener recursing through the SDK's
run-failure path to stack overflow) left the phase stuck in_progress with
no state save, no error, and no completion message — observed twice in
freno-dev, both times after comments wrote findings.md.

- agent-runner: 60-min settle watchdog on every agent phase
  (PYGIENIUM_AGENT_TIMEOUT_MS, env-tunable); timeout disposes the session
  and fails the phase loudly with a /pygienium-resume hint instead of
  hanging the check-runner's await forever.
- agent-runner: applySessionEvent — the session.subscribe listener can no
  longer throw into the SDK event pipeline (guards for partial/malformed
  events, optional-chained message_update, throwing chat forwarder).
- comments: scan no longer regenerates the full report as its final
  message (findings.md is the source of truth); fix phase reads
  findings.md with embedded fallback.
- check-runner: failing state-save inside the catch path can't double-
  fault or escape as an unhandled rejection; completion posting is
  best-effort.
- tests: watchdog timeout test, comments task-text regression tests,
  applySessionEvent malformed-shape unit tests. 146 pass, tsc clean.
- README: document PYGIENIUM_AGENT_TIMEOUT_MS.
This commit is contained in:
2026-08-11 08:47:12 -04:00
parent 6d23b04ef6
commit 8768f9de97
7 changed files with 435 additions and 48 deletions

View File

@@ -142,7 +142,12 @@ export async function runCheck(
): Promise<CheckRunOutcome> {
const startMs = Date.now();
const outcome = await runCheckImpl(opts);
postCheckCompletion(opts, outcome, Date.now() - startMs);
try {
postCheckCompletion(opts, outcome, Date.now() - startMs);
} catch {
// Completion posting is best-effort chat UI: a renderer or send
// failure must never reject the run after its state was persisted.
}
return outcome;
}
@@ -383,7 +388,16 @@ async function runCheckImplInner(
error = err instanceof Error ? err.message : String(err);
markCheckStatus(state, check.name, "failed", error);
markRunStatus(state, reconcileRunStatus(state));
await saveRunState(state);
try {
await saveRunState(state);
} catch (saveErr) {
// A failing state save inside the error path must not mask the
// original error or escape as an unhandled rejection (which would
// kill the run with nothing persisted or reported).
error += `; (also failed to persist run-state: ${
saveErr instanceof Error ? saveErr.message : String(saveErr)
})`;
}
return { status: "failed", error, findings, changes, state };
} finally {
strip.done();