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

@@ -30,6 +30,8 @@ import {
commentsCheck,
findingsPath,
changesPath,
buildCommentsScanTask,
buildCommentsFixTask,
} from "../src/checks/comments.js";
import type { CheckScope } from "../src/checks/registry.js";
@@ -256,4 +258,31 @@ describe("comments check (end-to-end)", () => {
expect(check!.findings).toBeDefined();
expect(check!.changes).toBeDefined();
});
it("scan task keeps the full report in findings.md, not the final message", () => {
// Regression: the scan task used to demand the agent regenerate the
// whole report as its final message right after writing findings.md —
// a second huge output that stalled the phase transition (observed in
// freno-dev twice). The final message must stay a one-line summary.
const scope: CheckScope = {
cwd,
target,
fix: false,
rest: [],
};
const task = buildCommentsScanTask(cwd, scope);
expect(task).toContain("ONE-LINE summary");
expect(task).toContain("do NOT regenerate the report text");
expect(task).not.toContain(
"Return the findings report text as your final message",
);
expect(task).not.toContain("same content as the file");
// The fix phase must read the detailed findings from the artifact so a
// one-line scan summary can't starve it.
const fixTask = buildCommentsFixTask(cwd, scope, "fallback-findings-text");
expect(fixTask).toContain("Read the detailed per-file findings");
expect(fixTask).toContain(findingsPath(scope));
expect(fixTask).toContain("fallback-findings-text");
});
});