Compare commits

...
19 changed files with 687 additions and 210 deletions

No files matched your search

+223
View File
@@ -8,6 +8,229 @@ deprecations, and behavioral changes. Breaking changes are called out explicitly
---
## [0.7.5]
OpenRig 0.7.5 adds `rig terminal open qitem:<id> --follow`: keep one terminal
workspace as a task's current owners join and leave. Joining viewers do not
request focus; departures remove viewers, not the agents or conversations.
Follow mode is scoped to Linux Herdr 0.9.5 / protocol 22. The exercised journey
used shell stand-ins; real-agent follow qualification and active render cost
are not established by that result. Changes since 0.7.4:
### Before you upgrade
- **Restart the daemon after installing** to run the new code and database
update. Migration 126 adds deferred `escalates_to` target addresses to the
edges table, preserving existing edges. Previously inert escalation edges
now affect recovery for seats without a delegation parent.
```bash
npm install -g @openrig/cli@0.7.5
rig daemon stop && rig daemon start
```
- **The bundled openrig-core plugin goes from 0.1.21 to 0.1.22.** Its
`queue-handoff` skill teaches agents to park waits on CI or a long job with
a continuation and wake. As before, managed refresh preserves replaced
files under `$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply any local
edits you still need from that copy. No setting is added or removed.
- **Codex hook refresh has a known limit.** The earlier remedy
`rig seat restart <seat> --keep-conversation --reason "refresh hooks"`
(preview with `--plan`) refuses a Codex seat launched by a 0.6.x
`openrig-tmux-send` script when Codex is a grandchild of the pane's shell.
Nothing is stopped by that refusal. This release does not fix
[#2200](https://github.com/mvschwarz/openrig/issues/2200); do not treat the
refused restart as a completed hook refresh. If Codex asks for hook review,
review the hooks you intend to enable.
### Follow one task in one workspace
- **Follow the current owners of a queue item:**
`rig terminal open qitem:<id> --follow --provider herdr`. The view derives
membership from the task's open rows and explicitly linked successors.
Joining seats get verified viewer tiles in the same workspace. A complete
empty task drains removable tiles and keeps the workspace; after retained
tiles are gone, following stops. Run `--follow` again to resume it.
([#2199](https://github.com/mvschwarz/openrig/pull/2199))
- **Keep control of the view:** `--pin`, `--unpin`, `--suppress`,
`--unsuppress`, `--suppression-ms` and `--max-tiles` adjust presentation.
`--no-follow` leaves the layout in place. The default is eight tiles,
four seconds of departure grace and twelve more seconds for a focused
leaver. Pins can exceed the cap; input-retained leavers do not consume a
joining seat's slot. See [terminal sessions](docs/reference/terminal-sessions.md).
([#2199](https://github.com/mvschwarz/openrig/pull/2199))
- **Uncertain input keeps its tile.** Typed drafts and unreadable composers
retain departed viewers, including Pi, terminal and community harness
seats whose input cannot be confirmed. Unpinning does not override that.
There is one delayed input recheck, then queue/session events or an explicit
follow command can reconsider it. Failed or incomplete reads retain the
previous view with an explanation. Activity hooks do not trigger renders.
([#2199](https://github.com/mvschwarz/openrig/pull/2199))
- **Protected Codex delivery no longer strands a message it can't submit.** On a
Codex seat whose typing guard is draft-aware or openrig-urgent, a message
whose complete text can't be confirmed visible before paste is retained, not
sent, instead of being pasted where it can't be submitted. The default (guard
off) is unchanged. Use a larger pane or a shorter message; turning the typing
guard off turns draft protection off and is not a recovery. Pastes over 1,000
code points are retained. Wrapped lines use a conservative bound, so most
wrapped wakes in common four-tile views can be retained even when they fit
natively; approximate standard-envelope examples are one-paragraph bodies over
about 115 characters at 80×22, 192 at 80×24, or 526 at 120×30 (examples, not
limits). A later resize is not atomic, and layouts with Codex's image-enabled
ambient pet are unqualified.
([#2242](https://github.com/mvschwarz/openrig/pull/2242))
- **Codex 0.164's two-row composer footer is recognized.** A departed Codex
agent's empty composer no longer reads as unconfirmed input that keeps its
tile. Delivery's draft-aware and openrig-urgent typing guards use the same
reader, so an empty Codex 0.164 composer there can now receive typed
delivery. ([#2206](https://github.com/mvschwarz/openrig/pull/2206))
- **Clients can ensure a missing viewer without relaying out existing panes**
through `POST /api/terminal/viewer/ensure`. It rechecks task membership and
existing viewer identity before creating or reusing a tile. Preview names
`not-provisioned`, `verified` or `unconfirmed`; membership gives no input
authority. This path has the same Linux Herdr 0.9.5 / protocol 22 scope.
([#2133](https://github.com/mvschwarz/openrig/pull/2133))
- **An unconfirmed pane no longer counts as opened.** CLI and TUI output name
it as degraded with `pane_unconfirmed`, distinguish it from a skipped seat,
and omit its restart-command report. An all-unconfirmed open exits nonzero.
Inspect before retrying; this is not proof that nothing opened.
([#2173](https://github.com/mvschwarz/openrig/pull/2173))
### Fixes by what you see
- **Slow successful sends have time to answer.** Local and HTTP `rig send`
wait 60 seconds, plus an explicit idle wait, instead of five seconds locally
or thirty over HTTP. A timeout still means unconfirmed. The advice now says
that a busy recipient may hold input off-screen: missing pane text does not
prove non-delivery, so reconcile before another send or duplicate queue item.
No automatic resend is added. This does not diagnose the daemon's latency.
([#2198](https://github.com/mvschwarz/openrig/pull/2198))
- **A wake that was unroutable before its seat had a pane is retried once the
binding exists,** through the row's existing wake intent. The outbox does
not send that same intent again after it is settled as delivered.
([#2170](https://github.com/mvschwarz/openrig/pull/2170))
- **A parentless seat's recovery can follow its declared `escalates_to`
address,** including another rig on the same daemon. A delegation parent
still takes precedence. Unavailable or ambiguous targets use the explained
fallback; an already-open recovery keeps its recorded owner. This is one
escalation hop, not cross-host routing.
([#1772](https://github.com/mvschwarz/openrig/pull/1772))
- **A stuck finding needs evidence to close.** Delivery, pickup or explicit
custody can resolve it; a changed reporting threshold, an unreadable
registry or a failed wake becoming unconfirmed cannot silently clear it.
([#2153](https://github.com/mvschwarz/openrig/pull/2153))
- **Fresh Claude handovers wait for the current launch.** Retained scrollback
or an echoed shell command cannot make a new process look ready. The context
packet waits for the matching process and Claude's complete drawn input frame.
([#2159](https://github.com/mvschwarz/openrig/pull/2159))
- **Runtime send refusals return HTTP 409 rather than 500.** A stale recorded
Claude pane names `rig reconcile-session <seat>` as the repair; nothing is
typed on that refusal. ([#2157](https://github.com/mvschwarz/openrig/pull/2157))
- **`rig down` keeps a rig down after a rolled-back start.** It records the
explicit stop without rewriting the superseded session's history. The
`rig start --last` help now explains which rigs return and how an explicit
stop keeps them out; the kernel is not selected by `--last`.
([#2166](https://github.com/mvschwarz/openrig/pull/2166))
- **Status and configuration describe the selected rig.** `rig ps` reports
`periodicSnapshotActive` only for rigs the scheduler actually snapshots
([#2168](https://github.com/mvschwarz/openrig/pull/2168)); the web Overview
reads the rig's current exported configuration instead of a same-named
library template ([#2188](https://github.com/mvschwarz/openrig/pull/2188)).
- **Host labels and next steps are clearer.** The TUI shows the configured
local host name while retaining its stable identity
([#2162](https://github.com/mvschwarz/openrig/pull/2162)). `rig host add` and
`pair` explain how to opt an HTTP host into remote Feed attention; registration
leaves that setting unchanged ([#2179](https://github.com/mvschwarz/openrig/pull/2179)).
- **Small CLI corrections:** `attach --self` reads tmux's rewritten separators
([#2178](https://github.com/mvschwarz/openrig/pull/2178)); transcript and
capture line counts use their normalized values on local and remote paths
([#2164](https://github.com/mvschwarz/openrig/pull/2164)); stdio MCP warns on
out-of-range daemon ports while continuing to serve, without substituting a
port ([#2160](https://github.com/mvschwarz/openrig/pull/2160)).
- **Notification adapters release unread HTTP response bodies,** without
changing their send result or waiting for cleanup. No performance improvement
is claimed here. ([#2172](https://github.com/mvschwarz/openrig/pull/2172))
- **Scope audits agree on slice folders numbered 100 and above.** The guide
also explains that moving a slice reopens its proof judgments; a clean
structural audit is not a proof judgment.
([#2194](https://github.com/mvschwarz/openrig/pull/2194))
### Presets, hosted members and guides
- **The `pi-tui` example runs Pi's own terminal UI as a community harness.**
It gives each seat its own Pi agent folder and passes startup input to Pi.
One Pi 1.4.0 / OpenRig 0.7.4 run showed a parked wake returning to the same
session and an unattended handoff to a second seat. It did not prove Pi
chooses that coordination by itself. Resume is not declared. Use it
unattended: a wake can submit a person's half-typed draft, and unrecognized
dialogs can receive the send. Busy-turn wakes and other Pi versions remain
unqualified. ([#2182](https://github.com/mvschwarz/openrig/pull/2182),
[#2184](https://github.com/mvschwarz/openrig/pull/2184))
- **Interactive Pi setup is explicit:** copy the coordination skills and use
`delivery_hint: send_text` for a startup file that must reach a preset's
first input. A plain terminal seat does not receive queue wakes. Setting its
typing guard to `off` can type notices into a shell; keep the default for a
seat that can return to one. These are documented limits, not new guards.
([#2174](https://github.com/mvschwarz/openrig/pull/2174),
[#2180](https://github.com/mvschwarz/openrig/pull/2180),
[#2181](https://github.com/mvschwarz/openrig/pull/2181),
[#2185](https://github.com/mvschwarz/openrig/pull/2185))
- **Hosted-member guidance names its real reach.** A member without a wake
trigger checks work when its owner next starts a session; a listed tool is
not a grant, and completed work closes with `no-follow-on`. Hosted members
exposed over the internet remain **experimental, unsupported, off by default
and at your own risk**. The guide explains narrow grants and token revocation;
this terminal-follow release requires no internet exposure.
([#2187](https://github.com/mvschwarz/openrig/pull/2187),
[#2189](https://github.com/mvschwarz/openrig/pull/2189))
- **Publishing a team bundle:** add and declare a README after `rig export`,
then run `rig bundle check` at the revision being submitted
([#2176](https://github.com/mvschwarz/openrig/pull/2176)). Queue guidance
teaches parking long waits with a continuation and wake
([#2167](https://github.com/mvschwarz/openrig/pull/2167)).
- **Contributor maintenance:** the test guide corrects installed-scenario
counts and cases ([#2165](https://github.com/mvschwarz/openrig/pull/2165));
the compaction receipt fixture shares one clock, with no product change
([#2169](https://github.com/mvschwarz/openrig/pull/2169)). Published release
notes, the capability map and roadmap records were carried forward
([#2161](https://github.com/mvschwarz/openrig/pull/2161),
[#2175](https://github.com/mvschwarz/openrig/pull/2175),
[#2197](https://github.com/mvschwarz/openrig/pull/2197)).
### Limits carried forward
- **Follow mode is Linux Herdr 0.9.5 / protocol 22 only.** Other platforms,
providers and selector kinds retain one-shot open. Its exercised journey
used shell stand-ins, not native agents. Active render cost is unmeasured.
The first workspace on an empty provider may gain focus, and a viewer can
resize the agent's terminal; `ignore-size` is not resize isolation.
- **Unreadable composers retain departed tiles.** This includes input the
inspector cannot read on Pi, terminal or community harness seats; unpinning
alone does not establish that closing is safe.
- **Escalation across rigs is same-daemon only.** Upgrade followed by snapshot
restore of deferred escalation targets has not been qualified.
- **A webhook 2xx is acceptance of the ring, not pickup.** A hosted member's
activity remains unknown between calls. Principals record attribution, not
authentication. See [hosted members](docs/reference/hosted-members.md).
- **Reviewed-history resume remains a client operation through startup review,**
not a CLI command. Kernel restore evidence lasts one daemon process.
- **OpenClaw interrupted local turns are not recovered; Hermes has no declared
resume or measured idle gateway exit.** `rig down` stops a community harness's pane; survival of independently spawned
children remains untested. Inspect actual processes before relying on their exit.
- **The Codex restart limitation in #2200 remains,** as do the other 0.7.4
limits unless a change above names them fixed: Codex permission qualification
only on 0.164.0, earlier removed hook approvals, SSH tiles after a Herdr
restart, unqualified native cmux, Slack-only shared channels and the existing
browser-sizing and old Slack-thread limits. Isolated account-home switching
is not included.
### Credits
Thanks to @icey-zzz (#1772), @Aschenbath (#2153, #2159, #2162, #2169, #2179),
@DeryFerd (#2164, #2165), @Nikhi00718 (#2172) and @dajiaohuang (#2188).
Thanks also to @rudycelekli for the contribution from #1416 completed in #2160;
to @AdamWhitehurst (#1544, #2049), @kevinhsykes (#2151), @aanestesia (#605),
@todie (#1419) and @tgrundtvig (#21), whose reports led to fixes here; and to
the other people whose reports and reviews led to these fixes.
## [0.7.4]
OpenRig 0.7.4 lets an assistant you already use work as a member of your team.
+5 -5
View File
@@ -26,9 +26,9 @@ use:
```sh
# Claude Code
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.3/skills/rigs --skill rigs --agent claude-code
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent claude-code
# Codex
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.3/skills/rigs --skill rigs --agent codex
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/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.3` 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.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:
```bash
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.3/scripts/install.sh | sh -s -- --dry-run
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.3/scripts/install.sh | 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.5/scripts/install.sh | sh
```
If a provider isn't signed in yet, the last step reports `FAILED [4/4]`, and `rig setup` lists the sign-in under "Some steps need attention". If the only remaining failures are provider sign-ins, the install steps finished; sign in to each selected provider as below and continue.
+2 -2
View File
@@ -191,11 +191,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.3/scripts/install.sh | sh -s -- --dry-run
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh -s -- --dry-run
```
The same command without `--dry-run`,
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.3/scripts/install.sh | sh`, installs the
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/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
+1 -1
View File
@@ -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.3`. If OpenRig is already installed, use the tag
For a new install, use the latest release's tag, for example `v0.7.5`. 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.
+153 -177
View File
@@ -1,192 +1,168 @@
# What changed in OpenRig 0.7.1
# What changed in OpenRig 0.7.5
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.5.
It lists changes since 0.7.4. For the whole capability map, read
`rig context get onboarding-width/public-what-you-can-do.md`.
Each release replaces this note: `rig context get reference/whats-new.md`
describes the installed version once the upgraded daemon is running. Without
a daemon, read `$OPENRIG_HOME/reference/whats-new.md`; that copy refreshes at
daemon start and can still describe the previous version just after an upgrade.
## 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** (`rig daemon stop`, then `rig daemon start`). The new
migration 126 stores deferred `escalates_to` addresses alongside node-bound
edges. Existing escalation edges that were inert now route recovery for
seats without a delegation parent, including to another rig on this daemon.
- **openrig-core goes from 0.1.21 to 0.1.22.** Its queue-handoff skill teaches
parking waits on CI or long jobs with a continuation and wake. Managed
refresh still preserves replaced files under
`$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you need.
No setting is added or removed.
- **Keep the Codex hook-refresh limit in mind.** The earlier remedy
`rig seat restart <seat> --keep-conversation --reason "refresh hooks"`
(preview with `--plan`) refuses a Codex seat launched by a 0.6.x
`openrig-tmux-send` script when Codex is a grandchild of the pane's shell.
Nothing is stopped, and the hook refresh has not happened. Issue
[#2200](https://github.com/mvschwarz/openrig/issues/2200) is not fixed in
0.7.5. If Codex asks for hook review, review the hooks you intend to enable;
upgrading does not restore approvals an earlier OpenRig removed.
## New things you can do
- **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.
- **Follow a task in one terminal workspace:**
`rig terminal open qitem:<id> --follow --provider herdr`. Current obligation
owners join as verified viewer tiles without requesting focus. Departures
close viewers after a grace period, not agents or conversations. This is
scoped to **Linux Herdr 0.9.5 / protocol 22**.
- **Adjust the view:** `--pin` / `--unpin`, `--suppress` / `--unsuppress`,
`--suppression-ms`, `--max-tiles`, `--grace-ms` and `--focus-retention-ms`.
The defaults are eight tiles, four seconds of departure grace and twelve
more seconds for a focused leaver. `--no-follow` keeps the fixed layout.
A completely empty task drains removable tiles; when none remain, following
stops. Run `--follow` again to resume the same workspace.
- **Inspect following:** `rig terminal views --json` and
`rig terminal status --json` include saved active selectors and their latest
status. Ordinary `rig terminal open qitem:<id>` remains a one-shot open.
A client can use `POST /api/terminal/viewer/ensure` to add or reuse a verified
missing viewer without relaying out existing panes. Membership grants no input
authority. See `rig context get reference/terminal-sessions.md`.
- **Use the `pi-tui` example for Pi's own terminal UI as a community harness.**
Copy its preset and launcher from `docs/reference/community-harness-examples/pi-tui/`
and maintain them as your own. It supplies a per-seat Pi agent folder and
startup input. One Pi 1.4.0 / OpenRig 0.7.4 run exercised a parked wake to
the same session and an unattended handoff to a second seat. Resume is not
declared; the limitations below matter.
## Behaviour that changed
## Behaviour and output that changed
- **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.
- **`rig send` waits longer for its result:** 60 seconds locally and over HTTP,
plus any requested `--wait-for-idle` duration. A longer send or lost response
is still unconfirmed, with no automatic resend. A busy agent can hold delivered
input off-screen: missing pane text does not prove non-delivery. Reconcile
before another send or filing the same work as a queue item.
- **Protected Codex delivery no longer strands a message it can't submit.** On a
Codex seat whose typing guard is draft-aware or openrig-urgent, a message
whose complete text can't be confirmed visible before paste is retained, not
sent, instead of being pasted where it can't be submitted. The default (guard
off) is unchanged. Use a larger pane or a shorter message; turning the typing
guard off turns draft protection off and is not a recovery. Pastes over 1,000
code points are retained. Wrapped lines use a conservative bound, so most
wrapped wakes in common four-tile views can be retained even when they fit
natively; approximate standard-envelope examples are one-paragraph bodies over
about 115 characters at 80×22, 192 at 80×24, or 526 at 120×30 (examples, not
limits). A later resize is not atomic, and layouts with Codex's image-enabled
ambient pet are unqualified.
([#2242](https://github.com/mvschwarz/openrig/pull/2242))
- **Codex 0.164's two-row composer footer is recognized.** A departed Codex
agent's empty composer no longer reads as unconfirmed input that keeps its
tile. Delivery's draft-aware and openrig-urgent typing guards use the same
reader, so an empty Codex 0.164 composer there can now receive typed delivery.
([#2206](https://github.com/mvschwarz/openrig/pull/2206))
- **Unconfirmed panes are not counted as opened.** Terminal CLI and TUI results
name `pane_unconfirmed` separately from skipped seats; an all-unconfirmed open
exits nonzero. Inspect before retrying rather than assuming nothing opened.
- **A wake recorded as unroutable can resume once its seat is pane-bound.** The
sweep uses the row's existing wake intent so the outbox does not send the same
settled intent again. Existing quiet hours and retry rules still apply.
- **A parentless seat's `escalates_to` destination is used for recovery,**
including another rig on this daemon. Delegation parents still take
precedence; an open recovery keeps its recorded owner. A stuck finding closes
on source evidence, not because a reporting threshold or unreadable registry
made it disappear from a report.
- **Fresh Claude handovers wait for the new process and current drawn input
frame.** Retained scrollback and an echoed launch command are not readiness.
- **Runtime send refusals return 409 instead of 500.** A stale Claude pane
names `rig reconcile-session <seat>` as the repair and types nothing.
- **`rig down` records an explicit stop after a rolled-back launch.**
`rig start --last` then keeps it down. `rig ps` marks periodic snapshots active
only for rigs the scheduler snapshots.
- **The web Overview shows the selected rig's current configuration,** not a
same-named library template. The TUI shows the configured local host name;
host registration explains the optional HTTP Feed subscription.
- **`attach --self` handles tmux's rewritten separators;** capture and transcript
requests use normalized line counts; stdio MCP warns on out-of-range daemon
ports while continuing to serve without replacing the supplied port.
- **The scope audit route accepts slice folders numbered 100 and above.**
Moving a slice still reopens its proof judgments: structural validity does
not mean an earlier judgment applies at the new path.
## Output that changed shape
## Keep using the 0.7.4 capabilities
- **`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.
- **Hosted assistants can remain team members over MCP**, with roster entries,
per-seat grants and optional webhook rings. Their reach comes from the grant,
not the tool list. Without a wake trigger they check work when their owner
next starts a session; finished work closes with `no-follow-on`.
Internet-facing hosted members remain **experimental, unsupported, off by
default and at your own risk**. This release's terminal view needs no internet
exposure. See `rig context get reference/hosted-members.md` for narrow grants
and token revocation.
- **External grant checks, OAuth discovery and MCP protocol 2026-07-28** remain
available beside the older MCP handshake. A recorded principal is attribution,
not authentication. A webhook 2xx accepts the ring, not the task; a queue claim
is pickup, and activity between calls remains unknown.
- **`rig seat history --rig <id> --node-id <id>`** still lists eligible recorded
conversations. Resuming a person's reviewed history remains a client operation
through startup review, not a CLI command.
- **Daemon start restores a kernel only when this start newly detects it went
down.** A kernel already recorded down stays down; use `rig up kernel --existing` when you want it back.
That restore evidence still lasts only one daemon process.
## What to stop doing
## Limits to carry into your work
- **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.
- **Follow mode and ensure-viewer are limited to Linux Herdr 0.9.5 / protocol 22.**
Other platforms, providers and selectors retain one-shot open. The exercised
follow journey used shell stand-ins, not native agents; real-agent follow
qualification is not claimed. Active render cost is unmeasured.
- **Unreadable composers keep departed tiles.** Pi, terminal and community
harness input may be unconfirmed. Unpinning does not make it safe to close;
typed drafts are retained too. Each input-retained departure gets one delayed
recheck, then an event or explicit follow command can reconsider it. Failed
provider or membership reads retain the old view with an explanation.
- **A viewer can resize its agent's terminal.** `ignore-size` is not isolation.
The first workspace on an empty Herdr server may gain focus. An SSH tile still
needs reopening after a Herdr restart, and native cmux remains unqualified.
- **Use `pi-tui` unattended, not alongside a person's typing.** A wake can submit
a half-typed draft, and an unrecognized dialog can receive a send. Draft-aware
mode is not a known fix. Busy-turn wakes, other Pi versions, native resume and
Pi choosing the park/handoff sequence for itself are not established by the
example's one run. Plain terminal seats do not receive queue wakes; setting
their typing guard to `off` can put notices into a shell. A preset's first
startup file must use `delivery_hint: send_text` to reach its first input.
- **Cross-rig escalation is same-daemon only;** upgrade followed by snapshot
restore of deferred targets has not been qualified.
- **The Codex restart refusal in #2200 remains.** Codex permission settings were
qualified on native 0.164.0 in 0.7.3, not every version; isolated account-home
switching is not part of this release.
- **OpenClaw interrupted local turns remain unrecovered;** inspect the pane and
use its Gateway preset for unattended work. Hermes has no declared resume or
measured idle gateway exit. `rig down` stops a community harness's pane;
survival of independently spawned children remains untested.
- **Shared channels remain Slack only.** TUI primary-user authority is attribution,
not authentication. Earlier Slack-thread, browser-sizing and other published
0.7.4 limits remain unless a change above explicitly names them fixed.
## Known gaps in 0.7.1
## Contributors
- **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`.
Thanks to @icey-zzz, @Aschenbath, @DeryFerd, @Nikhi00718 and @dajiaohuang for
merged contributions, and @rudycelekli for the work completed in #2160.
+5 -5
View File
@@ -1,12 +1,12 @@
{
"name": "openrig",
"version": "0.7.1",
"version": "0.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openrig",
"version": "0.7.1",
"version": "0.7.5",
"license": "Apache-2.0",
"workspaces": [
"packages/daemon",
@@ -7346,7 +7346,7 @@
},
"packages/cli": {
"name": "@openrig/cli",
"version": "0.7.1",
"version": "0.7.5",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -7384,7 +7384,7 @@
},
"packages/daemon": {
"name": "@openrig/daemon",
"version": "0.7.1",
"version": "0.7.5",
"license": "Apache-2.0",
"dependencies": {
"@hono/node-server": "^1.13.0",
@@ -7426,7 +7426,7 @@
},
"packages/ui": {
"name": "@openrig/ui",
"version": "0.7.1",
"version": "0.7.5",
"license": "Apache-2.0",
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.2.8",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openrig",
"version": "0.7.1",
"version": "0.7.5",
"private": true,
"license": "Apache-2.0",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@openrig/cli",
"version": "0.7.1",
"version": "0.7.5",
"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": [
@@ -1,6 +1,6 @@
{
"name": "openrig-core",
"version": "0.1.21",
"version": "0.1.22",
"description": "OpenRig canonical skills and hooks for cross-runtime agent topology coordination, continuity, and operating discipline.",
"author": {
"name": "Mike Schwarz",
@@ -1,6 +1,6 @@
{
"name": "openrig-core",
"version": "0.1.21",
"version": "0.1.22",
"description": "OpenRig canonical skills and hooks for cross-runtime agent topology coordination, continuity, and operating discipline.",
"author": {
"name": "Mike Schwarz",
@@ -11,7 +11,7 @@ not know exists.** So the point of what follows is not skill. It is recognition:
lands, something rhymes, and you go check instead of building.
Ninety top-level verbs ship (capability canon refreshed through
`capability-delta-v0.7.4`). This marker describes the pack's teaching, not publication or live
`capability-delta-v0.7.5`). 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`.
@@ -211,14 +211,24 @@ unseen — which makes messages the one delivery channel that never gets skipped
type. Only if the window cannot open, `rig tui --shared` is the dashboard-only fallback; explain
the failure. In the window's `--json` output, `reusedWorkspace` with no new tiles means an
existing view was reused, which is success. `--provider herdr` or `--provider cmux` opens the
view in a provider that is already running, without opening a window; each such open adds a new
workspace, so to come back to a view, focus its open space.
view in a provider that is already running, without opening a window. Ordinary opens add a new
workspace, so to come back to a fixed view, focus its open space; task follow mode reuses one.
**`qitem:<id>` as the view** previews or opens the current local owners of that task: its
pending, in-progress and blocked rows through explicitly linked successors, without closed
predecessors or past senders. It's a read-only membership view: stopped, missing, external and
unresolved owners are named, and incomplete lineage is reported rather than cut short. It doesn't
follow the task as it moves, and an explicit open still has the provider's usual workspace
effects. Preview and open responses carry `viewDescriptor` (`kind`, `name`).
unresolved owners are named, and incomplete lineage is reported rather than cut short. Ordinary
open is one-shot. Add **`--follow --provider herdr`** to keep one workspace as the task's owners
join and leave (Linux Herdr 0.9.5 / protocol 22 only). Joining viewers do not request focus;
departures close viewers, not agents or conversations. Typed drafts and unreadable composers
retain departed tiles, including unconfirmed Pi, terminal and community-harness input.
`--pin`, `--suppress` and `--max-tiles` adjust the view; `--no-follow` keeps the fixed layout.
A complete empty task drains removable tiles and stops following once none remain; an explicit
`--follow` resumes that workspace. `rig terminal views --json` shows saved active selectors.
The exercised journey used shell stand-ins, not native agents, and active render cost remains
unmeasured. See `reference/terminal-sessions.md` for controls and limits. Preview and open
responses carry `viewDescriptor` (`kind`, `name`); membership grants no input authority.
An unconfirmed pane is reported as `pane_unconfirmed`, not counted as opened. Inspect before
retrying: an unconfirmed result does not prove that nothing opened.
Add `--session <name>` to select a Herdr session for that request; `--session default`
explicitly selects its default session. `rig terminal status --session <name>` inspects
the same endpoint. Omitting the flag keeps the daemon's configured endpoint. Herdr tiles come
@@ -1319,6 +1329,10 @@ your circumstances is configuration, and the ones that are not, another agent ca
with secret names, never values): OpenRig then POSTs a pointer to the row when work lands, and a
2xx records only that the ring was accepted, not that anything woke. When work waits for a
member nothing can wake, or a rung member doesn't claim it in time, its person is told.
**Internet-facing hosted members are experimental, unsupported, off by default and at your
own risk.** Local assistants can connect locally. Internet access needs your own TLS tunnel or
proxy and a narrowly scoped, revocable token; see `reference/hosted-members.md`. Task terminal
following does not require internet exposure.
- **`rig skill loadout --runtime <claude-code|codex>`** — inspect the exact catalog revision,
selectors, target, and current/missing/shadowed/conflicting state for one working directory.
`--apply` writes only the managed ownership set, is idempotent, and refuses local edits or
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@openrig/daemon",
"version": "0.7.1",
"version": "0.7.5",
"private": true,
"license": "Apache-2.0",
"type": "module",
+1 -1
View File
@@ -899,7 +899,7 @@ export class TmuxAdapter {
const identity = this.freshManaged.get(target)?.nodeId ?? target;
const checkDraft = guard?.needsInputCheck(identity);
const policy = checkDraft || options?.beforePaste ? {
beforePaste: async () => { if (checkDraft) await guard!.beforeInput(identity); await options?.beforePaste?.(); },
beforePaste: async () => { if (checkDraft) await guard!.beforeInput(identity, false, text); await options?.beforePaste?.(); },
afterPaste: async () => { if (checkDraft) await guard!.notePasted(identity, text); },
} : undefined;
return this.sendTextUnchecked(pane, text, () => { beforeWrite(); beforeInput?.(); }, options?.bracketed, policy);
@@ -137,6 +137,8 @@ export interface ComposerInput {
state: "empty" | "text" | "unknown";
rows: string[];
columns: number;
/** Conservative visible capacity of the recognized Codex fullscreen composer. */
pasteViewport?: { columns: number; rows: number };
cursor?: ComposerSnapshot["cursor"];
unknownReason?: string;
suggestionRows?: string[];
@@ -177,6 +179,34 @@ export function composerContainsOwnedText(input: ComposerInput, expected: string
return false;
}
/** Conservative uncertainty before a protected Codex paste, never Enter authority.
* Codex 0.164 collapses >1000 code points to [Pasted Content N chars], which
* the current ownership reader cannot own. Only a strictly shorter logical
* line has an exact one-row charge. Wrapped lines use a conservative pair bound
* over all native semantic breaks, plus an insertion row; this can retain prose
* that fits natively, without recreating UAX#14 or weakening Enter ownership.
*/
export function composerPasteUncertainty(input: ComposerInput, text: string): "controls" | "long" | "viewport" | null {
const viewport = input.pasteViewport;
if (!viewport) return null;
const normalized = text.replace(/\r\n/g, "\n");
const lines = normalized.split("\n");
if (lines.some(line => /[\x00-\x1f\x7f-\x9f]/.test(line))) return "controls";
// rust-v0.164.0 chat_composer/paste_input.rs:184-192 (Unicode scalar count).
if ([...normalized].length > 1000) return "long";
if (viewport.columns < 1 || viewport.rows < 1) return "viewport";
let rows = 0;
for (const line of lines) {
const cells = [...line].reduce((sum, ch) => sum + (ch >= " " && ch <= "~" ? 1 : 2), 0);
// A native semantic fragment may move whole, even across an apparent space.
// Bound the entire line rather than assuming character or ASCII-space breaks.
// Equality is not the one-row fast path: a full line needs an insertion row.
rows += cells < viewport.columns ? 1 : Math.ceil(2 * cells / viewport.columns) + 1;
if (rows > viewport.rows) return "viewport";
}
return null;
}
/** Recognize this lease's paste immediately after pasting or before its Enter
* in the original composer. A later lease cannot acquire ownership from a label.
*/
@@ -2,7 +2,7 @@ import { hasOtherSeatHistory, isStoppedSeatPredecessor, resolveCurrentSessionAdd
import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import type Database from "better-sqlite3";
import { composerContainsOwnedText, ownCollapsedPaste, type ComposerInput, type ComposerOwnership } from "./composer-prompts.js";
import { composerPasteUncertainty, composerContainsOwnedText, ownCollapsedPaste, type ComposerInput, type ComposerOwnership } from "./composer-prompts.js";
/** A binding is captured before waiting. Never rebind an old operation to a new occupant. */
export interface GuardTarget {
@@ -414,7 +414,7 @@ export class SeatDeliveryGuard {
}
/** Called at the paste/key boundary after file and buffer preparation. */
async beforeInput(name: string, submit = false): Promise<void> {
async beforeInput(name: string, submit = false, pasteText?: string): Promise<void> {
if (!this.needsInputCheck(name)) return;
const target = this.target(name);
const lease = this.scope.getStore()!.get(target.nodeId)!;
@@ -433,6 +433,15 @@ export class SeatDeliveryGuard {
} else if (input.state !== "empty") {
throw new DeliveryGuardError("draft_input_busy", "The seat has an unsubmitted input. Automatic delivery is held.");
} else {
const uncertainty = pasteText === undefined ? null : composerPasteUncertainty(input, pasteText);
if (uncertainty) {
const message = uncertainty === "controls"
? "OpenRig couldn't confirm the layout of tabs or control characters. Send the message without them; nothing was pasted."
: uncertainty === "long"
? "OpenRig couldn't confirm ownership of Codex's collapsed long paste. Send a shorter message; nothing was pasted. Turning this seat's typing guard off explicitly disables draft protection."
: "OpenRig couldn't confirm the complete message would stay visible in this Codex viewport. Open a larger terminal or shorten the message, then retry this request; nothing was pasted.";
throw new DeliveryGuardError("draft_input_unknown", message);
}
lease.inputFrame = input.frame;
}
}
@@ -457,7 +457,8 @@ export function inspectComposerInput(snapshot: ComposerSnapshot | null, runtime:
// Claude replaces its mode hint after our paste; that noninteractive hint is not a new composer.
const stableFooter = prompt.glyph === "❯" && (CLAUDE_MODE_FOOTER.test(footer) || isClaudePasteHint(footer)) ? "claude-composer" : footer;
const frame = JSON.stringify([prompt.glyph, prefix, width, stableFooter]);
const input: ComposerInput = { state: empty ? "empty" : "text", cursor: { ...snapshot.cursor }, suggestionRows, rows: visible, columns: width - prefix, cursorAtEnd, collapsedPaste: label, frame,
const nativeCodex = prompt.glyph !== "❯" && codexFooterSeparator(lines) === end;
const input: ComposerInput = { state: empty ? "empty" : "text", cursor: { ...snapshot.cursor }, suggestionRows, rows: visible, columns: width - prefix - (nativeCodex ? 1 : 0), cursorAtEnd, collapsedPaste: label, frame,
...(ghost ? { ghost } : {}) };
// A numbered body is allowed only as this operation's exact text in its
// previously empty composer. A newly observed menu gains no such authority.
@@ -467,6 +468,13 @@ export function inspectComposerInput(snapshot: ComposerSnapshot | null, runtime:
input.collapsedPaste = visible.join("\n");
input.pasteMatchesExpected = true;
}
if (nativeCodex) {
// Codex 0.164 fullscreen: composer <= 2/3 of the pane (at least eight
// rows, capped by the pane), less padding/status/hint rows. Leave the
// one-column right inset out of the native printable width.
input.pasteViewport = { columns: input.columns,
rows: Math.min(height, Math.max(8, Math.floor(height * 2 / 3))) - 4 };
}
candidates.push(input);
}
return candidates.length === 1 ? candidates[0]! : unknown(candidates.length ? "ambiguous_composer" : "unrecognized_composer", snapshot.cursor);
@@ -6,6 +6,7 @@ import { TmuxAdapter } from "../src/adapters/tmux.js";
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
import { migrate } from "../src/db/migrate.js";
import nativePaste from "./fixtures/claude-paste-footer.json";
import { wrapPaneEnvelope } from "../src/lib/pane-envelope.js";
import type { ComposerSnapshot } from "../src/domain/composer-prompts.js";
import { EventBus } from "../src/domain/event-bus.js";
import { OutboxHandler } from "../src/domain/outbox-handler.js";
@@ -102,6 +103,222 @@ function fixture(saved?: Buffer, opts: { runtime?: string; idleHook?: boolean }
}
// Retained D1 geometry: empty Codex 0.164 at 65x9; after paste the editor
// shows only the envelope's reply footer and an upward scroll indicator.
function codexViewport(body: string, width: number, height: number, renderedRows?: string[]): ComposerSnapshot {
// rust-v0.164.0 uses this character-count label, not Claude's line-count label.
if ([...body].length > 1000) return codexViewport(`[Pasted Content ${[...body].length} chars]`, width, height);
if (!body) return { screen: ["• DONE", "", " Worked for 4s • 20:24", "", "",
"› \x1b[2mAsk Codex to do anything\x1b[0m", "", " GPT-6.1-Sol low · ~/project · Recover OpenRig identity context", " ? for shortcuts", ""].join("\n"),
cursor: { x: 2, y: 5, width, height }, inMode: false };
if (height === 9 && body.includes("From:")) return {
screen: ["", "", "› --- ↑",
" ↩ Reply if needed: rig queue create --destination host-",
' fixture-driver@external --summary "<one line>" --body "..."',
" --verify", "", " GPT-6.1-Sol low · ~/project · Recover OpenRig identity context", ""].join("\n"),
cursor: { x: 10, y: 5, width, height }, inMode: false };
// The fullscreen composer scrolls input once padding/status/hint consume its cap.
const capacity = Math.min(height, Math.max(8, Math.floor(height * 2 / 3))) - 4;
const rows = (renderedRows ?? body.split("\n")).slice(-capacity);
return { screen: ["Ready", "", `› ${rows[0]}`, ...rows.slice(1).map(row => ` ${row}`), "",
" GPT-6.1-Sol low · ~/project", " ? for shortcuts", ""].join("\n"),
cursor: { x: 2 + stringWidth(rows.at(-1)!), y: 1 + rows.length, width, height }, inMode: false };
}
const d1Envelope = wrapPaneEnvelope("host-fixture-driver@external", "worker@test",
"Write exactly draft-aware to /home/t1s075/project/delivery-draft-aware.txt with one shell tool; reply DONE briefly and wait.");
async function codexDelivery(mode = "draft-aware", size = { width: 65, height: 9 }) {
const f = fixture(undefined, { runtime: "codex" });
await f.guard.set("a", { mode }, "human@test", "D1 protected delivery");
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () => codexViewport(f.bodies.a ?? "", size.width, size.height));
return { ...f, size, options: { verify: false, ...(mode === "openrig-urgent" ? { urgent: { by: "human@test" } } : {}) } };
}
describe("Codex pre-paste observability through the real delivery path", () => {
it.each(["draft-aware", "openrig-urgent"])("retains the full envelope before writing into the native 65x9 frame (%s)", async mode => {
const f = await codexDelivery(mode);
expect(await f.send("D1", d1Envelope, f.options)).toMatchObject({ ok: true, outcome: "retained", sent: false,
inputPhase: "not_reached", reason: "draft_input_unknown" });
expect(f.writes).toEqual([]);
expect(f.submissions).toEqual([]);
expect(f.bodies.a).toBe("");
});
it.each(["draft-aware", "openrig-urgent"])("submits a fitting full envelope once (%s)", async mode => {
const f = await codexDelivery(mode, { width: 160, height: 45 });
expect(await f.send("fits", d1Envelope, f.options)).toMatchObject({ ok: true, sent: true });
expect(f.submissions).toEqual([mode === "openrig-urgent" ? `Urgent (marked by human@test):\n${d1Envelope}` : d1Envelope]);
expect(f.writes).toHaveLength(2);
expect(f.bodies.a).toBe("");
});
it("delivers nine native-fitting 61-column lines plus their envelope at 65x45", async () => {
const f = await codexDelivery("draft-aware", { width: 65, height: 45 });
const text = wrapPaneEnvelope("sender@test", "worker@test", Array(9).fill("x".repeat(61)).join("\n"));
expect(await f.send("native-fit", text, f.options)).toMatchObject({ ok: true, sent: true });
expect(f.submissions).toEqual([text]);
});
it("owns the complete envelope after a native 62-column hard wrap at 65x45", async () => {
const f = await codexDelivery("draft-aware", { width: 65, height: 45 });
const body = "x".repeat(64);
const text = wrapPaneEnvelope("sender@test", "worker@test", body);
expect(text.split("\n").filter(row => row.length > 62)).toEqual([body]);
// Codex's two-column prompt and one-column right inset leave 62 cells.
// This ASCII word wraps without dropping or replacing any authored byte.
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport((f.bodies.a ?? "").replace(body, `${body.slice(0, 62)}\n${body.slice(62)}`), 65, 45));
expect(await f.send("native-wrap", text, f.options)).toMatchObject({ ok: true, sent: true });
expect(f.submissions).toEqual([text]);
expect(f.bodies.a).toBe("");
});
it("retains fourteen exact-width body lines before their insertion rows scroll the full envelope", async () => {
const f = await codexDelivery("draft-aware", { width: 65, height: 45 });
const body = "x".repeat(62);
const text = wrapPaneEnvelope("sender@test", "worker@test", Array(14).fill(body).join("\n"));
expect([...text].length).toBeLessThan(1000);
const rows = text.split("\n").flatMap(row => row === body ? [row, ""] : [row]);
expect(rows).toHaveLength(33); // Native fullscreen cap is 30 rows, including chrome.
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 65, 45, f.bodies.a ? rows : undefined));
expect(await f.send("full-width", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
expect(f.bodies.a).toBe("");
});
it("counts the insertion row of an exact-width line at the input viewport limit", async () => {
const f = await codexDelivery("draft-aware", { width: 160, height: 45 });
const full = "x".repeat(157);
const text = [full, ...Array(25).fill("x")].join("\n"); // 26 logical rows, 27 visual rows.
const rows = [full, "", ...Array(25).fill("x")];
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 160, 45, f.bodies.a ? rows : undefined));
expect(await f.send("insertion-row", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("delivers a full envelope whose logical lines fit one native row at 80x22", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 22 });
const text = wrapPaneEnvelope("sender@test", "worker@test", "Please review this task.", { stampISO: "2026-10-28T22:06:00Z" });
expect(text.split("\n").every(row => stringWidth(row) < 77)).toBe(true);
expect(await f.send("one-row-fit", text, f.options)).toMatchObject({ ok: true, sent: true });
expect(f.submissions).toEqual([text]);
});
it("conservatively retains a natively fitting wrapped wake when its full-line bound exceeds 80x22", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 22 });
const line = "Please review the current task and reply with the smallest useful change.";
expect(stringWidth(line)).toBe(73);
const body = Array(3).fill(line).join(" ");
const text = wrapPaneEnvelope("sender@test", "worker@test", body, { stampISO: "2026-10-28T21:49:00Z" });
// Native FirstFit keeps each phrase together; the next word cannot fit on the previous row.
const rows = text.replace(body, Array(3).fill(line).join("\n")).split("\n");
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 80, 22, f.bodies.a ? rows : undefined));
expect(rows).toHaveLength(9); // Fits natively, but the conservative bound is larger.
expect(await f.send("ordinary-wake", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
expect(f.submissions).toEqual([]);
});
it("retains a hyphen-separated envelope before native semantic breaks scroll it at 80x24", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 24 });
const body = Array(9).fill("x".repeat(40)).join("-");
const text = wrapPaneEnvelope("sender@test", "worker@test", body, { stampISO: "2026-10-28T22:03:00Z" });
expect(body).toHaveLength(368);
// Native HyphenSplitter moves each 41-cell piece whole; two cannot fit in 77.
const rows = text.replace(body, body.match(/x{40}-?/g)!.join("\n")).split("\n");
expect(rows).toHaveLength(15);
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 80, 24, f.bodies.a ? rows : undefined));
expect(await f.send("semantic-hyphens", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("retains a three-segment path envelope at the native viewport limit", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 22 });
const segment = "x".repeat(38) + "/";
const path = segment.repeat(3);
const text = wrapPaneEnvelope("sender@test", "worker@test", path + "\n\n", { stampISO: "2026-10-28T22:03:00Z" });
expect(path).toHaveLength(117);
// Native UAX#14 path pieces are 39 cells; two need 78, exceeding 77.
const rows = text.replace(path, Array(3).fill(segment).join("\n")).split("\n");
expect(rows).toHaveLength(11);
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 80, 22, f.bodies.a ? rows : undefined));
expect(await f.send("semantic-path", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("retains an envelope whose space before /path cannot be a native break", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 22 });
const first = "a".repeat(30), glued = "b".repeat(46) + " /cc", last = "d".repeat(30);
const body = `${first} ${glued} ${last}`;
const text = wrapPaneEnvelope("sender@test", "worker@test", body + "\n\n", { stampISO: "2026-10-28T22:03:00Z" });
// UAX#14 LB13 glues the slash to the preceding word across the space.
const rows = text.replace(body, [first, glued, last].join("\n")).split("\n");
expect(rows).toHaveLength(11);
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 80, 22, f.bodies.a ? rows : undefined));
expect(await f.send("semantic-glue", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("retains a raw-fitting 1200-code-point envelope before Codex collapses it into an unowned label", async () => {
const f = await codexDelivery("draft-aware", { width: 160, height: 45 });
const empty = wrapPaneEnvelope("sender@test", "worker@test", "");
const text = wrapPaneEnvelope("sender@test", "worker@test", "x".repeat(1200 - [...empty].length));
expect([...text]).toHaveLength(1200);
const result = await f.send("native-long", text, f.options);
expect(result).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(result.error).toContain("Send a shorter message; nothing was pasted.");
expect(result.error).toContain("Turning this seat's typing guard off explicitly disables draft protection.");
expect(f.writes).toEqual([]);
expect(f.bodies.a).toBe("");
});
it.each(["\t", "\x1b"])("gives control-specific recovery at every size (%j)", async control => {
const f = await codexDelivery("draft-aware", { width: 160, height: 45 });
for (const text of [`a${control}b`, `${"x".repeat(1200)}${control}`]) {
const result = await f.send("control", text, f.options);
expect(result).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(result.error).toMatch(/without them/);
expect(result.error).not.toMatch(/larger terminal/);
}
expect(f.writes).toEqual([]);
});
it("leaves default-off delivery unchanged in the small viewport", async () => {
const f = await codexDelivery("off");
expect(await f.send("off", d1Envelope, f.options)).toMatchObject({ ok: true });
expect(f.submissions).toEqual([d1Envelope]);
});
it("rechecks the actual viewport after buffer preparation, before any paste", async () => {
const f = await codexDelivery("draft-aware", { width: 160, height: 45 });
f.hooks.load = () => { f.size.width = 65; f.size.height = 9; };
expect(await f.send("resized", d1Envelope, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("retains a wrapped line which cannot stay visible without writing it", async () => {
const f = await codexDelivery();
expect(await f.send("wraps", "one-long-word-".repeat(40), f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("keeps a concurrent real draft and the human-input epoch check", async () => {
const f = await codexDelivery("draft-aware", { width: 160, height: 45 });
f.hooks.load = () => f.guard.humanInput("a", async () => { f.bodies.a = "my real draft"; });
expect(await f.send("human", d1Envelope, f.options)).toMatchObject({ outcome: "retained", sent: false,
inputPhase: "not_reached", reason: "draft_input_changed" });
expect(f.writes).toEqual([]);
expect(f.bodies.a).toBe("my real draft");
});
});
describe("current draft-aware delivery through the real transport", () => {
it.each(["ordinary request ", "1. Review the change"])("attempts each ordinary authored request, even with a reused caller ID: %j", async text => {
const f = fixture(); await f.enable();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@openrig/ui",
"version": "0.7.1",
"version": "0.7.5",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -1,10 +1,10 @@
{
"release": "0.7.2",
"archiveSha256": "42b4d4980c062f49f99db7e843a2ba96c0066a420cbaace381f435e22699d4c5",
"release": "0.7.4",
"archiveSha256": "d11dd8a6fc78f0c4d7afeae462c07c2af0fb9fe460990950be495016734920a1",
"plugins": {
"openrig-core": {
"version": "0.1.19",
"sha256": "21343eb7a36b0726a50372edc781b8e5b344c819a456499552f2560fb9712cb1"
"version": "0.1.21",
"sha256": "add5b2b93b97863b248e01a384d5d7c81ccbe27b0b0a368c4b0ef93a9b1bddf1"
}
}
}