@phnx-labs/agents-cli 1.20.73 → 1.20.76

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 (209) hide show
  1. package/CHANGELOG.md +989 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +31 -1
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/export.d.ts +11 -0
  15. package/dist/commands/export.js +215 -0
  16. package/dist/commands/feed.d.ts +7 -1
  17. package/dist/commands/feed.js +47 -4
  18. package/dist/commands/go.d.ts +9 -2
  19. package/dist/commands/go.js +16 -6
  20. package/dist/commands/harness.d.ts +14 -0
  21. package/dist/commands/harness.js +145 -0
  22. package/dist/commands/import.js +87 -9
  23. package/dist/commands/inspect.js +5 -2
  24. package/dist/commands/profiles.d.ts +20 -0
  25. package/dist/commands/profiles.js +68 -21
  26. package/dist/commands/repo.js +14 -0
  27. package/dist/commands/routines.js +15 -2
  28. package/dist/commands/secrets.js +44 -21
  29. package/dist/commands/sessions-browser.js +1 -1
  30. package/dist/commands/sessions-export.js +19 -5
  31. package/dist/commands/sessions-migrate.d.ts +29 -0
  32. package/dist/commands/sessions-migrate.js +596 -0
  33. package/dist/commands/sessions-picker.js +16 -1
  34. package/dist/commands/sessions.d.ts +36 -13
  35. package/dist/commands/sessions.js +210 -40
  36. package/dist/commands/setup-computer.js +2 -2
  37. package/dist/commands/setup.js +6 -1
  38. package/dist/commands/ssh.js +71 -35
  39. package/dist/commands/status.js +10 -2
  40. package/dist/commands/versions.js +51 -11
  41. package/dist/commands/view.d.ts +5 -0
  42. package/dist/commands/view.js +99 -31
  43. package/dist/commands/watchdog.d.ts +7 -2
  44. package/dist/commands/watchdog.js +73 -57
  45. package/dist/index.js +59 -7
  46. package/dist/lib/activity.d.ts +14 -4
  47. package/dist/lib/activity.js +17 -5
  48. package/dist/lib/actor.d.ts +50 -0
  49. package/dist/lib/actor.js +166 -0
  50. package/dist/lib/agent-spec/provider.js +2 -1
  51. package/dist/lib/agent-spec/resolve.js +19 -5
  52. package/dist/lib/agent-spec/types.d.ts +9 -1
  53. package/dist/lib/agents.d.ts +56 -0
  54. package/dist/lib/agents.js +133 -7
  55. package/dist/lib/cloud/codex.d.ts +2 -0
  56. package/dist/lib/cloud/codex.js +14 -3
  57. package/dist/lib/cloud/session-index.d.ts +32 -0
  58. package/dist/lib/cloud/session-index.js +58 -0
  59. package/dist/lib/cloud/store.d.ts +7 -0
  60. package/dist/lib/cloud/store.js +25 -0
  61. package/dist/lib/config-transfer.d.ts +16 -0
  62. package/dist/lib/config-transfer.js +60 -0
  63. package/dist/lib/daemon.js +18 -2
  64. package/dist/lib/devices/connect.d.ts +15 -1
  65. package/dist/lib/devices/connect.js +15 -1
  66. package/dist/lib/devices/fleet.d.ts +21 -0
  67. package/dist/lib/devices/fleet.js +26 -0
  68. package/dist/lib/devices/health-report.d.ts +38 -0
  69. package/dist/lib/devices/health-report.js +214 -0
  70. package/dist/lib/devices/health.js +4 -1
  71. package/dist/lib/devices/reachability.d.ts +31 -0
  72. package/dist/lib/devices/reachability.js +40 -0
  73. package/dist/lib/devices/registry.d.ts +33 -0
  74. package/dist/lib/devices/registry.js +37 -0
  75. package/dist/lib/devices/resolve-target.d.ts +15 -27
  76. package/dist/lib/devices/resolve-target.js +63 -102
  77. package/dist/lib/devices/sync.d.ts +18 -0
  78. package/dist/lib/devices/sync.js +23 -1
  79. package/dist/lib/devices/tailscale.d.ts +3 -0
  80. package/dist/lib/devices/tailscale.js +1 -0
  81. package/dist/lib/events.d.ts +1 -1
  82. package/dist/lib/exec.d.ts +66 -0
  83. package/dist/lib/exec.js +195 -6
  84. package/dist/lib/export.d.ts +72 -0
  85. package/dist/lib/export.js +269 -0
  86. package/dist/lib/feed-post.d.ts +63 -0
  87. package/dist/lib/feed-post.js +204 -0
  88. package/dist/lib/fleet/apply.d.ts +32 -2
  89. package/dist/lib/fleet/apply.js +97 -10
  90. package/dist/lib/fleet/types.d.ts +11 -0
  91. package/dist/lib/fs-walk.d.ts +13 -0
  92. package/dist/lib/fs-walk.js +16 -7
  93. package/dist/lib/hooks.d.ts +1 -0
  94. package/dist/lib/hooks.js +156 -0
  95. package/dist/lib/hosts/dispatch.d.ts +7 -0
  96. package/dist/lib/hosts/dispatch.js +2 -0
  97. package/dist/lib/hosts/passthrough.js +2 -0
  98. package/dist/lib/hosts/reconnect.d.ts +78 -0
  99. package/dist/lib/hosts/reconnect.js +127 -0
  100. package/dist/lib/hosts/registry.d.ts +75 -14
  101. package/dist/lib/hosts/registry.js +205 -30
  102. package/dist/lib/hosts/remote-cmd.js +1 -0
  103. package/dist/lib/hosts/run-target.d.ts +13 -5
  104. package/dist/lib/hosts/run-target.js +26 -10
  105. package/dist/lib/hosts/session-index.d.ts +18 -1
  106. package/dist/lib/hosts/session-index.js +37 -2
  107. package/dist/lib/hosts/session-marker.d.ts +33 -0
  108. package/dist/lib/hosts/session-marker.js +51 -0
  109. package/dist/lib/hosts/tasks.d.ts +8 -4
  110. package/dist/lib/import.d.ts +25 -0
  111. package/dist/lib/import.js +107 -2
  112. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  113. package/dist/lib/isolation-boundary-report.js +35 -0
  114. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  115. package/dist/lib/menubar/install-menubar.js +45 -1
  116. package/dist/lib/migrate.d.ts +16 -0
  117. package/dist/lib/migrate.js +36 -0
  118. package/dist/lib/models.d.ts +27 -0
  119. package/dist/lib/models.js +54 -1
  120. package/dist/lib/overdue.d.ts +7 -2
  121. package/dist/lib/overdue.js +18 -11
  122. package/dist/lib/picker.js +4 -1
  123. package/dist/lib/platform/process.d.ts +17 -0
  124. package/dist/lib/platform/process.js +70 -0
  125. package/dist/lib/profiles-presets.js +9 -7
  126. package/dist/lib/profiles.d.ts +31 -0
  127. package/dist/lib/profiles.js +70 -0
  128. package/dist/lib/project-resources.js +20 -5
  129. package/dist/lib/pty-server.d.ts +2 -10
  130. package/dist/lib/pty-server.js +4 -38
  131. package/dist/lib/registry.d.ts +1 -1
  132. package/dist/lib/registry.js +48 -8
  133. package/dist/lib/rotate.d.ts +18 -0
  134. package/dist/lib/rotate.js +28 -0
  135. package/dist/lib/routines.d.ts +26 -0
  136. package/dist/lib/routines.js +19 -2
  137. package/dist/lib/runner.js +104 -7
  138. package/dist/lib/secrets/agent.d.ts +66 -1
  139. package/dist/lib/secrets/agent.js +154 -90
  140. package/dist/lib/secrets/bundles.d.ts +34 -0
  141. package/dist/lib/secrets/bundles.js +179 -19
  142. package/dist/lib/secrets/index.d.ts +15 -1
  143. package/dist/lib/secrets/index.js +21 -3
  144. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  145. package/dist/lib/secrets/rc-hygiene.js +154 -0
  146. package/dist/lib/secrets/remote.d.ts +7 -4
  147. package/dist/lib/secrets/remote.js +7 -4
  148. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  149. package/dist/lib/secrets/sync-commands.js +21 -0
  150. package/dist/lib/self-update.d.ts +23 -2
  151. package/dist/lib/self-update.js +86 -5
  152. package/dist/lib/session/active.d.ts +114 -7
  153. package/dist/lib/session/active.js +323 -52
  154. package/dist/lib/session/cloud.js +2 -1
  155. package/dist/lib/session/db.d.ts +48 -0
  156. package/dist/lib/session/db.js +238 -14
  157. package/dist/lib/session/detached.d.ts +30 -0
  158. package/dist/lib/session/detached.js +92 -0
  159. package/dist/lib/session/discover.d.ts +465 -2
  160. package/dist/lib/session/discover.js +1648 -468
  161. package/dist/lib/session/fork.js +2 -1
  162. package/dist/lib/session/hook-sessions.d.ts +43 -0
  163. package/dist/lib/session/hook-sessions.js +135 -0
  164. package/dist/lib/session/linear.d.ts +6 -0
  165. package/dist/lib/session/linear.js +55 -0
  166. package/dist/lib/session/migrate-targets.d.ts +65 -0
  167. package/dist/lib/session/migrate-targets.js +94 -0
  168. package/dist/lib/session/migrations.d.ts +37 -0
  169. package/dist/lib/session/migrations.js +60 -0
  170. package/dist/lib/session/parse.d.ts +18 -0
  171. package/dist/lib/session/parse.js +130 -30
  172. package/dist/lib/session/pid-registry.d.ts +34 -2
  173. package/dist/lib/session/pid-registry.js +49 -2
  174. package/dist/lib/session/render.d.ts +7 -2
  175. package/dist/lib/session/render.js +22 -10
  176. package/dist/lib/session/short-id.d.ts +17 -0
  177. package/dist/lib/session/short-id.js +20 -0
  178. package/dist/lib/session/state.js +6 -2
  179. package/dist/lib/shims.d.ts +27 -1
  180. package/dist/lib/shims.js +88 -6
  181. package/dist/lib/sqlite.d.ts +3 -2
  182. package/dist/lib/sqlite.js +27 -4
  183. package/dist/lib/staleness/detectors/commands.js +1 -1
  184. package/dist/lib/staleness/detectors/workflows.js +13 -2
  185. package/dist/lib/staleness/writers/commands.js +7 -7
  186. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  187. package/dist/lib/startup/command-registry.d.ts +2 -0
  188. package/dist/lib/startup/command-registry.js +5 -0
  189. package/dist/lib/state.d.ts +1 -1
  190. package/dist/lib/state.js +15 -36
  191. package/dist/lib/teams/agents.d.ts +19 -10
  192. package/dist/lib/teams/agents.js +40 -39
  193. package/dist/lib/types.d.ts +48 -2
  194. package/dist/lib/uninstall.js +1 -45
  195. package/dist/lib/usage.d.ts +33 -1
  196. package/dist/lib/usage.js +172 -12
  197. package/dist/lib/versions.d.ts +22 -2
  198. package/dist/lib/versions.js +98 -10
  199. package/dist/lib/watchdog/log.d.ts +43 -0
  200. package/dist/lib/watchdog/log.js +69 -0
  201. package/dist/lib/watchdog/routine.d.ts +44 -0
  202. package/dist/lib/watchdog/routine.js +69 -0
  203. package/dist/lib/watchdog/runner.d.ts +51 -7
  204. package/dist/lib/watchdog/runner.js +239 -64
  205. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  206. package/dist/lib/watchdog/watchdog.js +31 -16
  207. package/dist/lib/workflows.d.ts +16 -0
  208. package/dist/lib/workflows.js +110 -1
  209. package/package.json +1 -1
@@ -19,12 +19,13 @@
19
19
  import * as fs from 'fs';
20
20
  import * as path from 'path';
21
21
  import * as os from 'os';
22
- import { execFile } from 'child_process';
22
+ import { execFile, execFileSync } from 'child_process';
23
23
  import { promisify } from 'util';
24
24
  import { listActiveTasks } from '../cloud/store.js';
25
25
  import { AgentManager } from '../teams/agents.js';
26
26
  import { getTerminalsDir } from '../state.js';
27
- import { readPidSessionEntry, prunePidSessionRegistry } from './pid-registry.js';
27
+ import { readPidSessionEntry, listPidSessionEntries, prunePidSessionRegistry } from './pid-registry.js';
28
+ import { loadHookSessionIndex, resolveHookSessionRecord } from './hook-sessions.js';
28
29
  import { buildClaudeLabelMap } from './discover.js';
29
30
  import { buildRunNameMap } from './run-names.js';
30
31
  import { latestSessionFileForCwd } from './db.js';
@@ -33,6 +34,7 @@ import { readSessionTailWithRaw } from './tail.js';
33
34
  import { computeTokPerSec } from './throughput.js';
34
35
  import { inferSessionState } from './state.js';
35
36
  import { detectProvenance } from './provenance.js';
37
+ import { presenceFromStore } from './detached.js';
36
38
  import { mapBounded } from '../concurrency.js';
37
39
  const execFileAsync = promisify(execFile);
38
40
  /**
@@ -43,6 +45,15 @@ const execFileAsync = promisify(execFile);
43
45
  */
44
46
  export const LSOF_CONCURRENCY = 4;
45
47
  const LSOF_STAGGER_MS = 10;
48
+ /**
49
+ * Hard ceilings on the two syscalls the status path shells out to. Without them
50
+ * a single hung probe (a wedged NFS `lsof`, an EDR that stalls the `ps` snapshot)
51
+ * pins a bounded worker slot forever and silently drops live sessions to a
52
+ * fallback status. On timeout the call rejects, is caught, and the row degrades
53
+ * honestly (unknown / empty table) instead of the sweep hanging.
54
+ */
55
+ const LSOF_TIMEOUT_MS = 5_000;
56
+ const PS_SNAPSHOT_TIMEOUT_MS = 10_000;
46
57
  export function activeStatusFromCloudStatus(status) {
47
58
  switch (status) {
48
59
  case 'running':
@@ -102,16 +113,72 @@ export function agentKindFromComm(commRaw) {
102
113
  const key = stripped === base ? base : stripped.toLowerCase();
103
114
  return AGENT_CLI_NAMES[key];
104
115
  }
105
- function isPidAlive(pid) {
116
+ /**
117
+ * A process that began more than this long AFTER a session's recorded
118
+ * `startedAtMs` cannot be that session's process — the OS handed its pid to
119
+ * something newer. The window absorbs clock granularity (`ps -o lstart=` reports
120
+ * whole seconds) and the gap between a process spawning and the SessionStart
121
+ * hook recording `startedAtMs`; it is far below the minutes-to-hours it takes the
122
+ * pid space to wrap and actually recycle a pid, so it never false-kills a live
123
+ * session.
124
+ */
125
+ const PID_REUSE_TOLERANCE_MS = 60_000;
126
+ /**
127
+ * Epoch-ms start time of the process at `pid`, or null if unknowable.
128
+ *
129
+ * Distinct from teams/agents.ts's `captureProcessStartTime`, which returns an
130
+ * opaque token only meaningful for equality against a prior capture of the SAME
131
+ * pid. Here we need a value comparable to a session's `startedAtMs`, so we read
132
+ * `ps -o lstart=` — a ctime string on both macOS and Linux — and parse it to
133
+ * epoch ms. Windows and any exec/parse failure return null, so the caller falls
134
+ * back to a bare existence check (never worse than before).
135
+ */
136
+ function processStartMs(pid) {
137
+ if (process.platform === 'win32')
138
+ return null;
139
+ try {
140
+ const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
141
+ encoding: 'utf-8',
142
+ stdio: ['ignore', 'pipe', 'ignore'],
143
+ }).trim();
144
+ if (!out)
145
+ return null;
146
+ const ms = Date.parse(out);
147
+ return Number.isFinite(ms) ? ms : null;
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ /**
154
+ * True when `pid` names a live process AND — when a session's recorded
155
+ * `startedAtMs` is supplied — that process is plausibly the SAME one, not a later
156
+ * process that recycled the pid. The OS reuses pids, so a bare
157
+ * `process.kill(pid, 0)` existence check reports a dead session as alive (a
158
+ * "zombie") once its pid is handed to an unrelated process. A genuine session
159
+ * process starts at or before its own recorded start, so a process that began
160
+ * meaningfully AFTER `startedAtMs` is a reused pid and the session is dead. When
161
+ * the start time can't be read, we keep the existence answer.
162
+ */
163
+ export function isPidAlive(pid, startedAtMs) {
106
164
  if (!pid || pid < 1)
107
165
  return false;
108
166
  try {
109
167
  process.kill(pid, 0);
110
- return true;
111
168
  }
112
169
  catch (err) {
113
- return err?.code === 'EPERM';
170
+ // EPERM means the pid exists but is owned by another user — still "alive",
171
+ // fall through to the identity check. Any other error means no such process.
172
+ if (err?.code !== 'EPERM')
173
+ return false;
114
174
  }
175
+ if (startedAtMs && startedAtMs > 0) {
176
+ const procStartMs = processStartMs(pid);
177
+ if (procStartMs !== null && procStartMs > startedAtMs + PID_REUSE_TOLERANCE_MS) {
178
+ return false; // pid recycled by a newer process — this session is gone
179
+ }
180
+ }
181
+ return true;
115
182
  }
116
183
  /** Read the live-terminals registry, dedupe by sessionId, keep only pid-alive entries. */
117
184
  function readLiveTerminals() {
@@ -134,7 +201,7 @@ function readLiveTerminals() {
134
201
  const merged = new Map();
135
202
  for (const [windowId, slice] of Object.entries(parsed)) {
136
203
  for (const e of (slice?.entries ?? [])) {
137
- if (!e?.sessionId || !isPidAlive(e.pid))
204
+ if (!e?.sessionId || !isPidAlive(e.pid, e.startedAtMs))
138
205
  continue;
139
206
  merged.set(e.sessionId, { ...e, windowId });
140
207
  }
@@ -189,19 +256,49 @@ export function pickSessionFile(projectDir, sessionId) {
189
256
  }
190
257
  return best?.path;
191
258
  }
192
- function classifyActivity(sessionFile) {
193
- // No resolvable transcript is NOT evidence of activity default to idle. (Before
194
- // the no-borrow fix in pickSessionFile this rarely fired because every terminal
195
- // borrowed the newest file; now a session with an unresolved id lands here, and
196
- // "running" would wrongly light it up.)
259
+ /**
260
+ * One `stat` the transcript's creation (≈ session start) and last-write (≈ last
261
+ * activity) epochs. Both `undefined` when the file can't be stat'd (vanished /
262
+ * unresolved). `birthtimeMs` can be 0 on filesystems without creation time coerce
263
+ * that to `undefined` so callers fall through to a real signal instead of epoch 0.
264
+ */
265
+ export function sessionFileTimes(sessionFile) {
266
+ if (!sessionFile)
267
+ return {};
268
+ try {
269
+ const st = fs.statSync(sessionFile);
270
+ return { birthtimeMs: st.birthtimeMs || undefined, mtimeMs: st.mtimeMs || undefined };
271
+ }
272
+ catch {
273
+ return {};
274
+ }
275
+ }
276
+ /**
277
+ * The ONE place a fallback status is decided when no rich transcript state is
278
+ * available — a non-Claude/Codex kind we cannot parse, or a Claude/Codex tail
279
+ * that was empty or unreadable. Honest by construction: it never asserts a status
280
+ * it cannot justify from a measured signal.
281
+ *
282
+ * - Resolvable transcript, readable mtime → the MEASURED freshness signal:
283
+ * written within ACTIVE_MTIME_WINDOW_MS ⇒ `running`, else `idle`.
284
+ * - Resolvable transcript whose `stat` throws (file vanished / permission) → we
285
+ * genuinely cannot tell ⇒ `unknown`. (This branch previously returned
286
+ * `running`, which contradicted the `idle` default one branch up.)
287
+ * - No resolvable transcript but the process is alive → alive-but-opaque ⇒
288
+ * `unknown`. This is the truthful answer for a live gemini / droid / cursor /
289
+ * opencode whose format we don't parse — NOT a fabricated `idle` (which the
290
+ * UI reads as "done and waiting"), and it never lies as `running` either.
291
+ * - No transcript and the process is not known alive → nothing to report ⇒ `idle`.
292
+ */
293
+ export function resolveFallbackStatus(sessionFile, pidAlive) {
197
294
  if (!sessionFile)
198
- return 'idle';
295
+ return pidAlive ? 'unknown' : 'idle';
199
296
  try {
200
297
  const mtimeMs = fs.statSync(sessionFile).mtimeMs;
201
298
  return Date.now() - mtimeMs < ACTIVE_MTIME_WINDOW_MS ? 'running' : 'idle';
202
299
  }
203
300
  catch {
204
- return 'running';
301
+ return 'unknown';
205
302
  }
206
303
  }
207
304
  /**
@@ -255,11 +352,13 @@ function statusFromActivity(activity) {
255
352
  /**
256
353
  * Fold a computed SessionState onto an active-session row: rich status +
257
354
  * preview + PR/worktree/ticket badges. With no state (unreadable/non-Claude/
258
- * Codex file) it degrades to the mtime-only classification.
355
+ * Codex file) it degrades to {@link resolveFallbackStatus}, which needs
356
+ * `pidAlive` to tell an alive-but-opaque process (`unknown`) from a dead one
357
+ * (`idle`).
259
358
  */
260
- function applyState(base, state, fallbackFile) {
359
+ function applyState(base, state, fallbackFile, pidAlive) {
261
360
  if (!state)
262
- return { ...base, status: classifyActivity(fallbackFile) };
361
+ return { ...base, status: resolveFallbackStatus(fallbackFile, pidAlive) };
263
362
  return {
264
363
  ...base,
265
364
  status: statusFromActivity(state.activity),
@@ -361,7 +460,8 @@ export async function listTeamsActive() {
361
460
  const sessionId = a.parentSessionId ?? a.remoteSessionId ?? undefined;
362
461
  const sessionFile = findSessionFileForKind(a.agentType, a.cwd ?? undefined, sessionId ?? undefined);
363
462
  const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
364
- const { state, tokPerSec } = computeLiveSignals(a.agentType, sessionFile, a.cwd ?? undefined, a.pid ? isPidAlive(a.pid) : true);
463
+ const pidAlive = a.pid ? isPidAlive(a.pid) : true;
464
+ const { state, tokPerSec } = computeLiveSignals(a.agentType, sessionFile, a.cwd ?? undefined, pidAlive);
365
465
  return applyState({
366
466
  context: 'teams',
367
467
  kind: a.agentType,
@@ -373,9 +473,10 @@ export async function listTeamsActive() {
373
473
  tokPerSec,
374
474
  sessionFile,
375
475
  startedAtMs: a.startedAt.getTime(),
476
+ lastActivityMs: sessionFileTimes(sessionFile).mtimeMs,
376
477
  teamName: a.taskName,
377
478
  agentId: a.agentId,
378
- }, state, sessionFile);
479
+ }, state, sessionFile, pidAlive);
379
480
  });
380
481
  }
381
482
  /** Live editor-terminal agents across every IDE window. */
@@ -411,7 +512,8 @@ export async function listTerminalsActive() {
411
512
  const name = resolvedId ? runNameMap.get(resolvedId) ?? undefined : undefined;
412
513
  // Extract topic from session file (first meaningful user message)
413
514
  const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
414
- const { state, tokPerSec } = computeLiveSignals(t.kind, sessionFile, t.cwd ?? undefined, isPidAlive(t.pid));
515
+ const pidAlive = isPidAlive(t.pid, t.startedAtMs);
516
+ const { state, tokPerSec } = computeLiveSignals(t.kind, sessionFile, t.cwd ?? undefined, pidAlive);
415
517
  return applyState({
416
518
  context: 'terminal',
417
519
  kind: t.kind,
@@ -426,8 +528,9 @@ export async function listTerminalsActive() {
426
528
  tokPerSec,
427
529
  sessionFile,
428
530
  startedAtMs: t.startedAtMs,
531
+ lastActivityMs: sessionFileTimes(sessionFile).mtimeMs,
429
532
  windowId: t.windowId,
430
- }, state, sessionFile);
533
+ }, state, sessionFile, pidAlive);
431
534
  });
432
535
  }
433
536
  /** Cloud tasks still in a non-terminal state. `tasks.db` may not exist; that's fine. */
@@ -485,7 +588,7 @@ async function readProcessTable() {
485
588
  return readProcessTableWin32();
486
589
  let out;
487
590
  try {
488
- ({ stdout: out } = await execFileAsync('ps', ['-A', '-o', 'pid=,ppid=,tty=,comm='], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }));
591
+ ({ stdout: out } = await execFileAsync('ps', ['-A', '-o', 'pid=,ppid=,tty=,comm='], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: PS_SNAPSHOT_TIMEOUT_MS }));
489
592
  }
490
593
  catch {
491
594
  return [];
@@ -519,7 +622,7 @@ async function readProcessTableWin32() {
519
622
  ({ stdout: out } = await execFileAsync('powershell.exe', [
520
623
  '-NoProfile', '-NonInteractive', '-Command',
521
624
  'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name | ConvertTo-Csv -NoTypeInformation',
522
- ], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true }));
625
+ ], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true, timeout: PS_SNAPSHOT_TIMEOUT_MS }));
523
626
  }
524
627
  catch {
525
628
  return [];
@@ -581,6 +684,7 @@ async function getCwdForPid(pid) {
581
684
  try {
582
685
  const res = await execFileAsync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], {
583
686
  encoding: 'utf8',
687
+ timeout: LSOF_TIMEOUT_MS,
584
688
  });
585
689
  out = res.stdout;
586
690
  }
@@ -763,6 +867,33 @@ export async function listUnattributedActive(attributed) {
763
867
  // Bounded + staggered lsof probes: same cwds, but a trickle of spawns instead
764
868
  // of one simultaneous system-wide burst that behavioral EDR flags as recon.
765
869
  const cwds = await resolveCwds(kept.map(c => c.pid));
870
+ // The hook state dir is scanned at most ONCE per active-scan, and the ppid map
871
+ // is inverted at most once — both built lazily on the first candidate that
872
+ // lacks an exact launch-time id, so an all-Claude set does neither. The ~3s
873
+ // poll must not re-read the dir (or re-invert the map) per candidate.
874
+ let hookIndex;
875
+ let childrenByParent;
876
+ // Durable `agents run --name` handles keyed by session id — the same source the
877
+ // terminal path uses to name a row. Headless agents have no live-terminals
878
+ // label and no /rename, so without this a `--name`d headless run would surface
879
+ // with only a topic and no tab title. Built once per scan.
880
+ const runNameMap = buildRunNameMap();
881
+ const ensureChildren = () => {
882
+ if (childrenByParent)
883
+ return childrenByParent;
884
+ const m = new Map();
885
+ // The hook records under the agent pid; a wrapper/shell pid we recorded has
886
+ // the agent as a child, so we resolve via a recorded pid's immediate children.
887
+ for (const [childPid, parentPid] of ppidMap) {
888
+ const arr = m.get(parentPid);
889
+ if (arr)
890
+ arr.push(childPid);
891
+ else
892
+ m.set(parentPid, [childPid]);
893
+ }
894
+ childrenByParent = m;
895
+ return m;
896
+ };
766
897
  const out = [];
767
898
  for (let i = 0; i < kept.length; i++) {
768
899
  const { pid, kind } = kept[i];
@@ -772,12 +903,45 @@ export async function listUnattributedActive(attributed) {
772
903
  // .jsonl. The shim's entry may sit on a wrapper ancestor (Windows .cmd
773
904
  // path). Absent entirely (direct launch outside agents-cli) → heuristic.
774
905
  const entry = readPidSessionEntry(pid) ?? readAncestorSessionEntry(pid, ppidMap, kind);
906
+ // Exact session id, in priority: (1) the id we recorded at launch (Claude,
907
+ // known up front via --session-id); (2) the agent's OWN SessionStart hook,
908
+ // authoritative for non-Claude and for agents we didn't launch, joined by
909
+ // launchId/terminalId/pid and kind-guarded against a stale reused-pid file;
910
+ // (3) the newest-jsonl heuristic (sessionIdFromFile, below).
911
+ let exactId = entry?.sessionId;
912
+ // The hook record (when we fall to it) also carries the SessionStart `ts` — the
913
+ // real session-start epoch. Capture it so terminal/headless rows get a
914
+ // `startedAtMs` instead of rendering "0s ago" (they set none before this).
915
+ let hookRec;
916
+ if (!exactId) {
917
+ hookIndex ??= loadHookSessionIndex();
918
+ hookRec = resolveHookSessionRecord(hookIndex, {
919
+ pid,
920
+ kind,
921
+ launchId: entry?.launchId,
922
+ terminalId: entry?.terminalId,
923
+ childPids: ensureChildren().get(pid),
924
+ });
925
+ exactId = hookRec?.session_id;
926
+ }
775
927
  const cwd = cwds[i] ?? entry?.cwd ?? undefined;
776
- const sessionFile = findSessionFileForKind(kind, cwd, entry?.sessionId);
928
+ const sessionFile = findSessionFileForKind(kind, cwd, exactId);
777
929
  const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
778
930
  const host = detectHost(pid, procByPid);
779
931
  const context = host && UI_HOSTS.has(host) ? 'terminal' : 'headless';
932
+ // pidAlive is true by construction: this pid was just enumerated from the
933
+ // live process table, so an opaque (non-parseable) kind resolves to
934
+ // `unknown`, not a fake `idle`.
780
935
  const { state, tokPerSec } = computeLiveSignals(kind, sessionFile, cwd, true);
936
+ const { birthtimeMs, mtimeMs } = sessionFileTimes(sessionFile);
937
+ // Durable run name from `agents run --name`, resolved by the run's session id
938
+ // — the tab-title handle for a run we launched by name. A headless row carries
939
+ // no /rename label and no live-terminals label, so this handle IS its label
940
+ // (mirrors listTeamsActive `label: a.name`). Absent a `--name`, label stays
941
+ // undefined and the display falls back to the topic on its own — no band-aid.
942
+ const resolvedId = exactId ?? sessionIdFromFile(sessionFile);
943
+ const name = resolvedId ? runNameMap.get(resolvedId) ?? undefined : undefined;
944
+ const label = name;
781
945
  out.push(applyState({
782
946
  context,
783
947
  kind,
@@ -785,25 +949,73 @@ export async function listUnattributedActive(attributed) {
785
949
  tty: procByPid.get(pid)?.tty,
786
950
  pid,
787
951
  cwd,
788
- sessionId: entry?.sessionId ?? sessionIdFromFile(sessionFile),
952
+ sessionId: resolvedId,
953
+ label,
954
+ name,
789
955
  topic,
790
956
  tokPerSec,
791
957
  sessionFile,
958
+ // Session start: the hook's authoritative SessionStart `ts`, else the
959
+ // transcript's creation time. Last activity: the transcript's last write.
960
+ startedAtMs: hookRec?.ts ?? birthtimeMs,
961
+ lastActivityMs: mtimeMs,
792
962
  pidCount: 1 + (foldedByRoot.get(pid) ?? 0),
793
- }, state, sessionFile));
963
+ }, state, sessionFile, true));
794
964
  }
795
965
  // Housekeeping: drop registry files for pids that have since died.
796
966
  prunePidSessionRegistry(isPidAlive);
797
967
  return out;
798
968
  }
969
+ /**
970
+ * Attribute a single tmux pane to the agent actually running in it.
971
+ *
972
+ * The launch registry — written per bare-spawn AND per wrap, each stamped with the
973
+ * `tmuxPane` it targeted (see src/lib/exec.ts) — is the EXACT, per-pane source of
974
+ * truth. So an agent spawned into an EXISTING pane (a split, where `$TMUX` is
975
+ * already set so no new session meta is stamped) is attributed to its OWN launch,
976
+ * not the session's original agent — closing the gap where such an agent was
977
+ * dropped by this source and left to the weaker ps-scan fallback. Session-meta
978
+ * labels remain the fallback for the wrapped origin pane of a session whose
979
+ * registry entry is absent (a failed best-effort write, or a legacy session that
980
+ * predates the registry's `tmuxPane` field) — and ONLY for that origin pane
981
+ * (`meta.pane`), so a split shell pane of a labeled session isn't mis-attributed
982
+ * the wrapped agent. When `meta.pane` is unknown (attach-existing sessions), any
983
+ * labeled pane is accepted and the caller's per-session dedupe keeps one.
984
+ * `source: 'teams'` panes are skipped — teammates are surfaced by listTeamsActive.
985
+ * Pure so it is unit-tested without tmux.
986
+ */
987
+ export function resolvePaneIdentity(pane, meta, liveEntry, getHookIndex) {
988
+ if (meta?.source === 'teams')
989
+ return undefined;
990
+ if (liveEntry) {
991
+ // Exact id: the id recorded at launch (Claude), else the agent's own
992
+ // SessionStart hook joined by launchId/terminalId (non-Claude, or agents we
993
+ // didn't launch) — kind-guarded against a stale reused-pid file.
994
+ const sessionId = liveEntry.sessionId
995
+ ?? resolveHookSessionRecord(getHookIndex(), {
996
+ pid: liveEntry.pid,
997
+ kind: liveEntry.agent,
998
+ launchId: liveEntry.launchId,
999
+ terminalId: liveEntry.terminalId,
1000
+ })?.session_id;
1001
+ return { agent: liveEntry.agent, sessionId, pid: liveEntry.pid };
1002
+ }
1003
+ const agent = meta?.labels?.agent;
1004
+ const sessionId = meta?.labels?.sessionId;
1005
+ if (agent && sessionId && (meta?.pane == null || meta.pane === pane))
1006
+ return { agent, sessionId };
1007
+ return undefined;
1008
+ }
799
1009
  /**
800
1010
  * Agents hosted in the shared-socket tmux server — the authoritative source for
801
- * tmux-wrapped interactive spawns (see src/lib/exec.ts `runInTmux`). Enumerates
802
- * every pane on the shared socket and keeps those whose session meta was stamped
803
- * with `labels.agent` + `labels.sessionId` by the spawn-wrap. Because tmux (not a
804
- * per-window `live-terminals.json`) is the source of truth, a tmux-hosted agent is
805
- * ALWAYS captured with its exact `%pane` even when the extension registry is stale
806
- * or absent. `source: 'teams'` is skipped teammates are surfaced by listTeamsActive.
1011
+ * tmux-hosted interactive spawns (see src/lib/exec.ts `runInTmux`). Enumerates
1012
+ * every pane on the shared socket and attributes each to the agent running in it
1013
+ * via {@link resolvePaneIdentity}: the per-pane launch registry (exact) first, the
1014
+ * session-meta labels as fallback. Because tmux (not a per-window
1015
+ * `live-terminals.json`) is the source of truth, a tmux-hosted agent is captured
1016
+ * with its exact `%pane` even when the extension registry is stale — INCLUDING an
1017
+ * agent bare-spawned into a split of an existing session, which older logic dropped
1018
+ * (it kept only the first pane per session meta). `source: 'teams'` is skipped.
807
1019
  */
808
1020
  export async function listTmuxAgentSessions() {
809
1021
  const { getDefaultSocketPath } = await import('../tmux/paths.js');
@@ -825,6 +1037,21 @@ export async function listTmuxAgentSessions() {
825
1037
  }
826
1038
  if (res.code !== 0)
827
1039
  return [];
1040
+ // Index live launches by the pane they target, so a pane we did NOT wrap (a
1041
+ // split) resolves to its own agent. Newest launch wins a pane (pid reuse), and
1042
+ // only live pids count — a dead agent's stale entry can't light up its old pane.
1043
+ const liveByPane = new Map();
1044
+ for (const e of listPidSessionEntries()) {
1045
+ if (!e.tmuxPane || !isPidAlive(e.pid, e.startedAtMs))
1046
+ continue;
1047
+ const prev = liveByPane.get(e.tmuxPane);
1048
+ if (!prev || e.startedAtMs > prev.startedAtMs)
1049
+ liveByPane.set(e.tmuxPane, e);
1050
+ }
1051
+ // Hook index is scanned lazily — only when a live launch lacks a recorded id
1052
+ // (a non-Claude split) and needs the SessionStart-hook join.
1053
+ let hookIndex;
1054
+ const getHookIndex = () => (hookIndex ??= loadHookSessionIndex());
828
1055
  const out = [];
829
1056
  const seen = new Set();
830
1057
  for (const line of res.stdout.split('\n')) {
@@ -834,20 +1061,26 @@ export async function listTmuxAgentSessions() {
834
1061
  if (!pane || !sessName)
835
1062
  continue;
836
1063
  const meta = readSessionMeta(sessName);
837
- const agent = meta?.labels?.agent;
838
- const sessionId = meta?.labels?.sessionId;
839
- if (!agent || !sessionId)
840
- continue; // only our stamped agent sessions
841
- if (meta?.source === 'teams')
842
- continue; // teammates come from listTeamsActive
843
- if (seen.has(sessionId))
844
- continue; // first pane per session wins
845
- seen.add(sessionId);
846
- const pid = parseInt(pidRaw, 10) || undefined;
847
- const cwd = meta?.cwd ?? (curPath || undefined);
848
- const sessionFile = findSessionFileForKind(agent, cwd, sessionId);
1064
+ const liveEntry = liveByPane.get(pane);
1065
+ const id = resolvePaneIdentity(pane, meta, liveEntry, getHookIndex);
1066
+ if (!id)
1067
+ continue;
1068
+ // Dedupe by resolved session id; an as-yet-unresolved id (a hookless/lagging
1069
+ // split) keys on the unique pane so it still surfaces as its own row.
1070
+ const dedupKey = id.sessionId ?? pane;
1071
+ if (seen.has(dedupKey))
1072
+ continue;
1073
+ seen.add(dedupKey);
1074
+ // Prefer the registry pid (the agent), falling back to the pane leaf pid. A
1075
+ // pid we emit here goes into getActiveSessions' attributed set, so the ps-scan
1076
+ // does NOT also surface this agent as a duplicate headless row.
1077
+ const pid = id.pid ?? (parseInt(pidRaw, 10) || undefined);
1078
+ const cwd = liveEntry?.cwd ?? meta?.cwd ?? (curPath || undefined);
1079
+ const sessionFile = findSessionFileForKind(id.agent, cwd, id.sessionId);
849
1080
  const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
850
- const { state, tokPerSec } = computeLiveSignals(agent, sessionFile, cwd, pid ? isPidAlive(pid) : true);
1081
+ const pidAlive = pid ? isPidAlive(pid, liveEntry?.startedAtMs) : true;
1082
+ const { state, tokPerSec } = computeLiveSignals(id.agent, sessionFile, cwd, pidAlive);
1083
+ const { birthtimeMs, mtimeMs } = sessionFileTimes(sessionFile);
851
1084
  // Provenance is known exactly here (the pane IS a tmux pane) — set it so
852
1085
  // enrichProvenance skips it and the locator/reply rails resolve off the pane.
853
1086
  const provenance = {
@@ -858,16 +1091,20 @@ export async function listTmuxAgentSessions() {
858
1091
  };
859
1092
  out.push(applyState({
860
1093
  context: 'terminal',
861
- kind: agent,
1094
+ kind: id.agent,
862
1095
  host: 'tmux',
863
1096
  pid,
864
- sessionId,
1097
+ sessionId: id.sessionId ?? sessionIdFromFile(sessionFile),
865
1098
  cwd,
866
1099
  topic,
867
1100
  tokPerSec,
868
1101
  sessionFile,
1102
+ // tmux panes carry no start timestamp; derive both from the transcript
1103
+ // (creation ≈ start, last write ≈ last activity).
1104
+ startedAtMs: birthtimeMs,
1105
+ lastActivityMs: mtimeMs,
869
1106
  provenance,
870
- }, state, sessionFile));
1107
+ }, state, sessionFile, pidAlive));
871
1108
  }
872
1109
  return out;
873
1110
  }
@@ -898,8 +1135,26 @@ export async function getActiveSessions(opts = {}) {
898
1135
  const unattributed = opts.skipHeadless ? [] : await listUnattributedActive(knownPids);
899
1136
  const merged = dedupeBySession([...tmuxAgents, ...teams, ...terminals, ...cloud, ...unattributed]);
900
1137
  await enrichProvenance(merged);
1138
+ foldPresence(merged);
901
1139
  return merged;
902
1140
  }
1141
+ /**
1142
+ * Fold detach/attach presence onto each row from the detach store. A stored
1143
+ * record wins (`background`/`parked`); otherwise a live terminal session is
1144
+ * `attached`. Ad-hoc headless runs and cloud/team rows stay unmarked — they are
1145
+ * not on the foreground/background axis.
1146
+ */
1147
+ function foldPresence(rows) {
1148
+ for (const s of rows) {
1149
+ if (!s.sessionId)
1150
+ continue;
1151
+ const stored = presenceFromStore(s.sessionId);
1152
+ if (stored)
1153
+ s.presence = stored;
1154
+ else if (s.context === 'terminal')
1155
+ s.presence = 'attached';
1156
+ }
1157
+ }
903
1158
  /**
904
1159
  * Attach provenance (host / local-vs-SSH / tmux pane / reply rail) to every
905
1160
  * session that has a live pid. Mutates in place. Runs after dedupe so we probe
@@ -914,18 +1169,34 @@ async function enrichProvenance(sessions) {
914
1169
  s.provenance = await detectProvenance(s.pid);
915
1170
  }));
916
1171
  }
1172
+ /**
1173
+ * Identity for a row the scan could not tie to a session: a daemon's worker
1174
+ * processes (an OpenClaw gateway spawning `codex`, a supervisor pool) have no
1175
+ * session id, no transcript file, and no cloud/run handle — nothing tells two of
1176
+ * them apart, because nothing distinguishes them. Same binary + same working
1177
+ * directory + same context IS the identity, so N indistinguishable workers
1178
+ * collapse to one row carrying `pidCount: N`.
1179
+ *
1180
+ * Returns undefined with no cwd: without it there is no stable identity, and
1181
+ * keying on kind alone would fold unrelated agents onto one row.
1182
+ */
1183
+ function anonymousWorkerKey(s) {
1184
+ if (!s.cwd)
1185
+ return undefined;
1186
+ return `anon\0${s.kind}\0${s.context}\0${s.cwd}`;
1187
+ }
917
1188
  /**
918
1189
  * Collapse rows that resolve to the *same* session — a session with many
919
1190
  * subagent/fork PIDs (all matched to one transcript file) would otherwise print
920
- * dozens of identical rows. Keyed by session id (falling back to the file), the
921
- * first row wins and carries a `pidCount`. Rows with no session identity (cloud,
922
- * unresolved headless) pass through untouched.
1191
+ * dozens of identical rows. Keyed by session id, falling back to the transcript
1192
+ * file, then the cloud/run handle, then {@link anonymousWorkerKey}. The first row
1193
+ * wins and carries a `pidCount`.
923
1194
  */
924
- function dedupeBySession(sessions) {
1195
+ export function dedupeBySession(sessions) {
925
1196
  const out = [];
926
1197
  const byKey = new Map();
927
1198
  for (const s of sessions) {
928
- const key = s.sessionId || s.sessionFile;
1199
+ const key = s.sessionId || s.sessionFile || s.cloudTaskId || s.agentId || anonymousWorkerKey(s);
929
1200
  if (!key) {
930
1201
  out.push(s);
931
1202
  continue;
@@ -13,6 +13,7 @@ import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import * as os from 'os';
15
15
  import * as yaml from 'yaml';
16
+ import { deriveShortId } from './short-id.js';
16
17
  import { getCacheDir } from '../state.js';
17
18
  const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
18
19
  const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
@@ -94,7 +95,7 @@ export async function discoverCloudSessions(options) {
94
95
  const filePath = cachePathForExecution(id, agent);
95
96
  out.push({
96
97
  id,
97
- shortId: id.slice(0, 8),
98
+ shortId: deriveShortId(id),
98
99
  agent,
99
100
  timestamp,
100
101
  project,
@@ -109,6 +109,29 @@ export declare function getScanStampByPath(filePath: string): ScanStamp | null;
109
109
  * This is the fast path used by the incremental scanner — avoids N+1 queries.
110
110
  */
111
111
  export declare function getScanStampsForPaths(filePaths: string[]): Map<string, ScanStamp>;
112
+ /**
113
+ * A file's persisted resumable-parse continuation, read back from scan_ledger.
114
+ * `parserState` is the serialized {@link ClaudeParserState} JSON blob (offset +
115
+ * accumulator snapshot); `contentText` is the accumulated user doc. Both are
116
+ * written by the Claude scan (B-2) and consumed on the next scan to decide
117
+ * full-vs-incremental and to hydrate the resume.
118
+ */
119
+ export interface ParserStateRow {
120
+ parserState: string | null;
121
+ contentText: string | null;
122
+ fileMtimeMs: number;
123
+ fileSize: number;
124
+ scannedAt: number;
125
+ }
126
+ /**
127
+ * Bulk-load the resumable-parse continuation (parser_state + content_text) plus
128
+ * the stamp for a set of file paths in a single chunked query. Mirrors
129
+ * {@link getScanStampsForPaths}: keys by canonical path and fans results back to
130
+ * every original alias so callers can `.get(filePath)` with the path they passed.
131
+ * The Claude incremental scan uses this to fetch each changed file's prior
132
+ * continuation without an N+1 of {@link getScanStampByPath}.
133
+ */
134
+ export declare function getParserStatesForPaths(filePaths: string[]): Map<string, ParserStateRow>;
112
135
  /**
113
136
  * Record scan stamps for files we've looked at. Covers both files that produced
114
137
  * a session and files we looked at but chose not to index (e.g. malformed).
@@ -117,6 +140,29 @@ export declare function recordScans(entries: Array<{
117
140
  filePath: string;
118
141
  scan: ScanStamp;
119
142
  }>): void;
143
+ /** Snapshot of a leaf transcript directory used to detect create/delete/rename. */
144
+ export interface DirStamp {
145
+ dirMtimeMs: number;
146
+ entryCount: number;
147
+ }
148
+ /**
149
+ * Bulk-load the dir ledger for a set of leaf directories in a single SQL query.
150
+ * Mirrors {@link getScanStampsForPaths}: keys by canonical path (so a dir
151
+ * reachable via a symlinked version home and its realpath collapse to one row)
152
+ * and fans the result back out to every original alias so callers can
153
+ * `.get(dirPath)` with the path they passed in.
154
+ */
155
+ export declare function getDirLedgerForPaths(dirs: string[]): Map<string, DirStamp>;
156
+ /**
157
+ * Upsert dir-scan stamps. Recorded after a full readdir of a leaf transcript
158
+ * dir so the next scan can skip that dir when its (mtime, entry_count) is
159
+ * unchanged. Mirrors {@link recordScans}.
160
+ */
161
+ export declare function recordDirScans(entries: Array<{
162
+ dirPath: string;
163
+ dirMtimeMs: number;
164
+ entryCount: number;
165
+ }>): void;
120
166
  /**
121
167
  * Upsert a session row and replace its FTS5 content in a single transaction.
122
168
  * `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
@@ -127,6 +173,8 @@ export declare function upsertSessionsBatch(entries: Array<{
127
173
  meta: SessionMeta;
128
174
  content: string;
129
175
  scan?: ScanStamp;
176
+ parserState?: string;
177
+ contentText?: string;
130
178
  }>): void;
131
179
  /**
132
180
  * Sync labels for a set of sessions. For each id in the map, if the stored