@phnx-labs/agents-cli 1.22.46 → 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 (348) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/LICENSE +102 -182
  3. package/README.md +96 -49
  4. package/dist/bootstrap.js +4 -2
  5. package/dist/cli/command-registry.d.ts +2 -1
  6. package/dist/cli/command-registry.js +6 -2
  7. package/dist/commands/accounts.d.ts +58 -0
  8. package/dist/commands/accounts.js +243 -25
  9. package/dist/commands/artifacts.js +7 -6
  10. package/dist/commands/attach.js +1 -1
  11. package/dist/commands/auth.js +7 -5
  12. package/dist/commands/browser-sessions-picker.js +9 -7
  13. package/dist/commands/browser.js +711 -62
  14. package/dist/commands/config.d.ts +0 -1
  15. package/dist/commands/config.js +26 -24
  16. package/dist/commands/cost.d.ts +1 -1
  17. package/dist/commands/daemon.d.ts +21 -0
  18. package/dist/commands/daemon.js +147 -12
  19. package/dist/commands/detach.d.ts +13 -0
  20. package/dist/commands/detach.js +11 -8
  21. package/dist/commands/exec.js +107 -53
  22. package/dist/commands/feed-watch.d.ts +2 -0
  23. package/dist/commands/feed-watch.js +28 -0
  24. package/dist/commands/feed.js +27 -0
  25. package/dist/commands/feedback.js +3 -19
  26. package/dist/commands/focus.d.ts +14 -0
  27. package/dist/commands/focus.js +48 -5
  28. package/dist/commands/go.d.ts +3 -0
  29. package/dist/commands/go.js +16 -3
  30. package/dist/commands/insights.d.ts +1 -1
  31. package/dist/commands/insights.js +2 -2
  32. package/dist/commands/inspect.js +2 -2
  33. package/dist/commands/lease.d.ts +1 -1
  34. package/dist/commands/lease.js +7 -6
  35. package/dist/commands/mailboxes.js +4 -3
  36. package/dist/commands/menubar.js +11 -4
  37. package/dist/commands/projects.d.ts +16 -1
  38. package/dist/commands/projects.js +58 -64
  39. package/dist/commands/resume.js +1 -1
  40. package/dist/commands/route.js +4 -4
  41. package/dist/commands/routines.js +4 -3
  42. package/dist/commands/run-broadcast.d.ts +1 -1
  43. package/dist/commands/run-broadcast.js +1 -1
  44. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  45. package/dist/commands/sessions-picker-factory.js +1 -1
  46. package/dist/commands/sessions-picker.d.ts +22 -0
  47. package/dist/commands/sessions-picker.js +48 -9
  48. package/dist/commands/sessions-stop.d.ts +24 -0
  49. package/dist/commands/sessions-stop.js +66 -0
  50. package/dist/commands/sessions-trace.d.ts +82 -0
  51. package/dist/commands/sessions-trace.js +387 -0
  52. package/dist/commands/sessions.js +5 -0
  53. package/dist/commands/setup-secrets.js +23 -0
  54. package/dist/commands/share.d.ts +20 -5
  55. package/dist/commands/share.js +152 -52
  56. package/dist/commands/ssh.d.ts +15 -0
  57. package/dist/commands/ssh.js +243 -18
  58. package/dist/commands/teams.js +34 -2
  59. package/dist/commands/traces.d.ts +2 -0
  60. package/dist/commands/traces.js +184 -0
  61. package/dist/commands/view.d.ts +2 -0
  62. package/dist/commands/view.js +32 -62
  63. package/dist/commands/watchdog.js +9 -14
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +5 -0
  66. package/dist/lib/account-capabilities.d.ts +4 -0
  67. package/dist/lib/account-capabilities.js +13 -9
  68. package/dist/lib/account-registry.d.ts +2 -0
  69. package/dist/lib/account-registry.js +30 -2
  70. package/dist/lib/accounting/rotate.d.ts +9 -0
  71. package/dist/lib/accounting/rotate.js +12 -2
  72. package/dist/lib/accounting/usage.d.ts +84 -6
  73. package/dist/lib/accounting/usage.js +293 -75
  74. package/dist/lib/agent-modes.js +29 -1
  75. package/dist/lib/agent-spec/agents.d.ts +4 -3
  76. package/dist/lib/agent-spec/agents.js +25 -15
  77. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  78. package/dist/lib/analytics/mix-commands.js +2 -2
  79. package/dist/lib/auth-health.d.ts +13 -3
  80. package/dist/lib/auth-health.js +50 -8
  81. package/dist/lib/browser/devices.d.ts +19 -0
  82. package/dist/lib/browser/devices.js +23 -0
  83. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  84. package/dist/lib/browser/drivers/ssh.js +36 -9
  85. package/dist/lib/browser/ipc.d.ts +1 -1
  86. package/dist/lib/browser/ipc.js +40 -9
  87. package/dist/lib/browser/profiles.d.ts +104 -65
  88. package/dist/lib/browser/profiles.js +277 -165
  89. package/dist/lib/browser/registry.d.ts +39 -0
  90. package/dist/lib/browser/registry.js +149 -0
  91. package/dist/lib/browser/remote-control.d.ts +28 -2
  92. package/dist/lib/browser/remote-control.js +35 -3
  93. package/dist/lib/browser/resolve-target.d.ts +68 -0
  94. package/dist/lib/browser/resolve-target.js +250 -0
  95. package/dist/lib/browser/runtime-state.d.ts +44 -11
  96. package/dist/lib/browser/runtime-state.js +98 -15
  97. package/dist/lib/browser/service.d.ts +65 -9
  98. package/dist/lib/browser/service.js +196 -61
  99. package/dist/lib/browser/sessions-list.d.ts +1 -3
  100. package/dist/lib/browser/sessions-list.js +3 -16
  101. package/dist/lib/browser/task-index.d.ts +70 -0
  102. package/dist/lib/browser/task-index.js +180 -0
  103. package/dist/lib/browser/types.d.ts +12 -1
  104. package/dist/lib/byok-usage.js +2 -2
  105. package/dist/lib/claude-statusline.d.ts +29 -0
  106. package/dist/lib/claude-statusline.js +139 -0
  107. package/dist/lib/cloud/host.d.ts +1 -1
  108. package/dist/lib/cloud/host.js +1 -1
  109. package/dist/lib/cloud/rush.js +1 -1
  110. package/dist/lib/codex-policy.d.ts +11 -1
  111. package/dist/lib/codex-policy.js +20 -5
  112. package/dist/lib/computer/computer-rpc.js +2 -2
  113. package/dist/lib/computer/download.d.ts +15 -15
  114. package/dist/lib/computer/download.js +35 -98
  115. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  116. package/dist/lib/computer/ssh-tunnel.js +12 -22
  117. package/dist/lib/config-keys.d.ts +6 -11
  118. package/dist/lib/config-keys.js +18 -19
  119. package/dist/lib/crabbox/cli.d.ts +1 -1
  120. package/dist/lib/crabbox/cli.js +2 -2
  121. package/dist/lib/crabbox/lease.d.ts +9 -0
  122. package/dist/lib/crabbox/lease.js +70 -15
  123. package/dist/lib/crabbox/runtimes.js +1 -1
  124. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  125. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  126. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  127. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  128. package/dist/lib/daemon/daemon.d.ts +3 -0
  129. package/dist/lib/daemon/daemon.js +212 -444
  130. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  131. package/dist/lib/daemon/device-probe-service.js +47 -0
  132. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  133. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  134. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  135. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  136. package/dist/lib/daemon/runner.js +15 -2
  137. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  138. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  139. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  140. package/dist/lib/daemon/self-heal-service.js +48 -0
  141. package/dist/lib/daemon/service.d.ts +102 -0
  142. package/dist/lib/daemon/service.js +98 -0
  143. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  144. package/dist/lib/daemon/session-index-service.js +40 -0
  145. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  146. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  147. package/dist/lib/daemon/supervisor.d.ts +87 -0
  148. package/dist/lib/daemon/supervisor.js +298 -0
  149. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  150. package/dist/lib/daemon/watchdog-service.js +40 -0
  151. package/dist/lib/daemon-health.d.ts +15 -0
  152. package/dist/lib/daemon-health.js +29 -5
  153. package/dist/lib/daemon-services.d.ts +8 -1
  154. package/dist/lib/daemon-services.js +55 -0
  155. package/dist/lib/daemon-ticks.d.ts +13 -2
  156. package/dist/lib/daemon-ticks.js +22 -20
  157. package/dist/lib/device-config.d.ts +25 -10
  158. package/dist/lib/device-config.js +90 -39
  159. package/dist/lib/devices/config-migration.d.ts +7 -0
  160. package/dist/lib/devices/config-migration.js +67 -3
  161. package/dist/lib/devices/health.d.ts +27 -2
  162. package/dist/lib/devices/health.js +39 -9
  163. package/dist/lib/devices/interactive-host.d.ts +15 -0
  164. package/dist/lib/devices/interactive-host.js +53 -0
  165. package/dist/lib/devices/registry.d.ts +61 -4
  166. package/dist/lib/devices/registry.js +94 -38
  167. package/dist/lib/devices/stats-cache.d.ts +26 -0
  168. package/dist/lib/devices/stats-cache.js +59 -7
  169. package/dist/lib/devices/worker-pick.d.ts +48 -0
  170. package/dist/lib/devices/worker-pick.js +113 -0
  171. package/dist/lib/exec.d.ts +70 -14
  172. package/dist/lib/exec.js +110 -38
  173. package/dist/lib/feed/answer.d.ts +29 -0
  174. package/dist/lib/feed/answer.js +145 -0
  175. package/dist/lib/feed/attention.d.ts +83 -0
  176. package/dist/lib/feed/attention.js +0 -0
  177. package/dist/lib/feed/feed.d.ts +109 -0
  178. package/dist/lib/feed/feed.js +154 -3
  179. package/dist/lib/feed/pr-status.d.ts +14 -0
  180. package/dist/lib/feed/pr-status.js +47 -0
  181. package/dist/lib/feed/watch.d.ts +59 -0
  182. package/dist/lib/feed/watch.js +180 -0
  183. package/dist/lib/fleet/capture.js +6 -0
  184. package/dist/lib/fleet/remote-login.js +8 -3
  185. package/dist/lib/fleet/types.d.ts +17 -0
  186. package/dist/lib/harness/adapter.d.ts +11 -0
  187. package/dist/lib/harness/adapters/claude.js +38 -24
  188. package/dist/lib/harness/adapters/codex.js +2 -4
  189. package/dist/lib/harness/adapters/cursor.js +8 -11
  190. package/dist/lib/helper-download.d.ts +108 -0
  191. package/dist/lib/helper-download.js +194 -0
  192. package/dist/lib/helper-versions.d.ts +52 -0
  193. package/dist/lib/helper-versions.js +58 -0
  194. package/dist/lib/hooks/cache.js +19 -0
  195. package/dist/lib/hooks/install.js +2 -2
  196. package/dist/lib/hooks/match.d.ts +3 -0
  197. package/dist/lib/hooks/match.js +25 -0
  198. package/dist/lib/hosts/credentials.d.ts +1 -1
  199. package/dist/lib/hosts/credentials.js +1 -1
  200. package/dist/lib/hosts/dispatch.d.ts +7 -1
  201. package/dist/lib/hosts/dispatch.js +24 -5
  202. package/dist/lib/hosts/passthrough.d.ts +8 -15
  203. package/dist/lib/hosts/passthrough.js +35 -16
  204. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  205. package/dist/lib/hosts/providers/devices.js +1 -1
  206. package/dist/lib/hosts/reconnect.d.ts +179 -21
  207. package/dist/lib/hosts/reconnect.js +266 -45
  208. package/dist/lib/hosts/registry.js +7 -0
  209. package/dist/lib/installations/migrate.d.ts +4 -5
  210. package/dist/lib/installations/migrate.js +6 -7
  211. package/dist/lib/installations/shims.d.ts +1 -1
  212. package/dist/lib/installations/shims.js +23 -13
  213. package/dist/lib/installations/versions.js +25 -2
  214. package/dist/lib/linear-autoclose.d.ts +1 -1
  215. package/dist/lib/linear-autoclose.js +1 -1
  216. package/dist/lib/loop.d.ts +2 -2
  217. package/dist/lib/loop.js +1 -1
  218. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  219. package/dist/lib/menubar/download-menubar.js +60 -0
  220. package/dist/lib/menubar/install-menubar.d.ts +44 -7
  221. package/dist/lib/menubar/install-menubar.js +105 -23
  222. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  223. package/dist/lib/menubar/notify-desktop.js +6 -6
  224. package/dist/lib/model-tiers.d.ts +1 -1
  225. package/dist/lib/open-url.d.ts +60 -2
  226. package/dist/lib/open-url.js +167 -12
  227. package/dist/lib/project-probe.d.ts +1 -1
  228. package/dist/lib/project-probe.js +1 -1
  229. package/dist/lib/project-pull.d.ts +1 -1
  230. package/dist/lib/project-pull.js +2 -2
  231. package/dist/lib/projects.d.ts +1 -1
  232. package/dist/lib/projects.js +1 -1
  233. package/dist/lib/refresh-coordinator.d.ts +1 -1
  234. package/dist/lib/refresh-coordinator.js +1 -1
  235. package/dist/lib/sandbox.js +6 -7
  236. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  237. package/dist/lib/secrets/download-keychain.js +70 -0
  238. package/dist/lib/secrets/install-helper.d.ts +20 -1
  239. package/dist/lib/secrets/install-helper.js +44 -3
  240. package/dist/lib/session/active.d.ts +67 -0
  241. package/dist/lib/session/active.js +86 -2
  242. package/dist/lib/session/db.d.ts +11 -1
  243. package/dist/lib/session/db.js +142 -8
  244. package/dist/lib/session/discover.d.ts +23 -1
  245. package/dist/lib/session/discover.js +44 -10
  246. package/dist/lib/session/highlights.d.ts +49 -1
  247. package/dist/lib/session/highlights.js +95 -0
  248. package/dist/lib/session/host-link.d.ts +16 -2
  249. package/dist/lib/session/host-link.js +7 -1
  250. package/dist/lib/session/parse.js +33 -1
  251. package/dist/lib/session/prompt.d.ts +35 -0
  252. package/dist/lib/session/prompt.js +56 -0
  253. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  254. package/dist/lib/session/remote/remote-list.js +19 -2
  255. package/dist/lib/session/remote/watch.d.ts +1 -1
  256. package/dist/lib/session/remote/watch.js +17 -3
  257. package/dist/lib/session/render.js +7 -3
  258. package/dist/lib/session/session-cache.d.ts +53 -0
  259. package/dist/lib/session/session-cache.js +93 -1
  260. package/dist/lib/session/state.js +1 -1
  261. package/dist/lib/session/tool-calls.d.ts +2 -0
  262. package/dist/lib/session/tool-calls.js +1 -1
  263. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  264. package/dist/lib/session/trajectory-compare.js +116 -0
  265. package/dist/lib/session/trajectory-html.d.ts +19 -0
  266. package/dist/lib/session/trajectory-html.js +770 -0
  267. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  268. package/dist/lib/session/trajectory-lineage.js +195 -0
  269. package/dist/lib/session/trajectory-text.d.ts +37 -0
  270. package/dist/lib/session/trajectory-text.js +284 -0
  271. package/dist/lib/session/trajectory.d.ts +102 -0
  272. package/dist/lib/session/trajectory.js +384 -0
  273. package/dist/lib/session/types.d.ts +15 -0
  274. package/dist/lib/sha256-asset.d.ts +26 -0
  275. package/dist/lib/sha256-asset.js +41 -0
  276. package/dist/lib/share/backend.d.ts +95 -0
  277. package/dist/lib/share/backend.js +139 -0
  278. package/dist/lib/share/delete.d.ts +6 -2
  279. package/dist/lib/share/delete.js +8 -9
  280. package/dist/lib/share/html.d.ts +22 -0
  281. package/dist/lib/share/html.js +88 -0
  282. package/dist/lib/share/provision.d.ts +18 -4
  283. package/dist/lib/share/provision.js +38 -4
  284. package/dist/lib/share/publish.d.ts +39 -15
  285. package/dist/lib/share/publish.js +55 -26
  286. package/dist/lib/share/worker-template.js +171 -11
  287. package/dist/lib/sqlite.js +16 -3
  288. package/dist/lib/ssh-exec.d.ts +65 -0
  289. package/dist/lib/ssh-exec.js +120 -4
  290. package/dist/lib/startup/command-registry.d.ts +3 -1
  291. package/dist/lib/startup/command-registry.js +6 -3
  292. package/dist/lib/startup/dev-build.js +3 -3
  293. package/dist/lib/state.d.ts +2 -2
  294. package/dist/lib/state.js +11 -5
  295. package/dist/lib/subagents-registry.js +1 -1
  296. package/dist/lib/teams/agents.js +9 -0
  297. package/dist/lib/terminal/index.d.ts +1 -1
  298. package/dist/lib/terminal/inject.d.ts +19 -0
  299. package/dist/lib/terminal/inject.js +19 -9
  300. package/dist/lib/terminal/types.d.ts +1 -1
  301. package/dist/lib/terminal/types.js +1 -1
  302. package/dist/lib/tmux/orphan-reap.js +1 -1
  303. package/dist/lib/tmux/session.d.ts +46 -13
  304. package/dist/lib/tmux/session.js +204 -27
  305. package/dist/lib/traces/backend.d.ts +29 -0
  306. package/dist/lib/traces/backend.js +46 -0
  307. package/dist/lib/traces/classify.d.ts +52 -0
  308. package/dist/lib/traces/classify.js +72 -0
  309. package/dist/lib/traces/config.d.ts +10 -0
  310. package/dist/lib/traces/config.js +2 -0
  311. package/dist/lib/traces/provision.d.ts +19 -0
  312. package/dist/lib/traces/provision.js +30 -0
  313. package/dist/lib/traces/sync.d.ts +150 -0
  314. package/dist/lib/traces/sync.js +447 -0
  315. package/dist/lib/traces/worker-template.d.ts +4 -0
  316. package/dist/lib/traces/worker-template.js +159 -0
  317. package/dist/lib/types.d.ts +34 -19
  318. package/dist/lib/types.js +14 -0
  319. package/dist/lib/usage-backoff.d.ts +2 -1
  320. package/dist/lib/usage-backoff.js +60 -19
  321. package/dist/lib/usage-refresh.d.ts +18 -4
  322. package/dist/lib/usage-refresh.js +68 -19
  323. package/dist/lib/watchdog/log.d.ts +1 -1
  324. package/dist/lib/watchdog/log.js +1 -1
  325. package/dist/lib/watchdog/runner.d.ts +36 -45
  326. package/dist/lib/watchdog/runner.js +150 -119
  327. package/dist/lib/watchdog/service.d.ts +0 -1
  328. package/dist/lib/watchdog/service.js +0 -1
  329. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  330. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  331. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  332. package/dist/lib/watchdog/watchdog.js +37 -104
  333. package/dist/lib/workflows.d.ts +1 -1
  334. package/package.json +5 -7
  335. package/dist/commands/usage.d.ts +0 -27
  336. package/dist/commands/usage.js +0 -96
  337. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  338. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  339. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  340. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  341. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  342. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  343. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
  344. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  345. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
  346. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  347. package/dist/lib/usage-fleet.d.ts +0 -32
  348. package/dist/lib/usage-fleet.js +0 -125
@@ -26,8 +26,8 @@ import { getConfigSymlinkVersion } from '../installations/shims.js';
26
26
  import { SESSION_AGENTS } from './types.js';
27
27
  import { deriveShortId } from './short-id.js';
28
28
  import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
29
- import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
30
- import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
29
+ import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput, classifyUserPrompt } from './prompt.js';
30
+ import { isBackgroundShellStart, isSkillInvocation, extractSkills, extractSlashCommands, isSubAgentTool } from './highlights.js';
31
31
  import { parseAntigravity, parseCursor, splitSessionFilePath } from './parse.js';
32
32
  import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
33
33
  import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
@@ -1215,7 +1215,9 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
1215
1215
  accountKey: acct.key,
1216
1216
  accountOrg: acct.orgName ?? undefined,
1217
1217
  topic: scan.topic,
1218
- label,
1218
+ // The live sessions metadata file contains only user `/rename` values;
1219
+ // prefer it when present, then fall back to the title parsed from JSONL.
1220
+ label: label || scan.label,
1219
1221
  messageCount: scan.messageCount,
1220
1222
  toolCallCount: scan.toolCallCount,
1221
1223
  tokenCount: scan.tokenCount,
@@ -1237,6 +1239,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
1237
1239
  todos: scan.todos,
1238
1240
  recentDirectoriesTouched: scan.recentDirectoriesTouched,
1239
1241
  skillsUsed: scan.skillsUsed,
1242
+ subAgentCount: scan.subAgentCount,
1243
+ backgroundShellCount: scan.backgroundShellCount,
1240
1244
  slashCommandsUsed: scan.slashCommandsUsed,
1241
1245
  };
1242
1246
  }
@@ -1253,7 +1257,9 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
1253
1257
  accountKey: acct.key,
1254
1258
  accountOrg: acct.orgName ?? undefined,
1255
1259
  model: scan.model,
1256
- label,
1260
+ // Keep parsed ai-title/custom-title values even when the transcript has
1261
+ // no timestamp and therefore takes this stat-backed fallback path.
1262
+ label: label || scan.label,
1257
1263
  messageCount: scan.messageCount,
1258
1264
  toolCallCount: scan.toolCallCount,
1259
1265
  tokenCount: scan.tokenCount,
@@ -1276,6 +1282,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
1276
1282
  todos: scan.todos,
1277
1283
  recentDirectoriesTouched: scan.recentDirectoriesTouched,
1278
1284
  skillsUsed: scan.skillsUsed,
1285
+ subAgentCount: scan.subAgentCount,
1286
+ backgroundShellCount: scan.backgroundShellCount,
1279
1287
  slashCommandsUsed: scan.slashCommandsUsed,
1280
1288
  };
1281
1289
  }
@@ -2992,6 +3000,8 @@ function extractDroidMessageText(content) {
2992
3000
  /** Zero-value accumulator for a fresh (from-byte-0) Claude parse. */
2993
3001
  export function initClaudeParseState() {
2994
3002
  return {
3003
+ subAgents: 0,
3004
+ backgroundShells: 0,
2995
3005
  timestamp: undefined,
2996
3006
  cwd: undefined,
2997
3007
  gitBranch: undefined,
@@ -3040,6 +3050,14 @@ function foldDerivedToolState(state, event) {
3040
3050
  // from a full re-parse (see session/db.ts's writeResourceUsage doc comment).
3041
3051
  if (isSkillInvocation(event))
3042
3052
  state.skillEvents.push(event);
3053
+ // Fan-out tallies (RUSH-3091/3095): counted, not held — we need the number,
3054
+ // and a heavy orchestrator session can spawn hundreds of each.
3055
+ if (state.subAgents !== undefined && isSubAgentTool(event.tool ?? '', event.command ?? '')) {
3056
+ state.subAgents += 1;
3057
+ }
3058
+ if (state.backgroundShells !== undefined && isBackgroundShellStart(event)) {
3059
+ state.backgroundShells += 1;
3060
+ }
3043
3061
  if (event.tool === 'SlashCommand') {
3044
3062
  const slashCommand = extractSlashCommandFromToolInput(event.args);
3045
3063
  if (slashCommand)
@@ -3247,9 +3265,18 @@ export function finalizeClaudeScan(state) {
3247
3265
  const durationMs = state.firstTsMs !== undefined && state.lastTsMs !== undefined && state.lastTsMs > state.firstTsMs
3248
3266
  ? state.lastTsMs - state.firstTsMs
3249
3267
  : undefined;
3250
- // Prefer an explicit session title (user `/rename` > Claude auto-title) over
3251
- // the first-prompt topic.
3252
- const resolvedTopic = state.customTitle || state.aiTitle || state.topic;
3268
+ // A topic is the first meaningful prompt. Harness-owned names travel in the
3269
+ // separate label field so consumers can replace an early topic once Claude's
3270
+ // generated title (or a later `/rename`) arrives.
3271
+ // Claude's generated `ai-title` is derived from the first turn, so a session
3272
+ // opened with a skill gets named after the injected "Base directory for this
3273
+ // skill: …" line instead of its task — and that name then wins on every
3274
+ // surface for the session's whole life. `classifyUserPrompt` reports `skill`
3275
+ // only for that injected line, so collapse it to `/<skill>`. A `/rename`
3276
+ // (`custom-title`) is the user's own words and is never rewritten.
3277
+ const generated = classifyUserPrompt(state.aiTitle ?? '');
3278
+ const label = state.customTitle
3279
+ || (generated.kind === 'skill' ? generated.clean : state.aiTitle);
3253
3280
  const worktree = detectWorktree(state.cwd, state.gitBranch);
3254
3281
  const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
3255
3282
  return {
@@ -3258,7 +3285,8 @@ export function finalizeClaudeScan(state) {
3258
3285
  gitBranch: state.gitBranch,
3259
3286
  version: state.version,
3260
3287
  model: state.model,
3261
- topic: resolvedTopic,
3288
+ topic: state.topic,
3289
+ label,
3262
3290
  entrypoint: state.entrypoint,
3263
3291
  messageCount: state.messageCount,
3264
3292
  toolCallCount: state.toolCallCount,
@@ -3283,6 +3311,8 @@ export function finalizeClaudeScan(state) {
3283
3311
  recentDirectoriesTouched: state.recentDirectoriesTouched.length ? state.recentDirectoriesTouched : undefined,
3284
3312
  skillsUsed: state.skillEvents.length ? extractSkills(state.skillEvents) : undefined,
3285
3313
  slashCommandsUsed: state.slashCommandEvents.length ? extractSlashCommands(state.slashCommandEvents) : undefined,
3314
+ subAgentCount: state.subAgents,
3315
+ backgroundShellCount: state.backgroundShells,
3286
3316
  };
3287
3317
  }
3288
3318
  /** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
@@ -3324,7 +3354,9 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
3324
3354
  : allIds;
3325
3355
  const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
3326
3356
  return {
3327
- v: 3,
3357
+ v: 4,
3358
+ subAgents: state.subAgents,
3359
+ backgroundShells: state.backgroundShells,
3328
3360
  offset,
3329
3361
  jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
3330
3362
  timestamp: state.timestamp,
@@ -3396,6 +3428,8 @@ export function hydrateClaudeParseState(prior) {
3396
3428
  seen.add(`pad:${pad++}`);
3397
3429
  }
3398
3430
  return {
3431
+ subAgents: prior.subAgents,
3432
+ backgroundShells: prior.backgroundShells,
3399
3433
  timestamp: prior.timestamp,
3400
3434
  cwd: prior.cwd,
3401
3435
  gitBranch: prior.gitBranch,
@@ -3563,7 +3597,7 @@ function parsePriorClaudeState(row) {
3563
3597
  return null;
3564
3598
  try {
3565
3599
  const parsed = JSON.parse(row.parserState);
3566
- if (parsed?.v !== 3 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
3600
+ if (parsed?.v !== 4 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
3567
3601
  return null;
3568
3602
  return parsed;
3569
3603
  }
@@ -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
  };
@@ -34,5 +34,40 @@ export declare function extractSlashCommandFromToolInput(toolInput: Record<strin
34
34
  export declare const HEADLESS_PLAN_MODE_PREFIX = "You are running in HEADLESS PLAN MODE.";
35
35
  /** Strip framework noise from a raw session prompt, returning only meaningful user text. */
36
36
  export declare function cleanSessionPrompt(raw: string): string;
37
+ /**
38
+ * How a session's first user turn reads once its framework noise is stripped —
39
+ * so a UI row can show `[image]` / `$ cmd` / `/skill` instead of a screenshot
40
+ * path, a pasted shell line, or a skill's install directory.
41
+ */
42
+ export type UserPromptKind = 'text' | 'image' | 'command' | 'skill';
43
+ export interface ClassifiedPrompt {
44
+ /**
45
+ * The one-line, display-ready form of the prompt — never a bare path. NOT
46
+ * length-capped: the recap card shows it in full; row consumers cap it
47
+ * themselves (`deriveSessionRecap`).
48
+ */
49
+ clean: string;
50
+ kind: UserPromptKind;
51
+ }
52
+ /**
53
+ * Classify a raw first user turn into a display-ready `{ clean, kind }`, so a
54
+ * Fleet/session row shows the user's actual intent rather than path noise:
55
+ *
56
+ * - `skill` — a `/skill` invocation's injected "Base directory for this
57
+ * skill: …" line collapses to `/<name>`.
58
+ * - `command` — a `$ `-prefixed shell paste keeps only the first command.
59
+ * - `image` — a screenshot/image path standing alone, or an image
60
+ * attachment with no real accompanying text, becomes `[image]`.
61
+ * - `text` — everything else: wrapper tags stripped (via
62
+ * {@link cleanSessionPrompt}) and any inline image path replaced
63
+ * with `[image]` so a mixed prompt still drops the path.
64
+ *
65
+ * Pure and cross-harness (text markers, no per-agent branch). `hasImageAttachment`
66
+ * lets a caller that only has an already-extracted topic line still detect an
67
+ * image-only turn from the row's attachment list.
68
+ */
69
+ export declare function classifyUserPrompt(raw: string | undefined, opts?: {
70
+ hasImageAttachment?: boolean;
71
+ }): ClassifiedPrompt;
37
72
  /** Extract a one-line topic from a raw user message, or undefined if the message is pure noise. */
38
73
  export declare function extractSessionTopic(raw: string): string | undefined;
@@ -145,6 +145,62 @@ export function cleanSessionPrompt(raw) {
145
145
  .filter(line => !NOISE_LINE_PATTERNS.some(pattern => pattern.test(line)));
146
146
  return meaningful.join('\n').trim();
147
147
  }
148
+ /**
149
+ * A skill invocation injects a system line naming the skill's install directory:
150
+ * "Base directory for this skill: /home/.../skills/<name>"
151
+ * which collapses to "/<name>" so a row shows the skill, not the path. Anchored
152
+ * to that exact injected line — a bare `.../skills/<name>` mention in ordinary
153
+ * prose is NOT a skill invocation and must keep its real text.
154
+ */
155
+ const SKILL_BASEDIR_RE = /Base directory for this skill:\s*\S*[\/\\]skills[\/\\]([\w.-]+)/i;
156
+ /**
157
+ * An explicit shell-prompt paste (`$ cmd`). Only the command survives. Deliberately
158
+ * `$`-only: a leading `>` is a markdown blockquote far more often than a shell
159
+ * prompt, so matching it misread quoted prose as a command.
160
+ */
161
+ const COMMAND_PREFIX_RE = /^\$\s+(\S.*)$/;
162
+ /**
163
+ * A filesystem path (absolute or `~`-rooted) ending in an image extension —
164
+ * matched even across the spaces a CleanShot filename carries. Used both to
165
+ * detect an image-only prompt and to drop the path from a mixed one.
166
+ */
167
+ const IMAGE_PATH_RE = /[\/~][^\n]*?\.(?:png|jpe?g|gif|webp|heic|bmp|svg)\b/i;
168
+ /**
169
+ * Classify a raw first user turn into a display-ready `{ clean, kind }`, so a
170
+ * Fleet/session row shows the user's actual intent rather than path noise:
171
+ *
172
+ * - `skill` — a `/skill` invocation's injected "Base directory for this
173
+ * skill: …" line collapses to `/<name>`.
174
+ * - `command` — a `$ `-prefixed shell paste keeps only the first command.
175
+ * - `image` — a screenshot/image path standing alone, or an image
176
+ * attachment with no real accompanying text, becomes `[image]`.
177
+ * - `text` — everything else: wrapper tags stripped (via
178
+ * {@link cleanSessionPrompt}) and any inline image path replaced
179
+ * with `[image]` so a mixed prompt still drops the path.
180
+ *
181
+ * Pure and cross-harness (text markers, no per-agent branch). `hasImageAttachment`
182
+ * lets a caller that only has an already-extracted topic line still detect an
183
+ * image-only turn from the row's attachment list.
184
+ */
185
+ export function classifyUserPrompt(raw, opts = {}) {
186
+ const text = (raw ?? '').replace(/\r/g, '').trim();
187
+ const skill = text.match(SKILL_BASEDIR_RE);
188
+ if (skill)
189
+ return { clean: `/${skill[1]}`, kind: 'skill' };
190
+ const firstLine = text.split('\n').map(line => line.trim()).find(Boolean) ?? '';
191
+ const cmd = firstLine.match(COMMAND_PREFIX_RE);
192
+ if (cmd)
193
+ return { clean: `$ ${cmd[1]}`.trim(), kind: 'command' };
194
+ const cleaned = cleanSessionPrompt(text);
195
+ const cleanedFirst = cleaned.split('\n').map(line => line.trim()).find(Boolean) ?? '';
196
+ const imgMatch = firstLine.match(IMAGE_PATH_RE);
197
+ const imageOnly = !!imgMatch && firstLine.replace(imgMatch[0], '').replace(/[^\w]/g, '').length <= 8;
198
+ if (imageOnly || (opts.hasImageAttachment && !cleanedFirst)) {
199
+ return { clean: '[image]', kind: 'image' };
200
+ }
201
+ const display = (cleanedFirst || firstLine).replace(IMAGE_PATH_RE, '[image]').trim();
202
+ return { clean: display, kind: 'text' };
203
+ }
148
204
  /** Extract a one-line topic from a raw user message, or undefined if the message is pure noise. */
149
205
  export function extractSessionTopic(raw) {
150
206
  if (!raw.trim())
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import type { ActiveSession } from '../active.js';
1
+ import { type ActiveSession } from '../active.js';
2
2
  import { readActiveSessionsCache } from '../session-cache.js';
3
3
  export declare const SESSION_WATCH_VERSION: 1;
4
4
  export declare const SESSION_WATCH_HEARTBEAT_MS = 15000;
@@ -8,7 +8,8 @@ import { deviceIdentityArgs, sshTargetFor } from '../../devices/connect.js';
8
8
  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
- import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, } from '../session-cache.js';
11
+ import { isReapableOrphan } from '../active.js';
12
+ import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, noteActiveSessionsJournalReader, } from '../session-cache.js';
12
13
  export const SESSION_WATCH_VERSION = 1;
13
14
  export const SESSION_WATCH_HEARTBEAT_MS = 15_000;
14
15
  /** Stable, opaque identity for one row within one device scope. */
@@ -18,7 +19,11 @@ export function sessionWatchRowKey(scope, row) {
18
19
  }
19
20
  export function toSessionWatchRow(scope, row) {
20
21
  const rowKey = sessionWatchRowKey(scope, row);
21
- const resumable = Boolean(row.sessionId);
22
+ // A dead, days-stale crash-orphan is folded OUT of the reconnectable set — it
23
+ // is not resumable, so the "Needs reconnecting" list stops ballooning with
24
+ // leaked --device tunnel sessions (RUSH-3011 / issue #3b). A live or
25
+ // recently-exited session stays resumable.
26
+ const resumable = Boolean(row.sessionId) && !isReapableOrphan(row);
22
27
  const viewingIn = row.viewingIn
23
28
  ? [row.viewingIn.app, row.viewingIn.tab ? `tab ${row.viewingIn.tab}` : undefined].filter(Boolean).join(' ')
24
29
  : null;
@@ -111,9 +116,15 @@ export async function watchLocalSessions(options) {
111
116
  catch { /* first publication */ }
112
117
  const initial = readCache('local');
113
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.
114
123
  options.emit(state.scope(options.scope, initial ? 'available' : 'unavailable', initial ? undefined : 'awaiting publisher'));
115
124
  if (options.signal.aborted)
116
125
  return;
126
+ // Signal the daemon that a consumer is live so it does not skip the gather.
127
+ noteActiveSessionsJournalReader();
117
128
  await new Promise((resolve) => {
118
129
  let partial = '';
119
130
  let reading = false;
@@ -162,7 +173,10 @@ export async function watchLocalSessions(options) {
162
173
  fs.mkdirSync(path.dirname(journal), { recursive: true });
163
174
  const journalListener = () => readAppended();
164
175
  fs.watchFile(journal, { interval: options.journalPollMs ?? 250 }, journalListener);
165
- 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);
166
180
  const stop = () => { fs.unwatchFile(journal, journalListener); clearInterval(heartbeatTimer); resolve(); };
167
181
  options.signal.addEventListener('abort', stop, { once: true });
168
182
  // Close the offset/read/watch handoff: a publisher may append after the