Skip to content

feat(nodes): a device can belong to several workspaces at once - #614

Merged
Ashin-LX-98 merged 12 commits into
developfrom
feat/launcher-node-pairing
Aug 13, 2026
Merged

feat(nodes): a device can belong to several workspaces at once#614
Ashin-LX-98 merged 12 commits into
developfrom
feat/launcher-node-pairing

Conversation

@Ashin-LX-98

@Ashin-LX-98 Ashin-LX-98 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

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.ts maintains the pairing set

  • recordPairing() adds or refreshes one entry without disturbing the others
  • clearPairing(workspaceId) drops exactly one
  • 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

agent-manager.ts

  • NodeStatus grows workspaces: NodeConnection[] as the real answer; the singular fields describe the most recent pairing
  • refreshNodeStatus() 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 pairing
  • connectNode() is additive and no longer reports a replaced workspace

Renderer

  • Drops the "moved to another workspace" copy and its en/zh strings
  • The workspaces page renders from workspaces[]

Dependency: @openagents-org/agent-launcher 0.2.165 → 0.2.166 (#613 — the daemon heartbeats every pairing with a per-workspace client and roster).

Also in this release

  • Windows: no more 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 — there is no key in those fields for it to test, so it would report on whatever was saved before.
  • Install detail: the save / test buttons split the row evenly instead of huddling at the left edge.

Note on the merge commit

The core half shipped separately as 0.2.166 (#613), so the four packages/agent-connector files 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, and packages/agent-connector on 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

Ashin-LX-98 and others added 11 commits August 12, 2026 21:38
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.
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openagents-workspace Ready Ready Preview Aug 13, 2026 8:35am

Request Review

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 Ashin-LX-98 changed the title feat(nodes): 一台设备可以同时属于多个 workspace feat(nodes): a device can belong to several workspaces at once Aug 13, 2026
@Ashin-LX-98
Ashin-LX-98 force-pushed the feat/launcher-node-pairing branch from 0258f8f to 526068c Compare August 13, 2026 08:34
@Ashin-LX-98
Ashin-LX-98 merged commit 7061eb8 into develop Aug 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants