Skip to content

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

Draft
nedtwigg wants to merge 12 commits into
workspaces-dorfrom
workspaces-harden
Draft

Workspaces 5/10: hold derived events in the transfer gap; drift cleanup#618
nedtwigg wants to merge 12 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: e0f73c2
Status: ✅  Deploy successful!
Preview URL: https://85030685.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 on lines +185 to +186
// them are gone from its pane. A later stage recovers the gap
// (docs/specs/standalone.md -> "Arrival queue").

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.

This points at a spec line that says the opposite. docs/specs/standalone.md → "Arrival queue" now reads "The gap is lost on a hand-back: suppressed from the invoke with no replay to follow, it is the one path nothing recovers", and no spec carries a ## Future item for recovering it — standalone.md has no ## Future section at all, and there is no **Scope: workspaces** anywhere under docs/specs/. So a reader who follows the pointer to check what the later stage is finds the claim withdrawn, and nothing will bring it back when a stage does land.

Either drop the promise here, or record it as a ## Future item with a Reserved: line in the body per AGENTS.md → "Spec lifecycle". The suggestion is the former, matching the spec's own wording.

Suggested change
// them are gone from its pane. A later stage recovers the gap
// (docs/specs/standalone.md -> "Arrival queue").
// them are gone from its pane, the one path nothing recovers.

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

platform: PlatformAdapter,
payload: MovePayload,
): Promise<WallBootPlans[string]> {
// Seed the older persisted state before replay re-derives a running watch.

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.

The clause this replaces was the only statement of why the seed exists: the AlertManager is per webview, so the source's went with its window. The spec sentence rewritten in the same commit dropped "seeds each persisted TODO into its own AlertManager" as well, and the nearest sibling seed says the opposite of what the arrival path needs — restoreTerminal's in lib/src/lib/session-restore.ts is commented "Restore-only: a live resume still has the manager's own state". That holds where the manager lives in the extension host; it is false for standalone, where the manager is the arriving webview's. Keeping only the ordering rule leaves an editor free to read this loop as redundant on a live resume and delete it. Keep both halves:

Suggested change
// Seed the older persisted state before replay re-derives a running watch.
// The AlertManager is per webview, so a persisted TODO has to be seeded into
// this one — the source's went with its window. Before the replay, which
// re-derives the running command's watch over it.

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