@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.69
|
|
4
|
+
|
|
5
|
+
- **`agents monitors` no longer fires an action on a poll FAILURE (PHNX-3510).** An `[on-change]` monitor treated a failing poll — a `gh pr list … | jq` that intermittently printed `GraphQL: API rate limit already exceeded …` — as a value change: the empty→error→empty flap read as two changes and dispatched a full agent run each time on a premise that was false (~10 sessions burned re-verifying two already-merged PRs). A command/poll source now flags a snapshot that exited non-zero, or whose output matches a transport/auth/rate-limit error shape (caught even on exit 0, since a pipe to jq swallows the failing half's exit code), as an *observation failure*: the engine skips it, leaves watched-state untouched, does not fire, and records it as a failed check so a sustained streak escalates to the owner as a drought. `agents monitors test` labels a failed poll and reports `Would fire: no`. Note: a non-zero exit is now an observation failure rather than a watchable value — a monitor that wants to watch a command's success/failure should emit a stable token (`… && echo UP || echo DOWN`, always exit 0) so the state it cares about diffs as a value; sustained failures still reach the owner via the drought escalation. Source: `cli/src/lib/monitors/sources/failure.ts`, `cli/src/lib/monitors/sources/command.ts`, `cli/src/lib/monitors/engine.ts`, `cli/docs/automation.md`.
|
|
6
|
+
|
|
7
|
+
- **`agents fleet worktrees` surfaces the worktree-sweep's held set as buckets, not a silent count (PHNX-3520).** The nightly `worktree-sweep` (PHNX-3503, in `phnx-labs/.agents`) reclaims merged agent worktrees and correctly HOLDS anything dirty, unmerged, or undeterminable — but it only ever emitted a `held=<n>` count, so the held set became a permanent, growing residue nothing resolved. Measured 2026-08-30 it is consistently LARGER than the reclaimed set (~800 worktrees fleet-wide), and one bucket inside it is real stranded work: a worktree whose branch carries commits on no remote (the PHNX-2951 / PHNX-2732 class — an agent finished, its commits never reached a remote, nothing surfaced them). The new read-only command classifies every held worktree into its three buckets — **`unmerged-commits`** (the one that matters), **`uncommitted-changes`**, and **`undeterminable`** (`status-unreadable` / `merge-state-unknown`, failing closed exactly as the sweep does) — and reports each with device/repo/worktree/branch/reason/age/size. `--json` is the structured form for machine callers; `--fleet` fans out over every online device and aggregates; `--bucket <name>` filters. The safe recovery action for the stranded bucket is `--push`: it PUBLISHES each `unmerged-commits` branch that is on no remote (re-reading the merge state immediately before pushing and refusing anything already on origin), making the work visible — it never removes a worktree or a branch. The destructive reclaim stays in the sweep; this is the surfacing half the sweep can consume. Source: `cli/src/lib/worktree/held.ts`, `cli/src/commands/ssh.ts`, `cli/src/lib/worktree/held.test.ts`.
|
|
8
|
+
|
|
9
|
+
- **`agents run` starts ~0.6s faster on a box with many secrets bundles (PHNX-3585).** The AGI EXT "New Claude" boot runs `agents run claude --interactive`, and the whole wrapper cost is spent BEFORE the harness prints anything. Profiling that pre-exec window (new `AGENTS_PROFILE_BOOT=1` per-stage tracer, `cli/src/lib/boot-profile.ts`) showed the dominant stage was `--strategy balanced` account rotation: `readAccountRegistry` → `listBundles` decrypted EVERY file-store bundle's metadata to find the account bundles, and each decrypt runs a fresh scrypt KDF (~12ms) because every `.enc` file carries its own salt — ~690ms of pure KDF on a box with ~40 bundles, on every launch. Bundle metadata is non-secret by contract (it holds `keychain:`/`env:`/literal refs, never the secret bytes, and is already stored no-ACL so `secrets list` enumerates without Touch ID), so the file store now caches the decrypted metadata JSON keyed by each `.enc` file's `(mtime,size)` plus a passphrase fingerprint. Any bundle write (new mtime/size) or passphrase change (new fingerprint; rotation re-writes every file) misses and re-derives — no staleness, self-healing on any miss — and secret VALUE items never enter the cache. Measured on a worker box: `agents run claude` pre-exec `resolve-version` dropped from ~690ms to ~31ms (warm), total pre-exec ~1040ms → ~370ms. A committed `cli/scripts/bench-boot.sh` keeps it measured. Source: `cli/src/lib/secrets/filestore.ts`.
|
|
10
|
+
|
|
11
|
+
- **Grok's cheap scan now fills `firstUserMessage` from a bounded `chat_history.jsonl` prefix read (PHNX-3621).** The summary-only path never parsed the log. The prefix reader skips Grok `<user_info>` / `synthetic_reason` scaffolding, prefers the first `prompt_index` user turn, and unwraps `<user_query>`. Existing Grok rows backfill through `CONTENT_INDEX_VERSION` v4. Source: `cli/src/lib/session/discover.ts`.
|
|
12
|
+
|
|
13
|
+
- **Worker homes provisioned before seed-on-attach self-heal their account identity at resolve time (PHNX-3660).** `resolveClaudeSetupToken` required the version home's `.claude.json` to carry `oauthAccount.emailAddress`; homes whose `.oauth_token` was written by a pre-fix `accounts attach` had none, so exec-env injection and per-account usage reads were silently inert for them (they authenticated only via the shim's `.oauth_token` fallback). The resolver now recovers the email by matching the home's `.oauth_token` value against the `auth` bundle (re-encode-guarded slug decode, so a lossy or unmatched token can never invent a mapping), writes it back with `seedClaudeWorkerHomeIdentity` so the home converges after one read, and still fails closed — writing nothing — when the token matches no bundle key. Ambient no-home resolves never rewrite the operator's real `~/.claude.json`. Source: `cli/src/lib/claude-account-token.ts`.
|
|
14
|
+
|
|
15
|
+
- **Resume `/continue` no longer launches the exhausted origin login when the pick is a provider account (PHNX-3674).** After native-first rotation (PHNX-3626), a usage-limited origin whose transcript is not in the origin home (trash/backup/reinstall, or local fallback from an unreachable peer) fell through to `/continue` on the provider-inclusive pool without carrying `RecoveryAccount`, so exec authenticated as the rate-limited native login. That continue target now injects the healthy provider the same way native rotation does. Source: `cli/src/lib/session/recovery.ts`, `cli/src/commands/exec.ts`.
|
|
16
|
+
|
|
17
|
+
- **Private share OG covers are no longer cacheable by shared proxies (PHNX-3676).** Token-gated `visibility=private` pages already served HTML as `Cache-Control: private, no-store`, but the generated `/user/slug.png` cover used `public, max-age=31536000, immutable` after the token gate. RFC 9111 lets a shared cache reuse a `public` Authorization response for later unauthenticated requests keyed on the unauthenticated URL, so a Bearer fetch of the cover could leak the preview. `managedCoverHeaders` now treats `private` like `me`/`org` (`private, no-store` + `X-Robots-Tag: noindex`). Source: `cli/src/lib/share/worker-template.ts`.
|
|
18
|
+
|
|
19
|
+
- **`agents sessions inject` can nudge a live remote tmux session that has no session id, and no longer crashes on `--pane --device` (PHNX-3688).** Two bugs are fixed. (1) A live session whose `agents sessions --active` id column shows `-` — only a tmux label like `ag-claude-214edaae:0.0` is known — was untargetable: `inject <shortid>` failed `No active session matches`. The lookup now matches the `ag-<agent>-<shortid>` tmux name's suffix (and the full name / pane id), the only selector such a row exposes, and the discovered row carries its `tmuxName` so the match has something to key on. (2) `inject _ "text" --pane %122 --device <box>` crashed with `TypeError: host.startsWith is not a function`: under `optsWithGlobals()` the parent `sessions` command's variadic `-D, --device <target...>` shadows this subcommand's scalar `--device`, so a single `--device box` arrived as `['box']` and the array flowed straight into `sshExec`. It is now coerced to one host (and fails loud on more than one, since inject delivers to exactly one terminal). A bare session id combined with `--device` now resolves ON that device — its tmux panes live there, not on the box you typed the command from — by re-running the inject over SSH, the tool-native form of the old `agents ssh <box> "agents sessions inject <id> …"` workaround. Source: `cli/src/commands/sessions-inject.ts`, `cli/src/lib/session/active.ts`.
|
|
20
|
+
|
|
21
|
+
- **`agents` releases no longer wedge waiting for a human to mint an attestation (PHNX-3696).** RUSH-2666 made an exact-tree attestation for the *release commit* mandatory but shipped no producer for it, so every `release.sh --apply` since 2026-08-15 stopped at `missing exact attestation key` and required an operator to hand-run `release-attestation-produce.sh` and copy the tarball into the store. `attest-main.yml` already attests every push to `main`; the release commit differs from that base only by the version bump, the folded changelog, and the regenerated command index — exactly the set `release-attestation.sh derive` allowlists — so `release.sh` now derives the record itself (`derive_release_attestation`, inheriting the green base's suite via `--inherit-suite-from`, no suite re-run). Soundness is unchanged: `derive` still fails closed if the tree diff touches anything outside that allowlist, so a code change can never inherit a stale pass, and the derive is best-effort — any failure falls through to the previous poll-then-`require`, which still fails loud (the call site guards it with `|| true`, which is load-bearing under `set -euo pipefail`: a bare call returning non-zero would abort the release before that fallback ran). The regression survived review because `release.test.ts` asserted against `release.sh` as *text*; the new coverage executes the real `derive_release_attestation` body against a real git repo and store. Source: `cli/scripts/release.sh`, `cli/scripts/release.test.ts`, `cli/AGENTS.md`.
|
|
22
|
+
|
|
23
|
+
- **Owner phone pings are short and tappable — but only where a sink can render a link (PHNX-3698).** Every owner-bound message goes through one composer (`composeBroadcastMessage`), and the shared `{message}` now surfaces its links per-sink instead of dumping naked URLs after the footer. A **Slack `channel:` sink** gets mrkdwn labeled links `<url|label>`: the `Sent from claude/6fc1db18 on zion` crumb becomes `<https://prix.dev/console/sessions/<full-id>|claude/6fc1db18>` (an 8-char crumb is first upgraded to the full indexed id, since a truncated id would 404), and every `TEAM-N` key the title or body *names* — not just the session's own `ticketId`, so a ping that only mentions `PHNX-3689` in prose is still tappable — becomes `<https://linear.app/<workspace>/issue/<KEY>|<KEY>>` in place. **iMessage, the owner-scoped rush message, `command:` sinks, and every other channel stay plain** — they cannot render a labeled link and a dumped naked URL reads as noise, so the message is the human sentence with no URLs (keys and crumb as text); there is never a trailing URL line on any sink. `agents notify` and `agents send --to owner` route through that same composer on the owner-scoped (plain) path instead of shipping the raw body dump; a non-owner `agents send` is still delivered verbatim. An important/`--blocked` post (and an owner send) fires a best-effort, opt-in background `agents traces sync` so the linked console page exists when a Slack crumb is tapped (gated exactly like the run-exit arm; `AGENTS_NO_TRACE_SYNC=1` opts out). The Linear-key detector is canonical in one place shared with transcript ticket detection (`session/linear.ts`). Source: `cli/src/lib/feed-broadcast.ts`, `cli/src/lib/owner-message.ts`, `cli/src/lib/session/linear.ts`, `cli/src/lib/session/db.ts`, `cli/src/lib/run-trace-sync.ts`, `cli/src/commands/send.ts`, `cli/src/commands/feed.ts`.
|
|
24
|
+
|
|
25
|
+
- **An ordinary CLI release no longer signs or notarizes anything on macOS, and the macOS attestation producer works again (PHNX-3699, PHNX-3631).** `release-attestation-produce.sh` gated its sign+notarize block on `uname == Darwin` alone, with no `--with-helpers` condition — so a CLI-only attestation produced on a Mac codesigned the CLI binary and rebuilt + signed both helper `.app`s, none of which ship in the tarball (RUSH-3026 removed the binary, RUSH-3100 the bundles). That violates the R3 requirement ("no signing, no notarization on the ordinary path") and, because `agents secrets exec apple.com` cannot unlock a Touch-ID-gated bundle headlessly, it *killed* the release outright — hit live cutting 1.22.69 right after PHNX-3696 made `release.sh` produce this attestation itself. The block is now gated on `--with-helpers`, so cutting a helper release still signs and an ordinary CLI release does not. Shipped alongside the **PHNX-3631** fix in the same path: `ATTEST_TMP` used `mktemp ".../agents-cli-attest.XXXXXX.json"`, but BSD/macOS `mktemp` only substitutes trailing X's and treats a template with a suffix after them as a literal filename — so the first call created that exact file and every later call died with `mkstemp failed … File exists`. Now `agents-cli-attest.json.XXXXXX`, which GNU accepts identically; the same pattern in `remote-sign-mac.sh` is fixed too. Together these take `release-attestation-produce.test.ts` on macOS from 22-of-29 failing to 31/31 passing. Source: `cli/scripts/release-attestation-produce.sh`, `cli/scripts/remote-sign-mac.sh`.
|
|
26
|
+
|
|
27
|
+
- **`agents projects view <path>` auto-detects the cwd's project; `projects for-cwd` is gone (PHNX-3704).** `agents projects view [nameOrPath]` now accepts a DIRECTORY as well as a project name. A path-shaped argument — `.`, `..`, a `~`-prefixed value, a `/`-containing value, or an explicit `--path [dir]` (bare `--path` = cwd) — auto-detects the project that contains that directory via the existing path-based `projectNameForCwd` (def-root containment, longest wins); a bare token stays a project name. `view <path> --json` prints `{name, linear:{name,projectId}, root}` in ONE call, so a consumer gets the project name AND its Linear binding without a second `projects list` round-trip. Fail-open: an all-null shape when nothing contains the path, exit 0, never throws. The `projects for-cwd` subcommand and its `formatForCwdOutput` helper are deleted — `view <path> --json` subsumes them. Source: `cli/src/commands/projects.ts`.
|
|
28
|
+
|
|
29
|
+
- **`agents artifacts share` is private by default when signed in, and storage + visibility now live in one shared module.** A publish with no `--visibility` flag is now `me` (owner-only, Phoenix-gated) instead of `public` — signed in, your share is private unless you opt into `--visibility org` (everyone at your email domain, no org to create) or `--visibility public` (the gallery). A BYO (signed-out) publish still defaults to `public`, since the Worker refuses `me`/`org` without a Phoenix owner. The managed-vs-BYO selection policy and the visibility model are now the single `cli/src/lib/storage/` module (`selectStorageBackendKind` + `publishVisibility`/`defaultVisibilityForBackend`), consumed by both `agents artifacts share` (`lib/share/backend.ts`) and `agents traces sync` (`lib/traces/backend.ts`) instead of each re-deriving it — the seam the `sessions` backup surface reuses next. Failed share calls (publish/list/revisions/delete) now surface the Worker's own error — a 400/413/429 shows its `{"error":"…"}` reason and a 429's `Retry-After` — via one bounded extractor (`lib/share/http-error.ts`) that never dumps an arbitrary body. Source: `cli/src/lib/storage/`, `cli/src/lib/share/{backend,publish,delete,http-error}.ts`, `cli/src/commands/share.ts`.
|
|
30
|
+
|
|
31
|
+
- **`agents view grok` shows the last-known usage % again instead of a numberless "run grok once to refresh usage".** Two bugs compounded. (1) `getGrokUsageInfo` read `unified.jsonl` from the home it was given, but `agents view` fetches usage per INSTALLED VERSION, passing each version's isolated home (`~/.agents/.history/versions/grok/<ver>`) — whose `.grok/logs/unified.jsonl` never exists, because Grok writes its billing log only to the user's shared real home `~/.grok`. So every version fell back to the refresh hint while the real reading (e.g. week 42%) sat unread in the shared home. Grok usage now resolves the requested home's log first and falls back to the shared `~/.grok` log where Grok actually writes (a per-version log, if one ever appears, still wins). The shared last line is attributed only to the identity that owns `~/.grok` (matching `auth.json`, or the real home itself) — other version-scoped Grok accounts stay as no-recent-usage rather than inheriting that meter. (2) The usage cache serializer persisted only fresh `windows`, dropping the `staleWindows` Grok's collector pre-partitions an ended-period reading onto — so the daemon-refreshed cache the plain `agents view grok` reads rendered the plan alone (no bar), even right after a `--refresh` had shown "W: 42% · stale". The serializer now persists the union of fresh and stale windows; `deserializeClaudeUsageSnapshot` re-runs the freshness gate on read, so the round-trip is preserved for every collector (Claude, which returns raw windows, is unchanged). Source: `cli/src/lib/accounting/usage.ts`.
|
|
32
|
+
|
|
33
|
+
## 1.22.68
|
|
34
|
+
|
|
35
|
+
Fix OpenCode session history to index and backfill the genuine first user request.
|
|
36
|
+
|
|
37
|
+
- **`agents sessions resume` is native-first with same-harness account rotation, and records the origin version so it stops falling back (PHNX-3626).** Resume continues the SAME session with the harness's own native resume (`claude --resume <id>`, codex/grok/opencode native) on its recorded `agent@version` + device, rather than spinning up a fresh `/continue` transcript replay. When the origin account is rate/usage/session-limited, recovery rotates to a healthy INJECTABLE provider account of the same harness and stays native in the origin home (`--strategy balanced`, the same weighted-by-headroom picker dispatch uses); `/continue` replay is now the last-resort fallback only when no native path or no healthy same-harness account exists. The origin version is recorded forward at launch — `buildExecEnv` exports `AGENTS_RUN_VERSION`, the SessionStart hook joins it to the harness's real session id in the durable sidecar (like the launch mode), and the scan joins it at row build (`meta.version ?? actorRec.version`) and write-once COALESCEs it into the index — so a codex session whose transcript carried no derivable version (its relocated `.codex-homes/<version>/` home, which the scanner now also reads) no longer degrades to `/continue` for a missing recorded origin. Resume prefers the recorded device and falls back to a clearly-labelled local `/continue` replay from the synced mirror when that device is genuinely unreachable. Source: `cli/src/lib/session/recovery.ts`, `cli/src/lib/exec.ts`, `cli/src/commands/resume.ts`, `cli/src/lib/session/actor-sidecar.ts`, `cli/src/lib/session/db.ts`, `cli/src/lib/session/discover.ts`, `cli/src/lib/accounting/account-pool-collect.ts`, `packages/session-tracker/src/hook.sh`.
|
|
38
|
+
|
|
39
|
+
- **`agents artifacts share --private/--unlisted` had no read authentication — private shares were world-readable by URL (PHNX-3654).** The share Worker's GET served any object with `cache-control: public` after only existence + expiry checks, so `--private`/`--unlisted` were obscurity, not authentication, and a private share could get a title-derived guessable slug. Two fixes ship together. **(A)** An `unlisted`/`--private` publish now prints a loud stderr warning that unlisted is NOT authenticated and points at the real controls, and forces a **64-bit random slug tail** (regardless of `--label`/title) so the capability URL can't be guessed. **(B)** A new **`--protected`** flag (= `--visibility private`) is token-gated read auth: the CLI mints a random viewer token, the Worker stores only its **SHA-256 hash** (`viewer-token-hash`) and serves the object — its page, generated OG cover, and revision list — only to a request whose `?k=<token>` or `Authorization: Bearer` matches under a constant-time compare, returning `404` (never `401`, no existence leak) otherwise. The key rides only in the emitted `https://<host>/<user>/<slug>?k=<token>` URL; the namespace owner reads their own private page without it, and it works for BYO endpoints too. `private` is publish-only — the in-place `share visibility` / PATCH route rejects it with a `400`. Source: `cli/src/lib/share/publish.ts`, `cli/src/lib/share/worker-template.ts`, `cli/src/commands/share.ts`, `cli/docs/share.md`.
|
|
40
|
+
|
|
41
|
+
## 1.22.67
|
|
42
|
+
|
|
43
|
+
- **Keep the shared daemon client-safe (PHNX-3605).** Browser version reconciliation no longer restarts the process; browser and routines lifecycle commands now enable, disable, and reload only their own services while sibling work keeps running. Source: `src/lib/browser/ipc.ts`, `src/commands/browser.ts`, `src/commands/routines.ts`.
|
|
44
|
+
|
|
45
|
+
- **Daemon stop/start races can no longer erase a successor or signal a reused PID (PHNX-3607).** `stopDaemon()` now holds the same `daemon.lock` as start/claim through signal, wait, postcondition, and cleanup; it trusts a recorded PID only when the live process command ends in `__daemon-run`, and compare-deletes the PID/socket identity it captured instead of unlinking a successor's replacement. The stray-daemon reaper now waits for SIGTERM death, escalates to `killTree` when needed, waits again, and retains the registry marker if a duplicate somehow survives. Source: `cli/src/lib/daemon/daemon.ts`, `cli/src/lib/browser/ipc.ts`.
|
|
46
|
+
|
|
47
|
+
- **Daemon supervisor hardening: abandon hung ticks, thread an AbortSignal, and bring account-state/catch-up under the supervisor (PHNX-3608).** A supervised tick that awaited something that never settled used to park the service but hold its in-flight guard forever, so the backoff restart, `agents daemon services restart`, and SIGHUP reload were all blocked — a service could freeze for the daemon's life while still looking healthy (the 12h usage-dark class). On a deadline breach the supervisor now ABANDONS the tick: it aborts the tick's new `AbortSignal`, releases the in-flight guard immediately, and schedules the backoff restart right away, draining the runaway promise separately. `PeriodicService.tick(ctx, signal)` receives that signal so a service can bound its own I/O. `start()`/`stop()`/`restart()` are now bounded by a lifecycle deadline so a wedged bind/close can't stall daemon startup or shutdown. The account-state usage/auth refresh — previously its own un-deadlined `setInterval` behind a `usageRunning` latch (the exact hang that froze usage) — is now TWO supervised `PeriodicService`s (`account-state` usage + `account-auth`) with a real deadline + abort and INDEPENDENT circuit breakers, so a run of usage-refresh failures parks only usage and never starves the slower auth refresh; the deadline signal is threaded through to each provider fetch. Catch-up recovery moved off its bare `setInterval` onto a supervised `catchup` service. Disabling the `monitors` service now routes through `supervisor.stop`, and a stopped monitor engine dispatches nothing on tick — so a disabled monitors service actually stops firing instead of being ticked with its last-loaded set. Source: `cli/src/lib/daemon/supervisor.ts`, `cli/src/lib/daemon/service.ts`, `cli/src/lib/daemon/account-state-daemon-service.ts`, `cli/src/lib/daemon/catchup-service.ts`, `cli/src/lib/daemon/monitor-engine-service.ts`, `cli/src/lib/monitors/engine.ts`, `cli/src/lib/daemon/daemon.ts`, `cli/src/lib/daemon-services.ts`, `cli/src/lib/daemon-ticks.ts`, `cli/src/lib/usage-refresh.ts`, `cli/src/lib/accounting/usage.ts`, `cli/src/lib/auth-health.ts`.
|
|
48
|
+
|
|
49
|
+
- **A cold `agents browser navigate` on a worker now follows the fleet browser hub instead of failing locally (PHNX-3610).** A bare `agents browser start` already forwards to the hub (`config.defaultBrowserDevice`), but a page verb with no bound task — `agents browser navigate|screenshot|click|…` run without a preceding `start` — did not: `resolveTaskRoute`'s no-task branch returned the local box, so the verb ran on the worker and failed with "no default browser on this machine." Agents on other devices could only drive the hub's logged-in browser if they knew to `start` first. The no-task branch now follows `defaultBrowserHub()` (passed in as `resolveTaskRoute`'s `hub` option so the leaf stays pure), and the task-routing hook forwards a taskless drive verb to the hub. So the hub creates the caller's OWN task, the dispatch now **forwards the caller's session/launch identity** (`AGENT_SESSION_ID`/`AGENT_LAUNCH_ID`, the same surface-parity mechanism `run --device` uses) — without it a forwarded verb resolved BLANK identity on the hub and its no-identity task bucket could collide with an unrelated agent's or a human's browser. A cold CLOSE verb (`done`/`stop`) is deliberately **not** forwarded when taskless — with no task it would target a context this session never opened — so it can never blindly stop a hub task. Behavior-only, no new flags; the existing `isSelfHost`/`isFleetRemoteInvocation` guards mean the hub never forwards to itself and a re-exec never loops. Source: `cli/src/lib/browser/task-index.ts`, `cli/src/lib/browser/caller-identity.ts`, `cli/src/commands/browser.ts`.
|
|
50
|
+
|
|
51
|
+
- **The canonical session watch now carries the genuine request and recoverable history (PHNX-3621).** Transcript indexing stores `firstUserMessage` separately from the one-line topic, skips harness-injected plugin/developer scaffolding, preserves long multi-line requests, and backfills existing rows through the versioned content index. `agents feed watch --json` also folds in a bounded set of durable Previous rows on reset, marks ended live rows as Previous, and emits `agent.remove` before their replacement so presentation clients can render and resume history without parsing transcripts or issuing a second session fetch. Source: `cli/src/lib/session/prompt.ts`, `cli/src/lib/session/discover.ts`, `cli/src/lib/session/db.ts`, `cli/src/lib/session/active.ts`, `cli/src/lib/session/remote/watch.ts`, `cli/src/lib/feed/watch.ts`.
|
|
52
|
+
|
|
53
|
+
- **Auto-sync traces when a local run exits (PHNX-3628).** Trace upload was manual-only — the sole caller of `syncTraces()` was `agents traces sync`, so the Prix console went stale between hand-runs. A local, headless `agents run` now spawns a detached `agents traces sync` on exit (mirroring `run-notify`; the incremental ledger means it pushes essentially just the finished session). It fires only when you have already opted into the traces store (signed in AND synced at least once); opt out per-run with `--no-trace-sync` or `AGENTS_NO_TRACE_SYNC=1`. Local runs only. Source: `src/lib/run-trace-sync.ts`, `src/lib/traces/sync.ts`, `src/commands/exec.ts`.
|
|
54
|
+
|
|
55
|
+
- **The shared daemon now contains lock-refresh and reload failures instead of crashing, and concurrent health writers preserve the restart-backoff streak (PHNX-3606).** Every direct `proper-lockfile` lease used by device/team registries, routine launch claims, and usage refresh coordination overrides the library's timer-level compromise throw with an observable log-and-continue barrier. The daemon's `SIGHUP` reload callback is guarded independently of service supervision, and `health.json` updates now serialize the complete read-modify-write under a file lock and publish through atomic rename, so parallel processes cannot erase each other's failure counts or expose partial JSON. Source: `cli/src/lib/lock-compromise.ts`, `cli/src/lib/daemon/daemon.ts`, `cli/src/lib/daemon-health.ts`.
|
|
56
|
+
|
|
57
|
+
- **Daemon usage/auth sync no longer blocks the event loop or opens a per-tick SSH mesh (PHNX-3609).** Headed devices publish one identity-keyed usage snapshot into their conflict-free tracked `~/.agents/devices/<device>/daemon-state.json`; each daemon automatically commits only its owned file and exchanges the user repo under one cross-process lock and a 45-second hard process-tree deadline, so workers receive and merge snapshots newest-wins without a human `agents repo sync user`. `auth-sync` uses the same file for non-secret `ready`/`missing`/`invalid` verdicts, elects one ready publisher, and opens device-to-device SSH only to provision a peer known to be missing the reserved bundle. That exceptional secret transfer is fully async and every import/read-back operation has a 20-second deadline; timeout disables SSH multiplexing, sends `SIGTERM`, then `SIGKILL`s an uncooperative client after a short grace so both the local promise and direct remote connection are bounded. OAuth/setup-token values never enter the Git store. Source: `cli/src/lib/fleet-shared-state.ts`, `cli/src/lib/fleet-shared-repo-sync.ts`, `cli/src/lib/accounting/usage-sync.ts`, `cli/src/lib/secrets/reserved-sync.ts`, `cli/src/lib/secrets/push.ts`, `cli/src/lib/ssh-exec.ts`.
|
|
58
|
+
|
|
3
59
|
## 1.22.66
|
|
4
60
|
|
|
5
61
|
- **Fleet usage/auth sync now reaches FQDN-only-pinned workers (PHNX-3505).** The
|
package/README.md
CHANGED
|
@@ -416,11 +416,11 @@ agents sessions resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # original harness/
|
|
|
416
416
|
agents run auto --resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # adapt if its account is unavailable
|
|
417
417
|
```
|
|
418
418
|
|
|
419
|
-
`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 sessions 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, healthy, and still owns the indexed transcript, its isolated home performs native resume. Claude launches that native resume from the original project directory recorded before the first turn, so its `projects/<cwd-key>` lookup reaches the conversation even when the session later changed directories.
|
|
419
|
+
`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 sessions 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, healthy, and still owns the indexed transcript, its isolated home performs native resume. Claude launches that native resume from the original project directory recorded before the first turn, so its `projects/<cwd-key>` lookup reaches the conversation even when the session later changed directories. When that origin login is usage-limited but the home still owns the transcript, resume stays native and rotates to a healthy injectable provider account of the **same harness**. `/continue <id>` is the last-resort fallback — a signed-out, revoked, trashed, backup-only, or same-number-reinstalled origin, or a limited origin whose transcript is no longer in that home — and still authenticates as a healthy same-harness account (injecting a provider credential when that is the pick, so it never launches the exhausted native login). 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.
|
|
420
420
|
|
|
421
421
|
### Send an agent to the background — and bring it back
|
|
422
422
|
|
|
423
|
-
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 resume <id>` brings it back through the same origin-device recovery decision: native resume in the exact
|
|
423
|
+
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 resume <id>` brings it back through the same origin-device recovery decision: native resume in the exact origin home (healthy login, or a rotated injectable account on a usage limit), or same-harness `/continue` as last resort when that home is unavailable, with the full indexed history (including whatever it did while backgrounded).
|
|
424
424
|
|
|
425
425
|
```
|
|
426
426
|
agents sessions detach a1b2c3d4 # go headless in the background, keep working
|
|
@@ -658,6 +658,9 @@ agents fleet status # online/offline rollup + NEEDS ATTENTIO
|
|
|
658
658
|
agents fleet status --verbose # full per-device auth/CLI/sync/version grid
|
|
659
659
|
agents fleet status --live # force a live resource probe (alias of --refresh)
|
|
660
660
|
agents fleet status --json --strict # scriptable fleet health gate
|
|
661
|
+
agents fleet worktrees # held agent worktrees by bucket: stranded (unmerged) · dirty · undeterminable
|
|
662
|
+
agents fleet worktrees --fleet # aggregate the held set across every online device
|
|
663
|
+
agents fleet worktrees --push # publish stranded on-no-remote branches (recovers work; never deletes)
|
|
661
664
|
agents devices harnesses # per device: agent@version · account · signed · quota · ready
|
|
662
665
|
agents devices accounts # same, one row per account (which harnesses share it)
|
|
663
666
|
agents devices harnesses --agents claude,codex --json # scoped, machine-readable
|
|
@@ -1252,6 +1255,8 @@ agents daemon services # health of the two hosted services
|
|
|
1252
1255
|
agents daemon services list # every toggleable service and its current on/off state
|
|
1253
1256
|
agents daemon services enable secrets-broker
|
|
1254
1257
|
agents daemon services disable browser-ipc # stop hosting browser IPC without stopping the daemon
|
|
1258
|
+
agents browser stop --service # browser-scoped alias; next browser action requiring IPC re-enables it
|
|
1259
|
+
agents routines stop # disable/reload only the scheduler service
|
|
1255
1260
|
agents daemon logs -f --level warn --since 1h
|
|
1256
1261
|
agents daemon doctor # one-shot health check; non-zero exit on problems
|
|
1257
1262
|
```
|
|
@@ -1261,8 +1266,11 @@ watchdog, device probe, self-heal, keychain reap, account-state refresh,
|
|
|
1261
1266
|
state-dir checks) is an independent toggle in `~/.agents/daemon/services.yaml`.
|
|
1262
1267
|
`agents daemon services list` shows every service; `enable|disable <id>` flips
|
|
1263
1268
|
one. Missing keys default to enabled, so upgrades are no-ops. Most services take
|
|
1264
|
-
effect on the next daemon start;
|
|
1265
|
-
|
|
1269
|
+
effect on the next daemon start; browser IPC is registered even when boot-disabled
|
|
1270
|
+
so browser commands can enable it live, while scheduler and monitor engine also
|
|
1271
|
+
re-evaluate on `SIGHUP reload`. Only `agents daemon start|stop|restart` owns the
|
|
1272
|
+
whole process lifecycle. Browser and routines clients change their own service
|
|
1273
|
+
state without evicting sibling work.
|
|
1266
1274
|
|
|
1267
1275
|
There is no `agents daemon jobs` -- scheduled work is always `agents routines`
|
|
1268
1276
|
(see `agents routines stats` for per-routine failure detail). `disable` is a
|
|
@@ -1317,6 +1325,7 @@ Sources: a command's stdout (`--watch` / `--poll`), an HTTP endpoint (`--poll-ht
|
|
|
1317
1325
|
# Signed in? Just publish — no Cloudflare setup.
|
|
1318
1326
|
agents auth login
|
|
1319
1327
|
agents artifacts share plan.html --visibility unlisted # → https://share.agents-cli.sh/<handle>/<slug>-<id>
|
|
1328
|
+
agents artifacts share secret.html --protected # → https://share.agents-cli.sh/<handle>/<slug>-<id>?k=<token>
|
|
1320
1329
|
|
|
1321
1330
|
# Or provision your own Cloudflare R2 (~$0).
|
|
1322
1331
|
agents artifacts setup # once: provision bucket + Worker on your CF
|
|
@@ -1326,7 +1335,7 @@ agents artifacts share plan.html --label "Q3 fleet plan" --meta kind=plan # hu
|
|
|
1326
1335
|
agents artifacts share plan.html --json # URL object for plan-render hooks
|
|
1327
1336
|
agents artifacts share list --agent claude # public gallery, filterable
|
|
1328
1337
|
agents artifacts share list --meta kind=plan # exact, repeatable metadata filters
|
|
1329
|
-
agents artifacts share list --all # include hidden unlisted/me/org pages
|
|
1338
|
+
agents artifacts share list --all # include hidden unlisted/private/me/org pages
|
|
1330
1339
|
agents artifacts share list --scope me # just the owner-only pages
|
|
1331
1340
|
agents artifacts share edit fleet --label "Final fleet plan" --meta status=final
|
|
1332
1341
|
agents artifacts share revisions fleet # prior versions kept under a slug
|
|
@@ -1348,7 +1357,10 @@ Worker lazily renders and caches the branded 1200×630 Open Graph card at `<slug
|
|
|
1348
1357
|
so publishing from Linux does not require a local Chromium; BYO endpoints keep the
|
|
1349
1358
|
local screenshot fallback. `--visibility unlisted` (hidden aliases
|
|
1350
1359
|
`--unlisted` / `--private`) is a capability URL: GET still works, the gallery hides it,
|
|
1351
|
-
and the Worker sends `X-Robots-Tag: noindex`. `--visibility
|
|
1360
|
+
and the Worker sends `X-Robots-Tag: noindex`. `--protected` (`--visibility private`)
|
|
1361
|
+
is token-gated: the published URL carries a secret `?k=` key (`Authorization: Bearer`
|
|
1362
|
+
is accepted too) and GET returns 404 without it — treat the whole link as a secret.
|
|
1363
|
+
`--visibility me` is visible only to
|
|
1352
1364
|
you (the signed-in owner); `--visibility org` is visible to anyone at your email
|
|
1353
1365
|
**domain** — derived from your own address, so it needs a **workspace** Google
|
|
1354
1366
|
account and is refused on a public-inbox domain (`gmail.com`, `outlook.com`,
|
|
@@ -1368,8 +1380,9 @@ single **Phoenix ID** (Google-only device-code OAuth, `agents auth login`) — s
|
|
|
1368
1380
|
a Cloudflare API token from your `cloudflare.com` secrets bundle (or `--token`), creates
|
|
1369
1381
|
an R2 bucket, uploads a tiny Worker, and enables the free `*.workers.dev` subdomain (or
|
|
1370
1382
|
maps `--domain share.example.com` when the token owns the zone). Writes are bearer-gated
|
|
1371
|
-
**through** the Worker (Phoenix bearer or static `WRITE_TOKEN`);
|
|
1372
|
-
a link outlives the agent
|
|
1383
|
+
**through** the Worker (Phoenix bearer or static `WRITE_TOKEN`); public and unlisted
|
|
1384
|
+
reads are **public** so a link outlives the agent, while `--protected` token-gates
|
|
1385
|
+
reads on BYO too. R2 has zero egress + a 10 GB free tier, so BYO is still
|
|
1373
1386
|
effectively free.
|
|
1374
1387
|
|
|
1375
1388
|
**Fleet mode:** provision one endpoint, then every fleet / cloud / ephemeral agent
|
package/dist/bootstrap.js
CHANGED
|
@@ -19,6 +19,7 @@ import * as path from 'path';
|
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import { detectDevBuild } from './lib/startup/dev-build.js';
|
|
21
21
|
import { configureRootCommand } from './lib/startup/root-command.js';
|
|
22
|
+
import { bootMark } from './lib/boot-profile.js';
|
|
22
23
|
// `ora`, `@inquirer/prompts`, `./commands/utils.js`, and the agents/versions/shims
|
|
23
24
|
// modules are imported dynamically at their use sites: they are needed only on
|
|
24
25
|
// interactive / update / shim-repair paths, never for fast commands like
|
|
@@ -1029,6 +1030,7 @@ if (helpAllRequested) {
|
|
|
1029
1030
|
// immediately: skip the update check (PATH scan + cache read) and the detached
|
|
1030
1031
|
// background sync (spawns a child process) that every other invocation runs.
|
|
1031
1032
|
if (!isDocumentationRequest) {
|
|
1033
|
+
bootMark('bootstrap:evaluated');
|
|
1032
1034
|
// Run update check before parsing so the upgrade notice/prompt precedes output.
|
|
1033
1035
|
await checkForUpdates();
|
|
1034
1036
|
// Fire-and-forget the background sync. System repo gets a real fast-forward
|
|
@@ -1157,21 +1159,22 @@ if (passedArgs.length === 0) {
|
|
|
1157
1159
|
}
|
|
1158
1160
|
try {
|
|
1159
1161
|
await maybeBootstrapShimIntegration(requestedCommand, isDocumentationRequest, verboseStartup);
|
|
1162
|
+
bootMark('bootstrap:pre-parse');
|
|
1160
1163
|
await program.parseAsync();
|
|
1161
1164
|
}
|
|
1162
1165
|
catch (err) {
|
|
1163
1166
|
if (err instanceof Error && err.name === 'ExitPromptError') {
|
|
1164
1167
|
process.exit(130);
|
|
1165
1168
|
}
|
|
1166
|
-
// Browser-
|
|
1169
|
+
// Browser-service-not-running and CDP-not-reachable surface as typed errors
|
|
1167
1170
|
// from src/lib/browser/. Don't dump a Node stacktrace for these — they are
|
|
1168
1171
|
// user-actionable, not engineering bugs. See issues #41 and #43.
|
|
1169
1172
|
if (err instanceof Error) {
|
|
1170
|
-
const
|
|
1173
|
+
const isBrowserServiceNotRunning = err.name === 'BrowserServiceNotRunningError';
|
|
1171
1174
|
const isBrowserCdpUnreachable = err.name === 'BrowserCdpConnectionError';
|
|
1172
1175
|
const isBrowserIpcDown = err.message.startsWith('IPC error:') &&
|
|
1173
1176
|
(err.message.includes('ECONNREFUSED') || err.message.includes('ENOENT'));
|
|
1174
|
-
if (
|
|
1177
|
+
if (isBrowserServiceNotRunning || isBrowserCdpUnreachable || isBrowserIpcDown) {
|
|
1175
1178
|
console.error(err.message);
|
|
1176
1179
|
process.exit(1);
|
|
1177
1180
|
}
|
package/dist/commands/browser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
3
|
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import * as fs from 'fs';
|
|
@@ -14,8 +15,9 @@ import { DEFAULT_VIEWPORT, parseWindowSize, parseWindowPosition } from '../lib/b
|
|
|
14
15
|
import { runBrowserSessionsCommand } from './browser-sessions-picker.js';
|
|
15
16
|
import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
|
|
16
17
|
import { parseTargetFilter } from '../lib/browser/service.js';
|
|
17
|
-
import {
|
|
18
|
-
import { bindTask, getTaskBinding, honorScreenshotOutput, REJECT_DEVICE_MESSAGE, resolveTaskRoute, unbindTask, unbindTasksForProfile, updateTaskBinding, } from '../lib/browser/task-index.js';
|
|
18
|
+
import { BrowserServiceNotRunningError, formatBrowserServiceNotRunningError, sendIPCRequest as sendRawIPCRequest, stopBrowserService, } from '../lib/browser/ipc.js';
|
|
19
|
+
import { bindTask, getTaskBinding, honorScreenshotOutput, isTerminalBrowserVerb, REJECT_DEVICE_MESSAGE, resolveTaskRoute, unbindTask, unbindTasksForProfile, updateTaskBinding, } from '../lib/browser/task-index.js';
|
|
20
|
+
import { callerIdentityEnv, resolveCallerIdentity } from '../lib/browser/caller-identity.js';
|
|
19
21
|
import { isSelfHost } from '../lib/devices/self-host.js';
|
|
20
22
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
21
23
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
@@ -132,12 +134,17 @@ async function dispatchBrowserToDevice(device, forwardedArgs, mode) {
|
|
|
132
134
|
}
|
|
133
135
|
const target = sshTargetFor(host);
|
|
134
136
|
const remoteOs = resolveRemoteOsSync(host.name);
|
|
135
|
-
|
|
137
|
+
// Forward the caller's identity so the HUB resolves the same session/launch id
|
|
138
|
+
// the worker would stamp locally. Without it the forwarded verb lands blank on
|
|
139
|
+
// the hub and its no-identity task bucket collides with unrelated tasks — the
|
|
140
|
+
// surface-parity gap the run --device path already closes (caller-identity.ts).
|
|
141
|
+
const identityEnv = callerIdentityEnv(resolveCallerIdentity());
|
|
142
|
+
const env = withActorEnv({ ...identityEnv, AGENTS_FLEET_REMOTE: '1' });
|
|
136
143
|
const remoteCmd = buildRemoteAgentsInvocation(forwardedArgs, undefined, remoteOs, env);
|
|
137
144
|
if (mode === 'stream') {
|
|
138
145
|
const code = streamAgentsOnHost(host, forwardedArgs, {
|
|
139
146
|
interactive: !!process.stdout.isTTY && !process.argv.includes('--no-tty'),
|
|
140
|
-
extraEnv: { AGENTS_FLEET_REMOTE: '1' },
|
|
147
|
+
extraEnv: { ...identityEnv, AGENTS_FLEET_REMOTE: '1' },
|
|
141
148
|
remoteOs,
|
|
142
149
|
target,
|
|
143
150
|
});
|
|
@@ -304,7 +311,7 @@ export function registerBrowserCommand(program) {
|
|
|
304
311
|
agents browser navigate https://example.com
|
|
305
312
|
agents browser screenshot
|
|
306
313
|
|
|
307
|
-
# Keep one process and
|
|
314
|
+
# Keep one process and browser-service socket warm for repeated actions
|
|
308
315
|
agents browser stream --task "$AGENTS_BROWSER_TASK"
|
|
309
316
|
|
|
310
317
|
# Bind a device at start; later verbs resolve it from the task
|
|
@@ -324,9 +331,9 @@ export function registerBrowserCommand(program) {
|
|
|
324
331
|
# Close tabs the daemon's own reaper would have caught on its next 5-min tick
|
|
325
332
|
agents browser prune --dry-run
|
|
326
333
|
|
|
327
|
-
# Recover
|
|
328
|
-
# stop
|
|
329
|
-
agents browser stop --
|
|
334
|
+
# Recover browser IPC without interrupting routines, usage, or secrets:
|
|
335
|
+
# stop only the browser service, then start it clean
|
|
336
|
+
agents browser stop --service
|
|
330
337
|
agents browser start
|
|
331
338
|
`,
|
|
332
339
|
notes: `
|
|
@@ -1261,13 +1268,25 @@ function registerTaskCommands(browser) {
|
|
|
1261
1268
|
task: taskFlag ?? process.env.AGENTS_BROWSER_TASK,
|
|
1262
1269
|
sessionId: callerSessionId(),
|
|
1263
1270
|
launchId: callerLaunchId(),
|
|
1271
|
+
hub: defaultBrowserHub(),
|
|
1264
1272
|
});
|
|
1265
1273
|
if (route.kind !== 'proceed') {
|
|
1266
1274
|
console.error(route.message);
|
|
1267
1275
|
process.exit(1);
|
|
1268
1276
|
}
|
|
1269
1277
|
inferredTaskName = route.task;
|
|
1270
|
-
|
|
1278
|
+
// A cold page verb (no bound task) whose route is the fleet hub still
|
|
1279
|
+
// forwards: `route.task` is undefined but `route.device` is a peer, so the
|
|
1280
|
+
// `--task` push below is skipped and the hub's daemon creates the caller's
|
|
1281
|
+
// OWN task (keyed to the identity `dispatchBrowserToDevice` now forwards).
|
|
1282
|
+
// Only a self-host route runs locally.
|
|
1283
|
+
if (isSelfHost(route.device))
|
|
1284
|
+
return;
|
|
1285
|
+
// ...but a cold CLOSE verb (done/stop) must NOT forward: with no task it
|
|
1286
|
+
// would target a browsing context this session never opened. It stays local
|
|
1287
|
+
// (its historic no-task behaviour) unless an explicit --task/local binding
|
|
1288
|
+
// routes it — a taskless `done` must never blindly stop a hub task.
|
|
1289
|
+
if (!route.task && isTerminalBrowserVerb(top))
|
|
1271
1290
|
return;
|
|
1272
1291
|
const forwarded = browserForwardedArgv();
|
|
1273
1292
|
if (route.task && !forwarded.includes('--task')) {
|
|
@@ -1364,7 +1383,7 @@ function registerTaskCommands(browser) {
|
|
|
1364
1383
|
});
|
|
1365
1384
|
setHelpSections(stream, {
|
|
1366
1385
|
examples: `
|
|
1367
|
-
# Batch two warm actions through one process and one
|
|
1386
|
+
# Batch two warm actions through one process and one browser-service connection
|
|
1368
1387
|
printf '%s\\n' \\
|
|
1369
1388
|
'{"action":"screenshot","path":"/tmp/page.jpg"}' \\
|
|
1370
1389
|
'{"action":"click","atX":320,"atY":540}' \\
|
|
@@ -1688,24 +1707,29 @@ function registerTaskCommands(browser) {
|
|
|
1688
1707
|
});
|
|
1689
1708
|
browser
|
|
1690
1709
|
.command('stop')
|
|
1691
|
-
.description('Stop a browser task and close its tabs; with --profile, detach the whole profile
|
|
1710
|
+
.description('Stop a browser task and close its tabs; with --profile, detach the whole profile; with --service, stop only browser IPC while the shared daemon stays up')
|
|
1692
1711
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1693
1712
|
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1694
1713
|
.option('-p, --profile <name>', 'Detach the whole profile (incl. composite "name@endpoint") instead of stopping a single task')
|
|
1695
|
-
.option('--
|
|
1714
|
+
.option('--service', 'Stop only the browser-ipc service and clear a stale/wedged socket; the shared daemon and its other services stay up')
|
|
1715
|
+
.addOption(new Option('--daemon').hideHelp())
|
|
1696
1716
|
.action(async (opts) => {
|
|
1697
|
-
if (opts.daemon) {
|
|
1717
|
+
if (opts.service || opts.daemon) {
|
|
1698
1718
|
if (opts.profile || opts.task) {
|
|
1699
|
-
console.error('--
|
|
1719
|
+
console.error('--service stops browser IPC; do not combine it with --profile or --task.');
|
|
1700
1720
|
process.exit(1);
|
|
1701
1721
|
}
|
|
1722
|
+
if (opts.daemon) {
|
|
1723
|
+
console.error('`--daemon` now means the browser service only; use `--service`. The shared daemon will not be stopped.');
|
|
1724
|
+
}
|
|
1702
1725
|
try {
|
|
1703
|
-
const result = await
|
|
1726
|
+
const result = await stopBrowserService();
|
|
1704
1727
|
const parts = [];
|
|
1705
|
-
parts.push(result.wasRunning ? 'Stopped browser
|
|
1728
|
+
parts.push(result.wasRunning ? 'Stopped browser service' : 'Browser service was not running');
|
|
1706
1729
|
if (result.socketCleared)
|
|
1707
1730
|
parts.push('cleared stale socket');
|
|
1708
|
-
|
|
1731
|
+
parts.push(result.daemonRunning ? 'shared daemon is still running' : 'shared daemon was already stopped');
|
|
1732
|
+
console.log(`${parts.join('; ')}. Browser IPC starts again on the next browser action that requires it.`);
|
|
1709
1733
|
}
|
|
1710
1734
|
catch (err) {
|
|
1711
1735
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -2121,7 +2145,7 @@ function registerTaskCommands(browser) {
|
|
|
2121
2145
|
}
|
|
2122
2146
|
browser
|
|
2123
2147
|
.command('status')
|
|
2124
|
-
.description('Show running browser tasks')
|
|
2148
|
+
.description('Show browser service state and running browser tasks')
|
|
2125
2149
|
.option('-p, --profile <name>', 'Filter by profile')
|
|
2126
2150
|
.option('--json', 'Output machine-readable JSON')
|
|
2127
2151
|
.action(async (opts) => {
|
|
@@ -2133,10 +2157,10 @@ function registerTaskCommands(browser) {
|
|
|
2133
2157
|
}, { autoStartDaemon: false });
|
|
2134
2158
|
}
|
|
2135
2159
|
catch (err) {
|
|
2136
|
-
if (err instanceof
|
|
2137
|
-
const message =
|
|
2160
|
+
if (err instanceof BrowserServiceNotRunningError) {
|
|
2161
|
+
const message = formatBrowserServiceNotRunningError();
|
|
2138
2162
|
if (opts.json) {
|
|
2139
|
-
console.log(JSON.stringify({ ok: false, error: message }));
|
|
2163
|
+
console.log(JSON.stringify({ ok: false, service: { id: 'browser-ipc', state: 'stopped' }, error: message }));
|
|
2140
2164
|
}
|
|
2141
2165
|
else {
|
|
2142
2166
|
console.error(message);
|
|
@@ -2158,6 +2182,7 @@ function registerTaskCommands(browser) {
|
|
|
2158
2182
|
console.log(JSON.stringify(response.profiles ?? [], null, 2));
|
|
2159
2183
|
return;
|
|
2160
2184
|
}
|
|
2185
|
+
console.log(chalk.gray('Browser service: running (shared daemon unchanged)'));
|
|
2161
2186
|
// Build flat list of tasks with profile context
|
|
2162
2187
|
const allTasks = [];
|
|
2163
2188
|
for (const profile of response.profiles || []) {
|
|
@@ -28,7 +28,7 @@ export declare function makeHome(): string;
|
|
|
28
28
|
export declare function run(home: string, args: string[]): ReturnType<typeof spawnSync>;
|
|
29
29
|
/**
|
|
30
30
|
* Spawn a real, long-lived process whose command line ends in `__daemon-run`
|
|
31
|
-
* (so `
|
|
31
|
+
* (so `isLiveDaemon`'s process-command check accepts it — see
|
|
32
32
|
* `lib/daemon.test.ts`'s "reaps a live __daemon-run registrant" test, same
|
|
33
33
|
* technique) and register it in `home`'s OWN instance registry, exactly the
|
|
34
34
|
* marker `registerDaemonInstance` would write. A real live process, not a
|
|
@@ -54,7 +54,7 @@ export function run(home, args) {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Spawn a real, long-lived process whose command line ends in `__daemon-run`
|
|
57
|
-
* (so `
|
|
57
|
+
* (so `isLiveDaemon`'s process-command check accepts it — see
|
|
58
58
|
* `lib/daemon.test.ts`'s "reaps a live __daemon-run registrant" test, same
|
|
59
59
|
* technique) and register it in `home`'s OWN instance registry, exactly the
|
|
60
60
|
* marker `registerDaemonInstance` would write. A real live process, not a
|
|
@@ -67,7 +67,7 @@ export async function spawnFakeRegisteredDaemon(home) {
|
|
|
67
67
|
stdio: 'ignore',
|
|
68
68
|
});
|
|
69
69
|
// Give the exec a moment to land before `ps` (read by the status command's
|
|
70
|
-
//
|
|
70
|
+
// isLiveDaemon check) is asked to see its real argv — mirrors
|
|
71
71
|
// lib/daemon.test.ts's identical fake-daemon technique.
|
|
72
72
|
await new Promise((r) => setTimeout(r, 150));
|
|
73
73
|
const instancesDir = path.join(home, '.agents', '.cache', 'helpers', 'daemon', 'instances');
|
package/dist/commands/daemon.js
CHANGED
|
@@ -297,9 +297,9 @@ async function probeSecretsBroker() {
|
|
|
297
297
|
}
|
|
298
298
|
async function probeBrowserIPC() {
|
|
299
299
|
const record = readSubsystemHealth(SUBSYSTEM_BROWSER_IPC);
|
|
300
|
-
const {
|
|
300
|
+
const { isBrowserServiceReachable, getSocketPath } = await import('../lib/browser/ipc.js');
|
|
301
301
|
const { listAllProfileSnapshots } = await import('../lib/browser/runtime-state.js');
|
|
302
|
-
const bound = await
|
|
302
|
+
const bound = await isBrowserServiceReachable();
|
|
303
303
|
const sessionCount = listAllProfileSnapshots().filter((s) => s.pidAlive && s.daemonAlive).length;
|
|
304
304
|
return { bound, socketPath: getSocketPath(), sessionCount, record };
|
|
305
305
|
}
|
|
@@ -892,14 +892,13 @@ export function registerDaemonCommand(program) {
|
|
|
892
892
|
'systemctl start' on a disabled unit.
|
|
893
893
|
|
|
894
894
|
'agents daemon services enable|disable|restart <id>' applies live (no
|
|
895
|
-
daemon restart) for
|
|
896
|
-
browser-ipc
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
'agents daemon services' names which case you're in per row.
|
|
895
|
+
daemon restart) for supervisor-managed services that were registered at
|
|
896
|
+
boot. browser-ipc is also registered while disabled, specifically so a
|
|
897
|
+
browser client can enable it live. The inline scheduler re-evaluates its
|
|
898
|
+
toggle on every reload as well. A boot-disabled service other than
|
|
899
|
+
browser-ipc, the monitor on-transition, and webhook-receiver still need an
|
|
900
|
+
operator 'agents daemon restart'. 'agents daemon services' names which
|
|
901
|
+
case you're in per row.
|
|
903
902
|
`,
|
|
904
903
|
});
|
|
905
904
|
cmd.command('status')
|
|
@@ -1020,12 +1019,13 @@ export function registerDaemonCommand(program) {
|
|
|
1020
1019
|
agents daemon services restart secrets-broker
|
|
1021
1020
|
`,
|
|
1022
1021
|
notes: `
|
|
1023
|
-
A service disabled at daemon boot
|
|
1024
|
-
supervisor, so
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
true/false' does
|
|
1022
|
+
A service disabled at daemon boot is normally not registered on the
|
|
1023
|
+
supervisor, so enabling it needs an operator restart. browser-ipc is the
|
|
1024
|
+
exception: it is registered stopped and can be enabled live. The inline
|
|
1025
|
+
scheduler also applies enable/disable on reload; webhook-receiver and the
|
|
1026
|
+
monitor on-transition still require 'agents daemon restart'. Each row in
|
|
1027
|
+
the plain-text view names which case it is; 'supervised: true/false' does
|
|
1028
|
+
the same in --json.
|
|
1029
1029
|
`,
|
|
1030
1030
|
});
|
|
1031
1031
|
servicesCmd
|
|
@@ -1057,12 +1057,11 @@ export function registerDaemonCommand(program) {
|
|
|
1057
1057
|
* Apply an enable/disable toggle live (RUSH-3193 P4): persist it, then signal
|
|
1058
1058
|
* the running daemon to reload — its handler diffs the toggle and drives
|
|
1059
1059
|
* `supervisor.start/stop(id)` for a supervised service, so no restart is
|
|
1060
|
-
* needed for
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
* daemon's own reload log says so.
|
|
1060
|
+
* needed for supervisor-managed services registered at boot. browser-ipc is
|
|
1061
|
+
* registered even when disabled so it can be enabled live; the inline
|
|
1062
|
+
* scheduler also re-evaluates its toggle on reload. webhook-receiver,
|
|
1063
|
+
* monitors' on-transition, and other boot-disabled services still need a
|
|
1064
|
+
* deliberate operator restart — the daemon's own reload log says so.
|
|
1066
1065
|
*/
|
|
1067
1066
|
function applyServiceToggleLive(service) {
|
|
1068
1067
|
if (!isDaemonRunning())
|