mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-12 18:48:31 -10:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fc45a0bf2 | ||
|
|
126efafeeb | ||
|
|
568ccef975 | ||
|
|
ec2ad9e1bd | ||
|
|
7ee288cdaf | ||
|
|
6d2b7468ce | ||
|
|
761edea6a9 | ||
|
|
77bc2161ce | ||
|
|
18337c55ec |
No files matched your search
+181
@@ -8,6 +8,187 @@ deprecations, and behavioral changes. Breaking changes are called out explicitly
|
||||
|
||||
---
|
||||
|
||||
## [0.7.14]
|
||||
|
||||
OpenRig 0.7.14 is a patch for steadier circulation on a busy team. A burst of node
|
||||
reads now takes turns, so a health check, a queue read or a send waits for one
|
||||
step of it instead of the whole burst, and bounded queue listings over a long
|
||||
retained history use indexes instead of scanning it. An assistant connected to
|
||||
your team can reach a seat by the name its team shows, connected assistants
|
||||
appear in `rig ps` and the TUI, and setup guidance tells the agent to fit a
|
||||
first team to your logins. There are fixes across delivery, seats and the TUI,
|
||||
many contributed by people using OpenRig. See Known limits for what this
|
||||
release doesn't fix.
|
||||
|
||||
### Before you upgrade
|
||||
|
||||
- **Stop the daemon, back up the database, install 0.7.14, then start the
|
||||
daemon.** Your agents keep running through a daemon restart; don't use
|
||||
`rig down`, which ends them.
|
||||
|
||||
```sh
|
||||
rig daemon stop
|
||||
cp "$(rig config get db.path)" "$(rig config get db.path).before-0.7.14"
|
||||
npm install -g @openrig/cli@0.7.14 # or your existing installation method
|
||||
rig daemon start
|
||||
```
|
||||
|
||||
- **Three migrations run before the daemon listens.** Migration 163 adds the
|
||||
`network_installations` table and four columns to `network_bindings`.
|
||||
Migrations 164 and 166 build indexes. The author's measurement of 166 took
|
||||
3.47 s on 28,976 rows, not a real installation's size, so a large database can
|
||||
take longer. The write overhead, and going back to 0.7.13 after these three
|
||||
migrations, aren't established; keep the backup. ([#3007](https://github.com/mvschwarz/openrig/pull/3007), [#3008](https://github.com/mvschwarz/openrig/pull/3008), [#3090](https://github.com/mvschwarz/openrig/pull/3090))
|
||||
- **If the start is reported pending:** the CLI waits 20 s, then may print
|
||||
"Daemon startup is pending after the readiness deadline". Readiness remains
|
||||
unconfirmed. It leaves the child running and the startup reservation
|
||||
retained, without recording the daemon. Inspect the address and log it
|
||||
prints. The child may later answer, and status can then report it running,
|
||||
but that does not finish this start's recording or release its reservation.
|
||||
Don't start it again until the steps below are done. The message's restart
|
||||
steps stop that child and clear the reservation: stop the named child PID and
|
||||
wait for it to exit, inspect `daemon-start.lock`, and only when both recorded
|
||||
launcher and child PIDs are absent, archive the lock and run
|
||||
`rig daemon start`. On a large database the new start can be reported pending
|
||||
again; reaching a recorded start this way hasn't been tested.
|
||||
`rig daemon stop` cannot stop that unpublished child.
|
||||
- **The `openrig` plugin carries the CLI's version, 0.7.14; openrig-core stays
|
||||
0.1.29.**
|
||||
|
||||
### Steadier under load
|
||||
|
||||
- Node reads (`rig ps --nodes`, the TUI, several agents at once) take turns,
|
||||
one step at a time, so other reads and sends are no longer held behind a
|
||||
whole burst. ([#3101](https://github.com/mvschwarz/openrig/pull/3101))
|
||||
- Bounded queue listings over retained history use indexes for their ordering
|
||||
and for live and archived owner notifications. ([#3090](https://github.com/mvschwarz/openrig/pull/3090))
|
||||
- Codex seats re-checked in one readiness poll share process listings where
|
||||
each seat's observation order and the 2 s freshness allow; each keeps its own
|
||||
later observation and its own fallback. ([#3061](https://github.com/mvschwarz/openrig/pull/3061))
|
||||
- New tmux servers raise their soft open-file limit to 8,192 or the hard limit
|
||||
where they can, keeping an already higher limit. This makes descriptor
|
||||
exhaustion less likely as panes and viewers add up; servers already running
|
||||
keep their limits. ([#3070](https://github.com/mvschwarz/openrig/pull/3070))
|
||||
- A delegation parent gets one standing notice per child seat for unclaimed
|
||||
rows, not one per row, and held work gets its notice back when its typing
|
||||
hold clears. ([#3049](https://github.com/mvschwarz/openrig/pull/3049), [#3082](https://github.com/mvschwarz/openrig/pull/3082))
|
||||
|
||||
### Your team and the people it works for
|
||||
|
||||
- An assistant connected over MCP reaches a seat by its plain name, stored
|
||||
under the seat's actual session; an unknown name, or one several seats match,
|
||||
says so. ([#3088](https://github.com/mvschwarz/openrig/pull/3088))
|
||||
- Connected assistants appear, with their check-in, in `rig ps`, `rig_team` and
|
||||
the TUI's tables, and their person sees plain tool titles. ([#3057](https://github.com/mvschwarz/openrig/pull/3057), [#3058](https://github.com/mvschwarz/openrig/pull/3058), [#3092](https://github.com/mvschwarz/openrig/pull/3092))
|
||||
- `rig doctor` and setup say a login is needed when a seat you start runs on
|
||||
that harness. The operator is taught to check each seat's runtime against
|
||||
your logins before launch, name any missing sign-in and let you choose.
|
||||
([#3077](https://github.com/mvschwarz/openrig/pull/3077))
|
||||
- When the readable person registry holds nobody, `rig daemon start` and
|
||||
`rig gateway participants` tell the setup agent to offer registration. The
|
||||
kernel operator is taught to include that offer in its greeting and register
|
||||
only on your yes. ([#3041](https://github.com/mvschwarz/openrig/pull/3041), [#3071](https://github.com/mvschwarz/openrig/pull/3071))
|
||||
- A team your agent hands work to knows how to ask you a question, and your
|
||||
answer wakes it. A click on an ask with several questions records that
|
||||
choice; the ask stays open until the last one. ([#3059](https://github.com/mvschwarz/openrig/pull/3059), [#3045](https://github.com/mvschwarz/openrig/pull/3045))
|
||||
- A pairing request's description of itself is quoted wherever it's shown. ([#3067](https://github.com/mvschwarz/openrig/pull/3067))
|
||||
- `rig queue list` takes exact `--tag`, `--mission` and `--slice` filters, and
|
||||
several tags must all match. ([#3066](https://github.com/mvschwarz/openrig/pull/3066))
|
||||
- A skill-only edition of the `openrig` plugin, for Claude's plugin directory,
|
||||
follows handed-on work and keeps its reply route. ([#3056](https://github.com/mvschwarz/openrig/pull/3056), [#3075](https://github.com/mvschwarz/openrig/pull/3075))
|
||||
|
||||
### Fixes
|
||||
|
||||
- An ordinary send is refused as needing input while an open Claude Code or
|
||||
Codex `/model` picker is positively identified; nothing is typed. Close the
|
||||
picker before trying to deliver the message. ([#3083](https://github.com/mvschwarz/openrig/pull/3083))
|
||||
- On a resume of a Codex seat's own conversation in a rig with pods, including
|
||||
`rig up --existing`, OpenRig avoids adding missing guidance blocks to the
|
||||
repository's tracked `AGENTS.md` when Codex is 0.164.0 or later, no guidance
|
||||
file or developer instructions are configured, and the configuration and
|
||||
block sources can be read. ([#3039](https://github.com/mvschwarz/openrig/pull/3039))
|
||||
- With `recovery.auto_drive_provider_prompts` on, Claude Code's gated-grants
|
||||
trust check is never answered for you, and the plain trust prompt is answered
|
||||
"Yes". ([#3073](https://github.com/mvschwarz/openrig/pull/3073))
|
||||
- Codex seats with linked `CODEX_HOME` folders or config files get hook trust
|
||||
under the keys Codex reads. ([#3079](https://github.com/mvschwarz/openrig/pull/3079))
|
||||
- `rig terminal open --provider herdr` reuses a live Herdr workspace, and Rex
|
||||
tiles show the seat's agent and its activity. ([#3068](https://github.com/mvschwarz/openrig/pull/3068), [#2992](https://github.com/mvschwarz/openrig/pull/2992))
|
||||
- `rig broadcast` prints a held recipient as retained, not sent. ([#3054](https://github.com/mvschwarz/openrig/pull/3054))
|
||||
- Account erasure also removes OpenRig's own wakes and notices for that
|
||||
account's seats. ([#3089](https://github.com/mvschwarz/openrig/pull/3089))
|
||||
- A snapshot without sessions returns its `invalid_snapshot_data` blocker
|
||||
instead of throwing a TypeError; the restore still stops there. A retried
|
||||
no-launch import recovers the original rig by its operation ID. ([#3069](https://github.com/mvschwarz/openrig/pull/3069), [#3042](https://github.com/mvschwarz/openrig/pull/3042))
|
||||
- A rebuilt briefing limits optional superseded recap paths to the newest
|
||||
three by default when one folder covers that history; the current recap and
|
||||
all artifact records remain. Other layouts keep the full list, and
|
||||
`output.rebuild_recaps` set to 0 lists all paths. ([#3055](https://github.com/mvschwarz/openrig/pull/3055))
|
||||
- `rig compact --state` names the input check that stopped a managed
|
||||
compaction, and the TUI's graph shows a seat the daemon reads as unknown as
|
||||
unknown. ([#3053](https://github.com/mvschwarz/openrig/pull/3053), [#3046](https://github.com/mvschwarz/openrig/pull/3046))
|
||||
- Setting a context-pressure threshold explains when an environment override
|
||||
inverts the pair, and `rig workflow validate` warns that harness pins aren't
|
||||
checked against seats. ([#2567](https://github.com/mvschwarz/openrig/pull/2567), [#3102](https://github.com/mvschwarz/openrig/pull/3102))
|
||||
- The config browser's entries say each setting's type, choices, range, format
|
||||
and write route. ([#3050](https://github.com/mvschwarz/openrig/pull/3050))
|
||||
|
||||
### Experimental
|
||||
|
||||
- `rig network` is experimental, unsupported and at your own risk. Its connector
|
||||
isn't inert: the daemon starts it, sweeps leftover credential folders and
|
||||
schedules local rounds even with no binding. ([#3007](https://github.com/mvschwarz/openrig/pull/3007), [#3084](https://github.com/mvschwarz/openrig/pull/3084))
|
||||
- A grant service can hold back only new work under its fair-use limit, and an
|
||||
app can read what needs a person, and the answers to it, by link. ([#3048](https://github.com/mvschwarz/openrig/pull/3048), [#3008](https://github.com/mvschwarz/openrig/pull/3008))
|
||||
|
||||
### Behaviour that changed
|
||||
|
||||
- **Node reads:** a nodes response is no longer one snapshot across its steps,
|
||||
and in a burst each nodes response comes later: about 43 ms against 21 ms
|
||||
median in a 24-request burst. One very large rig is still one step. ([#3101](https://github.com/mvschwarz/openrig/pull/3101))
|
||||
- **An assistant naming a seat now hears three errors:** `destination_ambiguous`
|
||||
(several seats match; the candidates are listed), `destination_not_found` (no
|
||||
seat it can reach has that name) and `destination_unchecked` (the team
|
||||
couldn't be read; try again). ([#3088](https://github.com/mvschwarz/openrig/pull/3088))
|
||||
|
||||
### Known limits
|
||||
|
||||
- **Queue listings:** the new indexes aren't a cure for every stall. Listings of
|
||||
active work showed no gain, one case was slower, and writes weren't measured. ([#3090](https://github.com/mvschwarz/openrig/pull/3090))
|
||||
- **Draft-aware delivery ([#3076](https://github.com/mvschwarz/openrig/issues/3076)):** an automatic OpenRig notice can still be left beside
|
||||
a draft started during delivery. In the reproduced paste-window cases the person's draft
|
||||
stayed intact and no Enter was sent, but OpenRig's text stayed in the input.
|
||||
Those cases don't cover a later window. On a conditional send, one that names
|
||||
the conversation it expects, an isolated test with a scripted terminal showed
|
||||
characters typed while OpenRig ran its last checks before Enter submitted
|
||||
together with OpenRig's text, and the send reported success. The same test
|
||||
reproduces it on 0.7.13. It hasn't been tested with a real client or model.
|
||||
- **The TUI's tables** can drop a connected assistant's check-in line when its
|
||||
rig's seat inventory can't be read; `rig roster` still prints it. ([#3105](https://github.com/mvschwarz/openrig/pull/3105) isn't in this release)
|
||||
- **Repository guidance on restore:** rigs without pods, old or unknown Codex
|
||||
versions, configured guidance and unreadable configuration keep the existing
|
||||
file path. Fresh, fork and handover launches are outside this resume fix.
|
||||
Existing managed blocks may still refresh, and the seat-skills line still
|
||||
belongs to the file writer; check `git status`. A block whose source can't be
|
||||
read is reported missing.
|
||||
- **From 0.7.13:** [its known limits](https://github.com/mvschwarz/openrig/releases/tag/v0.7.13)
|
||||
still apply, except where this release fixes them. The runtime fixes cover
|
||||
adding missing guidance on the eligible Codex resumes described above
|
||||
([#3039](https://github.com/mvschwarz/openrig/pull/3039)), recognized model pickers ([#3083](https://github.com/mvschwarz/openrig/pull/3083)) and the unquoted
|
||||
pairing description ([#3067](https://github.com/mvschwarz/openrig/pull/3067)).
|
||||
This release also changes the operator-first registration ([#3041](https://github.com/mvschwarz/openrig/pull/3041),
|
||||
[#3071](https://github.com/mvschwarz/openrig/pull/3071)) and pre-launch login guidance ([#3077](https://github.com/mvschwarz/openrig/pull/3077)); their observed
|
||||
outcomes aren't established yet. They weren't re-checked for this release
|
||||
unless named here.
|
||||
|
||||
### Credits
|
||||
|
||||
Thanks to @Kayhusk, @billbenetti, @cavalcanticampos, @Aummadour, @Tankard,
|
||||
@w4rc0n and @Totopo27 for merged contributions. Thanks also for the reports
|
||||
behind these fixes: @Kayhusk ([#3035](https://github.com/mvschwarz/openrig/issues/3035) and [#3078](https://github.com/mvschwarz/openrig/issues/3078)), @Tankard ([#2583](https://github.com/mvschwarz/openrig/issues/2583)),
|
||||
@dajiaohuang ([#2529](https://github.com/mvschwarz/openrig/issues/2529)), @billbenetti ([#2728](https://github.com/mvschwarz/openrig/issues/2728)), @emilio-gagliardi ([#3087](https://github.com/mvschwarz/openrig/issues/3087)), @korallis
|
||||
([#161](https://github.com/mvschwarz/openrig/issues/161)) and @w4rc0n ([#3017](https://github.com/mvschwarz/openrig/issues/3017)).
|
||||
|
||||
## [0.7.13]
|
||||
|
||||
OpenRig 0.7.13 is a patch for people running their own agent teams. When you answer a runner's permission request, you now get a
|
||||
|
||||
@@ -26,9 +26,9 @@ use:
|
||||
|
||||
```sh
|
||||
# Claude Code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.12/skills/rigs --skill rigs --agent claude-code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.14/skills/rigs --skill rigs --agent claude-code
|
||||
# Codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.12/skills/rigs --skill rigs --agent codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.14/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.12` 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.14` 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.12/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.12/scripts/install.sh | sh
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.14/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.14/scripts/install.sh | sh
|
||||
```
|
||||
|
||||
If the last step isn't ready, the installer reports `INSTALLED, SETUP NOT COMPLETE` and points to the details from `rig setup` above. Follow that output and any reported Fix, then rerun `rig setup`; the CLI does not need reinstalling. If the only remaining failures are provider sign-ins, sign in to each selected provider as below and continue.
|
||||
|
||||
@@ -182,8 +182,9 @@ With `needs-me.read` supported:
|
||||
about the open rows blocked on it (at most 100; empty when none are recorded).
|
||||
- **A snapshot watermark:** the response's `watermark` is the newest event `seq` read before the snapshot. Follow
|
||||
`GET /api/events` from it with `Last-Event-ID: <watermark>`: every event after the snapshot is replayed, some possibly
|
||||
already reflected in it. Recording one answer of several emits no event yet, so re-read the snapshot (or the row)
|
||||
rather than relying on events alone for partial answers.
|
||||
already reflected in it. A click that records or changes an answer emits `queue.answer_recorded` even while the set
|
||||
is partial. An identical repeated choice, or a later click after a pending ask's answer set is complete, records
|
||||
nothing new and emits none. Events are best effort; re-read the snapshot or row after a stream gap.
|
||||
- **The answers returned to a request:** `GET /api/queue/<qitemId>` adds `answers`, oldest first, each
|
||||
`{ qitemId, by, at, text, episode, recordedIdentity }`. They are found by the answer row's link to the request, not
|
||||
by its text, so don't parse the `In reply to:` line in an answer's body. `text` is the person's answer without that
|
||||
|
||||
@@ -185,10 +185,10 @@ agent doing the setup. Both use this release's tag:
|
||||
|
||||
```sh
|
||||
# Claude Code
|
||||
npx skills add "https://github.com/mvschwarz/openrig/tree/v0.7.12/skills/rigs" --skill rigs --agent claude-code
|
||||
npx skills add "https://github.com/mvschwarz/openrig/tree/v0.7.14/skills/rigs" --skill rigs --agent claude-code
|
||||
|
||||
# Codex
|
||||
npx skills add "https://github.com/mvschwarz/openrig/tree/v0.7.12/skills/rigs" --skill rigs --agent codex
|
||||
npx skills add "https://github.com/mvschwarz/openrig/tree/v0.7.14/skills/rigs" --skill rigs --agent codex
|
||||
```
|
||||
|
||||
If OpenRig is already installed, use the release tag matching your installed `rig --version` instead: replace the tag
|
||||
@@ -203,11 +203,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.12/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.14/scripts/install.sh | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
The same command without `--dry-run`,
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.12/scripts/install.sh | sh`, installs the
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.14/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
|
||||
|
||||
@@ -12,9 +12,9 @@ In your project folder, add OpenRig's `rigs` skill to the agent you use:
|
||||
|
||||
```sh
|
||||
# Claude Code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.12/skills/rigs --skill rigs --agent claude-code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.14/skills/rigs --skill rigs --agent claude-code
|
||||
# Codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.12/skills/rigs --skill rigs --agent codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.14/skills/rigs --skill rigs --agent codex
|
||||
```
|
||||
|
||||
These use this release's tag, for a new install. If OpenRig is already installed, replace the tag after `tree/` with
|
||||
|
||||
+826
-178
File diff suppressed because it is too large.
Load diff
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/daemon",
|
||||
@@ -7298,7 +7298,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -7336,7 +7336,7 @@
|
||||
},
|
||||
"packages/daemon": {
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.13.0",
|
||||
@@ -7380,7 +7380,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"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.14",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/acp",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"description": "OpenRig as an agent in ACP apps: starts rig acp serve from the @openrig/cli it pins.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -15,7 +15,7 @@
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openrig/cli": "0.7.1"
|
||||
"@openrig/cli": "0.7.14"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "openrig",
|
||||
"name": "OpenRig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"description": "Send work to your OpenRig team of agents and follow it through to the result",
|
||||
"repository": "https://github.com/mvschwarz/openrig",
|
||||
"website": "https://openrig.dev",
|
||||
@@ -12,7 +12,7 @@
|
||||
"license_url": "https://github.com/mvschwarz/openrig/blob/main/LICENSE",
|
||||
"distribution": {
|
||||
"npx": {
|
||||
"package": "@openrig/[email protected]"
|
||||
"package": "@openrig/[email protected]4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"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",
|
||||
"version": "0.1.0",
|
||||
"version": "0.7.14",
|
||||
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns.",
|
||||
"author": {
|
||||
"name": "Mike Schwarz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.1.0",
|
||||
"version": "0.7.14",
|
||||
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns.",
|
||||
"author": {
|
||||
"name": "Mike Schwarz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.1.0",
|
||||
"version": "0.7.14",
|
||||
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns.",
|
||||
"logo": "assets/logo.svg"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "openrig",
|
||||
"version": "0.1.0",
|
||||
"version": "0.7.14",
|
||||
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns.",
|
||||
"author": {
|
||||
"name": "Mike Schwarz",
|
||||
|
||||
@@ -10,8 +10,8 @@ already does it, or two verbs crossed already answer it. **You cannot look up a
|
||||
not know exists.** So the point of what follows is not skill. It is recognition: that when a task
|
||||
lands, something rhymes, and you go check instead of building.
|
||||
|
||||
Ninety-two top-level verbs ship (capability canon refreshed through
|
||||
`capability-delta-v0.7.12`). This marker describes the pack's teaching, not publication or live
|
||||
Ninety-three top-level verbs ship (capability canon refreshed through
|
||||
`capability-delta-v0.7.14`). 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`.
|
||||
@@ -33,7 +33,10 @@ one command away. Your memory of the fleet is a claim about the past.
|
||||
*including the broken ones*. **The bare form is the question you usually want:** `--active`
|
||||
filters to running and **hides detached, exited and attention states** — which are exactly the
|
||||
cells you were scanning for. A seat whose tmux session is confirmed gone reads `detached`
|
||||
(activity reason `session_missing`), and `rig ps --nodes` says to restore or relaunch it.
|
||||
(activity reason `session_missing`), and `rig ps --nodes` says to restore or relaunch it. Since 0.7.14 `rig ps` ends with connected
|
||||
assistants and their check-in line, and node reads take turns through their three synchronous
|
||||
steps, so other reads and sends aren't held behind a burst of them; a nodes response is no longer
|
||||
one snapshot across those steps, and in a burst each one comes later.
|
||||
A verified exited process in a retained pane reads `pane_exited`, instead of running and ready;
|
||||
outstanding work uses the existing recovery-finding route. An unknown observation is not an exit.
|
||||
`rig ps --nodes` also shows the model each seat was last seen using (`modelObservation`, with its
|
||||
@@ -109,7 +112,8 @@ one command away. Your memory of the fleet is a claim about the past.
|
||||
Closed work's content and metadata can also be erased on a schedule
|
||||
(`retention.closed_content_days`, `retention.closed_metadata_days`; `0`, the default, keeps them),
|
||||
with an optional `retention.erasure_ledger`, for a retention policy or an account deletion. It's
|
||||
opt-in, and a retried create can't bring erased work back.
|
||||
opt-in, and a retried create can't bring erased work back. Since 0.7.14 an account's erasure also
|
||||
removes OpenRig's own wakes and notices for that account's seats.
|
||||
- **Check the exit code.** Many commands used to print nothing and exit 0 when a daemon read
|
||||
failed; now they exit 1 or 2. Empty output from a failed read is not empty state.
|
||||
|
||||
@@ -124,7 +128,9 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
consequential delivery gets an effect check at the far end rather than a blind retry.
|
||||
`--verify` exits 1 when it can't confirm what its Enter did (`submission unconfirmed`, or text
|
||||
staged but not submitted): the message may still have arrived, so inspect with `rig capture`
|
||||
before any resend, and don't let a script resend on that exit code alone. A
|
||||
before any resend, and don't let a script resend on that exit code alone. Since 0.7.14 an ordinary
|
||||
send is refused as needing input while an open Claude Code or Codex `/model` picker is
|
||||
positively identified; nothing is typed. A
|
||||
producer-link advisory reading `no_activity_signal` means activity could not be determined;
|
||||
confirm by effect, never relaunch on it. A send or wake to a seat whose latest hook is a
|
||||
question waiting for a person is refused with `target_needs_input`, even when its screen
|
||||
@@ -175,7 +181,8 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
and the box was empty after Enter; it only reports.
|
||||
- **`rig broadcast`** — one fact to everyone at once, rather than relaying it N times and getting
|
||||
the wording wrong on the fourth. **Blast radius is real** — on a large rig this lands in every
|
||||
seat's turn. A broadcast that reached no one says so.
|
||||
seat's turn. A broadcast that reached no one says so, and a recipient a seat's setting held
|
||||
reads as retained, not sent.
|
||||
- **`rig capture <seat>`** — read what is on their screen right now. **`--rig` / `--pod`** captures
|
||||
every seat in one call. `--lines N` is the last N lines (stderr says how many earlier lines were
|
||||
cut); `--history-plus-pane` gives the older N history lines plus the whole pane. A short capture
|
||||
@@ -255,7 +262,7 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
default. **Opening `rig terminal open saved:kernel --window` is the step after install or start**:
|
||||
say you're opening it, run it on the daemon's desktop, then tell the person where it opened. Don't
|
||||
wait for a phrase like “show me my agents”; the same command brings their agents back later. No
|
||||
YAML, herdr when installed or plain tmux. The layout follows the terminal's width: on a narrow
|
||||
YAML, herdr when installed or plain tmux. Rex tiles show each seat's agent and its activity. The layout follows the terminal's width: on a narrow
|
||||
one the operator gets the first tab; on a wide one the dashboard and operator sit side by side
|
||||
and the advisor gets its own tab. A `kernel` view the person saved still wins.
|
||||
Tiles show their seat's screen without attaching, so whole lines show at any tile width; a
|
||||
@@ -480,7 +487,8 @@ relay is a reason to inspect the available routing, not to surrender judgment to
|
||||
- **`rig workflow validate`** — will this spec instantiate at all, before a run finds out for you. A
|
||||
step with no allowed exit that can finish it or route onward is reported as `step_cannot_finish`.
|
||||
An allowed mapped exit is valid; in a dependency graph, `handoff` can finish a sink without a next
|
||||
step.
|
||||
step. Since 0.7.14 each step with a `harness:` pin gets a `harness_pin_unchecked` warning:
|
||||
validation doesn't check pins against seats.
|
||||
- **`rig workflow compile <mission>`** / **`instantiate-lifecycle`** — derive one executable graph
|
||||
from `project.yaml` → `mission.yaml` → `slice.yaml`, inspect it without writing, then start an
|
||||
eligible graph with an opaque replay key. Typed acceptance candidate, verdict, and evidence
|
||||
@@ -632,6 +640,8 @@ not have to tear one down to change it, and you rarely have to start from nothin
|
||||
steps): hand it work, follow it, answer the team's questions in their own words, and read the
|
||||
result. VS Code, Copilot CLI and Cursor load it with no seat: give the app its own
|
||||
`rig mcp serve --front-door` server with `OPENRIG_SESSION_NAME` set and turn the plugin's off.
|
||||
A skill-only edition for Claude's plugin directory, in `plugins/claude-directory/`, follows
|
||||
handed-on work and keeps its reply route.
|
||||
- **A Claude member can bring 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 own storage, never the project, so one seat in a shared repository can have its own
|
||||
@@ -749,7 +759,10 @@ not have to tear one down to change it, and you rarely have to start from nothin
|
||||
conversation continuity stays unverified. Both refuse while typing protection is on.
|
||||
- **`rig unclaim` / `rig release`** — stop managing one adopted session, or all of them,
|
||||
**without killing the process or the agent inside it**. Handing something back to its human is a
|
||||
first-class move, not an abandonment.
|
||||
first-class move, not an abandonment. To stop an adopted session after unclaiming it, end its
|
||||
process and its tmux session; closing only the viewing window detaches it. Since 0.7.13 a fresh
|
||||
`rig launch` after `rig unclaim` ended the server's last session reopens the server (a released
|
||||
session that's still running blocks it, as before).
|
||||
|
||||
> **Two words that mean different things at different altitudes.** `unclaim` and `release` each
|
||||
> exist both here — *stop managing a live session* — and in the queue — *put a work row back down*.
|
||||
@@ -866,7 +879,9 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
effectful handover path.** Both forms perform the operation by default; pass `--dry-run`
|
||||
to plan without changing the seat. Check the selected source and continuity evidence first. A
|
||||
seat can be handed over again into its own pane, so a seat left half-handed-over recovers by
|
||||
running the handover again. A Claude seat started through a `#!` wrapper that runs Claude without
|
||||
running the handover again. A handover whose successor was launched but not ready says it may be
|
||||
running: look at the pane before retrying. A handover cleans up the retiring occupant's own claims
|
||||
and jobs. A Claude seat started through a `#!` wrapper that runs Claude without
|
||||
`exec`, or through the installer's `~/.local/bin/claude` symlink, rebuilds with `--source
|
||||
rebuild`; a failed rebuild leaves the resume target on the earlier conversation, so rebuild again
|
||||
rather than relaunching it fresh.
|
||||
@@ -957,7 +972,8 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
- **`rig compact-plan`** → **`rig compact`** — who is near the context wall, then act on it.
|
||||
**Ordering matters: running `compact` without the plan is guessing which seat needed it.**
|
||||
Managed compaction waits for the seat's restore map before compacting; `rig compact --state`,
|
||||
`--cancel` and `--skip-map` inspect, stop or skip that wait. If preparation stops at an ordinary
|
||||
`--cancel` and `--skip-map` inspect, stop or skip that wait; since 0.7.14 `--state` also names the
|
||||
input check behind a `draft_input_unknown` stop (`inputReadFailure`). If preparation stops at an ordinary
|
||||
approval, a manual `rig compact` keeps waiting for someone to answer it, within its 180-second
|
||||
budget; OpenRig never answers it, and the request times out after 240 seconds. The `claude-compaction-restore`
|
||||
skill covers writing and reading the map; `claude-compact-in-place` no longer ships. After a
|
||||
@@ -1231,6 +1247,9 @@ reaching them is ordinary work rather than an escalation.
|
||||
see which host you are acting on, and check a registered host is *actually* usable end to end.
|
||||
`rig host edit <id>` corrects an entry (its id and transport stay fixed), and `rig host remove <id>` deletes
|
||||
only the registry entry: credential files, the learned identity and the selection stay as they are.
|
||||
- **`rig network`** — **experimental, unsupported and at your own risk.** It connects one local seat
|
||||
to a hub you already have. The daemon starts the connector, sweeps leftover credential folders and
|
||||
schedules local rounds even with no binding, so it isn't inert, and its migrations apply on upgrade.
|
||||
- **A cross-host queue write that times out reads INDETERMINATE, not failed.** The CLI waits 15
|
||||
seconds for the daemon's answer, and `rig queue create` with no answer reads the row back once by
|
||||
its ID. Reconcile by that ID before any retry; don't send the write twice.
|
||||
@@ -1275,6 +1294,11 @@ reaching them is ordinary work rather than an escalation.
|
||||
a person can decide directly in the TUI's pairing settings. A relay connector that can't accept
|
||||
incoming connections can pull instead (`OPENRIG_RELAY_MODE=pull` in the relay secrets file); push
|
||||
stays the default, and a person's connector binding can be unset.
|
||||
**Since 0.7.13 a person's runner answer gets a note back:** who decides and in what state, or that
|
||||
the answer is kept and the tool hasn't run, and a runner waiting on it wakes on the decision. Read
|
||||
that note before asking the person to answer again. The answer a receiving agent reads says what an
|
||||
answer typed at the machine's terminal means, to act on a clear one with the request's own decision
|
||||
command, and how to ask the person back, with a new ask to them, when it's unclear.
|
||||
- **The `relay` gateway connector** carries a registered person's questions, updates and notices to a
|
||||
bridge process you run, over loopback HTTP, and takes their replies back with the sender resolved
|
||||
against their bindings. It's off until a person has a relay binding (`rig gateway human add <id> …
|
||||
@@ -1413,7 +1437,8 @@ your circumstances is configuration, and the ones that are not, another agent ca
|
||||
conversation it is in the middle of. **`provider signals` reports anomalies rather than a
|
||||
listing** — unbound seats, accounts shared across several seats. A Codex seat showing its
|
||||
usage-limit banner reads activity `unknown` with reason `usage_limit`, and keeps it while its
|
||||
screen redraws; `provider signals` can show a provider-event signal for that seat's account that
|
||||
screen redraws (a banner with a curly apostrophe included); a Claude seat whose usage-limit message
|
||||
states a reset time is parked until then, instead of being retried every few minutes; `provider signals` can show a provider-event signal for that seat's account that
|
||||
allows a switch decision. With `provider.usage_windows` on (off by default), `rig provider status`
|
||||
shows Codex's windows and Claude's 5-hour and weekly windows from the seats' status lines, each
|
||||
seat's configured account, and with `--all-hosts` or `--hosts <ids>` every registered machine's
|
||||
@@ -1484,7 +1509,14 @@ your circumstances is configuration, and the ones that are not, another agent ca
|
||||
(empty, the default, means every signed-in one). `--router-line yes|no` decides whether OpenRig
|
||||
adds its one line to shared guidance files (`skills.router_line.enabled`).
|
||||
Setup offers to register the person when none is registered, and a team started from the
|
||||
person's own assistant fits their own logins, with a review before anything is committed.
|
||||
person's own assistant fits their own logins, with a review before anything is committed. Since
|
||||
0.7.13, `rig daemon start` ends with that offer when the person registry can be read and holds
|
||||
nobody. The kernel operator's startup is also taught to run `rig gateway participants` before its
|
||||
first greeting and offer registration when nobody is registered. The agent asks first and
|
||||
registers only on the person's yes. Since 0.7.14 setup guidance tells the agent to fit a first
|
||||
team to the person's logins: a login `rig doctor` reports missing matters when a seat you'd start
|
||||
runs on that harness, so say which one before launch
|
||||
(`rig specs preview <team> --kind rig --json` shows each seat's runtime).
|
||||
- **`rig usage series`** — what a seat's token curve has looked like over time: climbing steadily,
|
||||
reset, or stopped reporting entirely. The last one is a signal, not a gap.
|
||||
- **`rig usage top`** — which of this machine's seats used the most tokens over a window (`--window
|
||||
@@ -1569,7 +1601,12 @@ your circumstances is configuration, and the ones that are not, another agent ca
|
||||
Clients on MCP protocol revision 2026-07-28 and on the 2025 revisions share the same endpoint.
|
||||
With a grant service (experimental and off by default; you serve it to the internet behind
|
||||
your own tunnel), `--task-destination <member@rig>` names where a caller's `rig_queue_create`
|
||||
goes when it gives no destination, and a seat's name alone resolves within the grant's team. A
|
||||
goes when it gives no destination, and a seat's name alone resolves within the grant's team. Since
|
||||
0.7.14 a plain name finds the seat's actual session (its own name, then its pod-member form, then
|
||||
that name's slug, among seats the grant can address): an ambiguous name returns
|
||||
`destination_ambiguous` with the candidates, an unknown one `destination_not_found`, and an
|
||||
unreadable team `destination_unchecked`. Its person sees each tool's plain title, and under a
|
||||
grant service's fair-use limit only new work is held back. A
|
||||
grant that names a person adds `rig_question_list` and `rig_question_answer`, so that person
|
||||
answers the team's questions from their assistant, recorded as their answer through that
|
||||
connection.
|
||||
@@ -1581,6 +1618,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.
|
||||
A hosted member's roster entry shows whether it's checking in: its expected cadence and its last
|
||||
check-in, read as checking in, overdue, not yet checked in or checking in on request, and unknown
|
||||
when that can't be read. It's a status, never a gate on delivery. Since 0.7.14 `rig ps`,
|
||||
`rig_team` and the TUI's tables list connected assistants with that check-in line.
|
||||
**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
|
||||
@@ -1638,7 +1679,8 @@ database. It is one more set of primitives on a machine full of them.
|
||||
original byte size and the exact expansion command. Read the complete original record with
|
||||
`rig queue show <qitemId> --full --json`; a preview or a compact list is not the whole body.
|
||||
Compact `rig queue list` rows carry `summary`; the fields they leave out are absent, not empty,
|
||||
and `fieldsElided` names them.
|
||||
and `fieldsElided` names them. Since 0.7.14 `--tag` (repeatable; every tag must match),
|
||||
`--mission` and `--slice` filter exactly, and bounded listings over retained history use indexes.
|
||||
Delivery status is separate from content: inspect its receipt before claiming it was consumed.
|
||||
- **"What am I waiting on?"** — the queue row's `waiting` view derives its owner, exact blocker
|
||||
and blocker owner, last meaningful change, activity confidence and next backstop from live
|
||||
@@ -1676,6 +1718,6 @@ deliberate writing.
|
||||
|
||||
**`rig --help`**, then `rig <verb> --help`.
|
||||
|
||||
Ninety-two verbs ship, and this page named most of them once. **You will not remember which — you
|
||||
Ninety-three verbs ship, and this page named most of them once. **You will not remember which — you
|
||||
are meant to remember only that the list is long enough to be worth reading before you build
|
||||
anything.** The question is never *how do I write this*; it is **does this already exist**.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -494,29 +494,7 @@ export class TmuxAdapter {
|
||||
if (probe.state !== "present") return false;
|
||||
const panes = await this.listPanes(target.session);
|
||||
return panes.length === 1 && panes[0]!.id === target.pane && await this.isPaneDead(target.pane);
|
||||
}, target => this.lastClientActivity(target.session));
|
||||
}
|
||||
|
||||
/** When a writable tmux client attached to this session last had activity (epoch ms, whole seconds), or null when no
|
||||
* writable client is attached. tmux's client_activity is session-wide and covers more than text keys, so this is
|
||||
* recent writable-client activity, not proof of typing in the pane. A read-only client (a viewer tile) can't type
|
||||
* into the seat and doesn't count. An attach counts: tmux can't tell a key in the attach's second from the attach,
|
||||
* so a delivery can be held briefly after one. A tmux failure or an unreadable row throws: the guard then holds
|
||||
* the paste rather than take it for "no activity" (#3076). */
|
||||
async lastClientActivity(session: string): Promise<number | null> {
|
||||
const exact = exactTarget(session, "session");
|
||||
const format = `#{client_readonly}${TMUX_FIELD_SEPARATOR}#{client_activity}`;
|
||||
const output = await this.run(["tmux", "list-clients", "-t", exact, "-F", format],
|
||||
`tmux list-clients -t ${shellQuote(exact)} -F "${format}"`);
|
||||
let latest: number | null = null;
|
||||
for (const line of output.split("\n").map(row => row.trim()).filter(row => row !== "")) {
|
||||
const [readonly, activity, ...rest] = line.split(TMUX_FIELD_SEPARATOR);
|
||||
if (rest.length || !/^[01]$/.test(readonly ?? "") || !/^[1-9]\d*$/.test(activity ?? "")) {
|
||||
throw new Error(`tmux list-clients: unreadable client row for ${session}`);
|
||||
}
|
||||
if (readonly === "0") latest = Math.max(latest ?? 0, Number(activity) * 1000);
|
||||
}
|
||||
return latest;
|
||||
});
|
||||
}
|
||||
private readonly freshProbes = new Map<string, string>();
|
||||
private readonly freshManaged = new Map<string, {nodeId: string; pane: string}>();
|
||||
@@ -1103,13 +1081,12 @@ export class TmuxAdapter {
|
||||
// A paste the guard proves it left unsubmitted (#2429) is submitted first, under this lease, once this send's own
|
||||
// checks pass; they then aren't run a second time.
|
||||
beforePaste: async () => {
|
||||
// This send's own checks can wait (a conversation lookup, an input capture), so they run first, and the
|
||||
// guard's checks run last (#3076). A check that refuses leaves a leftover's record for the next delivery.
|
||||
await options?.beforePaste?.();
|
||||
const checks = async () => { beforeWrite(); beforeInput?.(); };
|
||||
let checked = false;
|
||||
const checks = async () => { await options?.beforePaste?.(); beforeWrite(); beforeInput?.(); checked = true; };
|
||||
// The synchronous checks run again just before the key: the guard's re-read awaits after `checks`.
|
||||
const enter = async () => { beforeWrite(); beforeInput?.(); return (await this.sendKeysUnchecked(pane, ["Enter"])).ok; };
|
||||
if (checkDraft) await guard!.beforeInput(identity, false, text, { checks, enter });
|
||||
if (!checked) await options?.beforePaste?.();
|
||||
},
|
||||
afterPaste: async () => { if (checkDraft) await guard!.notePasted(identity, text); },
|
||||
} : undefined;
|
||||
|
||||
@@ -60,15 +60,6 @@ export interface TypingGuardConfig extends TypingGuardSettings { revision: strin
|
||||
const DEFAULT_GUARD: TypingGuardConfig = { mode: "off", revision: "legacy-off" };
|
||||
/** How long the guard waits before reading again an input it couldn't read right after its own paste or Enter (#2429). */
|
||||
const OWN_INPUT_REREAD_MS = 300;
|
||||
/** Right before an automatic paste, the empty input is read once more after this wait: a draft typed (or redrawn by the
|
||||
* agent's TUI) since the first read is seen before the paste, not after it, where nothing is removed (#2447, #3076). */
|
||||
const PASTE_SETTLE_MS = 300;
|
||||
/** Recent activity at a writable tmux client of the seat (keys, but also other client input; session-wide, not proof of
|
||||
* typing in this pane) holds the paste: a later attempt can deliver once it reads quiet and the input empty. tmux
|
||||
* reports client activity in whole seconds, so this is three of them (#3076). */
|
||||
const INPUT_QUIET_MS = 3000;
|
||||
/** tmux's client_activity is in whole seconds; the activity may have come up to this much after the second it reports. */
|
||||
const CLIENT_ACTIVITY_PRECISION_MS = 999;
|
||||
/** A terminal member with nothing set holds OpenRig's own notices and takes deliberate sends. */
|
||||
const TERMINAL_DEFAULT: TypingGuardConfig = { mode: "off", level: "sends-only", revision: "terminal-default" };
|
||||
/** The actor a guard change records when its request carries no seat identity, as from an ordinary terminal (#2350).
|
||||
@@ -131,9 +122,6 @@ export class SeatDeliveryGuard {
|
||||
/** A paste this guard kept from being submitted because it couldn't read the input, per node (#2429). */
|
||||
private readonly leftovers = new Map<string, { target: GuardTarget; humanEpoch: number; text: string; collapsedPaste: string | null }>();
|
||||
private inspectInput?: (target: GuardTarget, owned?: ComposerOwnership) => Promise<ComposerInput | null>;
|
||||
/** When a writable tmux client of the seat last had activity (epoch ms, whole seconds); null when no writable client
|
||||
* is attached; a rejection when that can't be known. */
|
||||
private inputActivity?: (target: GuardTarget) => Promise<number | null>;
|
||||
private inputAbsent?: (target: GuardTarget) => Promise<boolean>;
|
||||
private readonly hasModeConfig: boolean;
|
||||
|
||||
@@ -145,11 +133,9 @@ export class SeatDeliveryGuard {
|
||||
this.hasModeConfig = (db.prepare("PRAGMA table_info(seat_delivery_guards)").all() as Array<{ name: string }>).some(c => c.name === "desired_config");
|
||||
}
|
||||
|
||||
attachInputInspection(inspect: (target: GuardTarget, owned?: ComposerOwnership) => Promise<ComposerInput | null>, absent: (target: GuardTarget) => Promise<boolean>,
|
||||
activity?: (target: GuardTarget) => Promise<number | null>): void {
|
||||
attachInputInspection(inspect: (target: GuardTarget, owned?: ComposerOwnership) => Promise<ComposerInput | null>, absent: (target: GuardTarget) => Promise<boolean>): void {
|
||||
this.inspectInput = inspect;
|
||||
this.inputAbsent = absent;
|
||||
this.inputActivity = activity;
|
||||
}
|
||||
|
||||
/** Startup-only, before exposing routes or starting writers. A stopped operation cannot
|
||||
@@ -497,9 +483,6 @@ 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 {
|
||||
// Right before the paste (the adapter's check passes the text): the read above is a snapshot, and a draft typed
|
||||
// after it would get the paste appended and then lose the Enter, with the paste left in the person's input (#3076).
|
||||
if (pasteText !== undefined) input = await this.quietBeforePaste(target, read, unknown);
|
||||
const uncertainty = pasteText === undefined ? null : composerPasteUncertainty(input, pasteText);
|
||||
if (uncertainty) {
|
||||
const message = uncertainty === "controls"
|
||||
@@ -513,31 +496,6 @@ export class SeatDeliveryGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/** No recent writable-client activity before a short wait, the input still empty when read again after it, and still
|
||||
* no recent activity after that read. A time tmux can't report holds the paste, as an unreadable input does. The
|
||||
* read it returns is the one the paste goes on. Typing after this final check and before the paste can still leave
|
||||
* the notice beside the person's draft. In the tested append-mode schedule the Enter is then refused and nothing is
|
||||
* submitted or deleted; that isn't a guarantee for every interleaving (#3076). */
|
||||
private async quietBeforePaste(target: GuardTarget, read: () => Promise<ComposerInput | null>,
|
||||
unknown: (reading: ComposerInput | null) => DeliveryGuardError): Promise<ComposerInput> {
|
||||
const quiet = async () => {
|
||||
if (!this.inputActivity) return;
|
||||
const last = await this.inputActivity(target).then(at => ({ at }), () => null);
|
||||
if (!last) throw new DeliveryGuardError("draft_input_unknown", "OpenRig couldn't read the recent activity of the seat's terminal clients. Automatic delivery is held; nothing was pasted.");
|
||||
// tmux reports whole seconds: activity at N.999 s reads as N, so the quiet window counts from the end of that second.
|
||||
if (last.at !== null && Date.now() - (last.at + CLIENT_ACTIVITY_PRECISION_MS) < INPUT_QUIET_MS) {
|
||||
throw new DeliveryGuardError("draft_input_busy", "A writable client of this seat had recent activity. Automatic delivery is held; nothing was pasted.");
|
||||
}
|
||||
};
|
||||
await quiet();
|
||||
await this.sleep(PASTE_SETTLE_MS);
|
||||
const again = await read();
|
||||
if (!again || again.state === "unknown") throw unknown(again);
|
||||
if (again.state !== "empty") throw new DeliveryGuardError("draft_input_busy", "The input changed just before the paste. Automatic delivery is held; nothing was pasted.");
|
||||
await quiet();
|
||||
return again;
|
||||
}
|
||||
|
||||
/** A leftover paste is OpenRig's only while it's exactly there: the same occupant, no person's input through OpenRig
|
||||
* since, and its text or the label recorded for it. A label first seen later proves nothing (#1871). */
|
||||
private ownsLeftover(target: GuardTarget, input: ComposerInput): boolean {
|
||||
|
||||
@@ -32,7 +32,7 @@ function signal() {
|
||||
return { ready: new Promise<void>(resolve => { release = resolve; }), release: () => release() };
|
||||
}
|
||||
|
||||
function fixture(saved?: Buffer, opts: { runtime?: string; idleHook?: boolean; appendPaste?: boolean } = {}) {
|
||||
function fixture(saved?: Buffer, opts: { runtime?: string; idleHook?: boolean } = {}) {
|
||||
const db = new Database(saved ?? ":memory:");
|
||||
if (!saved) {
|
||||
migrate(db, ALL_MIGRATIONS);
|
||||
@@ -42,27 +42,23 @@ function fixture(saved?: Buffer, opts: { runtime?: string; idleHook?: boolean; a
|
||||
INSERT INTO sessions(id,node_id,session_name,status) VALUES ('sa','a','worker@test','running'),('sb','b','sibling@test','running');
|
||||
INSERT INTO occupant_tenures(id,node_id,generation_ordinal,generation_uuid,kind) VALUES ('ga','a',1,'g1','fresh'),('gb','b',1,'g2','fresh');`);
|
||||
}
|
||||
const hooks: { load?: () => void | Promise<void>; paste?: () => void | Promise<void>; sleep?: (ms: number) => void | Promise<void>;
|
||||
settle?: (ms: number) => void | Promise<void>; clients?: () => string } = {};
|
||||
// The guard's own waits (its re-reads) go to `settle`, kept apart from the transport's `sleep`; none is real time.
|
||||
const guard = new SeatDeliveryGuard(db, name => resolveGuardTarget(db, name), async ms => { await hooks.settle?.(ms); });
|
||||
const guard = new SeatDeliveryGuard(db, name => resolveGuardTarget(db, name));
|
||||
guard.recoverActivation();
|
||||
const bodies: Record<string, string | null> = { a: "", b: "" };
|
||||
const history: Record<string, string> = { a: "", b: "" };
|
||||
const writes: string[] = [], submissions: string[] = [];
|
||||
const observations: unknown[] = [];
|
||||
const hooks: { load?: () => void | Promise<void>; paste?: () => void | Promise<void>; sleep?: (ms: number) => void | Promise<void> } = {};
|
||||
let staged = "";
|
||||
// A terminal pane shows a submitted line as typed; an agent pane echoes a sent message under its prompt, as Claude does.
|
||||
const echo = (node: string, body: string) => (db.prepare("SELECT runtime FROM nodes WHERE id=?").get(node) as { runtime: string }).runtime === "terminal"
|
||||
? `${body}\n` : `${body.split("\n").map((row, i) => (i ? " " : "❯ ") + row).join("\n")}\n`;
|
||||
const tmux = new TmuxAdapter(async command => {
|
||||
if (command.includes("list-clients")) return hooks.clients?.() ?? "";
|
||||
if (command.includes("load-buffer")) await hooks.load?.();
|
||||
const node = command.includes("%2") ? "b" : "a";
|
||||
if (command.includes("paste-buffer")) {
|
||||
await hooks.paste?.();
|
||||
// appendPaste: a paste lands after whatever the input already holds, as in a real composer (#3076 tests).
|
||||
writes.push(`paste:${node}:${staged}`); bodies[node] = opts.appendPaste ? (bodies[node] ?? "") + staged : staged;
|
||||
writes.push(`paste:${node}:${staged}`); bodies[node] = staged;
|
||||
}
|
||||
if (command.includes("send-keys")) {
|
||||
writes.push(`keys:${node}:${command}`);
|
||||
@@ -1160,158 +1156,3 @@ describe("draft-aware at a full held quota (#2430)", () => {
|
||||
expect(f.outbox.retentionUsage("a").count).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("#3076: a draft typed between the guard's read and the paste", () => {
|
||||
const draft = "Please send it to the team. Also";
|
||||
const notice = "[OpenRig watchdog scheduler · policy: periodic-reminder · job: j]\nWake timer fired.";
|
||||
const pasted = (f: ReturnType<typeof fixture>) => f.writes.filter(w => w.startsWith("paste:"));
|
||||
const setActivity = (f: ReturnType<typeof fixture>, at: () => number | null) => {
|
||||
vi.spyOn(f.tmux, "lastClientActivity").mockImplementation(async () => at());
|
||||
};
|
||||
|
||||
it("is seen by the re-read after the settle wait: the notice is held and nothing is pasted into the person's input", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
const waits: number[] = [];
|
||||
// The person starts typing during the guard's wait right before the paste (the first read saw an empty input).
|
||||
f.hooks.settle = ms => { waits.push(ms); if (!pasted(f).length) f.bodies.a = draft; };
|
||||
const result = await f.send("watchdog-wake", notice, { openrigNotice: true, verify: false });
|
||||
expect(result).toMatchObject({ outcome: "retained", sent: false, reason: "draft_input_busy" });
|
||||
expect(waits[0]).toBe(300);
|
||||
expect(pasted(f)).toEqual([]);
|
||||
expect(f.submissions).toEqual([]);
|
||||
expect(f.bodies.a).toBe(draft);
|
||||
});
|
||||
|
||||
it("holds on recent writable-client activity, also activity during the wait, and a later attempt delivers once quiet", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
let last: number | null = Date.now() - 1000;
|
||||
setActivity(f, () => last);
|
||||
const busy = await f.send("wake-1", "Wake timer fired.", { openrigNotice: true, verify: false });
|
||||
expect(busy).toMatchObject({ outcome: "retained", sent: false, reason: "draft_input_busy" });
|
||||
|
||||
last = Date.now() - 60_000;
|
||||
f.hooks.settle = () => { last = Date.now(); }; // activity during the settle wait, nothing drawn yet
|
||||
f.advance();
|
||||
const during = await f.send("wake-2", "Wake timer fired again.", { openrigNotice: true, verify: false });
|
||||
expect(during).toMatchObject({ outcome: "retained", sent: false, reason: "draft_input_busy" });
|
||||
expect(pasted(f)).toEqual([]);
|
||||
|
||||
last = Date.now() - 60_000;
|
||||
f.hooks.settle = undefined;
|
||||
f.advance();
|
||||
const sent = await f.send("wake-3", "Wake timer fired a third time.", { openrigNotice: true, verify: false });
|
||||
expect(sent).toMatchObject({ ok: true });
|
||||
expect(f.submissions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("counts the quiet window from the end of the whole second tmux reports: 3.5 s ago still holds, 4.5 s ago delivers", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
let last = Date.now() - 3500; // activity at N.999 s reads as N: it may have been 2.5 s ago
|
||||
setActivity(f, () => last);
|
||||
const held = await f.send("whole-second", "Wake timer fired.", { openrigNotice: true, verify: false });
|
||||
expect(held).toMatchObject({ outcome: "retained", sent: false, reason: "draft_input_busy" });
|
||||
last = Date.now() - 4500;
|
||||
f.advance();
|
||||
const sent = await f.send("whole-second-later", "Wake timer fired again.", { openrigNotice: true, verify: false });
|
||||
expect(sent).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("holds as unreadable when tmux can't report recent writable-client activity; with no writable client it delivers", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
vi.spyOn(f.tmux, "lastClientActivity").mockRejectedValue(new Error("command timed out"));
|
||||
const held = await f.send("activity-unreadable", notice, { openrigNotice: true, verify: false });
|
||||
expect(held).toMatchObject({ sent: false, reason: "draft_input_unknown" });
|
||||
expect(pasted(f)).toEqual([]);
|
||||
setActivity(f, () => null);
|
||||
f.advance();
|
||||
const sent = await f.send("no-client", "Wake timer fired again.", { openrigNotice: true, verify: false });
|
||||
expect(sent).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("holds as unreadable when the input can't be read after the wait", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
f.hooks.settle = () => { if (!pasted(f).length) f.bodies.a = null; };
|
||||
const result = await f.send("unreadable", notice, { openrigNotice: true, verify: false });
|
||||
expect(result).toMatchObject({ sent: false, reason: "draft_input_unknown" });
|
||||
expect(pasted(f)).toEqual([]);
|
||||
});
|
||||
|
||||
it("runs the send's own waiting checks before the guard's last read, so a draft typed while they wait is seen", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
// A conditional send's checks await (a conversation lookup); the person types meanwhile. The adapter returns the
|
||||
// guard's refusal as its result.
|
||||
const result = await f.guard.operation("worker@test", () =>
|
||||
f.tmux.sendText("worker@test", notice, undefined, { beforePaste: async () => { f.bodies.a = draft; } }));
|
||||
expect(result).toMatchObject({ ok: false, code: "draft_input_busy" });
|
||||
expect(pasted(f)).toEqual([]);
|
||||
expect(f.bodies.a).toBe(draft);
|
||||
});
|
||||
|
||||
it("applies to every automatic input under draft-aware, a deliberate send included, as the empty check already did", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
f.hooks.settle = () => { if (!pasted(f).length) f.bodies.a = draft; };
|
||||
const result = await f.send("deliberate", "a message from another seat", { verify: false });
|
||||
expect(result).toMatchObject({ sent: false, reason: "draft_input_busy" });
|
||||
expect(pasted(f)).toEqual([]);
|
||||
expect(f.bodies.a).toBe(draft);
|
||||
});
|
||||
|
||||
it("the remaining gap: typing after the final check and before the paste leaves the notice beside the draft, never submitted or deleted", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
let typed = false;
|
||||
// The person types between the guard's last check and tmux's paste: the paste lands after their text.
|
||||
f.hooks.paste = () => { if (!typed) { typed = true; f.bodies.a = draft; } };
|
||||
const result = await f.send("final-boundary", notice, { openrigNotice: true, verify: false });
|
||||
// Contamination: exactly one paste, and the input is the person's text followed by it, nothing removed.
|
||||
expect(pasted(f)).toHaveLength(1);
|
||||
const staged = pasted(f)[0]!.slice("paste:a:".length);
|
||||
expect(f.bodies.a).toBe(draft + staged);
|
||||
// Submission: none. No key at all reached the pane (no Enter, C-m or C-u), and the guard says why.
|
||||
expect(f.writes.filter(w => w.startsWith("keys:a"))).toEqual([]);
|
||||
expect(f.submissions).toEqual([]);
|
||||
expect(result).toMatchObject({ ok: false });
|
||||
expect(JSON.stringify(result)).toContain("draft_input_changed");
|
||||
});
|
||||
|
||||
it("checks the activity again after the final read: activity that appears at that read holds the paste", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
let last: number | null = Date.now() - 60_000;
|
||||
setActivity(f, () => last);
|
||||
const real = f.tmux.captureComposerSnapshot.getMockImplementation()!;
|
||||
let armed = false;
|
||||
f.hooks.settle = () => { armed = true; };
|
||||
// The composer still reads empty at the final read, but a writable client has activity right then.
|
||||
vi.spyOn(f.tmux, "captureComposerSnapshot").mockImplementation(async (name: string) => {
|
||||
const shot = await real(name);
|
||||
if (armed) { armed = false; last = Date.now(); }
|
||||
return shot;
|
||||
});
|
||||
const result = await f.send("activity-at-final-read", notice, { openrigNotice: true, verify: false });
|
||||
expect(result).toMatchObject({ outcome: "retained", sent: false, reason: "draft_input_busy" });
|
||||
expect(pasted(f)).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads tmux's real list-clients output end to end: an attach in this second holds, a quiet client delivers", async () => {
|
||||
const f = fixture(undefined, { runtime: "claude-code", appendPaste: true });
|
||||
await f.enable();
|
||||
const now = () => Math.floor(Date.now() / 1000);
|
||||
f.hooks.clients = () => `1|${now()}\n0|${now()}\n`; // a viewer tile and a writable client attached this second
|
||||
const held = await f.send("attach-now", notice, { openrigNotice: true, verify: false });
|
||||
expect(held).toMatchObject({ outcome: "retained", sent: false, reason: "draft_input_busy" });
|
||||
expect(pasted(f)).toEqual([]);
|
||||
f.hooks.clients = () => `1|${now()}\n0|${now() - 10}\n`; // only the read-only tile is active
|
||||
f.advance();
|
||||
const sent = await f.send("attach-quiet", "Wake timer fired again.", { openrigNotice: true, verify: false });
|
||||
expect(sent).toMatchObject({ ok: true });
|
||||
});
|
||||
});
|
||||
@@ -51,47 +51,6 @@ describe("TmuxAdapter.listSessionClientTerminals", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("TmuxAdapter.lastClientActivity (#3076)", () => {
|
||||
const activity = (stdout: string) => adapterFor(mockExec({ "list-clients": { stdout } })).lastClientActivity("op@kernel");
|
||||
|
||||
it("is the latest activity of a writable client, in milliseconds", async () => {
|
||||
expect(await activity("0|1790000100\n0|1790000105\n")).toBe(1790000105000);
|
||||
});
|
||||
|
||||
it("does not count a read-only client (a viewer tile)", async () => {
|
||||
expect(await activity("1|1790000200\n0|1790000100\n")).toBe(1790000100000);
|
||||
expect(await activity("1|1790000200\n")).toBeNull();
|
||||
});
|
||||
|
||||
it("counts an attach as recent activity: a key in the same second as the attach can't be told apart from it", async () => {
|
||||
// tmux would report client_created == client_activity for both an attach alone and an attach plus a key that second.
|
||||
expect(await activity("0|1790000300\n")).toBe(1790000300000);
|
||||
});
|
||||
|
||||
it("is null with no client", async () => {
|
||||
expect(await activity("")).toBeNull();
|
||||
expect(await activity("\n")).toBeNull();
|
||||
});
|
||||
|
||||
it("throws on a row it can't read, alone or next to a readable one, so the guard holds", async () => {
|
||||
for (const stdout of ["0|abc\n", "0|0\n", "0\n", "2|1790000100\n", "0|1790000000|1790000100\n", "0|1790000100\n0|abc\n"]) {
|
||||
await expect(activity(stdout)).rejects.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it("throws when tmux can't say", async () => {
|
||||
for (const error of [new Error("can't find session: op@kernel"), NO_SERVER_ERROR, new Error("command timed out")]) {
|
||||
await expect(adapterFor(mockExec({ "list-clients": { error } })).lastClientActivity("op@kernel")).rejects.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it("asks tmux for the read-only flag and the activity of each client of the session", async () => {
|
||||
const commands: string[] = [];
|
||||
await adapterFor(async command => { commands.push(command); return ""; }).lastClientActivity("op@kernel");
|
||||
expect(commands.some(c => c.includes("list-clients") && c.includes("#{client_readonly}|#{client_activity}"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TmuxAdapter", () => {
|
||||
it("starts only an empty terminal server and reconciles repeated requests", async () => {
|
||||
let live = false;
|
||||
|
||||
@@ -158,7 +158,6 @@ async function driveEveryMethod(): Promise<RunCall[]> {
|
||||
await a.readPaneLastActivity(SESSION);
|
||||
await a.listClients();
|
||||
await a.listSessionClientTerminals(SESSION);
|
||||
await a.lastClientActivity(SESSION); // #3076: the client activity read right before an automatic paste
|
||||
await a.switchClient("/dev/ttys001", SESSION);
|
||||
// A runtime switch's exchange: two session-id reads, then the guarded if-shell swap (nested quoting); and its env writes.
|
||||
await a.swapRuntimeSwitchPanes({ nodeId: "n1", stagingSession: "staging'q a", canonicalSession: SESSION, canonicalPane: "%1", stagedPane: "%1" });
|
||||
|
||||
@@ -10,7 +10,7 @@ export function toConnectedAssistants(body: unknown): ConnectedAssistant[] | nul
|
||||
if (!Array.isArray(members)) return null;
|
||||
return members.flatMap((member: Record<string, unknown>) => {
|
||||
if (typeof member?.seat !== "string" || !member.seat) return [];
|
||||
const known = typeof member.status === "string" && Object.hasOwn(CHECK_IN_DISPLAY, member.status);
|
||||
const known = typeof member.status === "string" && member.status in CHECK_IN_DISPLAY;
|
||||
return [{ seat: member.seat, checkIn: {
|
||||
status: known ? member.status as CheckInStatus : "unknown",
|
||||
lastCheckInAt: typeof member.lastCheckInAt === "string" ? member.lastCheckInAt : null,
|
||||
|
||||
@@ -958,11 +958,7 @@ function contentLines(state: ViewState, snap: FleetSnapshot, contentWidth: numbe
|
||||
{ text: `Its seats aren't read in this view: rig ps --host ${rig.remoteHost} --nodes --rig ${rig.name}` },
|
||||
];
|
||||
if (rig.inventoryNotLoaded) return [{ text: `Reading ${rig.name}…` }];
|
||||
if (rig.inventoryUnavailable) return [
|
||||
{ text: `Inventory unavailable for ${rig.name} · refresh to Retry` },
|
||||
...(leaf?.kind === "pod" ? [] : connectedAssistantLines(host.connectedAssistants, host.connectedAssistantsUnavailable,
|
||||
rig.name, state.filter, contentWidth, state.timeZone)),
|
||||
];
|
||||
if (rig.inventoryUnavailable) return [{ text: `Inventory unavailable for ${rig.name} · refresh to Retry` }];
|
||||
const podFilter = leaf?.kind === "pod" ? leaf.name : null;
|
||||
const all = rig.pods.flatMap((p) => p.agents.map((a) => ({ pod: p.name, ...a })));
|
||||
const rows = all
|
||||
|
||||
@@ -782,34 +782,6 @@ describe("snapshot hydration over the §4.A reads (Phase 2)", () => {
|
||||
expect(instanceLines).not.toContain("not shown");
|
||||
});
|
||||
|
||||
it("shows inherited-property check-in statuses as unknown", async () => {
|
||||
const hosted = { members: ["constructor", "toString"].map((status, i) => ({
|
||||
seat: `guest${i}@myrig`, status, reason: "unrecognized reason",
|
||||
})) };
|
||||
const snap = await hydrateSnapshot(fixtureClient({}, { "/api/queue/hosted-members": hosted }));
|
||||
const view = createViewState({ instanceId: "t", getSnapshot: () => snap });
|
||||
view.dispatch({ type: "drill", resource: "rig", name: "myrig" });
|
||||
const lines = renderScreen(view.get(), snap, { cols: 160, rows: 60 }).lines.join("\n");
|
||||
expect(lines).toContain("guest0@myrig unknown");
|
||||
expect(lines).toContain("guest1@myrig unknown");
|
||||
expect(lines).not.toContain("unrecognized reason");
|
||||
});
|
||||
|
||||
it("shows connected check-ins beside a failed rig nodes read", async () => {
|
||||
const hosted = { members: [
|
||||
{ seat: "chat@myrig", status: "on-request", lastCheckInAt: null },
|
||||
{ seat: "other@downrig", status: "checking-in", lastCheckInAt: null },
|
||||
] };
|
||||
const snap = await hydrateSnapshot(fixtureClient({ "/api/rigs/01JRIG/nodes": { status: 500 } },
|
||||
{ "/api/queue/hosted-members": hosted }));
|
||||
const view = createViewState({ instanceId: "t", getSnapshot: () => snap });
|
||||
view.dispatch({ type: "drill", resource: "rig", name: "myrig" });
|
||||
const lines = renderScreen(view.get(), snap, { cols: 160, rows: 60 }).lines.join("\n");
|
||||
expect(lines).toContain("Inventory unavailable for myrig");
|
||||
expect(lines).toContain("chat@myrig on request:");
|
||||
expect(lines).not.toContain("other@downrig");
|
||||
});
|
||||
|
||||
it("a known-empty list of connected assistants stays quiet", async () => {
|
||||
const snap = await hydrateSnapshot(fixtureClient());
|
||||
expect(snap.hosts[0]!.connectedAssistants).toEqual([]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.14",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"release": "0.7.12",
|
||||
"archiveSha256": "e90b2487fb744d7ca69e52e13b2aaef2a204fcfa76db00c373248acc0795eb52",
|
||||
"release": "0.7.13",
|
||||
"archiveSha256": "534e9d5d626368e585d1ba19df20df7d97577153aabef61b9fca68a05edc86e8",
|
||||
"plugins": {
|
||||
"openrig": {
|
||||
"version": "0.1.0",
|
||||
"sha256": "ee8a1300119576f36889cc6494988c69faa3dfb2c3172cfd856a7ecc3edc0506"
|
||||
"version": "0.7.13",
|
||||
"sha256": "62a5c308ec64b77d678b4276fc81dce933a7ef610da93790bc6a30a3dc3be990"
|
||||
},
|
||||
"openrig-core": {
|
||||
"version": "0.1.28",
|
||||
"sha256": "59b2fb34be931a423583283696c22034fa417530ea682268a68156a70730fb5f"
|
||||
"version": "0.1.29",
|
||||
"sha256": "5e08deffe33feed6f7beff6df39d70402248632f87e3b63f074835b400fff54d"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user