@phnx-labs/agents-cli 1.22.117 → 1.22.118

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 (248) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/browser.d.ts +52 -70
  4. package/dist/commands/browser.js +192 -3562
  5. package/dist/commands/daemon.d.ts +39 -2
  6. package/dist/commands/daemon.js +72 -73
  7. package/dist/commands/events.js +1 -1
  8. package/dist/commands/exec.js +53 -1
  9. package/dist/commands/repo.js +13 -49
  10. package/dist/commands/route.js +5 -5
  11. package/dist/commands/routines.js +39 -12
  12. package/dist/commands/sessions-render.d.ts +1 -1
  13. package/dist/commands/sessions-render.js +0 -1
  14. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  15. package/dist/commands/sessions.test-fixture.js +0 -37
  16. package/dist/commands/setup-browser.d.ts +17 -12
  17. package/dist/commands/setup-browser.js +52 -87
  18. package/dist/commands/setup-preferences.d.ts +0 -17
  19. package/dist/commands/setup-preferences.js +18 -49
  20. package/dist/commands/setup.js +9 -9
  21. package/dist/commands/ssh.js +10 -1
  22. package/dist/commands/sync.js +6 -44
  23. package/dist/commands/teams.js +0 -1
  24. package/dist/commands/update.d.ts +19 -0
  25. package/dist/commands/update.js +34 -6
  26. package/dist/commands/view.d.ts +12 -1
  27. package/dist/commands/view.js +47 -2
  28. package/dist/index.js +107 -26
  29. package/dist/lib/account-capabilities.js +0 -2
  30. package/dist/lib/account-catalog.d.ts +38 -0
  31. package/dist/lib/account-catalog.js +110 -26
  32. package/dist/lib/account-provider-registry.js +1 -1
  33. package/dist/lib/account-registry.d.ts +1 -1
  34. package/dist/lib/account-registry.js +36 -2
  35. package/dist/lib/accounting/rotate.js +22 -8
  36. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  37. package/dist/lib/accounting/usage-ingest.js +115 -45
  38. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  39. package/dist/lib/accounting/usage-sync.js +227 -66
  40. package/dist/lib/accounting/usage.d.ts +16 -3
  41. package/dist/lib/accounting/usage.js +26 -8
  42. package/dist/lib/accounts/slots.d.ts +8 -0
  43. package/dist/lib/accounts/slots.js +17 -0
  44. package/dist/lib/add-dir.js +0 -1
  45. package/dist/lib/agent-cli-commands.js +0 -1
  46. package/dist/lib/agent-spec/agents.d.ts +6 -7
  47. package/dist/lib/agent-spec/agents.js +6 -60
  48. package/dist/lib/auth-health.d.ts +69 -1
  49. package/dist/lib/auth-health.js +133 -4
  50. package/dist/lib/browser/context.d.ts +82 -0
  51. package/dist/lib/browser/context.js +88 -0
  52. package/dist/lib/browser/paths.d.ts +25 -0
  53. package/dist/lib/browser/paths.js +51 -0
  54. package/dist/lib/browser/record.d.ts +38 -0
  55. package/dist/lib/browser/record.js +57 -0
  56. package/dist/lib/browser/sessions-list.js +1 -2
  57. package/dist/lib/browser-client.d.ts +158 -0
  58. package/dist/lib/browser-client.js +234 -0
  59. package/dist/lib/channels/owner-sink.d.ts +1 -18
  60. package/dist/lib/channels/owner-sink.js +29 -82
  61. package/dist/lib/channels/providers/rush.d.ts +7 -7
  62. package/dist/lib/channels/providers/rush.js +127 -57
  63. package/dist/lib/claude-statusline.d.ts +1 -1
  64. package/dist/lib/claude-statusline.js +15 -3
  65. package/dist/lib/commands.js +1 -1
  66. package/dist/lib/crabbox/lease.js +1 -1
  67. package/dist/lib/crabbox/runtimes.js +1 -3
  68. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  69. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  70. package/dist/lib/daemon/auth-sync-service.js +37 -43
  71. package/dist/lib/daemon/daemon.d.ts +25 -14
  72. package/dist/lib/daemon/daemon.js +92 -154
  73. package/dist/lib/daemon/runner.d.ts +2 -2
  74. package/dist/lib/daemon/runner.js +32 -9
  75. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  76. package/dist/lib/daemon/self-update-service.js +40 -36
  77. package/dist/lib/daemon/service.d.ts +12 -8
  78. package/dist/lib/daemon/supervisor.d.ts +65 -54
  79. package/dist/lib/daemon/supervisor.js +136 -171
  80. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  81. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  82. package/dist/lib/daemon/usage-sync-service.js +46 -78
  83. package/dist/lib/daemon-health.d.ts +22 -5
  84. package/dist/lib/daemon-health.js +47 -3
  85. package/dist/lib/daemon-services.d.ts +1 -1
  86. package/dist/lib/daemon-services.js +0 -10
  87. package/dist/lib/daemon-ticks.js +4 -2
  88. package/dist/lib/devices/doctor-findings.js +8 -11
  89. package/dist/lib/devices/harness-inventory.js +14 -5
  90. package/dist/lib/exec-account-home.js +5 -0
  91. package/dist/lib/exec.js +0 -11
  92. package/dist/lib/feed/events.js +0 -1
  93. package/dist/lib/feed/tool-activity.js +5 -2
  94. package/dist/lib/fleet-shared-state.d.ts +45 -0
  95. package/dist/lib/fleet-shared-state.js +78 -9
  96. package/dist/lib/fs-atomic.js +25 -0
  97. package/dist/lib/harness-auth-capabilities.js +0 -1
  98. package/dist/lib/helper-versions.js +1 -1
  99. package/dist/lib/hooks/cache.d.ts +7 -0
  100. package/dist/lib/hooks/cache.js +27 -2
  101. package/dist/lib/hooks/install.js +7 -1
  102. package/dist/lib/hosts/ready.d.ts +38 -8
  103. package/dist/lib/hosts/ready.js +62 -14
  104. package/dist/lib/installations/active-check.d.ts +27 -0
  105. package/dist/lib/installations/active-check.js +61 -0
  106. package/dist/lib/installations/migrate.js +6 -3
  107. package/dist/lib/installations/shims.js +9 -7
  108. package/dist/lib/installations/update.js +21 -15
  109. package/dist/lib/installations/versions.js +12 -1
  110. package/dist/lib/manifest.js +1 -1
  111. package/dist/lib/models.js +0 -2
  112. package/dist/lib/open-url.js +28 -54
  113. package/dist/lib/profiles.js +0 -2
  114. package/dist/lib/refresh.js +1 -1
  115. package/dist/lib/routine-readiness.d.ts +35 -0
  116. package/dist/lib/routine-readiness.js +74 -16
  117. package/dist/lib/rules/compile.js +6 -0
  118. package/dist/lib/sandbox.js +0 -2
  119. package/dist/lib/secrets-policy.d.ts +89 -11
  120. package/dist/lib/secrets-policy.js +175 -28
  121. package/dist/lib/session/db.d.ts +1 -1
  122. package/dist/lib/session/db.js +65 -18
  123. package/dist/lib/session/discover.js +1 -283
  124. package/dist/lib/session/mirror.d.ts +4 -3
  125. package/dist/lib/session/mirror.js +4 -3
  126. package/dist/lib/session/parse.d.ts +0 -2
  127. package/dist/lib/session/parse.js +1 -138
  128. package/dist/lib/session/throughput.d.ts +1 -3
  129. package/dist/lib/session/throughput.js +0 -20
  130. package/dist/lib/session/types.d.ts +2 -2
  131. package/dist/lib/session/types.js +2 -2
  132. package/dist/lib/sessions-client.d.ts +60 -1
  133. package/dist/lib/sessions-client.js +227 -4
  134. package/dist/lib/setup-tool-status.js +5 -10
  135. package/dist/lib/signin-badge.d.ts +3 -2
  136. package/dist/lib/signin-badge.js +6 -3
  137. package/dist/lib/smart-launch.js +4 -1
  138. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  139. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  140. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  141. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  142. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  143. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  144. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  147. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  148. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  149. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  150. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  151. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  152. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  153. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  154. package/dist/lib/staleness/writers/sources.js +5 -0
  155. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  156. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  157. package/dist/lib/state.d.ts +6 -4
  158. package/dist/lib/state.js +7 -17
  159. package/dist/lib/teams/parsers.d.ts +1 -1
  160. package/dist/lib/teams/parsers.js +1 -136
  161. package/dist/lib/teams/placement-probe.js +50 -15
  162. package/dist/lib/teams/scheduler.d.ts +7 -0
  163. package/dist/lib/types.d.ts +10 -1
  164. package/dist/lib/types.js +1 -1
  165. package/dist/lib/view-types.d.ts +32 -0
  166. package/dist/lib/watchdog/read.js +0 -1
  167. package/dist/lib/watchdog/rotate.js +1 -2
  168. package/dist/lib/watchdog/runner.js +2 -2
  169. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  170. package/dist/lib/watchdog/watchdogTail.js +0 -31
  171. package/package.json +1 -1
  172. package/scripts/postinstall.js +8 -8
  173. package/dist/browser.d.ts +0 -2
  174. package/dist/browser.js +0 -17
  175. package/dist/commands/browser-picker.d.ts +0 -18
  176. package/dist/commands/browser-picker.js +0 -97
  177. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  178. package/dist/lib/browser/arc-discovery.js +0 -197
  179. package/dist/lib/browser/arc-dom.d.ts +0 -14
  180. package/dist/lib/browser/arc-dom.js +0 -121
  181. package/dist/lib/browser/caller-identity.d.ts +0 -40
  182. package/dist/lib/browser/caller-identity.js +0 -175
  183. package/dist/lib/browser/cdp.d.ts +0 -51
  184. package/dist/lib/browser/cdp.js +0 -254
  185. package/dist/lib/browser/chrome.d.ts +0 -141
  186. package/dist/lib/browser/chrome.js +0 -704
  187. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  188. package/dist/lib/browser/chromium-discovery.js +0 -96
  189. package/dist/lib/browser/devices.d.ts +0 -34
  190. package/dist/lib/browser/devices.js +0 -61
  191. package/dist/lib/browser/domain-skills.d.ts +0 -71
  192. package/dist/lib/browser/domain-skills.js +0 -195
  193. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  194. package/dist/lib/browser/drivers/arc.js +0 -276
  195. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  196. package/dist/lib/browser/drivers/firefox.js +0 -377
  197. package/dist/lib/browser/drivers/local.d.ts +0 -55
  198. package/dist/lib/browser/drivers/local.js +0 -266
  199. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  200. package/dist/lib/browser/drivers/ssh.js +0 -467
  201. package/dist/lib/browser/editor.d.ts +0 -3
  202. package/dist/lib/browser/editor.js +0 -50
  203. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  204. package/dist/lib/browser/ffmpeg.js +0 -184
  205. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  206. package/dist/lib/browser/firefox-discovery.js +0 -162
  207. package/dist/lib/browser/har.d.ts +0 -85
  208. package/dist/lib/browser/har.js +0 -77
  209. package/dist/lib/browser/hygiene.d.ts +0 -97
  210. package/dist/lib/browser/hygiene.js +0 -153
  211. package/dist/lib/browser/index.d.ts +0 -5
  212. package/dist/lib/browser/index.js +0 -5
  213. package/dist/lib/browser/input.d.ts +0 -7
  214. package/dist/lib/browser/input.js +0 -92
  215. package/dist/lib/browser/ipc.d.ts +0 -204
  216. package/dist/lib/browser/ipc.js +0 -1453
  217. package/dist/lib/browser/login-detection.d.ts +0 -87
  218. package/dist/lib/browser/login-detection.js +0 -274
  219. package/dist/lib/browser/profiles.d.ts +0 -422
  220. package/dist/lib/browser/profiles.js +0 -1157
  221. package/dist/lib/browser/refs.d.ts +0 -89
  222. package/dist/lib/browser/refs.js +0 -191
  223. package/dist/lib/browser/registry.d.ts +0 -70
  224. package/dist/lib/browser/registry.js +0 -252
  225. package/dist/lib/browser/remote-control.d.ts +0 -59
  226. package/dist/lib/browser/remote-control.js +0 -85
  227. package/dist/lib/browser/resolve-target.d.ts +0 -66
  228. package/dist/lib/browser/resolve-target.js +0 -264
  229. package/dist/lib/browser/runtime-state.d.ts +0 -291
  230. package/dist/lib/browser/runtime-state.js +0 -584
  231. package/dist/lib/browser/secret-ref.d.ts +0 -10
  232. package/dist/lib/browser/secret-ref.js +0 -14
  233. package/dist/lib/browser/service.d.ts +0 -812
  234. package/dist/lib/browser/service.js +0 -4693
  235. package/dist/lib/browser/stream.d.ts +0 -17
  236. package/dist/lib/browser/stream.js +0 -72
  237. package/dist/lib/browser/task-index.d.ts +0 -78
  238. package/dist/lib/browser/task-index.js +0 -199
  239. package/dist/lib/browser/types.d.ts +0 -647
  240. package/dist/lib/browser/types.js +0 -61
  241. package/dist/lib/browser/upload.d.ts +0 -29
  242. package/dist/lib/browser/upload.js +0 -298
  243. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  244. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  245. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  246. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  247. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  248. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -6,7 +6,7 @@ import { getBinaryPath, invalidateInstalledVersionsCache, invalidateLiveVersionC
6
6
  import { assertValidRelease, selectUpdateStrategy, } from './strategies.js';
7
7
  import { listInstallations, readInstallation, recordRelease, writeInstallation } from './store.js';
8
8
  import { effectiveUpdatePolicy, isAutoUpdateEnabledForAgent } from './update-policy.js';
9
- import { isInstallationLikelyActive } from './active-check.js';
9
+ import { describeInstallationActivity, formatInUseDeferral } from './active-check.js';
10
10
  import { installationLockTarget, INSTALLATION_LOCK_OPTIONS } from './installation-lock.js';
11
11
  /**
12
12
  * Move one frozen installation to a new vendor release, preserving its identity.
@@ -88,18 +88,21 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
88
88
  return { installation, strategy: strategy.id, fromRelease: installation.releaseVersion, toRelease: target,
89
89
  unchanged: true, deferred: 'Update cancelled or automatic update policy changed.', alsoUpdated: [] };
90
90
  }
91
- if (strategy.transactional && await isInstallationLikelyActive(installation)) {
92
- options.onProgress?.(`${AGENTS[agent].name}@${installation.label} looks active right now (a process or launch lease); `
93
- + `not staging release ${target}.`);
94
- return {
95
- installation,
96
- strategy: strategy.id,
97
- fromRelease: installation.releaseVersion,
98
- toRelease: target,
99
- unchanged: true,
100
- deferred: 'Account home is in use; retry after its sessions finish.',
101
- alsoUpdated: [],
102
- };
91
+ if (strategy.transactional) {
92
+ const activity = await describeInstallationActivity(installation);
93
+ if (activity.active) {
94
+ const name = `${AGENTS[agent].name}@${installation.label}`;
95
+ options.onProgress?.(`${name} looks active right now; not staging release ${target}.`);
96
+ return {
97
+ installation,
98
+ strategy: strategy.id,
99
+ fromRelease: installation.releaseVersion,
100
+ toRelease: target,
101
+ unchanged: true,
102
+ deferred: formatInUseDeferral(name, activity),
103
+ alsoUpdated: [],
104
+ };
105
+ }
103
106
  }
104
107
  let staged = null;
105
108
  try {
@@ -179,7 +182,8 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
179
182
  // identical pre-stage check above for why this cannot be opt-in: a launch
180
183
  // that starts AFTER that first check but before this swap is exactly the
181
184
  // window this closes.
182
- if (options.shouldCancel?.() || (strategy.transactional && await isInstallationLikelyActive(installation))) {
185
+ const lateActivity = strategy.transactional ? await describeInstallationActivity(installation) : null;
186
+ if (options.shouldCancel?.() || lateActivity?.active) {
183
187
  options.onProgress?.(`${AGENTS[agent].name}@${installation.label} looks active now (a process or launch lease appeared while `
184
188
  + `${staged.release} was staging); not committing it.`);
185
189
  return {
@@ -188,7 +192,9 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
188
192
  fromRelease: installation.releaseVersion,
189
193
  toRelease: staged.release,
190
194
  unchanged: true,
191
- deferred: 'Update cancelled or the account home became active.',
195
+ deferred: lateActivity?.active
196
+ ? formatInUseDeferral(`${AGENTS[agent].name}@${installation.label}`, lateActivity)
197
+ : 'Update cancelled while the release was being prepared.',
192
198
  alsoUpdated: [],
193
199
  };
194
200
  }
@@ -28,7 +28,7 @@ import { INSTALLATION_RECORD_FILE } from './types.js';
28
28
  import { composeWin32CommandLine } from '../platform/index.js';
29
29
  import { listInstalledSubagents } from '../subagents.js';
30
30
  import { listInstalledWorkflows } from '../workflows.js';
31
- import { parseHookManifest, registerHooksToSettings, selectHookManifest, pruneVersionHomeHookEntriesFromSettings, installSessionTrackerHookSync, installSessionTrackerHook } from '../hooks/install.js';
31
+ import { parseHookManifest, registerHooksToSettings, selectHookManifest, pruneVersionHomeHookEntriesFromSettings, installSessionTrackerHookSync, installSessionTrackerHook, repairManagedHookRuntimeArtifacts } from '../hooks/install.js';
32
32
  import { supports, explainSkip } from '../capabilities.js';
33
33
  import { discoverPlugins, syncPluginToVersion, pluginSupportsAgent, cleanOrphanedPluginSkills, marketplaceSpecForName } from '../plugins/plugins.js';
34
34
  import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from '../staleness/index.js';
@@ -1630,6 +1630,17 @@ export function removeVersion(agent, version) {
1630
1630
  pruneVersionHomeHookEntriesFromSettings(settingsPath, agent, version);
1631
1631
  }
1632
1632
  }
1633
+ // The global hook shims embed one SOURCE path. When it pointed into this
1634
+ // version home every shimmed guard would exit 127 (allow) until a self-heal
1635
+ // pass ran, and that pass needs a live daemon. Re-point them now. Best
1636
+ // effort: the removal itself already succeeded, and a repair failure on an
1637
+ // unrelated harness's shim must not abort a bulk removal.
1638
+ try {
1639
+ repairManagedHookRuntimeArtifacts();
1640
+ }
1641
+ catch (err) {
1642
+ console.warn(`hook shim repair after removing ${agent}@${version} failed: ${err instanceof Error ? err.message : String(err)}`);
1643
+ }
1633
1644
  emit('version.remove', { agent, version });
1634
1645
  return true;
1635
1646
  }
@@ -125,7 +125,7 @@ export function createDefaultManifest() {
125
125
  defaults: {
126
126
  method: 'symlink',
127
127
  scope: 'global',
128
- agents: ['claude', 'codex', 'gemini', 'cursor', 'opencode'],
128
+ agents: ['claude', 'codex', 'cursor', 'opencode'],
129
129
  },
130
130
  };
131
131
  }
@@ -113,8 +113,6 @@ export function locateModelSource(agent, version) {
113
113
  }
114
114
  return null;
115
115
  }
116
- // gemini is hard-deprecated (no launch path left to validate a model
117
- // against), so its bundle is deliberately not parsed for a catalog here.
118
116
  if (agent === 'opencode') {
119
117
  // The `opencode` shim under node_modules/.bin dispatches to a platform-
120
118
  // specific native binary. We don't parse the 100MB binary; we let the CLI
@@ -101,68 +101,42 @@ export async function resolveViewer(opts = {}) {
101
101
  if (opts.osBrowser)
102
102
  return 'os';
103
103
  const { getConfigValue } = await import('./device-config.js');
104
- const configured = opts.profile ?? (getConfigValue('browser.viewer').value || undefined);
105
- // Unset means "follow the profile agents drive" — the whole point is that a
106
- // machine with a configured browser stops leaking pages to the OS handler.
107
- const { getConfiguredDefaultProfileName, resolveProfileRef, getProfile, isProfileLaunchableHere } = await import('./browser/profiles.js');
108
- const name = configured ?? getConfiguredDefaultProfileName();
109
- if (!name)
104
+ // A configured viewer, else the profile agents drive (`browser.profile`) — the
105
+ // whole point is that a machine with a configured browser stops leaking pages
106
+ // to the OS handler. Both keys are shared with the standalone `browser` CLI,
107
+ // whose `show` verb applies exactly this precedence itself; agents-cli resolves
108
+ // the NAME only to decide viewer-vs-OS, then delegates suitability (Arc/Firefox/
109
+ // launchable-here) to the engine (PHNX-4101).
110
+ const name = opts.profile
111
+ ?? (getConfigValue('browser.viewer').value || undefined)
112
+ ?? (getConfigValue('browser.profile').value || undefined);
113
+ if (!name || name === 'os')
110
114
  return 'os';
111
- if (name === 'os')
112
- return 'os';
113
- let resolved;
114
- try {
115
- resolved = await resolveProfileRef(name);
116
- }
117
- catch (err) {
118
- console.error(`[viewer] ${name}: ${err instanceof Error ? err.message : String(err)} — using the OS browser.`);
119
- return 'os';
120
- }
121
- if (!resolved) {
122
- console.error(`[viewer] "${name}" does not resolve to a profile — using the OS browser.`);
123
- return 'os';
124
- }
125
- const profile = await getProfile(resolved);
126
- if (!profile) {
127
- console.error(`[viewer] profile "${resolved}" is not configured — using the OS browser.`);
128
- return 'os';
129
- }
130
- if (profile.browser === 'arc') {
131
- // agents browser can drive an attached Arc's existing tabs, but showing the
132
- // user a page needs its OWN fresh tab, and Arc crashes on tab creation over
133
- // CDP (Target.createTarget). So Arc is a fine automation profile but never a
134
- // viewer — fall back to the OS browser for anything a human is meant to read.
135
- console.error(`[viewer] "${resolved}" is Arc, which cannot open a new viewer tab — using the OS browser.`);
136
- return 'os';
137
- }
138
- if (profile.browser === 'firefox') {
139
- // agents drives Firefox headless over BiDi for its own read-back; a page a
140
- // human is meant to read goes to the visible OS browser, not the headless one.
141
- console.error(`[viewer] "${resolved}" is a headless-automation Firefox profile — using the OS browser.`);
142
- return 'os';
143
- }
144
- if (!isProfileLaunchableHere(profile)) {
145
- console.error(`[viewer] "${resolved}" cannot launch on this machine — using the OS browser.`);
146
- return 'os';
147
- }
148
- return { profile: resolved };
115
+ return { profile: name };
149
116
  }
150
117
  /** Show a URL to the human at this machine. Never throws. */
151
118
  export async function showUrl(url, opts = {}) {
152
119
  const viewer = await resolveViewer(opts);
153
120
  if (viewer === 'os')
154
121
  return osOpen(url, opts.spawnOpen);
122
+ // Delegate to the standalone `browser show`, which opens the URL in the viewer
123
+ // profile (or `browser.viewer`/`browser.profile`) and owns its own service
124
+ // lifecycle and viewer-suitability fallbacks. When the engine is not installed,
125
+ // or the call fails, degrade to the OS handler with one stderr line.
155
126
  try {
156
- const { sendIPCRequest } = await import('./browser/ipc.js');
157
- // Deliberately does NOT auto-start the browser daemon. Showing a page is a
158
- // side errand — `devices lease` opens a console and immediately prompts for
159
- // a pasted key — so blocking that on a daemon cold start is a surprising
160
- // multi-second stall. Daemon already up: use the viewer. Not up: the OS
161
- // handler is the fast, correct answer.
162
- const response = await sendIPCRequest({ action: 'show', url, profile: viewer.profile }, { autoStartDaemon: false });
163
- if (response.ok)
164
- return { via: 'profile', profile: viewer.profile, tabId: response.tabId };
165
- console.error(`[viewer] ${viewer.profile}: ${response.error} — using the OS browser.`);
127
+ const { browserInstalled, runBrowser } = await import('./browser-client.js');
128
+ if (!browserInstalled()) {
129
+ console.error(`[viewer] the standalone browser CLI is not installed — using the OS browser.`);
130
+ return osOpen(url, opts.spawnOpen);
131
+ }
132
+ const { buildBrowserContext } = await import('./browser/context.js');
133
+ const { exitCode } = await runBrowser({
134
+ argv: ['show', url, '--profile', viewer.profile],
135
+ context: await buildBrowserContext(),
136
+ });
137
+ if (exitCode === 0)
138
+ return { via: 'profile', profile: viewer.profile };
139
+ console.error(`[viewer] ${viewer.profile}: browser show exited ${exitCode} — using the OS browser.`);
166
140
  }
167
141
  catch (err) {
168
142
  console.error(`[viewer] ${viewer.profile}: ${err instanceof Error ? err.message : String(err)} — using the OS browser.`);
@@ -352,7 +352,6 @@ const MODEL_ENV_KEY_BY_HOST = {
352
352
  claude: 'ANTHROPIC_MODEL',
353
353
  opencode: 'OPENCODE_MODEL',
354
354
  grok: 'GROK_MODEL',
355
- gemini: 'GEMINI_MODEL',
356
355
  codex: 'OPENAI_MODEL',
357
356
  };
358
357
  /** Base-URL env var per host, for the OpenAI/Anthropic-compatible hosts where it applies. */
@@ -372,7 +371,6 @@ export function baseUrlEnvKeyForHost(host) {
372
371
  const AUTH_ENV_KEY_BY_HOST = {
373
372
  claude: 'ANTHROPIC_AUTH_TOKEN',
374
373
  codex: 'OPENAI_API_KEY',
375
- gemini: 'GEMINI_API_KEY',
376
374
  grok: 'XAI_API_KEY',
377
375
  opencode: 'OPENCODE_API_KEY',
378
376
  };
@@ -32,7 +32,7 @@ function migratePromptcutsToRoot(agentsDir, quiet = false) {
32
32
  const rootPath = path.join(agentsDir, 'promptcuts.yaml');
33
33
  if (fs.existsSync(rootPath))
34
34
  return;
35
- const agentDirs = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
35
+ const agentDirs = ['claude', 'codex', 'cursor', 'opencode'];
36
36
  for (const dir of agentDirs) {
37
37
  const legacyPath = path.join(agentsDir, dir, 'promptcuts.yaml');
38
38
  if (fs.existsSync(legacyPath)) {
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import type { JobConfig } from './scheduling/routines.js';
13
13
  import { type RoutineReadinessResult, type RoutineReadiness } from './routine-context.js';
14
+ import { type AuthVerdict } from './auth-health.js';
14
15
  /**
15
16
  * Fire-time auth preflight: read the daemon-warmed auth-health cache for the
16
17
  * exact (agent, version) a routine has resolved to run, and return an
@@ -48,6 +49,40 @@ export declare function parseRemoteProjectSnapshot(stdout: string): {
48
49
  export declare function buildRemoteWorkspaceProbe(cwd: string, windows: boolean): string;
49
50
  /** A successful probe must contain the exact requested postcondition, not merely exit zero. */
50
51
  export declare function probeOutputHasSentinel(stdout: string, sentinel: string): boolean;
52
+ /**
53
+ * ONE decision the local ({@link evaluateActivationReadinessLive}) and host
54
+ * ({@link evaluateHostActivationReadiness}) readiness paths share, so both reach
55
+ * the SAME answer for the same box (PHNX-4116). `row` is that box's probe verdict
56
+ * for the routine's agent — ABSENT when the box could not probe (a worker's
57
+ * setup-token box, or a headed box that spent its hourly usage-endpoint budget,
58
+ * both drop `no_evidence` in `probeLocalFleetAuth`). `launchable` is whether the
59
+ * box holds a launchable signed-in credential, from the SAME
60
+ * `collectRunCandidates`/`isLaunchableSignedIn` the run router uses — so a
61
+ * token-backed routine on a box that cannot probe stays activatable, a box with
62
+ * no credential at all fails `unconfigured`, and a server rejection (`revoked` —
63
+ * the only present-but-not-accepted verdict these force-probed paths produce)
64
+ * blocks regardless of a token on disk.
65
+ */
66
+ export declare function decideRoutineAuthReadiness(row: {
67
+ verdict: AuthVerdict;
68
+ } | undefined, launchable: boolean): {
69
+ ok: boolean;
70
+ reason?: string;
71
+ };
72
+ /**
73
+ * Decide a host-placed routine's auth readiness from the REMOTE box's `devices
74
+ * ping --local --json` payload (PHNX-4116). Pure over the payload TEXT so it is
75
+ * unit-tested without SSH: it finds this agent's probe row (ABSENT when the box
76
+ * dropped its `no_evidence` row) and reads the box's per-agent launchability the
77
+ * ping now carries, then runs {@link decideRoutineAuthReadiness} — so a host box
78
+ * that could not probe but holds a token is ready, exactly as the local path
79
+ * decides for the same box. Returns null when the payload cannot be parsed; the
80
+ * caller treats that as a probe failure, never as "no credential".
81
+ */
82
+ export declare function decideHostAuthFromPing(stdout: string, agent: string): {
83
+ ok: boolean;
84
+ reason?: string;
85
+ } | null;
51
86
  /**
52
87
  * Interactive setup/repair readiness. Unlike the scheduler's deterministic
53
88
  * structural gate, this completes a real local auth request and reads Codex's
@@ -162,6 +162,62 @@ function codexWorkspaceTrusted(version, cwd) {
162
162
  return false;
163
163
  }
164
164
  }
165
+ /**
166
+ * Verdicts a routine's activation auth check accepts as usable. `no_evidence` is
167
+ * here for robustness (a payload that still carries one), but a box that cannot
168
+ * probe DROPS it in {@link probeLocalFleetAuth}, so the real path for those boxes
169
+ * is the absent-row + launchability fallback in {@link decideRoutineAuthReadiness}
170
+ * (PHNX-4116).
171
+ */
172
+ const ROUTINE_AUTH_ACCEPTED = new Set([
173
+ 'live', 'rate_limited', 'unverified', 'no_evidence',
174
+ ]);
175
+ /**
176
+ * ONE decision the local ({@link evaluateActivationReadinessLive}) and host
177
+ * ({@link evaluateHostActivationReadiness}) readiness paths share, so both reach
178
+ * the SAME answer for the same box (PHNX-4116). `row` is that box's probe verdict
179
+ * for the routine's agent — ABSENT when the box could not probe (a worker's
180
+ * setup-token box, or a headed box that spent its hourly usage-endpoint budget,
181
+ * both drop `no_evidence` in `probeLocalFleetAuth`). `launchable` is whether the
182
+ * box holds a launchable signed-in credential, from the SAME
183
+ * `collectRunCandidates`/`isLaunchableSignedIn` the run router uses — so a
184
+ * token-backed routine on a box that cannot probe stays activatable, a box with
185
+ * no credential at all fails `unconfigured`, and a server rejection (`revoked` —
186
+ * the only present-but-not-accepted verdict these force-probed paths produce)
187
+ * blocks regardless of a token on disk.
188
+ */
189
+ export function decideRoutineAuthReadiness(row, launchable) {
190
+ if (row && ROUTINE_AUTH_ACCEPTED.has(row.verdict))
191
+ return { ok: true };
192
+ if (!row)
193
+ return launchable ? { ok: true } : { ok: false, reason: 'unconfigured' };
194
+ return { ok: false, reason: row.verdict };
195
+ }
196
+ /**
197
+ * Decide a host-placed routine's auth readiness from the REMOTE box's `devices
198
+ * ping --local --json` payload (PHNX-4116). Pure over the payload TEXT so it is
199
+ * unit-tested without SSH: it finds this agent's probe row (ABSENT when the box
200
+ * dropped its `no_evidence` row) and reads the box's per-agent launchability the
201
+ * ping now carries, then runs {@link decideRoutineAuthReadiness} — so a host box
202
+ * that could not probe but holds a token is ready, exactly as the local path
203
+ * decides for the same box. Returns null when the payload cannot be parsed; the
204
+ * caller treats that as a probe failure, never as "no credential".
205
+ */
206
+ export function decideHostAuthFromPing(stdout, agent) {
207
+ let payload;
208
+ try {
209
+ payload = JSON.parse(stdout);
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ if (!payload || typeof payload !== 'object')
215
+ return null;
216
+ const { rows, launchable } = payload;
217
+ const verdict = rows?.find((row) => row.agent === agent)?.health?.verdict;
218
+ const row = verdict ? { verdict } : undefined;
219
+ return decideRoutineAuthReadiness(row, launchable?.includes(agent) ?? false);
220
+ }
165
221
  /**
166
222
  * Interactive setup/repair readiness. Unlike the scheduler's deterministic
167
223
  * structural gate, this completes a real local auth request and reads Codex's
@@ -180,13 +236,14 @@ export async function evaluateActivationReadinessLive(config) {
180
236
  if (!version)
181
237
  return structural;
182
238
  const context = resolveJobExecutionContext(config, { mode: 'local' });
183
- let authVerdict;
239
+ // The probe row (absent when this box could not probe — a worker's setup-token
240
+ // box drops `no_evidence`) plus the run-router launchability, run through the
241
+ // ONE decision the host path also uses so both agree for the same box.
184
242
  const rows = await probeLocalFleetAuth({ agents: [config.agent] });
185
243
  const row = rows.find((candidate) => candidate.version === version);
186
- const accepted = new Set(['live', 'rate_limited', 'unverified']);
187
- authVerdict = row && accepted.has(row.health.verdict)
188
- ? { ok: true }
189
- : { ok: false, reason: row?.health.verdict ?? 'unconfigured' };
244
+ const { collectRunCandidates } = await import('./accounting/rotate.js');
245
+ const launchable = (await collectRunCandidates(config.agent)).some((candidate) => candidate.signedIn);
246
+ const authVerdict = decideRoutineAuthReadiness(row?.health, launchable);
190
247
  return evaluateRoutineReadiness(context, {
191
248
  agentInstalled: () => true,
192
249
  ...(config.agent === 'codex' && context.absoluteCwd
@@ -259,19 +316,20 @@ export async function evaluateHostActivationReadiness(config) {
259
316
  ? `powershell -NoProfile -EncodedCommand ${encodePowershell(`${POWERSHELL_PROGRESS_SILENCE}; & agents ${pingArgs.map(powershellQuote).join(' ')}`)}`
260
317
  : `agents ${pingArgs.map(shellQuote).join(' ')}`;
261
318
  const probe = sshExec(target, command, { timeoutMs: 30_000, extraSshArgs: identity });
262
- let verdict = 'error';
263
- if (probe.code === 0) {
264
- try {
265
- const payload = JSON.parse(probe.stdout);
266
- verdict = payload.rows?.find((row) => row.agent === config.agent)?.health.verdict ?? 'unconfigured';
267
- }
268
- catch {
269
- verdict = 'error';
270
- }
319
+ // A ping that could not run or parse is a probe FAILURE, never mistaken for
320
+ // "no credential": block with the failure so the operator sees it. A parsed
321
+ // payload runs the shared decision — an ABSENT row (the remote dropped its
322
+ // `no_evidence`) plus the launchability the ping now carries is ready,
323
+ // exactly as the local path decides for the same box (PHNX-4116).
324
+ const decision = probe.code === 0 ? decideHostAuthFromPing(probe.stdout, config.agent) : null;
325
+ if (!decision) {
326
+ return evaluateRoutineReadiness(unprobed, {
327
+ authOk: () => ({ ok: false, reason: `devices ping --local failed on ${host.name}` }),
328
+ }, { agent: config.agent });
271
329
  }
272
- if (!new Set(['live', 'rate_limited', 'unverified']).has(verdict)) {
330
+ if (!decision.ok) {
273
331
  return evaluateRoutineReadiness(unprobed, {
274
- authOk: () => ({ ok: false, reason: verdict }),
332
+ authOk: () => decision,
275
333
  }, { agent: config.agent });
276
334
  }
277
335
  }
@@ -303,6 +303,12 @@ export function compileRulesForProject(cwd, opts = {}) {
303
303
  const fname = agent.instructionsFile;
304
304
  if (seen.has(fname))
305
305
  continue;
306
+ // Hard-deprecated agents (e.g. Gemini, retired by Google) never get an
307
+ // instruction-file symlink — the harness is gone, so GEMINI.md would only
308
+ // litter the tree. Mirrors the deprecated?.hard skip used everywhere else
309
+ // (capabilities.ts, MANAGED_AGENT_IDS, modes.ts).
310
+ if (agent.deprecated?.hard)
311
+ continue;
306
312
  // Skip agents whose instructions live at a nested path (e.g. OpenClaw's
307
313
  // workspace/AGENTS.md) — those are managed by their own setup paths.
308
314
  if (fname.includes('/') || fname.includes('\\'))
@@ -137,8 +137,6 @@ export function prepareJobHome(config, version) {
137
137
  const overlayHome = getJobHomePath(config.name);
138
138
  cleanJobHome(config.name);
139
139
  fs.mkdirSync(overlayHome, { recursive: true });
140
- // gemini is hard-deprecated: runner.ts rejects a gemini routine before it
141
- // ever reaches prepareJobHome, so there is deliberately no branch here.
142
140
  if (config.agent === 'claude') {
143
141
  generateClaudeConfig(overlayHome, config);
144
142
  }
@@ -2,7 +2,7 @@ import { type SecretsContext } from './secrets-client.js';
2
2
  import type { PushBundleResult } from './secrets-types.js';
3
3
  import type { AgentId, Meta, NativeAccountRecord } from './types.js';
4
4
  import { type DeviceProfile } from './devices/registry.js';
5
- import { type SharedAuthStatus } from './fleet-shared-state.js';
5
+ import { type FleetSharedDeviceState, type SharedAuthStatus } from './fleet-shared-state.js';
6
6
  import { selfConfiguredDeviceRole } from './device-config.js';
7
7
  import { AUTH_STORE_ALIAS, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, type StorableCredentialKind } from './reserved-stores.js';
8
8
  export { AUTH_STORE_ALIAS, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, type StorableCredentialKind, };
@@ -107,6 +107,25 @@ export declare function publishReservedAuthVerdict(options?: PublishAuthVerdictO
107
107
  * only peers whose shared verdict says the bundle is missing.
108
108
  */
109
109
  export declare function syncReservedAuthBundle(deps?: AuthSyncDeps): Promise<AuthSyncResult>;
110
+ /**
111
+ * Skip reason for a peer that has never sent a daemon-state reply (no
112
+ * `devices/<peer>/daemon-state.json`). auth-sync logs these at INFO — a
113
+ * brand-new or never-dialed worker legitimately has no reply on an early tick,
114
+ * so it is informational, not a warning. Stable so the service classifies the
115
+ * skip without matching a substring.
116
+ */
117
+ export declare const SKIP_REASON_NO_PEER_REPLY = "no daemon-state reply from this peer yet";
118
+ /**
119
+ * Skip reason for a peer whose reply exists but carries no `accounts.rows` field
120
+ * at all (an older CLI that does not publish per-account verdicts, or a partial
121
+ * state written before the account-state daemon ran). Without those rows there is
122
+ * no first-hand knowledge of what the peer holds, so the plan FAILS CLOSED and
123
+ * skips rather than pushing every key blindly. auth-sync logs it at INFO — it is
124
+ * transient during a rolling upgrade. An EMPTY `accounts.rows` array (a peer with
125
+ * no registered account) is NOT this case: it is a legitimate "holds nothing",
126
+ * and its keys are (correctly) pushed to provision it.
127
+ */
128
+ export declare const SKIP_REASON_NO_ACCOUNT_ROWS = "daemon-state reply carries no account rows (fail closed)";
110
129
  /** One account's durable worker credential, resolved to (bundle, key). */
111
130
  export interface ReservedSyncAccount {
112
131
  accountId: string;
@@ -116,8 +135,11 @@ export interface ReservedSyncAccount {
116
135
  /** Storage key `<ENV>_<accountId>` (or the legacy email-keyed claude key). */
117
136
  key: string;
118
137
  /**
119
- * `workerCredential.mintedAt` -- a re-mint bumps it, so a delivered-memo hit on
120
- * the old value re-pushes. `'legacy'` for a pre-T1 claude row with no field.
138
+ * The credential's rotation fingerprint: `workerCredential.mintedAt` for a T1
139
+ * row, `'legacy'` for a pre-T1 claude row. A re-mint (`accounts login`) bumps
140
+ * `mintedAt`, so this changes even though the OLD token still authenticates —
141
+ * which is why presence is gated on BOTH the peer's verdict AND a delivered-
142
+ * fingerprint match (see {@link peerPresentKeys}).
121
143
  */
122
144
  fingerprint: string;
123
145
  }
@@ -128,7 +150,24 @@ export interface ReservedSyncPeer {
128
150
  headed: boolean;
129
151
  reachable: boolean;
130
152
  pinned: boolean;
131
- /** bundle -> keys the peer already holds at the current fingerprint. Absent ⇒ none known. */
153
+ /**
154
+ * Whether this peer has ever sent a daemon-state reply on this box
155
+ * (`devices/<peer>/daemon-state.json` exists). Without one there is no
156
+ * first-hand knowledge of what it holds, so the peer is skipped this tick.
157
+ */
158
+ hasReply: boolean;
159
+ /**
160
+ * Whether the peer's reply actually carried an `accounts.rows` array (present,
161
+ * even if empty). A reply file with no such field (older CLI, partial state)
162
+ * has no first-hand inventory, so the plan skips it fail-closed rather than
163
+ * pushing blindly. See {@link SKIP_REASON_NO_ACCOUNT_ROWS}.
164
+ */
165
+ hasAccountRows: boolean;
166
+ /**
167
+ * bundle -> keys the peer is KNOWN to hold: its own reply verdict says present
168
+ * AND the fingerprint we last delivered to it matches the current one. Absent ⇒
169
+ * none known.
170
+ */
132
171
  presentKeys: Record<string, ReadonlySet<string>>;
133
172
  }
134
173
  type ReservedSyncPlanItem = {
@@ -144,7 +183,12 @@ type ReservedSyncPlanItem = {
144
183
  /**
145
184
  * Pure plan: for each worker peer, push each bundle it is missing at least one
146
185
  * key of. Deterministic (peers and bundles sorted) so it pins exactly in tests.
147
- * A headed peer is skipped BEFORE any key comparison -- it never receives a key.
186
+ * A headed peer is skipped BEFORE any key comparison -- it never receives a key;
187
+ * a peer with no reply yet (`hasReply: false`) is skipped next -- there is no
188
+ * first-hand knowledge of what it holds, so nothing is pushed to it this tick;
189
+ * a peer whose reply carries no `accounts.rows` field (`hasAccountRows: false`)
190
+ * is skipped FAIL-CLOSED for the same reason (an older CLI's reply is not proof
191
+ * of an empty inventory), rather than pushing every key blindly.
148
192
  */
149
193
  export declare function planReservedStoreSync(accounts: ReservedSyncAccount[], peers: ReservedSyncPeer[]): ReservedSyncPlanItem[];
150
194
  /**
@@ -156,14 +200,41 @@ export declare function planReservedStoreSync(accounts: ReservedSyncAccount[], p
156
200
  * `workerCredential`) is not a sync target.
157
201
  */
158
202
  export declare function reservedSyncTargets(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): ReservedSyncAccount[];
203
+ /** The subset of a peer's per-account verdict row this planner reads. */
204
+ export interface PeerAccountVerdict {
205
+ accountId: string;
206
+ harness: string;
207
+ /** `missing` ⇒ no working credential on the peer; anything else ⇒ present. */
208
+ verdict: string;
209
+ }
210
+ /** Narrow a peer's opaque `accounts.rows` to the verdict fields the planner reads. */
211
+ export declare function readPeerAccountVerdicts(state: FleetSharedDeviceState | undefined): PeerAccountVerdict[];
212
+ /**
213
+ * True when the peer's reply carried an `accounts.rows` array at all (present,
214
+ * even if empty). An older CLI, or a partial state written before the account-
215
+ * state daemon ran, has no such field — and its absence must NOT read as "the
216
+ * peer holds nothing", or the plan would push every key blindly. An EMPTY array
217
+ * (a peer with no registered account) IS a valid first-hand "holds nothing".
218
+ */
219
+ export declare function peerHasAccountRows(state: FleetSharedDeviceState | undefined): boolean;
159
220
  /**
160
- * The keys a peer is KNOWN to hold, per bundle, from what we have delivered to it
161
- * (memo, matched by fingerprint) plus the coarse legacy-`auth` verdict: a peer
162
- * reporting `auth.status === 'ready'` is known to hold every legacy `auth` key.
163
- * Reserved `__<harness>__` bundles have no coarse verdict, so their presence comes
164
- * from the memo alone -- which is what makes a newly-added reserved account push.
221
+ * The reserved keys a peer is KNOWN to hold, per bundle. Two conditions must BOTH
222
+ * hold (PHNX-4116):
223
+ *
224
+ * 1. FIRST-HAND — the peer's own daemon-state reply reports a NON-`missing`
225
+ * verdict for the account, so it has a working credential for it (its slot
226
+ * materialized). A `missing` verdict, or no row for the account at all, means
227
+ * it does not — self-correcting: a key removed on the worker flips its next
228
+ * verdict to `missing` and the push resumes, which a publisher-side memo alone
229
+ * could never see.
230
+ * 2. ROTATION — the fingerprint we last delivered to this peer (`delivered`)
231
+ * matches the account's CURRENT fingerprint. A re-mint bumps
232
+ * `workerCredential.mintedAt` while the old token still authenticates, so the
233
+ * verdict stays non-`missing`; without this the rotated key would never
234
+ * propagate. A never-delivered key (no memo entry ⇒ `undefined`) never
235
+ * matches, so a newly-added account still pushes.
165
236
  */
166
- export declare function peerPresentKeys(peer: string, accounts: ReservedSyncAccount[], memo: Record<string, string>, legacyAuthReady: boolean): Record<string, ReadonlySet<string>>;
237
+ export declare function peerPresentKeys(accounts: ReservedSyncAccount[], verdicts: PeerAccountVerdict[], delivered: (bundle: string, key: string) => string | undefined): Record<string, ReadonlySet<string>>;
167
238
  interface ReservedStoreSyncResult {
168
239
  publisher: string | null;
169
240
  /** Legacy raw reserved items adopted into their bundle on this box before planning (local repair). */
@@ -189,6 +260,7 @@ interface ReservedStoreSyncDeps {
189
260
  listDevices?: () => DeviceProfile[];
190
261
  localName?: string;
191
262
  userAgentsDir?: string;
263
+ /** Where the publisher-side delivery memo lives; defaults to `getCacheDir()`. */
192
264
  cacheDir?: string;
193
265
  readMetaFn?: () => Pick<Meta, 'accounts' | 'deviceAccounts'>;
194
266
  isPinned?: (name: string) => boolean;
@@ -221,6 +293,8 @@ interface ReservedStoreSyncDeps {
221
293
  export declare function syncReservedStores(deps?: ReservedStoreSyncDeps): Promise<ReservedStoreSyncResult>;
222
294
  interface ReconcileWorkerSlotsResult {
223
295
  provisioned: string[];
296
+ /** Stale slots removed from the device doc (accountId no longer registered). */
297
+ dropped: string[];
224
298
  skipped: Array<{
225
299
  accountId: string;
226
300
  reason: string;
@@ -237,6 +311,10 @@ interface ReconcileWorkerSlotsDeps {
237
311
  provision?: (account: NativeAccountRecord) => void;
238
312
  /** True when an existing durable slot already carries everything provisioning seeds. */
239
313
  slotSeeded?: (harness: AgentId, slotDir: string) => boolean;
314
+ /** Remove stale slot records from the device doc. Default: {@link dropSlots}. */
315
+ dropSlots?: (accountIds: readonly string[]) => void;
316
+ /** Emit an operational log line. Default: the daemon log (fire-and-forget). */
317
+ log?: (level: 'INFO' | 'WARN', message: string) => void;
240
318
  }
241
319
  /**
242
320
  * Worker-side: after a durable key lands, materialize a slot for each portable