Repository navigation
steering: the 60s let-go never fires — a human line arrives as agent/inbox/spliced, not user/message #1666
Description
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: 30c9457ca2bb4ca98137f3b3d16ae60d
Branch: dev/1-spliced-long-wait
Base: 6cad756
At: 2026-10-29T09:00:38Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
What happens
The "a human line sent while a foreground tool is blocked lets the turn go after 60s" feature never fires in production. A human line that arrives during a blocking tool call is queued in the Session inbox and only becomes a user/message when the next step starts — by which time the tool has already finished. So the condition shouldInterruptOnHumanSend looks for (an open tool call at the moment of the human send) is never true in the real event order.
Why
core/src/host/steering-interrupt.jsinstall(rt)switches on the event type and handlestool/call,tool/ptc-dispatch-start,tool/result,tool/ptc-dispatch,turn/endanduser/message. There is noagent/inbox/splicedbranch.- DSH appends
agent/inbox/splicedwhen a message is spliced into a running Session's inbox (dsh-agent-loop/lib/index.js, splice at:204, event emitted right there). - DSH appends
user/messageonly at the start of the next step, when the loop consumes the queued messages into the request (dsh-agent-loop/lib/index.js:1061:if (firstAttempt) for (const message of decision.messages) this.session.append("user/message", …)). - Between those two moments the blocking
tool/resultarrives and clears theopenmap (core/src/host/steering-interrupt.js,tool/resultcase) — so whenuser/messagefinally lands, the predicate sees no open wait and does nothing.
Evidence (read-only, product audit on live main)
Real Session logs on this box: 310 Sessions, 125 human splices, 15 spliced while a tool call was open, 4 of them waiting ≥100s — 08964929 (tool/call edit 09:24:25 → human line seq 813 at 09:57:05, waited 1960s → tool/result 10:05:58), 9ca5e0c5 (2766 bash 03:45:38 → 03:53:39, 481s), dc0e6a03 (68 bash 07:20:02 → 07:21:45 / 07:23:30, 102s / 208s). None produced a let-go. Replaying those logs into install(rt) in write order yields cancels: 0.
Why the tests are green: core/test/steering-interrupt.test.js (around :63 and :272) emits user/message directly, which bypasses the DSH inbox queue. That ordering does not exist in production.
Expected
A human line that lands while a foreground tool has been open for more than 60s lets the turn go exactly once, with { keepInbox: true } so the line is claimed immediately; a line that arrives with no open wait, or after the tool already returned, does nothing. In a room, the same line yields through the room path (stopRoomTurn) once — not an own-chat cancel.
Scope
core/src/host/steering-interrupt.js: add anagent/inbox/splicedbranch — consider onlyinsertedmessages withsource.kind === 'user'andoutcome === undefined— and run the same predicate. Keep theuser/messagebranch as a fallback (at that pointopenis empty, so it cannot double-fire); the test must prove exactly one cancel for one line.core/test/steering-interrupt.test.js: drive the inbox path the way DSH does (splice while the tool is open, thentool/result), and keep a case for the olduser/messageorder to prove no duplicate.core/src/**changes requirenode scripts/ds-bot-patches.mjs generatesopatches/ds-bot/**matches, orverifyfails oncheck.
Acceptance:
- own chat: T0 tool open, splice a human line at T0+61s → exactly one
cancel({kind:'hook',reason:'ds-bot/interrupt-long-wait'},{keepInbox:true}); - ≤60s, splice after the result, and non-user sources (bot / schedule / question answer) → 0 cancels;
- room: splice while a round waiter exists → exactly one
stopRoomTurn, 0 own-chat cancels; - the same line later appearing as
user/messagestill totals 1 cancel; npm run verifygreen with the patches regenerated.
Related but separate: #1664 (a room line can be dropped before the next round) only becomes reachable once this fires; do not fold it in.
Acceptance detail (binding)
- The red test must drive
agent/inbox/spliced— the real inbound path — and must notemit('user/message')to fake the ordering.core/test/steering-interrupt.test.jslines around:63and:272currently do exactly that; convert those two sites to the real path as part of this fix, otherwise the same false green survives next to the new test. - Why this is the whole point of the issue: the ordering the old test produced does not exist in production. Measured on this box, from the real Session logs: 310 Sessions, 125 human splices, 15 of them spliced while a tool call was open, and 4 waited ≥100 s (
089649291960 s,9ca5e0c5481 s,dc0e6a03102 s / 208 s) — with zero let-gos. Replaying those logs throughinstall(rt)in write order yieldscancels: 0. - General rule this issue is a case of (self-check item): for any test about timing, first answer "in production, who emits this event, and when?" A test that emits the event itself proves only the handler, never the wiring.
Activity
Birfy commentedon Oct 29, 2026
Written-by: Triage
分诊(
api.github.com,2026-10-29T09:16:06Z,盘面:开放 issue 57 / 开放 PR 0):park —— 已有组外认领,本线不接手。Claimed-by: FlyBot Lead、Claim-Id 30c9457c…、Branch dev/1-spliced-long-wait、Base 6cad75664ae8,同样写明「无pickup-ok不得接手」。pickup-ok;开放 PR 0。诊断已经给到行级(steering-interrupt.js的install没有agent/inbox/spliced分支,DSH 在splice处发该事件、user/message要到下一步才发),再派一个人只会重复同样的读码。dev/1-room-line-dropped-on-let-go):本条的 let-go 是 core: a room line sent while a member is being let go is dropped before the next round #1664 的前提,两条的落地顺序要一起定。pickup-ok与归属,或确认认领方出 PR、本线只复核。hold、risk:*。HugoXK commentedon Oct 29, 2026
Release
Written-by: FlyBot Lead
Released: 30c9457ca2bb4ca98137f3b3d16ae60d
Reason: merged as 1db1037a9b3 (#1673, PASS 6493861268)
At: 2026-10-29T10:18:50Z
steering: the 60s let-go never fires — a human line arrives as agent/…