@getrift/rift 0.1.0-beta.20 → 0.1.0-beta.22

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 (280) hide show
  1. package/README.md +7 -3
  2. package/dist/src/capture/auto-capture.d.ts +105 -4
  3. package/dist/src/capture/auto-capture.d.ts.map +1 -1
  4. package/dist/src/capture/auto-capture.js +313 -34
  5. package/dist/src/capture/auto-capture.js.map +1 -1
  6. package/dist/src/capture/claude-cli-triage-provider.d.ts +28 -0
  7. package/dist/src/capture/claude-cli-triage-provider.d.ts.map +1 -0
  8. package/dist/src/capture/claude-cli-triage-provider.js +88 -0
  9. package/dist/src/capture/claude-cli-triage-provider.js.map +1 -0
  10. package/dist/src/capture/codex-cli-triage-provider.d.ts.map +1 -1
  11. package/dist/src/capture/codex-cli-triage-provider.js +1 -33
  12. package/dist/src/capture/codex-cli-triage-provider.js.map +1 -1
  13. package/dist/src/capture/cursor-capture.d.ts +89 -0
  14. package/dist/src/capture/cursor-capture.d.ts.map +1 -0
  15. package/dist/src/capture/cursor-capture.js +121 -0
  16. package/dist/src/capture/cursor-capture.js.map +1 -0
  17. package/dist/src/capture/observability.d.ts +30 -0
  18. package/dist/src/capture/observability.d.ts.map +1 -1
  19. package/dist/src/capture/observability.js +29 -0
  20. package/dist/src/capture/observability.js.map +1 -1
  21. package/dist/src/capture/sources.d.ts +41 -3
  22. package/dist/src/capture/sources.d.ts.map +1 -1
  23. package/dist/src/capture/sources.js +43 -1
  24. package/dist/src/capture/sources.js.map +1 -1
  25. package/dist/src/capture/triage-classification.d.ts +69 -0
  26. package/dist/src/capture/triage-classification.d.ts.map +1 -0
  27. package/dist/src/capture/triage-classification.js +62 -0
  28. package/dist/src/capture/triage-classification.js.map +1 -0
  29. package/dist/src/capture/triage-provider-factory.d.ts +36 -0
  30. package/dist/src/capture/triage-provider-factory.d.ts.map +1 -0
  31. package/dist/src/capture/triage-provider-factory.js +55 -0
  32. package/dist/src/capture/triage-provider-factory.js.map +1 -0
  33. package/dist/src/capture/triage.d.ts +1 -1
  34. package/dist/src/capture/triage.d.ts.map +1 -1
  35. package/dist/src/capture/triage.js +8 -6
  36. package/dist/src/capture/triage.js.map +1 -1
  37. package/dist/src/cli/commands/capture.d.ts.map +1 -1
  38. package/dist/src/cli/commands/capture.js +72 -17
  39. package/dist/src/cli/commands/capture.js.map +1 -1
  40. package/dist/src/cli/commands/chunk-backfill.d.ts +13 -0
  41. package/dist/src/cli/commands/chunk-backfill.d.ts.map +1 -0
  42. package/dist/src/cli/commands/chunk-backfill.js +157 -0
  43. package/dist/src/cli/commands/chunk-backfill.js.map +1 -0
  44. package/dist/src/cli/commands/cursor-probe.d.ts +20 -0
  45. package/dist/src/cli/commands/cursor-probe.d.ts.map +1 -0
  46. package/dist/src/cli/commands/cursor-probe.js +162 -0
  47. package/dist/src/cli/commands/cursor-probe.js.map +1 -0
  48. package/dist/src/cli/commands/menubar.d.ts +50 -0
  49. package/dist/src/cli/commands/menubar.d.ts.map +1 -1
  50. package/dist/src/cli/commands/menubar.js +224 -16
  51. package/dist/src/cli/commands/menubar.js.map +1 -1
  52. package/dist/src/cli/commands/onboard.d.ts +36 -7
  53. package/dist/src/cli/commands/onboard.d.ts.map +1 -1
  54. package/dist/src/cli/commands/onboard.js +256 -53
  55. package/dist/src/cli/commands/onboard.js.map +1 -1
  56. package/dist/src/cli/commands/status.d.ts.map +1 -1
  57. package/dist/src/cli/commands/status.js +16 -0
  58. package/dist/src/cli/commands/status.js.map +1 -1
  59. package/dist/src/cli/commands/update.d.ts +34 -1
  60. package/dist/src/cli/commands/update.d.ts.map +1 -1
  61. package/dist/src/cli/commands/update.js +179 -2
  62. package/dist/src/cli/commands/update.js.map +1 -1
  63. package/dist/src/cli/index.d.ts.map +1 -1
  64. package/dist/src/cli/index.js +4 -0
  65. package/dist/src/cli/index.js.map +1 -1
  66. package/dist/src/cli/postinstall-menubar.d.ts.map +1 -1
  67. package/dist/src/cli/postinstall-menubar.js +14 -0
  68. package/dist/src/cli/postinstall-menubar.js.map +1 -1
  69. package/dist/src/cli/status/friend-header.d.ts +18 -0
  70. package/dist/src/cli/status/friend-header.d.ts.map +1 -1
  71. package/dist/src/cli/status/friend-header.js +137 -0
  72. package/dist/src/cli/status/friend-header.js.map +1 -1
  73. package/dist/src/cli/status/local-signals.d.ts +41 -0
  74. package/dist/src/cli/status/local-signals.d.ts.map +1 -1
  75. package/dist/src/cli/status/local-signals.js +48 -0
  76. package/dist/src/cli/status/local-signals.js.map +1 -1
  77. package/dist/src/config/schema.d.ts +220 -14
  78. package/dist/src/config/schema.d.ts.map +1 -1
  79. package/dist/src/config/schema.js +82 -7
  80. package/dist/src/config/schema.js.map +1 -1
  81. package/dist/src/diagnostics/claude-preflight.d.ts +34 -0
  82. package/dist/src/diagnostics/claude-preflight.d.ts.map +1 -0
  83. package/dist/src/diagnostics/claude-preflight.js +89 -0
  84. package/dist/src/diagnostics/claude-preflight.js.map +1 -0
  85. package/dist/src/diagnostics/codex-preflight.d.ts +1 -1
  86. package/dist/src/diagnostics/codex-preflight.d.ts.map +1 -1
  87. package/dist/src/diagnostics/codex-preflight.js +14 -0
  88. package/dist/src/diagnostics/codex-preflight.js.map +1 -1
  89. package/dist/src/diagnostics/doctor.d.ts +9 -1
  90. package/dist/src/diagnostics/doctor.d.ts.map +1 -1
  91. package/dist/src/diagnostics/doctor.js +57 -2
  92. package/dist/src/diagnostics/doctor.js.map +1 -1
  93. package/dist/src/ingestion/chunk-meta.d.ts +85 -0
  94. package/dist/src/ingestion/chunk-meta.d.ts.map +1 -0
  95. package/dist/src/ingestion/chunk-meta.js +167 -0
  96. package/dist/src/ingestion/chunk-meta.js.map +1 -0
  97. package/dist/src/ingestion/chunk-text.d.ts +39 -0
  98. package/dist/src/ingestion/chunk-text.d.ts.map +1 -0
  99. package/dist/src/ingestion/chunk-text.js +114 -0
  100. package/dist/src/ingestion/chunk-text.js.map +1 -0
  101. package/dist/src/ingestion/cursor/cursor-store.d.ts +177 -0
  102. package/dist/src/ingestion/cursor/cursor-store.d.ts.map +1 -0
  103. package/dist/src/ingestion/cursor/cursor-store.js +243 -0
  104. package/dist/src/ingestion/cursor/cursor-store.js.map +1 -0
  105. package/dist/src/ingestion/cursor/enrich-roots.d.ts +16 -0
  106. package/dist/src/ingestion/cursor/enrich-roots.d.ts.map +1 -0
  107. package/dist/src/ingestion/cursor/enrich-roots.js +22 -0
  108. package/dist/src/ingestion/cursor/enrich-roots.js.map +1 -0
  109. package/dist/src/ingestion/cursor/vscdb-reader.d.ts +32 -0
  110. package/dist/src/ingestion/cursor/vscdb-reader.d.ts.map +1 -0
  111. package/dist/src/ingestion/cursor/vscdb-reader.js +113 -0
  112. package/dist/src/ingestion/cursor/vscdb-reader.js.map +1 -0
  113. package/dist/src/ingestion/cursor/workspace-root.d.ts +96 -0
  114. package/dist/src/ingestion/cursor/workspace-root.d.ts.map +1 -0
  115. package/dist/src/ingestion/cursor/workspace-root.js +187 -0
  116. package/dist/src/ingestion/cursor/workspace-root.js.map +1 -0
  117. package/dist/src/ingestion/indexer.d.ts.map +1 -1
  118. package/dist/src/ingestion/indexer.js +41 -32
  119. package/dist/src/ingestion/indexer.js.map +1 -1
  120. package/dist/src/jobs/handlers/compact.d.ts.map +1 -1
  121. package/dist/src/jobs/handlers/compact.js +9 -4
  122. package/dist/src/jobs/handlers/compact.js.map +1 -1
  123. package/dist/src/jobs/handlers/ingest.d.ts.map +1 -1
  124. package/dist/src/jobs/handlers/ingest.js +60 -30
  125. package/dist/src/jobs/handlers/ingest.js.map +1 -1
  126. package/dist/src/jobs/handlers/reconcile.d.ts.map +1 -1
  127. package/dist/src/jobs/handlers/reconcile.js +128 -45
  128. package/dist/src/jobs/handlers/reconcile.js.map +1 -1
  129. package/dist/src/jobs/handlers/save.d.ts.map +1 -1
  130. package/dist/src/jobs/handlers/save.js +122 -72
  131. package/dist/src/jobs/handlers/save.js.map +1 -1
  132. package/dist/src/jobs/types.d.ts +1 -1
  133. package/dist/src/main.js +26 -15
  134. package/dist/src/main.js.map +1 -1
  135. package/dist/src/mcp/server.d.ts.map +1 -1
  136. package/dist/src/mcp/server.js +10 -3
  137. package/dist/src/mcp/server.js.map +1 -1
  138. package/dist/src/mcp/tools/context-pack.d.ts.map +1 -1
  139. package/dist/src/mcp/tools/context-pack.js +7 -1
  140. package/dist/src/mcp/tools/context-pack.js.map +1 -1
  141. package/dist/src/mcp/tools/conversations-search.d.ts +1 -1
  142. package/dist/src/mcp/tools/conversations-search.d.ts.map +1 -1
  143. package/dist/src/mcp/tools/conversations-search.js +7 -1
  144. package/dist/src/mcp/tools/conversations-search.js.map +1 -1
  145. package/dist/src/mcp/tools/evidence-feedback.d.ts +60 -0
  146. package/dist/src/mcp/tools/evidence-feedback.d.ts.map +1 -0
  147. package/dist/src/mcp/tools/evidence-feedback.js +62 -0
  148. package/dist/src/mcp/tools/evidence-feedback.js.map +1 -0
  149. package/dist/src/mcp/tools/log-outcome.d.ts +72 -0
  150. package/dist/src/mcp/tools/log-outcome.d.ts.map +1 -0
  151. package/dist/src/mcp/tools/log-outcome.js +59 -0
  152. package/dist/src/mcp/tools/log-outcome.js.map +1 -0
  153. package/dist/src/mcp/tools/open-evidence.d.ts +37 -0
  154. package/dist/src/mcp/tools/open-evidence.d.ts.map +1 -0
  155. package/dist/src/mcp/tools/open-evidence.js +72 -0
  156. package/dist/src/mcp/tools/open-evidence.js.map +1 -0
  157. package/dist/src/mcp/tools/save.d.ts +7 -2
  158. package/dist/src/mcp/tools/save.d.ts.map +1 -1
  159. package/dist/src/mcp/tools/save.js +7 -2
  160. package/dist/src/mcp/tools/save.js.map +1 -1
  161. package/dist/src/mcp/tools/search.d.ts.map +1 -1
  162. package/dist/src/mcp/tools/search.js +7 -1
  163. package/dist/src/mcp/tools/search.js.map +1 -1
  164. package/dist/src/observability/retrieval-feedback.d.ts +82 -0
  165. package/dist/src/observability/retrieval-feedback.d.ts.map +1 -0
  166. package/dist/src/observability/retrieval-feedback.js +231 -0
  167. package/dist/src/observability/retrieval-feedback.js.map +1 -0
  168. package/dist/src/observability/rift-context.d.ts.map +1 -1
  169. package/dist/src/observability/rift-context.js +3 -0
  170. package/dist/src/observability/rift-context.js.map +1 -1
  171. package/dist/src/observability/tool-usage-stats.d.ts +13 -0
  172. package/dist/src/observability/tool-usage-stats.d.ts.map +1 -1
  173. package/dist/src/observability/tool-usage-stats.js +15 -0
  174. package/dist/src/observability/tool-usage-stats.js.map +1 -1
  175. package/dist/src/observability/tool-usage.d.ts +56 -0
  176. package/dist/src/observability/tool-usage.d.ts.map +1 -1
  177. package/dist/src/observability/tool-usage.js +86 -0
  178. package/dist/src/observability/tool-usage.js.map +1 -1
  179. package/dist/src/providers/claude-cli-metadata-extraction.d.ts +47 -0
  180. package/dist/src/providers/claude-cli-metadata-extraction.d.ts.map +1 -0
  181. package/dist/src/providers/claude-cli-metadata-extraction.js +120 -0
  182. package/dist/src/providers/claude-cli-metadata-extraction.js.map +1 -0
  183. package/dist/src/providers/claude-cli-runner.d.ts +92 -0
  184. package/dist/src/providers/claude-cli-runner.d.ts.map +1 -0
  185. package/dist/src/providers/claude-cli-runner.js +598 -0
  186. package/dist/src/providers/claude-cli-runner.js.map +1 -0
  187. package/dist/src/providers/codex-cli-metadata-extraction.d.ts.map +1 -1
  188. package/dist/src/providers/codex-cli-metadata-extraction.js +1 -40
  189. package/dist/src/providers/codex-cli-metadata-extraction.js.map +1 -1
  190. package/dist/src/providers/codex-cli-runner.d.ts +7 -0
  191. package/dist/src/providers/codex-cli-runner.d.ts.map +1 -1
  192. package/dist/src/providers/codex-cli-runner.js +131 -5
  193. package/dist/src/providers/codex-cli-runner.js.map +1 -1
  194. package/dist/src/providers/conversation-generation.d.ts +10 -0
  195. package/dist/src/providers/conversation-generation.d.ts.map +1 -1
  196. package/dist/src/providers/conversation-generation.js +54 -13
  197. package/dist/src/providers/conversation-generation.js.map +1 -1
  198. package/dist/src/providers/openai-metadata-extraction.d.ts +48 -1
  199. package/dist/src/providers/openai-metadata-extraction.d.ts.map +1 -1
  200. package/dist/src/providers/openai-metadata-extraction.js +51 -2
  201. package/dist/src/providers/openai-metadata-extraction.js.map +1 -1
  202. package/dist/src/providers/types.d.ts +1 -1
  203. package/dist/src/providers/types.d.ts.map +1 -1
  204. package/dist/src/providers/types.js +4 -0
  205. package/dist/src/providers/types.js.map +1 -1
  206. package/dist/src/retrieval/compact.d.ts +81 -0
  207. package/dist/src/retrieval/compact.d.ts.map +1 -1
  208. package/dist/src/retrieval/compact.js +248 -8
  209. package/dist/src/retrieval/compact.js.map +1 -1
  210. package/dist/src/retrieval/context-pack.d.ts.map +1 -1
  211. package/dist/src/retrieval/context-pack.js +28 -14
  212. package/dist/src/retrieval/context-pack.js.map +1 -1
  213. package/dist/src/retrieval/evidence-key.d.ts +48 -0
  214. package/dist/src/retrieval/evidence-key.d.ts.map +1 -0
  215. package/dist/src/retrieval/evidence-key.js +131 -0
  216. package/dist/src/retrieval/evidence-key.js.map +1 -0
  217. package/dist/src/retrieval/group-by-parent.d.ts +38 -0
  218. package/dist/src/retrieval/group-by-parent.d.ts.map +1 -0
  219. package/dist/src/retrieval/group-by-parent.js +40 -0
  220. package/dist/src/retrieval/group-by-parent.js.map +1 -0
  221. package/dist/src/retrieval/lexical.d.ts.map +1 -1
  222. package/dist/src/retrieval/lexical.js +1 -3
  223. package/dist/src/retrieval/lexical.js.map +1 -1
  224. package/dist/src/retrieval/receipt.d.ts +57 -0
  225. package/dist/src/retrieval/receipt.d.ts.map +1 -0
  226. package/dist/src/retrieval/receipt.js +119 -0
  227. package/dist/src/retrieval/receipt.js.map +1 -0
  228. package/dist/src/retrieval/reranker.d.ts +12 -2
  229. package/dist/src/retrieval/reranker.d.ts.map +1 -1
  230. package/dist/src/retrieval/reranker.js +11 -4
  231. package/dist/src/retrieval/reranker.js.map +1 -1
  232. package/dist/src/retrieval/stitch-chunks.d.ts +73 -0
  233. package/dist/src/retrieval/stitch-chunks.d.ts.map +1 -0
  234. package/dist/src/retrieval/stitch-chunks.js +106 -0
  235. package/dist/src/retrieval/stitch-chunks.js.map +1 -0
  236. package/dist/src/server/app.d.ts.map +1 -1
  237. package/dist/src/server/app.js +17 -1
  238. package/dist/src/server/app.js.map +1 -1
  239. package/dist/src/server/routes/conversations-search.d.ts.map +1 -1
  240. package/dist/src/server/routes/conversations-search.js +12 -3
  241. package/dist/src/server/routes/conversations-search.js.map +1 -1
  242. package/dist/src/server/routes/friend-status.d.ts +44 -5
  243. package/dist/src/server/routes/friend-status.d.ts.map +1 -1
  244. package/dist/src/server/routes/friend-status.js +74 -6
  245. package/dist/src/server/routes/friend-status.js.map +1 -1
  246. package/dist/src/server/routes/mcp-usage.d.ts +9 -6
  247. package/dist/src/server/routes/mcp-usage.d.ts.map +1 -1
  248. package/dist/src/server/routes/mcp-usage.js.map +1 -1
  249. package/dist/src/server/routes/retrieval-feedback.d.ts +3 -0
  250. package/dist/src/server/routes/retrieval-feedback.d.ts.map +1 -0
  251. package/dist/src/server/routes/retrieval-feedback.js +290 -0
  252. package/dist/src/server/routes/retrieval-feedback.js.map +1 -0
  253. package/dist/src/server/routes/save.d.ts +3 -3
  254. package/dist/src/server/routes/save.d.ts.map +1 -1
  255. package/dist/src/server/routes/save.js +6 -2
  256. package/dist/src/server/routes/save.js.map +1 -1
  257. package/dist/src/server/routes/search.d.ts.map +1 -1
  258. package/dist/src/server/routes/search.js +19 -7
  259. package/dist/src/server/routes/search.js.map +1 -1
  260. package/dist/src/server/serving-marker.d.ts +85 -0
  261. package/dist/src/server/serving-marker.d.ts.map +1 -0
  262. package/dist/src/server/serving-marker.js +226 -0
  263. package/dist/src/server/serving-marker.js.map +1 -0
  264. package/dist/src/storage/chunk-backfill.d.ts +39 -0
  265. package/dist/src/storage/chunk-backfill.d.ts.map +1 -0
  266. package/dist/src/storage/chunk-backfill.js +295 -0
  267. package/dist/src/storage/chunk-backfill.js.map +1 -0
  268. package/dist/src/storage/filter.d.ts +42 -0
  269. package/dist/src/storage/filter.d.ts.map +1 -0
  270. package/dist/src/storage/filter.js +70 -0
  271. package/dist/src/storage/filter.js.map +1 -0
  272. package/dist/src/storage/rebuild.d.ts.map +1 -1
  273. package/dist/src/storage/rebuild.js +44 -27
  274. package/dist/src/storage/rebuild.js.map +1 -1
  275. package/dist/src/storage/tables.d.ts +41 -0
  276. package/dist/src/storage/tables.d.ts.map +1 -1
  277. package/dist/src/storage/tables.js +64 -1
  278. package/dist/src/storage/tables.js.map +1 -1
  279. package/operator/swiftbar/render-menu.py +57 -15
  280. package/package.json +5 -3
@@ -21,12 +21,16 @@ import os from "node:os";
21
21
  import { z } from "zod";
22
22
  import { discoverClaudeCodeSessions, parseClaudeCodeSession, } from "../ingestion/parsers/claude-code-jsonl.js";
23
23
  import { discoverCodexSessions, parseCodexSession, } from "../ingestion/parsers/codex-jsonl.js";
24
+ import { parseCursorComposerSession } from "../ingestion/cursor/cursor-store.js";
25
+ import { defaultCursorDir } from "../ingestion/cursor/vscdb-reader.js";
26
+ import { cursorCaptureEnabled, cursorCapturePersistEnabled, cursorTitlesRevealed, discoverCursorCaptureSessions, redactedCursorTitle, } from "./cursor-capture.js";
24
27
  import { triageConversation } from "./triage.js";
25
28
  import { addToReview } from "./review-queue.js";
26
29
  import { atomicWrite } from "../storage/atomic.js";
27
- import { CodexCliTriageProvider } from "./codex-cli-triage-provider.js";
30
+ import { createConfiguredTriageProvider, getConfiguredTriageProviderName, } from "./triage-provider-factory.js";
28
31
  import { CodexCliTimeoutError } from "../providers/codex-cli-runner.js";
29
- import { DEFAULT_AUTO_CAPTURE_SOURCES, } from "./sources.js";
32
+ import { ClaudeCliTimeoutError } from "../providers/claude-cli-runner.js";
33
+ import { AUTO_CAPTURE_SOURCE_VALUES, CURSOR_CAPTURE_SOURCE, DEFAULT_AUTO_CAPTURE_SOURCES, } from "./sources.js";
30
34
  /**
31
35
  * Default upper bound for a Codex CLI session that auto-capture will hand
32
36
  * to the triage subprocess. Real sessions in `~/.codex/sessions/` are tens
@@ -36,6 +40,16 @@ import { DEFAULT_AUTO_CAPTURE_SOURCES, } from "./sources.js";
36
40
  * Operators raise this in `config.json` under `capture.codex_cli.max_session_bytes`.
37
41
  */
38
42
  export const DEFAULT_CODEX_MAX_SESSION_BYTES = 8 * 1024 * 1024;
43
+ /**
44
+ * Default upper bound for a native Cursor session's PARSED transcript before the
45
+ * size guard quarantines it instead of handing it to the triage subprocess.
46
+ * Unlike Codex (a session file we can `stat`), Cursor content is reassembled
47
+ * from `state.vscdb` bubbles in memory, so the bound is measured on the merged
48
+ * user+assistant text. 4 MB of text is ~1M tokens — far past any healthy
49
+ * session and well past what triage should ever receive. Overridable per run
50
+ * via `AutoCaptureDeps.cursorMaxSessionBytes`.
51
+ */
52
+ export const DEFAULT_CURSOR_MAX_SESSION_BYTES = 4 * 1024 * 1024;
39
53
  /** Sentinel idempotency_key written into capture-state.json for oversized sessions. */
40
54
  export const QUARANTINED_OVERSIZED_KEY = "quarantined:oversized";
41
55
  /**
@@ -54,6 +68,16 @@ export function resolveCodexCaptureDeps(config) {
54
68
  const override = config.capture?.codex_cli?.max_session_bytes;
55
69
  return override !== undefined ? { codexMaxSessionBytes: override } : {};
56
70
  }
71
+ /**
72
+ * Resolve the native-Cursor size override into the partial deps every call site
73
+ * (daemon scheduled run, `rift capture`) spreads into `runAutoCapture()`. Mirrors
74
+ * `resolveCodexCaptureDeps`. Returns an empty object (no-op spread) when absent,
75
+ * so `runAutoCapture` falls through to `DEFAULT_CURSOR_MAX_SESSION_BYTES`.
76
+ */
77
+ export function resolveCursorCaptureDeps(config) {
78
+ const override = config.capture?.cursor?.max_session_bytes;
79
+ return override !== undefined ? { cursorMaxSessionBytes: override } : {};
80
+ }
57
81
  /**
58
82
  * Build the full daemon-scheduled `runAutoCapture` deps from config + the
59
83
  * job-queue-backed `saveFn`. The factory exists so the daemon path is
@@ -66,11 +90,62 @@ export function buildDaemonAutoCaptureDeps(opts) {
66
90
  return {
67
91
  dataDir: opts.config.data_paths.data_dir,
68
92
  sources: opts.config.capture.sources,
93
+ // Config membership IS the Cursor opt-in: when the daemon's configured
94
+ // sources include cursor_composer, authorize discovery + persistence for
95
+ // this run without the env flags.
96
+ cursorConfigEnabled: opts.config.capture.sources.includes(CURSOR_CAPTURE_SOURCE),
69
97
  ...resolveCodexCaptureDeps(opts.config),
98
+ ...resolveCursorCaptureDeps(opts.config),
70
99
  saveFn: opts.saveFn,
71
100
  };
72
101
  }
73
102
  const STATE_FILE = "capture-state.json";
103
+ /**
104
+ * Whether a source may be PERSISTED. Claude/Codex always persist. Native Cursor
105
+ * persists when EITHER:
106
+ * - it is scheduled via config (`cursorConfigEnabled`, i.e. `cursor_composer`
107
+ * is in `config.capture.sources`) — the deliberate, persisted opt-in that
108
+ * authorizes the daemon (and a config-driven manual run) to capture + save
109
+ * Cursor without any env flags; OR
110
+ * - BOTH env flags are set (`RIFT_CURSOR_CAPTURE=1` discovery master switch and
111
+ * `CURSOR_CAPTURE_PERSIST=1`) — the ad-hoc/dogfood path for an explicit
112
+ * `--source cursor_composer` run with no config opt-in.
113
+ *
114
+ * The env discovery flag is part of the env gate on purpose: persisting a source
115
+ * we are not even discovering is incoherent, and — critically — it would let an
116
+ * explicit `--source cursor_composer` run with persist on but discovery OFF write
117
+ * a bootstrap watermark for zero discovered sessions, falsely marking history as
118
+ * "already baselined." A later run with discovery on would then skip bootstrap
119
+ * and save real existing sessions as new — the exact historical backfill this
120
+ * gate exists to prevent. Requiring both env flags (or a config opt-in, which
121
+ * always also enables discovery — see `cursorOn`) closes that hole.
122
+ *
123
+ * With persistence off, Cursor sessions are discovered, fingerprinted, and
124
+ * triaged, but never saved, queued for review, or written to capture-state —
125
+ * "no writes" by construction, independent of `--dry-run`. The check reads the
126
+ * env at call time, so it is consistent within a single run.
127
+ */
128
+ function writesEnabledForSource(source, cursorConfigEnabled) {
129
+ if (source === CURSOR_CAPTURE_SOURCE) {
130
+ return (cursorConfigEnabled ||
131
+ (cursorCaptureEnabled() && cursorCapturePersistEnabled()));
132
+ }
133
+ return true;
134
+ }
135
+ /**
136
+ * Content fingerprint for a discovered session. Cursor sessions hash their
137
+ * precomputed ordered-bubble material; file-backed sources hash size + tail.
138
+ */
139
+ function sessionFingerprint(session) {
140
+ if (session.cursor) {
141
+ return crypto
142
+ .createHash("sha256")
143
+ .update(session.cursor.fingerprintInput)
144
+ .digest("hex")
145
+ .slice(0, 16);
146
+ }
147
+ return contentFingerprint(session.path);
148
+ }
74
149
  export function zeroCounts() {
75
150
  return {
76
151
  discovered: 0,
@@ -134,19 +209,20 @@ function shouldBootstrapSource(state, source, dryRun) {
134
209
  return !hasPriorStateForSource(state, source);
135
210
  }
136
211
  /**
137
- * Whether a preflight failure is transient (a Codex timeout/termination) and
212
+ * Whether a preflight failure is transient (a worker timeout/termination) and
138
213
  * therefore safe to continue past, vs. a hard auth/path failure that should
139
214
  * abort the run. Timeouts are already retried once at the runner; if the probe
140
215
  * still timed out, individual sessions may still succeed, so we let them try.
216
+ * Covers both worker runtimes (Codex and Claude).
141
217
  */
142
218
  function isTransientPreflightError(err) {
143
- return err instanceof CodexCliTimeoutError;
219
+ return (err instanceof CodexCliTimeoutError || err instanceof ClaudeCliTimeoutError);
144
220
  }
145
221
  async function ensureAutoCaptureTriageReady() {
146
- // This is intentionally a tiny real Codex CLI model call, not a cheap
147
- // version check: it catches expired auth and runtime/model failures before
148
- // a capture run starts mutating state.
149
- await new CodexCliTriageProvider({ timeoutMs: 120_000 }).triage({
222
+ // This is intentionally a tiny real CLI model call on the CONFIGURED worker
223
+ // (codex_cli or claude_code), not a cheap version check: it catches expired
224
+ // auth and runtime/model failures before a capture run starts mutating state.
225
+ await createConfiguredTriageProvider({ timeoutMs: 120_000 }).triage({
150
226
  content: "User: Auto-capture preflight check.\n\nAssistant: Ready.",
151
227
  source: "codex_cli",
152
228
  });
@@ -181,7 +257,11 @@ function backupUnreadableState(statePath, error) {
181
257
  }
182
258
  function normalizeBootstrappedSources(sources) {
183
259
  const bootstrappedSources = {};
184
- for (const source of DEFAULT_AUTO_CAPTURE_SOURCES) {
260
+ // Iterate ALL auto-capture sources, not just the scheduled defaults: a
261
+ // flag-gated source that persists (Cursor, when CURSOR_CAPTURE_PERSIST is on)
262
+ // writes a bootstrap watermark, and that marker must survive a state reload —
263
+ // otherwise the source re-bootstraps every run and never captures anything.
264
+ for (const source of AUTO_CAPTURE_SOURCE_VALUES) {
185
265
  const value = sources?.[source];
186
266
  if (typeof value === "string") {
187
267
  bootstrappedSources[source] = value;
@@ -236,9 +316,20 @@ function loadState(dataDir) {
236
316
  : {}),
237
317
  };
238
318
  }
239
- function discoverAllSessions(allowedSources, claudeDir, codexDir) {
319
+ function discoverAllSessions(allowedSources, claudeDir, codexDir, cursor) {
240
320
  const sessions = [];
321
+ // Sources whose discovery actually RAN (and, for sources that catch their own
322
+ // errors, SUCCEEDED) this run. Only these may be bootstrapped: writing a
323
+ // first-enable watermark for a source we never read — because its flag was off
324
+ // or its store was unreadable — would falsely baseline real history and let a
325
+ // later run save it as new. Claude/Codex don't catch discovery errors, so a
326
+ // failure aborts the whole run before any watermark; they are live whenever
327
+ // attempted. Cursor catches (opt-in, best-effort), so it is live only when
328
+ // enabled AND its read succeeds.
329
+ const liveSources = new Set();
330
+ let cursorDiscovery;
241
331
  if (!allowedSources || allowedSources.has("claude_code")) {
332
+ liveSources.add("claude_code");
242
333
  sessions.push(...discoverClaudeCodeSessions(claudeDir).map((session) => ({
243
334
  source: "claude_code",
244
335
  sessionId: session.sessionId,
@@ -247,6 +338,7 @@ function discoverAllSessions(allowedSources, claudeDir, codexDir) {
247
338
  })));
248
339
  }
249
340
  if (!allowedSources || allowedSources.has("codex_cli")) {
341
+ liveSources.add("codex_cli");
250
342
  sessions.push(...discoverCodexSessions(codexDir).map((session) => ({
251
343
  source: "codex_cli",
252
344
  sessionId: session.sessionId,
@@ -254,18 +346,78 @@ function discoverAllSessions(allowedSources, claudeDir, codexDir) {
254
346
  project: session.root,
255
347
  })));
256
348
  }
257
- return sessions;
349
+ if (cursor.enabled &&
350
+ (!allowedSources || allowedSources.has(CURSOR_CAPTURE_SOURCE))) {
351
+ try {
352
+ const disco = discoverCursorCaptureSessions({
353
+ cursorDir: cursor.cursorDir,
354
+ ...(cursor.reader ? { reader: cursor.reader } : {}),
355
+ });
356
+ for (const s of disco.sessions) {
357
+ sessions.push({
358
+ source: CURSOR_CAPTURE_SOURCE,
359
+ sessionId: s.composerId,
360
+ path: disco.dbPath,
361
+ project: s.project,
362
+ cursor: {
363
+ raw: disco.raw,
364
+ fingerprintInput: s.fingerprintInput,
365
+ projectRoot: s.projectRoot,
366
+ projectRootStatus: s.projectRootStatus,
367
+ stableKey: s.stableKey,
368
+ },
369
+ });
370
+ }
371
+ // Reached only on a clean read — now Cursor is safe to bootstrap. If the
372
+ // store throws below, we deliberately leave it OUT of liveSources so a
373
+ // transient read failure on the first-enable run cannot watermark (and
374
+ // thereby skip) real history that a later, readable run would discover.
375
+ liveSources.add(CURSOR_CAPTURE_SOURCE);
376
+ cursorDiscovery = { ok: true };
377
+ }
378
+ catch (err) {
379
+ // Cursor not installed / sqlite3 missing / store read failed: a benign
380
+ // skip for the RUN (the opt-in must never fail the whole capture), but NO
381
+ // LONGER a silent one. Record the outcome so status can warn that a
382
+ // scheduled Cursor opt-in physically can't read its store — instead of
383
+ // claiming "capture on" off the file-presence probe alone.
384
+ const message = err instanceof Error ? err.message : String(err);
385
+ const code = err.code;
386
+ cursorDiscovery = {
387
+ ok: false,
388
+ error: message,
389
+ ...(typeof code === "string" ? { error_code: code } : {}),
390
+ };
391
+ process.stderr.write(`Cursor capture: skipped (store unreadable): ${message}\n`);
392
+ }
393
+ }
394
+ return {
395
+ sessions,
396
+ liveSources,
397
+ ...(cursorDiscovery ? { cursorDiscovery } : {}),
398
+ };
258
399
  }
259
- function applyBootstrap(ctx, sessions, enabledSources, dryRun) {
400
+ function applyBootstrap(ctx, sessions, enabledSources, liveSources, cursorConfigEnabled, dryRun) {
260
401
  const bootstrappedSessionKeys = new Set();
261
402
  const nowIso = new Date().toISOString();
262
403
  for (const source of enabledSources) {
404
+ // Only bootstrap a source whose discovery actually ran (and succeeded) this
405
+ // run — see `liveSources` in discoverAllSessions. A source that was listed
406
+ // but not read (flag off, or store unreadable) must NOT get a watermark, or
407
+ // it would falsely baseline history we never looked at.
408
+ if (!liveSources.has(source))
409
+ continue;
410
+ // Skip sources that don't persist (Cursor with neither the config opt-in nor
411
+ // both env flags): a watermark is a write we must not make for a triage-only
412
+ // source.
413
+ if (!writesEnabledForSource(source, cursorConfigEnabled))
414
+ continue;
263
415
  if (!shouldBootstrapSource(ctx.state, source, dryRun)) {
264
416
  continue;
265
417
  }
266
418
  const sourceSessions = sessions.filter((session) => session.source === source);
267
419
  for (const session of sourceSessions) {
268
- const fingerprint = contentFingerprint(session.path);
420
+ const fingerprint = sessionFingerprint(session);
269
421
  ctx.state.processed[stateKey(session.source, session.sessionId)] = {
270
422
  fingerprint,
271
423
  idempotency_key: "",
@@ -290,6 +442,8 @@ function deriveTopLevelTotals(report) {
290
442
  review: 0,
291
443
  quarantined: 0,
292
444
  errors: 0,
445
+ deferred: 0,
446
+ bootstrapped: 0,
293
447
  };
294
448
  for (const counts of Object.values(report.counts_by_source)) {
295
449
  if (!counts)
@@ -301,6 +455,8 @@ function deriveTopLevelTotals(report) {
301
455
  totals.review += counts.review;
302
456
  totals.quarantined += counts.quarantined;
303
457
  totals.errors += counts.errors;
458
+ totals.deferred += counts.deferred;
459
+ totals.bootstrapped += counts.bootstrapped;
304
460
  }
305
461
  report.total_discovered = totals.total_discovered;
306
462
  report.new_conversations = totals.new_conversations;
@@ -309,6 +465,8 @@ function deriveTopLevelTotals(report) {
309
465
  report.review = totals.review;
310
466
  report.quarantined = totals.quarantined;
311
467
  report.errors = totals.errors;
468
+ report.deferred = totals.deferred;
469
+ report.bootstrapped = totals.bootstrapped;
312
470
  return report;
313
471
  }
314
472
  function applySessionOutcome(ctx, outcome) {
@@ -320,6 +478,7 @@ function applySessionOutcome(ctx, outcome) {
320
478
  counts.discarded += outcome.counts.discarded ?? 0;
321
479
  counts.quarantined += outcome.counts.quarantined ?? 0;
322
480
  counts.errors += outcome.counts.errors ?? 0;
481
+ counts.deferred += outcome.counts.deferred ?? 0;
323
482
  ctx.report.results.push(...outcome.results);
324
483
  ctx.report.error_details.push(...outcome.errorDetails);
325
484
  ctx.stateChanged ||= outcome.stateChanged;
@@ -389,19 +548,22 @@ async function saveState(dataDir, state, initialProcessed) {
389
548
  async function writeOversizedQuarantineRecord(opts) {
390
549
  const dir = path.join(opts.dataDir, "quarantine");
391
550
  fs.mkdirSync(dir, { recursive: true });
392
- const filename = `codex_cli_${opts.sessionId}_${opts.fingerprint}.json`;
551
+ const filename = `${opts.source}_${opts.sessionId}_${opts.fingerprint}.json`;
393
552
  const target = path.join(dir, filename);
394
553
  if (fs.existsSync(target))
395
554
  return;
555
+ const remediation = opts.source === CURSOR_CAPTURE_SOURCE
556
+ ? "Raise AutoCaptureDeps.cursorMaxSessionBytes for the run, or split the Cursor session."
557
+ : "Raise capture.codex_cli.max_session_bytes (and restart the daemon), or use the future opt-in chunked-summarize backfill.";
396
558
  const record = {
397
- source: "codex_cli",
559
+ source: opts.source,
398
560
  session_id: opts.sessionId,
399
561
  session_path: opts.sessionPath,
400
562
  size_bytes: opts.sizeBytes,
401
563
  threshold_bytes: opts.thresholdBytes,
402
564
  fingerprint: opts.fingerprint,
403
565
  reason: "oversized-session",
404
- remediation: "Raise capture.codex_cli.max_session_bytes (and restart the daemon), or use the future opt-in chunked-summarize backfill.",
566
+ remediation,
405
567
  quarantined_at: new Date().toISOString(),
406
568
  };
407
569
  await atomicWrite(target, JSON.stringify(record, null, 2));
@@ -436,7 +598,27 @@ async function processSession(session, state, deps) {
436
598
  stateChanged: false,
437
599
  });
438
600
  try {
439
- const fingerprint = contentFingerprint(session.path);
601
+ const fingerprint = sessionFingerprint(session);
602
+ // Cursor is triage-only this slice: no /save, no review-queue, no
603
+ // capture-state write — regardless of --dry-run. See writesEnabledForSource.
604
+ const writesAllowed = !deps.dryRun &&
605
+ writesEnabledForSource(session.source, Boolean(deps.cursorConfigEnabled));
606
+ const cursorResultFields = session.cursor
607
+ ? {
608
+ projectRootStatus: session.cursor.projectRootStatus,
609
+ stableKey: session.cursor.stableKey,
610
+ }
611
+ : {};
612
+ // Cursor titles are private (user's session name / first prompt), and even
613
+ // the triage-only dry-run prints result titles to the terminal and JSON.
614
+ // Redact to a non-identifying label by default; reveal only behind an
615
+ // explicit opt-in. Claude/Codex titles are untouched.
616
+ const displayTitle = (parsedTitle) => {
617
+ if (session.cursor && !cursorTitlesRevealed()) {
618
+ return redactedCursorTitle(session.sessionId);
619
+ }
620
+ return parsedTitle ?? "(empty)";
621
+ };
440
622
  const prev = getPreviousEntry(state, session.source, session.sessionId);
441
623
  const prevFingerprint = typeof prev === "string" ? prev : prev?.fingerprint;
442
624
  const prevKey = typeof prev === "object" ? prev?.idempotency_key : undefined;
@@ -454,6 +636,7 @@ async function processSession(session, state, deps) {
454
636
  outcome.counts.quarantined = 1;
455
637
  if (!deps.dryRun) {
456
638
  await writeOversizedQuarantineRecord({
639
+ source: session.source,
457
640
  dataDir: deps.dataDir,
458
641
  sessionId: session.sessionId,
459
642
  sessionPath: session.path,
@@ -473,12 +656,18 @@ async function processSession(session, state, deps) {
473
656
  const isUpdate = prevFingerprint !== undefined;
474
657
  const outcome = empty();
475
658
  outcome.counts.new = 1;
476
- const data = fs.readFileSync(session.path, "utf-8");
477
- const parsed = session.source === "claude_code"
478
- ? parseClaudeCodeSession(session.sessionId, data)
479
- : parseCodexSession(session.sessionId, data);
659
+ let parsed;
660
+ if (session.cursor) {
661
+ parsed = parseCursorComposerSession(session.cursor.raw, session.sessionId);
662
+ }
663
+ else {
664
+ const data = fs.readFileSync(session.path, "utf-8");
665
+ parsed = session.source === "claude_code"
666
+ ? parseClaudeCodeSession(session.sessionId, data)
667
+ : parseCodexSession(session.sessionId, data);
668
+ }
480
669
  if (!parsed || parsed.content.length < 100) {
481
- if (!deps.dryRun) {
670
+ if (writesAllowed) {
482
671
  state.processed[stateKey(session.source, session.sessionId)] = {
483
672
  fingerprint,
484
673
  idempotency_key: "",
@@ -489,36 +678,97 @@ async function processSession(session, state, deps) {
489
678
  outcome.results.push({
490
679
  source: session.source,
491
680
  sessionId: session.sessionId,
492
- title: parsed?.title ?? "(empty)",
681
+ title: displayTitle(parsed?.title),
493
682
  decision: "discard",
494
683
  confidence: 1.0,
495
684
  model: "n/a",
496
685
  provider: "auto-capture",
497
686
  summary: "Too short or empty",
687
+ ...cursorResultFields,
498
688
  });
499
689
  return outcome;
500
690
  }
691
+ // Cursor size guard. Cursor has no file to stat (its content lives in a
692
+ // shared `state.vscdb`), so the guard runs on the PARSED transcript — the
693
+ // exact text triage would feed the subprocess — and fires BEFORE any triage
694
+ // or save. Mirrors the codex oversized path: quarantine, never triage. The
695
+ // quarantine record + capture-state sentinel are writes, so they only land
696
+ // when writes are allowed (persist on, non-dry); otherwise it is a pure skip.
697
+ //
698
+ // TERMINAL by design (decided pre-scheduling): unlike Codex, an oversized
699
+ // Cursor session has NO auto-recovery path — `recover-quarantine` globs only
700
+ // `codex_cli_` records, so it never touches the `cursor_composer_` record we
701
+ // write here, and `countParkedOversizedSessions` never raises a "run repair"
702
+ // advisory for it. The record is operator-visible metadata only; the sole
703
+ // remediation is to raise the byte limit or split the session, after which
704
+ // the grown/changed fingerprint re-discovers it on a later run. Cursor
705
+ // chunked-summarize backfill is deferred to the scheduling slice.
706
+ if (session.cursor) {
707
+ const sizeBytes = Buffer.byteLength(parsed.content, "utf8");
708
+ const maxBytes = deps.cursorMaxSessionBytes ?? DEFAULT_CURSOR_MAX_SESSION_BYTES;
709
+ if (sizeBytes > maxBytes) {
710
+ const oversized = empty();
711
+ oversized.counts.quarantined = 1;
712
+ if (writesAllowed) {
713
+ await writeOversizedQuarantineRecord({
714
+ source: session.source,
715
+ dataDir: deps.dataDir,
716
+ sessionId: session.sessionId,
717
+ sessionPath: session.path,
718
+ sizeBytes,
719
+ thresholdBytes: maxBytes,
720
+ fingerprint,
721
+ });
722
+ state.processed[stateKey(session.source, session.sessionId)] = {
723
+ fingerprint,
724
+ idempotency_key: QUARANTINED_OVERSIZED_KEY,
725
+ };
726
+ oversized.stateChanged = true;
727
+ }
728
+ return oversized;
729
+ }
730
+ }
501
731
  const triage = await triageConversation(parsed.content);
502
732
  const result = {
503
733
  source: session.source,
504
734
  sessionId: session.sessionId,
505
- title: parsed.title,
735
+ title: displayTitle(parsed.title),
506
736
  decision: triage.decision,
507
737
  confidence: triage.confidence,
508
738
  model: triage.model,
509
739
  provider: triage.provider,
510
740
  summary: triage.summary,
741
+ ...cursorResultFields,
511
742
  };
512
743
  outcome.results.push(result);
513
- if (deps.dryRun) {
744
+ if (!writesAllowed) {
745
+ // Writes are off for one of two reasons, and they are NOT equivalent:
746
+ // - dry-run of a persistable source: a faithful preview of what a real
747
+ // run WOULD persist, so save/review count as saved/review.
748
+ // - a source whose persistence is gated off this slice (Cursor): a real
749
+ // run would not persist it either, so reporting "saved" is a lie. Even
750
+ // a non-dry `rift capture` reaches the health ledger (capture.ts), and
751
+ // `rift status` would then sum those as real saved sessions. Bucket
752
+ // these as `deferred` so neither the ledger nor status ever inflates.
753
+ // Bucketing keys on the SOURCE (writesEnabledForSource), not --dry-run, so
754
+ // a gated source reads `deferred` consistently in dry and non-dry runs.
755
+ const persistable = writesEnabledForSource(session.source, Boolean(deps.cursorConfigEnabled));
514
756
  switch (triage.decision) {
515
757
  case "save":
516
- outcome.counts.saved = 1;
758
+ if (persistable)
759
+ outcome.counts.saved = 1;
760
+ else
761
+ outcome.counts.deferred = 1;
517
762
  break;
518
763
  case "review":
519
- outcome.counts.review = 1;
764
+ if (persistable)
765
+ outcome.counts.review = 1;
766
+ else
767
+ outcome.counts.deferred = 1;
520
768
  break;
521
769
  case "discard":
770
+ // A discard persists nothing regardless of the gate, so it is honest
771
+ // in either case and never inflates a "saved" count.
522
772
  outcome.counts.discarded = 1;
523
773
  break;
524
774
  }
@@ -592,6 +842,7 @@ async function processSession(session, state, deps) {
592
842
  export async function runAutoCapture(deps) {
593
843
  const claudeDir = deps.claudeDir ?? path.join(os.homedir(), ".claude");
594
844
  const codexDir = deps.codexDir ?? path.join(os.homedir(), ".codex");
845
+ const cursorDir = deps.cursorDir ?? defaultCursorDir();
595
846
  const state = loadState(deps.dataDir);
596
847
  // Snapshot the ledger exactly as loaded so the end-of-run merge can overlay
597
848
  // ONLY the keys this run actually changed (see saveState). Capturing it here
@@ -600,8 +851,25 @@ export async function runAutoCapture(deps) {
600
851
  // run never touched.
601
852
  const initialProcessed = { ...state.processed };
602
853
  const allowedSources = deps.sources ? new Set(deps.sources) : null;
603
- const enabledSources = deps.sources ?? DEFAULT_AUTO_CAPTURE_SOURCES;
604
- const sessions = discoverAllSessions(allowedSources, claudeDir, codexDir);
854
+ // Cursor is enabled for this run via EITHER the config opt-in
855
+ // (`cursorConfigEnabled` cursor_composer in config.capture.sources, the
856
+ // scheduled path) OR the `RIFT_CURSOR_CAPTURE` env flag (the ad-hoc/dogfood
857
+ // path). Config enablement also authorizes persistence (see
858
+ // writesEnabledForSource); the env path still needs CURSOR_CAPTURE_PERSIST.
859
+ const cursorConfigEnabled = Boolean(deps.cursorConfigEnabled);
860
+ const cursorOn = cursorConfigEnabled || cursorCaptureEnabled();
861
+ // When no explicit source filter was given and Cursor is on (config or env),
862
+ // it joins the default run so its sessions show up alongside claude/codex.
863
+ const enabledSources = deps.sources
864
+ ? deps.sources
865
+ : cursorOn
866
+ ? [...DEFAULT_AUTO_CAPTURE_SOURCES, CURSOR_CAPTURE_SOURCE]
867
+ : DEFAULT_AUTO_CAPTURE_SOURCES;
868
+ const { sessions, liveSources, cursorDiscovery } = discoverAllSessions(allowedSources, claudeDir, codexDir, {
869
+ enabled: cursorOn,
870
+ cursorDir,
871
+ ...(deps.cursorReader ? { reader: deps.cursorReader } : {}),
872
+ });
605
873
  const report = {
606
874
  total_discovered: 0,
607
875
  new_conversations: 0,
@@ -610,10 +878,13 @@ export async function runAutoCapture(deps) {
610
878
  review: 0,
611
879
  quarantined: 0,
612
880
  errors: 0,
881
+ deferred: 0,
882
+ bootstrapped: 0,
613
883
  preflight_ok: true,
614
884
  counts_by_source: {},
615
885
  results: [],
616
886
  error_details: [],
887
+ ...(cursorDiscovery ? { cursor_discovery: cursorDiscovery } : {}),
617
888
  };
618
889
  for (const source of enabledSources) {
619
890
  ensureCounts(report, source);
@@ -626,18 +897,26 @@ export async function runAutoCapture(deps) {
626
897
  report,
627
898
  stateChanged: false,
628
899
  };
629
- const bootstrappedSessionKeys = applyBootstrap(ctx, sessions, enabledSources, Boolean(deps.dryRun));
900
+ const bootstrappedSessionKeys = applyBootstrap(ctx, sessions, enabledSources, liveSources, cursorConfigEnabled, Boolean(deps.dryRun));
630
901
  const sessionsToProcess = sessions.filter((session) => !bootstrappedSessionKeys.has(stateKey(session.source, session.sessionId)));
631
902
  if (enabledSources.length > 0) {
903
+ // Stamp the worker that this run's preflight will use (and did use, on
904
+ // failure) BEFORE running it, so the recorded outcome is attributable to the
905
+ // worker that actually ran — never re-inferred later from drifting config or
906
+ // ambiguous error text. `deps.preflightTriage` (test seam) stands in for the
907
+ // configured provider, so the configured name is still the right label.
908
+ report.preflight_worker =
909
+ getConfiguredTriageProviderName() === "claude_code" ? "claude" : "codex";
632
910
  try {
633
911
  await (deps.preflightTriage ?? ensureAutoCaptureTriageReady)();
634
912
  }
635
913
  catch (err) {
636
914
  const error = err instanceof Error ? err.message : String(err);
637
- // Preflight is a Codex health probe, NOT a conversation. Record it on its
638
- // own field + the preflight_ok health flag, never as per-source
639
- // conversation errors — so the "N failed" count reflects only real
640
- // sessions. (autoCaptureRunFailed still flags the run via preflight_ok.)
915
+ // Preflight is a triage-worker health probe (Codex or Claude), NOT a
916
+ // conversation. Record it on its own field + the preflight_ok health flag,
917
+ // never as per-source conversation errors — so the "N failed" count
918
+ // reflects only real sessions. (autoCaptureRunFailed still flags the run
919
+ // via preflight_ok.)
641
920
  report.preflight_ok = false;
642
921
  report.preflight_error = error;
643
922
  // A timeout/termination is transient (cold start, brief contention) and