Skip to content

fix(meetings): hold the opening of a meeting through agent initialization - #6649

Merged
bolichen97 merged 1 commit into
mainfrom
fix/meetings-init-ingress-buffer-4610
Aug 30, 2026
Merged

fix(meetings): hold the opening of a meeting through agent initialization#6649
bolichen97 merged 1 commit into
mainfrom
fix/meetings-init-ingress-buffer-4610

Conversation

@iamwhatever

@iamwhatever iamwhatever commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4610.

Why no screenshot: this change adds no UI element, style, or layout. Its only frontend effect is that an existing state — the microphone being open — activates ~46s earlier, because the server now holds speech through agent initialization instead of refusing it. There is no new pixel to photograph; the behaviour is pinned by two canOpenTranscription tests instead (mic opens on the hold, stays shut when the server reports neither open state). The "Preparing agents…" badge, which would be a visual delta, is deliberately not in this PR.

The bug

handle_start_meeting persists active, installs the live session, then awaits init_agents — a sequence of model turns the reporter measured at ~46s. Ingress was suspended across that whole span, so every line spoken into it answered 409 no_active_meeting and reached no agent. The notes and tasks began partway through the first topic, with nothing in either to show a turn had been dropped.

The fix

Speech in the init window is held on the session and replayed in arrival order the moment initialization completes.

Two closed-ingress reasons, told apart. accepting_dispatches was false for two opposite reasons: the meeting is stopping / reviewing / expired (the line has nowhere to go — refuse it, the gate #1981 added) or the meeting is starting (the line is wanted — hold it). The holder now records which, as the session being initialized rather than a bare flag, so the state cannot outlive the identity it describes. suspend_dispatches grows a buffer_speech keyword that only the start path passes; stop, reviewing, expiry and outgoing-session replacement keep refusing, untouched.

Filtered and addressed at arrival. Each line is normalized, noise-filtered, and paired with the recipient names it had at the moment it was spoken, then stored. Both halves of "what happens to this line" are decided when a live line would have decided them, so the hold only ever shifts delivery in time. Names rather than queue objects, so an agent disabled mid-initialization is skipped instead of being fed a queue nothing flushes.

Bounded, by line count. MAX_INIT_BUFFER_LINES = 200. The producer is one finalized speech segment at a time and each is already capped at MAX_TRANSCRIPT_CHARS, so lines are what the overflow rule has to reason about. A real opening lands 15–25 lines here; the cap bounds the hold at ~800 KB for the one meeting MAX_CONCURRENT_MEETINGS permits. The bound is required, not a nicetyPOST …/dispatch accepts untrusted text, so an unbounded hold there is a memory-exhaustion lever.

Overflow drops the OLDEST, and tells the agents that actually lost something. A slow initialization is one where the newest speech is still in play, and the tail is what an agent needs to pick up a conversation mid-flight. The agent-facing marker is addressed to the union of the recipient sets recorded on the dropped lines — not to whoever is unmuted at drain, because those two audiences diverge under a mute landing between the drop and the drain, and the agent with the gap is exactly the one a drain-time audience omits. The transcript gets the same marker under a new system source, added to VALID_TRANSCRIPT_SOURCES deliberately: read_transcript_page drops records whose source it does not recognize, so a marker outside that tuple would be filtered out on read and the gap would be silent again.

The transcript is never truncated. Held and live lines are both appended at arrival, through one shared _record_line, so the human record stays complete and in spoken order even when the hold overflows. An overflow costs the agents context, never the user their transcript.

Nothing observability-only rides along. The hold's counters and a buffered response flag were carried in the first two rounds and had no reader — see the First Principles disposition below. They are left out; they belong with the "Preparing agents…" badge.

Why the frontend change is load-bearing, not cosmetic

canOpenTranscription on main gates the microphone on the polled accepting_dispatches. That gate was itself a mitigation for this window — it stopped early finals from burning the retry schedule, at the cost of not capturing the opening at all. With the server change alone the mic would still stay shut for those ~46s and nothing would ever reach the hold, so the backend fix would be dead code.

The meeting poll now reports a second flag, buffering_dispatches, and the gate opens on either. A server reporting neither is still the genuinely-closed case the gate was written for (stopping, reviewing, expired, or no live session) — that path is pinned by its own test.

Review rounds

Both rounds of findings were accepted in full; neither was rebutted.

Round 1 — GPT: filtering and addressing happened at drain, not arrival. Verified by reverting only session.py to c4ea94cdc: 6 cases fail.

Divergence from the live path Consequence Fix
noise filtered at drain filler occupied a cap slot until drain, so a burst of "uh" could evict the genuine opening speech the bound protects _prepare_line runs at arrival; a filtered line consumes no slot
recipients resolved at drain a mute landing mid-initialization reached backwards and robbed a line spoken while that agent was still listening each line stores _recipient_names() from its own arrival moment

Round 2 — GPT: the overflow marker still used the drain-time audience. The sibling of the same asymmetry, in my own round-1 fix: the lines moved to arrival-time recipients but the marker did not. An agent that lost an opening line and was muted before the drain received its surviving pre-mute lines with no notice of the gap — the silent truncation the marker exists to prevent. Fixed by recording the dropped lines' recipient union (init_dropped_recipients) and addressing the marker to it. Verified by reverting only the marker audience: 2 cases fail.

Round 2 — First Principles: three zero-consumer fields. Correct, and confirmed by grep: init_buffered / init_dropped on the status payload and buffered on the dispatch response had no reader — not the fix path, not the mic gate (which reads buffering_dispatches), not the client (dispatchWithRetry reads only response.segment). The defect is gone without them and the drain already logs delivered/dropped counts. All three are subtracted, along with the agents.py idle default and the LiveStatus / DispatchResponse type members. The pinned /status idle shape is back to its base form, so this PR no longer touches that contract at all.

Opus 4.8 and UX Review returned no findings.

Tests — red-before proven

Against the base commit: 17 backend cases and 1 frontend case fail, the headline one with the exact 409:

FAILED TestSpeechDuringAgentInitIsHeldNotRefused::test_speech_mid_init_is_buffered_and_delivered_in_order
  - AssertionError: {"error": "no active meeting", "code": "no_active_meeting"}

Coverage, per the issue's requirements:

Requirement Test
delivered after init test_speech_mid_init_is_buffered_and_delivered_in_order
order preserved same (asserts the exact queue contents of all three agents)
the cap holds test_the_hold_is_bounded_and_drops_the_oldest, test_it_accepts_up_to_the_cap_without_dropping
overflow drops oldest same, plus test_the_line_past_the_cap_displaces_the_oldest, test_a_lowered_cap_sheds_the_whole_excess_at_once
marker in the transcript test_an_overflow_marks_the_gap_for_the_agents_and_the_transcript
marker reaches the agents that lost a line test_the_marker_reaches_an_agent_muted_after_its_lines_were_dropped, test_no_marker_reaches_an_agent_that_lost_nothing
#1981 gate intact test_a_reviewing_meeting_still_refuses_instead_of_buffering, test_an_outgoing_session_being_replaced_does_not_buffer, and the pre-existing test_stop_closes_dispatch_admission_before_a_slow_agent_flush
no gratuitous markers test_no_marker_when_the_hold_did_not_overflow
held lines still redacted test_a_held_line_is_still_redacted
arrival-time filter + addressing test_filler_does_not_consume_a_slot_and_evict_real_speech, test_a_mute_during_init_does_not_rob_earlier_speech, test_noise_is_filtered_at_arrival_and_never_occupies_a_slot, test_each_line_keeps_the_recipients_it_had_when_spoken
mic follows the hold starts the microphone while the server is HOLDING speech through agent init

Verification

  • 991 backend tests green (all test_meetings_* + test_apps_registry_coverage + test_app_manager + test_ci_surface_tests)
  • Full frontend suite green: 25,858 passed
  • isort / flake8 on src/kiro_crew test conftest.py xdist_budget.py — clean
  • mypy src/kiro_crew/ — no issues in 1,157 files
  • tsc --noEmit — clean
  • Baselined gates: black, subprocess-encoding, lockdown-before-publish — all pass
  • push_guard.py --require-single-on-base — SAFE (single commit on base)

Notes for review

  • Ordering. The reopen and the drain share one DISPATCH_LOCK acquisition. A live dispatch needs that lock too, so nothing spoken after the reopen can overtake the held lines. Releasing between the two would let a meeting's opening land after its second topic.
  • A failed init_agents. _init_agents_plan can raise out of its to_thread, leaving ingress closed and the hold un-drained. That is the behaviour on main too (ingress simply stays shut), and it is exactly why the bound is not optional — the hold stops at 200 lines instead of growing. I deliberately did not add a finally that reopens ingress: that would fan speech out to agents that were never initialized, which is a worse outcome and wider scope than this issue.
  • Diff scope. Confined to the ingress/buffer path. The only pre-existing lines touched are one stale comment that said "every dispatch 409s" (no longer true) and the split of broadcast into _prepare_line + _recipient_names — same behaviour, now shared with the hold so the two cannot drift.
  • Rebased onto 41cc5a83b.

@iamwhatever
iamwhatever requested a review from a team August 28, 2026 22:13
@iamwhatever
iamwhatever requested a review from a team as a code owner August 28, 2026 22:13
@iamwhatever
iamwhatever requested a review from CrysisDeu August 28, 2026 22:13
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of 74c46f1f31c54d37ba3199909eb61d90247e8be2 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

Sound hold-and-replay design; but a failed init_agents now leaves the mic open into a hold nothing will ever drain.

Watch

  • The "Notes for review" claim that a failed init_agents matches main ("ingress simply stays shut") is wrong on the client half: an exception skips resume_dispatches, so buffering_session is never cleared, the poll keeps reporting buffering_dispatches: true, and the new ingressReady OR-gate holds the microphone open indefinitely. The user then conducts a meeting that looks live while agents receive nothing, the cap silently sheds oldest lines, and the overflow marker is never written (only the never-reached drain writes it) — reproducing on the failure path exactly the silent-gap outcome the PR exists to prevent. On main the mic stayed shut, making the failure visible. A try/except around init_agents that re-suspends without buffer_speech=True (not reopening ingress) closes the gate without the wider fan-out-to-uninitialized-agents change the author correctly declined.

Suggestions

  • Persisted system-source transcript records are silently dropped by a rolled-back backend's read_transcript_page; acceptable for a rare overflow marker, but worth a line in the source-tuple comment so a future source addition weighs the same rollback cost.

[DESIGN-REVIEWED] 74c46f1

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of 74c46f1f31c54d37ba3199909eb61d90247e8be2 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

The mic now opens the moment speech can land, pinned in both directions; the only new user-visible string is a near-unreachable overflow marker with fixable copy.

Suggestions

  • SYSTEM_INIT_BUFFER_OVERFLOW leads with "were dropped" and buries the reassurance — a reader of their own intact transcript panics before the last clause corrects them, and "exceeded the hold of {limit} lines" is mechanism vocabulary. Reorder to lead with the audience and outcome: "The agents missed {count} lines from the start of this meeting — spoken before the agents finished starting. The transcript above is complete."
  • TranscriptRow renders source === 'system' identically to speech, relying on the literal [system] prefix; the sibling typed badge (icon + muted label) is the established pattern in the same component — add the same treatment for system and drop the raw prefix from the durable record.

[UX-REVIEWED] 74c46f1

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 74c46f1f31c54d37ba3199909eb61d90247e8be2 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All claims verified. Compiling the review.

First-Principles-Verdict: PASS

Every item traces to the reported ~46s loss (#4610) or to an invariant the hold itself creates; counts and claimed mechanisms all check out in the repo.

What this change ships

Intent: stop a meeting's opening speech from being lost while agents initialize — a FIX.

  1. Speech during startup now reaches the agents, replayed in spoken order — justified (the fix).
  2. Microphone opens ~46s earlier, during initialization — justified; without it the hold is unreachable (canOpenTranscription gate verified).
  3. Meeting poll gains buffering_dispatches — 1 consumer (useMeetingSession.ts ingressReady), boolean, not generalized.
  4. A startup dispatch answers 200/dispatched: 0 instead of 409 — justified; no new response flag (the buffered flag was subtracted, confirmed absent).
  5. Startup speech is durable in the transcript at arrival — justified.
  6. Hold capped at 200 lines, overflow drops oldest — derived (untrusted POST …/dispatch input; agent queues have no reusable cap/pause — AgentQueue.paused is circuit-breaker-only, verified).
  7. Overflow announced under new persisted system transcript source — derived: read_transcript_page filters unknown sources (store.py:574, verified).
  8. Muting now waits out an in-flight dispatch (DISPATCH_LOCK) — cause-level: the one unlocked muted_agents writer (other writers: agents.py:122-locked toggle; pre-install seed at meeting_lifecycle.py:312 — no unfixed siblings; broadcast_system ignores mutes, so not a sibling).
  9. Shared _record_line/_prepare_line split — legitimate mechanism so held and live lines cannot diverge.

No duplicate mechanism exists for the hold (grepped pause|agents_paused: only the failure breaker), and the framing matches the diff — the two prior review rounds' subtractions are genuinely absent.

[FIRST-PRINCIPLES-REVIEWED] 74c46f1

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 74c46f1f31c54d37ba3199909eb61d90247e8be2 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 74c46f1

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 74c46f1f31c54d37ba3199909eb61d90247e8be2: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 74c46f1f31c54d37ba3199909eb61d90247e8be2 — this comment is updated in place on each push.

Review details

The candidate's named triggers — "a model turn errors, a slot spawn fails" during init_agents — are all swallowed by design: init_agents dispatches each agent through _safe_dispatch, which catches Exception and logs ("Failures are logged, not raised: one agent that cannot start must not abort the meeting for the others"). So the concrete inputs the candidate relies on to reach a raise out of init_agents do not occur — they cannot propagate past _safe_dispatch to leave buffering_session set. The only residual raise surface (_init_agents_plan config-read failure, build_init_message, or task cancellation) is speculative "could/might," not a concrete input that occurs in practice, and the same session-left-installed situation predates this diff. Requirement (a) is not re-derivable at the required bar, so the candidate does not survive falsification.

No findings.

[OPUS-REVIEWED] 74c46f1

Verdict parsed from the review's SHA-scoped output markers for commit 74c46f1f31c54d37ba3199909eb61d90247e8be2.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 74c46f1f31c54d37ba3199909eb61d90247e8be2: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 28, 2026
@iamwhatever
iamwhatever force-pushed the fix/meetings-init-ingress-buffer-4610 branch from c4ea94c to 020ccc3 Compare August 29, 2026 00:06
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

Round 1 disposition — GPT 5.6 finding accepted in full

BLOCKING — domain/session.py "Deferred filtering corrupts buffered delivery" — accepted, both halves, fixed in 020ccc3b4.

Verified before fixing, by reverting only session.py to the reviewed commit c4ea94cdc and running the new cases: 6 fail. Both halves were live defects, not theoretical.

Divergence from the live path Consequence
noise filtered at drain, not arrival filler occupied a cap slot until drain and only then was discarded, so a burst of "uh" could evict the genuine opening speech the bound exists to protect
recipients resolved at drain a mute landing mid-initialization reached backwards and robbed a line spoken while that agent was still listening — an outcome the live path cannot produce, since it fans out before the mute exists

Applied the suggested shape:

  • broadcast split into _prepare_line (correct + clamp + noise filter) and _recipient_names. Same behaviour for the live path; both halves now shared with the hold, so the two cannot drift again.
  • buffer_during_init runs _prepare_line at arrival — a filtered line consumes no slot and reports "hold intact" — and stores (line, _recipient_names()).
  • drain_init_buffer replays each line to its own recorded recipient set. The overflow marker still goes to current recipients, since it is a notice about the state of the meeting now rather than a line someone spoke earlier.

One deliberate departure from the literal suggestion: the snapshot stores agent names, not queue objects. An agent disabled mid-initialization has its queue removed from agents, and holding a reference would enqueue into a queue nothing flushes; a name that no longer resolves is skipped instead. Pinned by test_an_agent_removed_mid_hold_is_skipped_not_resurrected.

Four new regression tests: test_filler_does_not_consume_a_slot_and_evict_real_speech, test_a_mute_during_init_does_not_rob_earlier_speech, test_each_line_keeps_the_recipients_it_had_when_spoken, test_a_line_addressed_to_nobody_is_not_counted_as_delivered.

Screenshot Evidence — resolved with the <!-- no-visual-delta --> marker plus a justification in the description. The change adds no UI element, style, or layout; its only frontend effect is that an existing state (microphone open) activates ~46s earlier. Two canOpenTranscription tests pin that behaviour instead. The "Preparing agents…" badge, which would be a real visual delta, is deliberately left to a follow-up.

Also rebased onto dc88f142b. 990 backend tests, 25,858 frontend tests, isort/flake8/mypy/tsc and the three baselined script gates all green locally.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 29, 2026
@iamwhatever
iamwhatever force-pushed the fix/meetings-init-ingress-buffer-4610 branch from 020ccc3 to cc393a9 Compare August 29, 2026 01:24
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 29, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

Round 2 dispositions — head cc393a95e

Both blockers accepted in full. Neither rebutted.

GPT 5.6 — BLOCKING, session.py "overflow marker skips affected agents muted before drain" → fixed

Legitimate, and it is the sibling of the round-1 finding inside my own round-1 fix: I moved the lines to arrival-time recipients and left the marker on the drain-time audience. Those two diverge exactly when a mute lands between the drop and the drain, and the agent with the gap is the one the drain-time audience omits — it receives its surviving pre-mute lines with no notice that an opening turn was lost, which is the silent truncation the marker exists to prevent.

Verified before fixing by reverting only the marker audience (for name in deprivedfor name in self._recipient_names()), everything else unchanged: 2 cases fail.

buffer_during_init now unions the recipient sets of the lines it displaces into init_dropped_recipients, and drain_init_buffer addresses the marker to that set, skipping a name whose queue is gone.

One deliberate refinement on the suggested fix. The suggestion was "the union of current recipients and recipients stored on held lines"; I addressed it to the dropped lines' recipients instead. That set is the precise answer to "whose stream has a gap", and it is a superset of the agents the finding is about — the surviving-lines union misses an agent that was unmuted for the dropped lines but muted for the survivors, and adding the current recipients would put the notice in the context of agents that lost nothing. The complementary case is pinned too: test_no_marker_reaches_an_agent_that_lost_nothing.

Tests: test_the_marker_reaches_an_agent_muted_after_its_lines_were_dropped, test_no_marker_reaches_an_agent_that_lost_nothing.

First Principles (Fable 5) — BLOCK, three zero-consumer fields → fixed by subtraction

Correct on all three, and the grep result matches: init_buffered / init_dropped had no consumer, and nothing reads buffered (dispatchWithRetry at useMeetingTranscription.ts reads only response.segment). The fix path does not read them either — the mic gate reads buffering_dispatches, the drain reads session attributes — so the defect is closed without them, and the drain already logs the delivered/dropped counts for an operator.

Subtracted exactly as prescribed:

  • init_buffered / init_dropped out of MeetingSession.status()
  • the matching keys out of the agents.py idle-status default
  • init_buffered / init_dropped out of the LiveStatus TS type
  • buffered out of the dispatch response and out of DispatchResponse

Side benefit worth naming: the pinned /status idle shape is back to its base form, so this PR no longer touches that contract at all — one fewer thing for a reviewer to weigh. They come back with the "Preparing agents…" badge, which is the consumer, and which the description already scopes out of this PR.

init_dropped_recipients is internal session state feeding the GPT fix above, not a response field, so it is not in scope for this subtraction.

Verification on cc393a95e

991 backend tests, 282 in the two meetings files, 25,858 frontend tests, isort / flake8 / mypy (1,157 files) / tsc --noEmit clean, black + subprocess-encoding + lockdown gates pass, push_guard --require-single-on-base SAFE. Rebased onto 41cc5a83b.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 29, 2026
…tion

`handle_start_meeting` persists `active`, installs the live session, then
awaits `init_agents` — a sequence of model turns the reporter measured at
~46s. Ingress was suspended for that whole span, so every line spoken into
it was answered 409 `no_active_meeting` and never reached an agent: the
notes and tasks began partway through the first topic, with nothing in
either to show a turn had been dropped. Fixes #4610.

Speech in that window is now HELD on the session and replayed in arrival
order the moment initialization completes.

`accepting_dispatches` alone could not carry this, because it was false for
two opposite reasons: the meeting is stopping/reviewing/expired (the line
has nowhere to go — refuse it, the gate #1981 added) or the meeting is
STARTING (the line is wanted — hold it). The holder now records WHICH, as
the session being initialized rather than a bare flag, so the state cannot
outlive the identity it describes. `suspend_dispatches` grows a
`buffer_speech` keyword that only the start path passes; every other caller
keeps refusing, unchanged.

Each line is normalized, filtered and ADDRESSED at arrival, then stored with
the recipient names it had at that moment. Both halves of "what happens to
this line" are decided when a live line would have decided them, so the hold
only ever shifts delivery in TIME. Deferring either to drain was wrong in a
way the live path cannot reproduce: recognizer filler occupied a cap slot
until drain and only then was discarded, so a burst of `"uh"` could evict
the genuine opening speech the bound exists to protect; and recipients
resolved at drain let a mute landing mid-initialization reach backwards and
rob a line spoken while that agent was still listening. Names are stored
rather than queue objects so an agent disabled mid-initialization is skipped
instead of being fed a queue nothing flushes.

Both dispatch paths read `muted_agents` after their own awaited transcript
write, and `handle_mute_agent` was the one writer that touched that set with
no lock — so a mute landing inside that window re-addressed a line to the
mute state of a moment AFTER it was spoken. The lock goes on that writer
rather than on either reader: guarding one dispatch branch would close one
window and leave its twin open, and every other writer already holds
`DISPATCH_LOCK`.

The hold is bounded at `MAX_INIT_BUFFER_LINES` (200) and counted in LINES,
because the producer is one finalized speech segment at a time and each is
already capped at `MAX_TRANSCRIPT_CHARS`. A bound is required rather than
nice to have: `POST .../dispatch` accepts untrusted text, so an unbounded
hold on that path is a memory-exhaustion lever.

Overflow drops the OLDEST lines — a slow initialization is one where the
newest speech is still in play — and is announced to both readers. The
agent-facing marker is addressed to the union of the recipient sets recorded
on the lines the cap DROPPED, not to whoever is unmuted at drain: those two
audiences diverge under a mute landing between the drop and the drain, and
the agent with the gap is precisely the one a drain-time audience omits. It
would receive its surviving pre-mute lines with no notice that an opening
turn was lost. The transcript gets the same marker under a new `system`
source, added to `VALID_TRANSCRIPT_SOURCES` deliberately:
`read_transcript_page` drops records whose source it does not recognize, so
a marker outside that tuple would be filtered out on read and the gap would
be silent again.

The transcript append happens at ARRIVAL for held and live lines alike, via
one shared `_record_line`, so the human record stays complete and in spoken
order even when the hold overflows — an overflow costs the agents context,
never the user their transcript.

The frontend half is load-bearing, not cosmetic. `canOpenTranscription`
gates the microphone on the polled `accepting_dispatches`, so with the
server change alone the client would still keep the mic shut for those ~46s
and nothing would ever reach the hold. The meeting poll reports a second
flag, `buffering_dispatches`, and the gate opens on either — a server that
reports neither is still the closed case this gate was written for.

Nothing observability-only rides along: the hold's counters and a `buffered`
response flag were carried at first and had no reader — not the fix path,
not the mic gate, not the client — so they are left out. They belong with
the "Preparing agents…" badge, which is a separate follow-up.

Red-before proven: 17 backend cases and 1 frontend case fail on the base
commit, the headline one with the exact `no_active_meeting` 409.
@bolichen97

bolichen97 commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Revalidated after the concurrent head update to 74c46f1f31c54d37ba3199909eb61d90247e8be2, against current #5741 (a1ccf489fed05657262c86885a57ea1e72061835), from both actual full diffs.

#5741 extracts the meetings ingress policy into _common.dispatch_admission() / dispatch_line() (including imported transcript lines), with active-session identity checks and persist-before-fan-out. Current #6649 now records normalized lines plus arrival-time recipient snapshots, serializes mute changes under DISPATCH_LOCK, holds with a bounded drop-oldest buffer, drains/opens under the same lock, emits a durable targeted overflow marker, and exposes the frontend buffering_dispatches state. Those additions strengthen initialization buffering, but #6649 still implements its own _record_line() / handle_dispatch_text() branch over the same admission, persistence, and fan-out seam.

Please rebase by folding buffering in as a first-class admission result of #5741's extracted seam instead of preserving two dispatch pipelines. The integrated path should keep #6649's recipient-at-arrival semantics, mute serialization, session identity, strict ordering, bounds/drop accounting and targeted marker, while retaining #5741's common live/typed/imported persistence and dispatch behavior. Please add transition coverage for both live/typed dispatch and imported transcript lines across initialization and drain.

@iamwhatever
iamwhatever force-pushed the fix/meetings-init-ingress-buffer-4610 branch from cc393a9 to 74c46f1 Compare August 29, 2026 18:18
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 29, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator Author

Round 3 disposition — head 74c46f1f3

GPT 5.6 — BLOCKING, agents.py:356 "recipient snapshot occurs after awaited I/O" → fixed, at a different site than proposed

The race is real and I closed it. I did not apply the literal fix, because it would have closed one of two identical windows and reintroduced the asymmetry rounds 1 and 2 removed. Evidence for both halves:

The mutation being raced is unlocked, and it is not in this PR's diff. handle_mute_agent wrote session.muted_agents under no lock at all — not DISPATCH_LOCK, not START_LOCK. git diff origin/main...HEAD -- routes/agents.py matches nothing for muted_agents, DISPATCH_LOCK, or handle_mute, so that writer is exactly as main has it.

Both dispatch branches read the set after the same await. The live branch is await _record_line(...) then session.broadcast(line), which resolves recipients inside itself — the identical ordering the finding describes for the hold, four lines below it. Snapshotting before _record_line in the hold branch only would fix the held line and leave the live line still re-addressed by a mute that landed during its own transcript write, while making the hold read pre-I/O state and live read post-I/O state.

So the lock went on the writer, which is the shared chokepoint both readers go through:

async with DISPATCH_LOCK:
    session = ACTIVE.get(meeting_id)
    if session is not None:
        ...muted_agents.add / discard...

One site, both paths, and the hold stays symmetric with live. Every other writer to that set already holds this lock (handle_toggle_agent takes it; add_agent runs inside it), so handle_mute_agent was the lone gap. Only the in-memory mutation is covered — the metadata write is already serialized by its own transaction, and pulling it inside would hold admission across disk IO dispatch has no reason to wait for.

Red-before proven. Reverting only the lock and running the new test:

>       assert not mute.done(), "the mute applied inside the dispatch's window"
E       assert not True

The mute completes inside the dispatch's window without it, exactly as the finding says.

Test: TestMuteCannotLandInsideADispatch::test_a_mute_racing_the_transcript_write_keeps_the_line_s_audience — parks a dispatch inside the transcript write with a threading.Event, fires POST …/mute concurrently, asserts it cannot apply while admission is held, then asserts the held line kept the recipient set it had when it was spoken.

Verification on 74c46f1f3

992 backend tests (283 in the two meetings files), isort / flake8 / mypy (1,166 files) clean, black + push_guard --require-single-on-base SAFE. No frontend file changed this round, so tsc / vitest carry over from cc393a95e (clean, 25,858 passed). Rebased onto 954736180.

First Principles is green after the round-2 subtraction; Opus 4.8, Design Review and UX Review remain no-findings.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 29, 2026
@bolichen97
bolichen97 enabled auto-merge August 29, 2026 23:58

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving per triage sweep: readiness: passed, required check PR Readiness green, mergeable, no valid change requests or unresolved threads.

@bolichen97
bolichen97 merged commit c815f9c into main Aug 30, 2026
68 checks passed
@bolichen97
bolichen97 deleted the fix/meetings-init-ingress-buffer-4610 branch August 30, 2026 04:14
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 30, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #5741 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #5741: KEEP. Merged predecessor whose code this PR refactors rather than duplicates. 5741 is already rebased onto it (the PR body and the hold_during_init opt-in name the issue it fixed), so nothing is pending. Files: src/kiro_crew/apps/builtins/meetings/backend/routes/agents.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

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.

meetings: opening speech during agent initialization is not captured (ingress suspended ~46s)

2 participants