fix: surface hidden-pane steer failures and demote per-turn gateway log noise (#124560)

* fix(ui): surface hidden-pane steer terminal failures globally

Three terminal branches in steer-lifecycle.ts (transport null result,
failed queue-row restore, failed queue-row removal) still gated their
error on itemStillVisible, so a steer that failed after the operator
navigated away parked the error on the queue row with no visible
outcome — the exact invariant #124473 introduced
surfaceChatDeliveryFailure() to protect.

Route all three through the canonical helper and delete the divergent
visibility-only branches. Regression test fails pre-fix
(stash-verified): steer transport failure with the pane hidden now
surfaces the session-named global toast.

* fix(logging): demote per-turn gateway log noise to debug

Live campaign evidence showed three lines dominating operator logs at
info level with no per-turn diagnostic value:

- 'tool policy removed N tool(s)': the policy pipeline runs on every
  turn, so this repeated 42x in one session. Demote to debug and delete
  the now-dead toolPolicyAuditLogLevel/auditLogLevel plumbing that only
  existed to lower diagnostic probes to the level that is now the
  default (net -13 production LOC).
- 'codex app-server one-shot cleanup checked shared client retirement':
  routine per-attempt teardown detail; demote to debug.
- 'codex trajectory capture requires the SQLite host recorder': static
  config condition warned per attempt; warn once per process.

Skipped: the [model-fetch] info carve-out in model-transport-debug.ts is
a named contract (docs/logging.md, #89648) — always-info by design.

* fix(codex): drop test-only trajectory warn-once reset export

Knip's production unused-export gate rejects
resetCodexTrajectoryRecorderWarningForTest — it was a test-only seam in
production code. Reset the process-wide warn-once flag via
vi.resetModules() + fresh dynamic import in the test instead.

* test(cron): wait for backoff re-arm instead of fixed sleep

The 0ms retry timer arms only after async watcher-state persistence, so
'await delay(5)' races it on loaded CI workers (flaked on
checks-node-compact-large-2: spawn called 1 time, expected 2). Replace
both fixed-sleep re-arm waits with vi.waitFor on the spawn count. The
remaining delay(5) guards a negative no-further-spawn assertion after
cancel, where a bounded sleep is the correct shape.

* fix(codex): scope trajectory recorder warn dedupe to session

ClawSweeper P2: the host recorder factory returns null for per-session
target-mapping conflicts, not only static config, so a process-wide
warn-once flag silenced a later distinct session's recorder loss. Warn
once per session (bounded set, cleared past 64 entries) so retries stay
quiet but each newly affected session records its loss. Regression
covers a later distinct session still warning.
This commit is contained in:
Peter Steinberger
2026-08-16 06:23:44 -07:00
committed by GitHub
parent 243f51d314
commit 7173aeb663
17 changed files with 108 additions and 100 deletions
@@ -137,7 +137,9 @@ export function prepareCodexAttemptResources(prompt: CodexAttemptPrompt) {
}
state.sharedCodexClientRetiredForOneShotCleanup = true;
const retired = clearSharedCodexAppServerClientIfCurrentAndUnclaimed(state.client);
embeddedAgentLog.info("codex app-server one-shot cleanup checked shared client retirement", {
// Runs on every one-shot attempt teardown; routine retirement checks are
// diagnostic detail, not operator-facing info.
embeddedAgentLog.debug("codex app-server one-shot cleanup checked shared client retirement", {
runId: params.runId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
@@ -117,24 +117,41 @@ function createSqliteHostTrajectoryRecorder(params: {
}
describe("Codex trajectory recorder", () => {
it("warns when the SQLite host recorder is unavailable", () => {
it("warns once per session when the SQLite host recorder is unavailable", async () => {
// Import a fresh module instance so the process-wide dedupe set starts
// clean without a test-only reset export in production code.
vi.resetModules();
const { createCodexTrajectoryRecorder: createFreshRecorder } = await import("./trajectory.js");
const warn = vi.fn();
const recorder = createCodexTrajectoryRecorder({
cwd: testWorkspace.dir,
attempt: {
sessionFile: "agent:main:session-1",
sessionId: "session-1",
model: { api: "responses" },
} as never,
env: {},
warn,
});
const makeRecorder = (sessionId: string) =>
createFreshRecorder({
cwd: testWorkspace.dir,
attempt: {
sessionFile: `agent:main:${sessionId}`,
sessionId,
model: { api: "responses" },
} as never,
env: {},
warn,
});
expect(recorder).toBeNull();
expect(makeRecorder("session-1")).toBeNull();
// Retried attempts for the same session must not repeat the warn.
expect(makeRecorder("session-1")).toBeNull();
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenCalledWith(
"codex trajectory capture requires the SQLite host recorder",
{ sessionId: "session-1", reason: "sqlite-recorder-unavailable" },
);
// A later distinct session's recorder loss stays visible: the host can
// reject per-session (target-mapping conflicts), not only per-process.
expect(makeRecorder("session-2")).toBeNull();
expect(warn).toHaveBeenCalledTimes(2);
expect(warn).toHaveBeenLastCalledWith(
"codex trajectory capture requires the SQLite host recorder",
{ sessionId: "session-2", reason: "sqlite-recorder-unavailable" },
);
});
it("stores SQLite-backed captures for the canonical session-key target", async () => {
+19 -4
View File
@@ -121,6 +121,13 @@ function createCodexHostTrajectorySink(params: {
};
}
// The host recorder can be absent per session (target-mapping conflicts), not
// only per process, so dedupe the warn by session: repeated attempts for one
// session stay quiet while a later distinct session still records its loss.
// Bounded: cleared past the cap so a pathological session churn cannot grow it.
const warnedRecorderUnavailableSessions = new Set<string>();
const WARNED_RECORDER_SESSIONS_CAP = 64;
/** Creates a trajectory recorder when trajectory capture is enabled for the environment. */
export function createCodexTrajectoryRecorder(
params: CodexTrajectoryInit,
@@ -136,10 +143,18 @@ export function createCodexTrajectoryRecorder(
// from a session-file string silently drops every capture once the host
// stops emitting the legacy `sqlite:` marker.
if (!params.trajectoryRecorder) {
params.warn?.("codex trajectory capture requires the SQLite host recorder", {
sessionId: params.attempt.sessionId,
reason: "sqlite-recorder-unavailable",
});
// Per-attempt repeats for one session bury real diagnostics; warn once per
// session so retries stay quiet but each newly affected session is visible.
if (!warnedRecorderUnavailableSessions.has(params.attempt.sessionId)) {
if (warnedRecorderUnavailableSessions.size >= WARNED_RECORDER_SESSIONS_CAP) {
warnedRecorderUnavailableSessions.clear();
}
warnedRecorderUnavailableSessions.add(params.attempt.sessionId);
params.warn?.("codex trajectory capture requires the SQLite host recorder", {
sessionId: params.attempt.sessionId,
reason: "sqlite-recorder-unavailable",
});
}
return null;
}
const sink = createCodexHostTrajectorySink({ recorder: params.trajectoryRecorder });