Skip to content

steering: a human line in a group room never yields a member's 60s wait #1650

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 08967ee2a0e54489beccfef5786e9724
Branch: dev/3-room-steering-let-go
Base: c26089f
At: 2026-10-29T06:19:51Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

The 60s steering yield (#1509) only fires in a Bot's own chat, so a member Bot stuck in a long foreground wait inside a group room keeps the room's round held for the whole turn — the one place our day actually happens is the one place the rule does not reach.

Verified code facts (read at main c26089f55d1b6a9817457dfb084d46d9e2904af6):

  • core/src/host/steering-interrupt.js:8-22 — shouldInterruptOnHumanSend({ sourceKind, turnRunning, ownChat, waitStartedAt, now, thresholdMs }) requires ownChat === true.
  • core/src/host/steering-interrupt.js:83 — the only caller passes ownChat: rt.chatOwners?.has(id) === true, i.e. a group Session is excluded before the wait is even looked at.
  • core/src/host/steering-interrupt.js:50-56 — interrupt(id) drops the evidence, then agent.cancel({ kind:'hook', reason:'ds-bot/interrupt-long-wait' }, { keepInbox:true }); :58-92 is the session/event listener that feeds it.
  • Group Sessions are rt.groupOwners entries (core/src/host/room-participant.js:22), resolvable with rt.roomOf(id) (core/src/host/turns.js:364, :511).
  • core/src/host/turns.js:120-133 — stopRoomTurn(sessionId, exchangeId) already does what Stop does: marks the exchange in stoppedRoomTurns (bounded, STOPPED_KEPT = 200 at :119), cancels with { kind:'hook', reason:'ds-bot/stopped' } only when the live turn is that exchange (:126-128), else drops that exchange's queued messages (:130-132).
  • core/src/host/turns.js:634-645 — when such a member turn ends, the round's waiter resolves null and, with the stopped flag set, the partial text is not posted to the room (lateRoomReply skipped at :641).
  • core/src/host/turns.js:511-519 — a human line into a group Session is queued into the room's own inbox (queueRoomMessage), not the member's agent inbox, and the next round drains it. So letting the member turn go does not lose the line; keepInbox is not what saves it here, the room inbox is.
  • The exclusion is written into behaviour and docs: core/test/steering-interrupt.test.js:72-87 (// A group room the Bot speaks in is not the human's own chat.) and docs/subsystems/core-host.md:412 ("a group room or an idle Session is never touched").

Reproduction. Populate rt.groupOwners for the session (real install()), start a member turn (live entry with info.role === 'room' and an exchangeId), emit tool/call at T0, advance 61s, then emit a user/message with source.kind === 'user' into that room Session. Today: zero cancels — the turn runs on to its end and the line waits behind the whole turn.

Expected. A human line in a room where a member turn is running and a foreground wait has been open for more than 60s lets that member's turn go the same way Stop does: through stopRoomTurn(sessionId, exchangeId) with the live exchange, exactly once per wait. The line stays in the room inbox and the next round answers it. The interrupted member's partial text stays out of the room (same as Stop), waits of 60s and shorter are left alone, a finished wait is not a wait, and an idle Session is still never touched. The own-chat path is unchanged byte for byte.

Decision already made (do not re-open in review). Reuse stopRoomTurn instead of a second cancel path, so a let-go room turn and a stopped room turn look identical to the round; do not add keepInbox to it, because the room inbox — not the member inbox — is what holds the line.

Acceptance.

  • (a) the reproduction above yields exactly one agent.cancel with reason: 'ds-bot/stopped' on the live exchange, and the room inbox still holds the human line afterwards; a test then drives the next round and asserts the line is answered, not lost.
  • (b) at or below 60s, after the wait closed, without a live turn, and in an own chat, nothing changes.
  • (c) a second line in the same wait still cancels once (the evidence is dropped first, as in the own-chat path).
  • (d) a second member running in the same room is not cancelled.
  • (e) the own-chat path keeps reason: 'ds-bot/interrupt-long-wait' with keepInbox: true; no room id ever gets that reason.
  • (f) the group case in core/test/steering-interrupt.test.js:72-87 is deliberately inverted: it asserts the room path yields through stopRoomTurn and that the room never takes the own-chat path.
  • (g) docs/subsystems/core-host.md:412 is corrected: a group room is no longer "never touched", it yields through the room path; an idle Session still is.
  • (h) the red test is red on today's code and uses the real shapes (groupOwners populated, info.role === 'room' live turn with an exchangeId), not a fixture that fakes chatOwners.

Scope. core/src/host/steering-interrupt.js (the room branch and its gate), core/test/steering-interrupt.test.js, one corrected docs sentence, one changelog fragment, and the regenerated patches/ds-bot/. Touch core/src/host/turns.js only if stopRoomTurn cannot be called as it stands. Do not fold in: the trigger point (agent/inbox/spliced instead of a logged user/message, decided separately), the room busy ack (#1646), or any change to Stop semantics and the stoppedRoomTurns bound.

Written-by: FlyBot Lead

Activity

  1. HugoXK commented on Oct 29, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: 08967ee2a0e54489beccfef5786e9724
    Reason: shipped as #1660, merged f583c7d
    At: 2026-10-29T08:21:40Z

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions