Skip to content

fix(live): a post-close job result replays quietly instead of opening a reopened voice - #1312

Merged
lukacf merged 5 commits into
mainfrom
fix/s104-post-close-merge-quiet
Sep 30, 2026
Merged

lukacf merged 5 commits into
mainfrom
fix/s104-post-close-merge-quiet

Conversation

@lukacf

@lukacf lukacf commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Why

Release Turbo S run 36599321227 failed S104 on both attempts with "the assistant greeted on its own after the reopen with summary" and "no append lane may be used before the first user turn ... but 0 instructions and 3 thinking appends were sent".

A probe reproduced it locally on main (1 of 1):

  • The reopen's pre-open summary missed the 2.5 s bound, so the open took the Late path with its boundary at canonical row 7.
  • The DurableFork job's post-close merge (merge_result_into_source, queued work on the source member) committed rows 8 and 9 while channel 2 opened: "Result of the voice request ..." and the member's "Done, coffee.md reads ...".
  • The mirror queued both as rows the channel will voice, so EnqueueLiveContextRow recorded SpokenCanonicalRow. That cause came in with 5afbac0 and has been in every release since v0.8.46.
  • The late summary and the result then went out before the user spoke, and the model read the result aloud.

When the merge commits before the boundary, the result sits in the summary and the call stays silent. That is why it raced.

What

  • The merge is runtime-authored. It is queued as injected execution context via the new MobHandle::start_injected_context_work_for_identity_bounded (exact runtime-input custody), never as a conversational user row.
  • Runtime work output is quiet history, identified by stamped attribution.
    • Core TranscriptMessageIdentity gains turn_input: Option<TranscriptTurnInput> (RuntimeAuthored).
    • The mob's work-attribution lowering stamps it on the turn's transcript identity for WorkContentAttribution::InjectedExecutionContext, so every message the turn commits carries it. A batched turn keeps it only when every input carries it (TurnInputConsensus).
    • The mirror reads LiveContextRowSource::RuntimeWork (new EnqueueLiveContextRow input row_source, #[non_exhaustive]) from the committed assistant row alone. Transcript position is never used: a mid-turn steer can follow a typed row, and a reply can commit in a later boundary than its input.
    • Such a row gets the runtime-minted disposition LiveContextRowDisposition::ReplayRuntimeWork, is replayed on the quiet thinking lane, and never starts the conversation. Typed and peer rows are voiced as before; the typed turn of Voice reopen: canonical typed answer completes but the new voice channel stays silent meerkat-meta#474 still releases the summary.
  • The replay waits for the conversation. The guard quiet_history_waits_for_the_conversation holds it (typed deferral AuthorizeLiveContextAppendDeferredByConversation), and the user's turn start itself requests the drain.
  • The replay is admitted only in the user phase of a provider turn. A provider turn must be active and no assistant turn attributed to its interaction; the typed turn deferral covers the complement.
    • Why this cannot interleave into an in-flight response: once ObserveLiveAssistantTurnStarted attributes an assistant turn to the interaction, safe_provider_turn_boundary refuses the replay and it waits for the turn boundary like every other row. The machine test replayed_runtime_work_after_the_assistant_turn_started_stays_deferred builds that responding state and asserts the deferral.
    • Voiced rows and reassertions of live speech (ReassertCausalTail) are turn-deferred exactly as on main. An earlier head of this branch let every reassertion through mid-turn: S99 dropped to 0/3 (turbo-live's measurement) and one S106 reopen stalled.
    • Why mid-turn at all: holding the replayed result until the user's first turn ended let the model answer "what happened while I was gone" without it; it delegated instead in 1 of 6 runs.

Kernels, TLA models, cfgs, mapping, contract and the meerkat machine poster are regenerated (xtask machine-codegen, generate-machine-posters.mjs), and machine-check-drift and the poster --check are clean. The Breaking and Added entries are declared in the CHANGELOG.

Evidence

Live S104, local:

  • On the final head: 5 of 5 consecutive passes.
  • On earlier heads of this branch: 8 of 8 (5 + 3).
  • Every run took the Late race path with the merged reply queued as runtime work.
  • No greeting, and zero append-lane use before the first user turn.

Tests:

  • meerkat-runtime --test gpt_live_generated_authority: 65 passed, including the user-phase deferral test, runtime work waiting for the conversation, and live-speech reassertions still turn-deferred.
  • Runtime drain test a_started_user_turn_drains_replayed_runtime_work_held_for_the_conversation: the production observe_live_provider_turn_started requests a fresh drain that replays the held row on the quiet lane. The test fails when that drain request is removed.
  • Mirror unit test: a mid-turn steer after a typed row stays Conversation; a lone merge reply stays RuntimeWork.
  • meerkat-mob-mcp live_delegation (50 passed): the post-close merge test asserts the committed reply carries RuntimeAuthored.
  • meerkat-core --lib: 1896 passed. meerkat-runtime --features live --lib: 1742/1745; the 3 failures (hard-cancel/interrupt tests) pass individually and are an in-process concurrency flake unrelated to this change.
  • meerkat --lib experimental_gpt_live: 83 passed.

Lint and checks: clippy -D warnings --all-targets --all-features is clean on meerkat-core, meerkat-contracts, meerkat-machine-schema, meerkat-runtime, meerkat-mob, meerkat-mob-mcp and meerkat-session. make wasm-check (check + clippy) passes. The emitted schemas and the SDK codegen are refreshed.

Combined live validation (2026-09-30, local, VM load < 120 at each run start)

One build combining #1307 (9837acb), #1308 (74e627a), #1309, #1310 (df32876), #1311 (f9a2f50), #1312 (2962983) and origin/main:

scenario result
S103 5/5 consecutive (monologue overlap 0 ms in every run; no repeated readout lines)
S104 5/5 consecutive (every reopen on the Late race path, no greeting)
S106 5/5 consecutive after #1310's native-answer wait fix; the one failure before it was that wait race (answer audio 16 ms ahead of its closing transcript delta)
S100 3/3
S105 0/3, failing only "no WorkGraph item title equals the user's final transcript", identical to Turbo S run 36599321227 attempt 1 on main (pre-existing, owned by the S105 work)

🤖 Generated with Claude Code

… a reopened voice

Both S104 attempts in Turbo S run 36599321227 failed "the assistant greeted
on its own after the reopen with summary" and "no append lane may be used
before the first user turn, but 3 thinking appends were sent". A probe
reproduced it locally on main 1 of 1: the reopen's pre-open summary missed
the 2.5 s bound (Late), its boundary was canonical row 7, and the DurableFork
job's post-close merge (queued work on the source member) committed rows 8
and 9 ("Result of the voice request ..." and the member's "Done, coffee.md
reads ...") while channel 2 opened. The mirror queued both as rows the
channel will voice, `EnqueueLiveContextRow` recorded `SpokenCanonicalRow`
(5afbac0, in every release since v0.8.46), and the late summary and the
result went out before the user spoke; the model read the result aloud.
When the merge committed before the boundary the result sat in the summary
and the call stayed silent, which is why this raced.

- The post-close merge is runtime-authored: it is queued as injected
  execution context (`MobHandle::start_injected_context_work_for_identity_bounded`,
  exact runtime-input custody), never as a conversational user row.
- The live mirror marks the member's reply to injected execution context as
  `LiveContextRowSource::RuntimeWork` (new `EnqueueLiveContextRow` input
  `row_source`). Runtime work output becomes a quiet reassertion and never
  starts the conversation; typed and peer rows are voiced as before
  (lukacf/meerkat-meta#474 still releases the summary).
- A quiet reassertion waits for the conversation to start
  (`quiet_history_waits_for_the_conversation`, typed deferral
  `AuthorizeLiveContextAppendDeferredByConversation`), and a started user
  turn requests a drain.
- Quiet reassertions are no longer deferred by an active provider turn: the
  thinking lane does not take the turn, and holding the replayed result
  until the user's first turn ended let the model answer "what happened while
  I was gone" without it (it delegated instead in 1 of 6 runs of an earlier
  version of this fix). Voiced rows still wait for the turn boundary.

Kernels, TLA models, cfgs, mapping and contract are regenerated through
xtask machine-codegen. S104 live after the fix: 5 of 5 passed, every run on
the Late race path with the reply queued as runtime work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T21:14:15.180594Z 7fc3b2e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

lukacf and others added 2 commits September 29, 2026 23:55
The first commit let every quiet reassertion through during an active
provider turn. A local S106 run then stalled on its second reopen: the late
summary and two reassertions of the user's own in-flight utterance ("What's
the", "project codename again") were appended on the thinking lane while the
model was starting its answer; it emitted nine characters of transcript, no
audio, and never answered.

Runtime work output now has its own runtime-minted disposition,
`LiveContextRowDisposition::ReplayRuntimeWork`. Only it bypasses
`safe_provider_turn_boundary` (the merged job result still reaches the model
during the user's first utterance) and only it waits for the conversation
(`quiet_history_waits_for_the_conversation`, the typed deferral
`AuthorizeLiveContextAppendDeferredByConversation`). Reassertions of live
speech are deferred by the provider turn exactly as before this branch.
The meerkat machine poster is regenerated for the new guards.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lukacf and others added 2 commits September 30, 2026 00:51
…der turn

`safe_provider_turn_boundary` admitted ReplayRuntimeWork whenever a provider
turn was active. It now admits it only while no assistant turn is attributed
to the turn's interaction (`live_assistant_interaction_by_turn` against
`live_active_interaction_by_channel`), and the typed turn deferral covers the
complement, so a quiet replay can never be appended into an in-flight
response. The machine test builds the responding state and asserts the same
row is deferred there and admitted in the user phase.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… transcript position

Review of #1312: the live mirror inferred RuntimeWork from transcript
position (an assistant row after an injected-context user row in the same
boundary). A durable mid-turn steer after a typed row flipped the typed
turn's reply to RuntimeWork, and a merge reply committed in a later boundary
than its input fell back to Conversation and started the conversation.

- `TranscriptMessageIdentity` gains `turn_input: Option<TranscriptTurnInput>`
  (`RuntimeAuthored`), stamped at admission: the mob's work-attribution
  lowering sets it on the turn metadata's transcript identity when the work
  is `WorkContentAttribution::InjectedExecutionContext`, so every message the
  turn commits carries it. A batched turn keeps it only when every input
  carries it (`TurnInputConsensus`); absence is conversational input.
- The mirror reads `row_source` from the committed assistant row's identity
  alone. Tests: the mid-turn steer after a typed row stays Conversation, a
  lone merge reply stays RuntimeWork, and the mob-mcp post-close merge test
  asserts the committed reply carries `RuntimeAuthored`.
- A runtime test proves the user's turn start itself requests the drain that
  replays held runtime work output on the quiet lane (and fails without it).
- `LiveContextRowSource` is `#[non_exhaustive]`; wire schemas and generated
  SDK artifacts are re-emitted; the CHANGELOG declares the identity field and
  drops a stray blank line in ### Added.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lukacf added a commit that referenced this pull request Sep 30, 2026
Review of #1304 found the supersession guard fired on ANY later
ReassertCausalTail row. That disposition also covered the assistant's own
observed speech (and, before #1312's ReplayRuntimeWork, runtime work
output), so the assistant talking could demote a typed question, and the
demoted row carried the causal-replay framing ("already heard and
answered, do not respond"), false for a row the provider never received.

The live-context row model now keeps the origin. EnqueueLiveContextRow
gains row_author (LiveContextRowAuthor: User, Assistant, Runtime), derived
from the committed message: a live transcript row is
AlreadyPresentInLiveChannel whoever spoke it, so neither the input
disposition nor row_source (what drove the turn) identifies the author.
The runtime mints ReassertCausalTail only for heard user speech and the new
ReassertAssistantOutput for the assistant's observed or live speech; both
wait for the provider turn boundary, DeferredByTurn covers both, and
later ReassertCausalTail supersedes a typed row. Newer is canonical commit
order, documented on the guard: a typed row has no live observation
ordinal to compare with heard order.

Framing, one per quiet kind, none claiming what is false:
- LiveContextAppendKind::SupersededTypedRow with
  LIVE_SUPERSEDED_TYPED_PREFIX: a text-chat row delivered late, ordered
  before the newer speech that takes precedence where they conflict; a
  user request that speech did not replace still needs a response. The
  machine cannot tell a correction from an unrelated remark; the model can.
- LiveContextAppendKind::RuntimeWorkReplay with LIVE_RUNTIME_WORK_PREFIX
  for ReplayRuntimeWork: background work that finished while the model was
  away, context the model has not seen (S104 needs it used when asked).

Tests: runtime_control covers typed then assistant-only speech (voiced),
typed then unrelated user speech (delivered as SupersededTypedRow, text
intact), and typed then a spoken correction (S99 shape).
gpt_live_generated_authority covers the edge boundaries: later user speech
supersedes (flag true, complement false); later assistant output, a later
runtime work reply, a later voiced row and earlier user speech do not; an
equal cursor cannot occur; replayed heard speech is deferred during a
provider turn. A facade test pins the framing contract. S99's oracle
refuses an exchange whose question onset overlaps a response still
streaming. Changelog nits fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lukacf added a commit that referenced this pull request Sep 30, 2026
…ne invariant)

#1312 exempted ReplayRuntimeWork from the provider-turn deferral only while
no assistant turn was attributed to the active interaction, with the
negation on AuthorizeLiveContextAppendDeferredByTurn. That condition can
never be false: an assistant turn is attributed either through the
awaiting interaction, which a provider turn start clears, or as provider
initiated, which requires no active provider turn, and every provider turn
mints a fresh interaction.

The generated MeerkatMachine now states it as the invariant
live_active_provider_turn_has_no_assistant_turn. The exemption reads "no
provider turn, or the row is ReplayRuntimeWork", and the dead
ReplayRuntimeWork branch leaves the DeferredByTurn edge. The test that
built the unreachable state through recover_from_state now asserts that
recovery rejects it, and still checks that the runtime work row is
admitted in the user phase. Kernels, TLA models, contract, mapping and
poster regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@lukacf
lukacf merged commit 2962983 into main Sep 30, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant