@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
@@ -10,7 +10,7 @@
10
10
  * bounded `.git` walk over a handful of candidate dirs) and is the only
11
11
  * non-pure function here.
12
12
  */
13
- import type { SessionEvent } from './types.js';
13
+ import type { SessionAgentId, SessionEvent } from './types.js';
14
14
  import { type FileChange } from './digest.js';
15
15
  export interface SkillUse {
16
16
  name: string;
@@ -100,3 +100,51 @@ export declare function extractArtifacts(changes: FileChange[]): ProducedArtifac
100
100
  * happens to run in, which is a wrong answer, not a degraded one.
101
101
  */
102
102
  export declare function extractRepos(events: SessionEvent[], cwd?: string): string[];
103
+ /**
104
+ * True when this harness records backgrounded shells at all. Callers MUST gate
105
+ * on this before rendering a count: for a harness that cannot report it, "0" is
106
+ * a lie ("none running") where the truth is "unknown", so the segment is omitted
107
+ * entirely — the same way the Hooks line renders for Claude only.
108
+ */
109
+ export declare function harnessTracksBackgroundShells(agent: SessionAgentId): boolean;
110
+ /**
111
+ * True for a tool_use event that starts a background shell. Exported for the
112
+ * same reason {@link isSkillInvocation} is: the incremental parser can select
113
+ * matching events while streaming instead of re-reading the transcript.
114
+ */
115
+ export declare function isBackgroundShellStart(e: SessionEvent): boolean;
116
+ export interface BackgroundShell {
117
+ /** The command line as invoked, trimmed for display. */
118
+ command: string;
119
+ timestamp: string;
120
+ }
121
+ /**
122
+ * Shells the session started in the background, oldest first.
123
+ *
124
+ * This is "started", NOT "still running". A transcript records the start and
125
+ * never the death, and inferring liveness from its absence is the same trap
126
+ * `agents devices ps` documents — a killed process leaves no marker, so a naive
127
+ * reader reports "running" forever. Live status belongs to `sessions --active`,
128
+ * which resolves real pids.
129
+ */
130
+ export declare function extractBackgroundShells(events: SessionEvent[]): BackgroundShell[];
131
+ /**
132
+ * True for a tool call that spawns another agent. Deliberately counts BOTH
133
+ * shapes, because both are fan-out the operator needs to see:
134
+ *
135
+ * in-process an `Agent` / `Task` tool call inside this transcript
136
+ * shelled-out a Bash command that invokes `agents run`, `agents cloud run`,
137
+ * or `agents teams add|start`
138
+ *
139
+ * Moved here from `commands/sessions-picker.ts` so BOTH preview body paths (the
140
+ * digest body and `formatMetaOnlyBody`) share one definition instead of
141
+ * drifting apart. Semantics are unchanged: session f045b577 counts 23
142
+ * in-process + 30 shelled-out = 53.
143
+ *
144
+ * The full summary's `Subagents (N)` section (`render.ts`) deliberately does NOT
145
+ * use this. It builds a described ROSTER, so it needs `args.description` /
146
+ * `args.prompt` and is necessarily limited to in-process `Agent`/`Task` calls —
147
+ * a shelled-out `agents run` carries no such description. Different question,
148
+ * different answer; do not "unify" them.
149
+ */
150
+ export declare function isSubAgentTool(tool: string, command: string): boolean;
@@ -294,3 +294,98 @@ export function extractRepos(events, cwd) {
294
294
  }
295
295
  return repos;
296
296
  }
297
+ // ── Background shells ─────────────────────────────────────────────────────────
298
+ /**
299
+ * Per-harness shape of "start this shell command in the background", keyed the
300
+ * same way {@link SKILL_TOOL_NAME_BY_AGENT} is: one table entry per harness that
301
+ * has the capability, so adding a harness is a row and never another `else if`.
302
+ *
303
+ * An absent entry means "this harness cannot report it", which the renders must
304
+ * show as ABSENCE, never as a count of zero. Every absence below was probed
305
+ * against that harness's real transcripts on this fleet rather than assumed:
306
+ *
307
+ * codex 0 of 176 session files carry any background marker; its `exec` /
308
+ * `exec_command` record only `cmd` + `workdir`, no shell handle.
309
+ * droid 0 of 126 session files.
310
+ * antigravity 0 of 54 session files.
311
+ * opencode 0 of 79 session files.
312
+ * cursor stores no tool calls locally at all — only `meta.json` and
313
+ * `prompt_history.json`; the calls live server-side.
314
+ *
315
+ * Not probed, because no version home is installed on this fleet to probe:
316
+ * `muse`, `rush`, `hermes`, `openclaw`, `gemini`. They are absent for lack of
317
+ * evidence, NOT on a verified negative — add a row once one is confirmed, and
318
+ * until then absence renders as "unknown", which is the safe direction.
319
+ */
320
+ const BACKGROUND_SHELL_BY_AGENT = {
321
+ claude: { tool: 'Bash', flag: 'run_in_background' },
322
+ kimi: { tool: 'Bash', flag: 'run_in_background' },
323
+ grok: { tool: 'run_terminal_command', flag: 'background' },
324
+ };
325
+ /**
326
+ * True when this harness records backgrounded shells at all. Callers MUST gate
327
+ * on this before rendering a count: for a harness that cannot report it, "0" is
328
+ * a lie ("none running") where the truth is "unknown", so the segment is omitted
329
+ * entirely — the same way the Hooks line renders for Claude only.
330
+ */
331
+ export function harnessTracksBackgroundShells(agent) {
332
+ return BACKGROUND_SHELL_BY_AGENT[agent] !== undefined;
333
+ }
334
+ /**
335
+ * True for a tool_use event that starts a background shell. Exported for the
336
+ * same reason {@link isSkillInvocation} is: the incremental parser can select
337
+ * matching events while streaming instead of re-reading the transcript.
338
+ */
339
+ export function isBackgroundShellStart(e) {
340
+ if (e.type !== 'tool_use' || e._local)
341
+ return false;
342
+ const spec = BACKGROUND_SHELL_BY_AGENT[e.agent];
343
+ return !!spec && e.tool === spec.tool && e.args?.[spec.flag] === true;
344
+ }
345
+ /**
346
+ * Shells the session started in the background, oldest first.
347
+ *
348
+ * This is "started", NOT "still running". A transcript records the start and
349
+ * never the death, and inferring liveness from its absence is the same trap
350
+ * `agents devices ps` documents — a killed process leaves no marker, so a naive
351
+ * reader reports "running" forever. Live status belongs to `sessions --active`,
352
+ * which resolves real pids.
353
+ */
354
+ export function extractBackgroundShells(events) {
355
+ const out = [];
356
+ for (const e of events) {
357
+ if (!isBackgroundShellStart(e))
358
+ continue;
359
+ const raw = e.command ?? e.args?.command;
360
+ out.push({
361
+ command: typeof raw === 'string' ? raw.trim() : '',
362
+ timestamp: e.timestamp,
363
+ });
364
+ }
365
+ return out;
366
+ }
367
+ // ── Sub-agents ────────────────────────────────────────────────────────────────
368
+ /**
369
+ * True for a tool call that spawns another agent. Deliberately counts BOTH
370
+ * shapes, because both are fan-out the operator needs to see:
371
+ *
372
+ * in-process an `Agent` / `Task` tool call inside this transcript
373
+ * shelled-out a Bash command that invokes `agents run`, `agents cloud run`,
374
+ * or `agents teams add|start`
375
+ *
376
+ * Moved here from `commands/sessions-picker.ts` so BOTH preview body paths (the
377
+ * digest body and `formatMetaOnlyBody`) share one definition instead of
378
+ * drifting apart. Semantics are unchanged: session f045b577 counts 23
379
+ * in-process + 30 shelled-out = 53.
380
+ *
381
+ * The full summary's `Subagents (N)` section (`render.ts`) deliberately does NOT
382
+ * use this. It builds a described ROSTER, so it needs `args.description` /
383
+ * `args.prompt` and is necessarily limited to in-process `Agent`/`Task` calls —
384
+ * a shelled-out `agents run` carries no such description. Different question,
385
+ * different answer; do not "unify" them.
386
+ */
387
+ export function isSubAgentTool(tool, command) {
388
+ if (/^(Agent|Task)$/i.test(tool))
389
+ return true;
390
+ return /\b(?:agents|ag)\b[^\n;&|]*\b(?:run|cloud\s+run|teams\s+(?:add|start))\b/.test(command);
391
+ }
@@ -25,12 +25,26 @@
25
25
  */
26
26
  /** How a session is connected to the client that should be driving it. */
27
27
  export type HostLink =
28
- /** A client is attached, or nothing indicates otherwise. The normal case. */
28
+ /** A client is attached positively established, not assumed. */
29
29
  'connected'
30
30
  /** Alive, but nothing is viewing it — the host window is gone and the agent outlived it. */
31
31
  | 'no-client'
32
32
  /** The host window is gone AND the agent process died with it — an unclean exit. */
33
- | 'host-gone';
33
+ | 'host-gone'
34
+ /**
35
+ * Alive, and we have NO usable signal either way — no window owns it and it is
36
+ * not tmux-hosted, so neither input this function keys on exists.
37
+ *
38
+ * This case used to return `connected`, which reads as "verified fine" when the
39
+ * truth is "nobody looked". That is the wrong default for a detector: the rows
40
+ * it silently blesses are precisely the ones with no observer — a bare terminal,
41
+ * a team spawn, a cloud task, or any session whose pane lives on another
42
+ * machine (this function's inputs are all local, so a `--device` session is
43
+ * structurally unobservable from the launching box).
44
+ *
45
+ * Callers MUST NOT render this as healthy. It means the question is open.
46
+ */
47
+ | 'unknown';
34
48
  /**
35
49
  * How long an IDE window's registry slice may go without a refresh before we
36
50
  * treat that window as gone. AGI EXT force-republishes its slice
@@ -60,5 +60,11 @@ export function classifyHostLink(input) {
60
60
  // that owned it is gone. The agent outlived its editor.
61
61
  if (windowGone)
62
62
  return 'no-client';
63
- return 'connected';
63
+ // Positive evidence of a client: tmux counted at least one attached client, or
64
+ // a window is refreshing its registry slice. Either is a real observation.
65
+ if (input.tmuxClients !== undefined || input.windowHeartbeatMs !== undefined)
66
+ return 'connected';
67
+ // Neither input exists, so nothing here observed anything. Say so rather than
68
+ // reporting the healthy answer by default — see {@link HostLink}'s `unknown`.
69
+ return 'unknown';
64
70
  }
@@ -11,7 +11,7 @@ import { sanitizeForTerminal } from '../redact.js';
11
11
  import * as path from 'path';
12
12
  import Database from '../sqlite.js';
13
13
  import { isSyntheticUserMessage, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
14
- import { structuredToolResult } from './tool-calls.js';
14
+ import { structuredToolResult, commandsFromCodexExec } from './tool-calls.js';
15
15
  /**
16
16
  * Largest session file we will load into memory. Above this we throw a clean
17
17
  * error instead of OOMing or hitting V8's ERR_STRING_TOO_LONG. Aligns with
@@ -270,6 +270,15 @@ export function summarizeToolUse(tool, args) {
270
270
  // Codex tools
271
271
  case 'exec_command':
272
272
  return `Bash: ${truncate(String(args.command || args.cmd || '').replace(/\n/g, ' ').trim(), 120)}`;
273
+ // Newer Codex JS-cell shell wrapper: prefer the unwrapped command (set at parse
274
+ // time), else the extracted command, else the raw cell code.
275
+ case 'exec': {
276
+ const cmd = args.command || codexExecCommand(String(args.input || ''));
277
+ if (cmd)
278
+ return `Bash: ${truncate(String(cmd).replace(/\n/g, ' ').trim(), 120)}`;
279
+ const code = String(args.input || '').replace(/\n/g, ' ').trim();
280
+ return code ? `exec: ${truncate(code, 100)}` : 'exec';
281
+ }
273
282
  case 'read_file':
274
283
  return `Read ${shortenPath(args.file_path || args.path || '')}`;
275
284
  case 'write_file':
@@ -573,6 +582,23 @@ export function applyPatchTargetPaths(input) {
573
582
  function applyPatchTargetPath(input) {
574
583
  return applyPatchTargetPaths(input)[0];
575
584
  }
585
+ /**
586
+ * Newer Codex (gpt-5.6-sol / codex >=~0.145) runs every shell command inside a JS
587
+ * cell: a `custom_tool_call` named `exec` whose `input` is code like
588
+ * `const r = await tools.exec_command({cmd:"git status", "workdir":"…"});`. The real
589
+ * shell command(s) are buried in those `cmd:"…"` literals. Reuse the canonical
590
+ * acorn-based extractor (`commandsFromCodexExec` in `tool-calls.ts`, already used by
591
+ * the tool-call index): it walks the whole AST — so a cell that runs several via
592
+ * `Promise.all([tools.exec_command(...), tools.exec_command(...)])` yields every
593
+ * command, not just the first — and matches only real `tools.exec_command` calls,
594
+ * so a `cmd:"…"` string mentioned in a comment or docstring is never mistaken for an
595
+ * invocation. Returns `undefined` for a non-shell cell (`tools.view_image(...)`, a
596
+ * raw JS computation) so it stays labeled by its code, not faked into a shell step.
597
+ */
598
+ function codexExecCommand(input) {
599
+ const commands = commandsFromCodexExec(input);
600
+ return commands.length > 0 ? commands.join('\n') : undefined;
601
+ }
576
602
  /**
577
603
  * Parse Codex JSONL *content* (already read into a string) into normalized
578
604
  * events. Split from `parseCodex` so the tail reader can parse just the last
@@ -706,6 +732,9 @@ export function parseCodexContent(content) {
706
732
  const rawName = payload.name || 'unknown';
707
733
  const input = typeof payload.input === 'string' ? payload.input : '';
708
734
  const isApplyPatch = rawName === 'apply_patch';
735
+ // Newer Codex wraps shell in a JS `exec` cell — unwrap the real command so
736
+ // the step reads by its program instead of a bare "exec" (see extractor).
737
+ const execCommand = rawName === 'exec' ? codexExecCommand(input) : undefined;
709
738
  // Multi-file patches: one tool_use per file so artifact discovery sees
710
739
  // every path (RUSH-1410). Single-file / non-patch keep one event.
711
740
  const patchPaths = isApplyPatch ? applyPatchTargetPaths(input) : [];
@@ -715,6 +744,8 @@ export function parseCodexContent(content) {
715
744
  const args = { input: truncatedInput };
716
745
  if (patchPath)
717
746
  args.file_path = patchPath;
747
+ if (execCommand)
748
+ args.command = execCommand;
718
749
  const callId = payload.call_id || payload.id;
719
750
  if (callId)
720
751
  callMap.set(callId, { name: tool, args });
@@ -725,6 +756,7 @@ export function parseCodexContent(content) {
725
756
  tool,
726
757
  callId,
727
758
  args,
759
+ command: execCommand,
728
760
  path: patchPath,
729
761
  });
730
762
  };
@@ -141,8 +141,17 @@ export declare function fetchPeerPreviewDigest(sessionId: string, machine: strin
141
141
  * that would inherit it for its whole life — `agents browser start` inside it
142
142
  * would then be refused as a cross-machine drive. A one-shot `--device` command
143
143
  * can carry the marker; a session cannot.
144
+ *
145
+ * `opts.sessionId` (with `tty`) prints the session id and resume command when
146
+ * the SSH hop ends, so OpenSSH's `Shared connection … closed.` is not the last
147
+ * thing on the local shell (RUSH-3227). Omit it for one-shot non-TTY renders.
144
148
  */
149
+ export declare function peerHopCloseNotice(opts: {
150
+ tty?: boolean;
151
+ sessionId?: string;
152
+ }, machine: string, code: number | null): string | undefined;
145
153
  export declare function runOnPeer(args: string[], machine: string, opts?: {
146
154
  tty?: boolean;
147
155
  env?: Record<string, string>;
156
+ sessionId?: string;
148
157
  }): Promise<'ok' | 'no-target'>;
@@ -15,7 +15,8 @@
15
15
  */
16
16
  import { spawn } from 'child_process';
17
17
  import chalk from 'chalk';
18
- import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote, REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator, } from '../../ssh-exec.js';
18
+ import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote, SSH_CONN_FAILURE_CODE, REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator, } from '../../ssh-exec.js';
19
+ import { connectionEndedNotice } from '../../hosts/reconnect.js';
19
20
  import { sshTargetFor } from '../../devices/connect.js';
20
21
  import { resolveExplicitTargetSet } from '../../devices/resolve-target.js';
21
22
  import { loadDevices, isDialableDevice } from '../../devices/registry.js';
@@ -577,7 +578,16 @@ export async function fetchPeerPreviewDigest(sessionId, machine, timeoutMs = PEE
577
578
  * that would inherit it for its whole life — `agents browser start` inside it
578
579
  * would then be refused as a cross-machine drive. A one-shot `--device` command
579
580
  * can carry the marker; a session cannot.
581
+ *
582
+ * `opts.sessionId` (with `tty`) prints the session id and resume command when
583
+ * the SSH hop ends, so OpenSSH's `Shared connection … closed.` is not the last
584
+ * thing on the local shell (RUSH-3227). Omit it for one-shot non-TTY renders.
580
585
  */
586
+ export function peerHopCloseNotice(opts, machine, code) {
587
+ if (!opts.tty || !opts.sessionId)
588
+ return undefined;
589
+ return connectionEndedNotice({ kind: 'session', id: opts.sessionId }, machine, { dropped: code === SSH_CONN_FAILURE_CODE });
590
+ }
581
591
  export async function runOnPeer(args, machine, opts = {}) {
582
592
  const peer = await resolvePeerTarget(machine);
583
593
  if (!peer)
@@ -602,6 +612,13 @@ export async function runOnPeer(args, machine, opts = {}) {
602
612
  process.stderr.write(chalk.red(`Failed to reach ${machine}: ${err?.message ?? 'ssh failed to launch'}\n`));
603
613
  resolve('ok');
604
614
  });
605
- child.on('close', () => resolve('ok'));
615
+ child.on('close', (code) => {
616
+ // Interactive TTY hop: OpenSSH prints "Shared connection … closed." and
617
+ // nothing else. Leave the session id on the local shell (RUSH-3227).
618
+ const notice = peerHopCloseNotice(opts, machine, code);
619
+ if (notice)
620
+ process.stderr.write(notice);
621
+ resolve('ok');
622
+ });
606
623
  });
607
624
  }
@@ -9,7 +9,7 @@ import { machineId, normalizeHost } from '../../machine-id.js';
9
9
  import { SSH_OPTS, controlOpts, shellQuote } from '../../ssh-exec.js';
10
10
  import { buildWindowsAgentsCommand, remoteShellFor } from '../../hosts/remote-cmd.js';
11
11
  import { isReapableOrphan } from '../active.js';
12
- import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, } from '../session-cache.js';
12
+ import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, noteActiveSessionsJournalReader, } from '../session-cache.js';
13
13
  export const SESSION_WATCH_VERSION = 1;
14
14
  export const SESSION_WATCH_HEARTBEAT_MS = 15_000;
15
15
  /** Stable, opaque identity for one row within one device scope. */
@@ -116,9 +116,15 @@ export async function watchLocalSessions(options) {
116
116
  catch { /* first publication */ }
117
117
  const initial = readCache('local');
118
118
  options.emit(state.reset(options.scope, initial?.sessions ?? []));
119
+ // Known gap (out of scope for RUSH-2484): a present cache alone marks
120
+ // 'available' with no staleness/age check, so a reconnect can briefly render
121
+ // a stale snapshot as live before the out-of-band gather this file triggers
122
+ // (see watchActiveSessionsReaderPresence in session-cache.ts) replaces it.
119
123
  options.emit(state.scope(options.scope, initial ? 'available' : 'unavailable', initial ? undefined : 'awaiting publisher'));
120
124
  if (options.signal.aborted)
121
125
  return;
126
+ // Signal the daemon that a consumer is live so it does not skip the gather.
127
+ noteActiveSessionsJournalReader();
122
128
  await new Promise((resolve) => {
123
129
  let partial = '';
124
130
  let reading = false;
@@ -167,7 +173,10 @@ export async function watchLocalSessions(options) {
167
173
  fs.mkdirSync(path.dirname(journal), { recursive: true });
168
174
  const journalListener = () => readAppended();
169
175
  fs.watchFile(journal, { interval: options.journalPollMs ?? 250 }, journalListener);
170
- const heartbeatTimer = setInterval(() => options.emit(state.heartbeat(options.scope)), heartbeatMs);
176
+ const heartbeatTimer = setInterval(() => {
177
+ noteActiveSessionsJournalReader();
178
+ options.emit(state.heartbeat(options.scope));
179
+ }, heartbeatMs);
171
180
  const stop = () => { fs.unwatchFile(journal, journalListener); clearInterval(heartbeatTimer); resolve(); };
172
181
  options.signal.addEventListener('abort', stop, { once: true });
173
182
  // Close the offset/read/watch handoff: a publisher may append after the
@@ -59,6 +59,16 @@ export declare const IMMUTABLE_FIELD_KEYS: readonly ["topic", "label", "name", "
59
59
  * only cache that may carry them — and only as a whole-row snapshot.
60
60
  */
61
61
  export declare const LIVE_STATUS_KEYS: readonly ["status", "activity", "preview", "tokPerSec", "awaitingReason", "question", "todos", "tail", "lastActivityMs", "hostLink", "presence", "pidAlive", "tmuxClients", "windowHeartbeatMs", "provenance", "rateLimited", "plan"];
62
+ /**
63
+ * How long after the last reader heartbeat the daemon considers the journal
64
+ * to have no active consumer and skips the expensive `ps`+`lsof` gather.
65
+ *
66
+ * Must be significantly larger than {@link SESSION_WATCH_HEARTBEAT_MS} (15s)
67
+ * so a single delayed heartbeat does not cause a false-idle. Three tick-lengths
68
+ * gives adequate margin while still cutting load within ~45s of the last
69
+ * watcher disconnecting.
70
+ */
71
+ export declare const ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS = 45000;
62
72
  /** Test seam: redirect the snapshot file. Returns the previous override. */
63
73
  export declare function setActiveSessionsSnapshotPathForTest(p: string | null): string | null;
64
74
  /** Test seam: isolate process-local entries between fixtures. */
@@ -67,7 +77,50 @@ export declare function clearActiveSnapshotMemoryForTest(): void;
67
77
  export declare function setImmutableMemoPathForTest(p: string | null): string | null;
68
78
  /** Test seam: redirect the append-only live-state journal. */
69
79
  export declare function setActiveSessionsJournalPathForTest(p: string | null): string | null;
80
+ /** Test seam: redirect the reader-presence file. Returns the previous override. */
81
+ export declare function setActiveSessionsReaderPresencePathForTest(p: string | null): string | null;
70
82
  export declare function activeSessionsJournalPath(): string;
83
+ /**
84
+ * Record that a journal consumer is active right now.
85
+ *
86
+ * Called by {@link watchLocalSessions} on startup and on each heartbeat so
87
+ * the daemon warm tick can gate the expensive `ps`+`lsof` gather on reader
88
+ * presence and skip it when no watcher has checked in recently.
89
+ */
90
+ export declare function noteActiveSessionsJournalReader(nowMs?: number): void;
91
+ /**
92
+ * True when a journal consumer has signalled its presence within
93
+ * {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS}.
94
+ *
95
+ * Used by the daemon warm tick to decide whether to run the expensive
96
+ * `ps`+`lsof` gather. On an idle box with no watcher the tick skips the
97
+ * gather entirely; the moment a new watcher connects it calls
98
+ * {@link noteActiveSessionsJournalReader} and the following tick gathers.
99
+ */
100
+ export declare function isActiveSessionsJournalReaderRecent(nowMs?: number, idleWindowMs?: number): boolean;
101
+ /** Default poll cadence for {@link watchActiveSessionsReaderPresence}. */
102
+ export declare const ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS = 1000;
103
+ /**
104
+ * Watch for a reader going from absent/idle to present and fire `onConnect`
105
+ * out of band, instead of waiting for the next scheduled warm tick (RUSH-2484).
106
+ *
107
+ * {@link noteActiveSessionsJournalReader} only writes a timestamp — it has no
108
+ * path back to the daemon process that owns the warm tick's `setInterval`. A
109
+ * watcher connecting to a cold or long-idle daemon therefore used to wait up
110
+ * to one full {@link ACTIVE_SESSIONS_WARM_TICK_MS} tick for its first fresh
111
+ * rows. This polls the tiny presence file (not the expensive `ps`+`lsof`
112
+ * gather) at {@link ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS} and calls
113
+ * `onConnect` only on the idle→recent edge, so a steadily-connected reader's
114
+ * repeated heartbeats never re-trigger it and an idle box with no reader never
115
+ * gathers — the CPU win from RUSH-3193 is preserved.
116
+ *
117
+ * Returns a disposer that stops the poll.
118
+ */
119
+ export declare function watchActiveSessionsReaderPresence(onConnect: () => void, opts?: {
120
+ pollMs?: number;
121
+ idleWindowMs?: number;
122
+ nowMs?: () => number;
123
+ }): () => void;
71
124
  /** Read one scope from the snapshot file (best-effort; missing/corrupt → null). */
72
125
  export declare function readActiveSessionsCache(scope: ActiveCacheScope): ActiveSessionsSnapshot | null;
73
126
  /**
@@ -94,10 +94,21 @@ export const LIVE_STATUS_KEYS = [
94
94
  'rateLimited',
95
95
  'plan',
96
96
  ];
97
+ /**
98
+ * How long after the last reader heartbeat the daemon considers the journal
99
+ * to have no active consumer and skips the expensive `ps`+`lsof` gather.
100
+ *
101
+ * Must be significantly larger than {@link SESSION_WATCH_HEARTBEAT_MS} (15s)
102
+ * so a single delayed heartbeat does not cause a false-idle. Three tick-lengths
103
+ * gives adequate margin while still cutting load within ~45s of the last
104
+ * watcher disconnecting.
105
+ */
106
+ export const ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS = 45_000;
97
107
  // ── path overrides (test seam) ─────────────────────────────────────────────
98
108
  let snapshotPathOverride = null;
99
109
  let immutablePathOverride = null;
100
110
  let journalPathOverride = null;
111
+ let readerPresencePathOverride = null;
101
112
  /** Test seam: redirect the snapshot file. Returns the previous override. */
102
113
  export function setActiveSessionsSnapshotPathForTest(p) {
103
114
  const prev = snapshotPathOverride;
@@ -121,6 +132,12 @@ export function setActiveSessionsJournalPathForTest(p) {
121
132
  journalPathOverride = p;
122
133
  return prev;
123
134
  }
135
+ /** Test seam: redirect the reader-presence file. Returns the previous override. */
136
+ export function setActiveSessionsReaderPresencePathForTest(p) {
137
+ const prev = readerPresencePathOverride;
138
+ readerPresencePathOverride = p;
139
+ return prev;
140
+ }
124
141
  function snapshotPath() {
125
142
  return snapshotPathOverride ?? path.join(getCacheDir(), SNAPSHOT_FILE);
126
143
  }
@@ -131,6 +148,77 @@ export function activeSessionsJournalPath() {
131
148
  return journalPathOverride
132
149
  ?? (snapshotPathOverride ? `${snapshotPathOverride}.journal.jsonl` : path.join(getCacheDir(), JOURNAL_FILE));
133
150
  }
151
+ const READER_PRESENCE_FILE = '.active-sessions-reader.presence';
152
+ function readerPresencePath() {
153
+ return readerPresencePathOverride ?? path.join(getCacheDir(), READER_PRESENCE_FILE);
154
+ }
155
+ /**
156
+ * Record that a journal consumer is active right now.
157
+ *
158
+ * Called by {@link watchLocalSessions} on startup and on each heartbeat so
159
+ * the daemon warm tick can gate the expensive `ps`+`lsof` gather on reader
160
+ * presence and skip it when no watcher has checked in recently.
161
+ */
162
+ export function noteActiveSessionsJournalReader(nowMs = Date.now()) {
163
+ try {
164
+ const p = readerPresencePath();
165
+ fs.mkdirSync(path.dirname(p), { recursive: true });
166
+ fs.writeFileSync(p, String(nowMs));
167
+ }
168
+ catch { /* best-effort: a failed write does not block the caller */ }
169
+ }
170
+ /**
171
+ * True when a journal consumer has signalled its presence within
172
+ * {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS}.
173
+ *
174
+ * Used by the daemon warm tick to decide whether to run the expensive
175
+ * `ps`+`lsof` gather. On an idle box with no watcher the tick skips the
176
+ * gather entirely; the moment a new watcher connects it calls
177
+ * {@link noteActiveSessionsJournalReader} and the following tick gathers.
178
+ */
179
+ export function isActiveSessionsJournalReaderRecent(nowMs = Date.now(), idleWindowMs = ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS) {
180
+ try {
181
+ const content = fs.readFileSync(readerPresencePath(), 'utf8').trim();
182
+ const ts = Number(content);
183
+ if (!Number.isFinite(ts))
184
+ return false;
185
+ return nowMs - ts < idleWindowMs;
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ /** Default poll cadence for {@link watchActiveSessionsReaderPresence}. */
192
+ export const ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS = 1_000;
193
+ /**
194
+ * Watch for a reader going from absent/idle to present and fire `onConnect`
195
+ * out of band, instead of waiting for the next scheduled warm tick (RUSH-2484).
196
+ *
197
+ * {@link noteActiveSessionsJournalReader} only writes a timestamp — it has no
198
+ * path back to the daemon process that owns the warm tick's `setInterval`. A
199
+ * watcher connecting to a cold or long-idle daemon therefore used to wait up
200
+ * to one full {@link ACTIVE_SESSIONS_WARM_TICK_MS} tick for its first fresh
201
+ * rows. This polls the tiny presence file (not the expensive `ps`+`lsof`
202
+ * gather) at {@link ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS} and calls
203
+ * `onConnect` only on the idle→recent edge, so a steadily-connected reader's
204
+ * repeated heartbeats never re-trigger it and an idle box with no reader never
205
+ * gathers — the CPU win from RUSH-3193 is preserved.
206
+ *
207
+ * Returns a disposer that stops the poll.
208
+ */
209
+ export function watchActiveSessionsReaderPresence(onConnect, opts = {}) {
210
+ const pollMs = opts.pollMs ?? ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS;
211
+ const now = opts.nowMs ?? Date.now;
212
+ let lastRecent = isActiveSessionsJournalReaderRecent(now(), opts.idleWindowMs);
213
+ const timer = setInterval(() => {
214
+ const recent = isActiveSessionsJournalReaderRecent(now(), opts.idleWindowMs);
215
+ if (recent && !lastRecent)
216
+ onConnect();
217
+ lastRecent = recent;
218
+ }, pollMs);
219
+ timer.unref?.();
220
+ return () => clearInterval(timer);
221
+ }
134
222
  // ── snapshot read / write ──────────────────────────────────────────────────
135
223
  /** Read one scope from the snapshot file (best-effort; missing/corrupt → null). */
136
224
  export function readActiveSessionsCache(scope) {
@@ -174,7 +174,7 @@ export function extractRecentDirectoriesTouched(events, cwd) {
174
174
  if (['Edit', 'Write', 'edit_file', 'write_file', 'create_file', 'edit', 'write'].includes(tool)) {
175
175
  add(args.file_path ?? args.filePath ?? args.path ?? event.path, true);
176
176
  }
177
- else if (['Bash', 'exec_command', 'run_shell_command', 'shell', 'Execute'].includes(tool)) {
177
+ else if (['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute'].includes(tool)) {
178
178
  add(args.cwd ?? args.Cwd ?? args.workdir ?? args.working_directory ?? cwd);
179
179
  }
180
180
  }
@@ -34,6 +34,8 @@ export interface ToolCallCollectorSnapshot {
34
34
  pending: IndexedToolCall[];
35
35
  }
36
36
  export declare function sanitizeToolEvidenceText(value: string, maxBytes: number, knownValues?: readonly string[]): string;
37
+ /** Extract literal shell commands from the Codex orchestration tool without evaluating transcript code. */
38
+ export declare function commandsFromCodexExec(source: string): string[];
37
39
  export declare function structuredToolResult(value: unknown): {
38
40
  text: string;
39
41
  outcome: ToolCallOutcome;
@@ -181,7 +181,7 @@ function canonicalInput(tool, args, command) {
181
181
  }
182
182
  }
183
183
  /** Extract literal shell commands from the Codex orchestration tool without evaluating transcript code. */
184
- function commandsFromCodexExec(source) {
184
+ export function commandsFromCodexExec(source) {
185
185
  const commands = [];
186
186
  let root;
187
187
  try {
@@ -11,5 +11,9 @@ export declare function renderTrajectoryCompareHtml(cmp: TrajectoryComparison):
11
11
  * auto-discovered from the team records) plus a per-node summary card revealed
12
12
  * by clicking a node. Same shell, redaction, and no-CDN rule as the other two
13
13
  * layouts; local paths (`filePath`/`cwd`) are deliberately never rendered.
14
+ *
15
+ * `redacted` follows the trace's `--no-redact` flag so the footer states the
16
+ * honest label, exactly as the single-trajectory and compare renderers do; it
17
+ * defaults to the redacted claim to keep the shared/safe default.
14
18
  */
15
- export declare function renderLineageHtml(lineage: SessionLineage): string;
19
+ export declare function renderLineageHtml(lineage: SessionLineage, redacted?: boolean): string;