@phnx-labs/agents-cli 1.22.83 → 1.22.85

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 (291) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +80 -53
  3. package/dist/bootstrap.d.ts +7 -6
  4. package/dist/bootstrap.js +20 -27
  5. package/dist/cli/command-registry.js +5 -1
  6. package/dist/commands/accounts.d.ts +6 -13
  7. package/dist/commands/accounts.js +330 -143
  8. package/dist/commands/apply.js +1 -1
  9. package/dist/commands/auth-mint.d.ts +7 -3
  10. package/dist/commands/auth-mint.js +21 -9
  11. package/dist/commands/auth.js +2 -3
  12. package/dist/commands/browser.js +10 -10
  13. package/dist/commands/daemon.d.ts +7 -3
  14. package/dist/commands/daemon.js +40 -25
  15. package/dist/commands/doctor.js +28 -2
  16. package/dist/commands/exec.js +87 -39
  17. package/dist/commands/fleet-capture.js +21 -2
  18. package/dist/commands/harness-wizard.js +2 -2
  19. package/dist/commands/lease.js +7 -12
  20. package/dist/commands/profiles.d.ts +2 -2
  21. package/dist/commands/profiles.js +20 -13
  22. package/dist/commands/repo.js +3 -3
  23. package/dist/commands/run-account-picker.d.ts +3 -3
  24. package/dist/commands/run-account-picker.js +29 -30
  25. package/dist/commands/secrets-passthrough.d.ts +20 -0
  26. package/dist/commands/secrets-passthrough.js +43 -0
  27. package/dist/commands/sessions-trace.d.ts +11 -0
  28. package/dist/commands/sessions-trace.js +71 -0
  29. package/dist/commands/setup-accounts.d.ts +3 -2
  30. package/dist/commands/setup-accounts.js +4 -4
  31. package/dist/commands/setup-secrets.d.ts +16 -21
  32. package/dist/commands/setup-secrets.js +43 -224
  33. package/dist/commands/ssh.js +2 -2
  34. package/dist/commands/sync.js +1 -1
  35. package/dist/commands/update.d.ts +10 -0
  36. package/dist/commands/update.js +43 -40
  37. package/dist/commands/versions.d.ts +11 -11
  38. package/dist/commands/versions.js +131 -83
  39. package/dist/commands/view.d.ts +1 -9
  40. package/dist/commands/view.js +41 -55
  41. package/dist/commands/webhook.js +5 -5
  42. package/dist/commands/workflows.js +2 -1
  43. package/dist/index.d.ts +12 -12
  44. package/dist/index.js +13 -42
  45. package/dist/lib/account-capabilities.d.ts +1 -1
  46. package/dist/lib/account-capabilities.js +1 -1
  47. package/dist/lib/account-catalog.d.ts +121 -6
  48. package/dist/lib/account-catalog.js +421 -7
  49. package/dist/lib/account-registry.d.ts +8 -30
  50. package/dist/lib/account-registry.js +36 -79
  51. package/dist/lib/account-schema.d.ts +1 -1
  52. package/dist/lib/account-schema.js +1 -1
  53. package/dist/lib/accounting/account-pool-collect.js +16 -4
  54. package/dist/lib/accounting/rotate.d.ts +20 -7
  55. package/dist/lib/accounting/rotate.js +89 -13
  56. package/dist/lib/accounting/usage.js +11 -16
  57. package/dist/lib/accounts/add.d.ts +138 -0
  58. package/dist/lib/accounts/add.js +651 -0
  59. package/dist/lib/accounts/migrate.d.ts +117 -0
  60. package/dist/lib/accounts/migrate.js +536 -0
  61. package/dist/lib/accounts/slots.d.ts +13 -0
  62. package/dist/lib/accounts/slots.js +100 -0
  63. package/dist/lib/agent-spec/agents.d.ts +16 -0
  64. package/dist/lib/agent-spec/agents.js +32 -4
  65. package/dist/lib/app-bundle-install.js +5 -4
  66. package/dist/lib/auth-health.d.ts +3 -0
  67. package/dist/lib/auth-health.js +11 -0
  68. package/dist/lib/auth-mint.d.ts +52 -16
  69. package/dist/lib/auth-mint.js +117 -39
  70. package/dist/lib/browser/chrome.d.ts +1 -1
  71. package/dist/lib/browser/chrome.js +5 -5
  72. package/dist/lib/byok-usage.js +3 -3
  73. package/dist/lib/claude-account-token.d.ts +47 -3
  74. package/dist/lib/claude-account-token.js +172 -50
  75. package/dist/lib/cloud/antigravity.js +4 -4
  76. package/dist/lib/cloud/cursor.js +4 -4
  77. package/dist/lib/crabbox/cli.d.ts +1 -1
  78. package/dist/lib/crabbox/cli.js +6 -6
  79. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  80. package/dist/lib/crabbox/runtimes.js +5 -5
  81. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  82. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  83. package/dist/lib/daemon/auth-sync-service.js +28 -1
  84. package/dist/lib/daemon/daemon.d.ts +6 -16
  85. package/dist/lib/daemon/daemon.js +52 -97
  86. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  87. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  88. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  89. package/dist/lib/daemon/harness-update-service.js +1 -1
  90. package/dist/lib/daemon/runner.d.ts +15 -3
  91. package/dist/lib/daemon/runner.js +49 -24
  92. package/dist/lib/daemon-health.d.ts +1 -2
  93. package/dist/lib/daemon-health.js +7 -6
  94. package/dist/lib/daemon-services.d.ts +1 -1
  95. package/dist/lib/daemon-services.js +1 -11
  96. package/dist/lib/daemon-ticks.js +23 -2
  97. package/dist/lib/daemon-webhooks.d.ts +8 -7
  98. package/dist/lib/daemon-webhooks.js +10 -9
  99. package/dist/lib/device-config.js +1 -1
  100. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  101. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  102. package/dist/lib/devices/harness-inventory.js +126 -4
  103. package/dist/lib/doctor-diff.js +2 -1
  104. package/dist/lib/exec-account-home.d.ts +38 -0
  105. package/dist/lib/exec-account-home.js +164 -0
  106. package/dist/lib/exec.d.ts +30 -0
  107. package/dist/lib/exec.js +101 -31
  108. package/dist/lib/feed/activity-stream.d.ts +60 -0
  109. package/dist/lib/feed/activity-stream.js +271 -0
  110. package/dist/lib/feed/activity.d.ts +7 -0
  111. package/dist/lib/feed/activity.js +10 -4
  112. package/dist/lib/feed/watch.d.ts +9 -3
  113. package/dist/lib/feed/watch.js +72 -36
  114. package/dist/lib/fleet/apply.d.ts +2 -2
  115. package/dist/lib/fleet/apply.js +4 -4
  116. package/dist/lib/fleet/auth-sync.js +4 -3
  117. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  118. package/dist/lib/fleet-shared-state.d.ts +6 -0
  119. package/dist/lib/harness/adapter.d.ts +7 -7
  120. package/dist/lib/harness/adapter.js +10 -6
  121. package/dist/lib/harness/adapters/grok.js +8 -3
  122. package/dist/lib/harness/adapters/muse.js +1 -1
  123. package/dist/lib/harness/adapters/opencode.js +12 -3
  124. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  125. package/dist/lib/harness-auth-capabilities.js +52 -0
  126. package/dist/lib/helper-versions.d.ts +6 -4
  127. package/dist/lib/helper-versions.js +5 -4
  128. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  129. package/dist/lib/hosts/credential-transport.js +37 -0
  130. package/dist/lib/hosts/dispatch.d.ts +7 -0
  131. package/dist/lib/hosts/dispatch.js +13 -6
  132. package/dist/lib/identity/client.d.ts +3 -3
  133. package/dist/lib/identity/client.js +3 -3
  134. package/dist/lib/installations/index.d.ts +1 -1
  135. package/dist/lib/installations/index.js +1 -1
  136. package/dist/lib/installations/migrate.js +9 -0
  137. package/dist/lib/installations/resolve.js +1 -1
  138. package/dist/lib/installations/shims.d.ts +15 -0
  139. package/dist/lib/installations/shims.js +69 -0
  140. package/dist/lib/installations/store.d.ts +43 -0
  141. package/dist/lib/installations/store.js +75 -7
  142. package/dist/lib/installations/versions.js +1 -1
  143. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  144. package/dist/lib/menubar/install-menubar.js +3 -3
  145. package/dist/lib/native-accounts.d.ts +35 -0
  146. package/dist/lib/native-accounts.js +41 -0
  147. package/dist/lib/net-close.d.ts +11 -0
  148. package/dist/lib/net-close.js +25 -0
  149. package/dist/lib/openclaw-keychain.js +27 -1
  150. package/dist/lib/profiles.js +22 -9
  151. package/dist/lib/project-resources.js +1 -1
  152. package/dist/lib/reserved-stores.d.ts +53 -0
  153. package/dist/lib/reserved-stores.js +120 -0
  154. package/dist/lib/secrets-client.d.ts +191 -0
  155. package/dist/lib/secrets-client.js +710 -0
  156. package/dist/lib/secrets-policy.d.ts +222 -0
  157. package/dist/lib/secrets-policy.js +483 -0
  158. package/dist/lib/secrets-types.d.ts +188 -0
  159. package/dist/lib/secrets-types.js +24 -0
  160. package/dist/lib/service-manifest.js +3 -4
  161. package/dist/lib/session/active.d.ts +38 -4
  162. package/dist/lib/session/active.js +36 -4
  163. package/dist/lib/session/bash-command.js +10 -0
  164. package/dist/lib/session/db.d.ts +65 -2
  165. package/dist/lib/session/db.js +178 -18
  166. package/dist/lib/session/mirror.d.ts +5 -0
  167. package/dist/lib/session/mirror.js +166 -0
  168. package/dist/lib/session/parse.d.ts +49 -0
  169. package/dist/lib/session/parse.js +324 -30
  170. package/dist/lib/session/prompt.d.ts +93 -2
  171. package/dist/lib/session/prompt.js +266 -15
  172. package/dist/lib/session/remote/peer-stream.d.ts +47 -0
  173. package/dist/lib/session/remote/peer-stream.js +142 -0
  174. package/dist/lib/session/remote/watch.d.ts +1 -1
  175. package/dist/lib/session/remote/watch.js +41 -42
  176. package/dist/lib/session/session-cache.d.ts +9 -0
  177. package/dist/lib/session/session-cache.js +40 -2
  178. package/dist/lib/session/sync/config.js +2 -2
  179. package/dist/lib/session/timeline-pass.d.ts +129 -0
  180. package/dist/lib/session/timeline-pass.js +323 -0
  181. package/dist/lib/session/timeline.d.ts +182 -0
  182. package/dist/lib/session/timeline.js +636 -0
  183. package/dist/lib/session/types.d.ts +155 -1
  184. package/dist/lib/sha256-asset.d.ts +11 -16
  185. package/dist/lib/sha256-asset.js +11 -16
  186. package/dist/lib/share/config.js +14 -5
  187. package/dist/lib/signin-badge.d.ts +26 -0
  188. package/dist/lib/signin-badge.js +42 -0
  189. package/dist/lib/staleness/detectors/workflows.js +3 -105
  190. package/dist/lib/staleness/writers/workflows.js +2 -1
  191. package/dist/lib/state.d.ts +7 -4
  192. package/dist/lib/state.js +18 -7
  193. package/dist/lib/summarizer/pass.d.ts +2 -0
  194. package/dist/lib/summarizer/pass.js +14 -1
  195. package/dist/lib/summarizer/summarize.d.ts +7 -0
  196. package/dist/lib/summarizer/summarize.js +3 -0
  197. package/dist/lib/sync-umbrella.js +2 -2
  198. package/dist/lib/teams/agents.js +1 -1
  199. package/dist/lib/types.d.ts +74 -33
  200. package/dist/lib/view-types.d.ts +6 -3
  201. package/dist/lib/workflows-registry.d.ts +71 -0
  202. package/dist/lib/workflows-registry.js +280 -0
  203. package/dist/lib/workflows.d.ts +21 -21
  204. package/dist/lib/workflows.js +12 -327
  205. package/package.json +2 -3
  206. package/scripts/postinstall.js +4 -29
  207. package/dist/commands/secrets-import.d.ts +0 -18
  208. package/dist/commands/secrets-import.js +0 -74
  209. package/dist/commands/secrets-migrate.d.ts +0 -25
  210. package/dist/commands/secrets-migrate.js +0 -334
  211. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  212. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  213. package/dist/commands/secrets-sync.d.ts +0 -11
  214. package/dist/commands/secrets-sync.js +0 -153
  215. package/dist/commands/secrets-vault.d.ts +0 -10
  216. package/dist/commands/secrets-vault.js +0 -130
  217. package/dist/commands/secrets.d.ts +0 -212
  218. package/dist/commands/secrets.js +0 -3030
  219. package/dist/lib/accounts/connect.d.ts +0 -170
  220. package/dist/lib/accounts/connect.js +0 -383
  221. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  222. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  223. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  224. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  225. package/dist/lib/secrets/agent.d.ts +0 -358
  226. package/dist/lib/secrets/agent.js +0 -1291
  227. package/dist/lib/secrets/audit.d.ts +0 -46
  228. package/dist/lib/secrets/audit.js +0 -101
  229. package/dist/lib/secrets/bundles.d.ts +0 -290
  230. package/dist/lib/secrets/bundles.js +0 -1547
  231. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  232. package/dist/lib/secrets/download-keychain.js +0 -70
  233. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  234. package/dist/lib/secrets/drivers/rush.js +0 -90
  235. package/dist/lib/secrets/fallback.d.ts +0 -48
  236. package/dist/lib/secrets/fallback.js +0 -48
  237. package/dist/lib/secrets/filestore.d.ts +0 -222
  238. package/dist/lib/secrets/filestore.js +0 -1099
  239. package/dist/lib/secrets/headless.d.ts +0 -43
  240. package/dist/lib/secrets/headless.js +0 -56
  241. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  242. package/dist/lib/secrets/icloud-import.js +0 -206
  243. package/dist/lib/secrets/index.d.ts +0 -451
  244. package/dist/lib/secrets/index.js +0 -1568
  245. package/dist/lib/secrets/install-helper.d.ts +0 -72
  246. package/dist/lib/secrets/install-helper.js +0 -245
  247. package/dist/lib/secrets/lease.d.ts +0 -25
  248. package/dist/lib/secrets/lease.js +0 -44
  249. package/dist/lib/secrets/linux.d.ts +0 -77
  250. package/dist/lib/secrets/linux.js +0 -393
  251. package/dist/lib/secrets/list-filter.d.ts +0 -109
  252. package/dist/lib/secrets/list-filter.js +0 -261
  253. package/dist/lib/secrets/mcp.d.ts +0 -93
  254. package/dist/lib/secrets/mcp.js +0 -211
  255. package/dist/lib/secrets/profiles.d.ts +0 -10
  256. package/dist/lib/secrets/profiles.js +0 -13
  257. package/dist/lib/secrets/push.d.ts +0 -133
  258. package/dist/lib/secrets/push.js +0 -273
  259. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  260. package/dist/lib/secrets/rc-hygiene.js +0 -143
  261. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  262. package/dist/lib/secrets/read-backoff.js +0 -64
  263. package/dist/lib/secrets/reaper.d.ts +0 -90
  264. package/dist/lib/secrets/reaper.js +0 -243
  265. package/dist/lib/secrets/remote.d.ts +0 -152
  266. package/dist/lib/secrets/remote.js +0 -344
  267. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  268. package/dist/lib/secrets/reserved-sync.js +0 -147
  269. package/dist/lib/secrets/scope.d.ts +0 -26
  270. package/dist/lib/secrets/scope.js +0 -29
  271. package/dist/lib/secrets/session-store.d.ts +0 -107
  272. package/dist/lib/secrets/session-store.js +0 -342
  273. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  274. package/dist/lib/secrets/sync-backend.js +0 -13
  275. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  276. package/dist/lib/secrets/sync-commands.js +0 -21
  277. package/dist/lib/secrets/sync.d.ts +0 -48
  278. package/dist/lib/secrets/sync.js +0 -237
  279. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  280. package/dist/lib/secrets/unlock-hints.js +0 -36
  281. package/dist/lib/secrets/usage-db.d.ts +0 -46
  282. package/dist/lib/secrets/usage-db.js +0 -96
  283. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  284. package/dist/lib/secrets/vault-age-helper.js +0 -34
  285. package/dist/lib/secrets/vault.d.ts +0 -49
  286. package/dist/lib/secrets/vault.js +0 -399
  287. package/dist/lib/secrets/windows.d.ts +0 -81
  288. package/dist/lib/secrets/windows.js +0 -556
  289. package/scripts/install-helper.js +0 -97
  290. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  291. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Incremental reader for the activity log directory.
3
+ *
4
+ * `readRecentActivity` answers "what happened since T?" by tailing and parsing
5
+ * every session log in the directory. That is the right shape for a one-shot
6
+ * `agents feed` render and the wrong shape for `agents feed watch`, which asks
7
+ * the same question twice a second for as long as a VS Code window is open: on
8
+ * a real operator box the directory holds 1,437 logs / 64 MB, so every tick
9
+ * re-read and re-parsed the whole corpus to emit, almost always, nothing.
10
+ *
11
+ * This reader keeps a per-file cursor instead. The opening scan opens no files
12
+ * at all — it records each log's size, inode, and mtime — so only bytes appended
13
+ * *after* the stream started are ever read. Steady-state cost is one `stat` per
14
+ * changed file plus a parse of exactly the appended bytes.
15
+ *
16
+ * The emitted events, their order, and the `sinceMs` filter match
17
+ * `readRecentActivity` for everything appended while the stream is open; the
18
+ * equivalence is pinned by `activity-stream.test.ts`.
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import { getActivityDir } from '../state.js';
23
+ import { ACTIVITY_TAIL_BYTES, parseActivityLine } from './activity.js';
24
+ /** How often the stream falls back to a full directory stat sweep. */
25
+ export const ACTIVITY_SWEEP_MS = 5_000;
26
+ /** Bytes behind the cursor re-verified before appended bytes are trusted. */
27
+ export const ACTIVITY_ANCHOR_BYTES = 64;
28
+ const NEWLINE = 0x0a;
29
+ const EMPTY = Buffer.alloc(0);
30
+ /** Could this file have changed since its cursor last looked? */
31
+ function changed(cursor, stat) {
32
+ return stat.identity !== cursor.identity
33
+ || stat.size !== cursor.size
34
+ || stat.mtimeNs !== cursor.mtimeNs
35
+ || stat.ctimeNs !== cursor.ctimeNs;
36
+ }
37
+ /**
38
+ * A cursor over the activity directory. Construct it at the moment the caller's
39
+ * activity cursor starts, then call {@link read} once per tick.
40
+ */
41
+ export class ActivityStream {
42
+ dir;
43
+ maxBytesPerRead;
44
+ sweepMs;
45
+ cursors = new Map();
46
+ dirty = new Set();
47
+ watcher;
48
+ watchRequested;
49
+ lastSweepMs = 0;
50
+ /** False during the opening scan, so it registers history without reading it. */
51
+ started = false;
52
+ /** Bytes read from activity logs since construction. Observability + tests. */
53
+ bytesRead = 0;
54
+ constructor(options = {}) {
55
+ this.dir = options.root ?? getActivityDir();
56
+ this.maxBytesPerRead = options.maxBytesPerRead ?? ACTIVITY_TAIL_BYTES;
57
+ this.sweepMs = options.sweepMs ?? ACTIVITY_SWEEP_MS;
58
+ this.watchRequested = options.watch ?? true;
59
+ this.sweep(Date.now());
60
+ this.armWatcher();
61
+ }
62
+ /**
63
+ * Events appended since the last call, newest first, filtered to `sinceMs`
64
+ * inclusive — the same shape and order `readRecentActivity({ sinceMs })`
65
+ * returns for those events.
66
+ */
67
+ read(sinceMs, nowMs = Date.now()) {
68
+ const out = [];
69
+ for (const name of this.candidates(nowMs)) {
70
+ for (const event of this.readFile(name)) {
71
+ const at = Date.parse(event.ts);
72
+ if (Number.isFinite(at) && at >= sinceMs)
73
+ out.push(event);
74
+ }
75
+ }
76
+ out.sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
77
+ return out;
78
+ }
79
+ /** Release the directory watcher. Safe to call more than once. */
80
+ close() {
81
+ this.watchRequested = false;
82
+ this.watcher?.close();
83
+ this.watcher = undefined;
84
+ }
85
+ /** Which log names could have changed since the previous tick. */
86
+ candidates(nowMs) {
87
+ // A watcher that never armed (unsupported filesystem, or a directory that
88
+ // did not exist at construction) means every tick sweeps. That is the
89
+ // fallback: never a silent no-op that would drop events.
90
+ if (!this.watcher)
91
+ this.armWatcher();
92
+ if (!this.watcher || nowMs - this.lastSweepMs >= this.sweepMs)
93
+ this.sweep(nowMs);
94
+ const names = [...this.dirty];
95
+ this.dirty.clear();
96
+ return names;
97
+ }
98
+ /**
99
+ * Stat every log, marking the ones that could have changed. Opens nothing: a
100
+ * log the opening scan sees is registered past its own bytes, so history is
101
+ * never replayed onto the stream.
102
+ */
103
+ sweep(nowMs) {
104
+ this.lastSweepMs = nowMs;
105
+ let names;
106
+ try {
107
+ names = fs.readdirSync(this.dir).filter((name) => name.endsWith('.jsonl'));
108
+ }
109
+ catch {
110
+ return; // The directory appears with the first logged event.
111
+ }
112
+ const seen = new Set();
113
+ for (const name of names) {
114
+ seen.add(name);
115
+ const stat = this.statOf(name);
116
+ if (!stat)
117
+ continue;
118
+ const cursor = this.cursors.get(name);
119
+ if (!cursor) {
120
+ // A log first seen after the opening scan is new work: left
121
+ // unregistered so `readFile` opens it from a bounded tail.
122
+ if (this.started)
123
+ this.dirty.add(name);
124
+ else
125
+ this.cursors.set(name, {
126
+ identity: stat.identity, offset: stat.size, partial: EMPTY, partialIsFragment: false,
127
+ anchor: EMPTY, size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs,
128
+ });
129
+ continue;
130
+ }
131
+ if (changed(cursor, stat))
132
+ this.dirty.add(name);
133
+ }
134
+ // A cursor is dropped only when its log is gone. There is deliberately no
135
+ // size cap: the map cannot outgrow the directory this sweep already had to
136
+ // enumerate, so a cap bounds nothing the readdir does not — while dropping
137
+ // a live log's cursor would re-register it as new work on the next sweep
138
+ // and replay a bounded tail of it onto the stream as duplicates.
139
+ for (const name of [...this.cursors.keys()])
140
+ if (!seen.has(name))
141
+ this.cursors.delete(name);
142
+ this.started = true;
143
+ }
144
+ statOf(name) {
145
+ try {
146
+ const st = fs.statSync(path.join(this.dir, name), { bigint: true });
147
+ return {
148
+ identity: `${st.dev}:${st.ino}`,
149
+ size: Number(st.size),
150
+ mtimeNs: Number(st.mtimeNs),
151
+ ctimeNs: Number(st.ctimeNs),
152
+ };
153
+ }
154
+ catch {
155
+ return undefined; // Deleted between readdir and stat.
156
+ }
157
+ }
158
+ /** A cursor starting at a bounded tail of the file as it stands right now. */
159
+ freshCursor(stat) {
160
+ const offset = Math.max(0, stat.size - this.maxBytesPerRead);
161
+ return {
162
+ identity: stat.identity, offset, partial: EMPTY, partialIsFragment: offset > 0,
163
+ anchor: EMPTY, size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs,
164
+ };
165
+ }
166
+ /** Read and parse only the bytes appended to one log since its cursor. */
167
+ readFile(name, restarted = false) {
168
+ const stat = this.statOf(name);
169
+ if (!stat) {
170
+ this.cursors.delete(name);
171
+ return [];
172
+ }
173
+ let cursor = this.cursors.get(name);
174
+ // Unseen, replaced, truncated, or rewritten in place at the same length:
175
+ // restart from a bounded tail of the file as it now stands. The caller's
176
+ // `sinceMs` drops whatever predates the stream.
177
+ //
178
+ // The same-length case is why ctime is tracked. Growth is caught by the
179
+ // 64-byte anchor and a shrink by the offset compare, but a rewrite that
180
+ // lands on exactly the previous size moves neither, and the early return
181
+ // below would otherwise retire the file for good with content unread.
182
+ if (!cursor || cursor.identity !== stat.identity || stat.size < cursor.offset
183
+ || (stat.size === cursor.size && stat.ctimeNs !== cursor.ctimeNs)) {
184
+ cursor = this.freshCursor(stat);
185
+ this.cursors.set(name, cursor);
186
+ }
187
+ cursor.size = stat.size;
188
+ cursor.mtimeNs = stat.mtimeNs;
189
+ cursor.ctimeNs = stat.ctimeNs;
190
+ if (stat.size <= cursor.offset)
191
+ return [];
192
+ // A burst larger than the budget keeps the newest bytes; the skipped span is
193
+ // exactly what the bounded-tail reader would have dropped as well.
194
+ const start = Math.max(cursor.offset, stat.size - this.maxBytesPerRead);
195
+ if (start > cursor.offset) {
196
+ cursor.partial = EMPTY;
197
+ cursor.partialIsFragment = true;
198
+ cursor.anchor = EMPTY;
199
+ }
200
+ const verify = Math.min(cursor.anchor.length, start);
201
+ const buf = this.readRange(name, start - verify, stat.size - start + verify);
202
+ if (buf === undefined)
203
+ return []; // Transient I/O error: retry next tick.
204
+ if (verify > 0 && !buf.subarray(0, verify).equals(cursor.anchor.subarray(cursor.anchor.length - verify))) {
205
+ // The bytes behind the cursor changed, so this file was rewritten rather
206
+ // than appended to. Restart it once from a bounded tail.
207
+ if (restarted)
208
+ return [];
209
+ this.cursors.delete(name);
210
+ return this.readFile(name, true);
211
+ }
212
+ const fresh = buf.subarray(verify);
213
+ cursor.offset = stat.size;
214
+ cursor.anchor = Buffer.concat([cursor.anchor, fresh]).subarray(-ACTIVITY_ANCHOR_BYTES);
215
+ const chunk = Buffer.concat([cursor.partial, fresh]);
216
+ const fragment = cursor.partialIsFragment;
217
+ const end = chunk.lastIndexOf(NEWLINE);
218
+ // Hold an unterminated trailing line: the writer appends whole
219
+ // newline-terminated records (`appendActivityEvent`), so a tail without a
220
+ // newline is a write in progress and completes on a later tick.
221
+ cursor.partial = end < 0 ? chunk : chunk.subarray(end + 1);
222
+ cursor.partialIsFragment = end < 0 ? fragment : false;
223
+ if (end < 0)
224
+ return [];
225
+ const lines = chunk.subarray(0, end).toString('utf-8').split('\n');
226
+ // A range that began mid-file starts inside a record; that leading fragment
227
+ // is not one, exactly as the bounded-tail reader drops it.
228
+ if (fragment)
229
+ lines.shift();
230
+ const events = [];
231
+ for (const line of lines) {
232
+ const event = parseActivityLine(line);
233
+ if (event)
234
+ events.push(event);
235
+ }
236
+ return events;
237
+ }
238
+ readRange(name, start, length) {
239
+ let fd;
240
+ try {
241
+ fd = fs.openSync(path.join(this.dir, name), 'r');
242
+ const buf = Buffer.allocUnsafe(length);
243
+ const read = fs.readSync(fd, buf, 0, length, start);
244
+ this.bytesRead += read;
245
+ return buf.subarray(0, read);
246
+ }
247
+ catch {
248
+ return undefined;
249
+ }
250
+ finally {
251
+ if (fd !== undefined)
252
+ fs.closeSync(fd);
253
+ }
254
+ }
255
+ armWatcher() {
256
+ if (!this.watchRequested || this.watcher)
257
+ return;
258
+ try {
259
+ this.watcher = fs.watch(this.dir, (_event, name) => {
260
+ if (typeof name === 'string' && name.endsWith('.jsonl'))
261
+ this.dirty.add(name);
262
+ });
263
+ // A watch error (the directory is removed) degrades to sweeping rather
264
+ // than taking down the watcher process.
265
+ this.watcher.on('error', () => { this.watcher?.close(); this.watcher = undefined; });
266
+ }
267
+ catch {
268
+ this.watcher = undefined;
269
+ }
270
+ }
271
+ }
@@ -129,6 +129,13 @@ export declare function appendActivityEvent(event: Omit<ActivityEvent, 'v' | 'ti
129
129
  v?: number;
130
130
  tier?: ActivityTier;
131
131
  }, root?: string): void;
132
+ /**
133
+ * Parse one activity log line. Tolerant by design: a blank, corrupt, or
134
+ * half-written line is skipped rather than failing the whole read.
135
+ */
136
+ export declare function parseActivityLine(line: string): ActivityEvent | undefined;
137
+ /** Per-session byte budget for a bounded tail read of an activity log. */
138
+ export declare const ACTIVITY_TAIL_BYTES: number;
132
139
  /** Read all events for one session (bounded tail). */
133
140
  export declare function readSessionActivity(sessionId: string, root?: string, maxBytes?: number): ActivityEvent[];
134
141
  /** List session ids that have an activity log. */
@@ -119,7 +119,11 @@ export function appendActivityEvent(event, root) {
119
119
  };
120
120
  fs.appendFileSync(activityPath(dir, event.sessionId), `${JSON.stringify(record)}\n`, { mode: 0o644 });
121
121
  }
122
- function parseLine(line) {
122
+ /**
123
+ * Parse one activity log line. Tolerant by design: a blank, corrupt, or
124
+ * half-written line is skipped rather than failing the whole read.
125
+ */
126
+ export function parseActivityLine(line) {
123
127
  const trimmed = line.trim();
124
128
  if (!trimmed)
125
129
  return undefined;
@@ -160,6 +164,8 @@ function parseLine(line) {
160
164
  return undefined; // skip corrupt / partial lines (fail-open reader)
161
165
  }
162
166
  }
167
+ /** Per-session byte budget for a bounded tail read of an activity log. */
168
+ export const ACTIVITY_TAIL_BYTES = 256 * 1024;
163
169
  /** Read the tail of a file as UTF-8, bounded to the last `maxBytes`. */
164
170
  function readTail(file, maxBytes) {
165
171
  let fd;
@@ -232,7 +238,7 @@ function readActivityTail(file, maxBytes, sinceMs = -Infinity) {
232
238
  const events = [];
233
239
  let newestMs = -Infinity;
234
240
  for (const line of text.split('\n')) {
235
- const event = parseLine(line);
241
+ const event = parseActivityLine(line);
236
242
  if (!event)
237
243
  continue;
238
244
  events.push(event);
@@ -277,7 +283,7 @@ function readActivityTail(file, maxBytes, sinceMs = -Infinity) {
277
283
  return result;
278
284
  }
279
285
  /** Read all events for one session (bounded tail). */
280
- export function readSessionActivity(sessionId, root, maxBytes = 256 * 1024) {
286
+ export function readSessionActivity(sessionId, root, maxBytes = ACTIVITY_TAIL_BYTES) {
281
287
  const dir = root ?? getActivityDir();
282
288
  return structuredClone(readActivityTail(activityPath(dir, sessionId), maxBytes).events ?? []);
283
289
  }
@@ -301,7 +307,7 @@ export function readRecentActivity(opts = {}) {
301
307
  const wanted = opts.events && opts.events.length > 0 ? new Set(opts.events) : null;
302
308
  const all = [];
303
309
  for (const sessionId of listActivitySessions(dir)) {
304
- const tail = readActivityTail(activityPath(dir, sessionId), opts.maxBytesPerSession ?? 256 * 1024, sinceMs);
310
+ const tail = readActivityTail(activityPath(dir, sessionId), opts.maxBytesPerSession ?? ACTIVITY_TAIL_BYTES, sinceMs);
305
311
  if (tail.newestMs < sinceMs)
306
312
  continue;
307
313
  for (const ev of tail.events ?? []) {
@@ -1,4 +1,4 @@
1
- import { type SessionWatchEnvelope, type SessionWatchRow, type SessionWatchScopeStatus } from '../session/watch.js';
1
+ import { type SessionWatchEnvelope, type SessionWatchRow, type SessionWatchScopeStatus, type WatchLocalOptions } from '../session/watch.js';
2
2
  import { type AttentionItem } from './attention.js';
3
3
  import { type ActivityEvent } from './activity.js';
4
4
  export declare const FEED_WATCH_VERSION: 1;
@@ -48,12 +48,18 @@ export declare class FeedWatchState {
48
48
  emit(event: FeedWatchPayload): FeedWatchEnvelope;
49
49
  }
50
50
  export declare function projectSessionEnvelope(event: SessionWatchEnvelope, state: FeedWatchState): Promise<FeedWatchEnvelope[]>;
51
- export declare function watchLocalFeed(options: {
51
+ export interface WatchLocalFeedOptions {
52
52
  scope: string;
53
53
  signal: AbortSignal;
54
54
  emit: (event: FeedWatchEnvelope) => void;
55
+ /** Activity drain cadence. */
55
56
  activityPollMs?: number;
56
- }): Promise<void>;
57
+ /** Attention reconcile cadence when nothing has announced a change. */
58
+ reconcileMs?: number;
59
+ /** Session-watch inputs, forwarded verbatim to {@link watchLocalSessions}. */
60
+ sessions?: Pick<WatchLocalOptions, 'readCache' | 'readPrevious' | 'journalPath' | 'journalPollMs' | 'heartbeatMs'>;
61
+ }
62
+ export declare function watchLocalFeed(options: WatchLocalFeedOptions): Promise<void>;
57
63
  export declare function watchFleetFeed(options: {
58
64
  signal: AbortSignal;
59
65
  emit: (event: FeedWatchEnvelope) => void;
@@ -1,16 +1,17 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { spawn } from 'node:child_process';
3
- import { createInterface } from 'node:readline';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
4
  import { loadDevices, isDialableDevice } from '../devices/registry.js';
5
- import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
6
5
  import { machineId, normalizeHost } from '../machine-id.js';
7
- import { SSH_OPTS, controlOpts, shellQuote } from '../ssh-exec.js';
6
+ import { shellQuote } from '../ssh-exec.js';
8
7
  import { buildWindowsAgentsCommand, remoteShellFor } from '../hosts/remote-cmd.js';
8
+ import { getFeedDir } from '../state.js';
9
+ import { streamFromPeer } from '../session/remote/peer-stream.js';
9
10
  import { watchLocalSessions } from '../session/watch.js';
10
11
  import { readBlock, readResolution, blockIdForSession } from './feed.js';
11
12
  import { reconcileAttention } from './attention.js';
12
- import { readRecentActivity } from './activity.js';
13
- import { readPullRequestStatus } from './pr-status.js';
13
+ import { ActivityStream } from './activity-stream.js';
14
+ import { PR_STATUS_TTL_MS, readPullRequestStatus } from './pr-status.js';
14
15
  export const FEED_WATCH_VERSION = 1;
15
16
  export class FeedWatchState {
16
17
  streamId;
@@ -64,8 +65,37 @@ export async function projectSessionEnvelope(event, state) {
64
65
  return [state.emit({ type: 'scope', capturedAt: event.capturedAt, scope: event.scope, status: event.status, ...(event.reason ? { reason: event.reason } : {}) })];
65
66
  return [state.emit({ type: 'heartbeat', capturedAt: event.capturedAt, scope: event.scope })];
66
67
  }
68
+ /**
69
+ * Watch the feed dirs that can change attention without a session event: an
70
+ * `agents feed post --blocked` writes a block, `feed answer` writes a
71
+ * resolution. Both are external processes, so nothing on the session stream
72
+ * announces them — this is what lets the reconcile pass be event-driven instead
73
+ * of a poll over every row twice a second.
74
+ */
75
+ function watchAttentionStores(onChange) {
76
+ const feedDir = getFeedDir();
77
+ const watchers = [];
78
+ for (const dir of [feedDir, path.join(feedDir, 'resolutions')]) {
79
+ try {
80
+ const watcher = fs.watch(dir, () => onChange());
81
+ // A directory that disappears must not take the watcher process down; the
82
+ // PR-status cadence below still reconciles on its own timer.
83
+ watcher.on('error', () => watcher.close());
84
+ watchers.push(watcher);
85
+ }
86
+ catch { /* the dir appears with the first block; the timed pass covers it */ }
87
+ }
88
+ return () => { for (const watcher of watchers)
89
+ watcher.close(); };
90
+ }
67
91
  export async function watchLocalFeed(options) {
68
92
  const state = new FeedWatchState();
93
+ // The stream's opening scan registers every log past its own bytes, so the
94
+ // cursor has to be taken *after* it: a record appended while the scan was
95
+ // walking the directory is unreadable from the byte cursor, and only a
96
+ // timestamp taken after the scan classifies it as history rather than
97
+ // dropping it from a window the caller believes was covered.
98
+ const activity = new ActivityStream();
69
99
  let activityCursor = Date.now();
70
100
  const agents = new Map();
71
101
  const attention = new Map();
@@ -82,20 +112,34 @@ export async function watchLocalFeed(options) {
82
112
  : state.emit({ type: 'attention.remove', scope: options.scope, rowKey }));
83
113
  }
84
114
  };
115
+ // Attention is reconciled when something can actually have changed it — a row
116
+ // moved, a block/resolution file was written — or when the PR-status TTL has
117
+ // expired and the cached verdicts are stale. Re-running it every 500 ms cost
118
+ // two file reads per row per tick and changed nothing.
119
+ const reconcileMs = options.reconcileMs ?? PR_STATUS_TTL_MS;
120
+ let attentionDirty = true;
121
+ let lastReconcileMs = 0;
122
+ const markAttentionDirty = () => { attentionDirty = true; };
123
+ const stopAttentionWatch = watchAttentionStores(markAttentionDirty);
85
124
  const activityTimer = setInterval(() => {
86
125
  pending = pending.then(async () => {
87
- const events = readRecentActivity({ sinceMs: activityCursor + 1 }).reverse();
126
+ const nowMs = Date.now();
127
+ const events = activity.read(activityCursor + 1, nowMs).reverse();
88
128
  for (const event of events) {
89
129
  activityCursor = Math.max(activityCursor, Date.parse(event.ts));
90
130
  options.emit(state.emit({ type: 'activity.append', scope: options.scope, event }));
91
131
  }
132
+ if (!attentionDirty && nowMs - lastReconcileMs < reconcileMs)
133
+ return;
134
+ attentionDirty = false;
135
+ lastReconcileMs = nowMs;
92
136
  await reconcileRows();
93
137
  });
94
138
  }, options.activityPollMs ?? 500);
95
- const stopActivity = () => clearInterval(activityTimer);
139
+ const stopActivity = () => { clearInterval(activityTimer); stopAttentionWatch(); activity.close(); };
96
140
  options.signal.addEventListener('abort', stopActivity, { once: true });
97
141
  try {
98
- await watchLocalSessions({ scope: options.scope, signal: options.signal, emit: (event) => {
142
+ await watchLocalSessions({ ...options.sessions, scope: options.scope, signal: options.signal, emit: (event) => {
99
143
  if (event.type === 'reset') {
100
144
  agents.clear();
101
145
  for (const row of event.rows)
@@ -156,35 +200,27 @@ export async function watchFleetFeed(options) {
156
200
  }
157
201
  const self = machineId();
158
202
  const peers = Object.values(devices).filter((device) => isDialableDevice(device) && normalizeHost(device.name) !== self && ['windows', 'linux', 'macos'].includes(device.platform));
159
- const tasks = peers.map(async (device) => {
203
+ const tasks = peers.map((device) => {
160
204
  const scope = normalizeHost(device.name);
161
- while (!options.signal.aborted) {
162
- let target;
163
- try {
164
- target = sshTargetFor(device);
165
- }
166
- catch (error) {
167
- options.emit(coordinator.emit({ type: 'scope', capturedAt: Date.now(), scope, status: 'unavailable', reason: String(error) }));
168
- break;
169
- }
170
- const child = spawn('ssh', [...SSH_OPTS, ...controlOpts(), ...deviceIdentityArgs(device), target, remoteFeedWatchCommand(device.platform)], { stdio: ['ignore', 'pipe', 'ignore'] });
171
- const stop = () => child.kill('SIGTERM');
172
- options.signal.addEventListener('abort', stop, { once: true });
173
- const reader = createInterface({ input: child.stdout });
174
- reader.on('line', (line) => { try {
175
- const event = JSON.parse(line);
176
- if (event.v === 1)
205
+ return streamFromPeer({
206
+ device,
207
+ signal: options.signal,
208
+ command: remoteFeedWatchCommand(device.platform),
209
+ backoffBaseMs: options.reconnectMs,
210
+ onLine: (line) => {
211
+ try {
212
+ const event = JSON.parse(line);
213
+ if (event.v !== 1)
214
+ return false;
177
215
  forward(event);
178
- }
179
- catch { /* protocol only */ } });
180
- const code = await new Promise((resolve) => { child.once('error', () => resolve(null)); child.once('close', resolve); });
181
- reader.close();
182
- options.signal.removeEventListener('abort', stop);
183
- if (options.signal.aborted)
184
- break;
185
- options.emit(coordinator.emit({ type: 'scope', capturedAt: Date.now(), scope, status: 'unavailable', reason: code == null ? 'ssh failed' : `ssh exited ${code}` }));
186
- await new Promise((resolve) => { const timer = setTimeout(resolve, options.reconnectMs ?? 2_000); options.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true }); });
187
- }
216
+ return true;
217
+ }
218
+ catch {
219
+ return false; /* protocol only */
220
+ }
221
+ },
222
+ onUnavailable: (reason) => options.emit(coordinator.emit({ type: 'scope', capturedAt: Date.now(), scope, status: 'unavailable', reason })),
223
+ });
188
224
  });
189
225
  await Promise.all([local, ...tasks]);
190
226
  }
@@ -8,7 +8,7 @@
8
8
  * (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
9
9
  */
10
10
  import type { DeviceProfile } from '../devices/registry.js';
11
- import { type RemoteBackend } from '../secrets/push.js';
11
+ import type { RemoteBackend } from '../secrets-types.js';
12
12
  import type { DeviceDesired, DeviceProbe, DeviceDiff, FleetAction, FleetPlan, AuthFilePayload } from './types.js';
13
13
  /** Strip a version suffix from an agent spec: `claude@latest` -> `claude`. */
14
14
  export declare function agentIdOf(spec: string): string;
@@ -149,7 +149,7 @@ export interface ExecContext {
149
149
  dryRun?: boolean;
150
150
  }
151
151
  /** Execute one device's planned actions in order. Real SSH — no mocks. */
152
- export declare function reconcileDevice(row: DeviceDiff, device: DeviceProfile, ctx: ExecContext): DeviceApplyResult;
152
+ export declare function reconcileDevice(row: DeviceDiff, device: DeviceProfile, ctx: ExecContext): Promise<DeviceApplyResult>;
153
153
  /** Run a pool of async tasks with a concurrency cap, preserving input order. */
154
154
  export declare function pool<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
155
155
  /** Reconcile every device row in parallel (capped). */
@@ -8,8 +8,8 @@
8
8
  * (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
9
9
  */
10
10
  import * as os from 'os';
11
- import { pushBundleToHost } from '../secrets/push.js';
12
- import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle, isReservedBundleName } from '../secrets/bundles.js';
11
+ import { pushBundleToHost } from '../secrets-client.js';
12
+ import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle, isReservedBundleName } from '../secrets-policy.js';
13
13
  import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
14
14
  import { readyProbe, bootstrapAgentsCli } from '../hosts/ready.js';
15
15
  import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
@@ -371,7 +371,7 @@ export function parseRemoteBundles(stdout) {
371
371
  }
372
372
  }
373
373
  /** Execute one device's planned actions in order. Real SSH — no mocks. */
374
- export function reconcileDevice(row, device, ctx) {
374
+ export async function reconcileDevice(row, device, ctx) {
375
375
  if (!row.probe.reachable) {
376
376
  return { device: row.device, ok: false, steps: [], note: row.probe.note ?? 'unreachable' };
377
377
  }
@@ -438,7 +438,7 @@ export function reconcileDevice(row, device, ctx) {
438
438
  }
439
439
  const backend = isReservedBundleName(bundle) || device.platform === 'linux' ? 'file' : 'keychain';
440
440
  try {
441
- const out = pushBundleToHost(bundle, target, {
441
+ const out = await pushBundleToHost(bundle, target, {
442
442
  remoteBackend: backend,
443
443
  operation: `fleet apply ${row.device}`,
444
444
  // No passphrase, ever, from this path. On the file backend the remote
@@ -138,9 +138,10 @@ export const FLEET_LOGIN_FLOWS = {
138
138
  // claude: interactive `/login` inside the TUI is a loopback browser flow, and
139
139
  // the token is keychain-bound on macOS. That native login stays per-box and
140
140
  // is never copied. The shareable `claude setup-token` mint is a different
141
- // credential class — wired as `agents accounts mint claude` / `agents auth mint`
142
- // (`lib/auth-mint.ts`), not as a fleet-login device-code flow (a setup-token
143
- // is account-scoped, not per-machine).
141
+ // credential class — minted by `agents accounts add claude [name]` /
142
+ // `agents accounts login claude#<name>` (`lib/auth-mint.ts`), not as a
143
+ // fleet-login device-code flow (a setup-token is account-scoped, not
144
+ // per-machine).
144
145
  claude: {
145
146
  loginCommand: 'claude',
146
147
  flowType: 'loopback',
@@ -267,15 +267,12 @@ async function performFleetSharedRepoSync(root, device, timeoutMs) {
267
267
  }
268
268
  const ownedFile = fleetSharedStatePath(device, root);
269
269
  const relativeOwnedFile = path.relative(root, ownedFile).split(path.sep).join('/');
270
- // The central `agents.yaml` is fleet-shared state too — `agents accounts label`
271
- // writes version-scoped native account rows there (lib/state.ts writeMetaUnlocked),
272
- // as a plain file write with no commit. Publishing only the per-device file and
273
- // then `rebase --autostash`-ing over a dirty central file silently destroyed
274
- // those labels: every box lost its account labels on its next daemon publish
275
- // (PHNX-3887). Commit it alongside the device doc so the rebase carries the
276
- // rows instead of stashing them, which is also what `accounts label --help`
277
- // already promises ("labels live on the central account rows in agents.yaml,
278
- // which repo push/pull already syncs fleet-wide").
270
+ // The central `agents.yaml` is fleet-shared state too — native account rows
271
+ // land there (lib/state.ts writeMetaUnlocked) as a plain file write with no
272
+ // commit. Publishing only the per-device file and then `rebase --autostash`-ing
273
+ // over a dirty central file silently destroyed those names: every box lost its
274
+ // account names on its next daemon publish (PHNX-3887). Commit it alongside the
275
+ // device doc so the rebase carries the rows instead of stashing them.
279
276
  const centralFile = path.join(root, 'agents.yaml');
280
277
  const publishPaths = [relativeOwnedFile];
281
278
  if (fs.existsSync(centralFile))
@@ -34,6 +34,12 @@ export interface SessionMirrorRow {
34
34
  summaryChecklist?: import('./session/types.js').SessionChecklistItem[];
35
35
  /** Lifecycle of the daemon-computed summary (PHNX-3939). */
36
36
  summaryState?: import('./session/types.js').SummaryState;
37
+ /** Tidied latest user turn, so a peer row shows what the agent was asked (PHNX-3939). */
38
+ request?: import('./session/types.js').SessionRequest;
39
+ /** Bounded narration-anchored steps, so a peer row shows what the agent did. */
40
+ timeline?: import('./session/types.js').SessionTimeline;
41
+ /** Bounded file-change list for the peer row. */
42
+ files?: import('./session/types.js').SessionFiles;
37
43
  capturedAt: number;
38
44
  }
39
45
  export interface FleetSharedDeviceState {
@@ -150,16 +150,16 @@ export interface HarnessAdapter {
150
150
  routineModeArgs?(cmd: string[], ctx: RoutineLaunchCtx): void;
151
151
  }
152
152
  /**
153
- * The four config-dir env keys a harness pins to its version home. Every
154
- * per-harness branch in the old buildExecEnv deleted the ones it does NOT set,
155
- * so its own config pointer never leaks into a foreign harness's invocation.
156
- * (XDG_CONFIG_HOME / XDG_DATA_HOME — Muse/Cursor — are deliberately absent: the
157
- * old code sets them but never strips them, and this preserves that exactly.)
153
+ * Config-dir env keys a harness pins to its slot / version home. Every
154
+ * per-harness branch in buildExecEnv deletes the ones it does NOT set, so a
155
+ * slot never inherits another account's dir (PHNX-3940 T5). GROK_HOME,
156
+ * OPENCODE_CONFIG_DIR, and the XDG pair were previously omitted, which let a
157
+ * parent agent's pin leak into a sibling slot.
158
158
  */
159
- export declare const CONFIG_DIR_ENV_KEYS: readonly ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME", "KIMI_CODE_HOME"];
159
+ export declare const CONFIG_DIR_ENV_KEYS: readonly ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME", "KIMI_CODE_HOME", "GROK_HOME", "OPENCODE_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME"];
160
160
  /**
161
161
  * Strip every config-dir env key except the one(s) this harness sets. `keep=[]`
162
- * (the default / no-config-dir harness) deletes all four — the old `else` arm.
162
+ * (the default / no-config-dir harness) deletes all of them — the old `else` arm.
163
163
  */
164
164
  export declare function stripForeignConfigDir(result: NodeJS.ProcessEnv, keep?: readonly string[]): void;
165
165
  export declare function registerHarnessAdapter(adapter: HarnessAdapter): void;