@phnx-labs/agents-cli 1.22.47 → 1.22.49

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 (249) hide show
  1. package/CHANGELOG.md +401 -17
  2. package/README.md +66 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +206 -32
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon.d.ts +21 -0
  14. package/dist/commands/daemon.js +147 -12
  15. package/dist/commands/exec.js +52 -15
  16. package/dist/commands/focus.d.ts +14 -0
  17. package/dist/commands/focus.js +48 -5
  18. package/dist/commands/go.d.ts +3 -0
  19. package/dist/commands/go.js +16 -3
  20. package/dist/commands/inspect.js +2 -2
  21. package/dist/commands/lease.d.ts +1 -1
  22. package/dist/commands/lease.js +4 -3
  23. package/dist/commands/mailboxes.js +4 -3
  24. package/dist/commands/menubar.js +11 -4
  25. package/dist/commands/projects.d.ts +16 -1
  26. package/dist/commands/projects.js +58 -64
  27. package/dist/commands/resume.js +1 -1
  28. package/dist/commands/route.js +4 -4
  29. package/dist/commands/routines.js +3 -2
  30. package/dist/commands/run-broadcast.d.ts +1 -1
  31. package/dist/commands/run-broadcast.js +1 -1
  32. package/dist/commands/sessions-picker.d.ts +22 -0
  33. package/dist/commands/sessions-picker.js +48 -9
  34. package/dist/commands/sessions-trace.js +1 -1
  35. package/dist/commands/sessions.js +1 -0
  36. package/dist/commands/setup-secrets.js +23 -0
  37. package/dist/commands/share.d.ts +20 -5
  38. package/dist/commands/share.js +152 -52
  39. package/dist/commands/ssh.js +54 -7
  40. package/dist/commands/traces.d.ts +2 -0
  41. package/dist/commands/traces.js +184 -0
  42. package/dist/commands/view.d.ts +1 -1
  43. package/dist/commands/view.js +17 -34
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +5 -0
  46. package/dist/lib/accounting/usage.d.ts +14 -3
  47. package/dist/lib/accounting/usage.js +64 -18
  48. package/dist/lib/agent-spec/agents.js +11 -9
  49. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  50. package/dist/lib/browser/drivers/ssh.js +36 -9
  51. package/dist/lib/browser/ipc.js +8 -3
  52. package/dist/lib/browser/profiles.d.ts +35 -101
  53. package/dist/lib/browser/profiles.js +130 -301
  54. package/dist/lib/browser/registry.d.ts +39 -0
  55. package/dist/lib/browser/registry.js +149 -0
  56. package/dist/lib/browser/remote-control.d.ts +8 -6
  57. package/dist/lib/browser/remote-control.js +8 -6
  58. package/dist/lib/browser/resolve-target.d.ts +68 -0
  59. package/dist/lib/browser/resolve-target.js +250 -0
  60. package/dist/lib/browser/runtime-state.d.ts +40 -16
  61. package/dist/lib/browser/runtime-state.js +92 -25
  62. package/dist/lib/browser/service.d.ts +28 -9
  63. package/dist/lib/browser/service.js +139 -62
  64. package/dist/lib/browser/task-index.d.ts +70 -0
  65. package/dist/lib/browser/task-index.js +180 -0
  66. package/dist/lib/byok-usage.js +2 -2
  67. package/dist/lib/claude-statusline.d.ts +29 -0
  68. package/dist/lib/claude-statusline.js +139 -0
  69. package/dist/lib/cloud/host.d.ts +1 -1
  70. package/dist/lib/cloud/host.js +1 -1
  71. package/dist/lib/cloud/rush.js +1 -1
  72. package/dist/lib/computer/computer-rpc.js +2 -2
  73. package/dist/lib/computer/download.d.ts +15 -15
  74. package/dist/lib/computer/download.js +35 -98
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  76. package/dist/lib/computer/ssh-tunnel.js +12 -22
  77. package/dist/lib/config-keys.d.ts +2 -8
  78. package/dist/lib/config-keys.js +2 -13
  79. package/dist/lib/crabbox/cli.d.ts +1 -1
  80. package/dist/lib/crabbox/cli.js +2 -2
  81. package/dist/lib/crabbox/lease.d.ts +9 -0
  82. package/dist/lib/crabbox/lease.js +70 -15
  83. package/dist/lib/crabbox/runtimes.js +1 -1
  84. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  85. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  86. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  87. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  88. package/dist/lib/daemon/daemon.d.ts +3 -0
  89. package/dist/lib/daemon/daemon.js +212 -444
  90. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  91. package/dist/lib/daemon/device-probe-service.js +47 -0
  92. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  93. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  94. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  95. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  96. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  97. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  98. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  99. package/dist/lib/daemon/self-heal-service.js +48 -0
  100. package/dist/lib/daemon/service.d.ts +102 -0
  101. package/dist/lib/daemon/service.js +98 -0
  102. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  103. package/dist/lib/daemon/session-index-service.js +40 -0
  104. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  105. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  106. package/dist/lib/daemon/supervisor.d.ts +87 -0
  107. package/dist/lib/daemon/supervisor.js +298 -0
  108. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  109. package/dist/lib/daemon/watchdog-service.js +40 -0
  110. package/dist/lib/daemon-health.d.ts +15 -0
  111. package/dist/lib/daemon-health.js +29 -5
  112. package/dist/lib/daemon-services.d.ts +8 -1
  113. package/dist/lib/daemon-services.js +55 -0
  114. package/dist/lib/daemon-ticks.d.ts +13 -2
  115. package/dist/lib/daemon-ticks.js +20 -18
  116. package/dist/lib/device-config.d.ts +0 -2
  117. package/dist/lib/device-config.js +1 -23
  118. package/dist/lib/devices/registry.d.ts +1 -1
  119. package/dist/lib/devices/registry.js +1 -1
  120. package/dist/lib/devices/stats-cache.d.ts +20 -0
  121. package/dist/lib/devices/stats-cache.js +49 -5
  122. package/dist/lib/devices/worker-pick.d.ts +48 -0
  123. package/dist/lib/devices/worker-pick.js +113 -0
  124. package/dist/lib/exec.d.ts +67 -13
  125. package/dist/lib/exec.js +85 -29
  126. package/dist/lib/harness/adapters/cursor.js +8 -11
  127. package/dist/lib/helper-download.d.ts +108 -0
  128. package/dist/lib/helper-download.js +194 -0
  129. package/dist/lib/helper-versions.d.ts +52 -0
  130. package/dist/lib/helper-versions.js +58 -0
  131. package/dist/lib/hooks/cache.js +11 -0
  132. package/dist/lib/hooks/install.js +2 -2
  133. package/dist/lib/hosts/credentials.d.ts +1 -1
  134. package/dist/lib/hosts/credentials.js +1 -1
  135. package/dist/lib/hosts/dispatch.d.ts +7 -1
  136. package/dist/lib/hosts/dispatch.js +24 -5
  137. package/dist/lib/hosts/passthrough.d.ts +8 -15
  138. package/dist/lib/hosts/passthrough.js +19 -16
  139. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  140. package/dist/lib/hosts/providers/devices.js +1 -1
  141. package/dist/lib/hosts/reconnect.d.ts +179 -21
  142. package/dist/lib/hosts/reconnect.js +266 -45
  143. package/dist/lib/installations/migrate.d.ts +4 -5
  144. package/dist/lib/installations/migrate.js +6 -7
  145. package/dist/lib/installations/shims.d.ts +1 -1
  146. package/dist/lib/installations/shims.js +23 -13
  147. package/dist/lib/installations/versions.js +25 -2
  148. package/dist/lib/linear-autoclose.d.ts +1 -1
  149. package/dist/lib/linear-autoclose.js +1 -1
  150. package/dist/lib/loop.d.ts +2 -2
  151. package/dist/lib/loop.js +1 -1
  152. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  153. package/dist/lib/menubar/download-menubar.js +60 -0
  154. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  155. package/dist/lib/menubar/install-menubar.js +79 -21
  156. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  157. package/dist/lib/menubar/notify-desktop.js +6 -6
  158. package/dist/lib/model-tiers.d.ts +1 -1
  159. package/dist/lib/project-probe.d.ts +1 -1
  160. package/dist/lib/project-probe.js +1 -1
  161. package/dist/lib/project-pull.d.ts +1 -1
  162. package/dist/lib/project-pull.js +2 -2
  163. package/dist/lib/projects.d.ts +1 -1
  164. package/dist/lib/projects.js +1 -1
  165. package/dist/lib/sandbox.js +6 -7
  166. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  167. package/dist/lib/secrets/download-keychain.js +70 -0
  168. package/dist/lib/secrets/install-helper.d.ts +20 -1
  169. package/dist/lib/secrets/install-helper.js +44 -3
  170. package/dist/lib/session/active.js +17 -1
  171. package/dist/lib/session/db.d.ts +11 -1
  172. package/dist/lib/session/db.js +138 -6
  173. package/dist/lib/session/discover.d.ts +21 -1
  174. package/dist/lib/session/discover.js +33 -5
  175. package/dist/lib/session/highlights.d.ts +49 -1
  176. package/dist/lib/session/highlights.js +95 -0
  177. package/dist/lib/session/host-link.d.ts +16 -2
  178. package/dist/lib/session/host-link.js +7 -1
  179. package/dist/lib/session/parse.js +33 -1
  180. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  181. package/dist/lib/session/remote/remote-list.js +19 -2
  182. package/dist/lib/session/remote/watch.js +11 -2
  183. package/dist/lib/session/session-cache.d.ts +53 -0
  184. package/dist/lib/session/session-cache.js +88 -0
  185. package/dist/lib/session/state.js +1 -1
  186. package/dist/lib/session/tool-calls.d.ts +2 -0
  187. package/dist/lib/session/tool-calls.js +1 -1
  188. package/dist/lib/session/trajectory-html.d.ts +5 -1
  189. package/dist/lib/session/trajectory-html.js +42 -22
  190. package/dist/lib/session/trajectory-text.js +9 -2
  191. package/dist/lib/session/trajectory.js +53 -4
  192. package/dist/lib/session/types.d.ts +15 -0
  193. package/dist/lib/sha256-asset.d.ts +26 -0
  194. package/dist/lib/sha256-asset.js +41 -0
  195. package/dist/lib/share/backend.d.ts +95 -0
  196. package/dist/lib/share/backend.js +139 -0
  197. package/dist/lib/share/delete.d.ts +6 -2
  198. package/dist/lib/share/delete.js +8 -9
  199. package/dist/lib/share/html.d.ts +22 -0
  200. package/dist/lib/share/html.js +88 -0
  201. package/dist/lib/share/provision.d.ts +18 -4
  202. package/dist/lib/share/provision.js +38 -4
  203. package/dist/lib/share/publish.d.ts +39 -15
  204. package/dist/lib/share/publish.js +55 -26
  205. package/dist/lib/share/worker-template.js +171 -11
  206. package/dist/lib/sqlite.js +16 -3
  207. package/dist/lib/ssh-exec.d.ts +65 -0
  208. package/dist/lib/ssh-exec.js +120 -4
  209. package/dist/lib/startup/command-registry.js +1 -1
  210. package/dist/lib/startup/dev-build.js +3 -3
  211. package/dist/lib/state.d.ts +1 -1
  212. package/dist/lib/state.js +7 -3
  213. package/dist/lib/subagents-registry.js +1 -1
  214. package/dist/lib/terminal/index.d.ts +1 -1
  215. package/dist/lib/terminal/types.d.ts +1 -1
  216. package/dist/lib/terminal/types.js +1 -1
  217. package/dist/lib/tmux/orphan-reap.js +1 -1
  218. package/dist/lib/tmux/session.js +31 -0
  219. package/dist/lib/traces/backend.d.ts +29 -0
  220. package/dist/lib/traces/backend.js +46 -0
  221. package/dist/lib/traces/classify.d.ts +52 -0
  222. package/dist/lib/traces/classify.js +72 -0
  223. package/dist/lib/traces/config.d.ts +10 -0
  224. package/dist/lib/traces/config.js +2 -0
  225. package/dist/lib/traces/provision.d.ts +19 -0
  226. package/dist/lib/traces/provision.js +30 -0
  227. package/dist/lib/traces/sync.d.ts +150 -0
  228. package/dist/lib/traces/sync.js +447 -0
  229. package/dist/lib/traces/worker-template.d.ts +4 -0
  230. package/dist/lib/traces/worker-template.js +159 -0
  231. package/dist/lib/types.d.ts +18 -19
  232. package/dist/lib/types.js +14 -0
  233. package/dist/lib/usage-refresh.d.ts +1 -1
  234. package/dist/lib/usage-refresh.js +11 -8
  235. package/dist/lib/workflows.d.ts +1 -1
  236. package/package.json +4 -6
  237. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  240. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  241. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  242. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  243. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  244. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  245. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  246. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  247. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  248. package/dist/lib/usage-fleet.d.ts +0 -32
  249. package/dist/lib/usage-fleet.js +0 -125
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Incremental sync of derived, redacted SessionTrajectory blobs to the
3
+ * agents-traces R2 store.
4
+ *
5
+ * Security invariant: only the derived signal (steps + gaps + stats +
6
+ * errorCount + programTimeShare) is uploaded — no raw transcript text.
7
+ * redactSecrets() is called inside buildTrajectory() before we PUT.
8
+ *
9
+ * Sync gate: `sessions.db` `file_mtime_ms` is the source of truth. A ledger
10
+ * at `getRuntimeStateDir()/traces-sync.json` records the last sync timestamp
11
+ * per device so re-runs skip unchanged sessions.
12
+ */
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { getDB, readSessionInsights, readSessionTopics, writeSessionInsights, writeSessionTopics, } from '../session/db.js';
17
+ import { parseSession } from '../session/parse.js';
18
+ import { buildTrajectory } from '../session/trajectory.js';
19
+ import { computeInsightFacets } from '../session/insights.js';
20
+ import { knownSecretValuesFromEnv, redactSecrets } from '../redact.js';
21
+ import { getRuntimeStateDir } from '../state.js';
22
+ import { resolveTracesBackend } from './backend.js';
23
+ import { classifyCause, classifyTopic, computeDriftSignal, } from './classify.js';
24
+ /** Push derived, redacted trajectories for this device to the traces store. */
25
+ export async function syncTraces(opts = {}) {
26
+ const dryRun = opts.dryRun === true;
27
+ const outDir = opts.outDir;
28
+ if (dryRun && !outDir) {
29
+ throw new Error('traces sync --dry-run requires --out <dir>');
30
+ }
31
+ // A dry-run computes locally and writes to disk: no Phoenix backend needed.
32
+ const backend = dryRun ? null : resolveTracesBackend();
33
+ const owner = backend?.userId ?? 'local';
34
+ const ledger = readSyncLedger();
35
+ const db = getDB();
36
+ const device = localDevice();
37
+ // Scope to this machine only — the DB can contain peer rows mirrored from the
38
+ // fleet; uploading those under this device's prefix would corrupt the index.
39
+ // NULL machine rows are legacy local sessions (pre-machine-field). A dry-run
40
+ // ignores the incremental watermark so the export covers every session.
41
+ const sinceMtime = dryRun ? 0 : (ledger.lastSyncMtime ?? 0);
42
+ const rows = db
43
+ .prepare('SELECT * FROM sessions WHERE (machine = ? OR machine IS NULL) AND file_mtime_ms > ? ORDER BY file_mtime_ms ASC')
44
+ .all(device, sinceMtime);
45
+ const limited = opts.limit !== undefined ? rows.slice(0, opts.limit) : rows;
46
+ const knownSecrets = knownSecretValuesFromEnv();
47
+ let uploaded = 0;
48
+ let skipped = 0;
49
+ let errors = 0;
50
+ // Advance the watermark only to the max mtime of successfully uploaded sessions
51
+ // so failures are retried on the next run.
52
+ let maxSuccessMtime = ledger.lastSyncMtime ?? 0;
53
+ if (dryRun && outDir) {
54
+ fs.mkdirSync(path.join(outDir, 'sessions'), { recursive: true });
55
+ }
56
+ for (const row of limited) {
57
+ if (!row.file_path) {
58
+ skipped++;
59
+ maxSuccessMtime = Math.max(maxSuccessMtime, row.file_mtime_ms ?? 0);
60
+ continue;
61
+ }
62
+ let traj;
63
+ try {
64
+ const session = rowToMeta(row);
65
+ const events = parseSession(row.file_path, row.agent);
66
+ traj = buildTrajectory(events, session, { redact: true, knownSecrets });
67
+ }
68
+ catch {
69
+ errors++;
70
+ continue;
71
+ }
72
+ try {
73
+ if (dryRun && outDir) {
74
+ fs.writeFileSync(path.join(outDir, 'sessions', `${row.id}.json`), JSON.stringify(buildSessionDetail(traj)));
75
+ }
76
+ else {
77
+ await putSessionTrace(backend, device, row.id, traj);
78
+ }
79
+ uploaded++;
80
+ maxSuccessMtime = Math.max(maxSuccessMtime, row.file_mtime_ms ?? 0);
81
+ }
82
+ catch {
83
+ errors++;
84
+ }
85
+ }
86
+ if (!opts.skipIndex) {
87
+ try {
88
+ const allRows = db
89
+ .prepare('SELECT * FROM sessions WHERE machine = ? OR machine IS NULL')
90
+ .all(device);
91
+ // Seed rolling bucket history from the previous shard so drift signals accumulate.
92
+ let prevShard = null;
93
+ if (dryRun && outDir) {
94
+ const prevPath = path.join(outDir, 'index.json');
95
+ try {
96
+ prevShard = JSON.parse(fs.readFileSync(prevPath, 'utf8'));
97
+ }
98
+ catch { /* first run — no prior shard */ }
99
+ }
100
+ else if (backend) {
101
+ prevShard = await getIndexShard(backend, device);
102
+ }
103
+ const shard = buildIndexShard(allRows, device, owner, prevShard);
104
+ if (dryRun && outDir) {
105
+ fs.writeFileSync(path.join(outDir, 'index.json'), JSON.stringify(shard, null, 2));
106
+ }
107
+ else {
108
+ await putIndexShard(backend, device, owner, shard);
109
+ }
110
+ }
111
+ catch {
112
+ // index PUT/write failure is not fatal — the per-session data is already written
113
+ }
114
+ }
115
+ // A dry-run never advances the incremental watermark: it is a read-only export.
116
+ if (!dryRun) {
117
+ writeSyncLedger({ lastSyncMtime: maxSuccessMtime });
118
+ }
119
+ return { uploaded, skipped, errors };
120
+ }
121
+ function percentile(values, ratio) {
122
+ if (values.length === 0)
123
+ return 0;
124
+ const sorted = [...values].sort((a, b) => a - b);
125
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)];
126
+ }
127
+ function failureDescription(call, cause) {
128
+ if (cause === 'guard')
129
+ return /main-branch-guard/i.test(`${call.error_code ?? ''} ${call.error ?? ''}`)
130
+ ? 'main branch guard' : 'git guard';
131
+ if (cause === 'hook')
132
+ return 'auto-mode hook denial';
133
+ if (call.status_code != null)
134
+ return `HTTP ${call.status_code}`;
135
+ if (call.exit_code != null)
136
+ return `exit ${call.exit_code}`;
137
+ if (call.error_code)
138
+ return 'tool error code';
139
+ if (call.parse_error)
140
+ return 'parse error';
141
+ return 'tool error';
142
+ }
143
+ function attentionFlags(errorCount, facets) {
144
+ const flags = [];
145
+ if (errorCount > 0)
146
+ flags.push(`${errorCount} error${errorCount === 1 ? '' : 's'}`);
147
+ const friction = facets?.frictionSignals ?? {};
148
+ const corrections = facets?.correctionSignals ?? {};
149
+ const retryCount = Object.entries(friction)
150
+ .filter(([key]) => key.startsWith('failed tool loop:'))
151
+ .reduce((sum, [, count]) => sum + count, 0);
152
+ if (retryCount > 0)
153
+ flags.push('retry loop');
154
+ const stall = Object.entries(friction).find(([key, count]) => key.startsWith('silent stall:') && count > 0);
155
+ if (stall)
156
+ flags.push(stall[0].replace('silent stall: ', 'stalled '));
157
+ const correctionCount = Object.values(corrections).reduce((sum, count) => sum + count, 0);
158
+ if (correctionCount > 0)
159
+ flags.push(`${correctionCount} correction${correctionCount === 1 ? '' : 's'}`);
160
+ return flags;
161
+ }
162
+ /** Build the redacted rich console shard from indexed metadata and derived caches. */
163
+ export function buildIndexShard(rows, device, owner, prevShard) {
164
+ const db = getDB();
165
+ const knownSecrets = knownSecretValuesFromEnv();
166
+ const ids = rows.map((row) => row.id);
167
+ const calls = [];
168
+ for (let i = 0; i < ids.length; i += 400) {
169
+ const chunk = ids.slice(i, i + 400);
170
+ calls.push(...db.prepare(`
171
+ SELECT session_id, tool, outcome, exit_code, status_code, error_code, error, parse_error
172
+ FROM tool_calls
173
+ WHERE session_id IN (${chunk.map(() => '?').join(',')})
174
+ `).all(...chunk));
175
+ }
176
+ const toolMix = new Map();
177
+ const errorCounts = new Map();
178
+ for (const call of calls) {
179
+ const mix = toolMix.get(call.session_id) ?? {};
180
+ mix[call.tool] = (mix[call.tool] ?? 0) + 1;
181
+ toolMix.set(call.session_id, mix);
182
+ if (call.outcome === 'error') {
183
+ errorCounts.set(call.session_id, (errorCounts.get(call.session_id) ?? 0) + 1);
184
+ }
185
+ }
186
+ const topics = readSessionTopics(ids);
187
+ const missingTopics = rows.filter((row) => !topics.has(row.id)).map((row) => {
188
+ const topic = classifyTopic({
189
+ cwd: row.cwd,
190
+ gitBranch: row.git_branch,
191
+ topic: row.topic,
192
+ label: row.label,
193
+ toolMix: toolMix.get(row.id),
194
+ });
195
+ topics.set(row.id, topic);
196
+ return { id: row.id, fileMtimeMs: row.file_mtime_ms, fileSize: row.file_size, topic };
197
+ });
198
+ writeSessionTopics(missingTopics);
199
+ const insights = readSessionInsights(ids);
200
+ const missingInsights = [];
201
+ for (const row of rows.filter((candidate) => !insights.has(candidate.id))) {
202
+ try {
203
+ const events = parseSession(row.file_path, row.agent);
204
+ const facets = computeInsightFacets(events);
205
+ insights.set(row.id, facets);
206
+ missingInsights.push({
207
+ id: row.id,
208
+ fileMtimeMs: row.file_mtime_ms,
209
+ fileSize: row.file_size,
210
+ facets,
211
+ });
212
+ }
213
+ catch {
214
+ continue;
215
+ }
216
+ }
217
+ writeSessionInsights(missingInsights);
218
+ const needsAttention = rows.flatMap((row) => {
219
+ const facets = insights.get(row.id);
220
+ const errorCount = errorCounts.get(row.id) ?? 0;
221
+ const flags = attentionFlags(errorCount, facets);
222
+ if (flags.length === 0)
223
+ return [];
224
+ const friction = Object.values(facets?.frictionSignals ?? {}).reduce((sum, count) => sum + count, 0);
225
+ const corrections = Object.values(facets?.correctionSignals ?? {}).reduce((sum, count) => sum + count, 0);
226
+ return [{
227
+ id: row.id,
228
+ title: redactSecrets(row.label ?? row.topic ?? topics.get(row.id)?.label ?? 'Untitled session', knownSecrets),
229
+ repo: row.project ?? (row.cwd ? path.basename(row.cwd) : 'unknown'),
230
+ device,
231
+ agent: row.agent,
232
+ model: row.model ?? 'unknown',
233
+ severity: errorCount * 2 + friction * 3 + corrections * 2,
234
+ flags,
235
+ }];
236
+ }).sort((a, b) => b.severity - a.severity || a.id.localeCompare(b.id));
237
+ const topicCounts = new Map();
238
+ for (const topic of topics.values()) {
239
+ const current = topicCounts.get(topic.key) ?? { ...topic, count: 0 };
240
+ current.count++;
241
+ topicCounts.set(topic.key, current);
242
+ }
243
+ const failedCalls = calls.filter((call) => call.outcome === 'error');
244
+ const byCause = { real: 0, guard: 0, hook: 0 };
245
+ const failureCounts = new Map();
246
+ for (const call of failedCalls) {
247
+ const cause = classifyCause(call);
248
+ const desc = failureDescription(call, cause);
249
+ byCause[cause]++;
250
+ const key = `${call.tool}\u0000${desc}\u0000${cause}`;
251
+ const current = failureCounts.get(key) ?? { tool: call.tool, desc, cause, count: 0 };
252
+ current.count++;
253
+ failureCounts.set(key, current);
254
+ }
255
+ const durations = rows.flatMap((row) => row.duration_ms == null ? [] : [row.duration_ms]);
256
+ // Build today's per-bucket stats for the rolling drift window.
257
+ const todayDate = new Date().toISOString().slice(0, 10);
258
+ const todayStats = [...topicCounts.values()].map(({ key }) => {
259
+ const sessionsInBucket = [...topics.entries()]
260
+ .filter(([, t]) => t.key === key)
261
+ .map(([id]) => id);
262
+ const bucketCalls = calls.filter((c) => sessionsInBucket.includes(c.session_id));
263
+ const bucketErrors = bucketCalls.filter((c) => c.outcome === 'error').length;
264
+ const errorRate = bucketCalls.length === 0 ? 0 : bucketErrors / bucketCalls.length;
265
+ const stallCount = sessionsInBucket.filter((id) => {
266
+ const facets = insights.get(id);
267
+ return Object.entries(facets?.frictionSignals ?? {})
268
+ .some(([k, v]) => k.startsWith('silent stall:') && v > 0);
269
+ }).length;
270
+ const stallRate = sessionsInBucket.length === 0 ? 0 : stallCount / sessionsInBucket.length;
271
+ return { key, date: todayDate, count: topicCounts.get(key).count, errorRate, stallRate };
272
+ });
273
+ const prevHistory = prevShard?.bucketHistory ?? [];
274
+ const bucketHistory = [...prevHistory, todayStats].slice(-14);
275
+ const driftSignals = computeDriftSignal(prevHistory, todayStats);
276
+ return {
277
+ schema: 1,
278
+ device,
279
+ syncedAt: Date.now(),
280
+ owner,
281
+ stats: {
282
+ sessionsImported: rows.length,
283
+ medianMs: percentile(durations, 0.5),
284
+ p90Ms: percentile(durations, 0.9),
285
+ needAttention: needsAttention.length,
286
+ toolErrorRate: calls.length === 0 ? 0 : failedCalls.length / calls.length,
287
+ },
288
+ needsAttention,
289
+ topics: [...topicCounts.values()].sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)),
290
+ failures: {
291
+ byToolError: [...failureCounts.values()].sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)),
292
+ byCause,
293
+ },
294
+ bucketHistory,
295
+ driftSignals,
296
+ };
297
+ }
298
+ /** Plain-language summary of the friction in a run, or null when it ran clean. */
299
+ function buildWhereItWentWrong(traj) {
300
+ const errorSteps = traj.steps.filter((s) => s.outcome === 'error');
301
+ const biggestGap = traj.gaps.reduce((max, g) => (!max || g.durationMs > max.durationMs ? g : max), null);
302
+ const parts = [];
303
+ if (errorSteps.length > 0) {
304
+ const first = errorSteps[0];
305
+ const who = first.tool ?? first.lane;
306
+ parts.push(`${errorSteps.length} tool error${errorSteps.length === 1 ? '' : 's'} (first: ${who} — ${first.label})`);
307
+ }
308
+ if (biggestGap && biggestGap.durationMs >= 60_000) {
309
+ parts.push(`stalled ${Math.round(biggestGap.durationMs / 60_000)}m`);
310
+ }
311
+ if (parts.length === 0)
312
+ return null;
313
+ return `This run hit ${parts.join('; ')}.`;
314
+ }
315
+ /**
316
+ * Map the derived trajectory to the console's SessionDetail shape, stripping
317
+ * local-machine PII (full cwd, account) that would expose filesystem paths if
318
+ * written to R2. `repo` is the cwd basename only.
319
+ */
320
+ export function buildSessionDetail(traj) {
321
+ const s = traj.session;
322
+ const stats = traj.stats;
323
+ const repo = s.project ?? (s.cwd ? path.basename(s.cwd) : 'unknown');
324
+ return {
325
+ schema: 1,
326
+ id: s.id,
327
+ meta: {
328
+ spanMs: traj.spanMs,
329
+ turns: (stats.userTurns ?? 0) + (stats.assistantTurns ?? 0),
330
+ tools: stats.toolCount ?? 0,
331
+ errorCount: traj.errorCount,
332
+ tokens: stats.outputTokens ?? 0,
333
+ costUsd: s.costUsd ?? 0,
334
+ outcome: traj.errorCount > 0 ? 'errored' : 'completed',
335
+ repo,
336
+ agent: s.agent,
337
+ model: s.model ?? 'unknown',
338
+ },
339
+ steps: traj.steps,
340
+ gaps: traj.gaps,
341
+ truncatedSteps: traj.truncatedSteps,
342
+ whereItWentWrong: buildWhereItWentWrong(traj),
343
+ };
344
+ }
345
+ async function getIndexShard(backend, device) {
346
+ const url = `${backend.baseUrl}/${backend.userId}/${device}/index.json`;
347
+ try {
348
+ const res = await fetch(url, {
349
+ headers: { authorization: `Bearer ${backend.token}` },
350
+ });
351
+ if (!res.ok)
352
+ return null;
353
+ return await res.json();
354
+ }
355
+ catch {
356
+ return null;
357
+ }
358
+ }
359
+ async function putSessionTrace(backend, device, sessionId, traj) {
360
+ const url = `${backend.baseUrl}/${backend.userId}/${device}/sessions/${sessionId}.json`;
361
+ const res = await fetch(url, {
362
+ method: 'PUT',
363
+ headers: {
364
+ authorization: `Bearer ${backend.token}`,
365
+ 'content-type': 'application/json; charset=utf-8',
366
+ },
367
+ body: JSON.stringify(buildSessionDetail(traj)),
368
+ });
369
+ if (!res.ok) {
370
+ throw new Error(`PUT ${url} → ${res.status}`);
371
+ }
372
+ }
373
+ async function putIndexShard(backend, device, owner, shard) {
374
+ const full = { ...shard, device, owner, syncedAt: Date.now() };
375
+ const url = `${backend.baseUrl}/${backend.userId}/${device}/index.json`;
376
+ const res = await fetch(url, {
377
+ method: 'PUT',
378
+ headers: {
379
+ authorization: `Bearer ${backend.token}`,
380
+ 'content-type': 'application/json; charset=utf-8',
381
+ },
382
+ body: JSON.stringify(full),
383
+ });
384
+ if (!res.ok) {
385
+ throw new Error(`PUT ${url} → ${res.status}`);
386
+ }
387
+ }
388
+ function ledgerPath() {
389
+ return path.join(getRuntimeStateDir(), 'traces-sync.json');
390
+ }
391
+ export function readSyncLedger() {
392
+ try {
393
+ const raw = fs.readFileSync(ledgerPath(), 'utf8');
394
+ return JSON.parse(raw);
395
+ }
396
+ catch {
397
+ return {};
398
+ }
399
+ }
400
+ export function writeSyncLedger(ledger) {
401
+ const p = ledgerPath();
402
+ fs.mkdirSync(path.dirname(p), { recursive: true });
403
+ fs.writeFileSync(p, JSON.stringify(ledger, null, 2) + '\n', 'utf8');
404
+ }
405
+ // ---------------------------------------------------------------------------
406
+ // Local device name
407
+ // ---------------------------------------------------------------------------
408
+ function localDevice() {
409
+ return (process.env['AGENTS_SYNC_MACHINE_ID'] ?? os.hostname()).toLowerCase().replace(/\.local$/, '');
410
+ }
411
+ /** Minimal subset of rowToMeta needed to satisfy buildTrajectory's SessionMeta param. */
412
+ function rowToMeta(row) {
413
+ const SESSION_RUN_MODES = ['plan', 'edit', 'auto', 'skip'];
414
+ return {
415
+ id: row.id,
416
+ shortId: row.short_id,
417
+ agent: row.agent,
418
+ origin: row.origin === 'routine' ? 'routine' : 'cli',
419
+ routineName: row.routine_name ?? undefined,
420
+ routineRunId: row.routine_run_id ?? undefined,
421
+ timestamp: row.timestamp,
422
+ lastActivity: row.last_activity ?? undefined,
423
+ project: row.project ?? undefined,
424
+ cwd: row.cwd ?? undefined,
425
+ filePath: row.file_path,
426
+ gitBranch: row.git_branch ?? undefined,
427
+ messageCount: row.message_count ?? undefined,
428
+ tokenCount: row.token_count ?? undefined,
429
+ outputTokens: row.output_tokens ?? undefined,
430
+ inputTokens: row.input_tokens ?? undefined,
431
+ cacheReadTokens: row.cache_read_tokens ?? undefined,
432
+ cacheWriteTokens: row.cache_write_tokens ?? undefined,
433
+ costUsd: row.cost_usd ?? undefined,
434
+ costUsdNoCache: row.cost_usd_nocache ?? undefined,
435
+ durationMs: row.duration_ms ?? undefined,
436
+ model: row.model ?? undefined,
437
+ toolCallCount: row.tool_call_count ?? undefined,
438
+ version: row.version ?? undefined,
439
+ account: row.account ?? undefined,
440
+ accountKey: row.account_key ?? undefined,
441
+ accountOrg: row.account_org ?? undefined,
442
+ mode: SESSION_RUN_MODES.includes(row.mode ?? '') ? row.mode : undefined,
443
+ topic: row.topic ?? undefined,
444
+ label: row.label ?? undefined,
445
+ machine: row.machine ?? undefined,
446
+ };
447
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Render the traces Worker source. Pure — bindings are wired at deploy time.
3
+ */
4
+ export declare function renderTracesWorkerScript(): string;
@@ -0,0 +1,159 @@
1
+ // The Cloudflare Worker that fronts the R2 traces bucket.
2
+ //
3
+ // SECURITY: unlike agents-share, there is NO public GET path here. Every route
4
+ // — PUT and GET — requires a Phoenix bearer. The caller must be the record's
5
+ // owner (path prefix == verified userId). A missing bearer → 401; a wrong
6
+ // owner → 403; public cache headers are NEVER emitted.
7
+ //
8
+ // Routes (all require Phoenix bearer + userId owner match):
9
+ // PUT /<userId>/<device>/index.json — per-device stats shard
10
+ // PUT /<userId>/<device>/sessions/<id>.json — per-session SessionDetail
11
+ // GET /<userId>/... — returns the stored object
12
+ // GET / — 200 description (no data)
13
+ //
14
+ // Emitted as a string so it compiles into dist/** and ships with no
15
+ // package.json#files change. `provisionTraces.ts` uploads this verbatim as an
16
+ // ES-module Worker with a BUCKET (R2) binding + a PHOENIX_ID_BASE secret.
17
+ /**
18
+ * Render the traces Worker source. Pure — bindings are wired at deploy time.
19
+ */
20
+ export function renderTracesWorkerScript() {
21
+ return `// GENERATED by agents-cli agents traces — do not edit; edit
22
+ // src/lib/traces/worker-template.ts and re-provision.
23
+ export const hooks = {
24
+ verifyPhoenixToken: defaultVerifyPhoenixToken,
25
+ };
26
+
27
+ export default {
28
+ async fetch(request, env) {
29
+ const url = new URL(request.url);
30
+ const path = decodeURIComponent(url.pathname.replace(/^\\/+/, ''));
31
+
32
+ if (!path) {
33
+ return new Response(
34
+ 'agents traces — PUT /<userId>/<device>/index.json or sessions/<id>.json to upload; GET the same path to read. All routes require a Phoenix bearer.',
35
+ { status: 200, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'private, no-store' } },
36
+ );
37
+ }
38
+
39
+ if (request.method === 'PUT') {
40
+ return handlePut(request, env, path);
41
+ }
42
+
43
+ if (request.method === 'GET' || request.method === 'HEAD') {
44
+ return handleGet(request, env, path);
45
+ }
46
+
47
+ return json({ error: 'method not allowed' }, 405);
48
+ },
49
+ };
50
+
51
+ async function handlePut(request, env, path) {
52
+ const auth = await authorizeWrite(request, env);
53
+ if (auth.error) return auth.error;
54
+
55
+ const segments = path.split('/').filter(Boolean);
56
+ const expected = auth.owner;
57
+ if (!expected || segments[0] !== expected) {
58
+ return json({ error: 'namespace mismatch', owner: expected }, 403);
59
+ }
60
+
61
+ const contentType = request.headers.get('content-type') || 'application/json; charset=utf-8';
62
+ await env.BUCKET.put(path, request.body, {
63
+ httpMetadata: { contentType },
64
+ customMetadata: {
65
+ owner: auth.owner,
66
+ uploadedAt: new Date().toISOString(),
67
+ },
68
+ });
69
+
70
+ const headers = new Headers();
71
+ headers.set('cache-control', 'private, no-store');
72
+ return new Response(JSON.stringify({ ok: true, key: path }), {
73
+ status: 200,
74
+ headers,
75
+ });
76
+ }
77
+
78
+ async function handleGet(request, env, path) {
79
+ const auth = await authorizeRead(request, env, path);
80
+ if (auth.error) return auth.error;
81
+
82
+ const object = await env.BUCKET.get(path);
83
+ if (!object) return json({ error: 'not found' }, 404);
84
+
85
+ const headers = new Headers(object.httpMetadata ?? {});
86
+ headers.set('cache-control', 'private, no-store');
87
+ return new Response(object.body, { headers });
88
+ }
89
+
90
+ // Require a Phoenix bearer that owns the path prefix.
91
+ async function authorizeRead(request, env, path) {
92
+ const claims = await hooks.verifyPhoenixToken(request, env);
93
+ if (!claims) return { error: json({ error: 'unauthorized' }, 401) };
94
+ const ownerId = path.split('/').filter(Boolean)[0] || '';
95
+ if (!ownerId || ownerId !== claims.userId) {
96
+ return { error: json({ error: 'forbidden' }, 403) };
97
+ }
98
+ return { claims };
99
+ }
100
+
101
+ // Two legitimate principals for writes: static WRITE_TOKEN first (BYO),
102
+ // otherwise Phoenix bearer. 401 when neither authenticates.
103
+ async function authorizeWrite(request, env) {
104
+ const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
105
+ if (!presented) return { error: json({ error: 'unauthorized' }, 401) };
106
+ if (env.WRITE_TOKEN && safeEqual(presented, env.WRITE_TOKEN)) {
107
+ return { kind: 'byo', owner: env.TRACES_NAMESPACE || 'byo' };
108
+ }
109
+ const claims = await hooks.verifyPhoenixToken(request, env);
110
+ if (claims && typeof claims.userId === 'string' && claims.userId) {
111
+ return { kind: 'phoenix', owner: claims.userId };
112
+ }
113
+ return { error: json({ error: 'unauthorized' }, 401) };
114
+ }
115
+
116
+ async function defaultVerifyPhoenixToken(request, env) {
117
+ const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
118
+ if (!presented) return null;
119
+ const base = typeof env.PHOENIX_ID_BASE === 'string' ? env.PHOENIX_ID_BASE.replace(/\\/+$/, '') : '';
120
+ if (!base) return null;
121
+ let res;
122
+ try {
123
+ res = await fetch(base + '/api/v1/auth/me', {
124
+ method: 'GET',
125
+ headers: { Authorization: 'Bearer ' + presented },
126
+ });
127
+ } catch {
128
+ return null;
129
+ }
130
+ if (!res.ok) return null;
131
+ let body;
132
+ try {
133
+ body = await res.json();
134
+ } catch {
135
+ return null;
136
+ }
137
+ if (!body || typeof body.userId !== 'string' || !body.userId) return null;
138
+ return { userId: body.userId, email: typeof body.email === 'string' ? body.email : '' };
139
+ }
140
+
141
+ function json(body, status = 200) {
142
+ return new Response(JSON.stringify(body), {
143
+ status,
144
+ headers: {
145
+ 'content-type': 'application/json; charset=utf-8',
146
+ 'cache-control': 'private, no-store',
147
+ },
148
+ });
149
+ }
150
+
151
+ // Constant-time comparison so timing attacks can't probe a static WRITE_TOKEN.
152
+ function safeEqual(a, b) {
153
+ if (a.length !== b.length) return false;
154
+ let diff = 0;
155
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
156
+ return diff === 0;
157
+ }
158
+ `;
159
+ }
@@ -29,6 +29,20 @@ export declare const RUN_AUTO_KEYWORD = "auto";
29
29
  * process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
30
30
  */
31
31
  export declare const RUN_AUTO_HOST_RESOLVED_ENV = "AGENTS_RUN_AUTO_HOST_RESOLVED";
32
+ /**
33
+ * Env var a host dispatcher exports into the remote SHELL when it opens an
34
+ * INTERACTIVE `--device` run: tells the remote CLI that this agent's stdio is a
35
+ * network link, so the run must be detached (tmux-wrapped) to outlive it.
36
+ *
37
+ * Rides the same shell-export prelude as {@link RUN_AUTO_HOST_RESOLVED_ENV},
38
+ * and for the same reason — exec.ts `resolveTmuxWrap` reads the remote CLI's own
39
+ * `process.env`, which `--env` flags (they reach only the spawned AGENT) never
40
+ * touch.
41
+ *
42
+ * Set ONLY on the interactive path. A headless `--device` dispatch is already
43
+ * detached with setsid by `launchDetached`, so it neither needs nor gets this.
44
+ */
45
+ export declare const REMOTE_INTERACTIVE_ENV = "AGENTS_REMOTE_INTERACTIVE";
32
46
  /** Per-agent run strategy config. */
33
47
  export interface AgentRunConfig {
34
48
  strategy?: RunStrategy;
@@ -958,30 +972,15 @@ export interface Meta {
958
972
  */
959
973
  hooks?: Record<string, ManifestHook>;
960
974
  /**
961
- * Browser profile definitions keyed by profile name. Portable user config
962
- * that syncs with `agents repo push/pull`. Runtime state (chrome-data, pids)
963
- * lives separately in ~/.agents/.cache/browser/<profile>/.
964
- */
965
- browser?: Record<string, BrowserProfileConfig>;
966
- /**
967
- * Machine-local browser profiles — written to `config:`-adjacent
968
- * `browser:` in `~/.agents/devices/<machine>/agents.yaml`, never the synced
969
- * file.
970
- *
971
- * The auto-detected `default` profile lives here because it is inherently
972
- * machine-specific: it carries an absolute `binary:` path and a port chosen by
973
- * scanning THIS machine. Sharing it fleet-wide meant a macOS box wrote
974
- * `/Applications/...`, a Linux box found it unlaunchable and rewrote
975
- * `/usr/bin/chromium-browser`, and the two flipped the tracked agents.yaml back
976
- * and forth on every `agents browser start` — the single largest source of
977
- * churn on that file. User-created named profiles stay central and still sync.
975
+ * Browser profiles declared by this machine. Written as `browser:` in
976
+ * `~/.agents/devices/<machine>/agents.yaml`. The fleet registry is the union
977
+ * of every device file; central `agents.yaml` is not a browser-profile store.
978
978
  */
979
979
  deviceBrowser?: Record<string, BrowserProfileConfig>;
980
980
  /**
981
981
  * User-scope config block (`config:` in central agents.yaml). Holds the
982
982
  * user-scope keys from the device-config registry (`lib/device-config.ts`) —
983
- * currently `interactiveHost` and `usagePrimaryHost`. Syncs fleet-wide via
984
- * `agents repo push/pull`.
983
+ * currently `interactiveHost`. Syncs fleet-wide via `agents repo push/pull`.
985
984
  * Device-scope keys live in the per-device doc
986
985
  * `devices/<name>/agents.yaml` `config:` block, layered over the fleet-wide
987
986
  * defaults in {@link Meta.fleet} (`fleet.defaults.config`).
package/dist/lib/types.js CHANGED
@@ -22,6 +22,20 @@ export const RUN_AUTO_KEYWORD = 'auto';
22
22
  * process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
23
23
  */
24
24
  export const RUN_AUTO_HOST_RESOLVED_ENV = 'AGENTS_RUN_AUTO_HOST_RESOLVED';
25
+ /**
26
+ * Env var a host dispatcher exports into the remote SHELL when it opens an
27
+ * INTERACTIVE `--device` run: tells the remote CLI that this agent's stdio is a
28
+ * network link, so the run must be detached (tmux-wrapped) to outlive it.
29
+ *
30
+ * Rides the same shell-export prelude as {@link RUN_AUTO_HOST_RESOLVED_ENV},
31
+ * and for the same reason — exec.ts `resolveTmuxWrap` reads the remote CLI's own
32
+ * `process.env`, which `--env` flags (they reach only the spawned AGENT) never
33
+ * touch.
34
+ *
35
+ * Set ONLY on the interactive path. A headless `--device` dispatch is already
36
+ * detached with setsid by `launchDetached`, so it neither needs nor gets this.
37
+ */
38
+ export const REMOTE_INTERACTIVE_ENV = 'AGENTS_REMOTE_INTERACTIVE';
25
39
  /** Every canonical mode in declaration order. Useful for iteration / validation. */
26
40
  export const ALL_MODES = ['plan', 'edit', 'auto', 'skip'];
27
41
  /** Canonical system repo cloned into ~/.agents/.system/. */