@phnx-labs/agents-cli 1.22.66 → 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.
Files changed (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +7 -2
  3. package/dist/bootstrap.js +3 -3
  4. package/dist/commands/browser.js +46 -21
  5. package/dist/commands/daemon-test-harness.d.ts +1 -1
  6. package/dist/commands/daemon-test-harness.js +2 -2
  7. package/dist/commands/daemon.js +21 -22
  8. package/dist/commands/exec.js +40 -0
  9. package/dist/commands/monitors.js +2 -2
  10. package/dist/commands/repo.js +57 -19
  11. package/dist/commands/resume.d.ts +16 -0
  12. package/dist/commands/resume.js +41 -8
  13. package/dist/commands/routines.js +42 -21
  14. package/dist/commands/sessions-optimize.js +1 -1
  15. package/dist/commands/sessions.js +4 -11
  16. package/dist/commands/share.d.ts +5 -3
  17. package/dist/commands/share.js +33 -9
  18. package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
  19. package/dist/lib/accounting/account-pool-collect.js +6 -4
  20. package/dist/lib/accounting/usage-ingest.js +4 -2
  21. package/dist/lib/accounting/usage-sync.d.ts +38 -93
  22. package/dist/lib/accounting/usage-sync.js +66 -210
  23. package/dist/lib/accounting/usage.d.ts +11 -3
  24. package/dist/lib/accounting/usage.js +23 -10
  25. package/dist/lib/auth-health.d.ts +8 -0
  26. package/dist/lib/auth-health.js +4 -4
  27. package/dist/lib/browser/caller-identity.d.ts +12 -0
  28. package/dist/lib/browser/caller-identity.js +19 -0
  29. package/dist/lib/browser/ipc.d.ts +37 -32
  30. package/dist/lib/browser/ipc.js +146 -94
  31. package/dist/lib/browser/task-index.d.ts +10 -2
  32. package/dist/lib/browser/task-index.js +22 -3
  33. package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
  34. package/dist/lib/daemon/account-state-daemon-service.js +81 -18
  35. package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
  36. package/dist/lib/daemon/auth-sync-service.js +17 -6
  37. package/dist/lib/daemon/catchup-service.d.ts +51 -0
  38. package/dist/lib/daemon/catchup-service.js +51 -0
  39. package/dist/lib/daemon/daemon.d.ts +12 -22
  40. package/dist/lib/daemon/daemon.js +463 -176
  41. package/dist/lib/daemon/runner.js +2 -0
  42. package/dist/lib/daemon/service.d.ts +22 -4
  43. package/dist/lib/daemon/service.js +2 -2
  44. package/dist/lib/daemon/supervisor.d.ts +55 -15
  45. package/dist/lib/daemon/supervisor.js +119 -29
  46. package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
  47. package/dist/lib/daemon/usage-sync-service.js +22 -18
  48. package/dist/lib/daemon-health.js +36 -31
  49. package/dist/lib/daemon-services.d.ts +1 -1
  50. package/dist/lib/daemon-services.js +12 -2
  51. package/dist/lib/daemon-ticks.d.ts +9 -6
  52. package/dist/lib/daemon-ticks.js +14 -8
  53. package/dist/lib/devices/registry.js +2 -0
  54. package/dist/lib/exec.js +11 -0
  55. package/dist/lib/feed/watch.d.ts +3 -0
  56. package/dist/lib/feed/watch.js +13 -3
  57. package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
  58. package/dist/lib/fleet-shared-repo-sync.js +333 -0
  59. package/dist/lib/fleet-shared-state.d.ts +38 -0
  60. package/dist/lib/fleet-shared-state.js +105 -0
  61. package/dist/lib/hosts/remote-cmd.d.ts +2 -0
  62. package/dist/lib/hosts/remote-cmd.js +12 -3
  63. package/dist/lib/lock-compromise.d.ts +8 -0
  64. package/dist/lib/lock-compromise.js +12 -0
  65. package/dist/lib/monitors/engine.d.ts +2 -1
  66. package/dist/lib/monitors/engine.js +9 -2
  67. package/dist/lib/refresh-coordinator.js +2 -0
  68. package/dist/lib/run-trace-sync.d.ts +13 -0
  69. package/dist/lib/run-trace-sync.js +77 -0
  70. package/dist/lib/secrets/push.d.ts +10 -0
  71. package/dist/lib/secrets/push.js +86 -7
  72. package/dist/lib/secrets/remote.d.ts +18 -6
  73. package/dist/lib/secrets/remote.js +29 -4
  74. package/dist/lib/secrets/reserved-sync.d.ts +28 -27
  75. package/dist/lib/secrets/reserved-sync.js +119 -101
  76. package/dist/lib/session/active.d.ts +3 -1
  77. package/dist/lib/session/active.js +2 -0
  78. package/dist/lib/session/actor-sidecar.d.ts +12 -0
  79. package/dist/lib/session/actor-sidecar.js +2 -0
  80. package/dist/lib/session/db.d.ts +5 -2
  81. package/dist/lib/session/db.js +54 -22
  82. package/dist/lib/session/discover.d.ts +12 -3
  83. package/dist/lib/session/discover.js +106 -25
  84. package/dist/lib/session/prompt.d.ts +11 -0
  85. package/dist/lib/session/prompt.js +25 -0
  86. package/dist/lib/session/recovery.d.ts +30 -2
  87. package/dist/lib/session/recovery.js +56 -4
  88. package/dist/lib/session/remote/remote-list.d.ts +17 -1
  89. package/dist/lib/session/remote/remote-list.js +29 -4
  90. package/dist/lib/session/remote/watch.d.ts +25 -2
  91. package/dist/lib/session/remote/watch.js +185 -11
  92. package/dist/lib/session/session-cache.d.ts +2 -1
  93. package/dist/lib/session/session-cache.js +1 -0
  94. package/dist/lib/session/types.d.ts +2 -0
  95. package/dist/lib/share/publish.d.ts +55 -9
  96. package/dist/lib/share/publish.js +87 -9
  97. package/dist/lib/share/worker-template.js +100 -8
  98. package/dist/lib/ssh-exec.d.ts +2 -0
  99. package/dist/lib/ssh-exec.js +20 -4
  100. package/dist/lib/teams/registry.js +2 -0
  101. package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
  102. package/dist/lib/testdata/daemon-health-writer.js +8 -0
  103. package/dist/lib/traces/sync.d.ts +7 -0
  104. package/dist/lib/traces/sync.js +9 -0
  105. package/dist/lib/usage-refresh.d.ts +8 -2
  106. package/dist/lib/usage-refresh.js +3 -3
  107. package/package.json +1 -1
  108. package/dist/lib/account-state-service.d.ts +0 -21
  109. package/dist/lib/account-state-service.js +0 -60
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
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
+
3
29
  ## 1.22.66
4
30
 
5
31
  - **Fleet usage/auth sync now reaches FQDN-only-pinned workers (PHNX-3505).** The
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; scheduler and monitor engine also re-evaluate
1265
- on `SIGHUP reload`.
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-daemon-not-running and CDP-not-reachable surface as typed errors
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 isBrowserDaemonNotRunning = err.name === 'BrowserDaemonNotRunningError';
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 (isBrowserDaemonNotRunning || isBrowserCdpUnreachable || isBrowserIpcDown) {
1174
+ if (isBrowserServiceNotRunning || isBrowserCdpUnreachable || isBrowserIpcDown) {
1175
1175
  console.error(err.message);
1176
1176
  process.exit(1);
1177
1177
  }
@@ -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 { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest as sendRawIPCRequest, resetBrowserDaemon, } from '../lib/browser/ipc.js';
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
- const env = withActorEnv({ AGENTS_FLEET_REMOTE: '1' });
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 daemon socket warm for repeated actions
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 a wedged daemon ("Timeout waiting for browser daemon socket"):
328
- # stop it and clear a stale socket, then start clean
329
- agents browser stop --daemon
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
- if (isSelfHost(route.device) || !route.task)
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 daemon connection
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 (close browser + drop cached connection); with --daemon, stop the browser daemon and clear a wedged socket')
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('--daemon', 'Stop the browser daemon entirely and clear a stale/wedged socket so the next start comes up clean (recovery for "Timeout waiting for browser daemon socket")')
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('--daemon stops the whole browser daemon; do not combine it with --profile or --task.');
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 resetBrowserDaemon();
1726
+ const result = await stopBrowserService();
1704
1727
  const parts = [];
1705
- parts.push(result.wasRunning ? 'Stopped browser daemon' : 'Browser daemon was not running');
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
- console.log(`${parts.join('; ')}. It restarts on the next browser command.`);
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 BrowserDaemonNotRunningError) {
2137
- const message = formatBrowserDaemonNotRunningError();
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 `isDaemonRunProcess`'s `ps` check accepts it — see
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 `isDaemonRunProcess`'s `ps` check accepts it — see
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
- // isDaemonRunProcess check) is asked to see its real argv — mirrors
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');
@@ -297,9 +297,9 @@ async function probeSecretsBroker() {
297
297
  }
298
298
  async function probeBrowserIPC() {
299
299
  const record = readSubsystemHealth(SUBSYSTEM_BROWSER_IPC);
300
- const { isDaemonReachable, getSocketPath } = await import('../lib/browser/ipc.js');
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 isDaemonReachable();
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 the 10 supervisor-managed services: secrets-broker,
896
- browser-ipc, account-state, session-index, monitors (off only — turning
897
- monitors back on still needs a restart), watchdog, device-probe,
898
- self-heal, keychain-reap, and state-dir-check. The other 2 services
899
- (scheduler, webhook-receiver) are still bare interval/socket setups with
900
- no live start/stop hook, so a toggle there — and 'enable' on any service
901
- that was disabled at daemon boot — still needs 'agents daemon restart'.
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 was never registered on the
1024
- supervisor, so 'enable' on it — and any toggle/restart on the 2
1025
- services the supervisor doesn't manage yet (scheduler,
1026
- webhook-receiver) — falls back to 'restart the daemon to apply'. Each
1027
- row in the plain-text view names which case it is; 'supervised:
1028
- true/false' does the same in --json.
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 the 10 supervisor-managed services (secrets-broker,
1061
- * browser-ipc, account-state, session-index, monitors' off-transition,
1062
- * watchdog, device-probe, self-heal, keychain-reap, state-dir-check). The 2
1063
- * remaining `setInterval`/socket-driven services (scheduler,
1064
- * webhook-receiver) and monitors' on-transition still need a restart — the
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())
@@ -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('Stop anytime with: agents routines stop'));
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 routines start'));
254
+ stderrLine(chalk.yellow('Could not start the daemon. Start it manually with: agents daemon start'));
255
255
  }
256
256
  return true;
257
257
  }
@@ -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
- const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
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 = await syncRepoGit(t.dir, { push });
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;