Skip to content

Codex reports program status without probing, so its requests never alert #481

Closed

Description

@joelmoss

This was generated by AI during triage.

Problem

Codex reports OSC 7501 program status without first sending the support probe (ESC ] 7501 ; ?), so Workroom shows its marks but never alerts for it. A waiting Codex terminal gets the ✋ mark, but no "needs you" notification, and it isn't counted in the Dock badge or the menu bar.

This is not a Codex bug. The spec (revision 0.4) makes the probe optional:

Detection is optional. Terminals MUST ignore unknown OSCs, so a program MAY send reports without querying first, for example when it cannot read replies.

It doesn't tie notifications to the probe either: "A terminal MAY produce a notification when a record changes", and "Terminals SHOULD rate-limit anything a record causes outside the terminal, such as desktop notifications or sounds." So gating alerts on the probe is Workroom's own rule, which exists so that a cated recording can't raise alerts (docs/program-status.md, "Only a program that asked … can alert you").

Reproduced 2026-10-16: Codex 0.163.0 run as a session's command under a scratch wr-agent serve sent state=idle and then state=working with no probe, and wr-agent program-status showed app="codex" … notify=false. notify follows the process group, not the state, so a blocked record from Codex is notify=false too. A real permission prompt was not driven (it needs an API turn).

No report or request about this exists on openai/codex (searched 2026-10-16).

Direction

Fix it in Workroom: let a program that didn't probe alert once it has shown that it is a running program and not a replay, by its process group still being alive a short time after it reported blocked. The brief is in the comments.

Context: #358 (program status), ADR 0050, #361 (closed; hooks rejected, .out-of-scope/agent-lifecycle-hooks.md).


🤖 Left by Claude Code.

Activity

  1. joelmoss commented on Oct 16, 2026

    @joelmoss
    OwnerAuthor

    This was generated by AI during triage.

    Superseded by the revised brief: #481 (comment)


    🤖 Left by Claude Code.

  2. joelmoss commented on Oct 16, 2026

    @joelmoss
    OwnerAuthor

    This was generated by AI during triage.

    Agent Brief

    Category: enhancement
    Summary: Let a program that doesn't probe (Codex) raise "needs you" alerts once it has shown itself live, and make every waiting request produce exactly one notification.

    This supersedes the earlier brief on this issue. The decisions below were settled with the maintainer in a grilling session on 2026-10-16.

    Current behavior:

    • A program-status record's notify is true only when the agent answered a support probe from the record's owner group (the pty's foreground group when the report arrived). Codex never probes; the spec allows that ("a program MAY send reports without querying first"). So every Codex record has notify=false, it never alerts, and it never counts as waiting.
    • ProgramStatusAlerts.apply treats a blocked record with notify=false as already notified the first time it sees the episode, so a later change can't alert either.
    • The agent sweeps dead owners (Store::drop_dead_owners, group_alive) only on a pty read (Shadow::write_from_pty), never on a timer.
    • ProgramStatusAlerts.absorbsOSC folds a program's own desktop notification (any OSC notification) into the "needs you" alert only if that alert has already been raised, the session has exactly one blocked record, and the notification arrives within mergeWindow (10 s) of the request starting. Codex writes its own OSC 9 just before its OSC 7501 blocked report (in its redraw, maybe_post_pending_notification runs before the draw that refreshes program status), and the two reach the app by different routes (surface action vs the agent's program-status service). So a Codex request shows two notifications: Codex's, then Workroom's.

    Desired behavior:

    Alerts for a program that didn't probe

    • A blocked record whose owner group didn't probe becomes notify=true when, at the agent's existing screens tick (every 2 s), at least 1 s has passed since the report, the owner group is still alive, and it is still the pty's foreground group. The alert therefore lands 1–3 s after the request starts. The agent publishes the change as a normal record update.
    • A report owned by the session's shell group (the pty's child process, which leads its own group) never becomes notify=true without a probe.
    • A record whose group dies, stops being foreground, or is dropped by a prompt before the check never alerts. That is the cat case.
    • Programs that probe are unchanged: notify=true at once.
    • In the app, an open episode whose record goes from notify=false to notify=true alerts then, under the existing rules (once per episode, the per-tab cooldown, no alert while an attachment of the session has focus), and starts counting as waiting.

    One notification per request

    • Notification first. While Workroom's alerts are enabled, a program's own OSC notification from a session that is reporting program status is held for about 500 ms instead of being posted at once. If, during the hold, the session has (or gains) a blocked request that Workroom would alert for, the "needs you" alert is raised immediately, skipping the 1 s grace period (the notification is evidence of a live program), and the notification's text is folded into it. If not, the held notification is posted as an ordinary notification. Sessions not reporting program status are never held.
    • Notification later. A program's own notification is folded into the session's open request at any time while that request is open, not only within 10 s.
    • Several requests. With more than one blocked request in the session, the notification folds into the newest open one.
    • Backstop. When a "needs you" alert is raised, any ordinary notification Workroom posted for the same session in the previous 5 s is removed and its text folded into the alert.
    • With Workroom's alerts turned off, nothing is held and programs' own notifications post as they do today. When Workroom won't alert because an attachment of the session has focus, a held notification is posted as it would be today.

    Records

    • A new ADR (0052), "Alerts for programs that don't probe, and one notification per request", records these rules and names what it supersedes in ADR 0050 (the probe-only alert rule and the 10 s fold window). docs/program-status.md is updated in the same PR. That includes quoting the spec's "MAY send reports without querying first", and adding the tail -f limit below to Known limits.

    Key interfaces:

    • The program-status record's notify flag and how it is set (Store::apply_read / report, the probed group); unprobed blocked records need a "reported at" (or equivalent) for the grace check.
    • The agent's screens tick, group_alive, the pty's foreground group (foreground_pgid) and the session's child pid.
    • The program-status service's live events: a notify change is a record update the app must act on.
    • ProgramStatusAlerts: apply (the !record.notify branch), absorbsOSC (hold, fold-any-time, newest-request), and withdrawing a posted ordinary notification for the backstop.
    • ProgramStatusLimits: the hold (~500 ms) and backstop lookback (5 s) belong beside mergeWindow.

    Acceptance criteria:

    • Agent test: an unprobed blocked report whose owner group stays alive and foreground ends up notify=true at the first tick ≥1 s after the report; one whose group dies first, stops being foreground, or is followed by a prompt stays notify=false.
    • Agent test: an unprobed blocked report owned by the shell's own group never becomes notify=true.
    • Agent test: a probed report still gets notify=true immediately.
    • App test: a blocked record arriving notify=false then updated to notify=true in the same episode raises exactly one alert and enters the waiting count. With the session focused, it raises none.
    • App test: a program notification arriving before the blocked record (within the hold) yields exactly one notification, the "needs you" alert carrying the program's text, raised without waiting for the grace period.
    • App test: a program notification arriving after the alert, beyond 10 s but while the request is open, is folded (no second notification).
    • App test: with two blocked requests open, a program notification folds into the newest.
    • App test: an ordinary notification posted for a session within 5 s before its alert is raised is withdrawn and its text folded.
    • App test: a program notification from a session with no program status is posted immediately (not held); with alerts disabled, nothing is held.
    • cating a file holding a blocked report, in a terminal you aren't looking at, still raises no "needs you" alert.
    • ADR 0052 written and docs/program-status.md updated as above.

    Out of scope:

    • Asking Codex upstream to probe, or editing anyone's Codex or Claude Code config (for example tui.notifications).
    • Alerting for done or error.
    • Per-app allow lists (trusting app=codex by name).
    • The existing shortcut: in apply_read (probe and report landing in different reads of one replay).
    • BEL handling: Codex uses OSC 9 in Workroom terminals (TERM_PROGRAM=ghostty).

    Accepted limit: a long-running foreground program that prints someone else's blocked report (tail -f or less -R on a raw recording) can raise one alert. The rule cannot tell a replay from a relay (ssh, mosh, tmux), and relays are the case it must serve.

    Left to the maintainer at review: a manual check with a real Codex permission prompt in the running app, with the terminal unfocused. Expect exactly one "needs you" notification carrying Codex's text, the Dock count, and ⇧⌘M reaching the terminal. Agents must not run UI tests unasked.


    🤖 Left by Claude Code.

  3. joelmoss commented on Oct 16, 2026

    @joelmoss
    OwnerAuthor

    This was generated by AI during triage.

    Agent Brief: addendum (eng review, 2026-10-16)

    An engineering review of the brief above, with a second opinion from Codex, found the gaps below. Each amends the brief and is part of the contract. Where this addendum and the brief differ, this addendum wins.

    R1. The app must notice a later notify flip.
    ProgramStatusAlerts.apply only visits episodes it hasn't opened yet, so a record whose notify goes from false to true on an already-open episode is never looked at. apply must re-examine open episodes whose record changed to notify=true, and alert under the existing rules.

    R2. Promotion sticks for the whole episode.
    Store::report keeps a blocked record's episode on a repeated report, but recomputes notify from the probe alone. A promoted record would drop back to notify=false. Codex re-sends its current state after returning from an external program, so this happens in practice. The grace clock starts when the episode starts, not on each report. A promoted record stays promoted while the same owner re-reports the same blocked episode.

    R3. "The shell's group" means the pty's child only in an interactive-shell session.
    The app always starts sessions with an empty command, so in production the child is always the shell. A session with a command runs the program itself as the child (sh -c "exec …"), which test harnesses do. The exclusion applies only when the session has no command.

    R4. The alert carries the program's own text when the record has none.
    Codex's OSC 7501 reports carry state, app and kind only, with no msg. The detail ("Approval requested: ") is only in Codex's own OSC 9 notification. NotificationCenterStore.merge only fills an empty body, and a "needs you" body is never empty. So on the hold path, build the alert before posting it, using the program's notification text as its quoted detail when the record has no message. A notification folded in after the alert keeps today's merge rule.

    R5. A raised episode counts as waiting straight away.
    The waiting count uses record.notify && enabled. An alert raised on the hold path still has notify=false until the agent promotes it, so it would be missing from the Dock count. An episode counts while it is raised or notify=true.

    R6. Accepted limit: a replayed recording that holds an OSC 9.
    On the hold path, cating a raw recording that contains both a program notification and a blocked report, in an unfocused pane, raises a brief "needs you" alert. It is withdrawn when the shell's next prompt drops the record. The same replay already posts an ordinary notification today. Won't fix. Narrow the brief's cat acceptance criterion to a file with no program notification, and add this case to Known limits.

    R7. State the one-notification guarantee as it really is.
    On a lagging remote link, a banner and sound can already have been delivered before the backstop withdraws the duplicate. A program that sends its own notification before its first OSC 7501 report is not held. Codex and Claude Code both report idle at startup, so neither is affected. ADR 0052 and docs/program-status.md say: one notification in normal operation; on a lagging link a duplicate may show briefly and is then removed.

    Additional acceptance criteria:

    • App test (R1): an open episode whose record flips from notify=false to notify=true alerts exactly once.
    • Agent test (R2): an unprobed blocked record re-reported before promotion keeps its original grace start; one re-reported after promotion stays notify=true.
    • Agent test (R3): in a session with a command, a report owned by the child can be promoted; in a session without one, it can't.
    • App test (R4): a program notification held before a record with no message produces one alert whose detail is the notification's text.
    • App test (R5): an alert raised on the hold path counts as waiting before the agent promotes the record.
    • Docs (R6, R7): Known limits and ADR 0052 carry both limits.

    Build order: two independent lanes (the agent's promotion, and the app's alerts), then the ADR and docs. One PR.


    🤖 Left by Claude Code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions