@phnx-labs/agents-cli 1.22.65 → 1.22.68
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 +39 -0
- package/README.md +7 -2
- package/dist/bootstrap.js +3 -3
- package/dist/commands/apply.js +9 -2
- 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 +40 -0
- package/dist/commands/monitors.js +2 -2
- 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/sessions-optimize.js +1 -1
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +33 -9
- 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 +11 -3
- package/dist/lib/accounting/usage.js +23 -10
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- 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/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/known-hosts.d.ts +15 -0
- package/dist/lib/devices/known-hosts.js +18 -0
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/exec.js +11 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- 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 +9 -2
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +13 -0
- package/dist/lib/run-trace-sync.js +77 -0
- 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 +120 -102
- package/dist/lib/session/active.d.ts +3 -1
- package/dist/lib/session/active.js +2 -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 +5 -2
- package/dist/lib/session/db.js +54 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +106 -25
- package/dist/lib/session/prompt.d.ts +11 -0
- package/dist/lib/session/prompt.js +25 -0
- package/dist/lib/session/recovery.d.ts +30 -2
- package/dist/lib/session/recovery.js +56 -4
- 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 +185 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/publish.d.ts +55 -9
- package/dist/lib/share/publish.js +87 -9
- package/dist/lib/share/worker-template.js +100 -8
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/teams/registry.js +2 -0
- 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/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/package.json +1 -1
- 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,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.68
|
|
4
|
+
|
|
5
|
+
Fix OpenCode session history to index and backfill the genuine first user request.
|
|
6
|
+
|
|
7
|
+
- **`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`.
|
|
8
|
+
|
|
9
|
+
- **`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`.
|
|
10
|
+
|
|
11
|
+
## 1.22.67
|
|
12
|
+
|
|
13
|
+
- **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`.
|
|
14
|
+
|
|
15
|
+
- **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`.
|
|
16
|
+
|
|
17
|
+
- **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`.
|
|
18
|
+
|
|
19
|
+
- **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`.
|
|
20
|
+
|
|
21
|
+
- **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`.
|
|
22
|
+
|
|
23
|
+
- **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`.
|
|
24
|
+
|
|
25
|
+
- **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`.
|
|
26
|
+
|
|
27
|
+
- **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`.
|
|
28
|
+
|
|
29
|
+
## 1.22.66
|
|
30
|
+
|
|
31
|
+
- **Fleet usage/auth sync now reaches FQDN-only-pinned workers (PHNX-3505).** The
|
|
32
|
+
usage-sync and reserved-auth-sync push/pull planners judged a peer "pinned" by its
|
|
33
|
+
bare device name, but a tailnet-discovered worker's host key is pinned under its
|
|
34
|
+
FQDN (`yosemite-m6.tail….ts.net`). So every worker not also pinned under its short
|
|
35
|
+
name was silently skipped — its `claude-usage.json` went days stale and `balanced`
|
|
36
|
+
routed into weekly-maxed accounts, and its auth token never propagated. The pin
|
|
37
|
+
check now resolves the same host string the ssh connection dials
|
|
38
|
+
(`isDevicePinned`), matching either form. Source:
|
|
39
|
+
`cli/src/lib/devices/known-hosts.ts`, `cli/src/lib/accounting/usage-sync.ts`,
|
|
40
|
+
`cli/src/lib/secrets/reserved-sync.ts`.
|
|
41
|
+
|
|
3
42
|
## 1.22.65
|
|
4
43
|
|
|
5
44
|
- **`agents view` shows the last-known usage number with its age instead of hiding it (PHNX-3453).** When the usage endpoint 429-backs-off an account so a window is not refreshed within its own lifetime (Claude's 5h session window) or a billing period rolls over (Grok's weekly credit window), the view now renders the last reading with a staleness suffix — `S: █▌░░░ 30% · 6h old`, `W: ██▏░░ 42% · stale (period ended 1h ago)` — instead of `S: ┄┄┄┄┄ unavailable` or a numberless `run grok once to refresh usage`. The dropped windows ride a new VIEW-ONLY `UsageSnapshot.staleWindows` field; routing still reads only `windows`, so `isUsageVerified`/`hasStaleUsage`/`deriveUsageStatusFromSnapshot` continue to treat a stale/expired number as unverified and never route on it. Source: `cli/src/lib/accounting/usage.ts`.
|
package/README.md
CHANGED
|
@@ -1252,6 +1252,8 @@ agents daemon services # health of the two hosted services
|
|
|
1252
1252
|
agents daemon services list # every toggleable service and its current on/off state
|
|
1253
1253
|
agents daemon services enable secrets-broker
|
|
1254
1254
|
agents daemon services disable browser-ipc # stop hosting browser IPC without stopping the daemon
|
|
1255
|
+
agents browser stop --service # browser-scoped alias; next browser action requiring IPC re-enables it
|
|
1256
|
+
agents routines stop # disable/reload only the scheduler service
|
|
1255
1257
|
agents daemon logs -f --level warn --since 1h
|
|
1256
1258
|
agents daemon doctor # one-shot health check; non-zero exit on problems
|
|
1257
1259
|
```
|
|
@@ -1261,8 +1263,11 @@ watchdog, device probe, self-heal, keychain reap, account-state refresh,
|
|
|
1261
1263
|
state-dir checks) is an independent toggle in `~/.agents/daemon/services.yaml`.
|
|
1262
1264
|
`agents daemon services list` shows every service; `enable|disable <id>` flips
|
|
1263
1265
|
one. Missing keys default to enabled, so upgrades are no-ops. Most services take
|
|
1264
|
-
effect on the next daemon start;
|
|
1265
|
-
|
|
1266
|
+
effect on the next daemon start; browser IPC is registered even when boot-disabled
|
|
1267
|
+
so browser commands can enable it live, while scheduler and monitor engine also
|
|
1268
|
+
re-evaluate on `SIGHUP reload`. Only `agents daemon start|stop|restart` owns the
|
|
1269
|
+
whole process lifecycle. Browser and routines clients change their own service
|
|
1270
|
+
state without evicting sibling work.
|
|
1266
1271
|
|
|
1267
1272
|
There is no `agents daemon jobs` -- scheduled work is always `agents routines`
|
|
1268
1273
|
(see `agents routines stats` for per-routine failure detail). `disable` is a
|
package/dist/bootstrap.js
CHANGED
|
@@ -1163,15 +1163,15 @@ catch (err) {
|
|
|
1163
1163
|
if (err instanceof Error && err.name === 'ExitPromptError') {
|
|
1164
1164
|
process.exit(130);
|
|
1165
1165
|
}
|
|
1166
|
-
// Browser-
|
|
1166
|
+
// Browser-service-not-running and CDP-not-reachable surface as typed errors
|
|
1167
1167
|
// from src/lib/browser/. Don't dump a Node stacktrace for these — they are
|
|
1168
1168
|
// user-actionable, not engineering bugs. See issues #41 and #43.
|
|
1169
1169
|
if (err instanceof Error) {
|
|
1170
|
-
const
|
|
1170
|
+
const isBrowserServiceNotRunning = err.name === 'BrowserServiceNotRunningError';
|
|
1171
1171
|
const isBrowserCdpUnreachable = err.name === 'BrowserCdpConnectionError';
|
|
1172
1172
|
const isBrowserIpcDown = err.message.startsWith('IPC error:') &&
|
|
1173
1173
|
(err.message.includes('ECONNREFUSED') || err.message.includes('ENOENT'));
|
|
1174
|
-
if (
|
|
1174
|
+
if (isBrowserServiceNotRunning || isBrowserCdpUnreachable || isBrowserIpcDown) {
|
|
1175
1175
|
console.error(err.message);
|
|
1176
1176
|
process.exit(1);
|
|
1177
1177
|
}
|
package/dist/commands/apply.js
CHANGED
|
@@ -14,7 +14,7 @@ import chalk from 'chalk';
|
|
|
14
14
|
import { setHelpSections } from '../lib/help.js';
|
|
15
15
|
import { machineId } from '../lib/session/sync/config.js';
|
|
16
16
|
import { loadDevices } from '../lib/devices/registry.js';
|
|
17
|
-
import { isHostPinned, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
|
|
17
|
+
import { isHostPinned, isDevicePinned, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
|
|
18
18
|
import { ensureDevicesRegistered } from '../lib/devices/sync.js';
|
|
19
19
|
import { readFleetFile, resolveDesired, emptyTargetsMessage } from '../lib/fleet/manifest.js';
|
|
20
20
|
import { snapshotAuth } from '../lib/fleet/auth-sync.js';
|
|
@@ -233,7 +233,14 @@ async function runApply(opts) {
|
|
|
233
233
|
provisionSecrets: opts.provisionSecrets === true,
|
|
234
234
|
forceSecrets: opts.force === true,
|
|
235
235
|
// Portable secret values only ever go to a host whose key is already pinned.
|
|
236
|
-
|
|
236
|
+
// Resolve the device to its profile so the pin is checked against the FQDN the
|
|
237
|
+
// ssh connection dials (isDevicePinned), not the bare name — a tailnet worker
|
|
238
|
+
// pinned only under its FQDN would otherwise be refused as "not pinned"
|
|
239
|
+
// (PHNX-3505). A name with no profile falls back to the raw host-string check.
|
|
240
|
+
isHostPinned: (device) => {
|
|
241
|
+
const p = nameToProfile.get(device);
|
|
242
|
+
return p ? isDevicePinned(p) : isHostPinned(device, managedKnownHostsPath());
|
|
243
|
+
},
|
|
237
244
|
});
|
|
238
245
|
// --only filter.
|
|
239
246
|
if (opts.only) {
|
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())
|
package/dist/commands/exec.js
CHANGED
|
@@ -516,6 +516,7 @@ export function registerRunCommand(program) {
|
|
|
516
516
|
.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.')
|
|
517
517
|
.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 --device runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
|
|
518
518
|
.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).')
|
|
519
|
+
.option('--no-trace-sync', 'Skip the run-exit trace auto-sync for this run. Auto-sync fires by default only for local runs and only once you have run `agents traces sync` at least once (also silenced by AGENTS_NO_TRACE_SYNC=1).')
|
|
519
520
|
.option('--terminal [backend]', "Open this run in a real terminal tab instead of here. Without a value the terminal is detected from your live sessions (`agents sessions --active` host), so it lands where you already work — Ghostty for a Ghostty user, iTerm for an iTerm user. Name one to force it: iterm | ghostty | terminal | tmux | vscodium-agent. This is how the menu bar's New Session opens.")
|
|
520
521
|
.option('--verbose', 'Show detailed execution logs')
|
|
521
522
|
.option('--raw', 'Keep this interactive run direct when the device has opted into tmux wrapping. A no-op under the default tmux-off configuration; equivalent to AGENTS_NO_TMUX=1.')
|
|
@@ -1340,6 +1341,23 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1340
1341
|
// --device/--on/--computer: offload this run onto a registered agent host
|
|
1341
1342
|
// over SSH instead of running locally. The three flags are aliases.
|
|
1342
1343
|
const hostGiven = hostTargetGiven(options);
|
|
1344
|
+
// Auto-sync this run's redacted trajectory when it exits, so the console is
|
|
1345
|
+
// never stale and a session link/QR resolves the moment the run finishes
|
|
1346
|
+
// (PHNX-3628). Armed HERE — after hostGiven is finalized — so every
|
|
1347
|
+
// remote-placement signal is already reflected in options: --device/--on/
|
|
1348
|
+
// --computer and --where device: (expanded to options.host above), AND the
|
|
1349
|
+
// --resume-to-peer redirect that sets options.host during resume
|
|
1350
|
+
// resolution. A run that will dispatch remotely (hostGiven non-empty, or
|
|
1351
|
+
// --lease/--box) has its trajectory on that box and never arms a local
|
|
1352
|
+
// sync; --cloud returned earlier. The module additionally gates on opt-in
|
|
1353
|
+
// (signed in + already synced once) and is best-effort.
|
|
1354
|
+
if (prompt !== undefined &&
|
|
1355
|
+
hostGiven.length === 0 &&
|
|
1356
|
+
!options.lease &&
|
|
1357
|
+
!options.box) {
|
|
1358
|
+
const { armRunFinishTraceSync } = await import('../lib/run-trace-sync.js');
|
|
1359
|
+
armRunFinishTraceSync({ disabled: options.traceSync === false });
|
|
1360
|
+
}
|
|
1343
1361
|
// --project <slug>[@worktree]: resolve the projects-root shorthand into a
|
|
1344
1362
|
// cwd. On a host run it resolves home-relative (`~/…`, so the host expands
|
|
1345
1363
|
// it); locally it becomes an absolute path. It owns the working directory,
|
|
@@ -2337,6 +2355,28 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2337
2355
|
// differ from the earlier cwd that selected projects/<cwd-key>.
|
|
2338
2356
|
if (!options.cwd && resolvedRecoveryTarget.cwd)
|
|
2339
2357
|
options.cwd = resolvedRecoveryTarget.cwd;
|
|
2358
|
+
// Account rotation on a limit (PHNX-3626): recovery kept resume NATIVE
|
|
2359
|
+
// in the origin home but rotated to a healthy provider account of the
|
|
2360
|
+
// SAME harness. Inject that credential through the same `--account`
|
|
2361
|
+
// path an explicit selection uses, so exec authenticates as the
|
|
2362
|
+
// rotated account while reading the origin transcript. An explicit
|
|
2363
|
+
// --account (configuredAccount) always wins — never rotate over it.
|
|
2364
|
+
const rotatedAccount = resolvedRecoveryTarget.account;
|
|
2365
|
+
if (rotatedAccount && !configuredAccount) {
|
|
2366
|
+
try {
|
|
2367
|
+
const picked = resolveSpawnAccount(rotatedAccount.providerAccount, agent, version, readMeta(), { useDefault: false });
|
|
2368
|
+
if (picked?.kind === 'provider')
|
|
2369
|
+
accountEnv = picked.env;
|
|
2370
|
+
}
|
|
2371
|
+
catch {
|
|
2372
|
+
// Account-registry errors can carry provider credential material;
|
|
2373
|
+
// never relay them onto the terminal from this automatic path.
|
|
2374
|
+
console.error(chalk.red('Could not prepare the rotated provider account for native resume.'));
|
|
2375
|
+
process.exit(1);
|
|
2376
|
+
}
|
|
2377
|
+
if (!options.quiet)
|
|
2378
|
+
process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (native resume)\n`));
|
|
2379
|
+
}
|
|
2340
2380
|
if (!options.quiet)
|
|
2341
2381
|
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''} in ${options.cwd ?? cwd}\n`));
|
|
2342
2382
|
}
|
|
@@ -248,10 +248,10 @@ function ensureDaemonRunning() {
|
|
|
248
248
|
const result = startDaemon();
|
|
249
249
|
if (result.pid) {
|
|
250
250
|
console.log(chalk.green(`Daemon started (PID: ${result.pid}). It will watch monitors in the background.`));
|
|
251
|
-
console.log(chalk.gray('
|
|
251
|
+
console.log(chalk.gray('Disable only monitor polling with: agents daemon services disable monitors'));
|
|
252
252
|
}
|
|
253
253
|
else {
|
|
254
|
-
stderrLine(chalk.yellow('Could not start the daemon. Start it manually with: agents
|
|
254
|
+
stderrLine(chalk.yellow('Could not start the daemon. Start it manually with: agents daemon start'));
|
|
255
255
|
}
|
|
256
256
|
return true;
|
|
257
257
|
}
|
package/dist/commands/repo.js
CHANGED
|
@@ -57,6 +57,41 @@ function syncMarketplacesForDefaults() {
|
|
|
57
57
|
catch { /* best-effort */ }
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
/** Write this device's account metadata before the user repo is committed. */
|
|
61
|
+
async function publishUserRepoAccountState(provisionAuth) {
|
|
62
|
+
const { publishUsageSnapshotToSharedStore } = await import('../lib/accounting/usage-sync.js');
|
|
63
|
+
const usage = publishUsageSnapshotToSharedStore();
|
|
64
|
+
if (usage.error)
|
|
65
|
+
console.error(chalk.yellow(`Usage snapshot: ${usage.error}`));
|
|
66
|
+
if (provisionAuth) {
|
|
67
|
+
const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
|
|
68
|
+
const auth = await syncReservedAuthBundle();
|
|
69
|
+
if (auth.pushed.length > 0)
|
|
70
|
+
console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
|
|
71
|
+
for (const err of auth.errors)
|
|
72
|
+
console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const { publishReservedAuthVerdict } = await import('../lib/secrets/reserved-sync.js');
|
|
76
|
+
const auth = publishReservedAuthVerdict();
|
|
77
|
+
if (auth.error)
|
|
78
|
+
console.error(chalk.yellow(`Auth verdict: ${auth.device}: ${auth.error}`));
|
|
79
|
+
}
|
|
80
|
+
/** Consume freshly pulled usage/auth metadata from the local user-repo checkout. */
|
|
81
|
+
async function consumeUserRepoAccountState() {
|
|
82
|
+
const { consumeUsageSnapshotsFromSharedStore } = await import('../lib/accounting/usage-sync.js');
|
|
83
|
+
const usage = consumeUsageSnapshotsFromSharedStore();
|
|
84
|
+
if (usage.merged > 0)
|
|
85
|
+
console.log(chalk.gray(`Usage snapshot: merged ${usage.merged} row(s) from ${usage.sources.join(', ')}`));
|
|
86
|
+
for (const err of usage.errors)
|
|
87
|
+
console.error(chalk.yellow(`Usage snapshot: ${err.device}: ${err.message}`));
|
|
88
|
+
const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
|
|
89
|
+
const auth = await syncReservedAuthBundle();
|
|
90
|
+
if (auth.pushed.length > 0)
|
|
91
|
+
console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
|
|
92
|
+
for (const err of auth.errors)
|
|
93
|
+
console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
|
|
94
|
+
}
|
|
60
95
|
const ALIAS_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
61
96
|
/** Derive a default alias from a source URL (e.g. gh:foo/.agents-work -> agents-work). */
|
|
62
97
|
function deriveAlias(source) {
|
|
@@ -1052,14 +1087,7 @@ export function registerRepoCommands(program) {
|
|
|
1052
1087
|
].filter(Boolean);
|
|
1053
1088
|
if (parts.length > 0)
|
|
1054
1089
|
console.log(chalk.gray(`Device policy: ${parts.join(' · ')}`));
|
|
1055
|
-
|
|
1056
|
-
const auth = syncReservedAuthBundle();
|
|
1057
|
-
if (auth.pushed.length > 0) {
|
|
1058
|
-
console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
|
|
1059
|
-
}
|
|
1060
|
-
for (const err of auth.errors) {
|
|
1061
|
-
console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
|
|
1062
|
-
}
|
|
1090
|
+
await consumeUserRepoAccountState();
|
|
1063
1091
|
}
|
|
1064
1092
|
});
|
|
1065
1093
|
repoCmd
|
|
@@ -1095,20 +1123,14 @@ export function registerRepoCommands(program) {
|
|
|
1095
1123
|
return;
|
|
1096
1124
|
}
|
|
1097
1125
|
for (const t of pushable) {
|
|
1126
|
+
// Account snapshots must exist BEFORE commitAndPush or this push ships
|
|
1127
|
+
// the previous tick's state and delays propagation by one repo cycle.
|
|
1128
|
+
if (t.alias === 'user')
|
|
1129
|
+
await publishUserRepoAccountState(true);
|
|
1098
1130
|
const spinner = ora(`Pushing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1099
1131
|
const result = await commitAndPush(t.dir, options.message);
|
|
1100
1132
|
if (result.success) {
|
|
1101
1133
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.detail ?? 'pushed'}`);
|
|
1102
|
-
if (t.alias === 'user') {
|
|
1103
|
-
const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
|
|
1104
|
-
const auth = syncReservedAuthBundle();
|
|
1105
|
-
if (auth.pushed.length > 0) {
|
|
1106
|
-
console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
|
|
1107
|
-
}
|
|
1108
|
-
for (const err of auth.errors) {
|
|
1109
|
-
console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
1134
|
}
|
|
1113
1135
|
else {
|
|
1114
1136
|
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
|
|
@@ -1173,8 +1195,23 @@ Examples:
|
|
|
1173
1195
|
}
|
|
1174
1196
|
}
|
|
1175
1197
|
const push = t.alias !== 'system';
|
|
1198
|
+
// syncRepoGit pulls then pushes. Seed our conflict-free owner file first
|
|
1199
|
+
// so the same transaction carries it; consume peers after the pull.
|
|
1200
|
+
if (t.alias === 'user')
|
|
1201
|
+
await publishUserRepoAccountState(false);
|
|
1176
1202
|
const spinner = ora(`Syncing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1177
|
-
const result =
|
|
1203
|
+
const result = t.alias === 'user'
|
|
1204
|
+
? await (async () => {
|
|
1205
|
+
const { syncFleetSharedStateRepo } = await import('../lib/fleet-shared-repo-sync.js');
|
|
1206
|
+
const synced = await syncFleetSharedStateRepo({ userAgentsDir: t.dir });
|
|
1207
|
+
return {
|
|
1208
|
+
success: synced.success,
|
|
1209
|
+
commit: synced.commit ?? '',
|
|
1210
|
+
pushed: synced.pushed,
|
|
1211
|
+
error: synced.error ?? synced.skipped ?? undefined,
|
|
1212
|
+
};
|
|
1213
|
+
})()
|
|
1214
|
+
: await syncRepoGit(t.dir, { push });
|
|
1178
1215
|
if (result.success) {
|
|
1179
1216
|
const pushed = result.pushed ? ' (pushed)' : '';
|
|
1180
1217
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir)}: ${result.commit}${pushed}`);
|
|
@@ -1184,6 +1221,7 @@ Examples:
|
|
|
1184
1221
|
recordUserRepoRemote(t.dir, u);
|
|
1185
1222
|
const { reconcileDeviceDiscoveryPolicies } = await import('../lib/devices/discovery-policy.js');
|
|
1186
1223
|
await reconcileDeviceDiscoveryPolicies();
|
|
1224
|
+
await consumeUserRepoAccountState();
|
|
1187
1225
|
}
|
|
1188
1226
|
}
|
|
1189
1227
|
else {
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
+
import type { SessionMeta } from '../lib/session/types.js';
|
|
1
2
|
export declare const RESUME_SOURCE_ENV = "AGENTS_RESUME_SOURCE_JSON";
|
|
3
|
+
/**
|
|
4
|
+
* The source a dead-remote local fallback resumes from: the same session, but
|
|
5
|
+
* with `machine` rewritten to THIS box so the delegated `agents run --resume`
|
|
6
|
+
* resolves recovery locally (`sessionRecoveryPeer` returns undefined) instead of
|
|
7
|
+
* bouncing back to the unreachable owner. Because no local version home owns the
|
|
8
|
+
* peer's transcript, that local recovery lands on a labelled `/continue` replay
|
|
9
|
+
* from the synced mirror — the only way to continue a session whose owning device
|
|
10
|
+
* is gone. Owner-approved prefer-device, fall-back-local (PHNX-3626).
|
|
11
|
+
*
|
|
12
|
+
* Safe against the RUSH-2022 "silent local resume forks live state" hazard by
|
|
13
|
+
* PRECONDITION: this is reached only after `runOnPeer` proved the owner
|
|
14
|
+
* unreachable, so there is no live process on the peer to fork, and the fallback
|
|
15
|
+
* is announced with a log line — never silent.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resumeLocalFallbackSource(session: SessionMeta, self?: string): SessionMeta;
|
|
2
18
|
export interface StrictResumeOptions {
|
|
3
19
|
mode?: string;
|
|
4
20
|
interactive?: boolean;
|