From bb3efab2b3cfcaf87702723b29ce14aa18ba7c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sun, 11 Oct 2026 16:03:50 +0200 Subject: [PATCH 1/5] =?UTF-8?q?Docs:=20F47=20designs=20Nov=C3=A1=20karta,?= =?UTF-8?q?=20the=20holders=20of=20a=20remote=20tab=20and=20the=20overview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, , 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 --- docs/decisions.md | 179 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/docs/decisions.md b/docs/decisions.md index db461539..d38c0e72 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -6805,3 +6805,182 @@ bytes under the new revision. Not in this revision: the Dashboard's editor of the settings, the company apps of Google Workspace and Microsoft 365 (DEV-6626 task 685), the relay on Team and Automated Environments (Machines #382) and immediate delivery. + +## F47 — Nová karta: one shell element for every app's right panel, the places that hold a remote tab, and the Environment's overview + +**Decided by Matěj 2026-10-11 (root decision 0195 points 5 and 9–12, HumanAndMachines/Lazurio#523; +the final wireframe `HumanAndMachine-ai/prototypes-lazurio` #27; plans DEV-6656 task 917 and +DEV-6646 task 2026-10-11-501); this revision designs it, the steps at the end implement it.** +In the Launchpad the right panel's strip shows every tab of the Environment, in T3 Code the tabs +of one thread (0195 point 9). "+" opens the same Nová karta in every app: an address bar, the +app's own tools, Přehled with the Environment's memory, all its tabs and its running Modules, and +suggestions (point 10). A tab taken from Přehled into a thread stays one remote tab. It ends when +the last place that holds it lets go and no agent works in it, after a short grace with Vrátit; +Zavřít in Přehled or in the Launchpad's strip closes it for everyone (point 11). All of this is +the Environment browser of a Remote Environment (F38, F39); an Environment without it, and a +workstation, keep their apps' own "+". + +1. **Nová karta is a shell element, ``.** Apps embed it from + `/.lazurio/shell.js` as they embed `` (F36), give it their tools and learn what + the tab became (0195 point 10). In its shadow root the element frames one page of the + Launchpad, `/new-tab` on the Launchpad's origin (`apps` in `shell.json`), which draws Nová + karta and does its reads and writes. The writes are why the page is framed. On the + Launchpad's origin they are same-origin and pass the hosted admission as they are (`Origin` + equal to the Launchpad's, `Sec-Fetch-Site: same-origin`, the session revalidated at the + gateway). On the forks' origins the gateway relays only `GET` and `HEAD` of listed paths + under `/.lazurio` and answers other methods with 405 (Machines, `workspace-vm` ingress). + The Launchpad's own panel uses the same element. The page sends + `Content-Security-Policy: frame-ancestors` with the Environment's own app origins (`apps`, + `chat`, `automate`) and nothing else. +2. **The element's interface** is part of the shell's and only grows (F36's addendum of + 2026-10-05): + - `strip`: the strip the tab is for, `launchpad` or a T3 Code thread's agent-browser session + (`t3-`); `tools`: the app's own surfaces, each `{id, label, key?}` (T3 Code: + Terminal, Files, Diff, pull requests, Agents, Device), drawn under the address bar in that + order; the attribute `focus-address`. + - One event, `lazurio-new-tab`, with `detail` `{kind: "page", tab, view}` when an address + opened a remote tab, `{kind: "tab", tab, view}` when a tab of Přehled was chosen, or + `{kind: "tool", id}`. `view` is that remote tab's view (`https://browser.<…>/t/`, F39 + point 1). The app turns its tab into it; the element never navigates the app. + - The address bar takes the keys only with `focus-address`. The Launchpad sets it. T3 Code + does not, so its tools keep opening by their letters, which the page passes on while + nobody types (the wireframe). + - The element hands the page the app's colour roles and language, so Nová karta looks like + the app around it. + - Between the element and its page runs `postMessage`, each side checking the other's + origin and window. Both come from the same Launchpad, so that protocol is internal. +3. **The page.** + - An address creates a remote tab only then (`Target.createTarget` with `newWindow`, F39 + point 2), so an empty Nová karta takes none of the Environment's memory (0195 point 10). + - An address of another Environment's view is never opened in this Environment's browser + (F38's addendum of 2026-10-11); the page says so, as the wireframe does. + - Přehled is read only while the page is visible (the Page Visibility API), every 5 s; a + hidden Nová karta reads nothing. + - Uspat waits for DEV-6656 spike 916; until then Přehled offers Zavřít. +4. **The places that hold a remote tab are files their apps' servers write.** A holder + (0195 point 11) is a T3 Code thread whose strip has the tab, or the Launchpad for a tab a + person opened in it: from its Nová karta, or as a page's new tab while viewed there (F39 + point 4). Each holder is one file, `$XDG_RUNTIME_DIR/lazurio-browser/holders/.json` + = `{"strip", "label", "tabs": […]}`, mode 0600 in a directory of mode 0700. Only + its app's server replaces it, atomically (a temporary file and a rename). + - T3 Code's server keeps each thread's browser tabs, so the strip is the same on every + device, and writes `t3-.json` with the thread's title as `label`. A deleted + thread's file goes. + - The Launchpad writes `launchpad.json`. + + Files need no interface between the servers of one Unix user. Like agent-browser's binding + files they outlive a restart of the Launchpad, of T3 Code and of the view, and they go at a + reboot with the runtime directory, as the browser's tabs do. +5. **The view's hub decides when a remote tab ends.** A page target is **held** while a + holder file lists it or an agent works in it: an agent-browser session is bound to it + (`.target`) and that session's daemon runs (`.pid` names a live process). + The binding alone does not count. agent-browser keeps it past the daemon on purpose, and + the daemon ends after an hour without a command (its default idle timeout). The hub re-reads + both directories when they change and every 30 s. + - When the last hold goes while nobody views the tab, the tab ends after 8 s, the window of + Vrátit (the wireframe's grace). A hold or a viewer within it keeps the tab. + - When the last viewer leaves a tab nothing holds, F39 point 9 stands: it ends after 30 s, + and a tab that nobody has viewed or held yet after 10 minutes. + - A held tab never ends for lack of viewers. + - The hub keeps one blank window of its own, the `about:blank` the unit starts the browser + with, because Chrome ends with its last window and the unit would start it again. That + window is in no list. When someone navigates it, it becomes a tab like any other, and the + hub opens a new blank one. + + This changes F39 point 9 twice. An agent's window ends once no agent works in it and + nothing holds or views it; before, the view never closed it. After a restart of the view + every tab nothing holds gets the 10 minutes; before, the record was gone and nothing closed. +6. **Closing for everyone, and Vrátit.** The hub answers four requests only on its loopback + address (`Host: 127.0.0.1:`). Through the gateway, which sends the view's own host, + they do not exist. Pages in the Environment's own browser reach the loopback too, so the hub + refuses any of these requests that carries an `Origin` or a `Sec-Fetch-Site` header, as + Chrome's requests do; the Launchpad's server sends neither. + - `GET /.lazurio/tabs`: every page target with its title, address, icon, when it was opened + and last used (viewed or navigated), who opened it (the view, Nová karta, a page, or an + agent-browser session), whether an agent works in it, its holders and any grace. + - `POST /.lazurio/tabs {url}`: a new remote tab for Nová karta (point 3); answers its id. + - `POST /.lazurio/tabs//close`: the tab ends after 8 s for everyone, held or not. + - `POST /.lazurio/tabs//restore`: within that grace, or within the grace of point 5. + + The Launchpad is their caller; it knows the view's loopback port from its entry + (`entry.browser`). They give no new power: a process of the Environment that can call them + already reaches DevTools on 9222. When a tab ends, the view page tells its app + (`lazurio-browser:closed`), so a strip that still lists it drops it. +7. **The overview is the Launchpad's.** `GET /api/new-tab/overview` answers the page, beside the + Launchpad's other requests: + - **memory:** the Environment's total and what is in use (`/proc/meminfo`), the browser + (`memory.current` of `lazurio-browser.service`, whose `MemoryMax` F38's addendum of + 2026-10-10 sets), each running Module (its transient unit's `memory.current`), the + protected core (0195 point 3) and the rest; + - **tabs:** the hub's list (point 6) with the holders' labels. An agent that opened a tab is + named by its app from its session (`t3-` T3 Code with the thread's title from its holder + file, `codex-` Codex, `claude-` Claude Code); a person stays unnamed (F11). A tab's memory where it can be + measured, otherwise an estimate marked "≈" (open below); + - **Modules:** the running units of the Environment's Organizations with their memory, + source (`main` or a worktree) and mode, and Zastavit through the existing + `POST /api/modules///stop`. +8. **Actions,** same-origin from the page: + - `POST /api/new-tab/open {url, strip}` asks the hub for the tab and answers its `tab` and + `view`; for `launchpad` the Launchpad records its hold first. Another app records its hold + through its own server when the element reports `page` or `tab`. Until then the tab is + one nobody has viewed or held yet (point 5), and the app's frame of it becomes its viewer + within seconds. + - `POST /api/new-tab/close {tab}` and `POST /api/new-tab/restore {tab}` go to the hub + (point 6). The page asks first when an agent works in the tab (0195 point 5). + - Zastavit is the Module's existing stop. +9. **The Launchpad's panel** shows every tab of the Environment from the overview, and "+" + opens Nová karta; the pinned Přehled of the first wireframe is gone. Its first opening shows + Nová karta, and so does closing the last tab. Zavřít in its strip is the close for everyone + (point 6), with the warning when an agent works in the tab. A message during the grace + never takes Vrátit away (wireframe #27). +10. **T3 Code** (plan DEV-6646 task 2026-10-11-501): "+" opens a tab with `` + instead of the upstream menu. A page or a chosen tab turns it into the thread's tab, a tool + into T3 Code's own surface. Its server keeps the threads' browser tabs and writes their + holder files. Closing a tab in the strip takes it off that thread only, with Vrátit, and + the strip says when another thread or the Launchpad holds it too (from the holder files). + This replaces the interim "+ → Browser" of 0.0.45-lazurio.7. + +**Failure modes.** + +| Failure | What happens | +| --- | --- | +| The view restarts | The hub re-reads holders and bindings; held tabs stay, every other tab gets 10 minutes to be viewed or held again | +| The browser restarts | Its tabs are gone and do not come back (F38's addendum of 2026-10-10); holder files keep ids that no longer exist, which the hub ignores and the strips drop on `lazurio-browser:closed` | +| The Launchpad restarts | Holds stay in their files and no tab ends; Nová karta says the Launchpad is unreachable and tries again | +| T3 Code's server restarts | Its holder files stay; on start it rewrites them from the threads' stored tabs and removes those of deleted threads | +| An app records its hold late | The tab is new, so it has 10 minutes, and the app's frame views it within seconds | +| An agent's daemon ends (an hour idle, a crash) | Its binding stops counting; a tab nothing holds or views ends after 8 s. The agent's next command answers `tab_gone`, and `lazurio browser window` opens a new window (F38's addendum of 2026-10-10, Folder manual) | +| A killed daemon leaves its `.pid` | The process check finds no process; a reused pid at worst keeps a tab longer | +| The hub is down | No tab ends; close and restore answer 503, and Nová karta says so | +| A page in the Environment's browser calls the hub's loopback requests | Refused: its request carries `Origin` and `Sec-Fetch-Site` | +| Every tab ends | The hub's own blank window stays, so Chrome keeps running | +| Another origin frames `/new-tab` | `frame-ancestors` refuses it; the page ignores messages from any other origin or window | +| A reboot | The runtime directory goes with the browser's tabs; nothing comes back | +| An app without the element (an older shell or app) | It keeps its own "+"; T3 Code's release with task 501 requires a Platform release with steps 1–3 | + +**Variants considered.** + +| Alternative | Disposition | +| --- | --- | +| The element draws Nová karta in the app's page, with the endpoints on every origin | Writes from T3 Code's and MausBot's origins would need a new relay with its own admission in the gateway: a Machines release and a fleet rollout; rejected | +| Apps frame `/new-tab` themselves, without an element | Every app would carry the protocol and its origin checks, and 0195 point 10 says apps embed it as shell components; rejected | +| The page on the view's origin (`browser.<…>/new`) | Modules, their memory and Zastavit are the Launchpad's, so the view would call the Launchpad; rejected | +| The Launchpad decides when a tab ends | Two processes would end the same targets; the hub already does (F39 point 9); rejected | +| Holds as leases the people's browsers renew | A thread's tabs would end when a laptop sleeps, and T3 Code's panel differs per device; rejected | +| Holds in the hub's memory through its API | Lost when the view restarts, and every app would depend on the hub being up; rejected | +| A binding file alone means an agent works in the tab | The binding outlives the daemon on purpose, so agents' windows would never end; rejected | +| The element, the Launchpad's page, holder files and the hub deciding (selected) | No change to Machines or the gateway; one owner per fact: holds by their apps, a tab's end by the hub, the overview by the Launchpad | + +**Not decided here.** Uspat and putting tabs to sleep (DEV-6656 spike 916); a person's name as +a tab's opener (admission is not identity, F11); recent sites (until then the suggestions are +the Environment's apps); MausBot as an app of Nová karta; how to measure one tab's memory +(until a spike decides, a tab shows its JavaScript heap marked "≈"); whether archiving a +thread lets go of its tabs. + +**Steps.** (1) The hub: holds from holder files and live bindings, the 8 s grace, the loopback +requests and the tabs' facts; the view page reports a tab's end to its app. (2) The Launchpad: +the overview, the actions and its holder file. (3) The page `/new-tab`, the element +`` and the Launchpad's panel. (4) T3 Code (task 2026-10-11-501): the threads' +tabs on its server, their holder files, the element behind "+", the shared hint; a T3 Code +release. (5) A Platform release and Matěj's check in a browser on his Environment (root +decision 0178). From 526b8e3cb98acb31a4c2deb57039235213d45d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sun, 11 Oct 2026 16:31:29 +0200 Subject: [PATCH 2/5] Docs: F47 apps reconcile their holds with the hub's list of live tabs 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 --- docs/decisions.md | 49 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index d38c0e72..e22387ae 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -6871,6 +6871,23 @@ workstation, keep their apps' own "+". Files need no interface between the servers of one Unix user. Like agent-browser's binding files they outlive a restart of the Launchpad, of T3 Code and of the view, and they go at a reboot with the runtime directory, as the browser's tabs do. + + **Each app keeps its holds true to the browser.** The hub writes the ids of the browser's + live page targets to a file of its own, `$XDG_RUNTIME_DIR/lazurio-browser/tabs.json` = + `{"tabs": […], "writtenAt"}`, mode 0600 and replaced atomically. It writes it + after each discovery of the browser's targets and whenever a target appears or ends. It + never writes it while it does not know them: between a lost connection to the browser and + the next discovery, the file stays as it was. Each app's server reads the file when it + starts, whenever it changes (watched, and read again every 30 s) and before it writes its + holder file. It drops from its stored tabs and its holder file every id that the list does + not contain, provided the list was written after the app recorded that id, so a tab + recorded a moment ago is never dropped. This covers, with no view open: + - a tab closed for everyone or ended while no app shows it; + - a browser restart that ends every id; + - stored ids that ended while the app's server was down. + + A view page whose tab ends also tells its app at once (`lazurio-browser:closed`), so a strip + that shows the tab drops it without waiting for the file. 5. **The view's hub decides when a remote tab ends.** A page target is **held** while a holder file lists it or an agent works in it: an agent-browser session is bound to it (`.target`) and that session's daemon runs (`.pid` names a live process). @@ -6904,8 +6921,9 @@ workstation, keep their apps' own "+". The Launchpad is their caller; it knows the view's loopback port from its entry (`entry.browser`). They give no new power: a process of the Environment that can call them - already reaches DevTools on 9222. When a tab ends, the view page tells its app - (`lazurio-browser:closed`), so a strip that still lists it drops it. + already reaches DevTools on 9222. The apps reconcile with the list of live tabs in a file + (point 4), not through these requests, so an app's server needs neither the view's port nor + the view running. 7. **The overview is the Launchpad's.** `GET /api/new-tab/overview` answers the page, beside the Launchpad's other requests: - **memory:** the Environment's total and what is in use (`/proc/meminfo`), the browser @@ -6945,13 +6963,15 @@ workstation, keep their apps' own "+". | Failure | What happens | | --- | --- | | The view restarts | The hub re-reads holders and bindings; held tabs stay, every other tab gets 10 minutes to be viewed or held again | -| The browser restarts | Its tabs are gone and do not come back (F38's addendum of 2026-10-10); holder files keep ids that no longer exist, which the hub ignores and the strips drop on `lazurio-browser:closed` | +| The browser restarts | Its tabs are gone and do not come back (F38's addendum of 2026-10-10). After discovery the hub writes the new list, and each app's server drops the ended ids from its stored tabs and its holder file (point 4) | +| A tab is closed for everyone, or ends, while no app shows it | The hub rewrites the list when the target ends; each app's server drops it from its strips and its holder file, with no view involved | | The Launchpad restarts | Holds stay in their files and no tab ends; Nová karta says the Launchpad is unreachable and tries again | -| T3 Code's server restarts | Its holder files stay; on start it rewrites them from the threads' stored tabs and removes those of deleted threads | +| T3 Code's server restarts | Its holder files stay. On start it reconciles the threads' stored tabs with the hub's list, rewrites the files, and removes those of deleted threads | | An app records its hold late | The tab is new, so it has 10 minutes, and the app's frame views it within seconds | +| An app records a tab the list does not have yet | It drops an id only when a list written after it recorded the id lacks it, so a new tab stays | | An agent's daemon ends (an hour idle, a crash) | Its binding stops counting; a tab nothing holds or views ends after 8 s. The agent's next command answers `tab_gone`, and `lazurio browser window` opens a new window (F38's addendum of 2026-10-10, Folder manual) | | A killed daemon leaves its `.pid` | The process check finds no process; a reused pid at worst keeps a tab longer | -| The hub is down | No tab ends; close and restore answer 503, and Nová karta says so | +| The hub is down, or does not know the browser's targets | No tab ends, and the list stays as last written, so no app drops a tab because of it; close and restore answer 503, and Nová karta says so | | A page in the Environment's browser calls the hub's loopback requests | Refused: its request carries `Origin` and `Sec-Fetch-Site` | | Every tab ends | The hub's own blank window stays, so Chrome keeps running | | Another origin frames `/new-tab` | `frame-ancestors` refuses it; the page ignores messages from any other origin or window | @@ -6968,8 +6988,10 @@ workstation, keep their apps' own "+". | The Launchpad decides when a tab ends | Two processes would end the same targets; the hub already does (F39 point 9); rejected | | Holds as leases the people's browsers renew | A thread's tabs would end when a laptop sleeps, and T3 Code's panel differs per device; rejected | | Holds in the hub's memory through its API | Lost when the view restarts, and every app would depend on the hub being up; rejected | +| Apps learn that a tab ended only from its view page | Only a tab someone views has a view page; a hidden tab closed for everyone or a browser restart would leave dead ids in strips and holder files; rejected | +| Apps poll the hub's loopback list | T3 Code's server knows neither the view's loopback port nor whether the view runs; the hub's own file needs neither; rejected | | A binding file alone means an agent works in the tab | The binding outlives the daemon on purpose, so agents' windows would never end; rejected | -| The element, the Launchpad's page, holder files and the hub deciding (selected) | No change to Machines or the gateway; one owner per fact: holds by their apps, a tab's end by the hub, the overview by the Launchpad | +| The element, the Launchpad's page, holder files and the hub deciding (selected) | No change to Machines or the gateway; one owner per fact: holds by their apps, the live tabs and a tab's end by the hub, the overview by the Launchpad | **Not decided here.** Uspat and putting tabs to sleep (DEV-6656 spike 916); a person's name as a tab's opener (admission is not identity, F11); recent sites (until then the suggestions are @@ -6978,9 +7000,12 @@ the Environment's apps); MausBot as an app of Nová karta; how to measure one ta thread lets go of its tabs. **Steps.** (1) The hub: holds from holder files and live bindings, the 8 s grace, the loopback -requests and the tabs' facts; the view page reports a tab's end to its app. (2) The Launchpad: -the overview, the actions and its holder file. (3) The page `/new-tab`, the element -`` and the Launchpad's panel. (4) T3 Code (task 2026-10-11-501): the threads' -tabs on its server, their holder files, the element behind "+", the shared hint; a T3 Code -release. (5) A Platform release and Matěj's check in a browser on his Environment (root -decision 0178). +requests, the tabs' facts and the list of live tabs; the view page reports a tab's end to its +app. (2) The Launchpad: the overview, the actions, its holder file and its reconciliation with +the list. (3) The page `/new-tab`, the element `` and the Launchpad's panel. +(4) T3 Code (task 2026-10-11-501): the threads' tabs on its server, their holder files +reconciled with the list, the element behind "+", the shared hint; a T3 Code release. (5) A +Platform release and Matěj's check in a browser on his Environment (root decision 0178). Steps +1, 2 and 4 prove the reconciliation with tests: a tab closed for everyone or ended while no app +shows it, a browser restart with no view open, an app's server that starts with ids that ended, +and a tab recorded before the list has it. From df2c45f2f29a4daa0c70c7205649ada816335f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sun, 11 Oct 2026 16:49:08 +0200 Subject: [PATCH 3/5] Docs: F47 a record made after its tab ended converges too 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 --- docs/decisions.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index e22387ae..e04b2628 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -6875,16 +6875,24 @@ workstation, keep their apps' own "+". **Each app keeps its holds true to the browser.** The hub writes the ids of the browser's live page targets to a file of its own, `$XDG_RUNTIME_DIR/lazurio-browser/tabs.json` = `{"tabs": […], "writtenAt"}`, mode 0600 and replaced atomically. It writes it - after each discovery of the browser's targets and whenever a target appears or ends. It - never writes it while it does not know them: between a lost connection to the browser and - the next discovery, the file stays as it was. Each app's server reads the file when it - starts, whenever it changes (watched, and read again every 30 s) and before it writes its - holder file. It drops from its stored tabs and its holder file every id that the list does - not contain, provided the list was written after the app recorded that id, so a tab - recorded a moment ago is never dropped. This covers, with no view open: + after each discovery of the browser's targets, whenever a target appears or ends, and every + 30 s while it knows them, so every record an app makes meets a newer list. It never writes + it while it does not know them: between a lost connection to the browser and the next + discovery, the file stays as it was. Each app's server reads the file when it starts, + whenever it changes (watched, and read again every 30 s) and before it writes its holder + file. It drops from its stored tabs and its holder file every id that the list does not + contain, provided the list was written at least 5 s after the app recorded that id. The hub + sees a target within milliseconds of Chrome creating it, and an app learns an id only after + that (from the hub, the view or the browser), so a tab recorded a moment ago is never + dropped. An app keeps, with each stored tab, when it recorded it, in its own store (T3 + Code's thread state; for the Launchpad, its holder file's `recordedAt`, which the hub + ignores), so the time survives a restart. A stored tab without a time counts as recorded + long ago. This covers, with no view open: - a tab closed for everyone or ended while no app shows it; - a browser restart that ends every id; - - stored ids that ended while the app's server was down. + - stored ids that ended while the app's server was down; + - a tab that ended before the app recorded it, which the next periodic list drops, within + about 35 s. A view page whose tab ends also tells its app at once (`lazurio-browser:closed`), so a strip that shows the tab drops it without waiting for the file. @@ -6968,7 +6976,8 @@ workstation, keep their apps' own "+". | The Launchpad restarts | Holds stay in their files and no tab ends; Nová karta says the Launchpad is unreachable and tries again | | T3 Code's server restarts | Its holder files stay. On start it reconciles the threads' stored tabs with the hub's list, rewrites the files, and removes those of deleted threads | | An app records its hold late | The tab is new, so it has 10 minutes, and the app's frame views it within seconds | -| An app records a tab the list does not have yet | It drops an id only when a list written after it recorded the id lacks it, so a new tab stays | +| An app records a tab the list does not have yet | It drops an id only when a list written at least 5 s after it recorded the id lacks it, so a new tab stays | +| A tab ends before the app records it | No change writes a newer list, but the hub's periodic one does within 30 s; it lacks the id and is newer than the record, so the app drops it | | An agent's daemon ends (an hour idle, a crash) | Its binding stops counting; a tab nothing holds or views ends after 8 s. The agent's next command answers `tab_gone`, and `lazurio browser window` opens a new window (F38's addendum of 2026-10-10, Folder manual) | | A killed daemon leaves its `.pid` | The process check finds no process; a reused pid at worst keeps a tab longer | | The hub is down, or does not know the browser's targets | No tab ends, and the list stays as last written, so no app drops a tab because of it; close and restore answer 503, and Nová karta says so | @@ -7008,4 +7017,4 @@ reconciled with the list, the element behind "+", the shared hint; a T3 Code rel Platform release and Matěj's check in a browser on his Environment (root decision 0178). Steps 1, 2 and 4 prove the reconciliation with tests: a tab closed for everyone or ended while no app shows it, a browser restart with no view open, an app's server that starts with ids that ended, -and a tab recorded before the list has it. +a tab recorded before the list has it, and a tab that ended before the app recorded it. From d8f69c0cc3a5031b17833e064b48bc842b5c2e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sun, 11 Oct 2026 17:00:24 +0200 Subject: [PATCH 4/5] Docs: F47 every list of live tabs is a fresh snapshot of the browser 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 --- docs/decisions.md | 49 ++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index ee03ef0d..d4708304 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -7029,25 +7029,31 @@ workstation, keep their apps' own "+". **Each app keeps its holds true to the browser.** The hub writes the ids of the browser's live page targets to a file of its own, `$XDG_RUNTIME_DIR/lazurio-browser/tabs.json` = - `{"tabs": […], "writtenAt"}`, mode 0600 and replaced atomically. It writes it - after each discovery of the browser's targets, whenever a target appears or ends, and every - 30 s while it knows them, so every record an app makes meets a newer list. It never writes - it while it does not know them: between a lost connection to the browser and the next - discovery, the file stays as it was. Each app's server reads the file when it starts, - whenever it changes (watched, and read again every 30 s) and before it writes its holder - file. It drops from its stored tabs and its holder file every id that the list does not - contain, provided the list was written at least 5 s after the app recorded that id. The hub - sees a target within milliseconds of Chrome creating it, and an app learns an id only after - that (from the hub, the view or the browser), so a tab recorded a moment ago is never - dropped. An app keeps, with each stored tab, when it recorded it, in its own store (T3 - Code's thread state; for the Launchpad, its holder file's `recordedAt`, which the hub - ignores), so the time survives a restart. A stored tab without a time counts as recorded - long ago. This covers, with no view open: + `{"tabs": […], "takenAt"}`, mode 0600 and replaced atomically. + - **Every list is a fresh snapshot.** The hub notes the time, asks the browser for its + targets (`Target.getTargets`), and writes the page targets of the answer with that time + as `takenAt`. It takes one after each discovery, whenever a target appears or ends, and + every 30 s while it is connected to the browser. Without a connection it writes nothing, + and the file stays as it was. + - **The apps reconcile.** Each app's server reads the file when it starts, whenever it + changes (watched, and read again every 30 s) and before it writes its holder file. It + drops from its stored tabs and its holder file every id that a list lacks whose `takenAt` + is more than 1 s after the app recorded that id. + - **Why that order is safe.** A snapshot lists every target that exists when it begins, also + one whose event reaches the hub late on its own connection. So an id that a snapshot + begun after the record lacks had ended by then, and a live tab is never dropped. The + second guards a small step of the Environment's clock, which both times come from. + - **The record times survive a restart.** An app keeps, with each stored tab, when it + recorded it, in its own store: T3 Code's thread state; for the Launchpad, its holder + file's `recordedAt`, which the hub ignores. A stored tab without a time counts as + recorded long ago. + + This covers, with no view open: - a tab closed for everyone or ended while no app shows it; - a browser restart that ends every id; - stored ids that ended while the app's server was down; - - a tab that ended before the app recorded it, which the next periodic list drops, within - about 35 s. + - a tab that ended before the app recorded it, which the next periodic snapshot drops, + within about 31 s. A view page whose tab ends also tells its app at once (`lazurio-browser:closed`), so a strip that shows the tab drops it without waiting for the file. @@ -7131,11 +7137,12 @@ workstation, keep their apps' own "+". | The Launchpad restarts | Holds stay in their files and no tab ends; Nová karta says the Launchpad is unreachable and tries again | | T3 Code's server restarts | Its holder files stay. On start it reconciles the threads' stored tabs with the hub's list, rewrites the files, and removes those of deleted threads | | An app records its hold late | The tab is new, so it has 10 minutes, and the app's frame views it within seconds | -| An app records a tab the list does not have yet | It drops an id only when a list written at least 5 s after it recorded the id lacks it, so a new tab stays | -| A tab ends before the app records it | No change writes a newer list, but the hub's periodic one does within 30 s; it lacks the id and is newer than the record, so the app drops it | +| An app records a tab the list does not have yet | It drops an id only when a snapshot begun more than 1 s after the record lacks it, so a new tab stays | +| The hub's event about a new tab comes late | The snapshot asks the browser itself, so the tab is in it; no app drops a live tab | +| A tab ends before the app records it | No change triggers a snapshot, but the periodic one does within 30 s; it began after the record and lacks the id, so the app drops it | | An agent's daemon ends (an hour idle, a crash) | Its binding stops counting; a tab nothing holds or views ends after 8 s. The agent's next command answers `tab_gone`, and `lazurio browser window` opens a new window (F38's addendum of 2026-10-10, Folder manual) | | A killed daemon leaves its `.pid` | The process check finds no process; a reused pid at worst keeps a tab longer | -| The hub is down, or does not know the browser's targets | No tab ends, and the list stays as last written, so no app drops a tab because of it; close and restore answer 503, and Nová karta says so | +| The hub is down, or not connected to the browser | No tab ends, and the list stays as last written, so no app drops a tab because of it; close and restore answer 503, and Nová karta says so | | A page in the Environment's browser calls the hub's loopback requests | Refused: its request carries `Origin` and `Sec-Fetch-Site` | | Every tab ends | The hub's own blank window stays, so Chrome keeps running | | Another origin frames `/new-tab` | `frame-ancestors` refuses it; the page ignores messages from any other origin or window | @@ -7154,6 +7161,7 @@ workstation, keep their apps' own "+". | Holds in the hub's memory through its API | Lost when the view restarts, and every app would depend on the hub being up; rejected | | Apps learn that a tab ended only from its view page | Only a tab someone views has a view page; a hidden tab closed for everyone or a browser restart would leave dead ids in strips and holder files; rejected | | Apps poll the hub's loopback list | T3 Code's server knows neither the view's loopback port nor whether the view runs; the hub's own file needs neither; rejected | +| The hub writes its cached targets and apps wait a margin after a record | "Within milliseconds" is an expectation, not an order across two DevTools connections; a late event would drop a live tab; rejected for a fresh snapshot | | A binding file alone means an agent works in the tab | The binding outlives the daemon on purpose, so agents' windows would never end; rejected | | The element, the Launchpad's page, holder files and the hub deciding (selected) | No change to Machines or the gateway; one owner per fact: holds by their apps, the live tabs and a tab's end by the hub, the overview by the Launchpad | @@ -7172,4 +7180,5 @@ reconciled with the list, the element behind "+", the shared hint; a T3 Code rel Platform release and Matěj's check in a browser on his Environment (root decision 0178). Steps 1, 2 and 4 prove the reconciliation with tests: a tab closed for everyone or ended while no app shows it, a browser restart with no view open, an app's server that starts with ids that ended, -a tab recorded before the list has it, and a tab that ended before the app recorded it. +a tab recorded before the list has it, a live tab whose event reaches the hub late, and a tab +that ended before the app recorded it. From 1013da8b7aaca708348b9a93c8d7bdf297e6e6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sun, 11 Oct 2026 18:05:20 +0200 Subject: [PATCH 5/5] Docs: F47 orders records and snapshots by the boot clock, not the wall 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 --- docs/decisions.md | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index d4708304..538bf228 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -7029,23 +7029,28 @@ workstation, keep their apps' own "+". **Each app keeps its holds true to the browser.** The hub writes the ids of the browser's live page targets to a file of its own, `$XDG_RUNTIME_DIR/lazurio-browser/tabs.json` = - `{"tabs": […], "takenAt"}`, mode 0600 and replaced atomically. - - **Every list is a fresh snapshot.** The hub notes the time, asks the browser for its - targets (`Target.getTargets`), and writes the page targets of the answer with that time - as `takenAt`. It takes one after each discovery, whenever a target appears or ends, and - every 30 s while it is connected to the browser. Without a connection it writes nothing, - and the file stays as it was. + `{"tabs": […], "takenAt": {"boot", "at"}}`, mode 0600 and replaced atomically. + - **Times come from the boot clock, never the wall clock.** A time here is the Environment's + boot (`/proc/sys/kernel/random/boot_id`) and the seconds since it (`/proc/uptime`, the + kernel's boot clock). That clock never steps back, unlike the wall clock, and the hub and + the apps read the same one. + - **Every list is a fresh snapshot.** The hub reads the boot clock, asks the browser for its + targets (`Target.getTargets`), and writes the page targets of the answer with that + reading as `takenAt`. It takes one after each discovery, whenever a target appears or + ends, and every 30 s while it is connected to the browser, one at a time. Without a + connection it writes nothing, and the file stays as it was. - **The apps reconcile.** Each app's server reads the file when it starts, whenever it changes (watched, and read again every 30 s) and before it writes its holder file. It - drops from its stored tabs and its holder file every id that a list lacks whose `takenAt` - is more than 1 s after the app recorded that id. - - **Why that order is safe.** A snapshot lists every target that exists when it begins, also - one whose event reaches the hub late on its own connection. So an id that a snapshot - begun after the record lacks had ended by then, and a live tab is never dropped. The - second guards a small step of the Environment's clock, which both times come from. - - **The record times survive a restart.** An app keeps, with each stored tab, when it - recorded it, in its own store: T3 Code's thread state; for the Launchpad, its holder - file's `recordedAt`, which the hub ignores. A stored tab without a time counts as + drops from its stored tabs and its holder file every id that a list of the current boot + lacks, when that list's `takenAt` is later than the app's record of the id. A record + from an earlier boot is older than every list of this one, and a list of another boot is + ignored. + - **Why that order is safe.** A snapshot lists every target that exists when it begins, + also one whose event reaches the hub late on its own connection. So an id that a + snapshot begun after the record lacks had ended by then, and a live tab is never dropped. + - **The record times survive a restart.** An app keeps, with each stored tab, its record + time on the boot clock in its own store: T3 Code's thread state; for the Launchpad, its + holder file's `recordedAt`, which the hub ignores. A stored tab without a time counts as recorded long ago. This covers, with no view open: @@ -7053,7 +7058,7 @@ workstation, keep their apps' own "+". - a browser restart that ends every id; - stored ids that ended while the app's server was down; - a tab that ended before the app recorded it, which the next periodic snapshot drops, - within about 31 s. + within about 30 s. A view page whose tab ends also tells its app at once (`lazurio-browser:closed`), so a strip that shows the tab drops it without waiting for the file. @@ -7137,7 +7142,8 @@ workstation, keep their apps' own "+". | The Launchpad restarts | Holds stay in their files and no tab ends; Nová karta says the Launchpad is unreachable and tries again | | T3 Code's server restarts | Its holder files stay. On start it reconciles the threads' stored tabs with the hub's list, rewrites the files, and removes those of deleted threads | | An app records its hold late | The tab is new, so it has 10 minutes, and the app's frame views it within seconds | -| An app records a tab the list does not have yet | It drops an id only when a snapshot begun more than 1 s after the record lacks it, so a new tab stays | +| An app records a tab the list does not have yet | It drops an id only when a snapshot begun after the record lacks it, so a new tab stays | +| The Environment's wall clock steps back or forward | Nothing here reads it: the boot clock orders records and snapshots | | The hub's event about a new tab comes late | The snapshot asks the browser itself, so the tab is in it; no app drops a live tab | | A tab ends before the app records it | No change triggers a snapshot, but the periodic one does within 30 s; it began after the record and lacks the id, so the app drops it | | An agent's daemon ends (an hour idle, a crash) | Its binding stops counting; a tab nothing holds or views ends after 8 s. The agent's next command answers `tab_gone`, and `lazurio browser window` opens a new window (F38's addendum of 2026-10-10, Folder manual) | @@ -7162,6 +7168,7 @@ workstation, keep their apps' own "+". | Apps learn that a tab ended only from its view page | Only a tab someone views has a view page; a hidden tab closed for everyone or a browser restart would leave dead ids in strips and holder files; rejected | | Apps poll the hub's loopback list | T3 Code's server knows neither the view's loopback port nor whether the view runs; the hub's own file needs neither; rejected | | The hub writes its cached targets and apps wait a margin after a record | "Within milliseconds" is an expectation, not an order across two DevTools connections; a late event would drop a live tab; rejected for a fresh snapshot | +| Wall-clock times, with a margin for a step of the clock | The wall clock can step back by more than any margin, so a snapshot begun before a record could carry a later time and drop a live tab; rejected for the boot clock | | A binding file alone means an agent works in the tab | The binding outlives the daemon on purpose, so agents' windows would never end; rejected | | The element, the Launchpad's page, holder files and the hub deciding (selected) | No change to Machines or the gateway; one owner per fact: holds by their apps, the live tabs and a tab's end by the hub, the overview by the Launchpad | @@ -7180,5 +7187,5 @@ reconciled with the list, the element behind "+", the shared hint; a T3 Code rel Platform release and Matěj's check in a browser on his Environment (root decision 0178). Steps 1, 2 and 4 prove the reconciliation with tests: a tab closed for everyone or ended while no app shows it, a browser restart with no view open, an app's server that starts with ids that ended, -a tab recorded before the list has it, a live tab whose event reaches the hub late, and a tab -that ended before the app recorded it. +a tab recorded before the list has it, a live tab whose event reaches the hub late, a tab that +ended before the app recorded it, a record from an earlier boot, and a step of the wall clock.