feat(nodes): a device can belong to several workspaces at once - #614
Merged
Conversation
Onboarding now opens on a choice, defaulting to the short path: paste the 8-character code from the workspace's Connect Agent → Nodes view and this machine joins as a node — no agent, no keys, two steps. The old path (pick an agent, configure it, create an instance) is the other choice, unchanged. The Workspaces page grows the same entry: quick-connect's pairing tab replaces the "browser sign-in" tab, which only opened the site and told you to come back and paste, and its two header buttons — which opened the same dialog — collapse into one. Pairing is device-level, and the server models a node per (workspace, device), so a machine may belong to several workspaces. The daemon can't: node.json holds one identity and heartbeats one workspace, so a second pairing silently takes the device away from the first, which then goes offline with nothing to explain it. Fixing that properly needs a core release; until then the launcher stops being silent about it — node.json keeps a pairing history, the pair screens warn before and report after, and a workspace this device has moved away from says so on its card instead of reading as "disconnected". Unpairing happens on the other side and reaches this machine through no channel at all, so the launcher now asks: the node list is checked (throttled, and only a successful listing counts as evidence) and a pairing the workspace has forgotten is dropped locally. Two entry points that mixed local and everyone-facing acts are split: - Removing a workspace deleted it for every member. Local removal is now the default; deleting the real workspace is a separate checkbox with its own wording, button and warning. - Renaming only ever wrote a local alias. "Rename the workspace itself" is now offered explicitly, warned about, and off by default. Also fixed along the way: - Updates of npm agents pinned to a version in the registry (pi@0.83.0) reinstalled that same version, so "Update to v0.84.1" never moved and the badge never cleared. Installs and updates both target @latest now, and the confirm dialog prints what actually runs. - Agent rows printed lastError inline, breaking the fixed row height and squeezing every other column; the message moves into a dialog behind an icon and the card view gains the same affordance. - The agent list used one word, "connect", for three different facts. Split into workspace membership, credential setup and sign-in method. - The card grid dropped pagination (no page size divides both 3 and 4 columns) and its duplicate total.
agent-manager.ts had grown to 5683 lines and owned everything the main process knew about agents: auth specs, install/rollback, CLI sign-in probing, daemon lifecycle, log filtering, npm registry lookups and the whole workspace chat stack. It is now a ~2250-line facade. Every public method keeps its signature — the IPC layer in index.ts calls them directly — and delegates to modules that own one concern each: agents/paths, runtime, auth-specs, env-normalize, npm-registry, daemon-process, daemon-logs, llm-test agents/health (HealthResolver), install-service (InstallService), login-probe (LoginProbe) chat/types, messages, sessions, service (ChatService) The four service classes take dependency-injection objects instead of reaching back into AgentManager, wired up in its constructor. Two of those wires carry behavior that has no compile-time guard, so they are worth knowing about: LoginProbe's onSettled must invalidate both the health map and the agents cache (otherwise the Agents list goes stale after a sign-in probe lands), and ChatService's emit must forward on the "chat-event" channel the renderer listens to. Pure move, no behavior change: every extracted function and constant was diffed against the pre-split copy — 48 byte-identical, 3 differing only in prettier line wrapping.
…ndex.ts
index.ts was 3494 lines of everything the main process does. The six
concerns below had no reason to live next to the IPC layer, so they now
have files of their own and index.ts is down to 2689 lines:
bootstrap/startup-log startup log rotation, fatal-error reporting
bootstrap/node-runtime Node/npm download, checksum, extract, PATH fix
install-progress install phase inference + user-facing errors
window-chrome theme source, titlebar overlay, tray icon, splash
net-config download region, npm registry probe, proxy
web-security external-link policy, window navigation guards
Three things were deliberately left where they were:
- The `process.on("uncaughtException"/"unhandledRejection")` registration
stays at module scope in index.ts. Its whole point is covering the gap
between `require` and `whenReady`; moving it into the module would have
changed when it takes effect. Only the handler function moved.
- The pre-ready sequence (setName → appendSwitch →
disableHardwareAcceleration → globalPaths.push) is untouched, and none
of the new modules run anything at import time.
- InstallProgress takes `() => mainWindow` rather than the window itself.
mainWindow is reassigned when the window is closed and reopened from
the tray, so a captured reference would leave install progress silently
broken after the first reopen.
Also removes dead imports: crypto/pipeline/Transform were unused before
this change, and `dialog` only looked unused because three call sites
shadowed it with a local require("electron") — those requires are gone
instead, so the top-level import is what's used.
Verified as a pure move: the 124 IPC channels and AgentManager's 79
public methods are identical to before, every extracted function was
diffed against its original, the bundle still contains all of it, and a
throwaway smoke test exercised the wiring at runtime. tsc and the test
suite match the pre-refactor baseline (the 2 download.test.ts network
timeouts were already failing).
A node reported ALL locally-configured agents in its heartbeat and would execute agent commands by name regardless of ownership. So connecting a node to a new workspace exposed agents that belong to a *previous* workspace (and let the new one start/stop/remove them) — each agent is meant to be bound to a single workspace. _buildRoster(node) now reports only agents whose network matches the node's connected workspace, and _runNodeCommand refuses start/stop/ remove/configure on any agent not bound to it. A missing network never matches, so local-only agents don't leak either. Adds regression + isolation tests.
Selecting/switching an agent type in the node Add-agent form ran setName(type), silently reverting a name the user had typed (e.g. "claudecbd" -> "claude"). Track whether the name was edited and only seed it from the type while untouched.
Node roster/command workspace isolation (security) + agent name-clobber fix.
When an owner unpairs a device from the workspace, nothing told this
machine: the daemon heartbeat swallowed the error, node.json kept its
record, and the launcher went on claiming a membership that no longer
existed (onboarding showed "This device is connected" long after the
node was deleted).
The heartbeat is the only component that continuously learns the node is
gone — a 404 every 10s. Now a definitive 404 ("Node not found" /
"Workspace not found") clears the active pairing locally: the device key
is kept so a future re-pair reuses the same id, but the dead workspace
binding + token are dropped. Transient failures (timeouts, 5xx, auth
blips) are still swallowed and retried, so a wifi flicker never unpairs
a device.
- workspace-client: attach err.status so callers can tell a definitive
rejection from a transient one.
- node-config: add clearActivePairing(), schema-compatible with the
launcher's writer (node-pairing.ts).
- daemon: clear the pairing + drop the stale client on heartbeat 404.
Tests: node-config clearActivePairing (drop active, keep key/history,
no-op when unpaired); daemon _nodeHeartbeat clears on 404 and retains on
a transient failure.
The server-side Node row is keyed per (workspace, device), so one machine
legitimately holds a membership in every workspace it paired with — but the
launcher wrote and read a single active pairing. Redeeming a second code
silently took the device away from the first workspace, which then showed it
as offline, and the UI had to explain the displacement instead of just
letting both memberships stand.
node-pairing.ts now maintains the pairing set: recordPairing() adds or
refreshes one entry without disturbing the others, clearPairing(workspaceId)
drops exactly one, and listPairings() reconstructs the list for records
written before it existed. The top-level fields stay as a mirror of
pairings[0] — that is what a daemon predating multi-pairing reads.
NodeStatus grows `workspaces: NodeConnection[]` as the real answer, with the
singular fields describing the most recent pairing; refreshNodeStatus checks
every pairing against its own workspace concurrently, so one workspace being
unreachable no longer says anything about the others, and only the workspace
that has actually forgotten this node loses its pairing. connectNode is
additive and no longer reports a `replaced` workspace, so the renderer drops
the "moved to another workspace" copy and its strings.
Requires the matching core release: bundled @openagents-org/agent-launcher
goes to 0.2.166, whose daemon heartbeats every pairing with a per-workspace
client and roster.
Also in this release:
- Windows: stop a blank console window popping up every couple of minutes.
An Electron main process owns no console, so a piped child launched
without CREATE_NO_WINDOW gets a fresh one allocated by the OS. main/
win-console.ts defaults `windowsHide` process-wide (first import, so the
in-process core is covered even when an older copy is installed under
~/.openagents), the remaining call sites pass it explicitly, and the CLI
sign-in terminal opts out with an explicit `windowsHide: false`.
- Configure dialog: hide "Test connection" under the CLI sign-in tab, where
there is no key in the fields for it to test.
- Install detail: the save / test buttons split the row evenly instead of
huddling at the left edge.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The core side of multi-workspace pairing landed on develop as its own release (0.2.166, #613), so the four agent-connector files conflicted with the copies this branch still carried. develop's version is the one that shipped — it holds the same pairing work plus the 0.2.165 Windows console fix — so it wins outright here and packages/agent-connector on this branch is now identical to develop.
Ashin-LX-98
force-pushed
the
feat/launcher-node-pairing
branch
from
August 13, 2026 08:34
0258f8f to
526068c
Compare
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.
Why
The server-side Node row is keyed per (workspace, device), so one machine legitimately holds a membership in every workspace it paired with — but the launcher wrote and read a single active pairing. Redeeming a second code silently took the device away from the first workspace, which then showed it as offline, and the UI had to explain the displacement ("this device moved to X") instead of just letting both memberships stand.
Both sides are additive now: the device keeps every pairing, and the daemon heartbeats all of them.
What changed
node-pairing.tsmaintains the pairing setrecordPairing()adds or refreshes one entry without disturbing the othersclearPairing(workspaceId)drops exactly onelistPairings()reconstructs the list for records written before it existedpairings[0]— that is what a daemon predating multi-pairing readsagent-manager.tsNodeStatusgrowsworkspaces: NodeConnection[]as the real answer; the singular fields describe the most recent pairingrefreshNodeStatus()checks every pairing against its own workspace concurrently, so one workspace being unreachable says nothing about the others, and only the workspace that has actually forgotten this node loses its pairingconnectNode()is additive and no longer reports areplacedworkspaceRenderer
workspaces[]Dependency:
@openagents-org/agent-launcher0.2.165 → 0.2.166 (#613 — the daemon heartbeats every pairing with a per-workspace client and roster).Also in this release
CREATE_NO_WINDOWgets a fresh one allocated by the OS.main/win-console.tsdefaultswindowsHideprocess-wide (first import, so the in-process core is covered even when an older copy is installed under~/.openagents), the remaining call sites pass it explicitly, and the CLI sign-in terminal opts out with an explicitwindowsHide: false.Note on the merge commit
The core half shipped separately as 0.2.166 (#613), so the four
packages/agent-connectorfiles conflicted with the copies this branch still carried. develop's version — the one that actually shipped, holding the same pairing work plus the 0.2.165 Windows console fix — wins outright, andpackages/agent-connectoron this branch is now identical to develop.Verification
typecheck, 241 launcher tests, 933 core tests (the CI command, mini excluded), and the production build all pass.
🤖 Generated with Claude Code