Skip to content

Workspaces 5/10: hold derived events in the transfer gap; drift cleanup - #618

Merged
nedtwigg merged 13 commits into
workspaces-dorfrom
workspaces-harden
Sep 15, 2026
Merged

nedtwigg merged 13 commits into
workspaces-dorfrom
workspaces-harden

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

terminal:semanticEvents / terminal:protocolEvents landing while an id is suppressed are held in Rust and delivered behind the replay, instead of dropped. Stale updater comments and the updater:* vs updater:default spec mismatch corrected; a source test pins that main.js hands pty-core the shared sliceSince.

Based on workspaces-dor.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u

nedtwigg and others added 2 commits September 10, 2026 13:55
pty:data in the gap is dropped because the replay carries the bytes, but
terminal:semanticEvents and terminal:protocolEvents are derived once at the
sidecar's parse site and ride no replay, so a prompt mark, cwd change, or
notification landing mid-transfer never reached the target. Rust now holds
them per id, bounded, and emits them to the new owner right behind the
replay that lifts the suppression; a hand-back or exit drops them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u
- The three updater comments still described the reverted every-window
  grant; they now match capabilities/main-only.json, and the specs name
  the capability as written, updater:default.
- layout.md claimed every Workspace verb has a dor counterpart; reorder,
  transfer, and tear-out do not yet.
- A source test pins that main.js hands pty-core the shared sliceSince:
  without it recovery capture reads an empty buffer with no error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4a2e75f
Status: ✅  Deploy successful!
Preview URL: https://dfe403e5.mouseterm.pages.dev
Branch Preview URL: https://workspaces-harden.mouseterm.pages.dev

View logs

@nedtwigg
nedtwigg added this pull request to stack #624 September 10, 2026 21:44

@dormouse-bot dormouse-bot 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.

Feedback on work in progress, not a merge verdict — mark it ready when you want the full review.

The routing change rests on the premise that these events are in no replay. For terminal:semanticEvents in the standalone host that isn't true: the replay carries the raw OSC bytes and tauri-adapter.ts re-derives the semantic events from them itself, so the flushed queue applies them a second time. Mechanism and the narrower fix are in the thread on routing.rs. The other threads are the held queue's lifecycle and two spec claims.

Comment thread standalone/src-tauri/src/routing.rs Outdated
Comment thread standalone/src-tauri/src/lib.rs
Comment thread standalone/src-tauri/src/lib.rs Outdated
Comment thread docs/specs/layout.md Outdated
Comment thread docs/specs/standalone.md Outdated
nedtwigg and others added 2 commits September 10, 2026 17:39
…replay

The hold queued both derived streams on the premise that neither is in
any replay. Semantic events are: the replay carries the raw bytes, OSCs
included, and the target's `pty:replay` listener re-parses them. The
flushed queue then re-applied `commandStart` on top of state the replay
had just rebuilt, and `commandStart` is not idempotent, so a transfer that
split a `commandLine` from its `commandStart` left the arriving window
with a derived title for a command whose real line the replay had
recovered. Semantic events now route to `Drop` while suppressed; only
`terminal:protocolEvents`, which no replay path rebuilds, are held.

What the replay path genuinely did not rebuild was the AlertManager's
half, so both adapters' replay listeners now feed it too, and the tests
pin a watched command coming back from a replay alone.

`clear_suppression` and `mint` cleared `awaiting_replay` and left `held`
behind, so a queue could survive until the shell exited and be flushed
ahead of the next transfer's own gap. One helper, `lift_suppression`,
now takes both halves together, and every site goes through it.

The hand-back comment claimed the source saw the gap's bytes live; it
did not, since suppression is by id and ownership had already moved. The
comment and the Arrival queue spec now say the gap is lost on a
hand-back, the one path nothing recovers on this branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RChsJ5rMUMyfu22UZDfUus
Carries the review fixes from the earlier stages up the stack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RChsJ5rMUMyfu22UZDfUus

@dormouse-bot dormouse-bot 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.

Feedback on work in progress, not a merge verdict — mark it ready when you want the full review.

The semantic-events half of the fix doesn't survive the arrival path it was written for: planArrival calls alertSeed for every transferred pane right after collectLivePtys resolves, and AlertManager.seed nulls commandExitWatch, so the WATCHING state the replay just rebuilt is gone a turn later. Mechanism, and why this is a regression against the previous Hold, are in the thread on standalone/src/tauri-adapter.ts. The other thread is the hand-back comment against the spec line it cites.

Everything else from the last round reads as addressed — the routing split, the lift_suppression consolidation, the HELD_EVENTS_MAX bound in the table, and the layout.md reorder line.

Comment thread standalone/src/tauri-adapter.ts
Comment thread standalone/src-tauri/src/lib.rs

@dormouse-bot dormouse-bot 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.

Feedback on work in progress, not a merge verdict — mark it ready when you want the full review.

The reorder does close last round's thread: alertSeed now runs before adopt_ready, so the replay listener's applyTerminalSemanticEvents is the last writer of alert state on the arrival path, and alert.md -> "Public State" already guarantees the pre-registration seed survives (Must retain host Activity before xterm initialization). One decision the reorder raises, plus the thread on workspace-move.ts.

The seed now runs before the point where planArrival can still throw. On the live.timedOut path the Workspace is handed back and handleArrivalFailed only clears the source's transferring mark, so this window keeps an AlertManager entry — and, through onStateChange -> setTerminalActivity, an activity-store entry — for panes that stay with the source and never mount here. Nothing renders them that I can find: getLivePersistedAlertState returns null for an unregistered id so they are not persisted, and a second arrival of the same ids seeds over them. So it is retained state rather than a wrong display, but it is new with this commit — previously the throw came first. Either clear it on the throw, or say in standalone.md -> "Arrival queue" that a refused arrival leaves its seeded alerts behind, so the next reader does not have to re-derive that it is harmless.

Comment thread standalone/src/workspace-move.ts
@nedtwigg
nedtwigg marked this pull request as ready for review September 14, 2026 22:29

@dormouse-bot dormouse-bot 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.

nedtwigg's replies on the two open threads landed a few minutes before this review and
my snapshot predated them, so an earlier version of this body listed them as
outstanding. Withdrawn: both are deferred to #630 / 3c5fc72c, which does touch
standalone/src-tauri/src/lib.rs and standalone/src/workspace-move.ts.

The seed-before-throw point from the last pass — planArrival seeding before the
live.timedOut throw, so a refused arrival leaves an AlertManager entry, and through
onStateChange an activity-store entry, for panes that stay with the source — reads as
falling under the same consolidation. Say so if it does and it is closed with them.

That leaves the one new point inline, on the held queue's remaining escape hatch. The
verdict is comment rather than approval on that alone.

Comment thread standalone/src-tauri/src/lib.rs
@dormouse-bot

Copy link
Copy Markdown
Collaborator

Both open points check out against #630's head (8708519b), and one narrow case of the seed-before-throw point survives there.

Held queue — closed. sweep_awaiting now takes held, releases through lift_suppression, and returns each (id, queue); the compensating held.remove loop in dispatch_sidecar_event is gone, so it is the one exit from both maps.

Seed before throw — consolidated there too; the live.timedOut branch now clears alert, activity and pane state. It walks a different set than the seed, though: the seed loop is over payload.workspace.session.panes, which includes browser panes — browserPersistedPane in lib/src/lib/session-types.ts carries an alert — while the cleanup walks ptyIds, i.e. terminalIds = allIds.filter(hasTerminal) plus helpers, which excludes them by construction. So a refused arrival of a Workspace holding a browser pane with a persisted TODO leaves that seed in the target's AlertManager, and the source still holds its own — commit() only runs on success. Iterating the set the seed iterated would close it.

@nedtwigg
nedtwigg merged commit 43c4a1d into main Sep 15, 2026
26 checks passed
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.

2 participants