@phnx-labs/agents-cli 1.22.117 → 1.22.119

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 (271) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/accounts.js +7 -4
  4. package/dist/commands/browser.d.ts +52 -70
  5. package/dist/commands/browser.js +192 -3562
  6. package/dist/commands/daemon.d.ts +39 -2
  7. package/dist/commands/daemon.js +72 -73
  8. package/dist/commands/events.js +1 -1
  9. package/dist/commands/exec.js +53 -1
  10. package/dist/commands/projects.js +92 -7
  11. package/dist/commands/repo.js +13 -49
  12. package/dist/commands/route.js +5 -5
  13. package/dist/commands/routines.js +39 -12
  14. package/dist/commands/sessions-render.d.ts +1 -1
  15. package/dist/commands/sessions-render.js +0 -1
  16. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  17. package/dist/commands/sessions.test-fixture.js +0 -37
  18. package/dist/commands/setup-browser.d.ts +17 -12
  19. package/dist/commands/setup-browser.js +52 -87
  20. package/dist/commands/setup-preferences.d.ts +0 -17
  21. package/dist/commands/setup-preferences.js +18 -49
  22. package/dist/commands/setup.js +9 -9
  23. package/dist/commands/ssh.js +10 -1
  24. package/dist/commands/sync.js +6 -44
  25. package/dist/commands/teams.js +0 -1
  26. package/dist/commands/update.d.ts +19 -0
  27. package/dist/commands/update.js +34 -6
  28. package/dist/commands/view.d.ts +12 -1
  29. package/dist/commands/view.js +47 -2
  30. package/dist/index.js +107 -26
  31. package/dist/lib/account-capabilities.js +0 -2
  32. package/dist/lib/account-catalog.d.ts +38 -0
  33. package/dist/lib/account-catalog.js +110 -26
  34. package/dist/lib/account-provider-registry.js +1 -1
  35. package/dist/lib/account-registry.d.ts +1 -1
  36. package/dist/lib/account-registry.js +36 -2
  37. package/dist/lib/accounting/rotate.js +22 -8
  38. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  39. package/dist/lib/accounting/usage-ingest.js +115 -45
  40. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  41. package/dist/lib/accounting/usage-sync.js +227 -66
  42. package/dist/lib/accounting/usage.d.ts +16 -3
  43. package/dist/lib/accounting/usage.js +26 -8
  44. package/dist/lib/accounts/add.d.ts +3 -1
  45. package/dist/lib/accounts/add.js +24 -13
  46. package/dist/lib/accounts/slots.d.ts +8 -0
  47. package/dist/lib/accounts/slots.js +17 -0
  48. package/dist/lib/add-dir.js +0 -1
  49. package/dist/lib/agent-cli-commands.js +0 -1
  50. package/dist/lib/agent-spec/agents.d.ts +6 -7
  51. package/dist/lib/agent-spec/agents.js +7 -60
  52. package/dist/lib/auth-health.d.ts +69 -1
  53. package/dist/lib/auth-health.js +133 -4
  54. package/dist/lib/browser/context.d.ts +82 -0
  55. package/dist/lib/browser/context.js +88 -0
  56. package/dist/lib/browser/paths.d.ts +25 -0
  57. package/dist/lib/browser/paths.js +51 -0
  58. package/dist/lib/browser/record.d.ts +38 -0
  59. package/dist/lib/browser/record.js +57 -0
  60. package/dist/lib/browser/sessions-list.js +1 -2
  61. package/dist/lib/browser-client.d.ts +158 -0
  62. package/dist/lib/browser-client.js +234 -0
  63. package/dist/lib/channels/owner-sink.d.ts +1 -18
  64. package/dist/lib/channels/owner-sink.js +29 -82
  65. package/dist/lib/channels/providers/rush.d.ts +7 -7
  66. package/dist/lib/channels/providers/rush.js +127 -57
  67. package/dist/lib/claude-account-token.d.ts +7 -1
  68. package/dist/lib/claude-account-token.js +26 -4
  69. package/dist/lib/claude-statusline.d.ts +1 -1
  70. package/dist/lib/claude-statusline.js +15 -3
  71. package/dist/lib/commands.js +1 -1
  72. package/dist/lib/crabbox/lease.js +1 -1
  73. package/dist/lib/crabbox/runtimes.js +1 -3
  74. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  75. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  76. package/dist/lib/daemon/auth-sync-service.js +37 -43
  77. package/dist/lib/daemon/daemon.d.ts +25 -14
  78. package/dist/lib/daemon/daemon.js +92 -154
  79. package/dist/lib/daemon/runner.d.ts +2 -2
  80. package/dist/lib/daemon/runner.js +32 -9
  81. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  82. package/dist/lib/daemon/self-update-service.js +40 -36
  83. package/dist/lib/daemon/service.d.ts +12 -8
  84. package/dist/lib/daemon/supervisor.d.ts +65 -54
  85. package/dist/lib/daemon/supervisor.js +136 -171
  86. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  87. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  88. package/dist/lib/daemon/usage-sync-service.js +46 -78
  89. package/dist/lib/daemon-health.d.ts +22 -5
  90. package/dist/lib/daemon-health.js +47 -3
  91. package/dist/lib/daemon-services.d.ts +1 -1
  92. package/dist/lib/daemon-services.js +0 -10
  93. package/dist/lib/daemon-ticks.js +4 -2
  94. package/dist/lib/devices/doctor-findings.js +8 -11
  95. package/dist/lib/devices/harness-inventory.js +14 -5
  96. package/dist/lib/exec-account-home.js +5 -0
  97. package/dist/lib/exec.js +10 -13
  98. package/dist/lib/feed/events.js +0 -1
  99. package/dist/lib/feed/tool-activity.js +5 -2
  100. package/dist/lib/fleet-shared-state.d.ts +45 -0
  101. package/dist/lib/fleet-shared-state.js +78 -9
  102. package/dist/lib/fs-atomic.js +25 -0
  103. package/dist/lib/github/pr-mergeable.d.ts +4 -2
  104. package/dist/lib/github/pr-mergeable.js +5 -3
  105. package/dist/lib/github/project-prs.d.ts +58 -3
  106. package/dist/lib/github/project-prs.js +205 -7
  107. package/dist/lib/harness/adapters/cursor.js +14 -4
  108. package/dist/lib/harness-auth-capabilities.d.ts +4 -3
  109. package/dist/lib/harness-auth-capabilities.js +1 -2
  110. package/dist/lib/helper-versions.js +1 -1
  111. package/dist/lib/hooks/cache.d.ts +7 -0
  112. package/dist/lib/hooks/cache.js +27 -2
  113. package/dist/lib/hooks/install.js +7 -1
  114. package/dist/lib/hosts/ready.d.ts +38 -8
  115. package/dist/lib/hosts/ready.js +62 -14
  116. package/dist/lib/installations/active-check.d.ts +27 -0
  117. package/dist/lib/installations/active-check.js +61 -0
  118. package/dist/lib/installations/migrate.js +6 -3
  119. package/dist/lib/installations/shims.d.ts +1 -1
  120. package/dist/lib/installations/shims.js +32 -12
  121. package/dist/lib/installations/store.d.ts +9 -0
  122. package/dist/lib/installations/store.js +24 -0
  123. package/dist/lib/installations/update.js +21 -15
  124. package/dist/lib/installations/versions.js +15 -3
  125. package/dist/lib/manifest.js +1 -1
  126. package/dist/lib/models.js +0 -2
  127. package/dist/lib/open-url.js +28 -54
  128. package/dist/lib/profiles.js +0 -2
  129. package/dist/lib/projects.d.ts +16 -0
  130. package/dist/lib/projects.js +31 -0
  131. package/dist/lib/refresh.js +1 -1
  132. package/dist/lib/routine-readiness.d.ts +35 -0
  133. package/dist/lib/routine-readiness.js +74 -16
  134. package/dist/lib/rules/compile.js +6 -0
  135. package/dist/lib/sandbox.js +0 -2
  136. package/dist/lib/secrets-policy.d.ts +89 -11
  137. package/dist/lib/secrets-policy.js +175 -28
  138. package/dist/lib/session/active.d.ts +17 -6
  139. package/dist/lib/session/active.js +105 -49
  140. package/dist/lib/session/db.d.ts +1 -1
  141. package/dist/lib/session/db.js +65 -18
  142. package/dist/lib/session/discover.js +1 -283
  143. package/dist/lib/session/mirror.d.ts +4 -3
  144. package/dist/lib/session/mirror.js +4 -3
  145. package/dist/lib/session/parse.d.ts +0 -2
  146. package/dist/lib/session/parse.js +1 -138
  147. package/dist/lib/session/pid-registry.d.ts +6 -3
  148. package/dist/lib/session/pid-registry.js +18 -8
  149. package/dist/lib/session/remote/watch.d.ts +1 -1
  150. package/dist/lib/session/remote/watch.js +7 -3
  151. package/dist/lib/session/throughput.d.ts +1 -3
  152. package/dist/lib/session/throughput.js +0 -20
  153. package/dist/lib/session/types.d.ts +2 -2
  154. package/dist/lib/session/types.js +2 -2
  155. package/dist/lib/sessions-client.d.ts +60 -1
  156. package/dist/lib/sessions-client.js +227 -4
  157. package/dist/lib/setup-tool-status.js +5 -10
  158. package/dist/lib/signin-badge.d.ts +3 -2
  159. package/dist/lib/signin-badge.js +6 -3
  160. package/dist/lib/smart-launch.js +4 -1
  161. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  162. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  163. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  164. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  165. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  166. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  167. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  168. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  169. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  170. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  171. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  172. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  173. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  174. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  175. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  176. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  177. package/dist/lib/staleness/writers/sources.js +5 -0
  178. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  179. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  180. package/dist/lib/state.d.ts +6 -4
  181. package/dist/lib/state.js +7 -17
  182. package/dist/lib/teams/parsers.d.ts +1 -1
  183. package/dist/lib/teams/parsers.js +1 -136
  184. package/dist/lib/teams/placement-probe.js +50 -15
  185. package/dist/lib/teams/scheduler.d.ts +7 -0
  186. package/dist/lib/types.d.ts +16 -1
  187. package/dist/lib/types.js +1 -1
  188. package/dist/lib/view-types.d.ts +32 -0
  189. package/dist/lib/watchdog/read.js +0 -1
  190. package/dist/lib/watchdog/rotate.js +1 -2
  191. package/dist/lib/watchdog/runner.js +2 -2
  192. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  193. package/dist/lib/watchdog/watchdogTail.js +0 -31
  194. package/package.json +1 -1
  195. package/scripts/postinstall.js +8 -8
  196. package/dist/browser.d.ts +0 -2
  197. package/dist/browser.js +0 -17
  198. package/dist/commands/browser-picker.d.ts +0 -18
  199. package/dist/commands/browser-picker.js +0 -97
  200. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  201. package/dist/lib/browser/arc-discovery.js +0 -197
  202. package/dist/lib/browser/arc-dom.d.ts +0 -14
  203. package/dist/lib/browser/arc-dom.js +0 -121
  204. package/dist/lib/browser/caller-identity.d.ts +0 -40
  205. package/dist/lib/browser/caller-identity.js +0 -175
  206. package/dist/lib/browser/cdp.d.ts +0 -51
  207. package/dist/lib/browser/cdp.js +0 -254
  208. package/dist/lib/browser/chrome.d.ts +0 -141
  209. package/dist/lib/browser/chrome.js +0 -704
  210. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  211. package/dist/lib/browser/chromium-discovery.js +0 -96
  212. package/dist/lib/browser/devices.d.ts +0 -34
  213. package/dist/lib/browser/devices.js +0 -61
  214. package/dist/lib/browser/domain-skills.d.ts +0 -71
  215. package/dist/lib/browser/domain-skills.js +0 -195
  216. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  217. package/dist/lib/browser/drivers/arc.js +0 -276
  218. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  219. package/dist/lib/browser/drivers/firefox.js +0 -377
  220. package/dist/lib/browser/drivers/local.d.ts +0 -55
  221. package/dist/lib/browser/drivers/local.js +0 -266
  222. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  223. package/dist/lib/browser/drivers/ssh.js +0 -467
  224. package/dist/lib/browser/editor.d.ts +0 -3
  225. package/dist/lib/browser/editor.js +0 -50
  226. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  227. package/dist/lib/browser/ffmpeg.js +0 -184
  228. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  229. package/dist/lib/browser/firefox-discovery.js +0 -162
  230. package/dist/lib/browser/har.d.ts +0 -85
  231. package/dist/lib/browser/har.js +0 -77
  232. package/dist/lib/browser/hygiene.d.ts +0 -97
  233. package/dist/lib/browser/hygiene.js +0 -153
  234. package/dist/lib/browser/index.d.ts +0 -5
  235. package/dist/lib/browser/index.js +0 -5
  236. package/dist/lib/browser/input.d.ts +0 -7
  237. package/dist/lib/browser/input.js +0 -92
  238. package/dist/lib/browser/ipc.d.ts +0 -204
  239. package/dist/lib/browser/ipc.js +0 -1453
  240. package/dist/lib/browser/login-detection.d.ts +0 -87
  241. package/dist/lib/browser/login-detection.js +0 -274
  242. package/dist/lib/browser/profiles.d.ts +0 -422
  243. package/dist/lib/browser/profiles.js +0 -1157
  244. package/dist/lib/browser/refs.d.ts +0 -89
  245. package/dist/lib/browser/refs.js +0 -191
  246. package/dist/lib/browser/registry.d.ts +0 -70
  247. package/dist/lib/browser/registry.js +0 -252
  248. package/dist/lib/browser/remote-control.d.ts +0 -59
  249. package/dist/lib/browser/remote-control.js +0 -85
  250. package/dist/lib/browser/resolve-target.d.ts +0 -66
  251. package/dist/lib/browser/resolve-target.js +0 -264
  252. package/dist/lib/browser/runtime-state.d.ts +0 -291
  253. package/dist/lib/browser/runtime-state.js +0 -584
  254. package/dist/lib/browser/secret-ref.d.ts +0 -10
  255. package/dist/lib/browser/secret-ref.js +0 -14
  256. package/dist/lib/browser/service.d.ts +0 -812
  257. package/dist/lib/browser/service.js +0 -4693
  258. package/dist/lib/browser/stream.d.ts +0 -17
  259. package/dist/lib/browser/stream.js +0 -72
  260. package/dist/lib/browser/task-index.d.ts +0 -78
  261. package/dist/lib/browser/task-index.js +0 -199
  262. package/dist/lib/browser/types.d.ts +0 -647
  263. package/dist/lib/browser/types.js +0 -61
  264. package/dist/lib/browser/upload.d.ts +0 -29
  265. package/dist/lib/browser/upload.js +0 -298
  266. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  267. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  268. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  269. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  270. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  271. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -29,7 +29,7 @@ const DB_PATH = getSessionsDbPath();
29
29
  /** Current schema version; bumped when migrations are added. Exported so tests
30
30
  * assert against the constant instead of hardcoding a number that every bump
31
31
  * then has to chase (docs/sessions.md calls the constant the source of truth). */
32
- export const SCHEMA_VERSION = 50;
32
+ export const SCHEMA_VERSION = 51;
33
33
  /**
34
34
  * Bump to force the content extractor (assistant-answer text, alongside the
35
35
  * user-prompt text every harness already accumulates) to re-derive on every
@@ -182,6 +182,10 @@ CREATE INDEX IF NOT EXISTS idx_sessions_short_id ON sessions(short_id);
182
182
  -- v17 guarantees the machine column exists (same pattern as last_activity);
183
183
  -- idx_sessions_mirror_synced likewise waits for migration v46's column add.
184
184
 
185
+ -- A row sits at the rowid of the sessions row it describes (v51) and is
186
+ -- addressed by that rowid, never by session_id: an UNINDEXED FTS5 column cannot
187
+ -- be seeked, so a session_id predicate scans every row's content — the same
188
+ -- rule tool_call_text follows against tool_calls.rowid (v36).
185
189
  CREATE VIRTUAL TABLE IF NOT EXISTS session_text USING fts5(
186
190
  session_id UNINDEXED,
187
191
  label,
@@ -1411,6 +1415,39 @@ function migrateSchema(db, fromVersion) {
1411
1415
  if (!cols.has('generated_title_at'))
1412
1416
  db.exec(`ALTER TABLE sessions ADD COLUMN generated_title_at INTEGER`);
1413
1417
  }
1418
+ if (fromVersion < 51) {
1419
+ // v50 -> v51: session_text rows move to the rowid of the sessions row they
1420
+ // describe (PHNX-4154), the repair v36 made to tool_call_text. The only key
1421
+ // was the UNINDEXED session_id, so every delete and content read was a full
1422
+ // scan of the FTS content — 669 MB and 0.2-0.5 s per statement on a real
1423
+ // box — and the mirror ingest issues one per mirrored row (2,298 there)
1424
+ // inside a daemon tick: the event loop froze for minutes, every service
1425
+ // blew its deadline, and browser verbs timed out behind it.
1426
+ //
1427
+ // Same shape as v42: rename, recreate, copy back keyed by sessions.rowid,
1428
+ // drop — search never blacks out. OR REPLACE keeps the newest of any
1429
+ // duplicate session_id rows. A text row whose session is gone has no rowid
1430
+ // to sit at and is dropped; nothing could reach it anyway.
1431
+ db.exec(`ALTER TABLE session_text RENAME TO session_text_v50`);
1432
+ db.exec(`
1433
+ CREATE VIRTUAL TABLE session_text USING fts5(
1434
+ session_id UNINDEXED,
1435
+ label,
1436
+ topic,
1437
+ project,
1438
+ content,
1439
+ assistant,
1440
+ tokenize = 'unicode61 remove_diacritics 2'
1441
+ );
1442
+ `);
1443
+ db.exec(`
1444
+ INSERT OR REPLACE INTO session_text (rowid, session_id, label, topic, project, content, assistant)
1445
+ SELECT s.rowid, t.session_id, t.label, t.topic, t.project, t.content, t.assistant
1446
+ FROM session_text_v50 t JOIN sessions s ON s.id = t.session_id
1447
+ ORDER BY t.rowid
1448
+ `);
1449
+ db.exec(`DROP TABLE session_text_v50`);
1450
+ }
1414
1451
  }
1415
1452
  /**
1416
1453
  * Stamp `account_key` / `account_org` / `account` on every Claude row from its
@@ -2292,8 +2329,15 @@ function enrichCachedSessionMeta(meta) {
2292
2329
  return meta;
2293
2330
  }
2294
2331
  }
2295
- const deleteTextStmt = (db) => db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
2296
- const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (session_id, label, topic, project, content, assistant) VALUES (?, ?, ?, ?, ?, ?)`);
2332
+ /**
2333
+ * The one way to address a session's FTS row: its rowid is the sessions rowid
2334
+ * (schema comment on session_text). A `session_id = ?` predicate scans the whole
2335
+ * index instead — the PHNX-4154 daemon freeze.
2336
+ */
2337
+ const SESSION_TEXT_ROWID = `(SELECT rowid FROM sessions WHERE id = ?)`;
2338
+ const deleteTextStmt = (db) => db.prepare(`DELETE FROM session_text WHERE rowid = ${SESSION_TEXT_ROWID}`);
2339
+ // Binds the session id twice: once for the rowid lookup, once for the column.
2340
+ const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (rowid, session_id, label, topic, project, content, assistant) VALUES (${SESSION_TEXT_ROWID}, ?, ?, ?, ?, ?, ?)`);
2297
2341
  // Read back the label the upsert actually stored (which may be the preserved
2298
2342
  // one, not the incoming blank) so the FTS label column stays consistent with
2299
2343
  // sessions.label after a bare rescan.
@@ -2406,7 +2450,7 @@ export function upsertSession(meta, content, scan, assistantContent = '') {
2406
2450
  const txn = db.transaction(() => {
2407
2451
  upsert.run(row);
2408
2452
  delText.run(meta.id);
2409
- insText.run(meta.id,
2453
+ insText.run(meta.id, meta.id,
2410
2454
  // Use the label the upsert actually stored (preserve-non-empty rule),
2411
2455
  // not the raw incoming one, so FTS label ranking survives a bare rescan.
2412
2456
  storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '', assistantContent ?? '');
@@ -2427,8 +2471,10 @@ function reconcileCodexFileOwners(db, metas) {
2427
2471
  for (const row of rows) {
2428
2472
  if (owners.get(row.file_path) === row.id)
2429
2473
  continue;
2474
+ // The text row is keyed by the sessions rowid, so it goes before the row.
2475
+ db.prepare(`DELETE FROM session_text WHERE rowid = ${SESSION_TEXT_ROWID}`).run(row.id);
2430
2476
  db.prepare('DELETE FROM sessions WHERE id = ?').run(row.id);
2431
- for (const table of ['session_text', 'session_preview_cache', 'session_summaries', 'session_insights', 'session_topics', 'session_phenotypes', 'session_resource_usage', 'resource_scan_ledger']) {
2477
+ for (const table of ['session_preview_cache', 'session_summaries', 'session_insights', 'session_topics', 'session_phenotypes', 'session_resource_usage', 'resource_scan_ledger']) {
2432
2478
  db.prepare(`DELETE FROM ${table} WHERE session_id = ?`).run(row.id);
2433
2479
  }
2434
2480
  // The tool ledger has a unique file path too; release the wrong binding
@@ -2671,7 +2717,7 @@ export function upsertSessionsBatch(entries) {
2671
2717
  };
2672
2718
  upsert.run(row);
2673
2719
  delText.run(meta.id);
2674
- insText.run(meta.id,
2720
+ insText.run(meta.id, meta.id,
2675
2721
  // Mirror upsertSession: index the label the upsert actually stored
2676
2722
  // (preserve-non-empty rule), not the raw incoming one.
2677
2723
  storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '', assistantContent ?? '');
@@ -2746,7 +2792,7 @@ export function syncLabels(labelMap) {
2746
2792
  if (updates.length === 0)
2747
2793
  return 0;
2748
2794
  const updSessions = db.prepare(`UPDATE sessions SET label = ? WHERE id = ?`);
2749
- const updFts = db.prepare(`UPDATE session_text SET label = ? WHERE session_id = ?`);
2795
+ const updFts = db.prepare(`UPDATE session_text SET label = ? WHERE rowid = ${SESSION_TEXT_ROWID}`);
2750
2796
  const txn = db.transaction((items) => {
2751
2797
  for (const { id, label } of items) {
2752
2798
  updSessions.run(label, id);
@@ -2797,7 +2843,7 @@ export function seedLabelsFromNames(nameMap) {
2797
2843
  if (updates.length === 0)
2798
2844
  return 0;
2799
2845
  const updSessions = db.prepare(`UPDATE sessions SET label = ? WHERE id = ?`);
2800
- const updFts = db.prepare(`UPDATE session_text SET label = ? WHERE session_id = ?`);
2846
+ const updFts = db.prepare(`UPDATE session_text SET label = ? WHERE rowid = ${SESSION_TEXT_ROWID}`);
2801
2847
  const txn = db.transaction((items) => {
2802
2848
  for (const { id, label } of items) {
2803
2849
  updSessions.run(label, id);
@@ -2839,7 +2885,7 @@ export function syncTopics(topicMap) {
2839
2885
  if (updates.length === 0)
2840
2886
  return 0;
2841
2887
  const updSessions = db.prepare(`UPDATE sessions SET topic = ? WHERE id = ?`);
2842
- const updFts = db.prepare(`UPDATE session_text SET topic = ? WHERE session_id = ?`);
2888
+ const updFts = db.prepare(`UPDATE session_text SET topic = ? WHERE rowid = ${SESSION_TEXT_ROWID}`);
2843
2889
  const txn = db.transaction((items) => {
2844
2890
  for (const { id, topic } of items) {
2845
2891
  updSessions.run(topic, id);
@@ -3231,7 +3277,7 @@ export function querySessions(options = {}) {
3231
3277
  const missingIds = new Set(missing.map(r => r.id));
3232
3278
  const phantomIds = new Set();
3233
3279
  if (missing.length > 0) {
3234
- const readContent = db.prepare(`SELECT content FROM session_text WHERE session_id = ?`);
3280
+ const readContent = db.prepare(`SELECT content FROM session_text WHERE rowid = ${SESSION_TEXT_ROWID}`);
3235
3281
  const markArchived = db.prepare(`UPDATE sessions SET archived_at = ? WHERE id = ? AND archived_at IS NULL`);
3236
3282
  const now = Date.now();
3237
3283
  const classify = db.transaction(() => {
@@ -3706,7 +3752,7 @@ export function writeRemotePreviewCacheFailure(device, sessionId, reason, backof
3706
3752
  * when no row exists; an empty string when a row exists but carried no content.
3707
3753
  */
3708
3754
  export function readSessionContent(id) {
3709
- const row = getDB().prepare(`SELECT content FROM session_text WHERE session_id = ?`).get(id);
3755
+ const row = getDB().prepare(`SELECT content FROM session_text WHERE rowid = ${SESSION_TEXT_ROWID}`).get(id);
3710
3756
  return row?.content;
3711
3757
  }
3712
3758
  /**
@@ -3964,11 +4010,11 @@ export function upsertMirrorSession(row, source, syncedAt) {
3964
4010
  if (result.changes === 0)
3965
4011
  return false;
3966
4012
  // Keep the mirror row searchable by topic + first user turn, like a local row.
3967
- db.prepare(`DELETE FROM session_text WHERE session_id = ?`).run(row.id);
4013
+ db.prepare(`DELETE FROM session_text WHERE rowid = ${SESSION_TEXT_ROWID}`).run(row.id);
3968
4014
  db.prepare(`
3969
- INSERT INTO session_text (session_id, label, topic, project, content, assistant)
3970
- VALUES (?, ?, ?, '', ?, '')
3971
- `).run(row.id, row.label ?? '', row.topic ?? '', row.firstUser ?? '');
4015
+ INSERT INTO session_text (rowid, session_id, label, topic, project, content, assistant)
4016
+ VALUES (${SESSION_TEXT_ROWID}, ?, ?, ?, '', ?, '')
4017
+ `).run(row.id, row.id, row.label ?? '', row.topic ?? '', row.firstUser ?? '');
3972
4018
  // Carry the peer's daemon-computed summary into the same session_summaries
3973
4019
  // cache the local merge reads (PHNX-3939), so a peer session renders its
3974
4020
  // goal/checkpoints/checklist inline with no transcript. Stamp is null: the
@@ -4072,14 +4118,15 @@ export function pruneMirrorSessions(cutoffMs) {
4072
4118
  if (stale.length === 0)
4073
4119
  return 0;
4074
4120
  const delRow = db.prepare(`DELETE FROM sessions WHERE id = ?`);
4075
- const delText = db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
4121
+ const delText = db.prepare(`DELETE FROM session_text WHERE rowid = ${SESSION_TEXT_ROWID}`);
4076
4122
  const delPreview = db.prepare(`DELETE FROM session_preview_cache WHERE session_id = ?`);
4077
4123
  const delSummary = db.prepare(`DELETE FROM session_summaries WHERE session_id = ?`);
4078
4124
  const delTimeline = db.prepare(`DELETE FROM session_timelines WHERE session_id = ?`);
4079
4125
  const txn = db.transaction(() => {
4080
4126
  for (const { id } of stale) {
4081
- delRow.run(id);
4127
+ // Text first: its rowid lookup needs the sessions row still present.
4082
4128
  delText.run(id);
4129
+ delRow.run(id);
4083
4130
  delPreview.run(id);
4084
4131
  delSummary.run(id);
4085
4132
  delTimeline.run(id);
@@ -4461,7 +4508,7 @@ export function topSessionsByCost(n, options = {}) {
4461
4508
  // still excluded. archived_at is the persisted signal; stamp it here too (once)
4462
4509
  // so a session first surfaced through the cost rollup reports `archived`
4463
4510
  // consistently with the listing path.
4464
- const readContent = db.prepare(`SELECT content FROM session_text WHERE session_id = ?`);
4511
+ const readContent = db.prepare(`SELECT content FROM session_text WHERE rowid = ${SESSION_TEXT_ROWID}`);
4465
4512
  const markArchived = db.prepare(`UPDATE sessions SET archived_at = ? WHERE id = ? AND archived_at IS NULL`);
4466
4513
  const now = Date.now();
4467
4514
  const toStamp = [];
@@ -425,7 +425,6 @@ function dispatchAgentScan(agent, onProgress) {
425
425
  switch (agent) {
426
426
  case 'claude': return scanClaudeIncremental(onProgress);
427
427
  case 'codex': return scanCodexIncremental(onProgress);
428
- case 'gemini': return scanGeminiIncremental(onProgress);
429
428
  case 'antigravity': return scanAntigravityIncremental(onProgress);
430
429
  case 'opencode': return scanOpenCodeIncremental(onProgress);
431
430
  case 'openclaw': return scanOpenClawIncremental(onProgress);
@@ -948,7 +947,6 @@ export function getAgentSessionDirs(agent, subdir) {
948
947
  const SESSION_ROOT_SPECS = [
949
948
  { agent: 'claude', subdir: 'projects' },
950
949
  { agent: 'codex', subdir: 'sessions' },
951
- { agent: 'gemini', subdir: 'tmp' },
952
950
  { agent: 'antigravity', subdir: 'conversations' },
953
951
  { agent: 'droid', subdir: 'sessions' },
954
952
  { agent: 'kimi', subdir: 'sessions' },
@@ -1059,7 +1057,7 @@ async function scanRoutineArchivesIncremental(agent, onProgress) {
1059
1057
  const subdir = sessionRootSubdir(agent);
1060
1058
  if (!subdir)
1061
1059
  return;
1062
- const ext = agent === 'gemini' ? '.json' : '.jsonl';
1060
+ const ext = '.jsonl';
1063
1061
  const prestat = [];
1064
1062
  for (const sessionsDir of getRoutineArchiveSessionDirs(agent, subdir)) {
1065
1063
  for (const f of walkForFilesWithStat(sessionsDir, ext, 100_000)) {
@@ -1723,254 +1721,6 @@ function pickLatestCodexTimestamp(metaTimestamp, filePath) {
1723
1721
  return candidates.reduce((best, cur) => (cur > best ? cur : best));
1724
1722
  }
1725
1723
  // ---------------------------------------------------------------------------
1726
- // Gemini
1727
- // ---------------------------------------------------------------------------
1728
- /** Incrementally re-scan changed Gemini session files and upsert into the DB. */
1729
- async function scanGeminiIncremental(onProgress) {
1730
- const currentVersion = await getCurrentAgentVersion('gemini');
1731
- const projectMap = buildGeminiProjectMap();
1732
- // Each `<tmpDir>/<hashDir>/chats` is a leaf dir of Gemini transcripts. The
1733
- // FIRST tmp root is the live `~/.gemini/tmp` — its chats dirs are live roots;
1734
- // version-home + backup roots are immutable and short-circuit when unchanged.
1735
- const tmpRoots = getAgentSessionDirs('gemini', 'tmp');
1736
- const leafDirs = [];
1737
- const seenLeaf = new Set();
1738
- tmpRoots.forEach((tmpDir, rootIdx) => {
1739
- const isLiveRoot = rootIdx === 0;
1740
- let hashDirs;
1741
- try {
1742
- hashDirs = fs.readdirSync(tmpDir);
1743
- }
1744
- catch {
1745
- return;
1746
- }
1747
- for (const hashDir of hashDirs) {
1748
- const chatsDir = path.join(tmpDir, hashDir, 'chats');
1749
- if (!fs.existsSync(chatsDir))
1750
- continue;
1751
- const key = safeRealpathSync(chatsDir) || chatsDir;
1752
- if (seenLeaf.has(key))
1753
- continue;
1754
- seenLeaf.add(key);
1755
- leafDirs.push({ dirPath: chatsDir, isLiveRoot });
1756
- }
1757
- });
1758
- const { changed } = collectChangedFilesInLeafDirs(leafDirs, '.json');
1759
- const changedByPath = new Map(changed.map(c => [c.filePath, c.scan]));
1760
- if (changedByPath.size === 0)
1761
- return;
1762
- onProgress?.({ agent: 'gemini', parsed: 0, total: changedByPath.size });
1763
- const entries = [];
1764
- const touched = [];
1765
- const seen = new Set();
1766
- let parsed = 0;
1767
- for (const { filePath, scan } of changed) {
1768
- // The hashDir is the directory two levels up: <hashDir>/chats/<file>.json.
1769
- const hashDir = path.basename(path.dirname(path.dirname(filePath)));
1770
- try {
1771
- const result = readGeminiMeta(filePath, hashDir, projectMap, currentVersion);
1772
- if (result && !seen.has(result.meta.id)) {
1773
- seen.add(result.meta.id);
1774
- entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
1775
- }
1776
- else {
1777
- // Gemini file without a sessionId — record scan so we don't re-parse it next run.
1778
- touched.push({ filePath, scan });
1779
- }
1780
- }
1781
- catch {
1782
- touched.push({ filePath, scan });
1783
- }
1784
- parsed++;
1785
- onProgress?.({ agent: 'gemini', parsed, total: changedByPath.size });
1786
- }
1787
- upsertSessionsBatch(entries);
1788
- recordScans(touched);
1789
- }
1790
- /** Parse a single Gemini JSON session file to extract session metadata. */
1791
- function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
1792
- let session;
1793
- try {
1794
- session = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1795
- }
1796
- catch {
1797
- return null;
1798
- }
1799
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId : '';
1800
- const startTime = typeof session.startTime === 'string' ? session.startTime : '';
1801
- const projectHash = typeof session.projectHash === 'string' ? session.projectHash : '';
1802
- const embeddedVersion = typeof session.version === 'string'
1803
- ? session.version
1804
- : typeof session.cliVersion === 'string'
1805
- ? session.cliVersion
1806
- : undefined;
1807
- if (!sessionId)
1808
- return null;
1809
- const projectInfo = projectMap.get(projectHash || hashDir);
1810
- const project = projectInfo?.name || hashDir.slice(0, 12);
1811
- const cwd = projectInfo?.path ? normalizeCwd(projectInfo.path) : undefined;
1812
- const stat = safeStatSync(filePath);
1813
- const messages = Array.isArray(session.messages) ? session.messages : [];
1814
- const sessionModel = typeof session.model === 'string' ? session.model : undefined;
1815
- let topic;
1816
- let messageCount = 0;
1817
- let tokenCount = 0;
1818
- let outputTokens = 0;
1819
- let inputTokens = 0;
1820
- let cacheReadTokens = 0;
1821
- let sawTokenCount = false;
1822
- let costUsd = 0;
1823
- let costUsdNoCache = 0;
1824
- let sawCost = false;
1825
- let firstTsMs;
1826
- let lastTsMs;
1827
- const userTexts = [];
1828
- const assistantTexts = [];
1829
- for (const message of messages) {
1830
- if (message.type === 'user') {
1831
- const text = extractGeminiMessageText(message.content);
1832
- if (text) {
1833
- messageCount++;
1834
- userTexts.push(text);
1835
- if (!topic)
1836
- topic = extractSessionTopic(text);
1837
- }
1838
- }
1839
- else if (message.type === 'gemini') {
1840
- const text = extractGeminiMessageText(message.content);
1841
- if (text) {
1842
- messageCount++;
1843
- assistantTexts.push(text);
1844
- }
1845
- }
1846
- // Duration: messages carry a `timestamp` on most Gemini CLI versions.
1847
- const tsRaw = message.timestamp ?? message.time;
1848
- if (typeof tsRaw === 'string' || typeof tsRaw === 'number') {
1849
- const ms = new Date(tsRaw).getTime();
1850
- if (!Number.isNaN(ms)) {
1851
- if (firstTsMs === undefined || ms < firstTsMs)
1852
- firstTsMs = ms;
1853
- if (lastTsMs === undefined || ms > lastTsMs)
1854
- lastTsMs = ms;
1855
- }
1856
- }
1857
- const total = getGeminiTokenCount(message.tokens);
1858
- if (total !== null) {
1859
- tokenCount += total;
1860
- sawTokenCount = true;
1861
- }
1862
- // Output tokens: sum directional generation fields per message (output +
1863
- // thoughts + tool), mirroring the cost path — never `tokens.total`, which
1864
- // may be cumulative and would double-count when summed.
1865
- const gtk = message.tokens;
1866
- if (gtk && typeof gtk === 'object') {
1867
- outputTokens +=
1868
- (typeof gtk.output === 'number' ? gtk.output : 0) +
1869
- (typeof gtk.thoughts === 'number' ? gtk.thoughts : 0) +
1870
- (typeof gtk.tool === 'number' ? gtk.tool : 0);
1871
- // Burn split (RUSH-2287): Gemini has uncached input + cache-read, no cache-write.
1872
- if (typeof gtk.input === 'number')
1873
- inputTokens += gtk.input;
1874
- if (typeof gtk.cached === 'number')
1875
- cacheReadTokens += gtk.cached;
1876
- }
1877
- // Per-message cost: directional tokens × this message's model price.
1878
- const msgModel = (typeof message.model === 'string' ? message.model : undefined) || sessionModel;
1879
- const tk = message.tokens;
1880
- if (msgModel && tk && typeof tk === 'object') {
1881
- const usage = {
1882
- model: msgModel,
1883
- inputTokens: typeof tk.input === 'number' ? tk.input : undefined,
1884
- outputTokens: (typeof tk.output === 'number' ? tk.output : 0) +
1885
- (typeof tk.thoughts === 'number' ? tk.thoughts : 0) +
1886
- (typeof tk.tool === 'number' ? tk.tool : 0),
1887
- cacheReadTokens: typeof tk.cached === 'number' ? tk.cached : undefined,
1888
- };
1889
- const c = costOfUsage(usage);
1890
- if (c > 0) {
1891
- costUsd += c;
1892
- costUsdNoCache += costOfUsageNoCache(usage);
1893
- sawCost = true;
1894
- }
1895
- }
1896
- }
1897
- const durationMs = firstTsMs !== undefined && lastTsMs !== undefined && lastTsMs > firstTsMs
1898
- ? lastTsMs - firstTsMs
1899
- : undefined;
1900
- const meta = {
1901
- id: sessionId,
1902
- shortId: deriveShortId(sessionId),
1903
- agent: 'gemini',
1904
- timestamp: startTime || (stat ? stat.mtime.toISOString() : new Date().toISOString()),
1905
- lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
1906
- project,
1907
- cwd,
1908
- filePath,
1909
- version: resolveSessionVersion('gemini', filePath, embeddedVersion, currentVersion),
1910
- model: sessionModel,
1911
- topic,
1912
- firstUserMessage: userTexts.map(cleanFirstUserMessage).find((text) => !!text),
1913
- messageCount,
1914
- tokenCount: sawTokenCount ? tokenCount : undefined,
1915
- outputTokens: sawTokenCount ? outputTokens : undefined,
1916
- inputTokens: sawTokenCount ? inputTokens : undefined,
1917
- cacheReadTokens: sawTokenCount ? cacheReadTokens : undefined,
1918
- costUsd: sawCost ? costUsd : undefined,
1919
- costUsdNoCache: sawCost ? costUsdNoCache : undefined,
1920
- durationMs,
1921
- };
1922
- return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
1923
- }
1924
- /** Build a hash-to-project mapping from Gemini's projects.json and history directories. */
1925
- function buildGeminiProjectMap() {
1926
- const map = new Map();
1927
- const projectsJsonPath = path.join(HOME, '.gemini', 'projects.json');
1928
- if (fs.existsSync(projectsJsonPath)) {
1929
- try {
1930
- const data = JSON.parse(fs.readFileSync(projectsJsonPath, 'utf-8'));
1931
- const projects = data.projects;
1932
- if (typeof projects === 'object' && projects !== null) {
1933
- if (Array.isArray(projects)) {
1934
- for (const p of projects) {
1935
- if (typeof p === 'string') {
1936
- const hash = sha256(p);
1937
- map.set(hash, { name: path.basename(p), path: p });
1938
- map.set(p, { name: path.basename(p), path: p });
1939
- }
1940
- }
1941
- }
1942
- else {
1943
- for (const [p, name] of Object.entries(projects)) {
1944
- const hash = sha256(p);
1945
- map.set(hash, { name: String(name), path: p });
1946
- }
1947
- }
1948
- }
1949
- }
1950
- catch { /* projects.json missing or malformed */ }
1951
- }
1952
- const historyDir = path.join(HOME, '.gemini', 'history');
1953
- if (fs.existsSync(historyDir)) {
1954
- try {
1955
- for (const name of fs.readdirSync(historyDir)) {
1956
- const rootFile = path.join(historyDir, name, '.project_root');
1957
- if (fs.existsSync(rootFile)) {
1958
- try {
1959
- const projectPath = fs.readFileSync(rootFile, 'utf-8').trim();
1960
- if (projectPath) {
1961
- const hash = sha256(projectPath);
1962
- map.set(hash, { name, path: projectPath });
1963
- }
1964
- }
1965
- catch { /* history entry unreadable */ }
1966
- }
1967
- }
1968
- }
1969
- catch { /* history entry unreadable */ }
1970
- }
1971
- return map;
1972
- }
1973
- // ---------------------------------------------------------------------------
1974
1724
  // Antigravity
1975
1725
  //
1976
1726
  // Antigravity stores one SQLite DB per conversation at
@@ -4458,38 +4208,6 @@ function getCodexTokenCount(totalTokenUsage) {
4458
4208
  totalTokenUsage.reasoning_output_tokens,
4459
4209
  ]);
4460
4210
  }
4461
- /** Extract text from a Gemini message content field (string or array of parts). */
4462
- function extractGeminiMessageText(content) {
4463
- if (typeof content === 'string')
4464
- return content.trim();
4465
- if (Array.isArray(content)) {
4466
- return content
4467
- .map((part) => {
4468
- if (typeof part === 'string')
4469
- return part;
4470
- if (typeof part?.text === 'string')
4471
- return part.text;
4472
- return '';
4473
- })
4474
- .join('\n')
4475
- .trim();
4476
- }
4477
- return '';
4478
- }
4479
- /** Extract the total token count from a Gemini message's tokens object. */
4480
- function getGeminiTokenCount(tokens) {
4481
- if (!tokens || typeof tokens !== 'object')
4482
- return null;
4483
- if (typeof tokens.total === 'number')
4484
- return tokens.total;
4485
- return sumKnownNumbers([
4486
- tokens.input,
4487
- tokens.output,
4488
- tokens.cached,
4489
- tokens.thoughts,
4490
- tokens.tool,
4491
- ]);
4492
- }
4493
4211
  /** Sum all numeric values in an array, returning null if none are valid numbers. */
4494
4212
  function sumKnownNumbers(values) {
4495
4213
  let total = 0;
@@ -5,9 +5,10 @@
5
5
  * used to render as a bare `[host/<peer>]` row with no topic, and its preview
6
6
  * pane fetched the peer's digest LIVE over SSH per row — slow, and blank when
7
7
  * the peer is asleep. This module mirrors each box's own session digests into
8
- * its conflict-free `~/.agents/devices/<device>/daemon-state.json`, which the
9
- * daemon's existing bounded Git transport (`fleet-shared-repo-sync.ts`) already
10
- * delivers fleet-wide with no operator step. The consuming device folds peer
8
+ * its own `~/.agents/devices/<device>/daemon-state.json`, which the usage-sync
9
+ * SSH exchange (`accounting/usage-sync.ts`, PHNX-4116) carries to every peer:
10
+ * a headed box's tick sends its envelope and stores each peer's reply, so both
11
+ * directions land with no operator step and nothing rides the user repo. The consuming device folds peer
11
12
  * digests into its local `sessions` index as mirror rows, so the picker/list/
12
13
  * focus read a LOCAL row instead of dialing the peer. No transcript is shipped —
13
14
  * only topic/label, the peer's daemon-generated title (PHNX-3797), a
@@ -5,9 +5,10 @@
5
5
  * used to render as a bare `[host/<peer>]` row with no topic, and its preview
6
6
  * pane fetched the peer's digest LIVE over SSH per row — slow, and blank when
7
7
  * the peer is asleep. This module mirrors each box's own session digests into
8
- * its conflict-free `~/.agents/devices/<device>/daemon-state.json`, which the
9
- * daemon's existing bounded Git transport (`fleet-shared-repo-sync.ts`) already
10
- * delivers fleet-wide with no operator step. The consuming device folds peer
8
+ * its own `~/.agents/devices/<device>/daemon-state.json`, which the usage-sync
9
+ * SSH exchange (`accounting/usage-sync.ts`, PHNX-4116) carries to every peer:
10
+ * a headed box's tick sends its envelope and stores each peer's reply, so both
11
+ * directions land with no operator step and nothing rides the user repo. The consuming device folds peer
11
12
  * digests into its local `sessions` index as mirror rows, so the picker/list/
12
13
  * focus read a LOCAL row instead of dialing the peer. No transcript is shipped —
13
14
  * only topic/label, the peer's daemon-generated title (PHNX-3797), a
@@ -100,8 +100,6 @@ export declare function parseCodexItemsContent(content: string): SessionEvent[];
100
100
  * chunk without re-reading the whole file.
101
101
  */
102
102
  export declare function parseCodexContent(content: string): SessionEvent[];
103
- /** Parse a Gemini JSON session file into normalized events. */
104
- export declare function parseGemini(filePath: string): SessionEvent[];
105
103
  /**
106
104
  * Parse an Antigravity conversation SQLite DB into normalized tool_use events.
107
105
  * Deduped by the tool-call id so each tool appears once (Antigravity writes a