Repository navigation
Docs: F47 designs Nová karta, the holders of a remote tab and the Environment's overview - #314
Conversation
…rview Root decision 0195 points 9-12 and the approved wireframe give every app's right panel the same Nová karta and say when a shared remote tab ends. F47 designs it before the code: a shell element, <lazurio-new-tab>, that apps embed like the rail and that frames one Launchpad page, so its writes stay same-origin and pass the hosted admission with no gateway or Machines change; holder files that the apps' servers write; and the view's hub as the only place that ends a tab, with the 8 s grace of Vrátit and a loopback API that refuses browser requests. It changes F39 point 9 twice: an agent's window ends once no agent works in it and nothing holds or views it, and after a view restart unheld tabs get the 10 minutes instead of living for good. Plans DEV-6656 task 917 and DEV-6646 task 2026-10-11-501 implement it in the five steps the entry lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty — CHANGES_REQUESTED on bb3efab (F47 design only).
Blocking: F47 points 6 and “Failure modes” (docs/decisions.md:6907, 6948–6951) say that a closed tab disappears from every strip via lazurio-browser:closed, including after a browser restart. But the proposed notification is emitted by the view page to its embedding app; only a currently viewed tab has a view page/socket. A T3 thread can hold a tab in its server-side stored list while another tab/thread is displayed, or have no panel open at all. When the hub explicitly closes that hidden tab for everyone, or Chrome restarts and all target IDs disappear, that app receives no event. Its stored tabs and holder file keep dead IDs; on T3 server restart, the design rewrites the same stale IDs from storage. The hub ignores them, but the thread strip and shared-holder labels remain stale, contradicting the all-tabs and close-for-everyone contract.
Please specify an authoritative reconciliation of each app server’s persisted tabs and holder files against the hub’s live target IDs, including startup/reconnect, explicit close and browser restart while no tab is viewed. Clarify how a server learns a closure and removes the dead ID without requiring an active view, and add that failure mode/verification to the implementation steps. This can be polling, a server-to-server signal or another bounded mechanism; an event from an individual view alone is insufficient.
Verified: exact detached head and base, F36/F38/F39 and 0195(5,9–12), Machines shell relay and hosted admission, agent-browser binding/daemon lifecycle, clean git diff --check, bun run check:public PASS (660 files). No code or merge performed.
Pablo's review of bb3efab: a view page's lazurio-browser:closed only reaches an app while someone views that tab. A hidden tab closed for everyone, or a browser restart, would leave dead ids in a T3 Code thread's stored tabs and holder file, and a restarted server would write them back. The hub now writes the ids of the browser's live page targets to its own file, $XDG_RUNTIME_DIR/lazurio-browser/tabs.json, after each discovery and whenever a target appears or ends, never while it does not know them. Each app's server reconciles its stored tabs and holder file with it at start, on change and before writing, dropping only ids recorded before the list was written. The view's message stays as the immediate signal for a visible tab. Failure modes, rejected variants and the steps' tests say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fixed in 526b8e3 (re-review requested on that head). You were right: F47 point 4 now has one authoritative list. The hub writes the ids of the browser's live page targets to its own file,
Each app's server reconciles its stored tabs and holder file with it at start, on change (watched, plus a 30 s re-read) and before writing. It drops only ids it recorded before the list was written, so a just-recorded tab is never dropped. The view's message stays only as the immediate signal for a visible tab. Point 6 says why it is a file and not the loopback request: T3 Code's server knows neither the view's port nor whether the view runs. Also changed:
|
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty — CHANGES_REQUESTED on exact 526b8e3 (F47 design only).
The new tabs.json reconciliation addresses the original hidden-close/restart case when a hold predates the updated list, but its timestamp guard leaves a late-hold race unresolved (docs/decisions.md:6875–6887, 6971). Example: Nová karta reports an id to T3 Code, but before its server records the hold the target ends and the hub atomically writes a list without that id. If the app request/event is delayed and the server records the id after that write, the only authoritative list is older than the hold. The 30 s re-read, startup reconciliation and pre-write check all read the same file and refuse to remove the id; with no further target/discovery event, the dead id remains in the thread strip and holder file indefinitely, even across T3 server restarts. The same can happen when selecting an overview tab that ends before its hold is recorded.
Please make late records converge too (for example a bounded periodic fresh discovery/list generation while the hub knows the browser, or an explicit causal freshness protocol), while preserving the guard that a genuinely new target is not deleted before the hub observes it. Specify how the app timestamps survive server restart and test the ended-before-record case alongside the four listed tests. Do not rely on a viewed tab or a future unrelated target event.
Verified in isolated detached checkout: head/base pinned, docs-only diff, git diff --check clean, bun run check:public PASS (660 files) under the heavy lock. Live CI pass/skipped, zero unresolved threads. No merge performed.
Pablo's review of 526b8e3: if a tab ends before an app records it, the list the hub wrote at the end is older than the record, so the guard keeps the dead id, and without another target event no newer list ever comes. The hub now also writes the list every 30 s while it knows the browser's targets, so every record meets a newer list. An app drops an id only when a list written at least 5 s after its record lacks it: the hub sees a target within milliseconds of Chrome creating it, before any app can learn the id. Apps keep each record's time in their own store (T3 Code's thread state, the Launchpad's holder file's recordedAt), so it survives a restart. The failure modes and the steps' tests name the ended-before-record case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fixed in df2c45f (re-review requested on that head). Right again: if the tab ends before the app records it, the list written at its end is older than the record, so the guard keeps the dead id, and no newer list ever comes without another target event. Point 4 now:
The failure modes have an ended-before-record row, and the steps' tests list that case. |
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty — CHANGES_REQUESTED on exact df2c45f (F47 design only).
The periodic rewrite closes the previous ended-before-record liveness race when the hub already has an accurate target set, but the new 5 s margin does not establish the claimed causal ordering (docs/decisions.md:6878–6886, 6979–6980). A tab can be created through agent-browser/direct CDP (src/browser/window.ts:204–215) or learned from the browser on a connection separate from the hub’s Target.targetCreated stream (src/browser/people/hub.ts:579–586). If the app records this live id at t=0 while that event is delayed, the hub can periodically rewrite its cached target set without the id at t=6. The app then drops the genuinely live id, removes its hold, and the hub can end the tab. “Within milliseconds” is an expectation, not a cross-connection ordering guarantee; waiting 5 seconds cannot prove the hub observed the creation. Please make periodic lists authoritative by fresh browser discovery/snapshot after the record (with the timestamp tied to snapshot initiation), or specify an equivalent positive observation/causal protocol, and include the delayed-discovery live-tab case in the steps’ tests. Preserve the ended-before-record convergence.
Separately, main advanced to 00862d2 (F46/#307) after the stated base 3643e1a; the current head now has a docs/decisions.md merge conflict. Please integrate current main without losing F46 or F47 and rerun exact-head gates on the new SHA.
Verified in disposable detached checkout: docs-only diff, git diff --check clean, bun run check:public PASS (660 files) under heavy lock. Live checks pass/skipped and zero unresolved threads at review time. No merge performed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pablo's review of df2c45f: a 5 s margin is an expectation, not an order. A tab created through agent-browser or plain DevTools reaches the hub on its own connection, so a list written from the hub's cache could miss a live tab and an app would drop it. Each list is now a fresh snapshot: the hub notes the time, asks the browser for its targets (Target.getTargets) and writes the page targets with that time as takenAt, after each discovery, on every change and every 30 s while connected. An app drops an id only when a snapshot begun more than 1 s after its record lacks it; the snapshot lists every target that exists when it begins, also one whose event reaches the hub late, and the second guards a small clock step. Failure modes, a rejected variant and the steps' tests (a live tab whose event comes late) say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fixed in d8f69c0, after merging main (9d61c4e: F46 from #307 kept byte for byte, F47 after it; the diff against main is F47 only). Re-review requested on that head. You were right that the 5 s margin was an expectation, not an order across two DevTools connections. Every list is now a fresh snapshot:
Also changed:
|
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty — CHANGES_REQUESTED on exact d8f69c0 (F47 design only).
The fresh Target.getTargets snapshot fixes the delayed targetCreated event and the periodic snapshot fixes the ended-before-record case under a stable clock. One safety premise is still not guaranteed: point 4 compares persisted wall-clock takenAt and recordedAt and says the 1 s margin guards a small Environment clock step (docs/decisions.md:7033–7048). A Remote Environment's wall clock can jump back by more than 1 s. Concrete ordering: the hub starts a snapshot at clock 100 and receives a target list without a new tab; the clock steps back 10 s; the app creates and records that live tab at clock 91 on another CDP connection; the earlier snapshot is written after that record, with takenAt=100 and without the id, before the delayed targetCreated event. The app sees 100 > 91+1 and deletes a live tab from its stored strip and holder file. A later snapshot containing it cannot restore that app hold; the hub may then close it when unviewed. This contradicts “a live tab is never dropped” and the delayed-event test would miss it.
Please specify a cross-process, restart-safe causal comparison that does not rely on a bounded wall-clock step (e.g. a shared boot-scoped monotonic timebase with its boot identity in the persisted records, or a protocol that positively confirms a missing id after the record), and cover a backwards-clock-step fixture alongside both late-event and ended-before-record cases. Linux CLOCK_REALTIME is subject to discontinuous jumps; CLOCK_MONOTONIC is not. No code or merge performed.
Verified in disposable detached checkout: current main 00862d2 contains F46 unchanged, PR diff is F47 only; git diff --check clean; bun run check:public PASS (664 files) under heavy lock. Live checks pass/skipped, zero unresolved threads at submission gate.
…l clock Pablo's review of d8f69c0: the wall clock can step back by more than the 1 s margin, so a snapshot begun before an app's record could carry a later time and the app would drop a live tab. Times are now the Environment's boot (/proc/sys/kernel/random/boot_id) and the seconds since it (/proc/uptime), which never step back and which the hub and the apps read alike. An app drops an id only when a list of the current boot, taken after its record, lacks it. A record from an earlier boot is older than every list of this one, and a list of another boot is ignored. The margin is gone. A wall-clock row in the failure modes, a rejected variant, and two more cases in the steps' tests (an earlier boot, a step of the wall clock) say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fixed in 1013da8; re-review requested on that head. Right: the wall clock can step back by more than any margin. The fix removes the wall clock from the comparison rather than adding a protocol:
Your ordering (snapshot at 100, wall clock back 10 s, record at 91) cannot happen on the boot clock: the record reads a later boot time than the snapshot's start, so it is kept. Also changed:
|
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty — APPROVED on exact 1013da8 (F47 design only). The boot-scoped monotonic clock removes the prior wall-clock regression: a snapshot stamped before a later app record cannot appear newer after a wall-clock step. Fresh, serial Target.getTargets snapshots and strict later-than comparison retain the delayed-discovery live tab while the periodic snapshot converges an ended-before-record id. Earlier-boot records and wall-clock-step cases are now explicitly in the implementation steps. Verified isolated detached head against main 00862d2, docs-only delta, git diff --check clean, and bun run check:public PASS (664 files) under the heavy lock. Live CI success/skipped, zero unresolved review threads at submission gate. This is design approval, not implementation or release QA; no merge performed.
Status: ready for review (design only; the steps it lists follow as their own PRs)
Why
Root decision 0195, addendum points 9–12 (HumanAndMachines/Lazurio#523, 2026-10-11), and the final wireframe Matěj approved (
HumanAndMachine-ai/prototypes-lazurio#27) change the right panel of the Environment browser:None of this exists in the Platform today. The view's hub only closes tabs it opened, after their viewers leave. No list of tabs, no per-tab facts, no notion of a holder, and no way for T3 Code's origin to write to the Launchpad. This PR is the design note that plan DEV-6656 task 917 (Platform) and plan DEV-6646 task 2026-10-11-501 (T3 Code) implement; I took 917 over from the DEV-6656 thread.
What the design decides (F47 in
docs/decisions.md)<lazurio-new-tab>. Apps embed it from/.lazurio/shell.jsas they embed<lazurio-rail>, as 0195 point 10 says. In its shadow root it frames one Launchpad page,/new-tab, on the Launchpad's origin. That is the key choice: the page's writes (open, close, restore, stop) are then same-origin and pass the hosted admission unchanged. Drawn directly in T3 Code's page, they would hit the gateway's 405 for non-GET under/.lazurioon the forks' origins and need a Machines release and fleet rollout. The page limitsframe-ancestorsto the Environment's own app origins.strip,tools({id, label, key?}),focus-address, and onelazurio-new-tabevent that says whether the tab became a page, a chosen tab or a tool. The element and the page talk over origin-checkedpostMessage, internal to one Launchpad.$XDG_RUNTIME_DIR/lazurio-browser/holders/<strip>.json(0600, atomic rename). T3 Code's server writes one per thread from the threads' stored tabs, so the strip is the same on every device. The Launchpad writeslaunchpad.json.$XDG_RUNTIME_DIR/lazurio-browser/tabs.json. Every list is a freshTarget.getTargetssnapshot. Snapshots are taken after each discovery, on every change and every 30 s while connected, one at a time; with no connection nothing is written.boot_id+/proc/uptime), never the wall clock, which can step back. Each snapshot is stamped with the boot-clock time it began (takenAt).recordedAt.lazurio-browser:closedstays only as the immediate signal for a visible tab.OriginorSec-Fetch-Site, because pages in the Environment's own browser can reach the loopback too./proc/meminfoand the units'memory.current, tabs with opener and holders, running Modules with the existing stop. Read only while Nová karta is visible.What it changes in earlier decisions
F39 point 9, twice:
What does not change
Verification
Every mechanism the note relies on was checked in the code before writing it:
workloads/workspace-vm/ingress.ts);shell.json'sappsorigin (src/shell/contract.ts), T3 Code embedding<lazurio-rail>from/.lazurio/shell.js;.pidremoved on exit (cli/src/native/tab_binding.rs,daemon.rs, 1 h idle default);t3-<thread>(itsagentBrowserSession.ts);src/browser/units.ts);Hostrule (src/browser/people/service.ts);entry.browser;GRACE_MS).bun run check:publicpasses. The note is docs only.Risks and open questions
Next
Steps 1–3 (hub, Launchpad overview and actions, page + element + panel) as Platform PRs. Then step 4 in Lazurio/t3code, then a Platform release and Matěj's check in a browser (root decision 0178).
🤖 Generated with Claude Code