fix: sub-agent hang/crash stranding runs mid-phase with no save
Some checks failed
port-to-omp / port (push) Failing after 2s
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:
@@ -20,6 +20,8 @@ import {
|
||||
setAgentRunner,
|
||||
resetAgentRunner,
|
||||
fakeAgentRunner,
|
||||
AGENT_TIMEOUT_ENV,
|
||||
type AgentRunResult,
|
||||
} from "../src/agent-runner.js";
|
||||
import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
|
||||
import { loadRunState, runStatePath } from "../src/run-state.js";
|
||||
@@ -128,4 +130,29 @@ describe("check-runner integration", () => {
|
||||
expect(state?.checks.gated.status).toBe("skipped");
|
||||
expect(state?.checks.gated.error).toBe("no source files matched");
|
||||
});
|
||||
|
||||
it("fails the check loudly when the agent never settles", async () => {
|
||||
// Models the silent hang: the sub-agent session never resolves after
|
||||
// its last tool call (stalled provider stream / hung retry), leaving
|
||||
// the run stuck mid-phase with no state save and no completion. The
|
||||
// watchdog must abort the phase with a visible error instead.
|
||||
setAgentRunner(() => new Promise<AgentRunResult>(() => {}));
|
||||
const prev = process.env[AGENT_TIMEOUT_ENV];
|
||||
process.env[AGENT_TIMEOUT_ENV] = "50";
|
||||
try {
|
||||
await handleCheckCommand(smokeCheck(), "", stubCtx(cwd));
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env[AGENT_TIMEOUT_ENV];
|
||||
else process.env[AGENT_TIMEOUT_ENV] = prev;
|
||||
}
|
||||
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks.smoke.status).toBe("failed");
|
||||
expect(state?.checks.smoke.error).toContain("did not settle within");
|
||||
expect(state?.checks.smoke.error).toContain("/pygienium-resume");
|
||||
expect(
|
||||
state?.checks.smoke.phases.find((p) => p.id === "analysis")?.status,
|
||||
).toBe("failed");
|
||||
expect(state?.status).toBe("failed");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user