Skip to content

feat: add Chief-aware dual-VM workspace - #1

Merged
lightcloud00 merged 80 commits into
mainfrom
codex/chief-dual-vm-20260822
Aug 22, 2026
Merged

feat: add Chief-aware dual-VM workspace#1
lightcloud00 merged 80 commits into
mainfrom
codex/chief-dual-vm-20260822

Conversation

@lightcloud00

Copy link
Copy Markdown
Owner

Owner-fork integration mirror for upstream PR milind-soni#365.\n\nExact head: 10efdb9\n\nVerified before publication: macOS, Ubuntu, and Windows typecheck/tests; Ubuntu package smoke; Swift/iOS; focused Chief/status/control/workspace/Hermes suites; Node 24 typecheck and packaged-server checks.\n\nThis merge records clean source in the owner fork. Upstream acceptance, signed installation, host restart, VM restoration, and live canaries remain separate facts.

aivsomkar and others added 30 commits August 18, 2026 10:30
…ss per session

Verified against claude 2.1.221 with --input-format stream-json: the CLI
settles a turn with `result` while stdin stays OPEN (EOF is the exit
signal, not the turn signal); the next user message on the same stdin is
a new turn in the same process; a message that arrives MID-turn is
delivered before the model's next call and folded into the same turn's
one result. That last behaviour is exactly the "steer" the plan wanted.

- claude.ts keeps one live process per thread across turns: reused while
  idle, unchanged in spawn contract, and the session the harness wants;
  otherwise closed and respawned with --resume. `result` settles the
  turn, not the process; the process closes after 10 minutes idle
  (OMB_CLAUDE_SESSION_IDLE_MS). steer() writes into the open stdin.
- contract: capabilities.queueing and an optional adapter.steer() — the
  one-file driver promise holds; every other driver keeps the 409.
- harness: POST /messages while busy on a queueing engine steers instead
  of 409ing; the message is appended in order and marked `steered`. The
  composer stays open on such engines ("Enter sends this into the running
  turn"); a "sent mid-turn" tag on the bubble says the model saw it.
- 3.1 remainder: injected local models carry contextWindow from Ollama's
  /api/ps context_length when the model is running, so a small model's
  rebuild is sized to what it can hold instead of a name-based guess.
- fake claude rewritten line-driven (steer folding, `slow` mode).

Items 3.2 (and the 3.1 remainder) of docs/plans/agent-harness-upgrades-v2.md.
Answers the plan's open question 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Analytics stay on by default — the counts keep working — but there is now a
switch in Settings → General, and the choice survives restarts.

The guard sits before init(), not just around capture(): an install that
opted out never calls posthog.init(), so nothing leaves the machine, not even
the library's own bootstrap request. Flipping it while the app is running
takes effect immediately through opt_out_capturing(), which also drops what
is already queued. identifyEmail() carries a second, explicit check, because
it is the one call that would send a personal identifier; the address is
still stored locally in the profile either way — opting out stops it being
reported, not being used.

Why bother, when autocapture is already off and the event list is short: the
people who worry about this cannot read the source to find that out. Naming
what is sent, next to a switch, is what makes the existing restraint legible
— and docs/ios-privacy.md already promises exactly this posture for the
companion app, so the desktop side now matches.

optAction() carries the decision as a plain function, so the four cases are
checked without standing up a client to observe (no module mocking, per the
anti-slop rule). 9 tests, including the one that matters: opting out before
init must not reach PostHog to tell it so.
`pnpm check:contrast` parses src/styles.css and measures 21 pairs. It reads
the stylesheet rather than keeping a second copy of the values, so it cannot
pass against a palette that is no longer the shipped one.

Two things it does that reading the hex values does not:

- It composites alpha. --color-ink-secondary is #fcfcfc99, and measuring it
  as opaque overstates every secondary-text pair in the app by a wide margin
  (2.86:1 vs the 15:1 the raw value suggests on --color-app).
- It measures white on filled surfaces, because that is what the components
  render — `bg-accent … text-white` — rather than the token against the page.

Three pairs sit below AA today. They are listed in KNOWN with their call
sites, so this lands without changing a colour in the same commit and fails
only on something new:

  white on accent  3.65:1  every primary button, 12-13px (28 sites)
  white on danger  3.10:1  the hang-up buttons, 14px
  accent on card   4.15:1  accent links inside a Card (11.5-12px)

The shape matters more than the numbers: --color-accent is fine as text on
the page ground (5.33:1 on --color-app) and only falls short on the lighter
card, while white falls short ON the accent. Darkening the token fixes the
buttons and hurts the links, so the fix is a separate fill colour rather than
a nudge. That is a design call and yours to make — this only measures, and
deleting a line from KNOWN is how a fix gets locked in.

Verified both ways: exit 0 as shipped; drop --color-ink-secondary to #fcfcfc55
and it exits 1 naming all five surfaces.
With the Computer or Inspector panel open the chat column drops to
~700px and the header row — Stop, + Task, usage, working folder, model,
icons — wrapped onto three lines and crushed the bot avatar and name to
nothing. The header is now a CSS container (@container/chathead); below
4xl each chip folds to an icon-only shape and the right group stops
shrinking so the name truncates instead:

- Stop → round bubble with the square
- + Task → round bubble with the plus (count-only bubble once there are
  several tasks)
- usage → one short figure: cost when known, else tokens
- working folder → rounded square with the folder icon
- model → rounded square with the provider mark

Full labels still ride the tooltips. Wide headers are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- parseTokens strips CSS comments first. A declaration left in a comment
  reads exactly like a live one, so a removed-but-still-mentioned token was
  measured at its stale value instead of reported as undefined.
- KNOWN is a Map of ratios rather than a Set of names. It was a licence, not
  a floor: white-on-accent could have slid from 3.65:1 to 2:1 and the run
  stayed green. A carried pair that gets worse now fails like anything else,
  with 0.01 of rounding headroom since the floors are quoted to two decimals.
- A known pair that climbs past AA is announced so the line gets deleted,
  rather than sitting in KNOWN shielding a pair that no longer needs it.
- The header said two pairs; there are three.

Verified all three ways: accent → #0a5db3 fails with "WORSE than the recorded
4.15:1"; accent → #0d5aa8 prints "now measures 6.90:1 — remove it from KNOWN";
commenting out --color-warning fails with "undefined token" instead of quietly
measuring the dead value.
…real

Both findings were right.

An opt-out was only as durable as the write that persisted it. With storage
rejecting the write, the setter swallowed the error, the next read found
nothing and answered "enabled", and a later initAnalytics() would start the
client the user had just switched off — the one failure this feature exists
to prevent. The choice now lives in module state, set before the write is
attempted; storage is how it survives a restart, not where it lives.

The cold-start test also proved nothing: an earlier test had already set the
module-scoped `ready` flag, so initAnalytics() returned on that rather than on
the opt-out. It now loads the module fresh — resetModules, not module mocking:
nothing is replaced, the real module is simply loaded again.

Verified by removing the `!analyticsEnabled()` guard from initAnalytics: the
test fails. It did not before.

10 tests, full suite green.
Kimi ACP session/new checks default_model, not -m. A missing or expired
login then becomes "Authentication required" even when the picker is a
local host. Overlay Kimi's official KIMI_MODEL_* env on inject turns so
the child has an in-memory default, and write protocol plus
max_context_size on the on-disk alias so 0.36+ will bind it.
Aliases written before this PR were left as-is, so Kimi 0.36+ skipped
default-model binding. Fill in protocol and max_context_size when they
are missing, and leave any values the user already set. The applyTurnEnv
test now checks both the resolved model and the picker id.
Line-based heading and key checks missed quoted keys, headings with
comments, and bracket lines inside multiline strings. Walk the file
outside of strings so existing aliases are patched once, and document
the helpers the coverage check was counting.
Droid ACP session/new requires a Factory login or FACTORY_API_KEY even
when the picker is a BYOK custom host. The CLI only checks that the
variable is set, then uses the custom row's own key. On a local inject
turn, fill a placeholder if the user has no Factory key. Cloud models
are unchanged.
A usage chip on first paint called toFixed on undefined for bots.json
rows written before cost tracking. The packaged window rendered black.
Current Studio stores keys as servers[url].minted instead of a top-level
api_key. Without that, /v1/models returns 401 and Custom never lists
Unsloth models.
Skip the Droid FACTORY_API_KEY placeholder when a Factory auth file
already exists. Prefer localhost minted Unsloth tokens over a stale
top-level api_key. Treat NaN/Infinity costs as missing in the chip
and settings. Trim whitespace around dotted TOML headings and ignore
""" inside comments or single-line strings.
Skip # comments in tomlTables so an apostrophe in a comment cannot
open a phantom string and hide the real model heading. Treat [[array]]
headings as table boundaries without patching them, so protocol keys
land in the model table instead of the following hooks array.
Upstream blocked Auto while a bot was on the local computer. On macOS
the user can now confirm a warning and let the bot click and type here;
destructive and sensitive actions still stop. CUA can fall back to the
standalone CuaDriver.app so existing Accessibility grants keep working.

Also: Claude turns rewrite leftover Custom slugs onto a live local host
so the picker does not demand /login when Unsloth is already serving
the model.
\u0035 in a quoted table key is 5, not the letters u0035, so an existing
[models."omlx/GLM-\u0035.2-fp8"] matches the inject alias and is patched
instead of duplicating the table.
\u0035 in a quoted table key is 5, not the letters u0035, so an existing
[models."omlx/GLM-\u0035.2-fp8"] matches the inject alias and is patched
instead of duplicating the table.
- Destroy a failed embedded CUA host before falling back to standalone
- Refuse a one-arch CUA stage unless PARTIAL=1, matching dual-arch packaging
- Open System Settings and Retry are separate; retry after the window refocuses
- Sort usage by finite cost only (missing/NaN/Infinity last)
- Reset TOML string mode at newlines so a stray quote cannot hide later tables

Unclassified GUI clicks still auto-approve when Auto is on after the warning;
default-denying every click would restore the ban this PR removes. Destructive
and sensitive actions still stop.
…, one-shot retry.

Reject unknown/malformed/surrogate unicode escapes so they cannot canonicalize
to another alias. Empty OPENMAUSBOT_CUA_ARCHES throws even with PARTIAL=1.
Settings-return retry runs at most once if focus and visibility both fire.
`POST /api/teams/import?mode=project` adds the team and opens a room for it,
optionally on a folder: `&cwd=<path>`, `&room=<name>`. Without it, setting up
a project is three steps — import the team, create a room, pick its members —
and the third one is tedious once a team has more than a few people.

**The manifest still describes only people.** Room name and folder come from
the caller, never from the file. That is deliberate and preserves what v2
established when it dropped its `room` block: a manifest fetched from the
library must not be able to create structure in someone's workspace. As a
parameter, a local caller gets the one-step setup without opening the format
to a remote one. It reads the same way as the neighbouring guards —
`seedMessages: false`, `composio: false` — a shared team brings people, not
reach.

Details worth knowing:

- The folder goes to `cwd`, not `pinnedCwd`. It is what the room WANTS; the
  store pins it on the first turn, which is its call to make, not ours.
- The path is validated with the same `validateBotCwd` a bot's folder uses,
  before anything is created — a bad path is a 400 with no bots and no room
  left behind.
- The room is created last, so any failure above leaves nothing pointing at
  half-built state.
- `add` and `replace` are untouched: they still return no `group`, and the
  existing test asserting that still passes.

Verified by disabling the room creation: the new test fails. Full suite green.
The download+extract step in scripts/prepare-android-tools.mjs assumed a
native Windows shell where tar is bsdtar (zip-capable). On git-bash it is
GNU tar, which (a) reads the C: drive prefix in the -C path as a remote
host ('Cannot connect to C:') and (b) cannot read .zip at all. That broke
pnpm package:win out of the box on Windows.

- Prefer unzip, fall back to tar on Windows (covers git-bash AND native
  cmd/PowerShell).
- Normalize absolute Windows paths to MSYS/POSIX form so git-bash's tar
  -C never treats the drive letter as a host.

Verified: pnpm build:android-tools now stages adb.exe via the real
download+extract path; pnpm typecheck passes.
settle() closed the permission broker and forgot the turn, but never
killed the spawned claude -p --resume child. A one-shot process is
expected to exit right after printing result, but a backgrounded MCP
grandchild can keep it alive with a live broker connection.

Adds killCliTree(child) to settle(), unconditionally, on every
terminal path. A no-op when the process already exited.

Adds a result-then-hang fake-CLI mode (prints result but never exits)
to reproduce and test the leak, and exposes the fake CLI's pid via
the existing FAKE_CLAUDE_DUMP mechanism so the test can confirm the
process is actually gone, not just that turn.completed fired.

Part of milind-soni#211
net.Server.close() only stops accepting new connections — it does not
touch a connection that's already open. A still-alive child's MCP
proxy could keep sending asks on such a connection after the turn
ended, and the connection's data handler stayed fully wired to it,
adding new pending entries and emitting request.opened cards for a
turn the driver had already forgotten (active.delete(threadId)
already ran). That card could then never be answered.

Adds a closed flag set at the top of close(); any ask received while
closed is always replied to directly on the connection (mirroring
close()'s existing pending-ask handling) instead of registering a new
pending entry or notifying onAsk — never a silent drop, since
permission-proxy.ts's MCP tool call only resolves on an explicit
answer or the connection's own error/close.

Fixes milind-soni#211
… late-ask path

The late-ask reply and close()'s pending-drain loop derived the same
kind -> {behavior, message} mapping independently in two places.
systemEndedReply(kind) branches on question vs permission, but only
the permission/deny arm was exercised by the existing late-ask test.
Keep closed-broker handling terminal ahead of active-turn duplicate ask checks, preserve exact late replies, and settle in broker-close/process-kill/cleanup order.\n\nFixes behavior from milind-soni#211 while retaining the original milind-soni#229 author commits.
pi (@earendil-works/pi-coding-agent) exposes a JSON-RPC mode over stdio
(`pi --mode rpc --no-session`) rather than ACP, so — like the Claude Code
and Codex CLIs — it gets a native driver that speaks its own protocol and
emits canonical RuntimeEvents. pi is a BYOK agent: credentials live in
~/.pi/agent/auth.json and are injected by the pi binary, so the driver
holds no API key and needs no sign-in.

- server/drivers/pi.ts: native ProviderDriver. Per-turn it spawns
  `pi --mode rpc --no-session`, resumes the prior session via
  switch_session (the sessionFile from session.started is the resumeCursor),
  pins the chosen model with set_model (splitting the picker's
  provider/modelId composite), and translates the RPC event stream
  (message_update text/thinking deltas, tool_execution_*, turn_end) into
  canonical events. toolUse turns are not settled — pi auto-continues to
  synthesize the reply, and settling early drops it. extension_ui_request
  select/confirm/input become request.opened; respondToRequest answers via
  extension_ui_response. A missing CLI surfaces as snapshot unavailable.
- server/drivers/pi.test.ts + server/testing/fake-pi-cli.ts: contract tests
  against a scripted fake pi CLI — catalog parsing, the full happy turn,
  the toolUse auto-continue, permission brokering, interrupt, and snapshot.
- builtIn.ts registers PiDriver; config.ts seeds pi into DEFAULT_FLEET and
  CUSTOM_ONLY so the engine instance appears (and merges into existing
  configs via PRODUCT_FLEET_ADDITIONS).

`pnpm typecheck` and `pnpm test` pass; the live catalog is flagged
`custom` so the model picker's Local pane lists pi's BYOK models.
milind-soni and others added 27 commits August 21, 2026 16:48
…-header

Fold chat header chips to icon bubbles when the column is narrow
# Conflicts:
#	server/drivers/acp/kimi.ts
#	server/drivers/local-inject.test.ts
…local-vm

feat(local-vm): support isolated per-bot desktops
* fix(ios): follow active tasks and approval choices

* fix(ios): avoid duplicate standing grants
* fix(codex): preserve full shell commands

* test(codex): cover full approval commands
…ost cards

The warning dialog was the only thing between a bot and Auto mode on the
user's real Mac — and it lives in the renderer, so a blind PATCH (a bot
curling the loopback API from a tool call, a script, a stale client)
could create the grant unwarned. The PATCH route now refuses to combine
autoApprove with the local computer unless the request carries the
dialog's acknowledgeLocalAuto flag; the flag is never persisted, and the
guard test now proves the unwarned path is refused in both directions.

Codex request.opened cards now stamp approvalScope local-computer when
the turn mounts this Mac (mirroring claude.ts and acp/core.ts), so the
harness's local-computer-block backstop applies to remembered
always-allows for Codex bots too; a Local VM mount stays unscoped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…computer-auto

Allow Auto mode on this Mac's computer (after a warning)
chore: measure the palette against WCAG AA
- supportsMultiAccount / multiAccountConfigured gate on enable only: a
  recreated Session posts the same config and gets the same echo back, so
  strict equality on the cap and selection flags could only manufacture a
  recreate-per-request loop (config.json rewrite locally, D1 write in the
  Worker). A once-per-boot upgrade set backstops even the enable-missing
  case: if the fresh Session still is not multi-account, run with it.
- Broker pagination drops to 20 pages per sweep: two back-to-back sweeps
  stay under the Workers free-plan 50-subrequest cap.
- authorize (server + broker) and broker connectionStatus tolerate a
  denied account listing the way every inventory path already does; the
  alias guardrails degrade to first-account behavior instead of failing
  all authorization for scoped keys. Account DELETE keeps failing closed.
- connectionStatus synthesizes the Session-selected account exactly like
  allServiceStates, so a status poll can no longer wipe the account row
  the inventory rendered under a scoped key.
- The connected-tab no-auth card reads Included (disabled) instead of
  offering a pointless Connect.
- The paired phone keeps account inventory and connect, but revocation
  stays on the Mac: the sidecar account-DELETE allowlist entry is out
  (its iOS UI rides with the profile stack it was built on).
- New test fixture pins the generation guard in
  mergeCompleteConnectorStatus (mutation-checked: deleting the guard
  fails it); connectionStatus tests updated for the synthesis rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ti-account

feat(composio): manage multiple connected accounts
…steering

Let a message reach a running Claude turn (steer), one process per session
… copy

Keep-both resolutions preserve main's localVm alongside imageGen in both
the saveConfig section list and the reloadKeys filter, and merge the
sidecar allowlists (profiles' attachments/tts entries + main's connector
entries). Adds the paired bot-profile.test.ts the convention asks for —
the strict boundary now has named refusals for every privilege-bearing
bot field. storedAvatarExists stats the file instead of reading up to
10MB of pixels, and the no-voice-key error points at Settings, which is
where the key actually lives until the desktop profile rail ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sent flag

The queue is created in useMemo but disposed by the effect cleanup, and
StrictMode's dev probe runs that cleanup once against the same memoized
instance — every profile edit in development silently stopped saving.
revive() undoes the probe's dispose; a test pins dispose - revive -
enqueue still sending.

The milind-soni#315 consent flag (acknowledgeLocalAuto) now rides BotUpdatePatch:
it reaches the wire inside the coalesced PATCH body, and one strip point
(stateOverlay) keeps it out of overlayFor and both onAuthoritative
folds, so consent proof can never leak into renderer bot state. Test
covers coalesced-body delivery + overlay absence.

Also: keep-both resolutions (localVm + imageGen in ConfigStatusFrame,
merged SettingsPanel imports), dead density ternary removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-contract-ios

feat(profile): add paired-safe agent profiles and avatars
…rtial text

A room's approval/question notification carries the asker bot with the
GROUP's thread id, so the exact-task click path asked the bot to switch
to a thread it does not own — a 404 error banner on desktop and, on the
phone, an error with no navigation at all. Clicking now opens the room
itself; a thread that is neither a room nor one of the bot's own lands
on a plain bot select, and the phone tolerates a vanished task the same
way.

The duplicate-done suppression was guarded by a test whose fixture
crashed before streaming anything — with an empty reply the pre-existing
quiet-done rule suppressed the duplicate on its own, so the guard could
be deleted without failing anything. The new fail-after-text fake mode
streams half an answer and then fails the turn: a non-empty reply makes
the routine-failed/done dedup the only thing standing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-desktop

feat(profile): add persistent desktop profiles and avatar roster
…es-notifications

feat(routines): clarify scheduled tasks and exact notifications
Adds a comprehensive Fumadocs site, aligns it with openmausbot.com, includes clean product screenshots, redirects the root to the docs reader, and documents current shipped behavior.
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3346f140-b830-4f1e-bf6e-5e9ba23e4ae2


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lightcloud00
lightcloud00 merged commit 83922de into main Aug 22, 2026
5 of 6 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.

7 participants