Repository navigation
Runtime architecture refactor - #744
Open
Anton-Horn wants to merge 118 commits into
Open
Anton-Horn wants to merge 118 commits into
Anton-Horn wants to merge 118 commits into
Conversation
Implements docs/architecture.md: one runtime daemon per workspace that owns all workspace state and work, a portable client talking to it only through the runtime protocol, and src/ reorganised by layer and side (kernel, runtime, workspace, services, client, panels, shells) with the rules enforced by dependency-cruiser. Also: - Cate Connect client, pairing, Noise security layer and network transports (same network, WebRTC) - cate CLI generated from the declared API specs, plus `cate serve` - Hermes history: read message content as a BLOB, since node:sqlite on Node 22 truncates TEXT at the NUL of `\x00json:` rows - e2e cleanup: remove the temp HOME with async rm, which retries while the killed runtime's shells still write .zsh_history
Move actions into kernel/ui, add file refs and file server, SSH runtime support, runtime mismatch dialog, generated menu model, agent hook CI, and shared-workspace e2e fixtures.
Move the desktop UI under src/shells/desktop/ui, add the iOS app and its headless client core (src/shells/mobile, npm run build:mobile), move pairing join into client/workspaces, rework the runtime mismatch flow, and add shared-workspace e2e coverage (lifecycle, relations, startup, watching) plus nested-workspace and empty-sidebar specs.
…shimmer Port of #747 to the runtime architecture layout. The running-agent title shimmer animated background-position on clipped text, which can't be composited, so the window repainted every frame while any agent ran. Running is now a static dashed ring next to the title, and the waiting dot takes its colour from the theme instead of a hardcoded value.
File rows now take drops into their own folder, and that folder's row highlights while the cursor is over any of its files. The explorer only moves a drop to the root on the tree's empty space; anywhere else in the panel nothing drops. The explorer marks itself as a file drop host, so the dock's "Drop to open here" overlay no longer shows over it.
Port of 1bf1301 from main.
Every change on main since the branch point (b0dcf23) is already in the new layout here, so the tree stays as it is. Checked commit by commit: - Ported on this branch: #747 (static running indicator, 608541b) and 1bf1301 (shared-agent label removed from the editor toolbar). - Already present in the new layout: settings hooks page, skill sources and frontmatter, approval detection, agent hook CI and live tests, terminal and hook resource cleanup, provider sign-in, T3 blurry text and connected before first prompt, relation menu pan, Hermes on Node 22, the agent hook refactor (interrupts, sessions, cate agent read, agentId keying), editor, files, review, agent and browser panel fixes, stale PTY spawns, orphaned canvases, worktree drafts, Open in browser, the 2.0.5 review follow-ups, 2.0.5 release prep, dependency bumps and the skills index. - No longer apply to the new design: cross-window drop snapshots and detach transfers, saved-layout wording, dirty dots in saved titles, old-layout test fixes, and the explorer state saved across restarts (selection is client state). - Left out: the opt-in e2e specs panel-lifecycle-memory and session-soak-perf (with codex-hook-workload.mjs and profile-cate-lag.mjs), which use e2e helpers the refactor removed, the provider sign-in e2e fixture, and the live agentHookContracts suite.
… so far The iOS app gets an agents home across workspaces, a session view, a task composer, review and ship (commit, push, pull request, task apply/keep/ discard), keep-awake per computer and notifications, with a notification service extension that opens pushes sealed for the device. The runtime gets a push capability: a paired device registers an opaque delivery target and a key, and every notification event is sealed for it and handed to Cate Connect. Clients consume notification events through one shared consumer (attachAgentNotifications) on desktop and mobile. Tasks go through cate.codingAgent.*, where a client caller acts as the person. The iOS app is com.0ai.cate.ios on team F7PH54XQ6J.
Missions and cate.codingAgent.* are gone. cate.agent.start starts an agent's CLI in a new terminal panel or a T3 thread in a new chat panel, in the caller's checkout, an existing worktree or a new one, placed next to the calling panel or where the call says. cate.agent.types lists the agent CLIs, and cate.agent.interrupt stops a panel's turn with the CLI's own key or a T3 turn interrupt. The review panel starts its reviewers the same way. The runtime streams live conversations on a conversation channel (one watch per panel, read with the agent's state), and the client core gets watchAgentChat with prompts pending until the conversation shows them. The iOS app shows each agent as a chat: a new agent starts from an empty chat whose box picks a terminal agent or a T3 model and a worktree, and new panels and agents go where you are (PlacementSheet on a canvas, the dock otherwise). The agents home, session, task composer and ship views are removed. Signing moves to Signing.xcconfig, with Local.xcconfig for building on a personal team without push. A stopping daemon now releases its socket before its files and exits within 5 s; a starting daemon waits for the previous owner before removing a stale socket.
Anton-Horn
force-pushed
the
refactor/runtime-architecture
branch
from
October 7, 2026 08:49
2051f6d to
a9ee215
Compare
Cate Connect hands both sides short-lived TURN credentials with each session (the runtime in incoming, the client in opened), so ICE falls back to the relay when a direct path fails. The relay only carries the Noise channel. The runtime logs each session's path (direct or relay), bytes and duration. "Try same network" now only shows when no path works, relay included. The same-network listener serves only private addresses (RFC 1918, CGNAT, link-local, ULA, loopback) and closes any other connection before reading it; pairing links and mDNS carry only private addresses, so a cloud VM is reached through Cate Connect. Until a peer proves a paired key it may send frames of at most 16 KiB and must pair within 10 s; at most 32 such connections, 4 per address. Refusals name no version.
npm run lint failed with 2,029 errors from ios/build and the bundled core.js.
Five of six fixtures pass depcruise today (panel session.ts and definition.ts, runtime/push, src/cli, agents/runners are outside every rule); they are marked it.fails until R2.
The op counter restarted at 0 for each new document store and rpc client, so after a reopen in the same launch the runtime answered the new ops as duplicates (the test's op stayed pending: the rpc dedupe answered a stale outcome). The counter now belongs to the client identity; an evicted duplicate fails with the duplicate code, and a reply without a status is a failure.
With a recorded owner pid that is alive but not a daemon, two starts 100 ms apart both bound (the second unlinked the first's socket after its wait). The lock now asks ps first and does not wait on a non-daemon, probes again right before unlinking, and on EADDRINUSE returns running only when the socket answers. Dials waiting on a pending local start now share its failure instead of each starting again (three calls before, one now).
Two simultaneous starts with no socket both unlinked before binding; now they both try to bind and only the loser of the bind looks for a stale socket.
reapOrphanServers SIGKILLed a reused pid (a sleep with a different command). servers.json now records the command and start time ps reports for each child; a pid is reaped only when both still match. The file is written atomically through kernel/state.
close() marked the terminal dead before node-pty's exit callback, so exit observers never fired (the test's observer saw no exit) and the agents runner kept the terminal; a spawn that failed after the env contributors ran left the runner pointing at a terminal that never existed. One endTerminal now ends a terminal for an exit and a close, and a failed spawn (including the trust re-check) tells exit observers too.
The session host deleted sessions/<id>.json on removal, so undo re-added the panel with empty state (the test's review note was gone). The file is now set aside in sessions/removed/ and brought back when a panel with that id is added; removed entries older than a day are pruned at start. Retiring a store awaits its write in flight, which also closes the race where a late write's cleanup deleted the next session's file.
A client at seq 10 resuming on a runtime restarted from a document.json saved at seq 7 (and moved on to 10 with other ops) got no ops and kept a diverged mirror. The sequencer now has an epoch per start; a resume names the epoch of its seq, and another epoch gets the full document.
… key The path-hash runtime id was the network identity, so the first key to register an id on Cate Connect owned it forever: the same path on two machines, or a runtime whose secrets.json was lost (the test's restarted runtime was refused with key-mismatch), could never register again, and anyone who guessed a path could claim it. It also leaked the path hash on mDNS. Pairing re-pinned any key a code answered with (the test's pairing under an id its key does not derive succeeded). A runtime is now on the network under networkIdOf(its static key): Cate Connect, mDNS, pairing codes, pushes and paired clients use it; clients check that the key derives the id at pairing and on every connect, and a pin is never replaced by another key. The path id names only the data dir and socket. CONNECT_PROTOCOL is 2; the stand-in keeps no bindings.
The review session got the repository's raw git; only the vcs capability checked trust, so an untrusted workspace's review committed (the test's commit ran and failed only on the missing git identity). The repository runtime now hands out a git and gh whose every call needs trust, with writes in its queue, and its monitors run no git until trusted. The vcs capability and the review take that git as is; the capability's own checks and the review's write adapter are gone. The review passes an RpcError's code through instead of rewriting it to rejected.
B10 did not reproduce: the debounced write lands within the stop deadline, and a stop during an in-flight write keeps the newest document. The tests stay as guards.
A record still creating or removing after a daemon stopped midway was refused by every later operation (the test's half-made worktree stayed). Reconciling now finishes them inside the write queue: a checkout git lists becomes ready, a missing one is dropped, and a removal is completed.
B12 did not reproduce: the client's session subscription recovers once the restore finished. Adds a beforeStart test seam to serveWorkspace.
The settings mirror subscribed without resume and ignored the stream's end, so after a dropped connection it never saw another change (the test timed out waiting for B's edit). It now resumes, and starts over when the runtime ends the stream.
Every opened LAN connection is checked by the security handshake before it wins: one where another key answers is closed and the remaining addresses still count, then Cate Connect is tried. dialSameNetwork takes the check (and frees results that come in after another won).
openExternal refuses loopback URLs in main, where every caller passes (D10): such a page belongs to the runtime's machine and opens in a browser panel. The Windows pipe ACL is noted as not fixable through Node's net and untested.
…dline A session probe's until resolves on the session's own events (and the document's until the panel is known), within the test's budget, instead of polling against a ten second deadline a loaded machine overran.
Comments in runtime, contract and client-core code that described clients by kind (mobile, desktop) now say what they mean: a client, a shell's web view, Node. No runtime or core behaviour branched on a client kind.
isAgentFallbackTitle and agentPanelTitle move to the agents client, so iOS shows a terminal running an agent under the agent's name as the desktop does. Where an agent works (its session, its panel's worktree, the workspace root) is agentCheckout in the review client, and openAgentChanges finds it itself, so neither app resolves it.
openTrusted (client/workspaces) opens a workspace when it is not open, asks the trust question once its runtime answers and closes it again when declined; the desktop's navigation and the iOS core call it instead of each writing those steps.
… the UI The tab follower drops blank pages and turns load state into the reportLoad op (ending a followed load), so the desktop page host and the iOS core no longer wrap it the same way. The page host moves to panels/browser's desktop side; a client's local downloads become an instance created with the installed page bridge instead of module-level maps in the UI folder.
The chat page controller opens a page's links with openUrlFor itself; its port loses openLink. The iOS core installs a ClientUi whose openExternal goes to the app's system browser, so it no longer repeats the loopback rule.
createWorktree (a branch name from what was typed, the typed name as its label) and checkoutPullRequest move from a desktop React hook to workspace/repository/client; opening a pull request uses the latter.
The notification gate reads its settings slice from the client's settings store itself, so neither app maps the two keys by hand. Centring a new canvas panel on a point is originCentredOn in workspace/canvas, which the desktop toolbar drag and the iOS placement both use.
Creating them when the page bridge is installed subscribed to downloads on every bridge, including partial ones a settings page installs.
SSH serving becomes a machine folder flow (runtime/daemon/desktop/machine, MachineFolderDialog, machineIpc); pairings are kept as devices (devicesFile). Connection status, workspace list and join move logic into client/connections and client/workspaces. The Windows pipe name carries a per-user key. The iOS app gets place views, worktrees, a review panel and relations, with agent and T3 logos, backed by new mobile core modules. The architecture doc follows.
Anton-Horn
force-pushed
the
refactor/runtime-architecture
branch
from
October 9, 2026 21:37
0b45228 to
26725d1
Compare
A connection shares one session channel per panel and closes it a while (SESSION_LINGER_MS) after the last handle is released, so reopening a panel starts from its snapshot. The iOS core warms the sessions and buffers of panels likely to open next (panel.warm, buffer.warm), and buffers send versioned deltas instead of the whole text on every change. A terminal attach can ask for at most its own scrollback.
code.run and code.reset were addressed to the browser panel, but the client serves them only on the panel-less surface, so every run failed with "unknown page operation code.run" once a browser panel existed. They now go to the panel-less surface of the client that drives the target panel.
The T3 chat panel, services/t3, the t3 runner, the server host (runtime/server) and the T3 patches are gone, along with their e2e specs, iOS chat view and settings UI. The agents runtime keeps only the terminal runner (runners/terminal folded into agents/runtime), and cate agent start drops --runner, --instance and --model. Alongside: browser page driver and code session fixes, observation text moved into the browser contract, skills catalog in a contract, file search runner tests, and the docs, CLI skill and changelog follow.
* Fix `npm run dev` on Windows: skip bash electron patch there On Windows, `bash` in `predev` and `postinstall` resolves to WSL's bash.exe, which ran patch-electron-name.sh with Linux node. Its `node electron/install.js` installed the Linux Electron into the Windows node_modules (path.txt `electron`, Linux files in dist), so every `npm install` or `npm run dev` left an Electron that hard-crashed at startup (exit 0x80000003, no JS error). Run the script through a small node wrapper that skips it on win32. * Keep package.json non-executable --------- Co-authored-by: Anton <contact@cero-ai.com>
* Show the machine folder dialog above Settings "Open a folder" on a WSL distro or SSH machine is on the Remote machines settings page, an overlay at z-[100001]. The dialog is a Modal at the default z-[100000], so it opened behind Settings: it was mounted and had listed the folders, but could not be seen or clicked, and the page looked frozen. Give the dialog z-[100002] through Modal's zClassName. * Fix the machine folder dialog under StrictMode In dev, StrictMode mounts, unmounts and remounts the dialog. The unmount cleanup cancels the machine's running commands, which rejected the first ensureRuntime with "Cancelled." (logged by main as an IPC failure), and set `mounted` to false without the remount setting it back. The second check then finished but its result was dropped, leaving the dialog on "Checking the Cate runtime there" forever. Run the check in an effect that owns it: each run has its own `ignore` flag, and its cleanup sets it and cancels the machine's commands. A run cancelled by StrictMode or a close can no longer report its error or its result, and the run after the remount reports normally. "Try again" bumps an `attempt` counter in the effect's deps to run it again. `mounted` is reset on every mount for the folder calls that still read it. A regression test mounts the dialog under StrictMode with a setup that rejects on cancel.
Bump simple-git to 4.0.2 and allow the GIT_* variables the git host forwards on purpose, which v4 otherwise blocks. Override lodash-es, postcss-selector-parser and katex to patched versions, and pick up dompurify, sharp, source-map-js and http-cache-semantics fixes via npm audit fix.
Contributor
Author
already done something very similar locally. Moved the connected out of the settings into a modal accessible through the workspace overview sidebar. I'll push it later once i'm back home. I plan to merge the architechture rework pr soon (even though it probably still contains a few bugs/issues) and build a beta version so the dev process is more transparent and not hidden behind a huge pr. Appreciate the feedback, ideas and help on cate! |
- SSH machines: password/passphrase prompts via SSH_ASKPASS helper, shared ControlMaster connection, ~/.ssh/config hosts, runtime tarball streamed to the machine instead of install.sh - Desktop: Open dialog with workspace picker, Machines page replaces Remote machines, Share dialog, What's New, runtime install progress - Settings window split into Device and Workspace tabs - Agents: conversation steps (runtime, contract, iOS), composer updates - Terminal service and process scan cleanups
- A withdrawn SSH prompt no longer leaves its typed answer for the next machine's prompt. - Cancelling a machine's folder dialog withdraws only the prompts of the ssh it stops: each ssh owns its prompts and withdraws them on exit, instead of every prompt for the machine's label. - The runtime release downloads into a private temp directory. - ssh is not started when the file to stream is missing, and the file is let go once ssh exits. - A queued agent prompt the agent took is not sent again after a failed send; a slash command does not wait for a turn that may never start. - Agent steps go with their panel and are trimmed in chunks, so watchers do not get all of them on every step. - An install that ended before the installs snapshot stays ended. - Point two tests at the renamed settings sections.
The sidebar lists the workspaces open on this device, across restarts, until they are closed. Closing disconnects a workspace and drops it from the list; a paired one also loses its pinned key and asks first. Remove from Sidebar and Forget become Close Workspace, the Open picker loses its Recent section, and the welcome page becomes two buttons and what is new. - Declining trust only disconnects a workspace: it stays in the list and a paired one keeps its key. - Closing several workspaces at once asks once and shows the nearest workspace left. - A new workspace joins at the end of a list whose file had no order. - One close remedy replaces remove and forget.
…itecture # Conflicts: # package-lock.json # package.json # src/services/agents/runtime/live/hookLifecycle.itest.ts # src/workspace/repository/runtime/git.fetch.test.ts # src/workspace/repository/runtime/git.remotes.test.ts
The channel's snapshot is null once it ends at teardown; the listener read it unchecked and left an unhandled rejection that failed the run.
- Terminal link detection moves to services/terminal (contract + runtime); the desktop view keeps only an xterm link provider, and opening links goes through client/host and the desktop app links module. - iOS agent chat gets a shared chat timeline in the mobile core and a ChatPresentation layer in the app. - Loopback proxy, cate serve, pairing and network dialer fixes, with transport failures classified in one place. - Keep-awake button, canvas toolbar and agent spawn flyout updates. - docs/architecture.md and CLAUDE.md brought in line with the code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Implements docs/architecture.md (plan and progress in docs/refactor-plan.md). Each workspace now runs in its own runtime daemon that holds its state and does its work; the desktop app is just a client of it over the runtime protocol. Other devices can pair and connect to the same runtime (same network or Cate Connect).
Structure
src/is organised by layer (kernel, runtime, workspace, services, client, panels, shells) and by side (contract, runtime, node, client, desktop).npm run lint:depschecks the rules on every file, with fixtures.src/shells/desktop/; shared code asks for client features, never a client kind.Runtime and networking
iOS app
New SwiftUI app in
ios/running the client core headless: workspaces, places, terminals, agent chats with steps and a shared chat timeline.Agents
cate.agent.startreplaces missions; agents read as chats on both clients.Fixes along the way
Two daemons serving one workspace, dropped ops after reopen, a crashed runtime rewinding the document sequence, closes dropping unsaved edits, undoing a close losing session state, revoking trust leaving processes running, the review running git without the trust gate, worktrees left half created by a crash, watcher errors ending every subscriber, late watcher echoes overwriting newer edits, daemon.out.log growing forever, the server reaper killing unrelated processes, and more (see commits).
Also:
\x00json:rows came back empty. Now read as BLOB.npm run devexposes CDP on 9222 and works on Windows.Checked on Node 22: typecheck, lint, lint:deps, build, unit tests, electron smoke, full e2e (146 passed, 5 skipped). Not rerun after the latest commit.
The Cate Connect service lives in its own repo (0-AI-UG/cate-connect).