@phnx-labs/agents-cli 1.22.23 → 1.22.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +193 -0
- package/README.md +5 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +5 -0
- package/dist/commands/exec.js +80 -28
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +300 -56
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +26 -11
- package/dist/commands/sessions.d.ts +10 -0
- package/dist/commands/sessions.js +34 -59
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +12 -0
- package/dist/lib/exec.js +88 -9
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +22 -17
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.js +34 -8
- package/dist/lib/secrets/reaper.d.ts +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +24 -5
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,198 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.24
|
|
4
|
+
|
|
5
|
+
- **`agents sessions focus` recovers dead panes and shares the sessions browser's selectors (GH-2108).** A retained tmux `remain-on-exit` pane is probed through `#{pane_dead}` immediately before attach, so dead or missing panes no longer open a `Pane is dead` screen. `focus` accepts session ids, topic/path searches, `agent@version` selectors (including per-device `latest`/`oldest`), device, project/time, team/routine, skill/plugin, favorites, and the complete live-state union. Focus, resume, attach, and `run --resume` now use one recovery decision on the origin device: a healthy exact origin performs native resume; otherwise balanced selection chooses a healthy version of the same harness and sends `/continue <id>` to read the indexed transcript, including transcripts retained under version trash. Host-dispatched rows persist the dispatch host as their origin, and `attach` routes its detach-record cleanup there before resuming. No usable same-harness version fails with the device, origin version, and account-health reason. Source: `apps/cli/src/commands/focus.ts`, `apps/cli/src/commands/sessions-browser.ts`, `apps/cli/src/lib/session/recovery.ts`.
|
|
6
|
+
|
|
7
|
+
- **`agents secrets setup` no longer tells you to set `AGENTS_SECRETS_PASSPHRASE`, and
|
|
8
|
+
`docs/secrets.md` stops recommending the shell-rc export it flags as a leak
|
|
9
|
+
(RUSH-1968).** The file-backend note read `set AGENTS_SECRETS_PASSPHRASE for headless
|
|
10
|
+
encrypted-file reads`, implying a requirement; headless reads have worked with no
|
|
11
|
+
passphrase since the store began auto-provisioning a 0600 machine-local key, so it now
|
|
12
|
+
says so and names the real path. The docs were worse than merely stale: they called an
|
|
13
|
+
rc export *"Recommended for shared/CI machines"* and the 0600 key file *"identical to"*
|
|
14
|
+
it, which is how a master key ended up in `~/.zshenv` on seven worker boxes. That
|
|
15
|
+
equivalence is inverted — the key file is read by one process, an rc export is inherited
|
|
16
|
+
by every child and readable from `/proc/<pid>/environ` — and the section also named a
|
|
17
|
+
pre-#479 key path (`~/.agents/.cache/secrets/.passphrase`, now
|
|
18
|
+
`~/.agents/.secrets-key/passphrase`) and a TTY prompt step `getPassphrase` no longer
|
|
19
|
+
has. A new `docs-hygiene.test.ts` pins those claims against the shipped doc so the
|
|
20
|
+
advice cannot drift back. Source: `apps/cli/docs/secrets.md`,
|
|
21
|
+
`apps/cli/src/commands/setup-secrets.ts`, `apps/cli/src/lib/secrets/filestore.ts`.
|
|
22
|
+
|
|
23
|
+
- **`agents secrets push`/`pull`, `agents sync --secrets`, and `agents secrets
|
|
24
|
+
export --to-file` / `import --from-file` read `AGENTS_SYNC_PASSPHRASE` now;
|
|
25
|
+
`AGENTS_SECRETS_PASSPHRASE` is the file store's master key and nothing else
|
|
26
|
+
(RUSH-1968).** One variable meant two different secrets: the local file store's master
|
|
27
|
+
key, and the passphrase that seals a bundle for transport. The store stopped needing a
|
|
28
|
+
passphrase once it auto-provisioned a machine-local key, but headless `push`/`pull`
|
|
29
|
+
still hard-failed without one — so the only way to get unattended sync on a worker box
|
|
30
|
+
was to export the **master key** fleet-wide, handing every same-user process the key to
|
|
31
|
+
the whole store. Splitting them means a box that only needs headless sync sets
|
|
32
|
+
`AGENTS_SYNC_PASSPHRASE` and never has the master key in its environment. The old name
|
|
33
|
+
still works for sync as a deprecated fallback — warned exactly once per process, so a
|
|
34
|
+
`push --all` over many bundles does not flood stderr — so scripted CI and release
|
|
35
|
+
automation keep working across the upgrade. The headless error now names the new
|
|
36
|
+
variable (`A sync passphrase is required. Run from a TTY, or set
|
|
37
|
+
AGENTS_SYNC_PASSPHRASE.`), and `agents sync`'s skip line with it — it previously read
|
|
38
|
+
`no passphrase available`, naming nothing an operator could act on. Note the legacy
|
|
39
|
+
fallback only works where that value is also the store's master key, since the old name
|
|
40
|
+
still keys the store; that coupling is the thing being retired. Resolution moved to
|
|
41
|
+
one chokepoint so the once-per-process promise holds rather than being per-call-site. Also
|
|
42
|
+
corrects `SEC-29a`, which claimed the variable applied "exclusively" to the file and
|
|
43
|
+
age-vault backends: the age-vault backend never reads it (it is gated by `agents
|
|
44
|
+
login`), and sync plus the portable `--to-file` envelope were two more consumers — the
|
|
45
|
+
new `SEC-29b` states the split as a normative invariant. Source:
|
|
46
|
+
`apps/cli/src/lib/secrets/sync-passphrase.ts`, `apps/cli/src/commands/secrets-sync.ts`,
|
|
47
|
+
`apps/cli/src/commands/sync.ts`, `apps/cli/src/lib/sync-umbrella.ts`.
|
|
48
|
+
|
|
49
|
+
- Added `--team` as an alias for `agents sessions --teams`. (RUSH-1995)
|
|
50
|
+
- Added absolute `agents secrets unlock --until <date>` expiry, mutually exclusive with relative `--ttl`. (RUSH-1960)
|
|
51
|
+
- Added persisted per-agent-version reasoning effort defaults and surfaced them in `agents view`. (RUSH-2005)
|
|
52
|
+
|
|
53
|
+
- **`agents feed` no longer crashes when the session index is locked (RUSH-2006).**
|
|
54
|
+
Outcome enrichment calls `discoverSessions` against `sessions.db`; under concurrent
|
|
55
|
+
scanner/daemon pressure that open can throw `SQLITE_BUSY` / "database is locked"
|
|
56
|
+
and take down the whole feed. A lock error now degrades to an empty meta set with a
|
|
57
|
+
stderr warning so blocks still render. Source: `apps/cli/src/commands/feed.ts`.
|
|
58
|
+
|
|
59
|
+
- **`agents events --event` / filtered activity reads no longer miss matches under
|
|
60
|
+
`--limit` (RUSH-2093).** The unified reader capped activity records *before*
|
|
61
|
+
applying the event-type filter, so a rare match older than the newest-N routine
|
|
62
|
+
rows (e.g. one `pr.opened` under twenty `file.edited`) was silently dropped. Event
|
|
63
|
+
types are now passed into `readRecentActivity` so the cap counts matching rows; a
|
|
64
|
+
non-`activity` `--module` skips the activity half entirely. Source:
|
|
65
|
+
`apps/cli/src/lib/event-stream.ts`.
|
|
66
|
+
|
|
67
|
+
- **`agents mcp add` no longer strips comments from `agents.yaml`.** The
|
|
68
|
+
write path (`writeManifest` / `serializeManifest` in `src/lib/manifest.ts`)
|
|
69
|
+
used plain `yaml.stringify`, which dropped every hand-written comment on
|
|
70
|
+
every add. It now round-trips via `yaml.parseDocument` and edits only the
|
|
71
|
+
keys that changed — the same approach `serializeCentral` already uses for
|
|
72
|
+
the central meta file (RUSH-2090).
|
|
73
|
+
|
|
74
|
+
- **Hook cache refresh is now single-flight, backs off on failure, and logs its
|
|
75
|
+
real exit code (RUSH-2121).** The background refresh in the hook cache shim
|
|
76
|
+
acquires an atomic `mkdir` lock, so concurrent hook invocations no longer
|
|
77
|
+
stampede into N parallel refreshes; a persistently-failing refresh records a
|
|
78
|
+
failure timestamp and is skipped until a 60s backoff elapses instead of
|
|
79
|
+
re-firing on every invocation; and the refresh now emits a `hook.cache.refresh`
|
|
80
|
+
event carrying the subshell's real exit code instead of a hardcoded `0`.
|
|
81
|
+
Source: `apps/cli/src/lib/hooks/cache.ts`.
|
|
82
|
+
|
|
83
|
+
- **`agents sync --host all` no longer fails every peer with `unknown option
|
|
84
|
+
'--json'`.** Fleet fan-out injects `--json` on each remote so the roster can
|
|
85
|
+
parse per-device results, but `sync` never registered the flag. Register
|
|
86
|
+
`--json` on `agents sync` and emit a machine-readable umbrella/agent/repo
|
|
87
|
+
payload so peers accept the flag and return parseable stdout (RUSH-2216).
|
|
88
|
+
Source: `apps/cli/src/commands/sync.ts`.
|
|
89
|
+
|
|
90
|
+
- **A wedged keychain can no longer pile up `agents` processes (RUSH-2231, RUSH-2232).**
|
|
91
|
+
A stalled macOS `coreauthd` used to hang the signed keychain helper's XPC receive
|
|
92
|
+
forever, so every secrets-touching `agents` command blocked and dozens of helper
|
|
93
|
+
processes plus their `<defunct>` zombies accumulated and made the machine sluggish.
|
|
94
|
+
Two fixes: **(Layer 1)** every keychain-helper `spawnSync` is now bounded and
|
|
95
|
+
hard-killed (SIGKILL) on timeout — 8s for never-prompt verbs (`has`/`set`/`delete`/
|
|
96
|
+
`list*`), 60s for the may-prompt reads (`get`/`get-batch`/`migrate-*`) — surfacing a
|
|
97
|
+
typed timeout error and arming the read back-off instead of hanging.
|
|
98
|
+
**(Layer 3)** the daemon reaps the backlog: a 5-minute tick kills orphaned helpers
|
|
99
|
+
(reparented to PID 1, past a 30s grace) and, two-sweep-debounced, the helper child of
|
|
100
|
+
an `agents` proc stuck past 90s (child first, escalating to the parent only if it
|
|
101
|
+
stays wedged) — never touching a process whose start-time can't be captured or whose
|
|
102
|
+
path doesn't match the helper. Any keychain touch now also opportunistically starts
|
|
103
|
+
the daemon so the reaper runs even on a secrets-only box. Source:
|
|
104
|
+
`apps/cli/src/lib/secrets/index.ts`, `apps/cli/src/lib/secrets/reaper.ts`,
|
|
105
|
+
`apps/cli/src/lib/daemon.ts`.
|
|
106
|
+
|
|
107
|
+
- **`agents inspect` and `agents doctor` now report harness-scoped hook inventory as capable, on-disk, wired, and unmanaged state.** Their JSON output carries the same state sets from `getResourceInventory`, so a harness with hook files or native wiring no longer collapses to a misleading `Hooks (0)` summary. Inventory is keyed by agent harness and installed version, not by configured model. Source: `apps/cli/src/lib/resource-inventory.ts`, `apps/cli/src/commands/inspect.ts`, `apps/cli/src/lib/doctor-diff.ts`.
|
|
108
|
+
|
|
109
|
+
- **Fix: the keychain-helper reaper never reaped on macOS.** The daemon reaper shelled `ps -o etimes` — a GNU/Linux procps keyword that macOS `ps` rejects with a non-zero exit — so `execFileSync` threw on every tick and `reapOrphanedKeychainProcesses` returned `reaped: 0` on its only supported platform, leaving orphaned/wedged `Agents CLI` helper processes to pile up. Switched to the portable BSD `etime` keyword (`[[dd-]hh:]mm:ss`) with a parser to seconds. This also un-quarantines the darwin integration test that #2153 had to `it.skip` to unblock releases: that test's symlink fixture was correct all along — it read `reaped: 0` only because the reaper's own `ps etimes` call threw before parsing anything. Verified on macOS 15.4.1: 20/20 reaper tests pass, the integration test reaping a real orphaned sleeper. Source: `apps/cli/src/lib/secrets/reaper.ts`, `reaper.test.ts`.
|
|
110
|
+
|
|
111
|
+
- **`agents modes [agent[@version]]` lists the permission modes a harness accepts.** The modes analog of `agents models`: for Claude / Codex / Cursor / …, shows which `--mode plan|edit|auto|skip` values work, the native CLI flags, the native default (`*`), any configured `run.defaults` mode, and degrade notes (e.g. plan→edit on Cursor). `agents inspect` also prints the mode list next to capabilities, and `agents run --help` / `agents models` point at the new command. Source: `apps/cli/src/commands/modes.ts`, `apps/cli/src/lib/agent-modes.ts`.
|
|
112
|
+
|
|
113
|
+
- **`agents insights --all` now works as an alias for `--since all`.** It previously
|
|
114
|
+
exited with `unknown option '--all'` — a hard failure in the middle of a report the
|
|
115
|
+
user asked for, on the spelling most people reach for first. An explicit `--since`
|
|
116
|
+
still wins, so `--all --since 7d` resolves to 7d rather than silently contradicting
|
|
117
|
+
itself. Source: `apps/cli/src/commands/insights.ts`.
|
|
118
|
+
|
|
119
|
+
- **Coexisting agents-cli installs no longer fight over the menu-bar helper.** The
|
|
120
|
+
helper lives at one path in Application Support, but every install on the box runs
|
|
121
|
+
the startup self-heal, and both the version stamp and the plist's baked
|
|
122
|
+
`AGENTS_ENTRY` record whichever copy acted last — so each copy read the others'
|
|
123
|
+
marks as drift and recopied the app bundle over them. Recopying replaces the
|
|
124
|
+
executable under the running helper and kills it, launchd `KeepAlive` restarts it,
|
|
125
|
+
and the next copy repeats it: a new pid every 5-15 seconds, 578 launches in one
|
|
126
|
+
observed helper log, and a status item that never stayed visible while
|
|
127
|
+
`agents menubar status` still reported `running: yes` (a pid always existed). The
|
|
128
|
+
plist's `AGENTS_ENTRY` is now treated as the owner and only the owner reinstalls
|
|
129
|
+
freely; a same-install upgrade keeps its entry path, so `npm update` still installs
|
|
130
|
+
the new helper normally. Another install still gets there — immediately if the
|
|
131
|
+
recorded owner is gone from disk, otherwise at most once an hour — so a stale copy
|
|
132
|
+
that merely still sits on disk can't freeze the menu bar for whichever install the
|
|
133
|
+
user actually upgrades. Repairs (a missing helper executable, a Developer-ID heal)
|
|
134
|
+
are never gated, and `agents menubar setup` bypasses the gate as the immediate
|
|
135
|
+
manual fix. An ad-hoc/dev-signed build never wins the timed takeover — recopying
|
|
136
|
+
an un-notarized bundle over a good one gets it rejected as "damaged" — though it
|
|
137
|
+
can still adopt a helper whose owner is gone. Two installs that are both invoked
|
|
138
|
+
regularly still trade ownership at the cooldown, so the helper restarts about once
|
|
139
|
+
an hour until one is removed; that is bounded rather than converged, and the real
|
|
140
|
+
fix remains a single install. `agents menubar status` no longer promises that a
|
|
141
|
+
stale helper "runs on next `agents` startup", which is not guaranteed on a
|
|
142
|
+
multi-install box. Fixes #2109. Source:
|
|
143
|
+
`apps/cli/src/lib/menubar/install-menubar.ts`.
|
|
144
|
+
|
|
145
|
+
- **Muse Code global binary is visible to `agents view` / `isVersionInstalled`.**
|
|
146
|
+
Muse installs a single self-updating launcher at `~/.local/bin/muse` (same
|
|
147
|
+
shape as droid), but `getBinaryPath` still resolved a version-home
|
|
148
|
+
`node_modules/.bin/muse` that never exists. After `agents import muse` /
|
|
149
|
+
`agents add muse`, version dirs now resolve to that global path so managed
|
|
150
|
+
view, collapse, and live-version bookkeeping match what actually executes;
|
|
151
|
+
install no longer writes a self-referential shim symlink. Source:
|
|
152
|
+
`apps/cli/src/lib/versions.ts`.
|
|
153
|
+
|
|
154
|
+
- **Muse multi-version isolation matches Claude/Codex (XDG pin, not bare symlink).**
|
|
155
|
+
Claude/Codex isolate per version with `CLAUDE_CONFIG_DIR` / `CODEX_HOME`.
|
|
156
|
+
Muse has no dedicated config env; it reads `$XDG_CONFIG_HOME/muse` and
|
|
157
|
+
`$XDG_DATA_HOME/muse`. After `agents import muse`, `~/.config/muse` is a
|
|
158
|
+
symlink into the version home — Muse refuses that path with
|
|
159
|
+
`Agent Definition filesystem source failed: SymlinkOrReparse` and exits 1
|
|
160
|
+
(the Zion `agents run muse@0.1.0` failure). Managed launches now pin
|
|
161
|
+
`XDG_CONFIG_HOME` + `XDG_DATA_HOME` into the version home from `buildExecEnv`,
|
|
162
|
+
the main shim, and versioned aliases (`muse@0.1.0`), the same way Claude pins
|
|
163
|
+
`CLAUDE_CONFIG_DIR`. Muse is also listed in `CONFIG_ENV_ISOLATED_AGENTS` so
|
|
164
|
+
`--isolated` installs are honest. Source: `apps/cli/src/lib/{exec,shims}.ts`.
|
|
165
|
+
|
|
166
|
+
- **Scheduled routines now run on a per-account `claude setup-token` when one is
|
|
167
|
+
provisioned, instead of throwing it away.** `buildRoutineSpawnEnv` unconditionally
|
|
168
|
+
deleted `CLAUDE_CODE_OAUTH_TOKEN`, so even after `buildExecEnv` injected a long-lived,
|
|
169
|
+
non-rotating setup-token from the reserved file-backed `auth` bundle
|
|
170
|
+
(`resolveClaudeSetupToken`), a routine still fell back to the version home's rotating
|
|
171
|
+
`.credentials.json` login. With one Claude account signed into several version homes
|
|
172
|
+
(or several fleet boxes), that rotating login is the single-use-refresh-token
|
|
173
|
+
revocation storm — one home's refresh silently revokes every sibling copy, so an
|
|
174
|
+
unattended routine keeps landing on a just-revoked token and dies with `auth_failed:
|
|
175
|
+
OAuth access token has been revoked` / `Please run /login`, even though `agents view`
|
|
176
|
+
shows the account healthy. The delete now distinguishes the two flavours: a
|
|
177
|
+
per-account setup-token (keyed to this home's own account) is re-asserted and KEPT; an
|
|
178
|
+
inherited *ambient* token (a shared value the daemon env happened to carry — the
|
|
179
|
+
RUSH-1822 fleet-logout path) is still stripped so a routine never runs on it.
|
|
180
|
+
Provision the token with `/fleet:mint-auth`. Source:
|
|
181
|
+
`apps/cli/src/lib/runner.ts` (`buildRoutineSpawnEnv`).
|
|
182
|
+
|
|
183
|
+
- **Resolved secrets no longer appear in `ps` for tmux-launched agents
|
|
184
|
+
(RUSH-2100).** An interactive `agents run` wraps the agent in tmux via
|
|
185
|
+
`exec env K=V … <agent>`, which put the entire exec env — every resolved
|
|
186
|
+
secrets-bundle value — into the pane's command line, readable by any process of
|
|
187
|
+
the same user. On one fleet box six live processes carried
|
|
188
|
+
`AGENTS_SECRETS_PASSPHRASE`, the key that decrypts every file-backed bundle on
|
|
189
|
+
that machine. The pane now sources a `0600` env file and unlinks it before
|
|
190
|
+
`exec`, so only the file path is argv-visible; a missing file aborts the pane
|
|
191
|
+
rather than launching half-configured. Every key routes through the file, not a
|
|
192
|
+
curated "secret-bearing" subset, so a newly added credential is covered without
|
|
193
|
+
anyone maintaining a list. Source: `apps/cli/src/lib/exec.ts`,
|
|
194
|
+
`apps/cli/docs/specifications.md` (SEC-8a).
|
|
195
|
+
|
|
3
196
|
## 1.22.23
|
|
4
197
|
|
|
5
198
|
- **Daemon-warmed cross-surface session-status cache (RUSH-2062).** Menubar,
|
package/README.md
CHANGED
|
@@ -303,6 +303,7 @@ agents sessions --idle # stopped between turns (fleet-wide)
|
|
|
303
303
|
agents sessions --orphan # agent outlived its terminal client
|
|
304
304
|
agents sessions --crashed # terminal and agent disappeared uncleanly
|
|
305
305
|
agents sessions focus a1b2c3d4 # jump back into one — attach in place, or resume
|
|
306
|
+
agents sessions focus claude@latest --device yosemite-s0 # pick latest there
|
|
306
307
|
```
|
|
307
308
|
|
|
308
309
|
On a terminal, `agents sessions --active` (and a bare `agents sessions`) open the **interactive session browser** — one filter you drive with single keys, re-pulled live across the fleet:
|
|
@@ -313,7 +314,7 @@ On a terminal, `agents sessions --active` (and a bare `agents sessions`) open th
|
|
|
313
314
|
| `r` | running only | `--active` |
|
|
314
315
|
| `f` | favorites only | `--favorites` |
|
|
315
316
|
| `*` | star / unstar the highlighted session | `agents sessions favorite <id>` |
|
|
316
|
-
| `c` | team sessions | `--teams` |
|
|
317
|
+
| `c` | team sessions | `--team` (alias: `--teams`) |
|
|
317
318
|
| `a` | agent (cycles) | `-a` |
|
|
318
319
|
| `d` | device (cycles) | `--device` |
|
|
319
320
|
| `p` | this repo ↔ all dirs | `--all` |
|
|
@@ -332,7 +333,7 @@ Filters **stack** (they AND together), the active set shows in the header, and t
|
|
|
332
333
|
| --- | --- |
|
|
333
334
|
|  |  |
|
|
334
335
|
|
|
335
|
-
Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), `idle`, or a lifecycle state such as `orphaned`, `crashed`, `closed`, `abandoned`, `queued`, or `unknown`. Pass the matching flag (`--working`, `--idle`, `--waiting`, `--orphan`, `--crashed`, `--closed`, `--abandoned`, `--queued`, `--unknown`) directly; each implies `--active`, and several flags form a union. The fleet fan-out is already the default; `--local` opts out. `--all` instead widens historical directory and time scope. Rows also carry badges for the PR, worktree, and ticket. `agents sessions focus [
|
|
336
|
+
Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), `idle`, or a lifecycle state such as `orphaned`, `crashed`, `closed`, `abandoned`, `queued`, or `unknown`. Pass the matching flag (`--working`, `--idle`, `--waiting`, `--orphan`, `--crashed`, `--closed`, `--abandoned`, `--queued`, `--unknown`) directly; each implies `--active`, and several flags form a union. The fleet fan-out is already the default; `--local` opts out. `--all` instead widens historical directory and time scope. Rows also carry badges for the PR, worktree, and ticket. `agents sessions focus [selector]` accepts the same agent/version, device, time, team, project, skill/plugin, favorite, and live-state filters as the session browser. A unique id focuses directly; an agent/version or text selector always opens the preview picker. Immediately before attach it checks the tmux pane process: a living pane is joined in place, while a dead/missing pane enters recovery instead of showing tmux's `Pane is dead` screen.
|
|
336
337
|
|
|
337
338
|
Landing on a session cold? `agents sessions <id>` prints a catch-up digest: an inferred title, files changed grouped by directory (created / modified / deleted), a histogram of which tools did the work (including parsed Bash commands -- `git`, `npm`, `ffmpeg`, `ssh`, and so on), and the last test verdict -- the signals to reload a task in seconds.
|
|
338
339
|
|
|
@@ -351,11 +352,11 @@ agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # original harness/version/d
|
|
|
351
352
|
agents run auto --resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # adapt if its account is unavailable
|
|
352
353
|
```
|
|
353
354
|
|
|
354
|
-
`agents sessions resume` reopens several sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. `agents resume <id>` resumes one session without requiring you to name its harness: exact IDs take a local SQLite fast path, then resolve fleet-wide and
|
|
355
|
+
`agents sessions resume` reopens several sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. `agents resume <id>` resumes one session without requiring you to name its harness: exact IDs take a local SQLite fast path, then resolve fleet-wide and recover on the source device. If the origin version is installed, signed in, and healthy, its isolated home performs native resume. Otherwise a healthy version of the **same harness** starts with `/continue <id>`, which reads the indexed transcript even when the old version home is retained under version trash. It never native-resumes from a different isolated home. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
|
|
355
356
|
|
|
356
357
|
### Send an agent to the background — and bring it back
|
|
357
358
|
|
|
358
|
-
Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions attach <id>` brings it back
|
|
359
|
+
Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions attach <id>` brings it back through the same origin-device recovery decision: native resume in the exact healthy origin home, or same-harness `/continue` when that home is unavailable, with the full indexed history (including whatever it did while backgrounded).
|
|
359
360
|
|
|
360
361
|
```
|
|
361
362
|
agents sessions detach a1b2c3d4 # go headless in the background, keep working
|
package/dist/bin/agents
ADDED
|
Binary file
|
|
@@ -6,5 +6,7 @@
|
|
|
6
6
|
* history, the exact inverse of `agents sessions detach`.
|
|
7
7
|
*/
|
|
8
8
|
import type { Command } from 'commander';
|
|
9
|
+
import type { SessionMeta } from '../lib/session/types.js';
|
|
9
10
|
export declare function registerAttachCommand(program: Command): void;
|
|
10
11
|
export declare function attachAction(id: string): Promise<void>;
|
|
12
|
+
export declare function attachRecoveryArgs(session: Pick<SessionMeta, 'id'>): string[];
|
package/dist/commands/attach.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { runOnPeer } from '../lib/session/remote-list.js';
|
|
3
|
+
import { sessionRecoveryPeer } from '../lib/session/recovery.js';
|
|
4
|
+
import { resolveSessionMetadataValue, resumeSessionInPlace } from './sessions.js';
|
|
4
5
|
import { readDetachRecord, clearDetachRecord, isHeadlessAlive } from '../lib/session/detached.js';
|
|
5
6
|
export function registerAttachCommand(program) {
|
|
6
7
|
program
|
|
@@ -12,16 +13,37 @@ export function registerAttachCommand(program) {
|
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
15
|
export async function attachAction(id) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const outcome = await resolveSessionMetadataValue(id);
|
|
17
|
+
if (outcome.kind === 'partial') {
|
|
18
|
+
console.error(chalk.red(`Could not resolve session while these devices were unavailable: ${outcome.failedPeers.join(', ')}`));
|
|
19
|
+
process.exitCode = 2;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (outcome.kind === 'not-found') {
|
|
20
23
|
console.error(chalk.red(`No session matching "${id}".`));
|
|
21
24
|
console.error(chalk.gray(' See your sessions: agents sessions'));
|
|
22
25
|
process.exitCode = 1;
|
|
23
26
|
return;
|
|
24
27
|
}
|
|
28
|
+
if (outcome.kind === 'ambiguous') {
|
|
29
|
+
console.error(chalk.red(`"${id}" matches ${outcome.candidates.length} sessions. Pass the full session id.`));
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const meta = outcome.session;
|
|
34
|
+
// Route the WHOLE attach operation to the origin, not just its eventual
|
|
35
|
+
// resume. The detach record and headless PID are device-local; clearing them
|
|
36
|
+
// here would leave the real background continuation running beside a second
|
|
37
|
+
// process on the owning machine.
|
|
38
|
+
const peer = sessionRecoveryPeer(meta);
|
|
39
|
+
if (peer) {
|
|
40
|
+
const routed = await runOnPeer(attachRecoveryArgs(meta), peer, { tty: true });
|
|
41
|
+
if (routed === 'no-target') {
|
|
42
|
+
console.error(chalk.red(`Cannot attach ${meta.shortId}: origin device ${peer} is not a registered reachable peer.`));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
25
47
|
// If it's backgrounded, stop the headless continuation first so two processes
|
|
26
48
|
// don't resume the same transcript at once.
|
|
27
49
|
const rec = readDetachRecord(meta.id);
|
|
@@ -39,3 +61,6 @@ export async function attachAction(id) {
|
|
|
39
61
|
console.log(chalk.gray(`Attaching ${meta.agent} ${meta.id.slice(0, 8)} — resuming interactively…`));
|
|
40
62
|
await resumeSessionInPlace(meta);
|
|
41
63
|
}
|
|
64
|
+
export function attachRecoveryArgs(session) {
|
|
65
|
+
return ['sessions', 'attach', session.id];
|
|
66
|
+
}
|
|
@@ -47,11 +47,13 @@ export function registerDefaultsCommands(program) {
|
|
|
47
47
|
.description('Set defaults for an agent/version selector')
|
|
48
48
|
.option('--mode <mode>', "Default mode: plan, edit, auto, skip. 'full' accepted as alias for skip.")
|
|
49
49
|
.option('--model <model>', 'Default model or model alias, forwarded via --model')
|
|
50
|
+
.option('--effort <effort>', 'Default reasoning effort: low, medium, high, xhigh, max, or auto')
|
|
50
51
|
.action((selector, options) => {
|
|
51
52
|
try {
|
|
52
53
|
const entry = setRunDefault(selector, {
|
|
53
54
|
...(options.mode !== undefined ? { mode: options.mode } : {}),
|
|
54
55
|
...(options.model !== undefined ? { model: options.model } : {}),
|
|
56
|
+
...(options.effort !== undefined ? { effort: options.effort } : {}),
|
|
55
57
|
});
|
|
56
58
|
console.log(chalk.green('Set run default:'));
|
|
57
59
|
console.log(` ${formatRunDefaultEntry(entry)}`);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -848,6 +848,11 @@ function renderTargetText(report, options) {
|
|
|
848
848
|
// sees what was checked. options.requestedKinds drives this.
|
|
849
849
|
if (options.requestedKinds && !options.requestedKinds.has(kind))
|
|
850
850
|
continue;
|
|
851
|
+
if (kind === 'hooks' && report.hookInventory) {
|
|
852
|
+
const wired = report.hookInventory.wiringSupported ? String(report.hookInventory.wired.length) : 'unknown';
|
|
853
|
+
const unmanaged = report.hookInventory.unmanaged.length > 0 ? ` · unmanaged ${report.hookInventory.unmanaged.length}` : '';
|
|
854
|
+
console.log(chalk.gray(` inventory: capable ${report.hookInventory.capable ? 'yes' : 'no'} · on-disk ${report.hookInventory.onDisk.length} · wired ${wired}${unmanaged}`));
|
|
855
|
+
}
|
|
851
856
|
renderKindSection(kind, rows, report.layers, options);
|
|
852
857
|
// A hook file can reconcile "ok" above yet be absent from settings.json — a
|
|
853
858
|
// present-but-dead hook. Surface that right under the hooks section.
|
package/dist/commands/exec.js
CHANGED
|
@@ -532,7 +532,7 @@ export function registerRunCommand(program) {
|
|
|
532
532
|
.option('--headless', 'Force headless mode. Auto-enabled when a prompt is provided; pass explicitly to stay headless with no prompt (reads the prompt from stdin).', false)
|
|
533
533
|
.option('--no-auth-check', 'Skip the pre-launch "looks logged out" warning on an interactive run (advisory; never blocks anyway). Also silenced by AGENTS_NO_AUTH_CHECK=1.')
|
|
534
534
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
|
|
535
|
-
.option('--resume [id]', '
|
|
535
|
+
.option('--resume [id]', 'Recover a previous conversation on its origin device. The exact healthy origin uses native resume; otherwise a healthy version of the same harness replays via /continue. Pair with a prompt to continue headlessly.')
|
|
536
536
|
.option('--session-id <id>', 'Force a NEW conversation to use this exact session UUID (Claude only). This CREATES a session — to resume an existing one, use --resume.')
|
|
537
537
|
.option('--name <slug>', 'Name the run — seeds the session label so it shows up as `<name>` in `agents sessions` and resolves by it (and `agents hosts logs <name>` for --host runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
|
|
538
538
|
.option('--notify', 'Post a desktop notification when a headless run finishes. Fired by this process on exit, so it survives whatever launched the run (the menu bar dispatching it, a terminal you closed).')
|
|
@@ -647,12 +647,14 @@ export function registerRunCommand(program) {
|
|
|
647
647
|
agents run claude "fix the bug" -- --custom-flag
|
|
648
648
|
`,
|
|
649
649
|
notes: `
|
|
650
|
-
Modes (not every agent supports every mode —
|
|
650
|
+
Modes (not every agent supports every mode — run \`agents modes <agent>\`):
|
|
651
651
|
plan read-only investigation; no writes, no shell side-effects
|
|
652
652
|
edit may edit files; prompts for shell / risky operations
|
|
653
653
|
auto smart classifier auto-approves safe ops, prompts for risky (claude, copilot)
|
|
654
654
|
skip bypass every permission prompt (dangerously-skip-permissions)
|
|
655
655
|
Legacy 'full' is silently rewritten to 'skip'.
|
|
656
|
+
List per-harness support + native flags: agents modes · agents modes claude
|
|
657
|
+
Models (cheap|default|best|ultra): agents models <agent[@version]>
|
|
656
658
|
|
|
657
659
|
Headless plan support (a prompt makes the run headless):
|
|
658
660
|
plan works headless on claude, codex, droid, opencode.
|
|
@@ -689,7 +691,7 @@ export function registerRunCommand(program) {
|
|
|
689
691
|
given. --cloud is mutually exclusive with --host/--lease and with
|
|
690
692
|
local-run flags (--loop, --resume, --secrets, --terminal, …).
|
|
691
693
|
|
|
692
|
-
Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and
|
|
694
|
+
Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and recovers on the source device with its cwd/mode. The exact healthy origin version uses native resume; otherwise a healthy version of the same harness replays via /continue. agents resume <id> infers the harness too.
|
|
693
695
|
|
|
694
696
|
Passthrough: everything after -- is forwarded verbatim to the underlying agent CLI.
|
|
695
697
|
agents run kimi -- --plan --some-native-flag value
|
|
@@ -835,6 +837,7 @@ export function registerRunCommand(program) {
|
|
|
835
837
|
}
|
|
836
838
|
let autoHarnessRequested = normalizedAgentSpec === RUN_AUTO_KEYWORD;
|
|
837
839
|
let resolvedResumeSource;
|
|
840
|
+
let resolvedRecoveryTarget;
|
|
838
841
|
// Concrete resume ids resolve BEFORE placement. Full UUIDs take the local
|
|
839
842
|
// SQLite fast path; only a local miss fans out to the fleet. This lets a
|
|
840
843
|
// command entered on zion discover that the owning version-home is on a
|
|
@@ -890,32 +893,40 @@ export function registerRunCommand(program) {
|
|
|
890
893
|
process.stderr.write(chalk.yellow(`[agents] session ${resolvedResumeSource.shortId} predates stored launch modes; using --mode ${options.mode}\n`));
|
|
891
894
|
}
|
|
892
895
|
const { machineId } = await import('../lib/machine-id.js');
|
|
896
|
+
const { sessionRecoveryDestinationMatches, sessionRecoveryPeer, } = await import('../lib/session/recovery.js');
|
|
893
897
|
const sourceMachine = resolvedResumeSource.machine;
|
|
898
|
+
const sourcePeer = sessionRecoveryPeer(resolvedResumeSource);
|
|
894
899
|
const explicitPlacement = hostTargetGiven(options).length > 0;
|
|
895
|
-
if (
|
|
896
|
-
options.host =
|
|
900
|
+
if (sourcePeer && !explicitPlacement) {
|
|
901
|
+
options.host = sourcePeer;
|
|
897
902
|
}
|
|
898
|
-
else if (
|
|
899
|
-
console.error(chalk.red(`
|
|
900
|
-
`
|
|
903
|
+
else if (sourcePeer && explicitPlacement && !hostTargetGiven(options).some((host) => sessionRecoveryDestinationMatches(resolvedResumeSource, host))) {
|
|
904
|
+
console.error(chalk.red(`Session ${resolvedResumeSource.shortId} must recover on ${sourcePeer}, where its indexed transcript and version history are owned; ` +
|
|
905
|
+
`the requested device was ${hostTargetGiven(options).join(', ')}.`));
|
|
901
906
|
process.exit(1);
|
|
902
907
|
}
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
process.stderr.write(chalk.gray(`[agents] auto resume → native ${normalizedAgentSpec} on ${sourceMachine ?? machineId()}\n`));
|
|
917
|
-
}
|
|
908
|
+
// Recovery is resolved on the device that owns the transcript. A remote
|
|
909
|
+
// dispatch is pinned to the source HARNESS (not `run auto`'s cross-harness
|
|
910
|
+
// picker); the peer repeats this block with the injected SessionMeta and
|
|
911
|
+
// chooses its own healthy version. Locally, resolve it now.
|
|
912
|
+
const sourceAgent = resolvedResumeSource.agent;
|
|
913
|
+
if (!sourcePeer) {
|
|
914
|
+
try {
|
|
915
|
+
const { resolveSessionRecovery } = await import('../lib/session/recovery.js');
|
|
916
|
+
resolvedRecoveryTarget = await resolveSessionRecovery(resolvedResumeSource);
|
|
917
|
+
}
|
|
918
|
+
catch (err) {
|
|
919
|
+
console.error(chalk.red(err.message));
|
|
920
|
+
process.exit(1);
|
|
918
921
|
}
|
|
922
|
+
normalizedAgentSpec = `${resolvedRecoveryTarget.agent}@${resolvedRecoveryTarget.version}`;
|
|
923
|
+
autoHarnessRequested = false;
|
|
924
|
+
if (!options.quiet)
|
|
925
|
+
process.stderr.write(chalk.gray(`[agents] session recovery → ${resolvedRecoveryTarget.mode} ${normalizedAgentSpec} on ${sourceMachine ?? machineId()} · ${resolvedRecoveryTarget.reason}\n`));
|
|
926
|
+
}
|
|
927
|
+
else if (autoHarnessRequested) {
|
|
928
|
+
normalizedAgentSpec = sourceAgent;
|
|
929
|
+
autoHarnessRequested = false;
|
|
919
930
|
}
|
|
920
931
|
}
|
|
921
932
|
if (autoHarnessRequested) {
|
|
@@ -2217,10 +2228,44 @@ export function registerRunCommand(program) {
|
|
|
2217
2228
|
session = picked.session;
|
|
2218
2229
|
forceInteractive = true; // bare resume always lands in the agent's TUI
|
|
2219
2230
|
}
|
|
2220
|
-
//
|
|
2221
|
-
//
|
|
2222
|
-
//
|
|
2223
|
-
|
|
2231
|
+
// Bare `run <harness> --resume` learns the chosen SessionMeta only after
|
|
2232
|
+
// the host-placement phase above. If the picker chose a synced session
|
|
2233
|
+
// from another device, route the recovery command now; resolving local
|
|
2234
|
+
// candidates would otherwise native-resume through this device's
|
|
2235
|
+
// unrelated isolated home.
|
|
2236
|
+
if (!resolvedResumeSource) {
|
|
2237
|
+
const { sessionRecoveryPeer, sessionRecoveryRunArgs, } = await import('../lib/session/recovery.js');
|
|
2238
|
+
const peer = sessionRecoveryPeer(session);
|
|
2239
|
+
if (peer) {
|
|
2240
|
+
const { runOnPeer } = await import('../lib/session/remote-list.js');
|
|
2241
|
+
const routed = await runOnPeer(sessionRecoveryRunArgs(session), peer, { tty: true });
|
|
2242
|
+
if (routed === 'no-target') {
|
|
2243
|
+
console.error(chalk.red(`Cannot recover session ${session.shortId}: origin device ${peer} is not a registered reachable peer.`));
|
|
2244
|
+
process.exitCode = 1;
|
|
2245
|
+
}
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
// Bare interactive --resume selects the SessionMeta only down here, so
|
|
2250
|
+
// it has not gone through the early concrete-id resolver. Resolve it now;
|
|
2251
|
+
// concrete ids reuse the exact same target chosen above.
|
|
2252
|
+
if (!resolvedRecoveryTarget) {
|
|
2253
|
+
try {
|
|
2254
|
+
const { resolveSessionRecovery } = await import('../lib/session/recovery.js');
|
|
2255
|
+
resolvedRecoveryTarget = await resolveSessionRecovery(session);
|
|
2256
|
+
}
|
|
2257
|
+
catch (err) {
|
|
2258
|
+
console.error(chalk.red(err.message));
|
|
2259
|
+
process.exit(1);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
if (resolvedRecoveryTarget.agent !== agent) {
|
|
2263
|
+
console.error(chalk.red(`Session ${session.shortId} belongs to ${resolvedRecoveryTarget.agent}, not ${agent}. ` +
|
|
2264
|
+
`Use: agents run auto --resume ${session.id}`));
|
|
2265
|
+
process.exit(1);
|
|
2266
|
+
}
|
|
2267
|
+
version = resolvedRecoveryTarget.version;
|
|
2268
|
+
const canResumeNatively = resolvedRecoveryTarget.mode === 'native';
|
|
2224
2269
|
if (canResumeNatively) {
|
|
2225
2270
|
version = session.version;
|
|
2226
2271
|
resumeNative = true;
|
|
@@ -2330,6 +2375,12 @@ export function registerRunCommand(program) {
|
|
|
2330
2375
|
console.error(chalk.red(`agents: ${agent}@${launchTarget} is not runnable and could not be repaired. Try: ${hint}`));
|
|
2331
2376
|
process.exit(1);
|
|
2332
2377
|
}
|
|
2378
|
+
if (resolvedRecoveryTarget && healed !== launchTarget) {
|
|
2379
|
+
console.error(chalk.red(`agents: session recovery target ${agent}@${launchTarget} became unavailable on ` +
|
|
2380
|
+
`${resolvedResumeSource?.machine ?? 'this device'}; refusing to resume through another version home. ` +
|
|
2381
|
+
`Retry the command so recovery can select a healthy ${agent} version.`));
|
|
2382
|
+
process.exit(1);
|
|
2383
|
+
}
|
|
2333
2384
|
// Always adopt the healed version explicitly. In the version-undefined
|
|
2334
2385
|
// path a fallback re-pins the GLOBAL default, but `resolveVersion`
|
|
2335
2386
|
// prefers a PROJECT pin — so leaving `version` undefined would let the
|
|
@@ -2447,7 +2498,8 @@ export function registerRunCommand(program) {
|
|
|
2447
2498
|
console.error(chalk.gray(`Pass --mode plan explicitly if you really want a read-only run.`));
|
|
2448
2499
|
process.exit(1);
|
|
2449
2500
|
}
|
|
2450
|
-
const
|
|
2501
|
+
const effortSource = runCmd.getOptionValueSource('effort');
|
|
2502
|
+
const effort = (effortSource === 'default' && runDefaults.effort ? runDefaults.effort : options.effort);
|
|
2451
2503
|
if (!['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(effort)) {
|
|
2452
2504
|
console.error(chalk.red(`Invalid effort: ${effort}. Use 'low', 'medium', 'high', 'xhigh', 'max', or 'auto'`));
|
|
2453
2505
|
process.exit(1);
|
package/dist/commands/feed.d.ts
CHANGED
|
@@ -60,6 +60,24 @@ export declare function sessionHintsFromActive(sessions: Array<{
|
|
|
60
60
|
slug?: string;
|
|
61
61
|
};
|
|
62
62
|
}>): SessionOutcomeHint[];
|
|
63
|
+
/**
|
|
64
|
+
* True when a SQLite open/query failed because another writer holds the lock.
|
|
65
|
+
* better-sqlite3 surfaces this as message "database is locked" and/or code
|
|
66
|
+
* SQLITE_BUSY (RUSH-2006).
|
|
67
|
+
*/
|
|
68
|
+
export declare function isSqliteBusyError(err: unknown): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Load session metas for feed outcome enrichment. The sessions index can be
|
|
71
|
+
* locked by a concurrent scanner/daemon — enrichment is best-effort, so a lock
|
|
72
|
+
* degrades to an empty list instead of crashing `agents feed` (RUSH-2006).
|
|
73
|
+
*
|
|
74
|
+
* `load` is injected so tests can throw a real lock-shaped error without
|
|
75
|
+
* mocking the session module.
|
|
76
|
+
*/
|
|
77
|
+
export declare function loadSessionMetasForFeedEnrichment<T>(load: () => Promise<T[]>): Promise<{
|
|
78
|
+
metas: T[];
|
|
79
|
+
skippedLock: boolean;
|
|
80
|
+
}>;
|
|
63
81
|
export declare function registerFeedCommand(program: Command): void;
|
|
64
82
|
/** Feed view selector (RUSH-2015): decisions, progress, or both. */
|
|
65
83
|
export type FeedFilter = 'needs' | 'updates' | 'all';
|
package/dist/commands/feed.js
CHANGED
|
@@ -253,6 +253,39 @@ export function sessionHintsFromActive(sessions) {
|
|
|
253
253
|
project: s.cwd ? projectKeyFromCwd(s.cwd) : undefined,
|
|
254
254
|
}));
|
|
255
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* True when a SQLite open/query failed because another writer holds the lock.
|
|
258
|
+
* better-sqlite3 surfaces this as message "database is locked" and/or code
|
|
259
|
+
* SQLITE_BUSY (RUSH-2006).
|
|
260
|
+
*/
|
|
261
|
+
export function isSqliteBusyError(err) {
|
|
262
|
+
if (err == null)
|
|
263
|
+
return false;
|
|
264
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
265
|
+
const code = typeof err === 'object' && err !== null && 'code' in err
|
|
266
|
+
? String(err.code ?? '')
|
|
267
|
+
: '';
|
|
268
|
+
return /SQLITE_BUSY|database is locked/i.test(msg) || /SQLITE_BUSY/i.test(code);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Load session metas for feed outcome enrichment. The sessions index can be
|
|
272
|
+
* locked by a concurrent scanner/daemon — enrichment is best-effort, so a lock
|
|
273
|
+
* degrades to an empty list instead of crashing `agents feed` (RUSH-2006).
|
|
274
|
+
*
|
|
275
|
+
* `load` is injected so tests can throw a real lock-shaped error without
|
|
276
|
+
* mocking the session module.
|
|
277
|
+
*/
|
|
278
|
+
export async function loadSessionMetasForFeedEnrichment(load) {
|
|
279
|
+
try {
|
|
280
|
+
return { metas: await load(), skippedLock: false };
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (isSqliteBusyError(err)) {
|
|
284
|
+
return { metas: [], skippedLock: true };
|
|
285
|
+
}
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
256
289
|
export function registerFeedCommand(program) {
|
|
257
290
|
const feed = program
|
|
258
291
|
.command('feed')
|
|
@@ -446,7 +479,17 @@ export function registerFeedCommand(program) {
|
|
|
446
479
|
if (includeLocal) {
|
|
447
480
|
sessions = await getActiveSessions();
|
|
448
481
|
}
|
|
449
|
-
|
|
482
|
+
// discoverSessions touches sessions.db; under concurrent scan pressure it
|
|
483
|
+
// can throw SQLITE_BUSY. Outcome enrichment is best-effort — degrade with
|
|
484
|
+
// a warning rather than crash the whole feed (RUSH-2006).
|
|
485
|
+
let sessionMetas = [];
|
|
486
|
+
if (includeLocal && sessions.length > 0) {
|
|
487
|
+
const loaded = await loadSessionMetasForFeedEnrichment(() => discoverSessions({ all: true, limit: 5000 }));
|
|
488
|
+
if (loaded.skippedLock) {
|
|
489
|
+
console.error(chalk.yellow('Feed: session index is locked; skipping local outcome enrichment'));
|
|
490
|
+
}
|
|
491
|
+
sessionMetas = loaded.metas;
|
|
492
|
+
}
|
|
450
493
|
const localSignals = buildSessionSignals(sessions, sessionMetas);
|
|
451
494
|
if (opts.pause || opts.kill) {
|
|
452
495
|
if (!includeLocal) {
|