Skip to content

sminos bridge: a busy session reads a delivery at its next tool round; a delivery wakes a stopped session - #233

Merged
SSFSKIM merged 6 commits into
mainfrom
bridge-midturn
Oct 18, 2026
Merged

SSFSKIM merged 6 commits into
mainfrom
bridge-midturn

Conversation

@SSFSKIM

@SSFSKIM SSFSKIM commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

An audit of every addressed sminos message on this machine (283 pairs, matched against the recipients' transcripts) found two bridge problems besides #229:

  • Delay. deliver() sent each message with $.prompt.submit. The engine queues plugin prompts at priority later, and the mid-turn fold skips that priority (2.1.300: y6e, Air). So a busy recipient read a message only when its turn ended: median 39 s, max 253 s, unbounded in principle. sminos documents "a busy one reads it at its next tool round", which is what the older inbox path did (median 4.8 s).
  • Loss. An Esc removes every queued plugin prompt (zBt). After an Esc, the engine also refuses every new plugin prompt until the human types (k_e, tengu_plugin_input_refusal). doperpowers e3ce7feb:8 → CI-opt arrived in that window and was lost, although sminos settled it.

Change (hooks/mods/bridge.tsx, sminos_bridge.py, sminos_control.py)

  • While a turn runs, a delivery goes into it as a row ($.session.append), read at the next request. It uses steer's bookkeeping: if the turn answers before reading the row, it wakes once. Measured live: read 10.4 s after delivery, the rest of a 12 s Bash call.
  • A message addressed to a session wakes it even after an Esc (owner's ruling). An idle prompt the engine refuses for the stop is handed back to sminos, which sends it as an inbox frame on the engine's messaging socket; the refusal does not guard that socket. A delivered row that a stopped turn left unread wakes the session through an inbox event, which the child writes to the same socket. Steer keeps waking nothing after an Esc.
  • A row that an Esc-rewind branched past is checked against the conversation at the next turn start and appended again if it is missing. A compaction or /clear lets tracked rows go.
  • Both bridge wakes now open with the line [sminos bridge]: STEER_WAKE was the human-voiced "I added a note…". A transcript's reader can now tell a wake from the human's own prompt; the MAWS desk draws it as a system line.
  • DX5 rows deliver, delivered, steer and the new inbox row are updated in docs/doperpowers/plans/2026-10-15-arkho-desk-c2-bridge.md.

Verification

Left open

  • Steer rows are not re-checked after a rewind.
  • Refusals in local projects are kept and shown by sminos watch, but become an outcome only in hub projects.
  • The stop fallback matches the engine's two drop strings. If a renamed drop string ever appears, delivery goes back to a visible refused, not a silent loss.

Version 7.176.0.

🤖 Generated with Claude Code

@SSFSKIM
SSFSKIM marked this pull request as draft October 18, 2026 07:38
@SSFSKIM

SSFSKIM commented Oct 18, 2026

Copy link
Copy Markdown
Owner Author

Held until the desk reads its rows. #233's mid-turn deliveries are $.session.append rows (isMeta, plugin, no asUser), and MAWS desk-web counts a delivery only as a plugin asUser prompt or a peer/plugin queued_command. A desk Send to a busy session would stay at "delivered" and not be drawn as the owner's. The desk session (smns-erb) is changing MAWS to count a bridge isMeta row whose first line is a whole sminos frame. This merges after the hub carries that change. Don't roll a build containing this until then.

@SSFSKIM
SSFSKIM changed the base branch from bridge-dedupe to main October 18, 2026 07:42
…; an Esc no longer loses one

deliver() submitted every delivery with $.prompt.submit, a turn of its own
that runs once the session is idle: a busy recipient read the message only
when its turn ended (median 39 s, max 253 s on this machine), against
sminos's contract of the next tool round. While the main turn runs, the
delivery now goes in as a steer's row does ($.session.append, read at the
loop's next request), with the steer's wake when the turn answers before
reading it, and `entered` named as soon as the append returns.

An Esc empties the queue of plugin prompts and refuses each new one until
the human sends input (2.1.300 zBt / k_e). An idle delivery the engine
drops for that reason now goes in as a row, which starts no turn; any other
drop stays `refused`. An Esc before the model answered hands the prompt back
and the human's resend goes in where it stood, which leaves a row given
after it on a branch no request reads: each delivered row of a turn that did
not answer (or appended idle) is checked against the conversation as the
next turn starts, and given again when it is missing.

The turn's first request carries the conversation as the turn began, so a
row appended between turn.start and it no longer counts as read (steers
included): a turn that answers in one request wakes once.

Live: tests/mods/live-delivery-cancel-probe.py (busy, bash-esc, restore,
after-esc).
…at ends under it, and a /clear

Three review findings on the mid-turn delivery change:

- A compaction of the main conversation lets every tracked delivered row
  go (bridge-rows.tsx, compacted: the bridge's compact, the compact tool's,
  and the session.compact hook all pass through it). The compacted
  conversation holds a row only in its summary, so the next turn's check
  read a row that was delivered and read as missing and gave it twice. A
  row a rewind left behind before the compaction is the accepted loss.
- recheckDeliveries binds every due row's steer to its turn before its
  first await, the conversation read included, so a turn that ends while
  the check runs stamps them all and settleSteers owes the wake. Bound one
  at a time across the appends, a replay bound after the turn ended never
  woke. Each is let go as its row proves held, or settled as its append is.
- A /clear while a check runs ends it (state.conversation, the counter a
  /clear moves): nothing is appended to the conversation that ended, and
  its steers wake nothing in the new one. An append pending across a
  /clear no longer records its row for the new conversation's check.

Tests: one per finding in tests/mods/bridge.test.ts (deliver), each failing
without its fix; the harness can now hold the conversation read.
After an Esc the engine refuses every plugin prompt until the human sends
input (k_e), so an idle delivery used to go in as a row that started no turn.
A message addressed to a session is meant to wake it, and k_e does not guard
the session's messaging socket, so:

- an idle delivery whose prompt the engine refuses for a stop (decided at
  submit; the submit is raced against STOP_DROP_WAIT_MS) is answered not-ok,
  its mark put back and nothing emitted; sminos_control.deliver returns False
  and push_live sends the inbox frame, which starts a turn (one-line warning
  naming the stop). A stop drop after the answer goes to the child as an
  `inbox` event carrying the text and key.
- a delivered row a stopped turn left unread emits one `inbox` wake per turn;
  a turn that answered with one unread, whose STEER_WAKE is stop-dropped,
  wakes the same way. Steers are unchanged.
- the child sends `inbox` events as frames to the session's own socket from
  its export thread; a keyed one no socket took is kept as a refusal.

Live (2.1.302, haiku): an Esc'd session delivered to through `sminos send`
starts a turn and reads the message; a row delivered mid-Bash and Esc'd
before any request read it wakes the session, which reads it.
…a /clear ends pending wakes; a full queue keeps a keyed inbox frame as a refusal

- A STEER_WAKE queued behind the human's prompt could be dropped by an Esc
  after that prompt's turn had read the delivered row, and the drop still sent
  the inbox wake on the turn's snapshot. The wake is now tracked (state.wakes)
  and marked read by the main loop's next request that carries its rows (any
  request, but the first of a turn already running when it was given); the
  inbox frame goes only if it is still unread when the drop settles.
- A delivery's append pending through an Esc and a /clear settled into a
  steer that survived forgetLive, so DELIVERY_WAKE reached the new session's
  socket. forgetLive now lets every steer and pending wake go.
- The child's _inbox kept a keyed frame a full export queue dropped only as
  a stderr line, its sender already told `submitted`. It is now kept as a
  refusal, as _send_inbox keeps one no socket took, so the daemon reports
  `failed`.

Logged as debt: a late-drop keyed inbox event whose post fails before the
child takes it is lost with no refusal (the resync carries no inbox work).
…script's reader tells it from the human's prompt
@SSFSKIM
SSFSKIM marked this pull request as ready for review October 18, 2026 09:19
@SSFSKIM
SSFSKIM merged commit 01e7226 into main Oct 18, 2026
10 checks passed
@SSFSKIM
SSFSKIM deleted the bridge-midturn branch October 18, 2026 09:19
SSFSKIM added a commit that referenced this pull request Oct 18, 2026
…d a compaction meanwhile gives none again

Re-review findings (reviewer-medium, two P2):
- The next turn's check (recheckDeliveries) left its rows tracked while it
  read the conversation, so a stopped turn's check that started meanwhile
  (its wake delayed by an append pending at the turn's end) could take the
  same row, and both gave it again. Each check now takes its rows out
  before it reads; an unreadable conversation puts them back.
- A compaction that completed while either check read the conversation
  cleared the tracked rows, but the rows the check held escaped and were
  given again (at once, or put back for the next turn's check). A
  compaction counter now fences both checks as a /clear does. The next
  turn's check had this race since #233.
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