Workspaces 10/10: harness alert stores through the sidecar - #623
Conversation
The harness applied watched-command and settings changes to its own AlertManager and ignored the sidecar's broadcasts, so the seed-once and delta rules the shipped app relies on were never exercised there. It now sends alert_command like the Tauri adapter and applies the stores' broadcasts, with the harness forwarding the command to the sidecar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u
Deploying mouseterm with
|
| Latest commit: |
3f8f3a0
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://79da3490.mouseterm.pages.dev |
| Branch Preview URL: | https://workspaces-harness.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Feedback on work in progress — not a merge verdict. Mark the PR ready when you want the full review.
The seed's reply is now the only thing that populates the harness's AlertManager, and the harness can miss it. Before this change alertSetWatchedCommands / alertPublishSettings applied locally, so the manager always had the rules. Now the round trip is the only path: initAlertStateReceiver registers the handlers and immediately POSTs the two seeds, and the canonical snapshot comes back over the SSE stream. But BrowserSidecarHost.init() (standalone/src/browser-sidecar-host.ts, the method that does this.events = new EventSource(url)) resolves as soon as the EventSource is constructed, not when it is open — nothing awaits onopen. If the seed POST reaches the bridge before the /events request registers in sseClients, broadcast() fans out to nobody, the reply is gone, and since WatchedCommandHost.initialize only republishes when something asks again, the harness runs with an empty WATCHING rule set until the user toggles a rule by hand — silently, and only in the dev harness the spec rule you added exists to keep faithful. The same gap reopens on any EventSource auto-reconnect: nothing replays the stores afterwards. Resolving init() on the open event closes the startup half.
Tauri has no equivalent gap — listenToWindow registration is awaited before boot proceeds — so this is the one place where the harness's answer to the two-store rule differs from the shipped one.
The new test covers about half of what its name claims. It asserts the outbound commands and the alert:watchedCommands handler fan-out, but not that the broadcast reaches alertManager (setWatchedCommands / applySettings), and the alert:settings branch — including its if (!settings) return guard — is never delivered. Worth a deliver("alert:settings", …) with an onAlertSettings assertion beside the existing one.
The seed round trip is now the only thing that populates the harness's AlertManager, and BrowserSidecarHost.init() resolved as soon as the EventSource was constructed. If the seed POSTs reached the bridge before the GET /events registered in sseClients, the broadcast fanned out to nobody and the harness ran with an empty rule set until a manual toggle. init() now settles on the stream's open event, rejects on an error before that (or after a bounded wait) and closes the stream, and reports every later open through onReconnect. The adapter remembers the two seeds it last sent and re-offers them on a reconnect, which makes the sidecar republish both stores: a repeat seed is refused as a seed but still answered with the canonical snapshot. The adapter test also asserted a settings field the wire never carries and never checked that a broadcast reached the manager; it now uses speakEnabled, spies on setWatchedCommands / applySettings, and delivers alert:settings including its empty-payload guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RChsJ5rMUMyfu22UZDfUus
Carries every stage's review fixes to the tip. The replay-rebuilds-alerts test seeds through the sidecar path this stage installs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RChsJ5rMUMyfu22UZDfUus
dormouse-bot
left a comment
There was a problem hiding this comment.
Feedback on work in progress — not a merge verdict. Mark the PR ready when you want the full review.
The seed-before-stream gap from the last round is closed, and the replay's premise checks out: WatchedCommandHost.initialize and AlertSettingsHost.initialize both call publish() outside the initialized guard, so a repeat seed really does come back as the canonical snapshot.
init() can now reject, and its only caller can't show that. bootstrap() in standalone/src/main.tsx ends with createRoot(document.getElementById("root")!).render(…) and is invoked as a bare bootstrap(); with no .catch(), so a rejected host.init() aborts before the render: the harness is a blank page whose only explanation is an unhandled rejection in devtools. The old init() never rejected — a pre-open failure logged and EventSource retried on its own until the bridge answered. Either keep that self-healing (inline below) or catch in bootstrap() and put the reason on screen; as it stands the new failure mode is loud in code and silent to whoever ran pnpm innerdogfood.
The reconnect hook covers the alert stores but not the registry. dormouse://workspaces rides the same stream — broadcast('sidecar', { event: 'dormouse://workspaces', … }) in standalone/scripts/dev-agent-browser.mjs — so a drop stales it the same way, and installWorkspaceRegistry already holds the re-fetch it would need (host.invoke<WorkspaceRegistrySnapshot>("workspace_registry")). Inert while the harness is one window, but it is the gap the seed replay just closed, one subscriber away.
dormouse-bot
left a comment
There was a problem hiding this comment.
Feedback on work in progress — not a merge verdict. Mark the PR ready when you want the full review.
The retry split lands: EventSource keeps its own reconnect for a CONNECTING failure, OPEN_TIMEOUT_MS still bounds it, and FakeEventSource now carries the readyState/CLOSED pair the assertion needs, so "rejects, and stops the stream, when it errors before opening" is pinning something again.
The new startup-failure UI governs Tauri, but only the harness spec says so. void bootstrap().catch(…) in standalone/src/main.tsx is unconditional — it catches a rejected TauriAdapter.init() (its listenToWindow round trips are awaited and can reject) and a rejected restoreWindowOrFresh, not just the harness's stream timeout. The rule you wrote lives in docs/specs/transport.md → "Standalone browser-dev harness", but bootstrap()'s owning spec is docs/specs/standalone.md → "Boot sequence", which declares Source of truth: standalone/src/main.tsx (bootstrap()) and today carries the neighbouring invariant "planArrival never throws into bootstrap()." A refused sole arrival on the boot path renders a fresh one-pane Workspace, never a blank window — the premise this change generalizes. A reader of standalone.md still sees a boot rejection as unhandled, and AGENTS.md → "House form for rules" puts each rule once in the spec that owns it. Either add the step-9 line there and leave transport.md a pointer, or keep the clause harness-local and gate the catch on BROWSER_DEV_HOST.
RegistryWorkspace.ref's null no longer has a producer. standalone/src/workspace-registry.ts declares ref: string | null under /** `workspace:<n>` for a minted id; `null` for one the registry did not mint. */, but Rust never produced it — ref_for in standalone/src-tauri/src/workspaces.rs is Some(ref_number(id).map_or_else(|| format!("workspace:{id}"), …)), pinned by assert_eq!(ref_for("workspace-abc12345-3"), Some("workspace:workspace-abc12345-3".to_string())). The harness's null was the last one, and this commit removes it. The doc comment is the half that was wrong, so tightening to ref: string with a comment naming the opaque-id fallback makes the type match both hosts; leaving it means every consumer keeps narrowing a case neither host can send.
One housekeeping note on the open thread above: its suggestion block reproduces the pre-commit paragraph, so applying it now would silently drop the retry/startup-error clause you just added. Worth re-composing rather than clicking apply.
# Conflicts: # scripts/spec-word-budgets.json
dormouse-bot
left a comment
There was a problem hiding this comment.
Two findings, both on the new guide. The open threads from the draft rounds still stand.
§4's list is not the gate CI runs. Root pnpm test runs public-docs, loopback, deploy (with ps1-cmdlet and installer-verify), and e2e on top of spec-lint, plus every lint's self-test and pnpm -r run test; §4 names only pnpm lint:specs and a bare node scripts/xterm-lint.mjs, which also skips scripts/xterm-bump.test.mjs. An agent who works through §4, sees green, and pushes goes red in CI on a lint the section never named — the outcome the section exists to prevent. Suggestion inline.
§1 and §1a describe state that expires when the stack lands. The table names ten open draft PRs, ten branches, and their worktree paths; "Keep every PR a draft" and the merge-forward procedure hold only while those exist. This file merges to main with the last PR in the stack, and from then on §1 points a new agent at branches and worktrees that are gone while §5 still opens "Nothing below has been run in the Tauri app yet". One line scoping the two sections costs nothing now; suggestion inline.
One judgment call, not a blocker: §7's "Rules that bite" and §6's ratchet recipe paraphrase rules AGENTS.md already owns — "Never git switch -c inside an existing worktree", the --ratchet step, the bare-file-name pointer lint. AGENTS.md -> "What, not why" keeps each rule in the file that owns it and makes every other mention a one-line pointer. This guide is not a spec so no lint enforces that, which is also why the copies will drift silently once AGENTS.md moves.
|
All four findings verified as addressed in #630 — I read the commits rather than taking the claim on trust.
One thing worth confirming since it moved rather than changed: No remaining concerns from my side on this PR. |
The browser harness now sends
alert_commandand applies the sidecar stores' broadcasts like the Tauri adapter, so the seed-once and delta rules are exercised there. Simulating several windows in the harness is not built.Based on
workspaces-move-verb.🤖 Generated with Claude Code
https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u