@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -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 +18 -0
  13. package/dist/commands/exec.js +83 -13
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Detached-session store — the record `agents sessions detach` writes and both
3
+ * `agents sessions attach` and the active-session scan read to know an agent is
4
+ * "backgrounded": running headless with no terminal, continuing its task
5
+ * unattended.
6
+ *
7
+ * One file per detached session under `~/.agents/.system/detached/<id>.json`.
8
+ * Presence is DERIVED, never asserted: a record only means "this session was
9
+ * detached to a headless continuation"; whether it is still `background`
10
+ * (that continuation is alive) or `parked` (it has exited) is decided live from
11
+ * the recorded pid + its start-time fingerprint. That keeps the store honest
12
+ * even across a crash that never ran `attach`.
13
+ */
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { getSystemAgentsDir } from '../state.js';
17
+ import { captureProcessStartTime } from '../pty-server.js';
18
+ function detachedDir() {
19
+ return path.join(getSystemAgentsDir(), 'detached');
20
+ }
21
+ function recordPath(sessionId) {
22
+ return path.join(detachedDir(), `${sessionId}.json`);
23
+ }
24
+ export function writeDetachRecord(rec) {
25
+ fs.mkdirSync(detachedDir(), { recursive: true });
26
+ fs.writeFileSync(recordPath(rec.sessionId), JSON.stringify(rec, null, 2));
27
+ }
28
+ export function readDetachRecord(sessionId) {
29
+ try {
30
+ return JSON.parse(fs.readFileSync(recordPath(sessionId), 'utf8'));
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ export function clearDetachRecord(sessionId) {
37
+ try {
38
+ fs.rmSync(recordPath(sessionId));
39
+ }
40
+ catch {
41
+ /* already gone */
42
+ }
43
+ }
44
+ export function listDetachRecords() {
45
+ let names;
46
+ try {
47
+ names = fs.readdirSync(detachedDir());
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ const out = [];
53
+ for (const name of names) {
54
+ if (!name.endsWith('.json'))
55
+ continue;
56
+ const rec = readDetachRecord(name.slice(0, -'.json'.length));
57
+ if (rec)
58
+ out.push(rec);
59
+ }
60
+ return out;
61
+ }
62
+ /** True while the recorded headless continuation is still the live process we spawned. */
63
+ export function isHeadlessAlive(rec) {
64
+ if (!rec.headlessPid || rec.headlessPid <= 0)
65
+ return false;
66
+ try {
67
+ process.kill(rec.headlessPid, 0);
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ // Defeat PID reuse: if the pid now belongs to a different process, it is not ours.
73
+ if (rec.headlessStartTime !== null) {
74
+ const now = captureProcessStartTime(rec.headlessPid);
75
+ if (now !== null && now !== rec.headlessStartTime)
76
+ return false;
77
+ }
78
+ return true;
79
+ }
80
+ /**
81
+ * Presence for a session id from the detach store alone:
82
+ * - no record -> undefined (caller decides: `attached` for a live
83
+ * interactive row, nothing for cloud/team rows)
84
+ * - record + pid alive -> `background` (headless continuation running)
85
+ * - record + pid exited -> `parked` (the run finished; transcript is durable)
86
+ */
87
+ export function presenceFromStore(sessionId) {
88
+ const rec = readDetachRecord(sessionId);
89
+ if (!rec)
90
+ return undefined;
91
+ return isHeadlessAlive(rec) ? 'background' : 'parked';
92
+ }
@@ -6,11 +6,19 @@
6
6
  * the last run are re-parsed. All metadata is upserted into the sessions DB so
7
7
  * subsequent queries are served entirely from the cache.
8
8
  */
9
- import type { SessionAgentId, SessionMeta } from './types.js';
9
+ import type { SessionAgentId, SessionEvent, SessionMeta, TodoProgress } from './types.js';
10
10
  import { type ScanStamp } from './db.js';
11
11
  /** Options controlling which sessions to discover and how to report progress. */
12
12
  export interface DiscoverOptions {
13
13
  agent?: SessionAgentId;
14
+ /**
15
+ * Include sessions from the user's own (unmanaged) `~/.<agent>` alongside managed
16
+ * version homes. Defaults to true only when the agent has no managed versions, so
17
+ * a user who has never run `agents add` sees exactly what they see today.
18
+ */
19
+ includeUnmanaged?: boolean;
20
+ /** Called with how many rows the managed-only default hid, so callers can say so. */
21
+ onHiddenUnmanaged?: (count: number) => void;
14
22
  version?: string;
15
23
  project?: string;
16
24
  all?: boolean;
@@ -74,6 +82,33 @@ interface ClaudeSessionScan {
74
82
  spawnedTeam?: string;
75
83
  /** Plan markdown from the last ExitPlanMode tool call (Claude sessions only). */
76
84
  plan?: string;
85
+ todos?: TodoProgress;
86
+ recentDirectoriesTouched?: string[];
87
+ }
88
+ /** Lightweight metadata extracted from a Codex JSONL file during incremental scan. */
89
+ interface CodexSessionScan {
90
+ sessionId?: string;
91
+ timestamp?: string;
92
+ cwd?: string;
93
+ gitBranch?: string;
94
+ version?: string;
95
+ topic?: string;
96
+ messageCount: number;
97
+ tokenCount?: number;
98
+ /** Real generated (output) tokens, excluding cache-read/-write context. */
99
+ outputTokens?: number;
100
+ costUsd?: number;
101
+ durationMs?: number;
102
+ lastActivity?: string;
103
+ contentText?: string;
104
+ prUrl?: string;
105
+ prNumber?: number;
106
+ worktreeSlug?: string;
107
+ ticketId?: string;
108
+ createdTickets?: string[];
109
+ spawnedTeam?: string;
110
+ todos?: TodoProgress;
111
+ recentDirectoriesTouched?: string[];
77
112
  }
78
113
  /**
79
114
  * Discover sessions. Scans only files whose (mtime, size) have changed since
@@ -101,6 +136,18 @@ export declare function scanAgentsBounded<T>(items: readonly T[], run: (item: T)
101
136
  * when the path sits under the agent's backups root, the first segment below it
102
137
  * is the origin machine id; otherwise it's the local machine.
103
138
  */
139
+ /**
140
+ * True when this transcript belongs to a version agents-cli manages — i.e. it lives
141
+ * under a version home (or a backup mirror of one) rather than in the user's own
142
+ * `~/.<agent>`.
143
+ *
144
+ * `agents sessions` scans both, which is right for indexing: the DB stays a complete
145
+ * picture and `--unmanaged` can surface everything without a re-scan. But listing
146
+ * *by default* is a different question. Once you have managed versions, an unmanaged
147
+ * install's history is not really agents-cli's to show — most visibly after
148
+ * `agents add --isolated`, where the whole point was to keep the two apart.
149
+ */
150
+ export declare function isManagedSessionFile(filePath: string): boolean;
104
151
  export declare function machineForSessionFile(filePath: string, agent: string): string;
105
152
  /**
106
153
  * Count sessions in scope without running an incremental scan. Assumes the DB
@@ -108,6 +155,45 @@ export declare function machineForSessionFile(filePath: string, agent: string):
108
155
  * turn). Uses the exact same filter shape as the discover query.
109
156
  */
110
157
  export declare function countSessionsInScope(options: DiscoverOptions): number;
158
+ /**
159
+ * Canonicalize a working directory path (follows symlinks when it is local).
160
+ *
161
+ * Most callers pass a cwd RECORDED in a transcript, which may name a directory
162
+ * on another machine — a POSIX path read on a Windows host, say. `path.resolve()`
163
+ * rebases such a path onto the current drive (`/Users/me` -> `D:\Users\me`),
164
+ * inventing a location that never existed. So an absolute path is normalized but
165
+ * never rebased; only a genuinely relative one resolves against the process cwd.
166
+ *
167
+ * `path.normalize()` still runs on every branch: it collapses `.`, `..`, and
168
+ * duplicate separators, and folds separators on Windows. Both sides of the cwd
169
+ * filter in `db.ts` (`cwd = ?` and `cwd LIKE ? || path.sep || '%'`) come through
170
+ * here, so dropping that would leave a trailing slash or a `..` segment in one
171
+ * side and match nothing.
172
+ *
173
+ * Realpath is attempted only for a path that is absolute in THIS platform's
174
+ * terms. A POSIX-rooted path on Windows is drive-relative to `fs.realpathSync`,
175
+ * which would resolve `/Users/me` against the current drive and reintroduce the
176
+ * graft for any path that happens to exist locally.
177
+ */
178
+ export declare function _normalizeCwdForTest(cwd?: string): string;
179
+ /**
180
+ * Whether a query is a session id in full, rather than an id prefix or a search
181
+ * phrase.
182
+ *
183
+ * Callers use this to decide that an id lookup is the ONLY admissible
184
+ * interpretation: a complete id is unique, so when it misses there is nothing
185
+ * left to widen to. Without the check, `sessions <uuid>` fell through to the
186
+ * FTS content search, which tokenizes the UUID and matches every transcript that
187
+ * merely mentions it — surfacing unrelated sessions as if they were id matches.
188
+ *
189
+ * The accepted shapes are the ones the index actually holds, measured over a
190
+ * 12,507-row index: a bare UUID (11,116 rows), `session_` + UUID (1,360 — kimi
191
+ * and rush), and `ses_` + ULID (15 — opencode). Deliberately NOT covered, so a
192
+ * miss keeps today's search behavior rather than gaining a wrong error: routine
193
+ * run ids (ISO timestamps, matched via `routineRunId` below) and cloud execution
194
+ * ids, whose charset is too permissive to distinguish from a search phrase.
195
+ */
196
+ export declare function isCompleteSessionId(query: string): boolean;
111
197
  /**
112
198
  * Resolve a session by full or short ID. Accepts a pre-loaded session list
113
199
  * (fast path from discoverSessions) and falls back to a DB lookup for the
@@ -119,7 +205,87 @@ export declare function resolveSessionById(sessions: SessionMeta[], idQuery: str
119
205
  * preserving the existing SessionMeta[] contract so sessions.ts is unchanged.
120
206
  */
121
207
  export declare function searchContentIndex(sessions: SessionMeta[], query: string): Map<string, SessionMeta>;
208
+ /**
209
+ * For a list of files, stat each, compare to the DB ledger, and return only
210
+ * the ones that need rescanning. One bulk DB query for the whole list.
211
+ *
212
+ * Actively running agents append to their JSONL every few seconds. Without a
213
+ * small debounce, repeated `agents sessions` invocations stream-parse the same
214
+ * growing transcript over and over. The cached row is good enough for a few
215
+ * seconds; once writes settle or the debounce expires, the file is parsed once.
216
+ */
217
+ export declare function filterChangedFiles(filePaths: string[]): Array<{
218
+ filePath: string;
219
+ scan: ScanStamp;
220
+ }>;
221
+ /** A path already stat'd by the walk — mtime is the raw (unfloored) fs value. */
222
+ export interface PreStatEntry {
223
+ filePath: string;
224
+ fileMtimeMs: number;
225
+ fileSize: number;
226
+ }
227
+ /**
228
+ * Ledger-compare pre-stat'd entries (from the walk's own stat) without a second
229
+ * stat. Same debounce and change-detection as filterChangedFiles; the raw
230
+ * mtime is floored here so warm files match the ledger exactly as the stat path
231
+ * does (Math.floor(stat.mtimeMs)).
232
+ */
233
+ export declare function filterChangedEntries(entries: PreStatEntry[]): Array<{
234
+ filePath: string;
235
+ scan: ScanStamp;
236
+ }>;
122
237
  export declare function shouldDeferRecentAppend(prev: ScanStamp, current: ScanStamp, nowMs: number, debounceMs?: number): boolean;
238
+ /** One leaf directory of transcripts to change-detect, plus its live-root flag. */
239
+ export interface LeafDir {
240
+ /** Absolute path to the directory that directly holds transcript files. */
241
+ dirPath: string;
242
+ /**
243
+ * True if this dir is under the agent's LIVE `~/.<agent>` root — the only tree
244
+ * an agent process appends to live. Every file in such a dir is treated as
245
+ * hot (always re-stat'd), so an in-place append is never missed there.
246
+ */
247
+ isLiveRoot: boolean;
248
+ }
249
+ /** The changed files a leaf-dir walk surfaced, ready to parse + upsert. */
250
+ export interface LeafDirScan {
251
+ /** Files whose (mtime, size) changed vs the ledger — the parse set. */
252
+ changed: Array<{
253
+ filePath: string;
254
+ scan: ScanStamp;
255
+ }>;
256
+ /**
257
+ * Every transcript file seen across all leaf dirs (changed or not), in
258
+ * live-root-first order, each tagged with whether its dir is a live root.
259
+ * Lets a caller restore cross-root, session-id precedence (prefer the live
260
+ * copy of a session over a frozen backup copy) independent of which copy
261
+ * happened to be flagged "changed" this run.
262
+ */
263
+ allFiles: Array<{
264
+ filePath: string;
265
+ isLiveRoot: boolean;
266
+ }>;
267
+ }
268
+ /**
269
+ * Walk a set of leaf transcript directories and return the files that changed,
270
+ * skipping the per-file `stat` of directories whose (mtime, entry_count) matches
271
+ * the dir_ledger.
272
+ *
273
+ * Per leaf dir:
274
+ * - `stat` the dir once and `readdir` it (one cheap syscall) to get the entry
275
+ * count and the file list.
276
+ * - If the dir matches the dir_ledger (floored mtime AND entry_count), no file
277
+ * was created / deleted / renamed since we last walked it. We then stat ONLY
278
+ * the hot files (live-root files, or files scanned within HOT_FILE_WINDOW_MS)
279
+ * and run just those through the ledger compare — so an in-place append to a
280
+ * still-live session is still caught, while immutable backup/version dirs
281
+ * collapse to a single dir stat and zero per-file stats.
282
+ * - Else (changed dir, or no ledger row) we stat every file (today's full
283
+ * walk) and record the fresh dir stamp so the next run can short-circuit.
284
+ *
285
+ * The kill-switch (`AGENTS_SESSIONS_NO_DIR_LEDGER=1`) forces the full-walk branch
286
+ * for every dir and never consults or records the dir_ledger.
287
+ */
288
+ export declare function collectChangedFilesInLeafDirs(leafDirs: LeafDir[], ext: string): LeafDirScan;
123
289
  /**
124
290
  * Collect all directories to scan for an agent's sessions. Deduplicates by
125
291
  * realpath to avoid double-counting symlinked version homes.
@@ -165,14 +331,323 @@ export declare function parseCodexThreadNameIndex(raw: string): Map<string, stri
165
331
  * performs is deferred until we know this file is a real session worth building
166
332
  * metadata for, and only then — never during the file walk / stat phase.
167
333
  */
168
- export declare function readCodexMeta(filePath: string, resolveAccount?: () => string | undefined, currentVersion?: string): Promise<{
334
+ export declare function readCodexMeta(filePath: string, resolveAccount?: () => string | undefined, currentVersion?: string, scanStamp?: ScanStamp, priorRow?: {
335
+ parserState: string | null;
336
+ fileMtimeMs: number;
337
+ }): Promise<{
169
338
  meta: SessionMeta;
170
339
  content: string;
340
+ parserState?: string;
341
+ contentText?: string;
171
342
  } | null>;
343
+ /**
344
+ * Mutable accumulator for the Claude transcript reducer. One field per local
345
+ * that {@link scanClaudeSession} previously declared inline — the reducer
346
+ * mutates `state.*` instead of closure locals so the exact same logic can drive
347
+ * both a full parse and a resumable incremental parse (see
348
+ * {@link scanClaudeSessionIncremental}).
349
+ */
350
+ export interface ClaudeParseState {
351
+ timestamp?: string;
352
+ cwd?: string;
353
+ gitBranch?: string;
354
+ version?: string;
355
+ topic?: string;
356
+ customTitle?: string;
357
+ aiTitle?: string;
358
+ entrypoint?: string;
359
+ messageCount: number;
360
+ tokenCount: number;
361
+ outputTokens: number;
362
+ sawTokenCount: boolean;
363
+ costUsd: number;
364
+ sawCost: boolean;
365
+ firstTsMs?: number;
366
+ lastTsMs?: number;
367
+ seenAssistantIds: Set<string>;
368
+ userTexts: string[];
369
+ sawPrCreate: boolean;
370
+ prUrl?: string;
371
+ prNumber?: number;
372
+ createdTickets: Set<string>;
373
+ pendingTicketTools: Set<string>;
374
+ spawnedTeam?: string;
375
+ plan?: string;
376
+ checklistEvents: SessionEvent[];
377
+ recentDirectoriesTouched: string[];
378
+ }
379
+ /** Zero-value accumulator for a fresh (from-byte-0) Claude parse. */
380
+ export declare function initClaudeParseState(): ClaudeParseState;
381
+ /**
382
+ * Fold one parsed transcript line into the accumulator. This is the exact loop
383
+ * body {@link scanClaudeSession} used to run inline — extracted verbatim,
384
+ * mutating `state.*` in place. `parsed` is the already-`JSON.parse`d line (the
385
+ * malformed-line skip happens in the caller, as before).
386
+ */
387
+ export declare function applyClaudeLine(state: ClaudeParseState, parsed: any): void;
388
+ /**
389
+ * Build the {@link ClaudeSessionScan} return object from an accumulator. This is
390
+ * the exact return-building {@link scanClaudeSession} used to run inline.
391
+ */
392
+ export declare function finalizeClaudeScan(state: ClaudeParseState): ClaudeSessionScan;
172
393
  /** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
173
394
  export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessionScan>;
395
+ /**
396
+ * SERIALIZED continuation blob persisted in `scan_ledger.parser_state`. Carries
397
+ * everything {@link hydrateClaudeParseState} needs to resume a parse from
398
+ * `offset` such that resuming + applying the appended lines is byte-for-byte
399
+ * identical to a full parse of the whole file.
400
+ *
401
+ * `seenAssistantIds` is persisted as a size counter plus a bounded FIFO window
402
+ * of the most-recent ids: the fallback logical id `${ts}:${seenAssistantIds.size}`
403
+ * (see {@link applyClaudeLine}) depends on the set's *size*, so the size must be
404
+ * exact even when the recent window is smaller than the true count.
405
+ */
406
+ export interface ClaudeParserState {
407
+ v: 1;
408
+ offset: number;
409
+ timestamp?: string;
410
+ cwd?: string;
411
+ gitBranch?: string;
412
+ version?: string;
413
+ entrypoint?: string;
414
+ firstTsMs?: number;
415
+ topic?: string;
416
+ customTitle?: string;
417
+ aiTitle?: string;
418
+ plan?: string;
419
+ lastTsMs?: number;
420
+ messageCount: number;
421
+ tokenCount: number;
422
+ outputTokens: number;
423
+ sawTokenCount: boolean;
424
+ sawCost: boolean;
425
+ costUsd: number;
426
+ seenIdsSize: number;
427
+ seenIdsRecent: string[];
428
+ sawPrCreate: boolean;
429
+ prUrl?: string;
430
+ prNumber?: number;
431
+ pendingTicketTools: string[];
432
+ createdTickets: string[];
433
+ spawnedTeam?: string;
434
+ ticketId?: string;
435
+ contentText?: string;
436
+ checklistEvents: SessionEvent[];
437
+ recentDirectoriesTouched: string[];
438
+ }
439
+ /**
440
+ * Snapshot a live {@link ClaudeParseState} into its serializable form at
441
+ * `offset` bytes consumed. Round-trips through {@link hydrateClaudeParseState}
442
+ * so incremental replay equals a full parse.
443
+ */
444
+ export declare function serializeClaudeParserState(state: ClaudeParseState, offset: number): ClaudeParserState;
445
+ /**
446
+ * Rebuild a live {@link ClaudeParseState} from a persisted continuation so that
447
+ * applying the appended lines yields the same accumulator a full parse would.
448
+ *
449
+ * `seenAssistantIds` is rehydrated from the recent-id FIFO window, then padded
450
+ * with unique sentinel entries so its `.size` matches the true prior count
451
+ * (`seenIdsSize`) — the fallback id `${ts}:${size}` must line up with the full
452
+ * parse even when the window dropped older ids. Padding sentinels can never
453
+ * collide with a real logical id (real ids are message ids/uuids or
454
+ * `${ts}:${n}`; the sentinel prefix is not JSON-line-derived).
455
+ *
456
+ * `userTexts` is rehydrated as a single joined blob from `contentText`: only
457
+ * `userTexts.join('\n')` (detectTicket + contentText) and `userTexts.length > 0`
458
+ * are ever read downstream, and both are preserved by a one-element array
459
+ * holding the joined content.
460
+ */
461
+ export declare function hydrateClaudeParseState(prior: ClaudeParserState): ClaudeParseState;
462
+ /**
463
+ * Resume a Claude parse from `fromOffset` bytes into the file, folding only the
464
+ * newly-appended lines into `prior`. Returns the finalized scan, the next
465
+ * serialized continuation, and the byte offset to resume from next time —
466
+ * `newOffset` stops at the last `'\n'` seen so a half-written trailing record is
467
+ * re-read (not lost) on the next append.
468
+ *
469
+ * NOT wired into the live scan path yet (that is B-2); {@link scanClaudeSession}
470
+ * remains the only caller-facing entry point. This exists so the parity harness
471
+ * can prove full === hydrate(state@k) + apply(k+1..n).
472
+ */
473
+ export declare function scanClaudeSessionIncremental(filePath: string, fromOffset: number, prior: ClaudeParserState): Promise<{
474
+ scan: ClaudeSessionScan;
475
+ newState: ClaudeParserState;
476
+ newOffset: number;
477
+ }>;
478
+ /** Test seam: read the (incremental, full) Claude parse counters. */
479
+ export declare function __claudeScanBranchCountsForTest(): {
480
+ incremental: number;
481
+ full: number;
482
+ };
483
+ /** Test seam: reset the Claude parse-branch counters to observe a scan from a clean slate. */
484
+ export declare function __resetClaudeScanBranchCountsForTest(): void;
485
+ /**
486
+ * Live (in-memory) accumulator for a Codex parse — the mutable state
487
+ * {@link scanCodexSession} used to hold in local `let`s, extracted so the same
488
+ * fold ({@link applyCodexLine}) runs for both a full parse and an incremental
489
+ * resume. Mirrors {@link ClaudeParseState}.
490
+ */
491
+ export interface CodexParseState {
492
+ sessionId?: string;
493
+ timestamp?: string;
494
+ cwd?: string;
495
+ gitBranch?: string;
496
+ version?: string;
497
+ model?: string;
498
+ topic?: string;
499
+ messageCount: number;
500
+ tokenCount?: number;
501
+ lastTotalTokenUsage?: any;
502
+ firstTsMs?: number;
503
+ lastTsMs?: number;
504
+ userTexts: string[];
505
+ sawPrCreate: boolean;
506
+ prUrl?: string;
507
+ prNumber?: number;
508
+ createdTickets: Set<string>;
509
+ pendingTicketTools: Set<string>;
510
+ spawnedTeam?: string;
511
+ checklistEvents: SessionEvent[];
512
+ recentDirectoriesTouched: string[];
513
+ }
514
+ /** Zero-value accumulator for a fresh (from-byte-0) Codex parse. */
515
+ export declare function initCodexParseState(): CodexParseState;
516
+ /**
517
+ * Fold one parsed Codex line into the accumulator — the exact loop body
518
+ * {@link scanCodexSession} used to run inline, extracted verbatim and mutating
519
+ * `state.*` in place. `parsed` is the already-`JSON.parse`d line (the
520
+ * malformed-line skip happens in the caller, as before).
521
+ */
522
+ export declare function applyCodexLine(state: CodexParseState, parsed: any): void;
523
+ /**
524
+ * Build the {@link CodexSessionScan} return object from an accumulator — the
525
+ * exact return-building {@link scanCodexSession} used to run inline.
526
+ */
527
+ export declare function finalizeCodexScan(state: CodexParseState): CodexSessionScan;
528
+ /**
529
+ * SERIALIZED continuation blob persisted in `scan_ledger.parser_state` for a
530
+ * Codex rollout. Carries everything {@link hydrateCodexParseState} needs to
531
+ * resume a parse from `offset` such that resuming + applying the appended lines
532
+ * is byte-for-byte identical to a full parse of the whole file.
533
+ *
534
+ * Unlike Claude, Codex has NO per-message dedup set, so `messageCount` is a
535
+ * plain additive base with no recent-id window to persist. The `lastTotalTokenUsage`
536
+ * object is round-tripped whole so the last-wins cost/output-token pricing at
537
+ * finalize is identical after a resume.
538
+ */
539
+ export interface CodexParserState {
540
+ v: 1;
541
+ offset: number;
542
+ sessionId?: string;
543
+ timestamp?: string;
544
+ cwd?: string;
545
+ gitBranch?: string;
546
+ version?: string;
547
+ model?: string;
548
+ topic?: string;
549
+ messageCount: number;
550
+ tokenCount?: number;
551
+ lastTotalTokenUsage?: any;
552
+ firstTsMs?: number;
553
+ lastTsMs?: number;
554
+ sawPrCreate: boolean;
555
+ prUrl?: string;
556
+ prNumber?: number;
557
+ pendingTicketTools: string[];
558
+ createdTickets: string[];
559
+ spawnedTeam?: string;
560
+ ticketId?: string;
561
+ contentText?: string;
562
+ checklistEvents: SessionEvent[];
563
+ recentDirectoriesTouched: string[];
564
+ }
565
+ /**
566
+ * Snapshot a live {@link CodexParseState} into its serializable form at `offset`
567
+ * bytes consumed. Round-trips through {@link hydrateCodexParseState} so
568
+ * incremental replay equals a full parse.
569
+ */
570
+ export declare function serializeCodexParserState(state: CodexParseState, offset: number): CodexParserState;
571
+ /**
572
+ * Rebuild a live {@link CodexParseState} from a persisted continuation so that
573
+ * applying the appended lines yields the same accumulator a full parse would.
574
+ *
575
+ * `userTexts` is rehydrated as a single joined blob from `contentText`: only
576
+ * `userTexts.join('\n')` (detectTicket + contentText) and `userTexts.length > 0`
577
+ * are ever read downstream, and both are preserved by a one-element array
578
+ * holding the joined content. Topic is first-wins and already persisted, so a
579
+ * collapsed userTexts never changes it.
580
+ */
581
+ export declare function hydrateCodexParseState(prior: CodexParserState): CodexParseState;
582
+ /**
583
+ * Resume a Codex parse from `fromOffset` bytes into the file, folding only the
584
+ * newly-appended lines into `prior`. Returns the finalized scan, the next
585
+ * serialized continuation, and the byte offset to resume from next time.
586
+ *
587
+ * Same trailing-line discipline as {@link scanClaudeSessionIncremental}: apply
588
+ * ONLY the run of newline-terminated lines (slice at the last `'\n'`), and set
589
+ * `newOffset = fromOffset + consumedBytes`. Any tail after the last `'\n'` —
590
+ * syntactically broken OR a complete-but-not-yet-terminated record — is DEFERRED
591
+ * to the next pass once its `'\n'` lands. Codex `messageCount` is additive with
592
+ * NO dedup, so re-reading a still-unterminated complete line would double-count
593
+ * it; deferring prevents that (the bug class prix-cloud caught for Claude).
594
+ */
595
+ export declare function scanCodexSessionIncremental(filePath: string, fromOffset: number, prior: CodexParserState): Promise<{
596
+ scan: CodexSessionScan;
597
+ newState: CodexParserState;
598
+ newOffset: number;
599
+ }>;
600
+ /** Test seam: read the (incremental, full) Codex parse counters. */
601
+ export declare function __codexScanBranchCountsForTest(): {
602
+ incremental: number;
603
+ full: number;
604
+ };
605
+ /** Test seam: reset the Codex parse-branch counters to observe a scan from a clean slate. */
606
+ export declare function __resetCodexScanBranchCountsForTest(): void;
174
607
  /** Parse a single Kimi session state.json file to extract session metadata. */
175
- export declare function readKimiMeta(filePath: string): {
608
+ export declare function readKimiMeta(filePath: string, priorRow?: {
609
+ parserState: string | null;
610
+ }): {
611
+ meta: SessionMeta;
612
+ content: string;
613
+ parserState?: string;
614
+ } | null;
615
+ /**
616
+ * Kimi wire metrics are pure additive counters (messageCount, tokenCount,
617
+ * outputTokens) with NO straddle/dedup state, so the continuation is just those
618
+ * three bases plus the byte `offset` already consumed from wire.jsonl. Resuming
619
+ * from `offset` + adding the appended tail's deltas equals a full parse.
620
+ */
621
+ export interface KimiParserState {
622
+ v: 1;
623
+ offset: number;
624
+ messageCount: number;
625
+ tokenCount: number;
626
+ outputTokens: number;
627
+ }
628
+ /**
629
+ * Incrementally parse Kimi's wire.jsonl for message-count and token counters,
630
+ * resuming from a persisted continuation instead of re-reading from byte 0 every
631
+ * scan. Returns the finalized counters and the next {@link KimiParserState} to
632
+ * persist (offset + the three counter bases).
633
+ *
634
+ * Same trailing-line discipline as {@link scanClaudeSessionIncremental}: read
635
+ * only the appended byte range from `prior.offset`, apply ONLY the run of
636
+ * newline-terminated lines (slice at the last `'\n'`), and advance the offset to
637
+ * `prior.offset + consumedBytes`. A complete-but-not-yet-terminated last record
638
+ * is DEFERRED to the next pass; because these counters are additive with no
639
+ * dedup, re-reading such a line would double-count it. FULL parse from byte 0
640
+ * (fresh counters) when there is no prior OR the file shrank below the stored
641
+ * offset (truncation/rewrite).
642
+ */
643
+ export declare function parseKimiWireMetricsIncremental(sessionDir: string, prior: KimiParserState | null): {
644
+ messageCount: number;
645
+ tokenCount: number;
646
+ outputTokens: number;
647
+ newState: KimiParserState;
648
+ };
649
+ /** Parse a single Grok session summary.json into session metadata. */
650
+ export declare function readGrokMeta(filePath: string, currentVersion?: string): {
176
651
  meta: SessionMeta;
177
652
  content: string;
178
653
  } | null;