Bot writes from background commands keep their starting identity across head reconnects
Merged
AF
12h ago+12018+12018+01234567890123456789012345678901234567890123456789-265-265-012345678901234567890123456789
Background commands can post to GitHub as the bot after the head that started them reconnects, without falling back to the operator's login.
What changes
- A background
ghwrite (comment, review reply, PR edit, issue close) carries a speech ticket captured when the command starts: the agent, scope, repositories and the GitHub app binding of that moment. When it posts, the write takes the same identity checks whether or not its original session connection is still bound, so a rebound session, a rotated binding, an ended code-mode run or a withdrawn scope refuses with a named code instead of posting. - Tickets of every kind must match the identity captured at start. A ticket the daemon doesn't know is refused first as
ticket unknown. - A process-lifetime speech lock is released at module shutdown once no more writes can be queued.
- Migrations 22 (speech tickets) and 23 (dispatch records).
Review
- Report-only security audit: two High findings (an unguarded legacy relay path for still-bound connections; tickets adopting the current agent rather than the starting one), proven with three failing tests through the production relay. Fixed, and the three tests pass unchanged.
- A second cold closure check confirmed the fixes and found no ticket minted inside the async runtime on any production path.
- Calls without a bot ticket still delegate to the real
ghexactly as before.
Tests
Integration suite clean on Linux (2,577 passed; three subc_format goldens fail on that build server on main too). The lib suite's six failures under full load pass alone, twice each. Bin suite and Windows target check pass.
Summary by cubic
Background gh writes from background commands keep the bot identity captured when the command started, instead of falling back to the operator's login when the head session reconnects.
What changed
- A speech ticket captured at command start pins the agent, scope, repositories, and GitHub app binding; every write re-checks that capture when posting.
- A rebound session, rotated app binding, ended code-mode run, or withdrawn scope refuses with a named code and posts nothing.
- An unknown ticket refuses first as
ticket unknown; calls without a bot ticket still delegate to the realghunchanged. - Migrations 22 and 23 store speech and dispatch records; a per-storage speech-serving lease plus a process-lifetime speech lock keep a single forwarder until managed quiescence.
- Background writes hand off through the
subc-client-rsreservation (pinned 0.32.7) inside the final admission gate; the legacy local-mailbox dispatch is removed, with replay refusal retained for historical rows. bash_killand scope kills report when queued bot writes may still post because the revocation is not durable; both plugins render that warning.
Security audit
- A report-only audit found two High findings — an unguarded legacy relay path for still-bound connections, and tickets adopting the current agent instead of the starting one — both fixed and proven by three failing tests that now pass unchanged.
- A cold-closure check confirmed no production path mints a ticket inside the async runtime.
- Native macOS and Linux gate runs pass (gh_shim 533/534, speech 30), including a fixture fix that creates storage before speech registration to satisfy the production lookup precondition; no product behavior changed.