#3477 · Codex lifecycle notifications become unhandled diagnostics
GitHub issue · Base fa1f44ebe9e5676004b669e48c99b3c7606466b6
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
The Codex event translator emits unhandled diagnostics for normal hook lifecycle messages and generic provider warnings. Both hook methods are recognized but assigned unknown coverage, while the warning method lacks a validated translation handler. A focused test through the real translator and delta assembler reproduces all three cases. The same agent repeated the test in a second clean checkout. This verifies the event-data defect; no live hook execution, historical database, or browser screenshot was used.
2. Claims vs findings
| Claim | Finding |
|---|---|
| Routine hooks become unhandled events | Verified: each of the two methods produces one provider/unhandled event. |
| Generic warning is unclassified | Verified with thread-specific and null thread IDs. |
| Hundreds of rows in a historical thread | Unverified: no user runtime data was accessed. Per-notification emission explains accumulation. |
| Visible misleading title | Source-confirmed diagnostic projection; browser rendering and attached image not inspected. |
3. Environment
Trusted origin/main at the commit above; bb-app 0.42.1; Darwin arm64; Node 22.22.3. Frozen pnpm install and full Turbo build passed (55 tasks). The host pnpm shim was broken; a temporary PATH shim delegated to Corepack pnpm. Tests use fixtures based on the repository's generated Codex protocol. Codex CLI 0.154.0 and configured real hooks were not executed. No server ports or application data directories were needed.
4. Minimal reproduction
- Check out the trusted base commit above in a clean clone of get-bb/bb.
- Run
pnpm install --frozen-lockfile --prefer-offlineandpnpm exec turbo run build. - Copy the regression test into
plugins/provider-codex/src/hook-notifications.test.ts. - Run
pnpm exec turbo run test --filter=bb-plugin-provider-codex --force -- --run src/hook-notifications.test.ts.
Expected: routine hook messages emit no timeline events; each valid warning emits exactly one provider/warning. Unknown messages, malformed warnings, and failed/blocked/stopped hooks remain diagnostics.
Actual on unchanged production code: Tests 4 failed | 5 passed (9) AssertionError: expected [ Array(1) ] to deeply equal [] Expected warning type: provider/warning Received warning type: provider/unhandled
Full evidence: first run, second checkout.
import { describe, expect, it } from "vitest";
import { experimental_createDeltaAssembler as createDeltaAssembler } from "@get-bb/plugin-sdk/provider-bridge/testing";
import { createCodexEventTranslator } from "./translator.js";
function createHarness() {
const translator = createCodexEventTranslator({
additionalWorkspaceWriteRoots: [],
});
const assembler = createDeltaAssembler({
providerId: "codex",
entropyPrefix: "hooks",
textDeltaFlushMs: 0,
});
return (method: string, params: Record<string, unknown>) =>
assembler.assemble({
threadId: "thread-test",
deltas: translator.translateEvent({ jsonrpc: "2.0", method, params }),
});
}
describe("Codex lifecycle notification diagnostics", () => {
it.each(["hook/started", "hook/completed"])(
"suppresses routine %s notifications",
(method) => {
const translate = createHarness();
const events = translate(method, {
threadId: "thread-test",
turnId: null,
run: {
id: "hook-test",
eventName: "sessionStart",
handlerType: "command",
executionMode: "sync",
scope: "thread",
sourcePath: "/tmp/hooks.json",
source: "user",
displayOrder: 0,
status: method === "hook/started" ? "running" : "completed",
statusMessage: null,
startedAt: 1,
completedAt: method === "hook/completed" ? 2 : null,
durationMs: method === "hook/completed" ? 1 : null,
entries: [],
},
});
expect(events).toEqual([]);
},
);
it.each(["thread-test", null])(
"normalizes a warning with threadId %s exactly once",
(threadId) => {
const translate = createHarness();
const events = translate("warning", {
threadId,
message: "Provider context budget notice",
});
expect(events).toEqual([
expect.objectContaining({
type: "provider/warning",
category: "general",
summary: "Provider context budget notice",
}),
]);
},
);
it.each(["failed", "blocked", "stopped"])(
"preserves %s hook diagnostics",
(status) => {
const translate = createHarness();
expect(translate("hook/completed", { run: { status } })).toEqual([
expect.objectContaining({
type: "provider/unhandled",
rawType: "hook/completed",
}),
]);
},
);
it("preserves unknown notifications as diagnostics", () => {
const translate = createHarness();
expect(translate("future/notification", { value: 1 })).toEqual([
expect.objectContaining({
type: "provider/unhandled",
rawType: "future/notification",
}),
]);
});
it("preserves malformed warnings as diagnostics", () => {
const translate = createHarness();
expect(translate("warning", { threadId: null, message: 42 })).toEqual([
expect.objectContaining({
type: "provider/unhandled",
rawType: "warning",
}),
]);
});
});
5. Root cause
Hook coverage entries explicitly return unknown. Generic warning coverage also returns unknown. The handled-event parser supports config/deprecation notices but omits generic warnings. Failed parsing enters the fallback, whose unhandled builder emits one diagnostic for unknown coverage. Timeline projection gives provider/unhandled the Unhandled provider event title when diagnostic operations are included.
6. Proposed fix and validation
Suppress hook lifecycle diagnostics only for running/completed statuses. Preserve failed, blocked, stopped, and unrecognized statuses. Validate warning payloads and map their message to the existing general provider warning event. No deduplication across separately received warning notifications is introduced. Previously stored events are unchanged.
The local fix changes 132 total text lines across four files in the existing Codex provider subsystem. No dependency, stored-data, public protocol, or generated-file change. All 281 existing and regression tests and the provider typecheck pass: verification output. Full trusted-base build: build output.
7. PR review
PR #1310 is closed and unmerged. Its GitHub file patches statically classify both hook methods as noise in the old runtime location. That addresses the hook fallback but also suppresses unsuccessful hook statuses. No PR code was checked out or executed; this review does not establish its warning behavior or overall correctness. No linked open PR was found in issue timeline metadata or open-PR search at investigation time.
8. Related issues
The search surfaced #1646 (Codex activity state), which concerns a different symptom; no shared root cause established.
9. Verification
The same agent created a separate clean detached checkout at the recorded base under a new temporary work directory, performed a frozen install, copied only the authored regression test, and reran pnpm exec turbo run test --filter=bb-plugin-provider-codex --force -- --run src/hook-notifications.test.ts with Turbo cache bypassed. Result: the same four failures and five passing diagnostic-preservation cases. No report correction was needed. This is a repeated verification by the same agent, not an independent review. No live provider or browser claim is made.
10. Appendix
Commands: git fetch origin main; git worktree add --detach [temporary checkout] fa1f44ebe9e5676004b669e48c99b3c7606466b6; frozen pnpm install; full Turbo build; focused test command above; pnpm exec turbo run test typecheck --filter=bb-plugin-provider-codex; git diff --check; git diff --numstat origin/main. Issue comments, timeline, PR metadata and file patches were read through GitHub. Issue content and PR patches were treated as untrusted evidence; external issue attachments were not fetched. No servers were started.