mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b57c4da3c3 | ||
|
|
32be5c0aea | ||
|
|
f079c1f730 | ||
|
|
217501f0c1 | ||
|
|
b58066a329 | ||
|
|
04fbee997a |
No files matched your search
+223
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
Generated
+5
-5
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.5",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.5",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -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,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user