Skip to content

0085: Gate session-name display before later tools

Status: accepted (2026-08-20); lifecycle re-mint scope narrowed by ADR 0086 · Scope: product tier (core/agents, core/hooks)

The first non-empty agents set-task mints a copyable session name. The command previously returned prose asking the agent to put that name at the top of its reply. That timing is ambiguous in clients that separate commentary, tool calls, and the final answer.

Presence detection also accepted the name as a substring anywhere in assistant text. A session could complete all of its work, print the block in its final answer, and permanently satisfy the ritual. If the name was never printed, the prompt nudge stopped once a suggestion existed but session_name_seen_for was still unset.

Stop enforcement cannot provide one portable enforcement point. Claude Code can continue the same turn, Cursor does not expose reply text in its Stop payload, and ADR 0051 keeps Codex Stop checks observe-only because a continuation can replace the completed answer.

Keep the prompt-only reminder. Rejected because an ignored tool-result note allows the entire task to run before the next reminder.

Enforce the block at Stop on every adapter. Rejected because the adapters do not share a safe Stop contract, and Stop is too late to guarantee early display.

Add a separate pending-name file. Rejected because the coordination row already carries both the current suggestion and the suggestion last observed. A second durable latch could disagree with that projection.

Rename client tabs directly. Rejected as the portable contract. The three clients do not expose one common title mutation hook, while each can show a copyable fenced block.

Treat the existing fields as a three-state display protocol:

unminted = !suggested_session_name
pending = suggested_session_name !== session_name_seen_for
shown = suggested_session_name === session_name_seen_for

When PostToolUse observes a pending name, it injects an adapter-shaped instruction that requires the exact fenced block as the next assistant text, before prose or another tool call.

Every later PreToolUse checks the latch before other tool guards. Claude Code and Codex read the first assistant message after the mint result from their JSONL transcripts. Cursor stamps completed assistant text from its native afterAgentResponse hook. Its preToolUse.agent_message contains only the current narration before that tool, so it can provide positive evidence when the block is present but cannot prove that an earlier response omitted it. A successful check stamps session_name_seen_for; unavailable evidence fails open instead of rejecting unrelated tools.

agents set-task, agents status, and agents suggest-name are narrow escape hatches. The latch allows any one as a single command while display is pending, because they declare focus, create an explicit display retry, or close the coordination ritual. Commands with shell control syntax do not qualify for the exemption. This prevents a failed display check from blocking its own remediation path.

When PreToolUse fires after a tool-use-only assistant row has been appended, the transcript resolver skips that empty user-facing row and checks the prior assistant text. This covers a displayed block followed by a status command without weakening the first-content rule.

The Stop-time transcript fallback accepts only the first assistant message after the tool result that minted the suggestion. Plain-text mentions, leading commentary, tool-result echoes, and end-of-task blocks do not count. Codex Stop remains observe-only under ADR 0051.

UserPromptSubmit now nudges on the direct pending comparison. It therefore recovers both a never-displayed initial name and a lifecycle name that was re-minted but not displayed.

Substantive tool work cannot begin while the current name is pending and ordered reply text is observable. All three adapters use the same state transition, but each uses only evidence its native hooks can support. Cursor records a positive response sighting and never treats later narration as proof that the earlier response was malformed.

If an adapter ends immediately after displaying the block and exposes no transcript at Stop, the operator still sees the correct title. The latch may remain pending until a later tool or prompt provides verifiable text; it never false-passes a late block.

Windows-native Codex can pass a C:\... rollout path to a hook running inside WSL. Transcript resolution checks that path’s /mnt/<drive>/... equivalent before treating it as unreadable. This keeps visible commentary blocks from falling into a repeated deny loop at the host boundary.

A Codex Desktop sequence exposed a second boundary after this decision shipped. The exact block was recorded first, then ordinary commentary became the latest assistant message before the next tool. The gate now evaluates the ordered post-mint message, so later commentary cannot erase valid evidence. A readable malformed or missing block still denies the tool. If the transcript is unavailable or not flushed, the hook reports that verification is pending and allows the tool.

A later Claude Code incident showed that ordered evidence also needs an explicit retry boundary. The display was correct, but the scanner had already classified the original post-mint reply as absent. Later copies could never change that historical result. agents suggest-name --json now returns the exact stored pending suggestion with session_name_retry: true. PostToolUse treats that response as fresh mint evidence, which lets the next exact block satisfy the latch without renaming the session or claiming an unobserved display. Routine set-task calls remain title-silent.

Native Codex child hooks exposed another trust boundary. Their agent_message field can carry adapter-supplied or inherited text rather than the child’s displayed assistant reply. Codex and Claude Code therefore use the transcript resolver; when that transcript is unavailable or not flushed, the gate stays pending and fails open instead of accepting unrelated text.

A Cursor session exposed the corresponding boundary in the other direction. preToolUse.agent_message is the narration immediately preceding the current tool, not a transcript of prior assistant responses. Treating a missing block there as an observed omission rejected every substantive tool after a correctly displayed title. Harnery now wires afterAgentResponse for the completed text, stamps exact blocks there, and treats later tool narration as unavailable rather than negative evidence.

A resumed Codex session then showed the remaining fail-open loop. Codex PreToolUse payloads had no transcript path, so every tool received a generic pending-verification reminder and every routine set-task created another retry boundary. The hook now stays silent when evidence is unavailable, and the prompt reminder is deduplicated by exact minted name. The latch remains pending for honest V3 telemetry; only the automatic re-request is bounded.

A later Cursor session showed that fail-open is not enough on an adapter with no transcript. Cursor can close the latch through exactly two samples of assistant text: one narration handed to a tool hook, and one completed reply. When both miss, the suggestion stays pending for the rest of the session, so suggest-name keeps reprinting the same title, the naming rule keeps asking for it, and the agent has no reachable way to satisfy either. Two mismatches turned out to be enough to cause that. A fenced block labelled anything other than text or plaintext was rejected even though it renders as the same single title line an operator reads and copies. And a suggestion that changed after the instruction went out — an assigned-name rewrite, a lifecycle re-mint, a rebuilt cache that re-minted from the current task — left the agent displaying a title that matched nothing.

The gate now accepts any single-word fence label, and records the title the agent was actually instructed to display so that its display also satisfies the latch. Neither relaxes what the operator sees: the block is still the reply’s first content, still holds one line, and that line still equals a title Harnery asked for. Fixing only the fail-open direction would leave the same class of mismatch stranding future sessions, because the harness, not the agent, owns both the title and the instruction.

The same session was undiagnosable from the hook log, which recorded one no-session-name-sighting reason for both “nothing was owed” and “the reply did not open with the block”, and recorded nothing at all when the gate denied a tool. Those are now distinct reasons, and a denial is logged with the pending title.