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)
Context
Section titled “Context”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.
Alternatives
Section titled “Alternatives”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.
Decision
Section titled “Decision”Treat the existing fields as a three-state display protocol:
unminted = !suggested_session_namepending = suggested_session_name !== session_name_seen_forshown = suggested_session_name === session_name_seen_forWhen 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.
Result
Section titled “Result”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.