mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 03:39:51 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a226b1b6cb | ||
|
|
d83962a843 | ||
|
|
f90481bc23 | ||
|
|
cd8e32f18d | ||
|
|
6130b4308c | ||
|
|
6f0f0888c1 | ||
|
|
d98f4759fe | ||
|
|
bc793ba3ff | ||
|
|
5eb917891a | ||
|
|
32531ae08e | ||
|
|
c17f8a53d6 | ||
|
|
3c8b72d54c | ||
|
|
14d89431f9 | ||
|
|
c105b9c2f3 | ||
|
|
52d6dff860 |
No files matched your search
+341
@@ -8,6 +8,347 @@ deprecations, and behavioral changes. Breaking changes are called out explicitly
|
||||
|
||||
---
|
||||
|
||||
## [0.7.10]
|
||||
|
||||
OpenRig 0.7.10 is a patch for people running their own agent teams. After a
|
||||
managed compaction, a step held because OpenRig can't read the seat's input box
|
||||
now goes in after two idle polls instead of waiting with no end. Wakes and sends
|
||||
get through a seat pane left in more than one tmux mode, and `rig exec` hands a
|
||||
queue row's task to Claude Code or Codex as one finite run that outlives the
|
||||
command (not yet run with a real adapter). Installing through the rigs skill tries a user-writable npm prefix
|
||||
before a project-local install, and agent spec advisories now show at launch.
|
||||
There are fixes across refocus, routing, status and speed under load, many
|
||||
contributed by people using OpenRig. The two problems that kept the OpenShell
|
||||
worker from working in 0.7.9 are fixed in the source, but the worker hasn't run
|
||||
end to end on 0.7.10: see [the OpenShell worker](#the-openshell-worker-in-0710).
|
||||
|
||||
### Before you upgrade
|
||||
|
||||
- **Restart the daemon after installing:**
|
||||
```bash
|
||||
npm install -g @openrig/cli@0.7.10
|
||||
rig daemon stop && rig daemon start
|
||||
```
|
||||
This release adds no migrations.
|
||||
- **The bundled openrig-core plugin advances from 0.1.25 to 0.1.26.** Its Claude
|
||||
hooks add a `PermissionRequest` hook that reports permission requests to seat
|
||||
status, and its refocus hook and the refocusing skill's trace script changed.
|
||||
Managed refresh preserves replaced files under
|
||||
`$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you still
|
||||
need from that copy. ([#2615](https://github.com/mvschwarz/openrig/pull/2615), [#2562](https://github.com/mvschwarz/openrig/pull/2562), [#2582](https://github.com/mvschwarz/openrig/pull/2582), [#2587](https://github.com/mvschwarz/openrig/pull/2587))
|
||||
- **Check your agent specs:** `rig agent validate <path>` now prints each
|
||||
advisory. A profile's `uses.<category>` written as a single value now selects
|
||||
that resource instead of being dropped, so a member using that profile fails to
|
||||
resolve if the resource pool doesn't hold it (see Behaviour that changed).
|
||||
([#2585](https://github.com/mvschwarz/openrig/pull/2585), [#2606](https://github.com/mvschwarz/openrig/pull/2606))
|
||||
- **If you run an OpenShell environment, rebuild its guest image** with this
|
||||
release's `guest.py` (`daemon/assets/openshell/README.md` describes the build).
|
||||
The CA repair below is in that file, which the image carries. ([#2602](https://github.com/mvschwarz/openrig/pull/2602))
|
||||
|
||||
### Finite runs: `rig exec`
|
||||
|
||||
- **`rig exec` runs one turn of a queue row's task on Claude Code or Codex**
|
||||
through its maintained ACP adapter, in a connector that outlives the command.
|
||||
`rig exec harnesses` shows the versions installed,
|
||||
`rig exec run <row> --harness claude` (or `codex`) starts it (`--plan` starts
|
||||
nothing, `--wait` waits), and `rig exec status` and `rig exec cancel` follow
|
||||
it. ([#2597](https://github.com/mvschwarz/openrig/pull/2597))
|
||||
- **One run per row, and the row stays open.** Running it again returns the
|
||||
row's run instead of sending the task twice; `--again` starts another turn once
|
||||
it has ended. A run whose connector died without recording an outcome reads
|
||||
`interrupted`, outcome unknown. The outcome is the agent's report, and
|
||||
accepting the work stays with you. `--wait` exits 0 for `completed`, 2 for any
|
||||
other outcome and 3 when you stop waiting; a Ctrl-C there stops the wait, not
|
||||
the run. ([#2597](https://github.com/mvschwarz/openrig/pull/2597), [#2601](https://github.com/mvschwarz/openrig/pull/2601))
|
||||
- **An ACP run's outcome note,** `rig acp run`'s included, carries the usage and
|
||||
cost the agent reported, each with its source, or unknown. ([#2597](https://github.com/mvschwarz/openrig/pull/2597))
|
||||
- **`docs/reference/acp-agents.md`** covers both adapters
|
||||
(`@agentclientprotocol/claude-agent-acp` 0.101.0 and
|
||||
`@agentclientprotocol/codex-acp` 2.5.0): installing them, which native
|
||||
versions run, and how their permissions, refusals and cancels differ. ([#2596](https://github.com/mvschwarz/openrig/pull/2596))
|
||||
|
||||
### The OpenShell worker in 0.7.10
|
||||
|
||||
**The OpenShell worker hasn't run end to end on 0.7.10. Don't rely on this path
|
||||
yet.**
|
||||
|
||||
0.7.10 fixes, in the source, the two problems our release testing on 0.7.9
|
||||
found:
|
||||
- **The guest's CA settings reach its Claude.** The guest launcher built
|
||||
Claude's environment from scratch with only the provider settings, so a CA the
|
||||
OpenShell exec supplied never arrived, and API calls failed with
|
||||
`SELF_SIGNED_CERT_IN_CHAIN`. It now also passes `NODE_EXTRA_CA_CERTS`,
|
||||
`SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE`, each only when the
|
||||
exec supplies it. Nothing else is copied, and nothing turns TLS checks off.
|
||||
Rebuild the guest image to get it (above). ([#2602](https://github.com/mvschwarz/openrig/pull/2602))
|
||||
- **Deleting an owned environment reads Podman's inventory.** The cleanup step
|
||||
asked for routes Podman serves only under a version prefix, misread the "Not
|
||||
Found" reply, and never sent the delete. It now uses routes Podman serves
|
||||
without one, and a volume counts as gone only when one complete volume listing
|
||||
leaves it out. Any other answer stops as `environment_cleanup_unconfirmed`,
|
||||
with the HTTP status. ([#2603](https://github.com/mvschwarz/openrig/pull/2603))
|
||||
|
||||
A verified export, a finished task and an exact restore haven't been shown.
|
||||
|
||||
### Compaction, delivery and terminals
|
||||
|
||||
- **A held post-compaction step now goes in.** Each step after a managed
|
||||
compaction (the turn boundary, the restore request, the audit) went only into
|
||||
an input box that read empty, so a box OpenRig couldn't read held the restore
|
||||
request on every poll, and the seat sat idle without its restore. Now, after
|
||||
two idle polls in a row that can't read the box, the next poll sends the step,
|
||||
and `rig compact <seat> --state` and `rig seat status` say it went in over a
|
||||
box OpenRig couldn't read, never that the box was empty. Some cases still hold
|
||||
(see Known limits). ([#2576](https://github.com/mvschwarz/openrig/pull/2576), refs [#2575](https://github.com/mvschwarz/openrig/issues/2575))
|
||||
- **Wakes and sends leave every tmux mode.** A pane scrolled into copy mode, with
|
||||
run-shell output, a chooser, customize or clock mode on top, kept a mode after
|
||||
one cancel, and the send was refused until someone left it by hand. Delivery
|
||||
now runs tmux's `copy-mode -q`, which leaves them all, with a single cancel
|
||||
where that fails (tmux before 3.1a), and a refusal names why leaving failed.
|
||||
([#2594](https://github.com/mvschwarz/openrig/pull/2594))
|
||||
- **A scrolled pane with a stale record no longer blocks delivery.** tmux
|
||||
reuses pane IDs after a server restart, so a stale binding, from another rig
|
||||
for example, can name the same pane as a live seat. Leaving the mode was
|
||||
addressed by that raw pane ID, which then matched two seats, so it was
|
||||
refused before anything was written and the send failed with nothing sent.
|
||||
Leaving the mode now goes through the seat's address, as the text and Enter
|
||||
writes already did. The stale binding itself is unchanged.
|
||||
([#2644](https://github.com/mvschwarz/openrig/pull/2644))
|
||||
- **A writable rig, pod or seat view sizes the pane it shows,** instead of
|
||||
keeping an older attached client's size. Read-only views are unchanged. ([#2614](https://github.com/mvschwarz/openrig/pull/2614))
|
||||
- **Herdr views kept across an upgrade keep working,** on Linux Herdr 0.9.5 /
|
||||
protocol 22. A viewer or follow view kept from 0.7.5 through 0.7.9 now counts
|
||||
as the same attachment when only OpenRig's tile script changed, and moves to
|
||||
the current command, so the viewer stays confirmed and the follow view goes on
|
||||
adding and closing tiles. A kept viewer that's already running keeps its
|
||||
earlier sizing for as long as it runs, its own re-attaches included; writable
|
||||
viewers opened after the upgrade size their pane. A different session, tmux,
|
||||
server or read-only setting still holds the view, as before. Checked against
|
||||
the scripts those releases used, not yet with a live Herdr. It follows
|
||||
@natefaerber's approach in
|
||||
[#2467](https://github.com/mvschwarz/openrig/pull/2467). ([#2634](https://github.com/mvschwarz/openrig/pull/2634))
|
||||
|
||||
### Coordination and status
|
||||
|
||||
- **Routing and receipts:**
|
||||
- a send to `<seat>@<rig>@<this host's ID>` goes to this host even when a
|
||||
remote host is selected;
|
||||
- `rig broadcast` prints `UNCONFIRMED`, not `FAILED`, for a recipient its text
|
||||
or Enter may have reached, and points to `rig capture` before any resend;
|
||||
- `rig seat stop`, `clean` and in-place restart, when they refuse a seat
|
||||
handed over before 0.7.9, say why and no longer suggest `rig unclaim`;
|
||||
- `rig roster show` and `find` read a seat's activity as unknown
|
||||
("Conflicting activity observations") when two routes to it disagree;
|
||||
- `rig seat set-runtime` finds a pod seat's authored recap.
|
||||
([#2600](https://github.com/mvschwarz/openrig/pull/2600), [#2572](https://github.com/mvschwarz/openrig/pull/2572), [#2574](https://github.com/mvschwarz/openrig/pull/2574), [#2593](https://github.com/mvschwarz/openrig/pull/2593))
|
||||
- **Refocus:** unresolved typed work, and a `rig queue whoami` that timed out,
|
||||
print a named trace gap with project-level orientation, labelled as not
|
||||
evidence of a current mission, instead of counting as a refocus failure.
|
||||
([#2582](https://github.com/mvschwarz/openrig/pull/2582), [#2587](https://github.com/mvschwarz/openrig/pull/2587))
|
||||
- **Updates to people:** the stuck sweep no longer files an informational update
|
||||
to a registered person as an unclaimed obligation, unless its quiet-hours hold
|
||||
was orphaned. ([#2586](https://github.com/mvschwarz/openrig/pull/2586))
|
||||
- **Slack:** `rig slack status` shows relay attempts to another host still
|
||||
awaiting a response. It's a count for the current connector at that moment, so
|
||||
zero doesn't prove nothing is in flight. ([#2569](https://github.com/mvschwarz/openrig/pull/2569))
|
||||
- **Permission requests in seat status:** `rig seat status` shows a Claude
|
||||
seat's latest permission request as openrig-core's hook observed it, with
|
||||
Claude's own suggested rules labelled as suggestions. Nothing is applied,
|
||||
approved or stored. ([#2562](https://github.com/mvschwarz/openrig/pull/2562))
|
||||
|
||||
### Install, specs and errors
|
||||
|
||||
- **The rigs skill's install step,** when npm's global prefix isn't writable,
|
||||
now has the agent point npm's prefix at a folder the person owns whose `bin` is
|
||||
on their login PATH, or use a version manager's Node, saying first what that
|
||||
changes, and install inside the project only if neither works. ([#2611](https://github.com/mvschwarz/openrig/pull/2611))
|
||||
- **Plain `rig` works in managed Codex seats however OpenRig was installed,**
|
||||
with a Codex that supports `allow_login_shell` (checked in Codex's source at
|
||||
0.162.1 and 0.166); not yet run with a live Codex seat. OpenRig now passes Codex `-c allow_login_shell=false`, so tool commands run in
|
||||
a non-login shell and inherit the environment their pane's shell set up,
|
||||
OpenRig's command folder included. To defer to Codex's own setting instead,
|
||||
run `rig config set launch.codex_login_shell true`; it applies at each seat's
|
||||
next launch, and a restart in place keeps the running choice. ([#2613](https://github.com/mvschwarz/openrig/pull/2613))
|
||||
- **Agent spec advisories show where you'll see them:** `rig up`, a fresh restore
|
||||
and a first-start retry report each as a launch warning naming the member,
|
||||
spec and path, and `rig agent validate` prints them whether or not the spec is
|
||||
valid. They never stop a launch. ([#2606](https://github.com/mvschwarz/openrig/pull/2606))
|
||||
- **An unknown key under a profile's `uses`,** such as a typo,
|
||||
gets an advisory instead of being ignored silently. ([#2618](https://github.com/mvschwarz/openrig/pull/2618))
|
||||
- **Clearer errors:**
|
||||
- a rejected terminal-token request says "Terminal auth failed" and names
|
||||
`OPENRIG_TERMINAL_BEARER_TOKEN`, or `OPENRIG_AUTH_BEARER_TOKEN` when
|
||||
terminal auth falls back to it;
|
||||
- the TUI shows the daemon's own message when it refuses a launch;
|
||||
- `rig doctor` gives up on its cmux status and `--spec` topology reads after 2
|
||||
seconds each, so a listener that never answers can't hold it.
|
||||
([#2577](https://github.com/mvschwarz/openrig/pull/2577), [#2556](https://github.com/mvschwarz/openrig/pull/2556), [#2564](https://github.com/mvschwarz/openrig/pull/2564))
|
||||
- **Docs:** a Codex config fragment can't install a named Codex profile, with
|
||||
how to keep a profile with full access or a `codex_permissions` envelope; and
|
||||
a Claude seat that selects openrig-core loads its skills with the plugin.
|
||||
([#2604](https://github.com/mvschwarz/openrig/pull/2604), [#2584](https://github.com/mvschwarz/openrig/pull/2584))
|
||||
|
||||
### Speed under load
|
||||
|
||||
- **The execution view** (`GET /api/views/execution`, behind the TUI's execution
|
||||
view and `rig view show execution`) reads the review registry once per request
|
||||
instead of once per slice, and yields to the event loop while it does. Its
|
||||
other synchronous steps are unchanged. ([#2578](https://github.com/mvschwarz/openrig/pull/2578))
|
||||
- **A queue listing** looks recovery rows up through one index per listing
|
||||
instead of a scan for each listed row. It hasn't been measured on a live
|
||||
daemon. ([#2588](https://github.com/mvschwarz/openrig/pull/2588))
|
||||
- **The event notification drain** reads pages of at most 500 rows instead of
|
||||
the whole remaining tail. It bounds rows per read, not bytes, and still runs to
|
||||
the end synchronously; no latency change on a live install is claimed.
|
||||
([#2610](https://github.com/mvschwarz/openrig/pull/2610))
|
||||
|
||||
### For runner authors and integrators
|
||||
|
||||
- **The runner contract says what has run with a real runner.**
|
||||
`docs/reference/harness-runner-contract.md` and the dsh example say what has
|
||||
run natively, at what scope, and what hasn't; any other runner, provider or
|
||||
path stays unqualified until shown. The 0.7.8 Known limits line below that said
|
||||
no harness had run end to end is struck and corrected in place.
|
||||
([#2592](https://github.com/mvschwarz/openrig/pull/2592), [#2595](https://github.com/mvschwarz/openrig/pull/2595))
|
||||
- **Telemetry:** `docs/reference/telemetry.md` says OpenRig's guarantee ends at
|
||||
its finite reads, versioned by `schemaVersion` 1, and links the separately
|
||||
maintained openrig-telemetry exporter, which OpenRig doesn't test or
|
||||
guarantee. ([#2589](https://github.com/mvschwarz/openrig/pull/2589))
|
||||
- **Also:** the real-tmux transport test waits for the shell's prompt before
|
||||
typing. ([#2559](https://github.com/mvschwarz/openrig/pull/2559))
|
||||
|
||||
### Behaviour that changed
|
||||
|
||||
- **Managed Codex seats run tool commands in a non-login shell** (above).
|
||||
Inherited setup isn't re-run for each command, so setup that lives only in
|
||||
login or rc files (such as `~/.bashrc` lines your login file doesn't source, or
|
||||
a tmux `default-command` that doesn't start a login shell), shell functions and
|
||||
aliases may be missing from tool commands. A model request for `login: true`
|
||||
gets Codex's own refusal, "login shell is disabled by config". With
|
||||
`launch.codex_login_shell` on, OpenRig omits its override rather than forcing
|
||||
login shells, so a login file that sets `PATH` outright can then hide `rig`.
|
||||
([#2613](https://github.com/mvschwarz/openrig/pull/2613))
|
||||
- **A held post-compaction step goes in after two idle polls that can't read the
|
||||
input box** (above). A draft OpenRig can't recognise in that box may receive
|
||||
the restore text. ([#2576](https://github.com/mvschwarz/openrig/pull/2576))
|
||||
- **A send or wake leaves every tmux mode in a seat's pane,** including a
|
||||
chooser, customize or clock mode a person has open, where before it was
|
||||
refused. ([#2594](https://github.com/mvschwarz/openrig/pull/2594))
|
||||
- **A send to `<seat>@<rig>@<this host's ID>`** goes to this host, not the
|
||||
selected remote host. ([#2600](https://github.com/mvschwarz/openrig/pull/2600))
|
||||
- **A profile's `uses.<category>` written as a single value** (for example
|
||||
`uses.skills: review`) used to validate and then be dropped. It now counts as a
|
||||
one-item list, with an advisory suggesting list syntax: a member using that
|
||||
profile gets the resource, or fails to resolve with "not found in resource
|
||||
pool" when the pool doesn't hold it. Other malformed `uses` shapes are
|
||||
advisories, not new errors. ([#2585](https://github.com/mvschwarz/openrig/pull/2585))
|
||||
- **`rig up` can print new warnings,** one per agent spec advisory, and
|
||||
`rig agent validate` prints them. ([#2606](https://github.com/mvschwarz/openrig/pull/2606))
|
||||
- **`rig broadcast`** prints `UNCONFIRMED` for a recipient it may have reached;
|
||||
its exit code and `N/M delivered` count are unchanged. ([#2572](https://github.com/mvschwarz/openrig/pull/2572))
|
||||
- **`rig roster show` and `find`** read conflicting activity as unknown.
|
||||
([#2574](https://github.com/mvschwarz/openrig/pull/2574))
|
||||
- **The refocus hook** no longer counts unresolved typed work or a timed-out
|
||||
`rig queue whoami` as a failure, and a timed-out refocus clears the pending
|
||||
post-compaction retry. ([#2582](https://github.com/mvschwarz/openrig/pull/2582), [#2587](https://github.com/mvschwarz/openrig/pull/2587))
|
||||
- **The stuck sweep** no longer flags informational updates to registered people
|
||||
as unclaimed obligations. ([#2586](https://github.com/mvschwarz/openrig/pull/2586))
|
||||
- **A rejected terminal-token request** gets "Terminal auth failed" instead of
|
||||
"Mission Control auth failed"; its status code and fields are unchanged.
|
||||
([#2577](https://github.com/mvschwarz/openrig/pull/2577))
|
||||
|
||||
### Known limits
|
||||
|
||||
- **OpenShell:** the two fixes haven't run end to end (above). A cleanup error
|
||||
that's thrown still omits its stage and whether its delete was sent, and the
|
||||
inventory is a snapshot, not an atomic read. 0.7.8's requirements, cost note
|
||||
and cleanup limits still apply, including "Never grant the guest access to the
|
||||
daemon's main API port (normally 7433)." A same-generation reconciled launch
|
||||
opens no viewer, so run `rig environment connect` yourself; non-text startup
|
||||
actions aren't supported on environment seats. ([#2603](https://github.com/mvschwarz/openrig/pull/2603))
|
||||
- **`rig exec`:** no real adapter has run under it yet; one run per row,
|
||||
detached ownership and reconciling were checked against a stand-in agent.
|
||||
Concurrent runs, long or background tasks, recovery across a reboot, MCP
|
||||
servers and Windows aren't covered. A cancel is acknowledged, not settled, and
|
||||
says nothing about further spend. Cancel an exec run with `rig exec cancel`:
|
||||
`rig acp cancel` given its ID while it's still requested can signal the wrong
|
||||
process. A Ctrl-C just before the connector is launched isn't caught by
|
||||
`--wait`.
|
||||
([#2597](https://github.com/mvschwarz/openrig/pull/2597), [#2601](https://github.com/mvschwarz/openrig/pull/2601))
|
||||
- **After a managed compaction, a step can still hold:** when the input box
|
||||
reads as typed, with or without a suggestion; on a seat whose draft-aware
|
||||
delivery or typing guard holds OpenRig's input; and after a `/compact` whose
|
||||
result was lost. To take over by hand, run `rig compact <seat> --cancel`, then
|
||||
send the seat a restore request naming its newest restore map, its restore
|
||||
marker and its packet; a step already being pasted can still land. Which
|
||||
reading the reported seat's box gave isn't established, so the report stays
|
||||
open. ([#2575](https://github.com/mvschwarz/openrig/issues/2575))
|
||||
- **Draft-aware delivery to Claude:** on 0.7.8 with Claude Code 2.1.319,
|
||||
deliveries to idle Claude agents, queue wakes included, were reported held at
|
||||
an empty prompt when a narrow pane showed
|
||||
`✔ Update installed · Restart to update` on its own line. 0.7.10 still doesn't
|
||||
recognize that bare banner, though it recognizes `/rc` and the full versioned
|
||||
banner. A message the draft-aware delivery path holds isn't replayed
|
||||
automatically by that path. ([#2605](https://github.com/mvschwarz/openrig/issues/2605))
|
||||
- **Grant-service assistants aren't woken:** "an item stays queued until its
|
||||
receiver claims it."
|
||||
- **Open updates for a person** read a 1,001-row window, so a page can come back
|
||||
short, marked `truncated`.
|
||||
- **Slack:** a private channel needs the documented route, which we don't test,
|
||||
and direct messages aren't supported. The channel and people reads, and a
|
||||
click on a re-parked ask, haven't been run against a live Slack workspace.
|
||||
Redaction of the bot token from a transport error can still leave a malformed
|
||||
token fragment shorter than six characters, so review an error before sharing
|
||||
it. ([#2503](https://github.com/mvschwarz/openrig/pull/2503))
|
||||
- **SSH host status** reads `unknown` behind a ProxyJump or ProxyCommand; that
|
||||
isn't proof the connection is broken. ([#2488](https://github.com/mvschwarz/openrig/pull/2488))
|
||||
- **Shared asks:** "a click's option written before its answer row … can stand
|
||||
beside or inside the closer's answer without the first clicker's authorship."
|
||||
- **Existing seats** last launched fresh before 0.7.9 pick up Codex launch
|
||||
instructions and the single skill load at their next fresh launch. A
|
||||
`CLAUDE.md` or `AGENTS.md` that already grew isn't trimmed.
|
||||
- **Permit records** last as long as stream retention keeps them. A permit a
|
||||
runner makes on its own, when it can't reach OpenRig, is in its pane only. For
|
||||
an ask opened before 0.7.9, a record may omit the option's kind. ([#2361](https://github.com/mvschwarz/openrig/pull/2361))
|
||||
- **Permission requests in seat status** show only the latest one, held in
|
||||
memory for five minutes and lost on a daemon restart. Claude's suggested rules
|
||||
can contain sensitive values, and `rig seat status` and both seat-status routes
|
||||
can return them. ([#2562](https://github.com/mvschwarz/openrig/pull/2562))
|
||||
- **Agent spec advisories** don't show for a native resume that continues a
|
||||
saved conversation, since it doesn't reload the spec. ([#2606](https://github.com/mvschwarz/openrig/pull/2606))
|
||||
- **Seats handed over before 0.7.9** keep their adopted origin until a later
|
||||
launched handover or managed launch records a new session, so until then
|
||||
`rig seat stop`, `restart` and `clean` refuse them; the refusal now says so.
|
||||
([#2440](https://github.com/mvschwarz/openrig/pull/2440), [#2572](https://github.com/mvschwarz/openrig/pull/2572))
|
||||
- **Codex hooks after an upgrade:** Codex sessions kept running across the
|
||||
upgrade to 0.7.9 showed "Hook failed — plugin contents changed; hook approval
|
||||
required" until relaunched; in that upgrade test, one seat restarted with
|
||||
OpenRig's managed restart showed no review afterwards. 0.7.10 changes
|
||||
openrig-core's files again.
|
||||
- **The runner contract** has run natively only at the scope its status section
|
||||
states (`docs/reference/harness-runner-contract.md`); any other runner,
|
||||
provider or path is unqualified until shown.
|
||||
- **Fixed or changed from 0.7.9's limits:** the OpenShell worker's CA settings
|
||||
and owned-delete inventory (fixed in the source, not yet run end to end), a
|
||||
restore request held after a managed compaction, `rig broadcast`'s `FAILED`
|
||||
for an uncertain recipient, the refusal for a seat handed over before 0.7.9,
|
||||
where to read what has run under the runner contract, and Codex seats'
|
||||
`rig: command not found` (now a non-login shell by default; see Behaviour that
|
||||
changed).
|
||||
- **The 0.7.8 limits that 0.7.9 didn't fix or change, and the 0.7.7 and 0.7.6
|
||||
limits, below still apply.**
|
||||
|
||||
### Credits
|
||||
|
||||
Thanks to @dajiaohuang, @Aummadour, @billbenetti, @smota, @Nikhila-Dhevi-R and
|
||||
@Vaishnavi220506 for merged contributions, and to @AlephCasara, whose work on a
|
||||
fork pointed us to the unpaged event notification drain
|
||||
([#2610](https://github.com/mvschwarz/openrig/pull/2610)), and to @natefaerber, whose
|
||||
approach in [#2467](https://github.com/mvschwarz/openrig/pull/2467) the Herdr view fix follows. Thanks to
|
||||
@billbenetti, @KM199, @ziadeh, @dajiaohuang, @jajabong, @george661 and
|
||||
@aanestesia for reports carried into this release, and to @smota for the
|
||||
OpenTelemetry discussion behind [#2589](https://github.com/mvschwarz/openrig/pull/2589).
|
||||
|
||||
## [0.7.9]
|
||||
|
||||
Published 2026-11-06 as `@openrig/[email protected]`, built from the commit tagged
|
||||
|
||||
@@ -26,9 +26,9 @@ use:
|
||||
|
||||
```sh
|
||||
# Claude Code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent claude-code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.10/skills/rigs --skill rigs --agent claude-code
|
||||
# Codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.10/skills/rigs --skill rigs --agent codex
|
||||
```
|
||||
|
||||
Then type `/rigs` in Claude Code, or `$rigs` in Codex, and say what you want built. The skill has your agent tell
|
||||
@@ -68,11 +68,11 @@ Requires Node.js 22 or 24 and tmux, on macOS or Linux, and one working Claude Co
|
||||
|
||||
### Install it yourself
|
||||
|
||||
This runs OpenRig's install script from the `v0.7.5` release. It checks Node.js and npm, installs the latest published `@openrig/cli` with `npm install -g`, runs the Node.js and SQLite check, then runs `rig setup --dry-run` and `rig setup`. The first line prints that plan and changes nothing:
|
||||
This runs OpenRig's install script from the `v0.7.10` release. It checks Node.js and npm, installs the latest published `@openrig/cli` with `npm install -g`, runs the Node.js and SQLite check, then runs `rig setup --dry-run` and `rig setup`. The first line prints that plan and changes nothing:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.10/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.10/scripts/install.sh | sh
|
||||
```
|
||||
|
||||
If the last step isn't ready, the installer reports `INSTALLED, SETUP NOT COMPLETE` and points to the details from `rig setup` above. Follow that output and any reported Fix, then rerun `rig setup`; the CLI does not need reinstalling. If the only remaining failures are provider sign-ins, sign in to each selected provider as below and continue.
|
||||
|
||||
@@ -289,7 +289,10 @@ Each array contains resource IDs. These can be:
|
||||
|
||||
The `uses` categories are: `skills`, `guidance`, `subagents`, `plugins`, `runtime_resources`. A `uses.hooks` key, even an
|
||||
empty list, is refused: hooks ship inside plugins. Any other key selects nothing and gets a validation advisory, shown at
|
||||
launch and by `rig agent validate`, so a typo such as `skils` doesn't go unnoticed.
|
||||
launch and by `rig agent validate`, so a typo such as `skils` doesn't go unnoticed. A single value written in place of a
|
||||
category's list (`skills: vault-user`) is read as a one-item list, with an advisory suggesting list syntax, and resolves
|
||||
exactly like that list: when the profile is selected, a resource that isn't available fails that member's resolution with
|
||||
"not found in resource pool".
|
||||
|
||||
**Skills resolve more widely than other resources.** For Claude Code and Codex seats, a skill ID that no declared
|
||||
resource provides is looked up at launch in the project's runtime skill folder in the seat's cwd (`<cwd>/.claude/skills` or
|
||||
|
||||
@@ -201,11 +201,11 @@ You can also read [`skills/rigs/SKILL.md`](../../skills/rigs/SKILL.md) directly.
|
||||
script will do and changes nothing:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.10/scripts/install.sh | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
The same command without `--dry-run`,
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh`, installs the
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.10/scripts/install.sh | sh`, installs the
|
||||
latest published `@openrig/cli` with `npm install -g`,
|
||||
runs the Node.js and SQLite check, then `rig setup --dry-run` and `rig setup`. `rig setup` checks both Claude Code and
|
||||
Codex and may install a missing one, as described below. A failed step prints
|
||||
@@ -270,6 +270,17 @@ opt in with
|
||||
`rig config set terminal.inherit_tmux_environment true`. Setup points out this option
|
||||
when it detects custom tmux configuration; it never enables it automatically. The setting
|
||||
applies to future launches and successors, not running agents or your personal tmux server.
|
||||
|
||||
Managed Codex seats run tool commands in a non-login shell: OpenRig passes Codex
|
||||
`-c allow_login_shell=false`. Tool commands inherit the environment their pane's shell set up,
|
||||
OpenRig's command folder included, so plain `rig` works however OpenRig was installed.
|
||||
Inherited setup isn't re-run for each command. Setup that lives only in login or rc files, such as
|
||||
lines in `~/.bashrc` that your login file doesn't source, or a tmux `default-command` that doesn't
|
||||
start a login shell, may be missing from tool commands, and so may shell functions and aliases.
|
||||
To defer to Codex's own setting instead, run `rig config set launch.codex_login_shell true`. That
|
||||
omits OpenRig's override rather than forcing login shells, and a login file that sets `PATH`
|
||||
outright can then hide `rig`. It applies at each seat's next launch; a restart in place keeps
|
||||
the running choice.
|
||||
If the scoped environment cannot be prepared, launch keeps its previous environment and
|
||||
reports a warning in fresh launch results and the daemon log. For handovers, check
|
||||
that log for this warning. This is not a promise that shell startup cannot add or override values.
|
||||
|
||||
@@ -17,7 +17,7 @@ npx skills add https://github.com/mvschwarz/openrig/tree/<release-tag>/skills/ri
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/<release-tag>/skills/rigs --skill rigs --agent codex
|
||||
```
|
||||
|
||||
For a new install, use the latest release's tag, for example `v0.7.5`. If OpenRig is already installed, use the tag
|
||||
For a new install, use the latest release's tag, for example `v0.7.10`. If OpenRig is already installed, use the tag
|
||||
that matches `rig --version`, or upgrade first. If the command fails with `npx: command not found`, Node.js isn't
|
||||
installed or isn't on your PATH yet (see [What you need](#what-you-need)). Then type `/rigs` in Claude Code, or `$rigs` in Codex, and say what
|
||||
you want built.
|
||||
|
||||
+472
-178
@@ -1,192 +1,486 @@
|
||||
# What changed in OpenRig 0.7.1
|
||||
# What changed in OpenRig 0.7.10
|
||||
|
||||
This note is for an agent on an install that was just upgraded to 0.7.1. It lists only what behaves differently from
|
||||
0.7.0 and what to do about it. For everything OpenRig can do, read the capability map:
|
||||
`rig context get onboarding-width/public-what-you-can-do.md`. Each release replaces this file, so
|
||||
`rig context get reference/whats-new.md` describes the installed version once the upgraded daemon is running. Without
|
||||
a running daemon, read `$OPENRIG_HOME/reference/whats-new.md`. The daemon refreshes that copy when it starts, so right
|
||||
after an upgrade it can still hold the previous version's note.
|
||||
This note is for an agent on an install that was just upgraded to 0.7.10. OpenRig
|
||||
0.7.10 adds `rig exec` for finite runs on Claude Code or Codex (not yet run with
|
||||
a real adapter), sends a held
|
||||
post-compaction step after two idle polls that can't read the input box, leaves
|
||||
every tmux mode before a delivery, and shows agent spec advisories at launch. It
|
||||
fixes in the source the two problems that stopped the OpenShell worker in 0.7.9,
|
||||
but that worker hasn't run end to end. If you upgraded from 0.7.8 or earlier,
|
||||
also read "What changed in OpenRig 0.7.9" below, and from 0.7.7 or earlier,
|
||||
"What changed in OpenRig 0.7.8" too. For the full capability map, read
|
||||
`rig context get onboarding-width/public-what-you-can-do.md`. After the upgraded
|
||||
daemon starts, `rig context get reference/whats-new.md` describes this version.
|
||||
Without a running daemon, read the packaged `daemon/docs/reference/whats-new.md`;
|
||||
`$OPENRIG_HOME/reference/whats-new.md` can still be the previous copy until
|
||||
startup.
|
||||
|
||||
## After the upgrade
|
||||
|
||||
- **Restart the daemon** (`rig daemon stop`, then `rig daemon start`). Eight migrations run on the first start (124, 125,
|
||||
127, 128, 129, 130, 131 and 135). Nine settings are added: `policies.claude_compaction.preparation_root` (unset),
|
||||
`skills.router_line.enabled` (off), `decisions.enabled` (false), `decisions.consumers`, `decisions.template_dir`,
|
||||
`retention.stream_days` (30), `retention.stream_max_items` (20000), `provider.selected` (empty) and
|
||||
`daemon.db_conflict` (`warn`). None is removed.
|
||||
- **Stream retention is on.** Stream items emitted after the upgrade are deleted after 30 days, or beyond the newest
|
||||
20,000, unless a record names them (a classification, duplicate link, attempt or disposition). History from before the
|
||||
upgrade is kept. Raise `retention.stream_days` or `retention.stream_max_items` to keep more.
|
||||
- **The bundled openrig-core plugin goes from 0.1.17 to 0.1.18.** Startup replaces the installed plugin's changed files:
|
||||
its Claude hook definitions, three hook scripts and the pre-compaction hook (so hooks start on a seat whose PATH has
|
||||
no Node), and the `openrig-user`, `messaging-the-human` and `forming-an-openrig-mental-model` skills. As in
|
||||
0.7.0, it first copies each file it replaces under `$OPENRIG_HOME/backups/managed-refresh/<id>/`, edited or not, and
|
||||
logs `[plugin-vendor] preserved previous contents of <file> at <copy> before managed refresh`. A copy doesn't mean
|
||||
anyone edited the file; if someone did, reapply the change from the copy.
|
||||
- **Claude seats need Claude Code 2.1.280 or newer to see their team skills,** which now arrive as a per-seat plugin
|
||||
through `CLAUDE_CODE_PLUGIN_DIRS` (below). OpenRig doesn't check the version. A running seat gets the new layout at its
|
||||
next launch, restore or handover.
|
||||
- **If a worktree's `.claude/settings.local.json` says `"defaultMode": "acceptEdits"` and the person didn't choose it,**
|
||||
an earlier OpenRig wrote it when it added or freshly launched a managed Claude seat. 0.7.1 no longer writes it but
|
||||
doesn't repair it; ask the person which mode they want and set it once.
|
||||
- **Restart the daemon** to load the new code: `rig daemon stop`, then
|
||||
`rig daemon start`. This release adds no migrations.
|
||||
- **The bundled openrig-core plugin goes from 0.1.25 to 0.1.26.** Its Claude
|
||||
hooks add a `PermissionRequest` hook that reports permission requests to seat
|
||||
status, and its refocus hook and the refocusing skill's trace script changed.
|
||||
Managed refresh preserves replaced files under
|
||||
`$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you still
|
||||
need from the preserved copy.
|
||||
- **Check agent specs before relaunching seats:** run `rig agent validate <path>`
|
||||
and read any `agent spec advisory` line. A profile's `uses.<category>` written
|
||||
as a single value now selects that resource; if the resource pool doesn't hold
|
||||
it, a member using that profile fails to resolve with "not found in resource
|
||||
pool". Ask the person before you change their spec.
|
||||
- **Codex hooks:** a Codex seat kept running across the upgrade can show "Hook
|
||||
failed — plugin contents changed; hook approval required" until relaunched; in
|
||||
0.7.9's upgrade test, one seat restarted with OpenRig's managed restart showed
|
||||
no review afterwards.
|
||||
- **OpenShell:** rebuild a guest image with this release's `guest.py` before
|
||||
using it; the CA repair is in that file, which the image carries.
|
||||
- **Codex tool commands use a non-login shell:** managed Codex seats now pass
|
||||
Codex `-c allow_login_shell=false`, so plain `rig` works however OpenRig was
|
||||
installed, with a Codex that supports `allow_login_shell` (checked in Codex's
|
||||
source at 0.162.1 and 0.166); not yet run with a live Codex seat. Setup that lives only in login or rc files, and shell functions and
|
||||
aliases, may be missing from tool commands, and a model request for
|
||||
`login: true` gets Codex's refusal, "login shell is disabled by config". To
|
||||
defer to Codex's own setting, run `rig config set launch.codex_login_shell
|
||||
true`; a login file that sets `PATH` outright can then hide `rig`. It applies
|
||||
at each seat's next launch, and a restart in place keeps the running choice.
|
||||
([#2613](https://github.com/mvschwarz/openrig/pull/2613))
|
||||
|
||||
## New things you can do
|
||||
## Finite runs: `rig exec`
|
||||
|
||||
- **Choose how much automatic input reaches a seat:** `rig seat set-typing-guard <seat> --mode <mode> --reason "…"`, where
|
||||
`<mode>` adds `openrig-urgent` (OpenRig's own notices and anything urgent) and `urgent-only` between `off` and `hold`.
|
||||
Mark a message urgent with `rig send --urgent`, or create the row at priority `urgent` or `critical`. Held input stays
|
||||
readable (`rig seat held-messages`) and is never replayed.
|
||||
- **Choose how far OpenRig and peers may act on a seat:** `rig seat set-terminal-policy <seat> --preset
|
||||
no-peer-interventions|help-within-instructions|full-control --reason "…"`, with `--helper <addresses…>`,
|
||||
`--rig-default` (seats added later), and exceptions such as `--peer-compaction deny` or `--openrig-model allow`.
|
||||
Managed compaction and saved model or effort changes are checked; a denial is `terminal_policy_refused`. Other
|
||||
interventions are guidance only.
|
||||
- **Read a seat's planned permissions:** `rig seat status <seat>` shows what its next fresh managed launch will pass and
|
||||
where each part comes from, and its terminal policy. `rig setup --router-line yes|no` decides whether OpenRig adds its
|
||||
one line to shared guidance files.
|
||||
- **Let a person choose whether every ask reaches Slack:** `rig gateway human set <id> slack-subscription
|
||||
every-ask|urgent-only|unset`. Every ask is on the TUI's Feed either way. Under `urgent-only`, only an ask
|
||||
created or handed off with `--alert` posts. An ask marked `--alert` stands out in Slack (an interrupt with a mention)
|
||||
under either setting, so use it only for something only they can do, or an approval whose wait costs now.
|
||||
- **Give one Claude member its own setup** in the rig spec: `agent_file`, `append_system_prompt_file`, `plugin_dirs` and
|
||||
`advisor` (an Advisor model). They're copied into that seat's storage, never the project.
|
||||
- **See a seat's own skills:** `rig skill loadout --seat <session|self>`.
|
||||
- **Reach the team from an assistant without a shell:** the MCP server has 34 tools, including `rig_whoami`,
|
||||
`rig_restore_status`, `rig_team`, `rig_telemetry_events`, following a handoff, reading a finished item's artifact and
|
||||
deciding inside a person's decision room. `rig mcp serve --http` serves them over HTTP; `rig mcp token add <name>
|
||||
--seat <session>` gives each caller its own seat.
|
||||
- **Hand a row's task to an outside agent:** `rig acp run <row>` for an ACP agent (Hermes, OpenClaw).
|
||||
`docs/reference/a2a-agents.md` covers A2A agents.
|
||||
- **Run a community harness that names, reports or reopens its conversations:** preset fields `launch_result`, `resume`,
|
||||
`names_conversation` and `startup_assisted`, with examples for Cursor, Hermes and OpenClaw under
|
||||
`docs/reference/community-harness-examples/`.
|
||||
- **Change one edge on a running rig:** `rig edge add <rig> <from> <to> --kind <kind>` and `rig edge remove <rig>
|
||||
<edge-id>` (`--plan`, `--json`). A `delegates_to` change says when it moves who work routes to.
|
||||
- **Run one spec as several rigs:** `rig up <spec> --name <rig>`.
|
||||
- **Retune a watchdog in place:** `rig watchdog update <jobId> [--interval-seconds n] [--message text] [--spec path]`
|
||||
keeps its id and attachments. The context watch takes `--threshold-tokens` or `--threshold-percent`.
|
||||
- **Put restore maps where seats may write:** `rig config set policies.claude_compaction.preparation_root <abs path>`.
|
||||
- **Pick a Herdr session:** `rig terminal open <view> --provider herdr --session <name>`.
|
||||
- **Close a row with its artifact:** `rig queue update <id> --state done --evidence-ref <path>`.
|
||||
- **File and read subscription move reports:** `rig provider moves report` and `rig provider moves`. `--host <id>` and
|
||||
`--codex-home <dir>` on `rig auth status|list|switch` and `rig seat restart` act on another machine's Codex home.
|
||||
- **Route stream observations with typed decisions (opt-in, off by default):** once `decisions.enabled` is true,
|
||||
`decisions.consumers` includes `stream` and the intake seat has `OPENROUTER_API_KEY`, `rig project wake --decide`
|
||||
gives each eligible stream observation one bounded typed decision during that seat's wake: create a row for a supplied
|
||||
owner, join supplied work, or record a duplicate or no action. Anything uncertain, refused or failed goes to the
|
||||
intake seat as its own disposition, and the model isn't asked again. `rig project decisions status` shows the state
|
||||
without making a call. Without `--decide`, or with decisions off, nothing changes; turning decisions off no longer
|
||||
strands an accepted create or join whose effect hadn't landed.
|
||||
- **Read the stream as a subscriber:** `rig stream subscribe <seat>` (`--tags`, `--type`; re-subscribing keeps the
|
||||
place), then `rig stream read` returns the next bounded page for the calling seat (at most 20 items and 64 KiB, with
|
||||
`more`, `unread` and any `gap`). `rig stream subscriptions` lists readers; `rig stream unsubscribe <seat>` stops one.
|
||||
`rig stream show <id>` says what became of an observation.
|
||||
- **Keep the kernel to the harnesses you chose:** `rig setup --providers <list>` is now saved as `provider.selected`, and
|
||||
the kernel starts seats only for those.
|
||||
- **Stop a second daemon on a live database:** `rig config set daemon.db_conflict stop` refuses a start whose database a
|
||||
matching live daemon already holds (the default `warn` only warns). `rig daemon start --allow-db-conflict` allows one
|
||||
deliberate start.
|
||||
`docs/reference/acp-agents.md` is the rule. Read it before handing a row to
|
||||
Claude Code or Codex.
|
||||
- **Install the adapters first:** `@agentclientprotocol/claude-agent-acp`
|
||||
0.101.0 and `@agentclientprotocol/codex-acp` 2.5.0, as that page shows.
|
||||
`rig exec harnesses` reports the adapter and native versions it finds.
|
||||
- **Run:** `rig exec run <row> --harness claude` (or `codex`). `--plan` shows
|
||||
what would run and starts nothing; `--wait` waits for the outcome. A connector
|
||||
owns the run, so leaving `--wait` or closing the terminal doesn't stop it.
|
||||
Running it again returns the row's run instead of sending the task twice;
|
||||
`--again` starts another turn once that run has ended.
|
||||
- **Read the outcome** with `rig exec status <runId|row>`. `interrupted` means
|
||||
the connector died without recording an outcome, so the outcome is unknown. A
|
||||
`completed` turn is the agent's report, not finished work: check the work
|
||||
before you close the row. `--wait` exits 0 for `completed`, 2 for any other
|
||||
outcome and 3 when you stop waiting.
|
||||
- **Cancel** with `rig exec cancel <runId>`, not `rig acp cancel`. An
|
||||
acknowledgement isn't a settled shutdown, and it says nothing about further
|
||||
spend.
|
||||
- **Usage and cost** the agent reported are on the row's outcome note, for
|
||||
`rig acp run` too, each with its source; what it didn't report reads unknown.
|
||||
|
||||
## Behaviour that changed
|
||||
## Compaction and delivery
|
||||
|
||||
- **Team skills live in each seat's own storage** (`$OPENRIG_HOME/state/seats/<rig id>/<logical seat>/<runtime>/`), not
|
||||
in the project's `.claude/skills` or `.agents/skills`. Claude seats get them as a generated plugin (`openrig:<id>`),
|
||||
Codex seats as their own catalog. If a link sits in that path, the seat starts without them and the result names
|
||||
what wasn't delivered.
|
||||
- **Urgent wakes are labelled:** an urgent or critical row's wake starts with `Urgent (marked by …):` at every seat. A
|
||||
seat on `hold` gets one held notice per wake instead of re-nudges, and lifecycle work is refused at the middle levels as
|
||||
at `hold`.
|
||||
- **A daemon restart picks up a managed compaction where it stopped.** An owed restore step resumes, a `/compact` that
|
||||
may have been sent is never sent again, and `rig compact --state` shows anything stranded or owed.
|
||||
- **A Claude seat whose startup turn is refused** (an expired login, say) holds the rest of its startup context as owed.
|
||||
A startup input sent before the refusal is noticed can still be submitted (see Known issues in the
|
||||
[0.7.1 release notes](https://github.com/mvschwarz/openrig/releases/tag/v0.7.1)).
|
||||
- **`rig send` refuses with `target_needs_input`** when a Claude seat's hook has just reported it running and its screen
|
||||
shows an MCP enablement menu, a question or a permission prompt.
|
||||
- **`rig down` asks before stopping the caller's own rig.** Without a terminal it exits 1, stops nothing and prints the
|
||||
`--yes` command.
|
||||
- **`rig up --plan` doesn't start a stopped daemon:** it exits 1 with `daemon_not_running` and names `rig daemon start`
|
||||
and `--no-kernel`.
|
||||
- **`rig daemon start`** waits up to 20 seconds for a slow daemon, and starts with a warning when only a secondary
|
||||
listener, such as Tailscale's, can't be verified.
|
||||
- **A seat joined with `rig attach` as an outside app** (Claude Desktop) stays attached through `rig down` and restore
|
||||
(`left_attached`). To replace it, `rig unclaim <name>`, then launch.
|
||||
- **New seats get the daemon's `OPENRIG_DB` and `TMUX_TMPDIR`.**
|
||||
- **A rebuilt Codex seat keeps its observed permissions,** and a read-only Codex profile launches without OpenRig's
|
||||
writable roots.
|
||||
- **A seat's own role** goes only to that seat, never into a shared `CLAUDE.md` or `AGENTS.md`, and `rig down` returns
|
||||
guidance files byte for byte.
|
||||
- **Workflow actions** (`project`, `route`, `resume`, `abort`) record the calling agent; a request with neither identity
|
||||
nor actor gets the shared `actor_required` error.
|
||||
- **`rig provider moves` is back** for subscription move reports. It switches nothing.
|
||||
- **`--dangerously-interact` into a seat's input box stops first** when the box already holds typed or pasted text: it
|
||||
shows that text (apart from suggestions) and types nothing. Clear the box and retry, or pass `--type-without-checking`,
|
||||
which clears nothing. Keys for a recognized native menu are sent as before.
|
||||
- **A terminal seat gets exactly the text sent to it,** without the From/To lines (the result says `envelopeOmitted`).
|
||||
A queue wake to a terminal seat types nothing and fails `failed:terminal-seat`; the row stays queued.
|
||||
- **A daemon restart that can't see the seats' tmux server** no longer detaches seats whose recorded process still
|
||||
runs, so it doesn't relaunch them beside themselves.
|
||||
- **Provider credentials and the activity-hook token stay off tmux, shell and `env` command lines** at launch and
|
||||
handover. A launch whose staging fails stops instead of falling back.
|
||||
- **An answer retried from Slack or Steering** after an interrupted close reuses the first reply and finishes the
|
||||
request once.
|
||||
- **One bad row no longer stops the stuck sweep** from flagging the others; **a malformed unrelated diagnosis row** no
|
||||
longer stops health diagnoses from listing.
|
||||
- **A held post-compaction step goes in.** After two idle polls in a row that
|
||||
can't read a seat's input box, the next poll sends the step (the turn
|
||||
boundary, the restore request or the audit). `rig compact <seat> --state` and
|
||||
`rig seat status` say it went in over a box OpenRig couldn't read; they never
|
||||
call that box empty. A draft OpenRig can't recognise in that box may receive
|
||||
the restore text.
|
||||
- **Still held:** a box that reads as typed, with or without a suggestion; a
|
||||
seat whose draft-aware delivery or typing guard holds OpenRig's input; and the
|
||||
step after a `/compact` whose result was lost. To take over a held restore by
|
||||
hand, run `rig compact <seat> --cancel`, then send the seat a restore request
|
||||
naming its newest restore map, its restore marker and its packet. A step
|
||||
already being pasted can still land.
|
||||
- **tmux modes:** a send or wake now leaves every tmux mode in a seat's pane,
|
||||
including a chooser, customize or clock mode a person has open. If a send is
|
||||
still refused for a tmux mode, the refusal names why leaving failed.
|
||||
- **A scrolled pane with a stale record:** leaving the mode now goes through the
|
||||
seat's address, so a stale binding naming the same reused tmux pane, from
|
||||
another rig for example, no longer makes the send fail with nothing sent.
|
||||
- **Herdr views kept across an upgrade keep working,** on Linux Herdr 0.9.5 /
|
||||
protocol 22. A viewer or follow view kept from 0.7.5 through 0.7.9 now counts
|
||||
as the same attachment when only OpenRig's tile script changed, and moves to
|
||||
the current command, so the viewer stays confirmed and the follow view goes on
|
||||
adding and closing tiles. A kept viewer that's already running keeps its
|
||||
earlier sizing for as long as it runs, its own re-attaches included; writable
|
||||
viewers opened after the upgrade size their pane. A different session, tmux,
|
||||
server or read-only setting still holds the view, as before. Checked against
|
||||
the scripts those releases used, not yet with a live Herdr. It follows
|
||||
@natefaerber's approach in
|
||||
[#2467](https://github.com/mvschwarz/openrig/pull/2467). ([#2634](https://github.com/mvschwarz/openrig/pull/2634))
|
||||
|
||||
## Output that changed shape
|
||||
## OpenShell workers
|
||||
|
||||
- **`rig ps --nodes`** separates every cell with one space (the default row is 5 columns wider), adds an `ATTN` column
|
||||
(`yes`, `no` or `?`) before `REASON`, and marks a `running` seat whose tmux couldn't be reached as unconfirmed.
|
||||
- **`rig queue show --json`** no longer has `lastNudgeInputBox`, and send results no longer describe the input box.
|
||||
- **`rig queue show` and `rig queue whoami`** print their human notices on stderr, so stdout is the JSON.
|
||||
- **A claimed row whose owner waits on a person** reads pickup `needs-input`, with the reason.
|
||||
- **`rig down --json`** adds `leftRunning`; `rig reconcile-session` says whether resume metadata was recorded.
|
||||
- **`rig compact-plan`** labels its thresholds as its own and says whether automatic compaction is on.
|
||||
- **A send held for a draft** in the seat's input box reports `inputPhase: "not_reached"`: it was never typed.
|
||||
- **A stream body over 16 KiB** is stored truncated, ending `[truncated by OpenRig: kept N of M bytes]`, and reads mark it
|
||||
`bodyTruncated`. `rig stream list --after <sortKey>` from a pruned item returns 410 `stream_cursor_expired`, naming
|
||||
`--since` to resume, instead of an empty list.
|
||||
0.7.10 fixes, in the source, the two problems that stopped the OpenShell worker
|
||||
in 0.7.9, but the worker hasn't run end to end. Don't rely on this path yet. The
|
||||
guest launcher now passes the CA settings the OpenShell exec supplies
|
||||
(`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE` and
|
||||
`CURL_CA_BUNDLE`) to its Claude, and deleting an owned environment reads
|
||||
Podman's inventory through routes Podman serves, stopping as
|
||||
`environment_cleanup_unconfirmed` on any other answer. A verified export, a
|
||||
finished task and an exact restore haven't been shown. If you set one up anyway,
|
||||
follow `daemon/assets/openshell/README.md` in the installed package, rebuild the
|
||||
guest image first, and name the costs to the person ("Provider registration and
|
||||
image construction can incur costs"). "Never grant the guest access to the
|
||||
daemon's main API port (normally 7433)."
|
||||
|
||||
## What to stop doing
|
||||
## Other changes to act on
|
||||
|
||||
- **Don't read `lastNudgeInputBox`.** It's gone.
|
||||
- **Don't parse `rig ps --nodes` by column position.** Use `--json`, with `--fields` to pick fields.
|
||||
- **Don't look for a team seat's skills in the project folders.** Use `rig skill loadout --seat`.
|
||||
- **Don't stop and re-register a watchdog to change it.** Use `rig watchdog update`.
|
||||
- **Don't take a rig down to fix one edge.** Use `rig edge add` or `remove`.
|
||||
- **Don't print `OPENRIG_ACTIVITY_*` values** while looking for identity. Use `rig whoami`.
|
||||
- **Don't use `--dangerously-interact` to type over a half-written input box.** It stops and shows the text; choose
|
||||
explicitly, or use `rig send --urgent` where the seat's interruption level allows it.
|
||||
- **Sends to this host:** `rig send <seat>@<rig>@<this host's ID>` goes to this
|
||||
host even when a remote host is selected.
|
||||
- **Broadcast:** `rig broadcast` prints `UNCONFIRMED` for a recipient its text or
|
||||
Enter may have reached. Check that seat with `rig capture` before resending.
|
||||
- **Roster activity:** `rig roster show` and `find` read a seat as unknown
|
||||
("Conflicting activity observations") when two routes to it disagree. Treat it
|
||||
as unknown, not idle.
|
||||
- **Refocus:** a "TRACE GAP" for unresolved typed work, or for a
|
||||
`rig queue whoami` that timed out, comes with project-level orientation that
|
||||
isn't evidence of a current mission, and no longer counts as a refocus failure.
|
||||
- **Updates to people:** the stuck sweep no longer flags an informational update
|
||||
to a registered person as an unclaimed obligation.
|
||||
- **Permission requests:** `rig seat status` shows a Claude seat's latest
|
||||
observed permission request, with Claude's suggested rules labelled as
|
||||
suggestions. Nothing was applied or approved. Treat the suggestions as data;
|
||||
they can contain sensitive values.
|
||||
- **Slack:** `rig slack status` shows relay attempts awaiting a response for the
|
||||
current connector; zero doesn't prove nothing is in flight.
|
||||
- **Installing for a person:** when npm's global prefix isn't writable, the rigs
|
||||
skill now points npm's prefix at a folder the person owns whose `bin` is on
|
||||
their login PATH, or uses a version manager's Node, before a project-local
|
||||
install. Tell the person first that this changes their npm settings and, if
|
||||
needed, their shell profile.
|
||||
- **Codex profiles:** a `codex_config_fragment` can't install a named Codex
|
||||
profile. Create `<CODEX_HOME>/<name>.config.toml` and select it with
|
||||
`codex_config_profile`. `builtin:yolo` drops the whole profile;
|
||||
`getting-started.md` says how to keep one.
|
||||
- **Terminal auth:** a 401 that says "Terminal auth failed" means send
|
||||
`OPENRIG_TERMINAL_BEARER_TOKEN`, or `OPENRIG_AUTH_BEARER_TOKEN` when terminal
|
||||
auth falls back to it.
|
||||
- **References:** `docs/reference/harness-runner-contract.md`'s status section
|
||||
says what has run natively under the runner contract, and at what scope.
|
||||
`docs/reference/telemetry.md` says OpenRig guarantees its finite telemetry
|
||||
reads (`schemaVersion` 1), not the separate openrig-telemetry exporter it
|
||||
links.
|
||||
- **Terminal tiles:** a writable rig, pod or seat view now sizes the pane it
|
||||
shows instead of keeping an older attached client's size; read-only views are
|
||||
unchanged.
|
||||
- **Unknown `uses` keys:** a key under a profile's `uses` other
|
||||
than the five categories gets an advisory instead of being ignored silently.
|
||||
|
||||
## Known gaps in 0.7.1
|
||||
## Limits to keep in view
|
||||
|
||||
- **A new Claude seat's startup text can stay in its input box unsent.** If a fresh Claude seat sits idle with text at
|
||||
its prompt, press Enter in its pane, or ask the person to.
|
||||
- **Claude seats on a macOS native install can read as needing attention after a restore.** Check the pane before
|
||||
acting on it.
|
||||
- **A Slack channel-mapping change needs a daemon restart.** Don't use `rig slack disable` then `enable` for it: enabling
|
||||
marks any alerts waiting to post to Slack as already seen.
|
||||
- **Ubuntu 22.04's tmux 3.2a:** the query known to crash its server is gone, but a narrower race remains. Avoid 3.2a
|
||||
(`tmux -V`).
|
||||
- **`rig daemon start` can take 20 to 40 seconds** before its listener warning, longer than the TUI's 30-second "restore
|
||||
everything" wait. On a slow host, start the daemon yourself first.
|
||||
- **A launch that warns "Scoped tmux environment unavailable; retaining the previous environment"** didn't give that seat
|
||||
the clean environment. Check `env` in the seat before relying on OpenRig's variables.
|
||||
- **Going back to 0.7.0 with a Slack subscription set:** 0.7.0 can't read a person file that holds one, and its people
|
||||
registry stops loading. Save the choice first (`rig gateway human show <id> --json > <id>.json`), then run
|
||||
`rig gateway human set <id> slack-subscription unset` (`urgent-only` becomes every-ask, as on 0.7.0); set it again after
|
||||
returning to 0.7.1. A person file still holding a `subscriptions:` entry fails on 0.7.0 too. Don't restore an older
|
||||
gateway folder or database instead: that can lose newer people, bindings and delivery records.
|
||||
- **A Herdr tile over SSH** doesn't come back by itself after a Herdr restart. Reopen it with `rig terminal open <view>
|
||||
--provider herdr`.
|
||||
- **Delegated decisions:** the ask is created before the decision, so without quiet hours it can post to the person
|
||||
first. Pair a decision room with quiet hours, or decide before asking.
|
||||
- **`restore-check` still treats a missing startup file as rig-wide.** Trust `rig up --existing`'s per-seat result.
|
||||
- **Managed Claude launch requires `/usr/bin/env` support for `-0` and `-u`** (option support checked on GNU coreutils
|
||||
and macOS 15; BusyBox and older macOS unverified). Without the required option support, launch stops instead of
|
||||
falling back to credential-bearing command-line arguments.
|
||||
- **In draft-aware mode, an agent's multi-line message to a Claude seat can remain unsubmitted when the paste changes
|
||||
Claude's footer hint.** OpenRig withholds Enter, leaving the text in the composer and later sends waiting. Observed on
|
||||
Claude 2.1.309 with both 0.7.0 and the 0.7.1 pre-cut build; #14 remains open.
|
||||
- **A Claude seat signed in with a long-lived token (`claude setup-token`) gets no usage reading.** The reader accepts
|
||||
only claude.ai subscription logins and reports `not_subscription_login`.
|
||||
- **OpenShell** hasn't run end to end on 0.7.10 (above). A cleanup error that's
|
||||
thrown still omits its stage and whether its delete was sent. A
|
||||
same-generation reconciled launch opens no viewer, so run
|
||||
`rig environment connect` yourself; non-text startup actions aren't supported
|
||||
on environment seats.
|
||||
- **`rig exec`:** no real adapter has run under it yet; one run per row,
|
||||
detached ownership and reconciling were checked against a stand-in agent.
|
||||
Concurrent runs, long or background tasks, recovery across a reboot, MCP
|
||||
servers and Windows aren't covered. A Ctrl-C just before the connector is
|
||||
launched isn't caught by `--wait`.
|
||||
- **Draft-aware delivery to Claude:** a narrow Claude pane showing
|
||||
`✔ Update installed · Restart to update` on its own line can still hold
|
||||
deliveries at an empty prompt, queue wakes included; 0.7.10 doesn't recognize
|
||||
that bare banner. A held message isn't replayed automatically by that path.
|
||||
- **Seats handed over before 0.7.9** keep their adopted origin until a later
|
||||
launched handover or managed launch records a new session. Until then
|
||||
`rig seat stop`, `clean` and in-place restart refuse them, and now say why.
|
||||
Don't use `rig unclaim` to get around it: that ends OpenRig's management of
|
||||
the seat.
|
||||
- **Permission requests in seat status** show only the latest one, held in
|
||||
memory for five minutes and lost on a daemon restart.
|
||||
- **Agent spec advisories** don't show for a native resume that continues a
|
||||
saved conversation.
|
||||
- **The other 0.7.9 limits below still apply:** open updates for a person, Slack,
|
||||
SSH host status, shared asks, permit records, existing seats, Codex hooks, and
|
||||
grant-service assistants not being woken. 0.7.9's limits on a restore request
|
||||
held after compaction, `rig broadcast`'s `FAILED`, what has run under the
|
||||
runner contract, and Codex seats' `rig: command not found` are fixed or
|
||||
changed. The 0.7.8 limits below that 0.7.9
|
||||
didn't fix or change, and the
|
||||
[0.7.7](https://github.com/mvschwarz/openrig/releases/tag/v0.7.7) and
|
||||
[0.7.6](https://github.com/mvschwarz/openrig/releases/tag/v0.7.6) limits, still
|
||||
apply.
|
||||
|
||||
---
|
||||
|
||||
## What changed in OpenRig 0.7.9
|
||||
|
||||
The changes from 0.7.8 to 0.7.9, published on 2026-11-06, for an install
|
||||
upgrading from 0.7.8 or earlier.
|
||||
|
||||
### After the upgrade
|
||||
|
||||
- **Restart the daemon** to load the new code: `rig daemon stop`, then
|
||||
`rig daemon start`.
|
||||
- **Migrations 146 to 149** record a Pi seat's guidance file, what OpenRig last
|
||||
wrote in each managed guidance block, an index for reading a rig's outbox,
|
||||
and the stream intake position.
|
||||
- **Tell the person that stream intake is on.** Each `rig stream emit` now opens
|
||||
an intake row for the kernel queue worker (`queue.worker`), or adds a note to
|
||||
the one still unclaimed, and the row wakes the queue worker like any queue row.
|
||||
Earlier stream history isn't replayed. If they want 0.7.8's behaviour, with no
|
||||
intake rows, run `rig config set stream.intake off`.
|
||||
- **The bundled openrig-core plugin goes from 0.1.24 to 0.1.25.** Its
|
||||
seat-continuity-and-handover skill changed. Managed refresh preserves replaced
|
||||
files under `$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits
|
||||
you still need from the preserved copy.
|
||||
- **Where seat guidance goes now:**
|
||||
- a fresh Pi seat gets OpenRig's guidance in its own agent folder's
|
||||
`AGENTS.md`, unless that file or folder is a link;
|
||||
- a fresh Codex seat on Codex 0.164.0 or later gets it as launch developer
|
||||
instructions, not in the repository `AGENTS.md`, when no config layer
|
||||
OpenRig reads sets `developer_instructions`;
|
||||
- a Codex resume, fork, restore or handover successor, an older or unknown
|
||||
Codex, and a config layer that sets `developer_instructions` or can't be read
|
||||
still use the repository `AGENTS.md`;
|
||||
- if only the person's shell startup sets `CODEX_HOME`, OpenRig reads
|
||||
`~/.codex`, so a `developer_instructions` set in their home is replaced for
|
||||
that session. Declare the same absolute path as `CODEX_HOME` in the member's
|
||||
or rig's `env:` to have it checked. `docs/reference/agent-startup-guide.md`
|
||||
("Codex guidance at launch") is the rule;
|
||||
- a managed block a person edited by hand is kept and named
|
||||
(`guidance_block_edited`), not replaced. Read the warning before you change
|
||||
the block.
|
||||
- **Slack:** on the first start after upgrading, the connector edits the asks
|
||||
that ended earlier to show they were handled, at Slack's edit rate: about an
|
||||
hour for 450 asks. `rig slack status` shows pending edits.
|
||||
- **OpenShell:** rebuild a guest image built for 0.7.8 with this release's
|
||||
helper; an older helper rejects the new startup step, and the error names the
|
||||
rebuild.
|
||||
|
||||
### Runner seats
|
||||
|
||||
Section 7 of `docs/reference/harness-runner-contract.md` is the rule for
|
||||
permissions, and section 4 for activity. Read them before writing or changing a
|
||||
runner.
|
||||
- **Typed words:** a reply of just yes or no counts as the agent's allow or
|
||||
reject. Any other words choose nothing: OpenRig's default permits the request
|
||||
when the agent offered an allow option, and the read result carries the words
|
||||
whole as `said`. A runner may queue them for the agent as an ordinary prompt
|
||||
after the current turn, never as the answer; one that drops them still keeps
|
||||
the contract. The ask now tells the person this.
|
||||
- **Permit records:** each permit OpenRig decides by default (no one to ask, or
|
||||
an ask closed without a choice, including a row passed to an agent seat) is
|
||||
recorded once, as a stream item from the seat, naming the tool call, why no
|
||||
person answered and the option given. Read them with
|
||||
`rig stream list --tag runner-permission-permit` or `--source <seat>`. A record
|
||||
has no destination and never changes the permit. Stream intake reads past it, so
|
||||
it opens no intake row and wakes no one.
|
||||
- **The working beat:** while a prompt is outstanding and the agent is alive, a
|
||||
runner may post `active` / `liveness` so a long call keeps reading working.
|
||||
After delivering an answer or permit, it posts `active` /
|
||||
`permission_resolved`. A runner that sends no beats behaves as before. The Pi
|
||||
runner and the dsh example post it.
|
||||
- **Check a runner** with
|
||||
`node daemon/docs/reference/runner-conformance/check-runner.mjs --runner '<command with {agent}>' --name <harness>`.
|
||||
It now covers the beat, and no longer falsely fails a correct runner on a busy
|
||||
machine.
|
||||
- **The dsh example** reads `DSH_SEAT_BIN` and `DSH_SEAT_HOME` from the member's
|
||||
`env:`, and section 8 names ACP's `session/resume`.
|
||||
|
||||
### OpenShell workers
|
||||
|
||||
0.7.9 includes a repair for the OpenShell worker that wasn't usable in 0.7.8,
|
||||
but the worker still doesn't work in 0.7.9. Don't rely on this path in 0.7.9.
|
||||
Startup reaches the guest through a hash-checked first-input file, and nothing
|
||||
is typed through the host viewer. Readiness checks the guest's own execution,
|
||||
and a failed owned delete reports the stage that failed. Release testing on
|
||||
0.7.9 then found two more problems: the guest's Claude couldn't reach its model
|
||||
(`SELF_SIGNED_CERT_IN_CHAIN`), because the guest launcher left out the CA
|
||||
settings the OpenShell exec supplies, and deleting an owned environment never
|
||||
sent the delete, because its inventory step misread a "Not Found" reply. 0.7.10's
|
||||
fixes for both are described above. "Never grant the guest access to the
|
||||
daemon's main API port (normally 7433)."
|
||||
|
||||
### Assistants connected through a grant service (experimental)
|
||||
|
||||
Experimental, unsupported, off by default, and at the person's own risk. With
|
||||
`rig mcp serve --http --grant-service <url>` and a grant service the person
|
||||
supplies and runs, a connected assistant can claim, update and park the rows
|
||||
addressed to its own seat, and see its team with `rig_team`, within the scope its
|
||||
existing tools have. Nothing wakes it: an item stays queued until its receiver
|
||||
claims it. Name the outside service, and the exposure of serving MCP over HTTP,
|
||||
to the person before you set this up. `docs/reference/hosted-members.md` is the
|
||||
rule.
|
||||
|
||||
### Other changes to act on
|
||||
|
||||
- **Held messages:** a seat that starts, or takes over a seat, with held
|
||||
messages is told how many there are. Read them with
|
||||
`rig seat held-messages <seat>`.
|
||||
- **Unconfirmed sends:** `rig send --verify` now reports unconfirmed and exits 1
|
||||
when it can't see what its Enter did, and `rig walk` reports unconfirmed
|
||||
instead of lost. Inspect the recipient before any resend.
|
||||
- **Deadlines:** to accept a re-estimate without unclaiming the work, run
|
||||
`rig queue update <id> --closure-required-at <ISO>`.
|
||||
- **Shared workspaces:** any registered person can settle another's ask as
|
||||
themselves, and the first committed answer wins; a later answer gets 409
|
||||
`qitem_already_terminal` and records nothing. A person registered with no
|
||||
connector binding is asked on Steering and in the Feed only.
|
||||
- **Handover:** a fresh or rebuild successor's first turn lists its startup
|
||||
files, role first. A successor that started without a required startup file is
|
||||
marked in the seat list.
|
||||
- **Ambiguous rig names:** when an active rig name matches more than one rig,
|
||||
`rig env down`, `status` and `logs` list the matching rig IDs and stop, and
|
||||
`rig doctor --spec` reports conformance as unknown. Use the rig ID.
|
||||
- **Seat activity:** window samples older than their bound no longer decide a
|
||||
seat's activity, so a seat whose pane can't be read reads unknown rather than
|
||||
its last verdict. Treat unknown as unknown, not idle.
|
||||
- **Launch options:** `specs.builtins_visible: false` hides built-in specs
|
||||
(`rig specs ls --include-builtins` still lists them); `plugin_channels` gives
|
||||
one Claude member its own `--channels`, untested with a real channel plugin.
|
||||
- **Files:** `rig down` removes OpenRig's own additions from a working folder
|
||||
once no live seat uses them. `rig export` with no `-o` writes
|
||||
`rig-export.yaml` beside an existing `rig.yaml`. `rig workspace validate` exits
|
||||
1 for malformed optional frontmatter.
|
||||
- **Kernels:** a new default kernel starts in `<workspace.root>/kernel`.
|
||||
Existing kernels stay where they are. `docs/reference/agent-startup-guide.md`
|
||||
explains the optional move; a fresh launch there starts a new native
|
||||
conversation, so ask the person before moving.
|
||||
- **PATH:** if preflight warns that a Codex seat's login shell can't find `rig`,
|
||||
follow its fix. `getting-started.md` explains the `~/.profile` line and how to
|
||||
check it with `bash -lc 'command -v node rig'`.
|
||||
|
||||
### Limits to keep in view
|
||||
|
||||
- **OpenShell:** the worker doesn't work in 0.7.9 (above). A same-generation
|
||||
reconciled launch opens no viewer, so run `rig environment connect` yourself;
|
||||
non-text startup actions aren't supported on environment seats.
|
||||
- **Open updates for a person** read a 1,001-row window, so a page can come back
|
||||
short, marked `truncated`.
|
||||
- **Slack:** a private channel needs the documented route, which isn't tested,
|
||||
and direct messages aren't supported. The channel and people reads haven't
|
||||
been run against a live Slack workspace. Redaction of the bot token from a
|
||||
transport error can still leave a malformed token fragment shorter than six
|
||||
characters, so review an error before sharing it.
|
||||
- **SSH host status** reads `unknown` behind a ProxyJump or ProxyCommand; that
|
||||
isn't proof the connection is broken. `rig host ls` reads the ssh config with
|
||||
`ssh -G`, which runs any `Match exec` command in it.
|
||||
- **Shared asks:** a click's option written before its answer row can stand
|
||||
beside or inside the closer's answer without the first clicker's authorship.
|
||||
- **Permit records** last as long as stream retention keeps them. A permit a
|
||||
runner makes on its own, when it can't reach OpenRig, is in its pane only. For
|
||||
an ask opened before 0.7.9, a record may omit the option's kind.
|
||||
- **Existing seats** pick up Codex launch instructions and the single
|
||||
openrig-core skill load at their next fresh launch. A `CLAUDE.md` or
|
||||
`AGENTS.md` that already grew a line per launch isn't trimmed.
|
||||
- **Codex hooks after an upgrade:** Codex sessions kept running across the
|
||||
upgrade show "Hook failed — plugin contents changed; hook approval required"
|
||||
until relaunched.
|
||||
- **0.7.8's limits** on the dsh `env:` names, conformance false failures, typed
|
||||
words, the README's operator address, a symlinked rebuild, the rebuild receipt,
|
||||
a re-parked click and unrecorded default permits are fixed or changed. The
|
||||
other 0.7.8 limits below, and the
|
||||
[0.7.7](https://github.com/mvschwarz/openrig/releases/tag/v0.7.7) and
|
||||
[0.7.6](https://github.com/mvschwarz/openrig/releases/tag/v0.7.6) limits, still
|
||||
apply.
|
||||
|
||||
---
|
||||
|
||||
## What changed in OpenRig 0.7.8
|
||||
|
||||
The changes from 0.7.7 to 0.7.8, published on 2026-11-05, for an install
|
||||
upgrading from 0.7.7 or earlier.
|
||||
|
||||
### After the upgrade
|
||||
|
||||
- **Restart the daemon** to load the new code: `rig daemon stop`, then
|
||||
`rig daemon start`.
|
||||
- **Migrations 142 to 145** store a Claude seat's MCP opt-out, seat activity
|
||||
grants, OpenShell environment bindings and a seat's chosen permission person.
|
||||
- **The bundled openrig-core plugin goes from 0.1.23 to 0.1.24.** One skill line
|
||||
changed. Managed refresh preserves replaced files under
|
||||
`$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you still
|
||||
need from the preserved copy.
|
||||
|
||||
### Runner seats: a person answers permission requests
|
||||
|
||||
Section 7 of `docs/reference/harness-runner-contract.md` is the rule. Read it
|
||||
before writing or changing a runner.
|
||||
- **Who is asked:**
|
||||
- **the default person** (`rig gateway default`, or the only registered
|
||||
person) gets an ordinary ask in the Feed and on Slack;
|
||||
- **a chosen person,** after
|
||||
`rig seat set-permissions-to <seat> --to <person> --reason "…"`. `--clear`
|
||||
returns to the default;
|
||||
- **no one:** when no one can be asked, the request is "Permitted at once,
|
||||
with the agent's own allow option";
|
||||
- **refusal** is opt-in, with `--deny`. `rig seat status` shows the choice.
|
||||
- **What a person's answer does:** a click gives that option, a plain yes gives
|
||||
the allow, and a plain no gives the reject. "A person's no is never a grant."
|
||||
An unanswered request stays pending until the agent's turn ends.
|
||||
- **The runner's protocol:**
|
||||
- `POST /api/activity/permission-requests` submits the request;
|
||||
- `GET …/<askId>?waitSeconds=25` reads the answer (25 at most);
|
||||
- `POST …/<askId>/withdraw` withdraws it at the turn's end.
|
||||
|
||||
Deliver an answer once, only to the live request.
|
||||
- **Check a runner** with
|
||||
`node daemon/docs/reference/runner-conformance/check-runner.mjs --runner '<command with {agent}>' --name <harness>`
|
||||
from the installed package. It needs Node and tmux, and no daemon, model or
|
||||
network.
|
||||
- **The worked example** is the dsh runner under
|
||||
`daemon/docs/reference/community-harness-examples/dsh/`.
|
||||
- **A seat activity grant** lets a runner outside the host's hook setup report
|
||||
activity for one seat and launch, without the install-wide hook token.
|
||||
|
||||
### OpenShell workers
|
||||
|
||||
0.7.8 includes `rig environment` (`setup`, `create`, `attach`) and a reference
|
||||
for binding a seat to an NVIDIA OpenShell sandbox (`daemon/assets/openshell/README.md`).
|
||||
**In our release testing the worker never became usable:** after the sandbox was
|
||||
created and the native process started, `rig environment connect` failed when its
|
||||
check timed out after 5 seconds, so no team work ran. Don't rely on this path in
|
||||
0.7.8; the later repairs are described above.
|
||||
|
||||
**What it needs:**
|
||||
- NVIDIA OpenShell 0.1.6;
|
||||
- a Linux image;
|
||||
- an API-backed provider using `ANTHROPIC_API_KEY`. Subscription or OAuth
|
||||
credentials aren't supported.
|
||||
|
||||
"Provider registration and image construction can incur costs", so name them to
|
||||
the person before you start. "Never grant the guest access to the daemon's main
|
||||
API port (normally 7433)."
|
||||
|
||||
### Other changes to act on
|
||||
|
||||
- **Mission following:** `rig terminal open mission:<id> --follow --provider herdr`
|
||||
follows a mission's current owners, with the same Herdr path as slices.
|
||||
- **Launch choices:**
|
||||
- `claude_mcp: none` on a Claude member launches it without MCP servers;
|
||||
- `rig config set terminal.tmux_server isolated` gives this OpenRig home its
|
||||
own persistent tmux server.
|
||||
- **Hosts:** `rig host edit` and `rig host remove` correct or retire a host
|
||||
entry. Use them instead of editing `hosts.yaml` by hand.
|
||||
- **Another user's daemon:** when `rig doctor` warns that the port is held by
|
||||
another OS user's daemon, follow its fix (`rig config set daemon.port <free-port>`),
|
||||
or set `OPENRIG_URL` to share that daemon on purpose.
|
||||
- **tmux control mode:** with `OPENRIG_TMUX_CONTROL=1`, OpenRig starts a control
|
||||
client only on tmux 3.7c or newer, and uses spawned clients otherwise.
|
||||
- **Read-only tmux clients:** a submit key refused because of one goes through
|
||||
the paste route.
|
||||
- **Slack:** a typed reply to a decision parked on its person is recorded as the
|
||||
answer. Read the person's words; don't treat the close as approval.
|
||||
- **The TUI:** agent pages now show health findings and recently finished items,
|
||||
and an agent spec's "not loaded in this view" means it wasn't read, not that
|
||||
nothing runs it.
|
||||
|
||||
### Limits to keep in view
|
||||
|
||||
- ~~**No harness has run end to end under the runner contract yet.** The dsh
|
||||
example and the conformance check were checked against stand-ins.~~ *Corrected
|
||||
after release:* 0.7.8's release testing ran a real dsh 0.2.1-rc.2 through the
|
||||
shipped dsh example runner for a person's allow-once and reject-once, and
|
||||
through an outside runner, dsh-seat 0.3.0, for launch, queue work, multiline
|
||||
input, activity, cancel and exact resume. The conformance check itself runs
|
||||
against a stand-in agent.
|
||||
- **The answer owner is a registered person,** and a colleague's answer is
|
||||
followed on this host only. A request handed to another host waits as needs
|
||||
input until the turn ends.
|
||||
- **Permits and answers aren't recorded** beyond the runner's pane and the ask.
|
||||
If the agent offers only `allow_always`, a permit makes its harness remember a
|
||||
permission nobody chose.
|
||||
- **Coming as follow-ups:** recording permits made when no one can be asked, and
|
||||
passing typed words that aren't a plain yes or no to the agent.
|
||||
- **The dsh example keeps one hold per seat:** two asks in one turn can leave one
|
||||
waiting after the hold is released.
|
||||
- **An OpenShell worker's activity reads unknown,** sampled from the pane, never
|
||||
idle.
|
||||
- **The [0.7.7](https://github.com/mvschwarz/openrig/releases/tag/v0.7.7) and
|
||||
[0.7.6](https://github.com/mvschwarz/openrig/releases/tag/v0.7.6) limits still apply.**
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/daemon",
|
||||
@@ -7262,7 +7262,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -7300,7 +7300,7 @@
|
||||
},
|
||||
"packages/daemon": {
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.13.0",
|
||||
@@ -7342,7 +7342,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"type": "module",
|
||||
"description": "Build your own network of agents from Claude Code, Codex and Pi: persistent teams with roles, shared context and owned work.",
|
||||
"keywords": [
|
||||
|
||||
@@ -104,6 +104,13 @@ Keys:
|
||||
retention.* enabled, transitions_days, watchdog_days,
|
||||
watchdog_keep_per_job, batch_size
|
||||
launch.non_interruptive accept harness first-launch warnings at full bypass for new rigs (default off)
|
||||
launch.codex_login_shell defer to Codex's own login-shell setting in managed Codex seats (default off)
|
||||
Off adds allow_login_shell=false: tool commands inherit the seat's environment, so plain rig
|
||||
works wherever OpenRig is installed. Inherited setup isn't re-run per command: setup only in
|
||||
login or rc files (including a tmux default-command that isn't a login shell) and shell
|
||||
functions or aliases may be absent. On omits that override; it doesn't force login shells,
|
||||
and a login file that sets PATH outright can then hide rig. Applies at each seat's next
|
||||
launch; a restart in place keeps the running choice.
|
||||
terminal.inherit_tmux_environment inherit stored tmux global values for new agents and successors (default off)
|
||||
POSIX launches otherwise start from daemon + seat environment, preserving shell startup.
|
||||
Turn on for custom setups that intentionally rely on tmux globals. Running agents are unchanged.
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
|
||||
export interface RiggedConfig {
|
||||
specs: { builtinsVisible: boolean };
|
||||
launch: { nonInterruptive: boolean };
|
||||
launch: { nonInterruptive: boolean; codexLoginShell: boolean };
|
||||
daemon: { port: number; host: string; dbConflict?: "warn" | "stop" };
|
||||
// OPR.0.4.6.MH1 FR-1 — the persisted host-selection pointer.
|
||||
// OPR.0.4.6.MH1 FR-4 — the own-host display name (default "localhost").
|
||||
@@ -267,7 +267,7 @@ const DEFAULT_CLAUDE_COMPACTION_EXTRA_INSTRUCTION_FILE_PATH = getDefaultOpenRigP
|
||||
);
|
||||
|
||||
const DEFAULTS = {
|
||||
launch: { nonInterruptive: false },
|
||||
launch: { nonInterruptive: false, codexLoginShell: false },
|
||||
daemon: { port: 7433, host: "127.0.0.1", dbConflict: "warn" },
|
||||
// OPR.0.4.6.MH1 FR-1 — "local" ≡ no remote selection (LOCAL_HOST_ID).
|
||||
// FR-4 — own-host display name; default "localhost" (PRD-named).
|
||||
@@ -470,6 +470,7 @@ export const VALID_KEYS = [
|
||||
"skills.router_line.enabled",
|
||||
"onboarding.default_pack.enabled",
|
||||
"launch.non_interruptive",
|
||||
"launch.codex_login_shell",
|
||||
"health.context_pressure.warning_percent",
|
||||
"health.context_pressure.critical_percent",
|
||||
"files.allowlist",
|
||||
@@ -598,6 +599,7 @@ export const ENV_MAP: Record<ValidKey, { primary: string; legacy?: string }> = {
|
||||
"skills.router_line.enabled": { primary: "OPENRIG_SKILLS_ROUTER_LINE_ENABLED" },
|
||||
"onboarding.default_pack.enabled": { primary: "OPENRIG_ONBOARDING_DEFAULT_PACK_ENABLED" },
|
||||
"launch.non_interruptive": { primary: "OPENRIG_LAUNCH_NON_INTERRUPTIVE" },
|
||||
"launch.codex_login_shell": { primary: "OPENRIG_LAUNCH_CODEX_LOGIN_SHELL" },
|
||||
"health.context_pressure.warning_percent": { primary: "OPENRIG_HEALTH_CONTEXT_PRESSURE_WARNING_PERCENT" },
|
||||
"health.context_pressure.critical_percent": { primary: "OPENRIG_HEALTH_CONTEXT_PRESSURE_CRITICAL_PERCENT" },
|
||||
// UEP env-var graduation: existing OPENRIG_FILES_ALLOWLIST /
|
||||
@@ -707,6 +709,7 @@ const KEY_TO_PATH: Record<ValidKey, string[]> = {
|
||||
"skills.router_line.enabled": ["skills", "routerLine", "enabled"],
|
||||
"onboarding.default_pack.enabled": ["onboarding", "defaultPack", "enabled"],
|
||||
"launch.non_interruptive": ["launch", "nonInterruptive"],
|
||||
"launch.codex_login_shell": ["launch", "codexLoginShell"],
|
||||
"health.context_pressure.warning_percent": ["health", "contextPressure", "warningPercent"],
|
||||
"health.context_pressure.critical_percent": ["health", "contextPressure", "criticalPercent"],
|
||||
"files.allowlist": ["files", "allowlist"],
|
||||
@@ -1329,7 +1332,7 @@ export class ConfigStore {
|
||||
auditLog: v("feed.subscriptions.audit_log") as boolean,
|
||||
},
|
||||
},
|
||||
launch: { nonInterruptive: v("launch.non_interruptive") as boolean },
|
||||
launch: { nonInterruptive: v("launch.non_interruptive") as boolean, codexLoginShell: v("launch.codex_login_shell") as boolean },
|
||||
runtime: {
|
||||
readinessTimeoutSeconds: v("runtime.readiness_timeout_seconds") as number,
|
||||
codex: {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { it, expect, vi } from "vitest";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { ConfigStore } from "../src/config-store.js";
|
||||
import { SettingsStore } from "../../daemon/src/domain/user-settings/settings-store.js";
|
||||
it("launch.codex_login_shell defaults off in both stores, with file and env precedence", () => {
|
||||
const home=mkdtempSync(join(tmpdir(),"codex-login-shell-")), file=join(home,"config.json");
|
||||
const cli=new ConfigStore(file), daemon=new SettingsStore(file);
|
||||
try {
|
||||
vi.stubEnv("OPENRIG_LAUNCH_CODEX_LOGIN_SHELL",undefined);
|
||||
expect(cli.resolveWithSource("launch.codex_login_shell")).toMatchObject({value:false,source:"default"});
|
||||
expect(daemon.resolveOne("launch.codex_login_shell")).toMatchObject({value:false,source:"default"});
|
||||
writeFileSync(file, JSON.stringify({launch:{codexLoginShell:true}}));
|
||||
expect(cli.resolveWithSource("launch.codex_login_shell")).toMatchObject({value:true,source:"file"});
|
||||
expect(daemon.resolveOne("launch.codex_login_shell")).toMatchObject({value:true,source:"file"});
|
||||
vi.stubEnv("OPENRIG_LAUNCH_CODEX_LOGIN_SHELL","false");
|
||||
expect(cli.resolveWithSource("launch.codex_login_shell")).toMatchObject({value:false,source:"env"});
|
||||
expect(daemon.resolveOne("launch.codex_login_shell")).toMatchObject({value:false,source:"env"});
|
||||
} finally { vi.unstubAllEnvs(); rmSync(home,{recursive:true,force:true}); }
|
||||
});
|
||||
@@ -143,6 +143,7 @@ describe("ConfigStore — extended namespaces (User Settings v0)", () => {
|
||||
"skills.router_line.enabled",
|
||||
"onboarding.default_pack.enabled",
|
||||
"launch.non_interruptive",
|
||||
"launch.codex_login_shell",
|
||||
"health.context_pressure.warning_percent",
|
||||
"health.context_pressure.critical_percent",
|
||||
"files.allowlist", "progress.scan_roots",
|
||||
|
||||
@@ -10,8 +10,8 @@ already does it, or two verbs crossed already answer it. **You cannot look up a
|
||||
not know exists.** So the point of what follows is not skill. It is recognition: that when a task
|
||||
lands, something rhymes, and you go check instead of building.
|
||||
|
||||
Ninety-one top-level verbs ship (capability canon refreshed through
|
||||
`capability-delta-v0.7.9`). This marker describes the pack's teaching, not publication or live
|
||||
Ninety-two top-level verbs ship (capability canon refreshed through
|
||||
`capability-delta-v0.7.10`). This marker describes the pack's teaching, not publication or live
|
||||
adoption. Read this once for shape, and let it make you suspicious that a thing already exists.
|
||||
What changed in the installed version, and what to do about it, is one read away:
|
||||
`rig context get reference/whats-new.md`.
|
||||
@@ -56,7 +56,12 @@ one command away. Your memory of the fleet is a claim about the past.
|
||||
and whether the thing you did an hour ago actually landed. Before a relaunch, it shows what the
|
||||
seat's next fresh managed launch will ask for, labelled planned rather than observed: the native
|
||||
permission argument, where each part comes from, what OpenRig adds and the commands that change
|
||||
it, plus the seat's terminal policy.
|
||||
it, plus the seat's terminal policy. **When a Claude seat seems stuck on a permission prompt, read
|
||||
this before capturing panes:** with openrig-core 0.1.26's `PermissionRequest` hook it shows the
|
||||
latest observed permission request (time, actor and a fixed command descriptor that hides the
|
||||
arguments) and Claude's bounded suggested rules, which can contain paths, patterns or sensitive
|
||||
values. It's evidence, not an applied permission: it applies and approves nothing, and it's held
|
||||
in memory for five minutes and lost on a daemon restart (`docs/reference/seat-delivery.md`).
|
||||
- **`rig view list` / `rig view show <lens>`** — named lenses over coordination state: use
|
||||
`view show escalations` for owner attention, `view show pickup` for claimed-row state, and
|
||||
`view show execution` for done/now/next and current authored planning guidance with its
|
||||
@@ -141,8 +146,9 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
holds only a suggestion, or typed text beside one. Don't use it to type over a half-written box:
|
||||
to reach a seat with something that can't wait, use `--urgent` where its typing-guard level lets
|
||||
urgent input through.
|
||||
A send to a pane in tmux copy or view mode leaves the mode once and delivers; a pane that can't
|
||||
leave is refused with `pane_in_mode`. With `transport.verify_input_box` on (off by default),
|
||||
A send or wake to a pane in a tmux mode leaves every mode first and delivers, including a
|
||||
chooser, customize or clock mode a person has open; a pane that can't leave is refused with
|
||||
`pane_in_mode`, and the refusal names why. With `transport.verify_input_box` on (off by default),
|
||||
`--verify` to Claude Code or Codex reads verified only when the text was seen in the input box
|
||||
and the box was empty after Enter; it only reports.
|
||||
- **`rig broadcast`** — one fact to everyone at once, rather than relaying it N times and getting
|
||||
@@ -200,6 +206,20 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
judge and close. Each permission request is a row to `--permissions-to` (default: you). The guide
|
||||
is `docs/reference/acp-agents.md` in the source repo, and `docs/reference/a2a-agents.md` covers
|
||||
A2A agents.
|
||||
- **`rig exec run <row> --harness claude|codex`** — a finite task for Claude Code or Codex without
|
||||
building a seat: the same single turn through that harness's ACP adapter, run by a detached
|
||||
connector that outlives the command, so closing the terminal or leaving `--wait` doesn't stop it.
|
||||
Install the adapters first (`@agentclientprotocol/claude-agent-acp` 0.101.0 and
|
||||
`@agentclientprotocol/codex-acp` 2.5.0); `rig exec harnesses` shows the versions found, and
|
||||
`--plan` starts nothing. **The row stays the custody record:** it gets the same notes as `rig acp
|
||||
run`, each permission request is a row to `--permissions-to`, and the outcome is the agent's
|
||||
report, so accepting the work stays with the row's custodian. One run per row: running it again
|
||||
returns that run, and `--again` starts another turn once it has ended. `rig exec status
|
||||
<runId|row>` reads it; a connector that died without an outcome reads `interrupted`, outcome
|
||||
unknown. **`rig exec cancel <runId>`** (not `rig acp cancel`) asks the agent to stop its turn;
|
||||
the acknowledgement isn't a settled shutdown and says nothing about further spend. It hasn't run
|
||||
with a real adapter yet, and concurrent runs, long tasks and recovery across a reboot aren't
|
||||
covered. `rig acp run` stays the synchronous form, in your terminal, for any ACP agent.
|
||||
- **`rig terminal open <view>`** — open a desktop terminal with live agent tiles; a window is the
|
||||
default. **Opening `rig terminal open saved:kernel --window` is the step after install or start**:
|
||||
say you're opening it, run it on the daemon's desktop, then tell the person where it opened. Don't
|
||||
@@ -790,6 +810,10 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
ID is verified.
|
||||
Managed Codex launches turn off Codex's startup update check, so no update notice stops them;
|
||||
updating Codex is the operator's job.
|
||||
Managed Codex seats run tool commands in a non-login shell, so plain `rig` works however OpenRig
|
||||
was installed (with a Codex that supports `allow_login_shell`); setup that lives only in login or
|
||||
rc files, and shell functions and aliases, may be missing there. `rig config set
|
||||
launch.codex_login_shell true` defers to Codex's own setting at each seat's next launch.
|
||||
- **`rig seat set-model` / `stop` / `clean`** — persist the model for later managed resumes, stop
|
||||
exactly one live seat (ending its session and any work in progress), or clear a dead seat's
|
||||
stale binding. A stop is recorded, so the rig doesn't read `degraded` for that seat, and a rig
|
||||
@@ -875,9 +899,17 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
kept short enough to run as a command and is sent only to an idle seat; its own result reads
|
||||
`submitted, unconfirmed` (it reached the seat, not proof compaction ran), `uncertain` (not
|
||||
replayed) or `not sent`, so confirm by effect. Before each paste (preparation prompt, `/compact`,
|
||||
restore prompt) it checks the input box and holds that step while the box holds typed,
|
||||
unreadable or changed input, so a draft needs no typing guard to protect it; the check is a read just before the paste,
|
||||
not a lock. An idle Claude seat under the installed-update notice still reads idle, so it needs no
|
||||
then the turn boundary, restore request and audit) it checks the input box, so a draft needs no
|
||||
typing guard to protect it. Typed, unreadable or changed input holds the preparation prompt and
|
||||
`/compact`. After `/compact`, a box that reads typed still holds the step, so a typed draft left
|
||||
there holds the restore until it's cleared or you take over; but after two idle polls in a row
|
||||
that can't read the box, the next poll sends the step, recorded as delivered over a box OpenRig
|
||||
couldn't read (never as empty), so a draft OpenRig can't recognise there may receive the restore
|
||||
text. The step after a `/compact` whose result was lost, and a seat whose draft-aware delivery or
|
||||
typing guard holds OpenRig's input, still hold. To take over a held restore, run `rig compact
|
||||
<seat> --cancel`, then send a restore request naming the newest restore map, the restore marker
|
||||
and its packet; a step already being pasted can still land. The check is a read just before the
|
||||
paste, not a lock. An idle Claude seat under the installed-update notice still reads idle, so it needs no
|
||||
hand compaction for that. Claude seats that select `openrig-core` load its
|
||||
hooks; a seat can give a slow refocus more time with `OPENRIG_REFOCUS_BUDGET_MS` (4500 to 52000).
|
||||
The refocus work packet stays off by default for Claude Code and Codex;
|
||||
@@ -889,7 +921,8 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
through the next retry interval; `--state` shows `idleDeadlineAt`. In either mode, expiry reads
|
||||
`map_complete_seat_never_idle` when the map is finished, or `preparation_incomplete` when it is not.
|
||||
`rig seat status` shows an owed restore stage and its last
|
||||
delivery result, and repeated delivery failures open one queue item for the seat's orchestrator, or a
|
||||
delivery result, including a step that went in over an unreadable box, and repeated delivery
|
||||
failures open one queue item for the seat's orchestrator, or a
|
||||
person when it has none.
|
||||
- **Codex seats get `openrig-core` too: its refocus after a compaction and its skills, with no
|
||||
first-launch review stop.** OpenRig installs the plugin into Codex on boot and on a fresh launch.
|
||||
@@ -1057,6 +1090,14 @@ working arrangement becomes something someone else can instantiate.
|
||||
baseline, and at launch the runner says whether the seat has a stored sign-in or provider key
|
||||
(advisory; launch continues). A provider key reaches a Pi seat only when its name is in
|
||||
`recovery.provider_auth_env_allowlist`; `MINIMAX_API_KEY` serves `minimax/<id>` models.
|
||||
- **`rig agent validate <agent.yaml>`** — check an agent spec before you relaunch from it; that it
|
||||
launched doesn't make it right. It and every launch (`rig up`, a restore, a first-start retry)
|
||||
print `agent spec advisory` lines, which never make a spec invalid: an unknown key under a
|
||||
profile's `uses`, or a single value where `uses.<category>` takes a list. That single value is
|
||||
read as a one-item list and resolves like one, so **a member whose profile selects a resource
|
||||
that isn't available fails that member's resolution** ("not found in resource pool"), as the
|
||||
list form does; it isn't only a warning. A native resume that continues a saved conversation
|
||||
shows no advisories.
|
||||
- **`rig context trace --pod <pod>`** — walk the context chain through instance → rig → pod → seat
|
||||
when the pod altitude matters.
|
||||
- **`rig specs show` / `preview` / `add` / `sync` / `rename` / `remove`** — where a spec lives,
|
||||
@@ -1332,8 +1373,9 @@ your circumstances is configuration, and the ones that are not, another agent ca
|
||||
stop them without killing the rig.
|
||||
- **`rig environment`** — not `rig env`: places a worker seat in an NVIDIA OpenShell sandbox and returns
|
||||
verified result files (`setup`, `create` or `attach`, `export`; reference in the packaged
|
||||
`daemon/assets/openshell/README.md`). In 0.7.8's release testing the worker never became usable, so
|
||||
read the installed version's release notes before relying on it.
|
||||
`daemon/assets/openshell/README.md`). In 0.7.8's release testing the worker never became usable;
|
||||
0.7.10 fixes in the source the two problems that stopped it in 0.7.9, but it hasn't run end to
|
||||
end. Read the installed version's release notes before relying on it.
|
||||
- **`rig setup`** — what OpenRig would change about this machine, shown before it touches anything.
|
||||
It installs herdr by default on macOS and Linux (`--no-herdr` declines; if the install fails,
|
||||
views fall back to plain tmux) and on a Mac offers Ghostty once (`--ghostty` / `--no-ghostty`).
|
||||
@@ -1521,6 +1563,6 @@ deliberate writing.
|
||||
|
||||
**`rig --help`**, then `rig <verb> --help`.
|
||||
|
||||
Ninety-one verbs ship, and this page named most of them once. **You will not remember which — you
|
||||
Ninety-two verbs ship, and this page named most of them once. **You will not remember which — you
|
||||
are meant to remember only that the list is long enough to be worth reading before you build
|
||||
anything.** The question is never *how do I write this*; it is **does this already exist**.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { setTimeout as sleep } from "node:timers/promises";
|
||||
import type { TmuxAdapter } from "./tmux.js";
|
||||
import type { SeatLaunchEnvironment } from "../domain/seat-launch-environment.js";
|
||||
import type { ResumeResult } from "./claude-resume.js";
|
||||
import { assessNativeResumeProbe, buildCodexResumeCore } from "../domain/native-resume-probe.js";
|
||||
import { assessNativeResumeProbe, buildCodexResumeCore, codexNonLoginShellArg } from "../domain/native-resume-probe.js";
|
||||
import { verifyCodexPaneProcess, verifyPaneIdleShell, type NativeProcessLister } from "../domain/native-process-lineage.js";
|
||||
import { execRuntimeProbe } from "./runtime-probe-exec.js";
|
||||
import { shellQuote } from "./shell-quote.js";
|
||||
@@ -38,6 +38,8 @@ interface CodexResumeOptions {
|
||||
detectDaemonSupport?: CodexDaemonSupportDetector;
|
||||
/** #275: Codex's own answer on the plain floor's network default; absent keeps the existing invocation. */
|
||||
readNetworkDefault?: CodexNetworkDefaultReader;
|
||||
/** launch.codex_login_shell: true keeps Codex's own login shells for tool commands. Absent: off. */
|
||||
codexLoginShell?: () => boolean;
|
||||
/** Ensure the managed Codex config (hooks, trust) just before a resume, as a runtime-adapter launch does, in the
|
||||
* seat's own home when it has one. */
|
||||
ensureManagedConfig?: (codexHome?: string) => Promise<void>;
|
||||
@@ -138,7 +140,7 @@ export class CodexResumeAdapter {
|
||||
workspaceArg.trim() || undefined,
|
||||
resolvedPosture,
|
||||
model,
|
||||
`${postureArg}${networkArg}`,
|
||||
`${postureArg}${networkArg}${this.options.codexLoginShell?.() ? "" : codexNonLoginShellArg}`,
|
||||
daemonSupport?.kind === "supported",
|
||||
effort,
|
||||
) + ownRoots.filter((root): root is string => !!root).map((root) => ` --add-dir ${shellQuote(root)}`).join("");
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
readCodexThreadIdFromCandidateHomes,
|
||||
type ResolveHomeDirByPid,
|
||||
} from "../domain/codex-thread-id.js";
|
||||
import { assessNativeResumeProbe, buildCodexResumeCore, hasCodexUpdateHeader, type NativeResumeProbeResult } from "../domain/native-resume-probe.js";
|
||||
import { assessNativeResumeProbe, buildCodexResumeCore, codexNonLoginShellArg, hasCodexUpdateHeader, type NativeResumeProbeResult } from "../domain/native-resume-probe.js";
|
||||
import { unknownDaemonSupportMessage, type CodexDaemonSupportDetector } from "../domain/codex-daemon-support.js";
|
||||
import { CODEX_NETWORK_DEFAULT_OVERRIDE, codexNetworkDefaultArg, type CodexNetworkDefaultReader } from "../domain/codex-network-default.js";
|
||||
import { resolveCodexGitAddDirs, type CodexGitAddDirResolver } from "../domain/codex-git-add-dirs.js";
|
||||
@@ -95,6 +95,8 @@ type CodexProcess = NativeProcessRow;
|
||||
export interface RunningCodexLaunch {
|
||||
networkOverride: boolean;
|
||||
daemonOptOut: boolean;
|
||||
/** Whether it was launched with tool commands in a non-login shell (`allow_login_shell=false`). */
|
||||
nonLoginShell: boolean;
|
||||
addDirs: string[];
|
||||
}
|
||||
|
||||
@@ -115,6 +117,8 @@ interface PreparedCodexLaunch {
|
||||
daemonOptOut: boolean;
|
||||
daemonArg: string;
|
||||
networkArg: string;
|
||||
/** Keeps OpenRig's command folder on the PATH of the seat's tool commands; see codexNonLoginShellArg. */
|
||||
loginShellArg: string;
|
||||
}
|
||||
|
||||
export interface CodexAdapterFsOps {
|
||||
@@ -176,6 +180,8 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
// #275: Codex's own answer on whether the plain floor may get network access. Startup wires the
|
||||
// real reader; absent keeps every invocation unchanged.
|
||||
private readNetworkDefault?: CodexNetworkDefaultReader;
|
||||
// launch.codex_login_shell: true keeps Codex's own login shells for tool commands. Absent: off.
|
||||
private codexLoginShell?: () => boolean;
|
||||
// Issue #121: git metadata dirs for the `--add-dir`s of fresh, resume and fork (a linked worktree's `.git` is a file).
|
||||
// Default = the real resolver; tests may inject a controlled one.
|
||||
private resolveGitAddDirs: CodexGitAddDirResolver;
|
||||
@@ -230,6 +236,7 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
profileFingerprint?: (path: string) => Promise<ProfileFingerprint>;
|
||||
detectDaemonSupport?: CodexDaemonSupportDetector;
|
||||
readNetworkDefault?: CodexNetworkDefaultReader;
|
||||
codexLoginShell?: () => boolean;
|
||||
resolveGitAddDirs?: CodexGitAddDirResolver;
|
||||
/** The seat's own topology folder, ready to write (its LEARNED.md and recap). Absent: no seat folder is added. */
|
||||
prepareSeatFolder?: PrepareCodexSeatFolder;
|
||||
@@ -267,6 +274,7 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
this.launchPath = deps.launchPath;
|
||||
this.detectDaemonSupport = deps.detectDaemonSupport;
|
||||
this.readNetworkDefault = deps.readNetworkDefault;
|
||||
this.codexLoginShell = deps.codexLoginShell;
|
||||
this.resolveGitAddDirs = deps.resolveGitAddDirs ?? resolveCodexGitAddDirs;
|
||||
this.activityRelayPath = deps.activityRelayPath;
|
||||
this.listProcesses = deps.listProcesses ?? defaultListProcesses;
|
||||
@@ -900,7 +908,7 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
// Only a launch that starts a conversation may install the core plugin; a resume never does.
|
||||
const prepared = await this.prepareLaunch(binding, opts.name, undefined, { installCorePlugin: !opts.resumeToken });
|
||||
if (!prepared.ok) return prepared;
|
||||
const { modelArg, effortArg, appliedLaunch, postureArg, queueStateDirArg, gitDirArg, seatFolderArg, daemonArg, networkArg } = prepared;
|
||||
const { modelArg, effortArg, appliedLaunch, postureArg, queueStateDirArg, gitDirArg, seatFolderArg, daemonArg, networkArg, loginShellArg } = prepared;
|
||||
|
||||
// Fork branch: `codex fork <parent_thread_id>`. Captures the NEW thread id
|
||||
// post-fork. Parent thread id is NOT persisted onto the new seat record
|
||||
@@ -922,7 +930,7 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
// 0.5.2-07 A2-3: the FORK path threads the SPEC model too (fork-instantiate reverted it before).
|
||||
// The git dirs and the seat folder follow the parent id: ps flattens argv, so a path with a space placed before the
|
||||
// id would be read as the id when the fork is verified.
|
||||
const cmd = `codex${daemonArg}${postureArg}${networkArg}${modelArg}${effortArg} fork${queueStateDirArg} ${shellQuote(parentId)}${gitDirArg}${seatFolderArg}`;
|
||||
const cmd = `codex${daemonArg}${postureArg}${networkArg}${loginShellArg}${modelArg}${effortArg} fork${queueStateDirArg} ${shellQuote(parentId)}${gitDirArg}${seatFolderArg}`;
|
||||
const textResult = await this.tmux.sendShellCommand(binding.tmuxSession, await this.seatCommand(binding, cmd));
|
||||
if (!textResult.ok) {
|
||||
return { ok: false, error: `Failed to send launch command: ${textResult.message}` };
|
||||
@@ -959,7 +967,7 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
// 0.5.2-07 A2-3: the pod-aware RESUME path threads the SPEC model too (reverted before — the
|
||||
// grounding map assumed codex parity with the claude adapter, but only fresh emitted -m).
|
||||
? this.resumeCore(prepared, opts.resumeToken, binding)
|
||||
: `codex${daemonArg}${postureArg}${networkArg} -C ${shellQuote(binding.cwd)}${gitDirArg}${seatFolderArg}${queueStateDirArg}${modelArg}${effortArg}${binding.guidanceLaunchInstructions ? this.developerInstructionsArg(binding.tmuxSession) : ""}`;
|
||||
: `codex${daemonArg}${postureArg}${networkArg}${loginShellArg} -C ${shellQuote(binding.cwd)}${gitDirArg}${seatFolderArg}${queueStateDirArg}${modelArg}${effortArg}${binding.guidanceLaunchInstructions ? this.developerInstructionsArg(binding.tmuxSession) : ""}`;
|
||||
|
||||
const staged = await this.seatCommand(binding, cmd);
|
||||
if (opts.beforeNativeDispatch && !opts.beforeNativeDispatch()) return { ok: false, error: "Selected history changed during Codex launch preparation; no native command was sent.", recovery: "attention_required" };
|
||||
@@ -1088,7 +1096,8 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
gitDirArg: running.addDirs.map((dir) => ` --add-dir ${shellQuote(dir)}`).join(""),
|
||||
seatFolderArg: "",
|
||||
daemonOptOut: running.daemonOptOut, daemonArg: running.daemonOptOut ? " --no-daemon" : "",
|
||||
networkArg: running.networkOverride ? ` -c ${shellQuote(CODEX_NETWORK_DEFAULT_OVERRIDE)}` : "" };
|
||||
networkArg: running.networkOverride ? ` -c ${shellQuote(CODEX_NETWORK_DEFAULT_OVERRIDE)}` : "",
|
||||
loginShellArg: running.nonLoginShell ? codexNonLoginShellArg : "" };
|
||||
}
|
||||
|
||||
// OPR.0.3.4.7 — profile-LOAD probe before launch/resume. A legacy
|
||||
@@ -1135,14 +1144,15 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
|
||||
// The launch facts keep the probed daemon choice, so a printed recovery command can repeat it; no probe, no fact.
|
||||
const launchFacts = daemonSupport ? { ...appliedLaunch, daemonOptOut } : appliedLaunch;
|
||||
return { ok: true, model, modelArg, effort, effortArg, profile, appliedLaunch: launchFacts, postureArg, queueStateDirArg, gitDirArg, seatFolderArg, daemonOptOut, daemonArg, networkArg };
|
||||
return { ok: true, model, modelArg, effort, effortArg, profile, appliedLaunch: launchFacts, postureArg, queueStateDirArg, gitDirArg, seatFolderArg, daemonOptOut, daemonArg, networkArg,
|
||||
loginShellArg: this.codexLoginShell?.() ? "" : codexNonLoginShellArg };
|
||||
}
|
||||
|
||||
private resumeCore(prepared: PreparedCodexLaunch, resumeToken: string, binding: NodeBinding): string {
|
||||
const { profile, queueStateDirArg, gitDirArg, seatFolderArg, model, postureArg, networkArg, daemonOptOut, effort } = prepared;
|
||||
const { profile, queueStateDirArg, gitDirArg, seatFolderArg, model, postureArg, networkArg, loginShellArg, daemonOptOut, effort } = prepared;
|
||||
// The git dirs and the seat folder follow the conversation id, as on a fork, so the resume proof reads the id even
|
||||
// from a path with a space.
|
||||
return buildCodexResumeCore(resumeToken, profile, false, queueStateDirArg.trim() || undefined, binding.launchPosture, model, `${postureArg}${networkArg}`, daemonOptOut, effort)
|
||||
return buildCodexResumeCore(resumeToken, profile, false, queueStateDirArg.trim() || undefined, binding.launchPosture, model, `${postureArg}${networkArg}${loginShellArg}`, daemonOptOut, effort)
|
||||
+ gitDirArg + seatFolderArg;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,12 @@ export function buildNativeResumeCommand(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Managed Codex seats run tool commands in a non-login shell, so they keep the seat's own environment, including the
|
||||
* OpenRig command folder at the front of PATH. Codex otherwise gives each command the user's login-shell environment
|
||||
* and puts back only its own directories, so a login file that sets PATH outright drops that folder. */
|
||||
export const CODEX_NON_LOGIN_SHELL_OVERRIDE = "allow_login_shell=false";
|
||||
export const codexNonLoginShellArg = ` -c ${shellQuote(CODEX_NON_LOGIN_SHELL_OVERRIDE)}`;
|
||||
|
||||
export function buildCodexResumeCore(
|
||||
resumeToken: string,
|
||||
codexConfigProfile?: string | null,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { rebindAndVerifyPaneIdentity } from "./seat-attention-reconciler.js";
|
||||
import type { RunningCodexLaunch } from "../adapters/codex-runtime-adapter.js";
|
||||
import { emittedEffortFromArgv } from "./seat-effort.js";
|
||||
import { CODEX_NETWORK_DEFAULT_OVERRIDE } from "./codex-network-default.js";
|
||||
import { CODEX_NON_LOGIN_SHELL_OVERRIDE } from "./native-resume-probe.js";
|
||||
|
||||
/**
|
||||
* OPR.0.7.0.5 — restart ONE Codex seat in place so it reads its Codex home's current account: the same
|
||||
@@ -1007,7 +1008,8 @@ function runningLaunch(argv: string[]): RunningCodexLaunch {
|
||||
else if ((arg === "-c" || arg === "--config") && argv[index + 1] !== undefined) configs.push(argv[++index]!);
|
||||
else if (arg.startsWith("--config=")) configs.push(arg.slice("--config=".length));
|
||||
}
|
||||
return { networkOverride: configs.includes(CODEX_NETWORK_DEFAULT_OVERRIDE), daemonOptOut: argv.includes("--no-daemon"), addDirs };
|
||||
return { networkOverride: configs.includes(CODEX_NETWORK_DEFAULT_OVERRIDE), daemonOptOut: argv.includes("--no-daemon"),
|
||||
nonLoginShell: configs.includes(CODEX_NON_LOGIN_SHELL_OVERRIDE), addDirs };
|
||||
}
|
||||
|
||||
/** The settings a Codex command line runs with. cwd, add-dirs, images and the resume target are not settings. */
|
||||
|
||||
@@ -2904,7 +2904,10 @@ export class SessionTransport {
|
||||
let failure = "";
|
||||
const failed = (message: string) => { failure = ` Leaving it failed: ${message.trim().replace(/\s+/g, " ").replace(/\.$/, "")}.`; };
|
||||
try {
|
||||
const left = typeof this.tmuxAdapter.cancelPaneMode === "function" ? await this.tmuxAdapter.cancelPaneMode(readTarget) : undefined;
|
||||
// Leave through the seat's address, as the text and Enter writes do; the guard writes to its bound pane. A raw
|
||||
// pane id can match a stale binding elsewhere (tmux reuses ids after a server restart), and the guard refuses an
|
||||
// ambiguous target before writing anything, so the mode was never left.
|
||||
const left = typeof this.tmuxAdapter.cancelPaneMode === "function" ? await this.tmuxAdapter.cancelPaneMode(sessionName) : undefined;
|
||||
if (left && !left.ok) failed(left.message);
|
||||
} catch (error) { failed(error instanceof Error ? error.message : String(error)); }
|
||||
if (await readPaneInMode() !== true) return null;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// The tile commands earlier releases generated, verbatim, so a view kept across an upgrade is recognised only by
|
||||
// a command OpenRig actually generated for the same seat. Kept views exist from 0.7.5 on.
|
||||
// 0.7.9: tile-attach.ts at 7cabac44 (#2464). 0.7.5 through 0.7.8: tile-attach.ts at 80616b79 (#2133).
|
||||
export function tileAttachArgv079(
|
||||
tmux: string, server: readonly string[], session: string, readOnly: boolean,
|
||||
retrySeconds = 30,
|
||||
): string[] {
|
||||
// tmux honours ignore-size server-wide: while any client of any session sizes
|
||||
// its window, ignore-size clients size nothing, so a session they alone view
|
||||
// keeps its 80x24 creation size. A writable tile therefore ignores size only
|
||||
// when its session already has a sizing client.
|
||||
const ro = readOnly ? "-r " : "$ro ";
|
||||
const pick = (target: string) => (readOnly ? "" : `pick ${target} "$@"; `);
|
||||
// No apostrophes or control characters in the script: Herdr also stores this
|
||||
// argv as its restart command. All external values stay positional arguments.
|
||||
const script = [
|
||||
'target=$1; shift;',
|
||||
...(readOnly ? [] : [
|
||||
// Shell builtins only: the provider PATH may hold nothing but tmux.
|
||||
'pick() { s=$1; shift; ro=; for f in $("$@" list-clients -t "$s" -F "#{client_flags}" 2>/dev/null); do',
|
||||
'case ",$f," in *,ignore-size,*) ;; *) ro="-f ignore-size";; esac; done; };',
|
||||
]),
|
||||
`snapshot=$("$@" display-message -p -t "=$target:" "#{pid} #{session_id}:#{session_name}" 2>/dev/null) || { ${pick('"=$target"')}exec "$@" attach ${ro}-t "=$target"; };`,
|
||||
// display-message may succeed with empty/fallback context for a missing target.
|
||||
`if [ "\${snapshot#*:}" != "$target" ]; then ${pick('"=$target"')}exec "$@" attach ${ro}-t "=$target"; fi;`,
|
||||
'snapshot=${snapshot%%:*};',
|
||||
'node=$("$@" show-environment -t "${snapshot#* }" OPENRIG_NODE_ID 2>/dev/null) || node=;',
|
||||
'case "$node" in OPENRIG_NODE_ID=?*) ;; *) node=;; esac;',
|
||||
'if [ -z "$node" ]; then',
|
||||
'printf "%s\\n" "No managed identity for $target; reopen this view after replacement.";',
|
||||
`${pick('"${snapshot#* }"')}exec "$@" attach ${ro}-t "\${snapshot#* }"; fi;`,
|
||||
'while :; do',
|
||||
`${pick('"${snapshot#* }"')}"$@" attach ${ro}-t "\${snapshot#* }";`,
|
||||
`left=${retrySeconds}; same=0;`,
|
||||
'while [ "$left" -gt 0 ]; do',
|
||||
'next=$("$@" display-message -p -t "=$target:" "#{pid} #{session_id}:#{session_name}" 2>/dev/null) || next=;',
|
||||
'if [ "${next#*:}" = "$target" ]; then next=${next%%:*}; else next=; fi;',
|
||||
'if [ -n "$next" ]; then',
|
||||
'identity=$("$@" show-environment -t "${next#* }" OPENRIG_NODE_ID 2>/dev/null) || identity=;',
|
||||
// A session can disappear between the two metadata reads; unknown waits.
|
||||
'if [ -n "$identity" ]; then',
|
||||
'if [ "$identity" != "$node" ]; then printf "%s\\n" "Seat identity changed for $target; reopen its view."; exit 1; fi;',
|
||||
'if [ "$next" != "$snapshot" ]; then snapshot=$next; break; fi;',
|
||||
// The stop adapter detaches BEFORE killing. Give that kill a chance to
|
||||
// finish; an ordinary Ctrl-b d leaves the original alive and ends the tile.
|
||||
'same=$((same + 1)); if [ "$same" -ge 3 ]; then exit 0; fi;',
|
||||
'else same=0; fi; else same=0; fi;',
|
||||
'sleep 1; left=$((left - 1));',
|
||||
'done;',
|
||||
'if [ "$left" -eq 0 ]; then printf "%s\\n" "No replacement for $target; reopen the view with rig terminal open."; exit 0; fi;',
|
||||
'done',
|
||||
].join(" ");
|
||||
return ["/bin/sh", "-c", script, "openrig-tile", session, tmux, ...server];
|
||||
}
|
||||
|
||||
export function tileAttachArgv078(
|
||||
tmux: string, server: readonly string[], session: string, readOnly: boolean,
|
||||
retrySeconds = 30,
|
||||
): string[] {
|
||||
const ro = readOnly ? "-r " : "-f ignore-size ";
|
||||
// No apostrophes or control characters in the script: Herdr also stores this
|
||||
// argv as its restart command. All external values stay positional arguments.
|
||||
const script = [
|
||||
'target=$1; shift;',
|
||||
`snapshot=$("$@" display-message -p -t "=$target:" "#{pid} #{session_id}:#{session_name}" 2>/dev/null) || exec "$@" attach ${ro}-t "=$target";`,
|
||||
// display-message may succeed with empty/fallback context for a missing target.
|
||||
`if [ "\${snapshot#*:}" != "$target" ]; then exec "$@" attach ${ro}-t "=$target"; fi;`,
|
||||
'snapshot=${snapshot%%:*};',
|
||||
'node=$("$@" show-environment -t "${snapshot#* }" OPENRIG_NODE_ID 2>/dev/null) || node=;',
|
||||
'case "$node" in OPENRIG_NODE_ID=?*) ;; *) node=;; esac;',
|
||||
'if [ -z "$node" ]; then',
|
||||
'printf "%s\\n" "No managed identity for $target; reopen this view after replacement.";',
|
||||
`exec "$@" attach ${ro}-t "\${snapshot#* }"; fi;`,
|
||||
'while :; do',
|
||||
`"$@" attach ${ro}-t "\${snapshot#* }";`,
|
||||
`left=${retrySeconds}; same=0;`,
|
||||
'while [ "$left" -gt 0 ]; do',
|
||||
'next=$("$@" display-message -p -t "=$target:" "#{pid} #{session_id}:#{session_name}" 2>/dev/null) || next=;',
|
||||
'if [ "${next#*:}" = "$target" ]; then next=${next%%:*}; else next=; fi;',
|
||||
'if [ -n "$next" ]; then',
|
||||
'identity=$("$@" show-environment -t "${next#* }" OPENRIG_NODE_ID 2>/dev/null) || identity=;',
|
||||
// A session can disappear between the two metadata reads; unknown waits.
|
||||
'if [ -n "$identity" ]; then',
|
||||
'if [ "$identity" != "$node" ]; then printf "%s\\n" "Seat identity changed for $target; reopen its view."; exit 1; fi;',
|
||||
'if [ "$next" != "$snapshot" ]; then snapshot=$next; break; fi;',
|
||||
// The stop adapter detaches BEFORE killing. Give that kill a chance to
|
||||
// finish; an ordinary Ctrl-b d leaves the original alive and ends the tile.
|
||||
'same=$((same + 1)); if [ "$same" -ge 3 ]; then exit 0; fi;',
|
||||
'else same=0; fi; else same=0; fi;',
|
||||
'sleep 1; left=$((left - 1));',
|
||||
'done;',
|
||||
'if [ "$left" -eq 0 ]; then printf "%s\\n" "No replacement for $target; reopen the view with rig terminal open."; exit 0; fi;',
|
||||
'done',
|
||||
].join(" ");
|
||||
return ["/bin/sh", "-c", script, "openrig-tile", session, tmux, ...server];
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { HerdrViewer, ViewerCreateNotDispatchedError, findAttributedViewerPanes,
|
||||
import { DEFAULT_FOLLOW_POLICY, planFollowingTiles, type FollowControls, type FollowPolicy, type FollowTile, type FollowingStatus } from "./terminal-follow-policy.js";
|
||||
import type { ComposedPane, ComposedView, OpenViewResult } from "./terminal-provider.js";
|
||||
import type { HerdrTransport } from "./herdr-transport.js";
|
||||
import { earlierTileAttachment } from "./tile-attach.js";
|
||||
|
||||
export type ReadFollowInput = (target: ComposedPane) => Promise<"empty" | "text" | "unknown">;
|
||||
interface Tile extends FollowTile { member: ViewerKey; target: ComposedPane; viewer: ViewerMapping; reason?: string }
|
||||
@@ -167,6 +168,11 @@ export class HerdrFollowView {
|
||||
const currentMember = view.task.members.find(m => keyOf(m.key.hostId, m.key.nodeId) === tile.key);
|
||||
if (currentMember?.availability === "available") {
|
||||
const target = view.opened.find(p => p.seat === currentMember.seat);
|
||||
// An upgrade can change only the tile script; the kept tile then follows the current target.
|
||||
if (target?.attachArgv && tile.target.attachArgv && target.paneCommand !== tile.target.paneCommand
|
||||
&& target.readOnly === tile.target.readOnly && earlierTileAttachment(tile.target.attachArgv, target.attachArgv, target.readOnly)) {
|
||||
tile.target = target;
|
||||
}
|
||||
if (!target || target.paneCommand !== tile.target.paneCommand || target.readOnly !== tile.target.readOnly
|
||||
|| JSON.stringify(target.attachArgv) !== JSON.stringify(tile.target.attachArgv)) {
|
||||
throw new Error(`${tile.seat}: Core's current attachment differs from the retained viewer; keeping the previous view unconfirmed.`);
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from "node:path";
|
||||
import { getDefaultOpenRigPath } from "../../openrig-compat.js";
|
||||
import { resolveOnPath } from "../../adapters/tmux-exec.js";
|
||||
import type { ComposedPane } from "./terminal-provider.js";
|
||||
import { earlierTileAttachment } from "./tile-attach.js";
|
||||
|
||||
export interface ViewerKey { hostId: string; nodeId: string }
|
||||
export interface ViewerMapping { workspaceId: string; paneId: string; terminalId: string; readOnly: boolean }
|
||||
@@ -133,6 +134,12 @@ export class HerdrViewer {
|
||||
const id = JSON.stringify([key.hostId, key.nodeId]);
|
||||
entry = journal.entries[id];
|
||||
let created = false;
|
||||
// An upgrade can change only the tile script (argv[2]); the seat it attaches is the same, so the kept
|
||||
// viewer moves to the current script.
|
||||
if (entry && entry.command !== target.paneCommand && entry.readOnly === target.readOnly && Array.isArray(entry.argv)
|
||||
&& earlierTileAttachment(entry.argv, target.attachArgv, target.readOnly)) {
|
||||
entry.command = target.paneCommand; entry.argv = target.attachArgv; if (provision) save();
|
||||
}
|
||||
if (entry && (entry.command !== target.paneCommand || entry.readOnly !== target.readOnly)) {
|
||||
return unconfirmed("The seat attachment changed; the existing viewer must be inspected before reprovisioning.");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { tileAttachArgv078, tileAttachArgv079 } from "./earlier-tile-attach.js";
|
||||
|
||||
// A tile owns its attach process; no daemon watcher or steady-state polling.
|
||||
// Session names survive fresh launch, handover and restart. The node identity
|
||||
// must survive too: never follow a same-name session belonging to another seat.
|
||||
@@ -44,3 +46,15 @@ export function tileAttachArgv(
|
||||
].join(" ");
|
||||
return ["/bin/sh", "-c", script, "openrig-tile", session, tmux, ...server];
|
||||
}
|
||||
|
||||
/** Whether a kept tile is the one an earlier release generated for the same seat: exactly the command 0.7.9's, or
|
||||
* 0.7.5-0.7.8's, script makes for the current tile's session, tmux, server and read-only intent. An upgrade changes
|
||||
* only OpenRig's own script; any other difference, an edited script included, is a different attachment. */
|
||||
export function earlierTileAttachment(kept: readonly string[], current: readonly string[], readOnly: boolean): boolean {
|
||||
if (current.length < 6) return false;
|
||||
const [session, tmux, ...server] = current.slice(4) as [string, string, ...string[]];
|
||||
return [tileAttachArgv079, tileAttachArgv078].some((make) => {
|
||||
const earlier = make(tmux, server, session, readOnly);
|
||||
return earlier.length === kept.length && earlier.every((word, i) => word === kept[i]);
|
||||
});
|
||||
}
|
||||
@@ -112,6 +112,7 @@ export const SETTINGS_VALID_KEYS = [
|
||||
"skills.router_line.enabled",
|
||||
"onboarding.default_pack.enabled",
|
||||
"launch.non_interruptive",
|
||||
"launch.codex_login_shell",
|
||||
"health.context_pressure.warning_percent",
|
||||
"health.context_pressure.critical_percent",
|
||||
"files.allowlist",
|
||||
@@ -273,6 +274,7 @@ const ENV_MAP: Record<SettingsValidKey, { primary: string; legacy?: string }> =
|
||||
"skills.router_line.enabled": { primary: "OPENRIG_SKILLS_ROUTER_LINE_ENABLED" },
|
||||
"onboarding.default_pack.enabled": { primary: "OPENRIG_ONBOARDING_DEFAULT_PACK_ENABLED" },
|
||||
"launch.non_interruptive": { primary: "OPENRIG_LAUNCH_NON_INTERRUPTIVE" },
|
||||
"launch.codex_login_shell": { primary: "OPENRIG_LAUNCH_CODEX_LOGIN_SHELL" },
|
||||
"health.context_pressure.warning_percent": { primary: "OPENRIG_HEALTH_CONTEXT_PRESSURE_WARNING_PERCENT" },
|
||||
"health.context_pressure.critical_percent": { primary: "OPENRIG_HEALTH_CONTEXT_PRESSURE_CRITICAL_PERCENT" },
|
||||
"files.allowlist": { primary: "OPENRIG_FILES_ALLOWLIST" },
|
||||
@@ -380,6 +382,7 @@ const KEY_TO_PATH: Record<SettingsValidKey, string[]> = {
|
||||
"skills.router_line.enabled": ["skills", "routerLine", "enabled"],
|
||||
"onboarding.default_pack.enabled": ["onboarding", "defaultPack", "enabled"],
|
||||
"launch.non_interruptive": ["launch", "nonInterruptive"],
|
||||
"launch.codex_login_shell": ["launch", "codexLoginShell"],
|
||||
"health.context_pressure.warning_percent": ["health", "contextPressure", "warningPercent"],
|
||||
"health.context_pressure.critical_percent": ["health", "contextPressure", "criticalPercent"],
|
||||
"files.allowlist": ["files", "allowlist"],
|
||||
@@ -677,6 +680,7 @@ function getDefaultValue(key: SettingsValidKey, workspaceRoot: string): string |
|
||||
case "skills.router_line.enabled": return false;
|
||||
case "onboarding.default_pack.enabled": return true;
|
||||
case "launch.non_interruptive": return false;
|
||||
case "launch.codex_login_shell": return false;
|
||||
case "health.context_pressure.warning_percent": return 95;
|
||||
case "health.context_pressure.critical_percent": return 99;
|
||||
// Preview Terminal v0 (PL-018) defaults — match cli/src/config-store.ts.
|
||||
|
||||
@@ -761,7 +761,9 @@ export async function createDaemon(opts?: DaemonOptions): Promise<DaemonResult>
|
||||
// A Codex seat's own topology folder (its LEARNED.md and recap) as a writable root; topology.root is read per launch.
|
||||
const prepareSeatFolder = (sessionName: string) => prepareCodexSeatFolder(sessionName,
|
||||
String(new ContextPackSettingsStore().resolveOne("topology.root").value));
|
||||
const codexResume = new CodexResumeAdapter(tmuxAdapter, { ensureManagedConfig: async (home?: string) => { await codexAdapter.ensureManagedConfig(home); }, prepareTeamWorkspace, prepareSeatFolder, resolveGitAddDirs: resolveCodexGitAddDirs, seatLaunchEnvironment, codexHome: configuredCodexHome, launchPath: process.env.PATH, detectDaemonSupport: codexDaemonSupportProbe(process.env.PATH, undefined, configuredCodexHome), readNetworkDefault: readCodexNetworkDefault });
|
||||
// launch.codex_login_shell, read at each Codex launch (absent or unreadable = off).
|
||||
const readCodexLoginShell = () => { try { return new ContextPackSettingsStore().resolveOne("launch.codex_login_shell").value === true; } catch { return false; } };
|
||||
const codexResume = new CodexResumeAdapter(tmuxAdapter, { ensureManagedConfig: async (home?: string) => { await codexAdapter.ensureManagedConfig(home); }, prepareTeamWorkspace, prepareSeatFolder, resolveGitAddDirs: resolveCodexGitAddDirs, seatLaunchEnvironment, codexHome: configuredCodexHome, launchPath: process.env.PATH, detectDaemonSupport: codexDaemonSupportProbe(process.env.PATH, undefined, configuredCodexHome), readNetworkDefault: readCodexNetworkDefault, codexLoginShell: readCodexLoginShell });
|
||||
// OPR.0.4.6.PI1 — the Pi seat-state root + the compiled runner entry (daemon
|
||||
// dist). Shared by the Pi runtime adapter, the resume adapter, and the
|
||||
// resume-token capture sidecar reader.
|
||||
@@ -965,7 +967,7 @@ export async function createDaemon(opts?: DaemonOptions): Promise<DaemonResult>
|
||||
// #2456: the adapters' guidance writes check and keep OpenRig's record of each managed block.
|
||||
const guidanceBlockRecords = managedBlockRecords(db);
|
||||
const claudeAdapter = new ClaudeCodeAdapter({ tmux: tmuxAdapter, seatLaunchEnvironment, claudeManagedLaunch, restoreReadPolicy, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), copyFile: replaceFile, listFiles: (dir: string) => { const r: string[] = []; /** Collect relative file paths recursively. */ function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, readdir: (dir: string) => fs.readdirSync(dir), statMode: (p: string) => fs.statSync(p).mode, chmod: (p: string, m: number) => fs.chmodSync(p, m), homedir: os.homedir() }, stateDir: OPENRIG_HOME, collectorAssetPath: claudeCollectorAssetPath, autoDriveProviderPrompts: runtimeSettings.recoveryAutoDriveProviderPrompts, activityRelayPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs"), claudeHooksManifestPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/claude.json"), managedBlockRecords: guidanceBlockRecords, recordProjection: (targetPath: string, content: string) => projectionManifestStore.record({ targetPath, lastHash: hashContent(content), writtenAt: new Date().toISOString() }) });
|
||||
const codexAdapter = new CodexRuntimeAdapter({ managedBlockRecords: guidanceBlockRecords, prepareTeamWorkspace, prepareSeatFolder, tmux: tmuxAdapter, seatLaunchEnvironment, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), listFiles: (dir: string) => { const r: string[] = []; /** Collect relative file paths recursively. */ function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, statMode: (p: string) => fs.statSync(p).mode, chmod: (p: string, m: number) => fs.chmodSync(p, m), homedir: daemonHome }, codexHome: configuredCodexHome, seatHome: (nodeId: string, ignoreAccountHome?: boolean) => seatRuntimeHome(db, nodeId, "codex", ignoreAccountHome).home, launchPath: process.env.PATH, detectDaemonSupport: codexDaemonSupportProbe(process.env.PATH, undefined, configuredCodexHome), readNetworkDefault: readCodexNetworkDefault, activityRelayPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs"), resolveCorePluginDir: () => vendoredCorePluginDir(), resolveCorePluginSelected: (nodeId: string) => Boolean(seatCodexCorePluginDir({ id: nodeId, runtime: "codex" }, db)), resolveHooksEnabled: async () => {
|
||||
const codexAdapter = new CodexRuntimeAdapter({ managedBlockRecords: guidanceBlockRecords, prepareTeamWorkspace, prepareSeatFolder, tmux: tmuxAdapter, seatLaunchEnvironment, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), listFiles: (dir: string) => { const r: string[] = []; /** Collect relative file paths recursively. */ function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, statMode: (p: string) => fs.statSync(p).mode, chmod: (p: string, m: number) => fs.chmodSync(p, m), homedir: daemonHome }, codexHome: configuredCodexHome, seatHome: (nodeId: string, ignoreAccountHome?: boolean) => seatRuntimeHome(db, nodeId, "codex", ignoreAccountHome).home, launchPath: process.env.PATH, detectDaemonSupport: codexDaemonSupportProbe(process.env.PATH, undefined, configuredCodexHome), readNetworkDefault: readCodexNetworkDefault, codexLoginShell: readCodexLoginShell, activityRelayPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs"), resolveCorePluginDir: () => vendoredCorePluginDir(), resolveCorePluginSelected: (nodeId: string) => Boolean(seatCodexCorePluginDir({ id: nodeId, runtime: "codex" }, db)), resolveHooksEnabled: async () => {
|
||||
const { SettingsStore } = await import("./domain/user-settings/settings-store.js");
|
||||
return new SettingsStore().resolveOne("runtime.codex.hooks_enabled").value as boolean;
|
||||
}, resolveManagedConfigSettings: async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ async function finalObservation(output: string) {
|
||||
fs.mkdirSync(bin);
|
||||
fs.writeFileSync(path.join(bin, "codex"), `#!${process.execPath}\n` +
|
||||
`const fs = require("node:fs");\n` +
|
||||
`if (process.argv.slice(2).join(" ") !== "-s workspace-write -c check_for_update_on_startup=false resume fixture-id") process.exit(2);\n` +
|
||||
`if (process.argv.slice(2).join(" ") !== "-s workspace-write -c check_for_update_on_startup=false -c allow_login_shell=false resume fixture-id") process.exit(2);\n` +
|
||||
`process.stdout.write("\\n".repeat(36) + "fixture waiting for final observation\\n");\n` +
|
||||
`const tick = setInterval(() => { if (fs.existsSync(${JSON.stringify(gate)})) {\n` +
|
||||
`process.stdout.write(fs.readFileSync(${JSON.stringify(gate)}, "utf8") + "\\n"); clearInterval(tick);\n` +
|
||||
|
||||
@@ -67,12 +67,20 @@ describe("CodexResumeAdapter", () => {
|
||||
|
||||
expect(sendText).toHaveBeenCalledOnce();
|
||||
expect(sendText.mock.calls[0]![0]).toBe("r99-demo1-impl");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' resume 'uuid-123'");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume 'uuid-123'");
|
||||
expect(sendKeys).toHaveBeenCalledOnce();
|
||||
expect(sendKeys.mock.calls[0]![1]).toEqual(["Enter"]);
|
||||
expect(sendText.mock.invocationCallOrder[0]).toBeLessThan(sendKeys.mock.invocationCallOrder[0]!);
|
||||
});
|
||||
|
||||
it("launch.codex_login_shell on: the legacy resume keeps Codex's own login shells", async () => {
|
||||
const sendText = vi.fn(async () => ({ ok: true as const }));
|
||||
const tmux = mockTmux({ sendText, sendKeys: vi.fn(async () => ({ ok: true as const })) });
|
||||
const adapter = new CodexResumeAdapter(tmux, { codexLoginShell: () => true });
|
||||
await adapter.resume("r99-demo1-impl", "codex_id", "uuid-123", "/repo");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' resume 'uuid-123'");
|
||||
});
|
||||
|
||||
it("0.5.2-07: a SPEC-pinned model threads -m onto the legacy codex resume command", async () => {
|
||||
const sendText = vi.fn(async () => ({ ok: true as const }));
|
||||
const sendKeys = vi.fn(async () => ({ ok: true as const }));
|
||||
@@ -81,7 +89,7 @@ describe("CodexResumeAdapter", () => {
|
||||
|
||||
await adapter.resume("r99-demo1-impl", "codex_id", "uuid-123", "/repo", null, undefined, "gpt-5.4-cheap");
|
||||
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -m 'gpt-5.4-cheap' resume 'uuid-123'");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' -m 'gpt-5.4-cheap' resume 'uuid-123'");
|
||||
});
|
||||
|
||||
it("#75: reasoning effort threads -c model_reasoning_effort onto the legacy codex resume command", async () => {
|
||||
@@ -92,7 +100,7 @@ describe("CodexResumeAdapter", () => {
|
||||
|
||||
await adapter.resume("r99-demo1-impl", "codex_id", "uuid-123", "/repo", null, undefined, "gpt-5.4-cheap", "high");
|
||||
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -m 'gpt-5.4-cheap' -c 'model_reasoning_effort=\"high\"' resume 'uuid-123'");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' -m 'gpt-5.4-cheap' -c 'model_reasoning_effort=\"high\"' resume 'uuid-123'");
|
||||
});
|
||||
|
||||
it("codex_last: sendText posture-preserving codex -s workspace-write resume --last", async () => {
|
||||
@@ -104,7 +112,7 @@ describe("CodexResumeAdapter", () => {
|
||||
await adapter.resume("r99-demo1-impl", "codex_last", null, "/repo");
|
||||
|
||||
expect(sendText).toHaveBeenCalledOnce();
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' resume --last");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume --last");
|
||||
expect(sendKeys).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -143,7 +151,7 @@ describe("CodexResumeAdapter", () => {
|
||||
|
||||
await adapter.resume("r99-demo1-impl", "codex_id", "uuid; rm -rf /", "/repo");
|
||||
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' resume 'uuid; rm -rf /'");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume 'uuid; rm -rf /'");
|
||||
});
|
||||
|
||||
it("profile-bearing resume uses -p flag after preflight passes", async () => {
|
||||
@@ -155,7 +163,7 @@ describe("CodexResumeAdapter", () => {
|
||||
await adapter.resume("r99-demo1-impl", "codex_id", "uuid-123", "/repo", "my-profile");
|
||||
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -p 'my-profile' '-c' 'check_for_update_on_startup=false' resume 'uuid-123'");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -p 'my-profile' '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume 'uuid-123'");
|
||||
});
|
||||
|
||||
// A restored seat keeps its own topology folder (its LEARNED.md and recap) writable on the plain floor. A named
|
||||
@@ -166,7 +174,7 @@ describe("CodexResumeAdapter", () => {
|
||||
await new CodexResumeAdapter(mockTmux({ sendText }), { prepareSeatFolder })
|
||||
.resume("r99-demo1-impl", "codex_id", "uuid-123", "/repo");
|
||||
expect(prepareSeatFolder).toHaveBeenCalledWith("r99-demo1-impl");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' resume 'uuid-123' --add-dir '/topology/rigs/demo/seats/r99-demo1-impl'");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume 'uuid-123' --add-dir '/topology/rigs/demo/seats/r99-demo1-impl'");
|
||||
|
||||
const profileText = vi.fn(async () => ({ ok: true as const }));
|
||||
const profileFolder = vi.fn(() => "/topology/rigs/demo/seats/r99-demo1-impl");
|
||||
@@ -185,7 +193,7 @@ describe("CodexResumeAdapter", () => {
|
||||
await new CodexResumeAdapter(mockTmux({ sendText }), { resolveGitAddDirs, prepareSeatFolder })
|
||||
.resume("r99-demo1-impl", "codex_id", "uuid-123", "/repo");
|
||||
expect(resolveGitAddDirs).toHaveBeenCalledWith("/repo");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' resume 'uuid-123' --add-dir '/repo/.git' --add-dir '/topology/rigs/demo/seats/r99-demo1-impl'");
|
||||
expect(sendText.mock.calls[0]![1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume 'uuid-123' --add-dir '/repo/.git' --add-dir '/topology/rigs/demo/seats/r99-demo1-impl'");
|
||||
|
||||
const profileText = vi.fn(async () => ({ ok: true as const }));
|
||||
const profileDirs = vi.fn(async () => ["/repo/.git"]);
|
||||
@@ -456,6 +464,6 @@ describe("a seat's own permission envelope on the legacy resume (argv only)", ()
|
||||
it("keeps the call unchanged without an envelope", async () => {
|
||||
const sendText = vi.fn(async () => ({ ok: true as const }));
|
||||
await new CodexResumeAdapter(mockTmux({ sendText })).resume("r99-demo1-impl", "codex_id", "uuid-123", "/repo");
|
||||
expect((sendText.mock.calls[0] as unknown as [string, string])[1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' resume 'uuid-123'");
|
||||
expect((sendText.mock.calls[0] as unknown as [string, string])[1]).toBe("codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume 'uuid-123'");
|
||||
});
|
||||
});
|
||||
@@ -101,7 +101,7 @@ function expectedFreshLaunchCommand(options: { cwd?: string; model?: string; eff
|
||||
const queueDirArg = options.queueRoot === null ? "" : ` --add-dir ${quote(options.queueRoot ?? testQueueRoot())}`;
|
||||
const modelArg = options.model ? ` -m ${quote(options.model)}` : "";
|
||||
const effortArg = options.effort ? ` -c ${quote(`model_reasoning_effort="${options.effort}"`)}` : "";
|
||||
return `codex -s workspace-write '-c' 'check_for_update_on_startup=false' -C ${quote(cwd)}${gitDirArg}${queueDirArg}${modelArg}${effortArg}`;
|
||||
return `codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' -C ${quote(cwd)}${gitDirArg}${queueDirArg}${modelArg}${effortArg}`;
|
||||
}
|
||||
|
||||
// A resume and a fork add the same git metadata dir as the fresh launch, after the conversation id (`gitCwd` null: none,
|
||||
@@ -114,14 +114,14 @@ function expectedResumeCommand(token = "sess-456", queueRoot: string | null = te
|
||||
const queueDirArg = queueRoot === null ? "" : ` --add-dir ${quote(queueRoot)}`;
|
||||
const modelArg = model ? ` -m ${quote(model)}` : "";
|
||||
const effortArg = effort ? ` -c ${quote(`model_reasoning_effort="${effort}"`)}` : "";
|
||||
return `codex -s workspace-write '-c' 'check_for_update_on_startup=false'${modelArg}${effortArg} resume ${quote(token)}${queueDirArg}${gitDirOption(gitCwd)}`;
|
||||
return `codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false'${modelArg}${effortArg} resume ${quote(token)}${queueDirArg}${gitDirOption(gitCwd)}`;
|
||||
}
|
||||
|
||||
function expectedForkCommand(parentId = "parent-thread-id", options: { model?: string; effort?: string; queueRoot?: string | null; gitCwd?: string | null } = {}): string {
|
||||
const queueDirArg = options.queueRoot === null ? "" : ` --add-dir ${quote(options.queueRoot ?? testQueueRoot())}`;
|
||||
const modelArg = options.model ? ` -m ${quote(options.model)}` : "";
|
||||
const effortArg = options.effort ? ` -c ${quote(`model_reasoning_effort="${options.effort}"`)}` : "";
|
||||
return `codex -s workspace-write '-c' 'check_for_update_on_startup=false'${modelArg}${effortArg} fork${queueDirArg} ${quote(parentId)}${gitDirOption(options.gitCwd === undefined ? "/project" : options.gitCwd)}`;
|
||||
return `codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false'${modelArg}${effortArg} fork${queueDirArg} ${quote(parentId)}${gitDirOption(options.gitCwd === undefined ? "/project" : options.gitCwd)}`;
|
||||
}
|
||||
|
||||
function expectedProfileFreshLaunchCommand(profile: string, options: { cwd?: string; model?: string; queueRoot?: string | null } = {}): string {
|
||||
@@ -129,12 +129,12 @@ function expectedProfileFreshLaunchCommand(profile: string, options: { cwd?: str
|
||||
const gitDirArg = ` --add-dir ${quote(nodePath.join(cwd, ".git"))}`;
|
||||
const queueDirArg = options.queueRoot === null ? "" : ` --add-dir ${quote(options.queueRoot ?? testQueueRoot())}`;
|
||||
const modelArg = options.model ? ` -m ${quote(options.model)}` : "";
|
||||
return `codex -p ${quote(profile)} '-c' 'check_for_update_on_startup=false' -C ${quote(cwd)}${gitDirArg}${queueDirArg}${modelArg}`;
|
||||
return `codex -p ${quote(profile)} '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' -C ${quote(cwd)}${gitDirArg}${queueDirArg}${modelArg}`;
|
||||
}
|
||||
|
||||
function expectedProfileResumeCommand(profile: string, token = "sess-456", queueRoot: string | null = testQueueRoot(), gitCwd: string | null = "/project"): string {
|
||||
const queueDirArg = queueRoot === null ? "" : ` --add-dir ${quote(queueRoot)}`;
|
||||
return `codex -p ${quote(profile)} '-c' 'check_for_update_on_startup=false' resume ${quote(token)}${queueDirArg}${gitDirOption(gitCwd)}`;
|
||||
return `codex -p ${quote(profile)} '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume ${quote(token)}${queueDirArg}${gitDirOption(gitCwd)}`;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -608,6 +608,15 @@ describe("Codex runtime adapter", () => {
|
||||
expect(sendText).toHaveBeenCalledWith("r01-qa", expectedFreshLaunchCommand());
|
||||
});
|
||||
|
||||
it("launch.codex_login_shell on keeps Codex's own login shells: the fresh launch has no non-login flag", async () => {
|
||||
const tmux = mockTmux();
|
||||
const adapter = new CodexRuntimeAdapter({ tmux, fsOps: mockFs(), listProcesses: () => [], sleep: async () => {}, codexLoginShell: () => true });
|
||||
const result = await adapter.launchHarness(makeBinding(), { name: "dev-qa@test-rig" });
|
||||
expect(result.ok).toBe(true);
|
||||
const sendText = tmux.sendText as ReturnType<typeof vi.fn>;
|
||||
expect(sendText).toHaveBeenCalledWith("r01-qa", expectedFreshLaunchCommand().replace(" -c 'allow_login_shell=false'", ""));
|
||||
});
|
||||
|
||||
it("launchHarness passes the requested Codex model on fresh launch", async () => {
|
||||
const tmux = mockTmux();
|
||||
const adapter = new CodexRuntimeAdapter({
|
||||
@@ -983,7 +992,7 @@ describe("Codex runtime adapter", () => {
|
||||
|
||||
const result = await adapter.buildResumeCommand(binding, {
|
||||
name: "dev-qa@test-rig", resumeToken: "sess-456",
|
||||
running: { addDirs: ["/carried/git", "/carried/queue"], daemonOptOut: true, networkOverride: false },
|
||||
running: { addDirs: ["/carried/git", "/carried/queue"], daemonOptOut: true, networkOverride: false, nonLoginShell: false },
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
@@ -995,6 +1004,15 @@ describe("Codex runtime adapter", () => {
|
||||
expect(verifyProfilePreflight).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a relaunch from a running Codex adds the non-login shell flag only when that Codex ran with it", async () => {
|
||||
const adapter = new CodexRuntimeAdapter({ tmux: mockTmux(), fsOps: mockFs() });
|
||||
const running = { addDirs: [], daemonOptOut: false, networkOverride: false };
|
||||
const kept = await adapter.buildResumeCommand(makeBinding(), { name: "dev-qa@test-rig", resumeToken: "sess-456", running: { ...running, nonLoginShell: true } });
|
||||
const unchanged = await adapter.buildResumeCommand(makeBinding(), { name: "dev-qa@test-rig", resumeToken: "sess-456", running: { ...running, nonLoginShell: false } });
|
||||
expect(kept.ok && kept.core).toContain("-c 'allow_login_shell=false' resume 'sess-456'");
|
||||
expect(unchanged.ok && unchanged.core).not.toContain("allow_login_shell");
|
||||
});
|
||||
|
||||
it("launchHarness passes the disposable proof Codex model on fresh launch", async () => {
|
||||
const tmux = mockTmux();
|
||||
const adapter = new CodexRuntimeAdapter({
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { ViewerPane } from "../src/domain/terminal/herdr-viewer.js";
|
||||
import { inspectComposerInput } from "../src/domain/session-transport.js";
|
||||
import type { ComposerSnapshot } from "../src/domain/composer-prompts.js";
|
||||
import { codexTwoRowComposer } from "./fixtures/codex-two-row-composer.js";
|
||||
import { tileAttachArgv078, tileAttachArgv079 } from "../src/domain/terminal/earlier-tile-attach.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
||||
@@ -417,3 +418,44 @@ it("keeps a real empty mission workspace enabled through Core with a mission wai
|
||||
expect((await f.service.status()).following).toHaveLength(1);
|
||||
} finally { await f.service.stop(); }
|
||||
});
|
||||
|
||||
describe("follow views kept from an earlier release", () => {
|
||||
const quote = (s: string) => "'" + s.replace(/'/g, "'\"'\"'") + "'";
|
||||
it.each([["0.7.9", tileAttachArgv079], ["0.7.8", tileAttachArgv078]] as const)(
|
||||
"a writable follow view kept from %s still adds and closes tiles, and moves to the current script", async (_, make) => {
|
||||
const f = fixture(); expect(await f.follow()).toMatchObject({ ok: true, opened: ["a", "b", "c"] });
|
||||
// Rewrite the saved canvas and the viewer journal as that release left them.
|
||||
const earlier = (seat: string) => make("/usr/bin/tmux", ["-S", "/tmp/follow-seats.sock"], seat, false);
|
||||
const canvasFile = path.join(path.dirname(path.dirname(f.journal())), "canvas.json");
|
||||
const canvas = JSON.parse(readFileSync(canvasFile, "utf8"));
|
||||
for (const tile of canvas.tiles) { tile.target.attachArgv = earlier(tile.seat); tile.target.paneCommand = tile.target.attachArgv.map(quote).join(" "); }
|
||||
writeFileSync(canvasFile, JSON.stringify(canvas));
|
||||
const journal = JSON.parse(readFileSync(f.journal(), "utf8"));
|
||||
for (const [id, entry] of Object.entries<{ argv: string[]; command: string }>(journal.entries)) {
|
||||
entry.argv = earlier(JSON.parse(id)[1]); entry.command = entry.argv.map(quote).join(" ");
|
||||
}
|
||||
writeFileSync(f.journal(), JSON.stringify(journal));
|
||||
|
||||
expect(await f.follow(["a", "b", "c", "d"])).toMatchObject({ ok: true, opened: ["a", "b", "c", "d"] });
|
||||
await f.follow(["a", "b", "d"]); f.advance(4000);
|
||||
expect(await f.follow(["a", "b", "d"])).toMatchObject({ ok: true, opened: ["a", "b", "d"] });
|
||||
expect(f.calls.filter(c => c[1] === "close")).toHaveLength(1);
|
||||
const current = task(["a", "b", "d"]).opened;
|
||||
expect(JSON.parse(readFileSync(canvasFile, "utf8")).tiles.map((t: { target: { paneCommand: string } }) => t.target.paneCommand))
|
||||
.toEqual(current.map(t => t.paneCommand));
|
||||
});
|
||||
|
||||
it("holds a kept follow view whose tile script only shares a tile script's opening, and leaves its saved tiles", async () => {
|
||||
const f = fixture(); expect(await f.follow()).toMatchObject({ ok: true, opened: ["a", "b", "c"] });
|
||||
const canvasFile = path.join(path.dirname(path.dirname(f.journal())), "canvas.json");
|
||||
const canvas = JSON.parse(readFileSync(canvasFile, "utf8"));
|
||||
const tile = canvas.tiles[0];
|
||||
tile.target.attachArgv[2] = 'target=$1; shift; printf "%s\\n" "custom viewer";';
|
||||
tile.target.paneCommand = tile.target.attachArgv.map(quote).join(" ");
|
||||
writeFileSync(canvasFile, JSON.stringify(canvas));
|
||||
const before = f.calls.length;
|
||||
expect(await f.follow(["a", "b", "c", "d"])).toMatchObject({ ok: false, following: { state: "retained" }, error: expect.stringContaining("current attachment differs") });
|
||||
expect(f.calls.slice(before).some(c => ["create", "split", "run", "close"].includes(c[1]!))).toBe(false);
|
||||
expect(JSON.parse(readFileSync(canvasFile, "utf8")).tiles[0].target).toEqual(tile.target);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { HerdrViewer, type HerdrViewerRun } from "../src/domain/terminal/herdr-viewer.js";
|
||||
import { tileAttachArgv } from "../src/domain/terminal/tile-attach.js";
|
||||
import { tileAttachArgv078, tileAttachArgv079 } from "../src/domain/terminal/earlier-tile-attach.js";
|
||||
import { composeView } from "../src/domain/terminal/view-composer.js";
|
||||
import { Hono } from "hono";
|
||||
import { HerdrAdapter } from "../src/domain/terminal/herdr-adapter.js";
|
||||
@@ -230,3 +231,70 @@ describe("owned Herdr viewer", () => {
|
||||
expect((await send("Bearer fixture-owner-token")).status).toBe(200); expect(calls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("viewers kept from an earlier release", () => {
|
||||
const quote = (s: string) => "'" + s.replace(/'/g, "'\"'\"'") + "'";
|
||||
const id = JSON.stringify([key.hostId, key.nodeId]);
|
||||
const readOnlyTarget = () => composeView("qitem:qitem-root", [{ seat: "a@team", label: "a", tmuxSession: "a@team", host: null, alive: true, readOnly: true }],
|
||||
{ resolveHost: () => null, localTmux: "/usr/bin/tmux", localTmuxServer: ["-S", "/tmp/own-tmux.sock"] }).opened[0]!;
|
||||
/** Rewrite the journal as an earlier release left it: that release's script for a seat, with the same tmux and server. */
|
||||
function keepFrom(f: ReturnType<typeof fixture>, make: typeof tileAttachArgv079, readOnly: boolean, session = "a@team") {
|
||||
const file = path.join(f.root, readdirSync(f.root).find(n => n.endsWith(".json"))!);
|
||||
const journal = JSON.parse(readFileSync(file, "utf8"));
|
||||
const entry = journal.entries[id];
|
||||
entry.argv = make("/usr/bin/tmux", ["-S", "/tmp/own-tmux.sock"], session, readOnly);
|
||||
entry.command = entry.argv.map(quote).join(" "); entry.readOnly = readOnly;
|
||||
writeFileSync(file, JSON.stringify(journal));
|
||||
return () => JSON.parse(readFileSync(file, "utf8")).entries[id] as { command: string; argv: string[] };
|
||||
}
|
||||
|
||||
it.each([["0.7.9", tileAttachArgv079], ["0.7.8", tileAttachArgv078]] as const)(
|
||||
"keeps a writable viewer recorded by %s and moves it to the current script", async (_, make) => {
|
||||
const f = fixture(); expect((await f.viewer().ensure(key, f.target)).ok).toBe(true);
|
||||
const stored = keepFrom(f, make, false);
|
||||
expect(stored().command).not.toBe(f.target.paneCommand);
|
||||
expect(await f.viewer().lookup(key, f.target)).toMatchObject({ ok: true });
|
||||
expect(await f.viewer().ensure(key, f.target)).toMatchObject({ ok: true, created: false });
|
||||
expect(stored()).toMatchObject({ command: f.target.paneCommand, argv: f.target.attachArgv });
|
||||
expect(f.calls.filter(a => a[1] === "create")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([["0.7.9", tileAttachArgv079], ["0.7.8", tileAttachArgv078]] as const)(
|
||||
"keeps a read-only viewer recorded by %s", async (_, make) => {
|
||||
const f = fixture(); f.setAttachFlags(["-r"]); const target = readOnlyTarget();
|
||||
expect((await f.viewer().ensure(key, target)).ok).toBe(true);
|
||||
keepFrom(f, make, true);
|
||||
expect(await f.viewer().ensure(key, target)).toMatchObject({ ok: true, created: false });
|
||||
expect(f.calls.filter(a => a[1] === "create")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("still holds a kept viewer for another seat, for the other mode, or under a command that isn't a tile script", async () => {
|
||||
const f = fixture(); expect((await f.viewer().ensure(key, f.target)).ok).toBe(true);
|
||||
keepFrom(f, tileAttachArgv079, false, "other@team");
|
||||
expect(await f.viewer().ensure(key, f.target)).toMatchObject({ ok: false, code: "viewer_unconfirmed" });
|
||||
keepFrom(f, tileAttachArgv079, true);
|
||||
expect(await f.viewer().ensure(key, f.target)).toMatchObject({ ok: false, code: "viewer_unconfirmed" });
|
||||
const stored = keepFrom(f, tileAttachArgv079, false);
|
||||
const file = path.join(f.root, readdirSync(f.root).find(n => n.endsWith(".json"))!);
|
||||
const journal = JSON.parse(readFileSync(file, "utf8"));
|
||||
journal.entries[id].argv[2] = "exec /usr/bin/tmux attach"; journal.entries[id].command = journal.entries[id].argv.map(quote).join(" ");
|
||||
writeFileSync(file, JSON.stringify(journal));
|
||||
expect(await f.viewer().ensure(key, f.target)).toMatchObject({ ok: false, code: "viewer_unconfirmed" });
|
||||
expect(stored().argv[2]).toBe("exec /usr/bin/tmux attach");
|
||||
expect(f.calls.filter(a => a[1] === "create")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("holds a kept viewer whose script only shares a tile script's opening, and leaves its saved command", async () => {
|
||||
const f = fixture(); expect((await f.viewer().ensure(key, f.target)).ok).toBe(true);
|
||||
const stored = keepFrom(f, tileAttachArgv079, false);
|
||||
const file = path.join(f.root, readdirSync(f.root).find(n => n.endsWith(".json"))!);
|
||||
const journal = JSON.parse(readFileSync(file, "utf8"));
|
||||
journal.entries[id].argv[2] = 'target=$1; shift; printf "%s\\n" "custom viewer";';
|
||||
journal.entries[id].command = journal.entries[id].argv.map(quote).join(" ");
|
||||
writeFileSync(file, JSON.stringify(journal));
|
||||
const before = stored();
|
||||
expect(await f.viewer().lookup(key, f.target)).toMatchObject({ ok: false, code: "viewer_unconfirmed" });
|
||||
expect(await f.viewer().ensure(key, f.target)).toMatchObject({ ok: false, code: "viewer_unconfirmed" });
|
||||
expect(stored()).toEqual(before);
|
||||
});
|
||||
});
|
||||
@@ -380,7 +380,7 @@ describe("Lifecycle reboot/recovery scenario matrix (Tier 1)", () => {
|
||||
|
||||
const r = await adapter.resume("r99-worker", "codex_id", "tok-abc", "/tmp");
|
||||
|
||||
expect(tmux.sendShellCommand).toHaveBeenCalledWith("r99-worker", "codex -s workspace-write '-c' 'check_for_update_on_startup=false' resume 'tok-abc'");
|
||||
expect(tmux.sendShellCommand).toHaveBeenCalledWith("r99-worker", "codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume 'tok-abc'");
|
||||
expect(tmux.sendText).not.toHaveBeenCalled();
|
||||
expect(r).toEqual({
|
||||
ok: true,
|
||||
|
||||
@@ -40,9 +40,9 @@ describe("0.5.2-07 A2-3 — model-fidelity enumeration guard", () => {
|
||||
it("codex adapter: the resume branch passes the model into buildCodexResumeCore", () => {
|
||||
const src = read("adapters/codex-runtime-adapter.ts");
|
||||
// The LAUNCH resume call threads binding.launchPosture, model, then the exact precomputed
|
||||
// posture segment (followed by #275's network segment). The latter prevents W3 observation
|
||||
// from re-deciding policy.
|
||||
expect(src).toMatch(/buildCodexResumeCore\([^;]*binding\.launchPosture,\s*model,\s*(?:postureArg|`\$\{postureArg\}\$\{networkArg\}`)(?:,\s*daemonOptOut)?(?:,\s*effort)?\)/);
|
||||
// posture segment (followed by #275's network segment and the non-login tool-shell segment). The latter
|
||||
// prevents W3 observation from re-deciding policy.
|
||||
expect(src).toMatch(/buildCodexResumeCore\([^;]*binding\.launchPosture,\s*model,\s*(?:postureArg|`\$\{postureArg\}\$\{networkArg\}(?:\$\{loginShellArg\})?`)(?:,\s*daemonOptOut)?(?:,\s*effort)?\)/);
|
||||
});
|
||||
|
||||
it("claude adapter: every claude seat-launch template threads modelArg", () => {
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("S03 selected arguments and unchanged defaults", () => {
|
||||
});
|
||||
it("Codex legacy resume uses the same explicit selection", async () => {
|
||||
const t = refusedTransport(); await new CodexResumeAdapter(t.tmux).resume("seat", "codex_id", "original", "/inert", null, "full_bypass");
|
||||
expect(t.send.mock.calls[0]?.[1]).toContain("codex -s danger-full-access -a never '-c' 'check_for_update_on_startup=false' resume");
|
||||
expect(t.send.mock.calls[0]?.[1]).toContain("codex -s danger-full-access -a never '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' resume");
|
||||
});
|
||||
it("does not widen unset, floor, named-profile or environment-only behavior", () => {
|
||||
expect(codexPostureArg("", {})).toBe(" -s workspace-write");
|
||||
|
||||
@@ -1028,7 +1028,13 @@ describe("#1196 round 1: launch choices come from the running Codex (LOW-1); ver
|
||||
it("passes the running argv's network override, --no-daemon and add-dirs to the relaunch", async () => {
|
||||
const w = world({ runningArgs: `--no-daemon -m gpt-5.5 -s workspace-write -c sandbox_workspace_write.network_access=true resume --add-dir /shared/state ${TOKEN}` });
|
||||
await w.service.restartInPlace({ seatRef: SEAT, plan: true });
|
||||
expect(w.builds).toEqual([{ running: { networkOverride: true, daemonOptOut: true, addDirs: ["/shared/state"] } }]);
|
||||
expect(w.builds).toEqual([{ running: { networkOverride: true, daemonOptOut: true, nonLoginShell: false, addDirs: ["/shared/state"] } }]);
|
||||
});
|
||||
|
||||
it("reads the running Codex's non-login shell choice, so the relaunch keeps it", async () => {
|
||||
const w = world({ runningArgs: `-s workspace-write -c allow_login_shell=false resume ${TOKEN}` });
|
||||
await w.service.restartInPlace({ seatRef: SEAT, plan: true });
|
||||
expect(w.builds).toEqual([{ running: { networkOverride: false, daemonOptOut: false, nonLoginShell: true, addDirs: [] } }]);
|
||||
});
|
||||
|
||||
it("an --add-dir holding a space survives whole: the relaunch reads the exact argv, not ps's whitespace split", async () => {
|
||||
@@ -1036,7 +1042,7 @@ describe("#1196 round 1: launch choices come from the running Codex (LOW-1); ver
|
||||
const argv = ["/usr/local/bin/codex", "-m", "gpt-5.5", "-s", "workspace-write", "resume", TOKEN, "--add-dir", "/home/u/my repo/.git"];
|
||||
const w = world({ runningArgs: argv.slice(1).join(" "), runningArgv: argv, facts: { 200: facts({ argv }) } });
|
||||
await w.service.restartInPlace({ seatRef: SEAT, plan: true });
|
||||
expect(w.builds).toEqual([{ running: { networkOverride: false, daemonOptOut: false, addDirs: ["/home/u/my repo/.git"] } }]);
|
||||
expect(w.builds).toEqual([{ running: { networkOverride: false, daemonOptOut: false, nonLoginShell: false, addDirs: ["/home/u/my repo/.git"] } }]);
|
||||
});
|
||||
|
||||
it("does not plan a restart when the saved token occurs only inside an argv option value", async () => {
|
||||
@@ -1081,7 +1087,7 @@ describe("#1196 round 1: launch choices come from the running Codex (LOW-1); ver
|
||||
});
|
||||
const binding = { id: "", nodeId: "n", attachmentType: "tmux", tmuxSession: SEAT, tmuxWindow: null, tmuxPane: "%1",
|
||||
cmuxWorkspace: null, cmuxSurface: null, updatedAt: "", cwd: "/work", codexConfigProfile: "prof", teamPermissionDefault: true } as NodeBinding;
|
||||
const built = await adapter.buildResumeCommand(binding, { name: SEAT, resumeToken: TOKEN, running: { networkOverride: false, daemonOptOut: true, addDirs: ["/a b"] } });
|
||||
const built = await adapter.buildResumeCommand(binding, { name: SEAT, resumeToken: TOKEN, running: { networkOverride: false, daemonOptOut: true, nonLoginShell: false, addDirs: ["/a b"] } });
|
||||
expect(probes).toEqual([]);
|
||||
expect(built).toMatchObject({ ok: true, core: expect.stringContaining("--no-daemon") });
|
||||
if (built.ok) {
|
||||
@@ -1120,7 +1126,7 @@ describe("#1196 round 2: the shell branch keeps the running settings (F1)", () =
|
||||
const result = await w.service.restartInPlace({ seatRef: SEAT, reason: "x" });
|
||||
expect(result).toMatchObject({ ok: true, outcome: "restarted_account_unverified", paneShape: "shell_backed" });
|
||||
// Built once, before anything stopped; nothing is prepared again after Codex exits.
|
||||
expect(w.builds).toEqual([{ running: { networkOverride: true, daemonOptOut: true, addDirs: ["/shared/one", "/shared/two"] } }]);
|
||||
expect(w.builds).toEqual([{ running: { networkOverride: true, daemonOptOut: true, nonLoginShell: false, addDirs: ["/shared/one", "/shared/two"] } }]);
|
||||
expect(w.launches).toHaveLength(1);
|
||||
const typed = shellWords(w.launches[0]!.command);
|
||||
expect(typed).toContain("--no-daemon");
|
||||
|
||||
@@ -492,8 +492,8 @@ describe("CodexRuntimeAdapter.launchHarness fork branch", () => {
|
||||
expect(result.ok).toBe(true);
|
||||
const sendText = tmux.sendText as ReturnType<typeof vi.fn>;
|
||||
const sentCmd = sendText.mock.calls[0]?.[1] as string;
|
||||
// The no-profile floor and managed update setting precede the fork subcommand.
|
||||
expect(sentCmd).toMatch(/^codex -s workspace-write '-c' 'check_for_update_on_startup=false' fork/);
|
||||
// The no-profile floor, managed update setting and non-login tool shells precede the fork subcommand.
|
||||
expect(sentCmd).toMatch(/^codex -s workspace-write '-c' 'check_for_update_on_startup=false' -c 'allow_login_shell=false' fork/);
|
||||
expect(sentCmd).toContain("PARENT-THREAD-ABC");
|
||||
if (result.ok) {
|
||||
expect(result.resumeToken).toBe("NEW-CODEX-THREAD-XYZ");
|
||||
|
||||
@@ -694,6 +694,40 @@ describe("SessionTransport", () => {
|
||||
expect(callOrder).toEqual(["cancelPaneMode", "sendText", "sendKeys:Enter"]);
|
||||
});
|
||||
|
||||
it("leaves the mode through the seat's address when a stale binding elsewhere names the same pane", async () => {
|
||||
const { node } = seedCanonicalRig();
|
||||
sessionRegistry.updateBinding(node.id, { tmuxPane: "%27" });
|
||||
// tmux reuses pane ids after a server restart: another rig's binding can still name %27.
|
||||
const studio = rigRepo.createRig("studio-build");
|
||||
const stale = rigRepo.addNode(studio.id, "sdk.impl", { role: "worker", runtime: "claude-code" });
|
||||
sessionRegistry.registerSession(stale.id, "sdk-impl@studio-build");
|
||||
sessionRegistry.updateBinding(stale.id, { tmuxSession: "sdk-impl@studio-build", tmuxPane: "%27" });
|
||||
// The guard can't resolve the raw pane id to one seat, so a write addressed to it is refused before any key.
|
||||
expect(resolveGuardTarget(db, "%27")).toBeNull();
|
||||
expect(resolveGuardTarget(db, "dev-impl@my-rig")).toMatchObject({ nodeId: node.id, pane: "%27" });
|
||||
const callOrder: string[] = [];
|
||||
const tmux = modalPane({ leaves: true, callOrder });
|
||||
const modeReads: string[] = [];
|
||||
let left = false;
|
||||
tmux.isPaneInMode = vi.fn(async (target: string) => { modeReads.push(target); return !left; });
|
||||
const cancelTargets: string[] = [];
|
||||
// As the adapter's guarded write does: an unresolvable target writes nothing, so the pane stays in its mode.
|
||||
tmux.cancelPaneMode = vi.fn(async (target: string) => {
|
||||
cancelTargets.push(target); callOrder.push("cancelPaneMode");
|
||||
if (!resolveGuardTarget(db, target)) return { ok: false as const, code: "guard_target_unknown", message: `Cannot establish managed input target ${target}; no input written.` };
|
||||
left = true;
|
||||
return { ok: true as const };
|
||||
});
|
||||
const transport = createTransport(tmux);
|
||||
|
||||
const result = await transport.send("dev-impl@my-rig", "hello");
|
||||
// The person's harm first: the message is delivered and submitted, not refused as pane_in_mode.
|
||||
expect(result).toMatchObject({ ok: true });
|
||||
expect(callOrder).toEqual(["cancelPaneMode", "sendText", "sendKeys:Enter"]);
|
||||
expect(cancelTargets).toEqual(["dev-impl@my-rig"]);
|
||||
expect(modeReads.every(target => target === "%27")).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves the mode before a submitOnly Enter", async () => {
|
||||
seedCanonicalRig();
|
||||
const callOrder: string[] = [];
|
||||
|
||||
@@ -133,7 +133,9 @@ describe("config routes (User Settings v0)", () => {
|
||||
// + 2 retention.stream_days / stream_max_items (slice 22 outcome 9 stream retention; default 30, 20000).
|
||||
// + 1 specs.builtins_visible (show built-in library entries by default).
|
||||
// + 1 stream.intake (issue #1856 queue intake routing; default queue.worker).
|
||||
expect(Object.keys(body.settings).length).toBe(95);
|
||||
// + 1 launch.codex_login_shell (defer to Codex's own login-shell setting; default off).
|
||||
expect(Object.keys(body.settings).length).toBe(96);
|
||||
expect(body.settings["launch.codex_login_shell"]).toMatchObject({ value: false, source: "default", defaultValue: false });
|
||||
expect(body.settings["specs.builtins_visible"]).toMatchObject({ value: true, source: "default", defaultValue: true });
|
||||
expect(body.settings["stream.intake"]).toMatchObject({ value: "queue.worker", source: "default", defaultValue: "queue.worker" });
|
||||
expect(body.settings["retention.stream_days"]).toMatchObject({ value: 30, source: "default", defaultValue: 30 });
|
||||
|
||||
@@ -123,6 +123,7 @@ describe("SettingsStore (User Settings v0)", () => {
|
||||
"skills.router_line.enabled",
|
||||
"onboarding.default_pack.enabled",
|
||||
"launch.non_interruptive",
|
||||
"launch.codex_login_shell",
|
||||
"health.context_pressure.warning_percent",
|
||||
"health.context_pressure.critical_percent",
|
||||
"files.allowlist", "progress.scan_roots",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.10",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
Reference in new issue
Block a user