elepha 0.1.0

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 (290) hide show
  1. package/LICENSE.md +375 -0
  2. package/README.md +135 -0
  3. package/bin/elepha.js +2 -0
  4. package/dist/adapters/base.js +600 -0
  5. package/dist/adapters/base.js.map +1 -0
  6. package/dist/adapters/claude-code.js +260 -0
  7. package/dist/adapters/claude-code.js.map +1 -0
  8. package/dist/adapters/codex.js +499 -0
  9. package/dist/adapters/codex.js.map +1 -0
  10. package/dist/adapters/discriminators.js +39 -0
  11. package/dist/adapters/discriminators.js.map +1 -0
  12. package/dist/adapters/index.js +6 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/adapters/internal-command.js +3 -0
  15. package/dist/adapters/internal-command.js.map +1 -0
  16. package/dist/cli/backup-wizard.js +97 -0
  17. package/dist/cli/backup-wizard.js.map +1 -0
  18. package/dist/cli/capture-service.js +80 -0
  19. package/dist/cli/capture-service.js.map +1 -0
  20. package/dist/cli/commands/backfills.js +336 -0
  21. package/dist/cli/commands/backfills.js.map +1 -0
  22. package/dist/cli/commands/backup.js +234 -0
  23. package/dist/cli/commands/backup.js.map +1 -0
  24. package/dist/cli/commands/config.js +66 -0
  25. package/dist/cli/commands/config.js.map +1 -0
  26. package/dist/cli/commands/consent.js +237 -0
  27. package/dist/cli/commands/consent.js.map +1 -0
  28. package/dist/cli/commands/daemon-control.js +76 -0
  29. package/dist/cli/commands/daemon-control.js.map +1 -0
  30. package/dist/cli/commands/doctor.js +31 -0
  31. package/dist/cli/commands/doctor.js.map +1 -0
  32. package/dist/cli/commands/hook.js +24 -0
  33. package/dist/cli/commands/hook.js.map +1 -0
  34. package/dist/cli/commands/import.js +611 -0
  35. package/dist/cli/commands/import.js.map +1 -0
  36. package/dist/cli/commands/init.js +19 -0
  37. package/dist/cli/commands/init.js.map +1 -0
  38. package/dist/cli/commands/inspect.js +54 -0
  39. package/dist/cli/commands/inspect.js.map +1 -0
  40. package/dist/cli/commands/install.js +49 -0
  41. package/dist/cli/commands/install.js.map +1 -0
  42. package/dist/cli/commands/internal.js +16 -0
  43. package/dist/cli/commands/internal.js.map +1 -0
  44. package/dist/cli/commands/mcp.js +12 -0
  45. package/dist/cli/commands/mcp.js.map +1 -0
  46. package/dist/cli/commands/projects.js +26 -0
  47. package/dist/cli/commands/projects.js.map +1 -0
  48. package/dist/cli/commands/purge.js +216 -0
  49. package/dist/cli/commands/purge.js.map +1 -0
  50. package/dist/cli/commands/reingest.js +80 -0
  51. package/dist/cli/commands/reingest.js.map +1 -0
  52. package/dist/cli/commands/rekey.js +58 -0
  53. package/dist/cli/commands/rekey.js.map +1 -0
  54. package/dist/cli/commands/restore.js +376 -0
  55. package/dist/cli/commands/restore.js.map +1 -0
  56. package/dist/cli/commands/rollup.js +130 -0
  57. package/dist/cli/commands/rollup.js.map +1 -0
  58. package/dist/cli/commands/sanitize.js +72 -0
  59. package/dist/cli/commands/sanitize.js.map +1 -0
  60. package/dist/cli/commands/segment.js +153 -0
  61. package/dist/cli/commands/segment.js.map +1 -0
  62. package/dist/cli/commands/self-update.js +26 -0
  63. package/dist/cli/commands/self-update.js.map +1 -0
  64. package/dist/cli/commands/start.js +53 -0
  65. package/dist/cli/commands/start.js.map +1 -0
  66. package/dist/cli/commands/stats.js +42 -0
  67. package/dist/cli/commands/stats.js.map +1 -0
  68. package/dist/cli/commands/status.js +73 -0
  69. package/dist/cli/commands/status.js.map +1 -0
  70. package/dist/cli/commands/uninstall.js +23 -0
  71. package/dist/cli/commands/uninstall.js.map +1 -0
  72. package/dist/cli/config-wizard.js +104 -0
  73. package/dist/cli/config-wizard.js.map +1 -0
  74. package/dist/cli/destructive-op.js +32 -0
  75. package/dist/cli/destructive-op.js.map +1 -0
  76. package/dist/cli/import-wizard.js +89 -0
  77. package/dist/cli/import-wizard.js.map +1 -0
  78. package/dist/cli/index.js +74 -0
  79. package/dist/cli/index.js.map +1 -0
  80. package/dist/cli/init-wizard.js +80 -0
  81. package/dist/cli/init-wizard.js.map +1 -0
  82. package/dist/cli/init.js +159 -0
  83. package/dist/cli/init.js.map +1 -0
  84. package/dist/cli/launcher-probe.js +71 -0
  85. package/dist/cli/launcher-probe.js.map +1 -0
  86. package/dist/cli/project-path.js +16 -0
  87. package/dist/cli/project-path.js.map +1 -0
  88. package/dist/cli/purge-wizard.js +200 -0
  89. package/dist/cli/purge-wizard.js.map +1 -0
  90. package/dist/cli/restore-wizard.js +101 -0
  91. package/dist/cli/restore-wizard.js.map +1 -0
  92. package/dist/cli/shared.js +128 -0
  93. package/dist/cli/shared.js.map +1 -0
  94. package/dist/cli/status.js +22 -0
  95. package/dist/cli/status.js.map +1 -0
  96. package/dist/cli/wordmark.js +15 -0
  97. package/dist/cli/wordmark.js.map +1 -0
  98. package/dist/config/constants.js +115 -0
  99. package/dist/config/constants.js.map +1 -0
  100. package/dist/config/env.js +168 -0
  101. package/dist/config/env.js.map +1 -0
  102. package/dist/config/memory-config.js +63 -0
  103. package/dist/config/memory-config.js.map +1 -0
  104. package/dist/config/paths.js +343 -0
  105. package/dist/config/paths.js.map +1 -0
  106. package/dist/config/settings.js +132 -0
  107. package/dist/config/settings.js.map +1 -0
  108. package/dist/config/version.js +12 -0
  109. package/dist/config/version.js.map +1 -0
  110. package/dist/daemon/failure-window.js +38 -0
  111. package/dist/daemon/failure-window.js.map +1 -0
  112. package/dist/daemon/heartbeat.js +40 -0
  113. package/dist/daemon/heartbeat.js.map +1 -0
  114. package/dist/daemon/index.js +873 -0
  115. package/dist/daemon/index.js.map +1 -0
  116. package/dist/daemon/log-rotation.js +27 -0
  117. package/dist/daemon/log-rotation.js.map +1 -0
  118. package/dist/daemon/readability-guard.js +89 -0
  119. package/dist/daemon/readability-guard.js.map +1 -0
  120. package/dist/daemon/rollup-service.js +239 -0
  121. package/dist/daemon/rollup-service.js.map +1 -0
  122. package/dist/daemon/update-check.js +87 -0
  123. package/dist/daemon/update-check.js.map +1 -0
  124. package/dist/daemon/work-queue.js +28 -0
  125. package/dist/daemon/work-queue.js.map +1 -0
  126. package/dist/discovery/session-projects.js +242 -0
  127. package/dist/discovery/session-projects.js.map +1 -0
  128. package/dist/hooks/codex-trust.js +80 -0
  129. package/dist/hooks/codex-trust.js.map +1 -0
  130. package/dist/hooks/common.js +81 -0
  131. package/dist/hooks/common.js.map +1 -0
  132. package/dist/hooks/hook-log.js +38 -0
  133. package/dist/hooks/hook-log.js.map +1 -0
  134. package/dist/hooks/installer.js +200 -0
  135. package/dist/hooks/installer.js.map +1 -0
  136. package/dist/hooks/session-start.js +321 -0
  137. package/dist/hooks/session-start.js.map +1 -0
  138. package/dist/hooks/user-prompt-submit.js +248 -0
  139. package/dist/hooks/user-prompt-submit.js.map +1 -0
  140. package/dist/install/binary.js +72 -0
  141. package/dist/install/binary.js.map +1 -0
  142. package/dist/install/config-file.js +119 -0
  143. package/dist/install/config-file.js.map +1 -0
  144. package/dist/install/daemon-health.js +48 -0
  145. package/dist/install/daemon-health.js.map +1 -0
  146. package/dist/install/daemon-service.js +309 -0
  147. package/dist/install/daemon-service.js.map +1 -0
  148. package/dist/install/doctor.js +180 -0
  149. package/dist/install/doctor.js.map +1 -0
  150. package/dist/install/health-checks.js +90 -0
  151. package/dist/install/health-checks.js.map +1 -0
  152. package/dist/install/installer.js +411 -0
  153. package/dist/install/installer.js.map +1 -0
  154. package/dist/install/launchd-backend.js +261 -0
  155. package/dist/install/launchd-backend.js.map +1 -0
  156. package/dist/install/launcher.js +224 -0
  157. package/dist/install/launcher.js.map +1 -0
  158. package/dist/install/markers.js +14 -0
  159. package/dist/install/markers.js.map +1 -0
  160. package/dist/install/platform.js +29 -0
  161. package/dist/install/platform.js.map +1 -0
  162. package/dist/install/present-tools.js +16 -0
  163. package/dist/install/present-tools.js.map +1 -0
  164. package/dist/install/self-update.js +120 -0
  165. package/dist/install/self-update.js.map +1 -0
  166. package/dist/install/service-backend.js +36 -0
  167. package/dist/install/service-backend.js.map +1 -0
  168. package/dist/install/service-manifest.js +36 -0
  169. package/dist/install/service-manifest.js.map +1 -0
  170. package/dist/install/status.js +58 -0
  171. package/dist/install/status.js.map +1 -0
  172. package/dist/install/systemd-backend.js +225 -0
  173. package/dist/install/systemd-backend.js.map +1 -0
  174. package/dist/markers.js +5 -0
  175. package/dist/markers.js.map +1 -0
  176. package/dist/mcp/installer.js +131 -0
  177. package/dist/mcp/installer.js.map +1 -0
  178. package/dist/mcp/server.js +81 -0
  179. package/dist/mcp/server.js.map +1 -0
  180. package/dist/mcp/tools/get-context.js +3 -0
  181. package/dist/mcp/tools/get-context.js.map +1 -0
  182. package/dist/mcp/tools/list-projects.js +3 -0
  183. package/dist/mcp/tools/list-projects.js.map +1 -0
  184. package/dist/mcp/tools/save-memory.js +3 -0
  185. package/dist/mcp/tools/save-memory.js.map +1 -0
  186. package/dist/mcp/tools.js +288 -0
  187. package/dist/mcp/tools.js.map +1 -0
  188. package/dist/rendering/raw-turn-renderer.js +85 -0
  189. package/dist/rendering/raw-turn-renderer.js.map +1 -0
  190. package/dist/security/file-permissions.js +15 -0
  191. package/dist/security/file-permissions.js.map +1 -0
  192. package/dist/security/provider-transcript.js +74 -0
  193. package/dist/security/provider-transcript.js.map +1 -0
  194. package/dist/security/sanitize.js +210 -0
  195. package/dist/security/sanitize.js.map +1 -0
  196. package/dist/security/self-ingestion.js +47 -0
  197. package/dist/security/self-ingestion.js.map +1 -0
  198. package/dist/security/sentinel.js +19 -0
  199. package/dist/security/sentinel.js.map +1 -0
  200. package/dist/security/subprocess-allowlist.js +421 -0
  201. package/dist/security/subprocess-allowlist.js.map +1 -0
  202. package/dist/serving/instructions.js +33 -0
  203. package/dist/serving/instructions.js.map +1 -0
  204. package/dist/serving/lexical-recall.js +249 -0
  205. package/dist/serving/lexical-recall.js.map +1 -0
  206. package/dist/serving/session-reader.js +302 -0
  207. package/dist/serving/session-reader.js.map +1 -0
  208. package/dist/storage/backfill-runner.js +34 -0
  209. package/dist/storage/backfill-runner.js.map +1 -0
  210. package/dist/storage/backup.js +46 -0
  211. package/dist/storage/backup.js.map +1 -0
  212. package/dist/storage/candidate-validator.js +77 -0
  213. package/dist/storage/candidate-validator.js.map +1 -0
  214. package/dist/storage/consent-store.js +198 -0
  215. package/dist/storage/consent-store.js.map +1 -0
  216. package/dist/storage/custom-title-backfill.js +68 -0
  217. package/dist/storage/custom-title-backfill.js.map +1 -0
  218. package/dist/storage/db.js +312 -0
  219. package/dist/storage/db.js.map +1 -0
  220. package/dist/storage/external-agent-import-purge.js +182 -0
  221. package/dist/storage/external-agent-import-purge.js.map +1 -0
  222. package/dist/storage/first-prompt-search-backfill.js +76 -0
  223. package/dist/storage/first-prompt-search-backfill.js.map +1 -0
  224. package/dist/storage/first-prompt-search.js +7 -0
  225. package/dist/storage/first-prompt-search.js.map +1 -0
  226. package/dist/storage/injection-store.js +37 -0
  227. package/dist/storage/injection-store.js.map +1 -0
  228. package/dist/storage/memory-store.js +428 -0
  229. package/dist/storage/memory-store.js.map +1 -0
  230. package/dist/storage/project-resolver.js +294 -0
  231. package/dist/storage/project-resolver.js.map +1 -0
  232. package/dist/storage/project-store.js +104 -0
  233. package/dist/storage/project-store.js.map +1 -0
  234. package/dist/storage/rendered-chars-backfill.js +85 -0
  235. package/dist/storage/rendered-chars-backfill.js.map +1 -0
  236. package/dist/storage/resegmentation.js +582 -0
  237. package/dist/storage/resegmentation.js.map +1 -0
  238. package/dist/storage/rollup-store.js +246 -0
  239. package/dist/storage/rollup-store.js.map +1 -0
  240. package/dist/storage/root-commit-backfill.js +32 -0
  241. package/dist/storage/root-commit-backfill.js.map +1 -0
  242. package/dist/storage/sanitize-backfill.js +184 -0
  243. package/dist/storage/sanitize-backfill.js.map +1 -0
  244. package/dist/storage/segmentation.js +87 -0
  245. package/dist/storage/segmentation.js.map +1 -0
  246. package/dist/storage/session-fields-backfill.js +196 -0
  247. package/dist/storage/session-fields-backfill.js.map +1 -0
  248. package/dist/storage/session-read-model.js +130 -0
  249. package/dist/storage/session-read-model.js.map +1 -0
  250. package/dist/storage/session-store.js +200 -0
  251. package/dist/storage/session-store.js.map +1 -0
  252. package/dist/storage/session-title-backfill.js +75 -0
  253. package/dist/storage/session-title-backfill.js.map +1 -0
  254. package/dist/storage/session-title.js +71 -0
  255. package/dist/storage/session-title.js.map +1 -0
  256. package/dist/storage/shown-session-list-store.js +33 -0
  257. package/dist/storage/shown-session-list-store.js.map +1 -0
  258. package/dist/storage/stats.js +30 -0
  259. package/dist/storage/stats.js.map +1 -0
  260. package/dist/storage/turn-store.js +202 -0
  261. package/dist/storage/turn-store.js.map +1 -0
  262. package/dist/storage/ulid.js +17 -0
  263. package/dist/storage/ulid.js.map +1 -0
  264. package/dist/summarizer/anthropic-call.js +76 -0
  265. package/dist/summarizer/anthropic-call.js.map +1 -0
  266. package/dist/summarizer/call-log.js +120 -0
  267. package/dist/summarizer/call-log.js.map +1 -0
  268. package/dist/summarizer/haiku-provider.js +136 -0
  269. package/dist/summarizer/haiku-provider.js.map +1 -0
  270. package/dist/summarizer/pricing.js +8 -0
  271. package/dist/summarizer/pricing.js.map +1 -0
  272. package/dist/summarizer/prompt-cache.js +6 -0
  273. package/dist/summarizer/prompt-cache.js.map +1 -0
  274. package/dist/summarizer/prompt.js +47 -0
  275. package/dist/summarizer/prompt.js.map +1 -0
  276. package/dist/summarizer/provider-config.js +25 -0
  277. package/dist/summarizer/provider-config.js.map +1 -0
  278. package/dist/summarizer/rollup-prompt.js +225 -0
  279. package/dist/summarizer/rollup-prompt.js.map +1 -0
  280. package/dist/summarizer/rollup-provider.js +224 -0
  281. package/dist/summarizer/rollup-provider.js.map +1 -0
  282. package/dist/types/index.js +10 -0
  283. package/dist/types/index.js.map +1 -0
  284. package/dist/util/error.js +5 -0
  285. package/dist/util/error.js.map +1 -0
  286. package/dist/util/fs.js +97 -0
  287. package/dist/util/fs.js.map +1 -0
  288. package/dist/util/relative-time.js +12 -0
  289. package/dist/util/relative-time.js.map +1 -0
  290. package/package.json +81 -0
@@ -0,0 +1,246 @@
1
+ // Persistence for session-level rollups.
2
+ //
3
+ // The hard problem here is idempotency. `memories` is protected by
4
+ // UNIQUE(session_id, turn_index): re-processing a turn is a no-op. A rollup is
5
+ // a MUTABLE AGGREGATE with no such guard - applying the same new turns twice
6
+ // would double-count decisions, resurrect resolved pending_items, and inflate
7
+ // turn_count. And re-processing is not hypothetical: "closed" is a heuristic
8
+ // (file idle), and Codex Desktop reopens sessions days later and appends to the
9
+ // original rollout, so final -> live -> final can repeat indefinitely.
10
+ //
11
+ // Two independent guards, either of which alone would be sufficient:
12
+ //
13
+ // 1. WATERMARK, enforced in SQL. rolled_up_through_turn_index advances in the
14
+ // SAME transaction that writes the content it accounts for, and the write
15
+ // is conditional on the watermark still being where the caller read it
16
+ // (`WHERE rolled_up_through_turn_index = :expected`). A duplicate or
17
+ // concurrent apply finds the watermark moved and writes nothing. A crash
18
+ // between the LLM call and the commit costs one wasted call and changes
19
+ // nothing on disk.
20
+ // 2. SET SEMANTICS in the merge itself. decisions dedupe by `what`,
21
+ // pending_items and files_touched by value. Applying the same merge twice
22
+ // yields the same arrays.
23
+ import { dedupePaths } from '../config/paths.js';
24
+ import { escapeShellSyntax, stripShellSyntax } from '../security/sanitize.js';
25
+ /**
26
+ * Every rollup from the old schema is stale, for three independent reasons
27
+ * that all landed together so the corpus is rebuilt once rather than three
28
+ * times:
29
+ *
30
+ * - the prompt builder end-truncated its input, so on large sessions the
31
+ * newest turns never reached the model (measured: 83% of all lost decisions);
32
+ * - decisions now carry `turnIndex`/`at` provenance, assigned in code;
33
+ * - `why` is now captured at the turn, where the transcript is still in the
34
+ * prompt, instead of being inferred afterwards from the decision text.
35
+ *
36
+ * `elepha rollup --rebuild` is what actually reaches them: the default sweep
37
+ * skips `final` rollups.
38
+ */
39
+ export const ROLLUP_VERSION = 2;
40
+ /**
41
+ * Newest-first by provenance. Decisions with no `turnIndex` (pre-provenance
42
+ * rows) sort oldest, which is both true and the safe direction: they are never
43
+ * preferred over a decision we can actually place in time.
44
+ */
45
+ export function sortDecisionsByTurn(decisions) {
46
+ return [...decisions].sort((a, b) => (b.turnIndex ?? -1) - (a.turnIndex ?? -1));
47
+ }
48
+ /**
49
+ * The newest K decisions, in turn order. This is the selection the brief and
50
+ * the carried-rollup budget both need, and it lives HERE, in code, rather than
51
+ * in a prompt instruction - the merge model demonstrably drops new decisions
52
+ * when asked politely not to, and a selection rule that the model can violate
53
+ * is not a selection rule.
54
+ */
55
+ export function newestDecisions(decisions, k) {
56
+ return sortDecisionsByTurn(decisions)
57
+ .slice(0, k)
58
+ .sort((a, b) => (a.turnIndex ?? -1) - (b.turnIndex ?? -1));
59
+ }
60
+ function hydrate(row) {
61
+ if (!row) {
62
+ return undefined;
63
+ }
64
+ return {
65
+ ...row,
66
+ decisions: JSON.parse(row.decisions),
67
+ pending_items: JSON.parse(row.pending_items),
68
+ files_touched: JSON.parse(row.files_touched),
69
+ };
70
+ }
71
+ /** Merges two rollup halves with set semantics, so applying the same merge twice is a no-op. */
72
+ export function mergeRollupContent(previous, incoming) {
73
+ const byWhat = new Map();
74
+ for (const d of [...previous.decisions, ...incoming.decisions]) {
75
+ // Sanitize-normalized key. `previous` was read back from the store and
76
+ // is therefore already escaped (Rule 3 runs in write() below), while
77
+ // `incoming` is raw summarizer output. Keying on the raw text would
78
+ // make the same decision hash two different ways across the sanitize
79
+ // boundary and defeat the dedupe that makes this merge idempotent.
80
+ const key = escapeShellSyntax(d.what).trim().toLowerCase();
81
+ const seen = byWhat.get(key);
82
+ if (!seen) {
83
+ byWhat.set(key, d);
84
+ continue;
85
+ }
86
+ // Same decision seen twice: keep the EARLIEST turn_index, because the
87
+ // first occurrence is when the choice was actually made - a later
88
+ // restatement is the model repeating itself, not a new decision. But
89
+ // prefer any provenance over none, so a pre-provenance row does not
90
+ // block a placed one.
91
+ if (seen.turnIndex === undefined && d.turnIndex !== undefined) {
92
+ byWhat.set(key, { ...seen, turnIndex: d.turnIndex, at: d.at });
93
+ }
94
+ else if (seen.turnIndex !== undefined && d.turnIndex !== undefined && d.turnIndex < seen.turnIndex) {
95
+ byWhat.set(key, { ...seen, turnIndex: d.turnIndex, at: d.at });
96
+ }
97
+ }
98
+ // pending_items come from the merge model, which is told to drop resolved
99
+ // ones - so incoming REPLACES rather than unions, otherwise a resolved item
100
+ // could never leave the list.
101
+ const pendingItems = [...new Set(incoming.pendingItems.map((p) => p.trim()).filter(Boolean))];
102
+ return {
103
+ // Stored in turn order, oldest first, so array position carries meaning
104
+ // and "the newest K" is a slice rather than a search.
105
+ decisions: [...byWhat.values()].sort((a, b) => (a.turnIndex ?? -1) - (b.turnIndex ?? -1)),
106
+ pendingItems,
107
+ filesTouched: dedupePaths([...previous.filesTouched, ...incoming.filesTouched]),
108
+ };
109
+ }
110
+ export class RollupStore {
111
+ db;
112
+ stmts;
113
+ constructor(db) {
114
+ this.db = db;
115
+ this.stmts = {
116
+ get: db.prepare('SELECT * FROM session_rollups WHERE session_id = ?'),
117
+ // Upsert, not a plain insert: this path serves both the first
118
+ // rollup and a rollup_version rebuild, where a row already exists
119
+ // and must be replaced wholesale rather than merged onto.
120
+ insert: db.prepare(`INSERT INTO session_rollups
121
+ (session_id, project_id, tool, title, summary, decisions, pending_items, files_touched,
122
+ turn_count, started_at, ended_at, kind, parent_session_id, summarizer_status,
123
+ rollup_state, rolled_up_through_turn_index, computed_at, rollup_version)
124
+ VALUES (@session_id, @project_id, @tool, @title, @summary, @decisions, @pending_items, @files_touched,
125
+ @turn_count, @started_at, @ended_at, @kind, @parent_session_id, @summarizer_status,
126
+ @rollup_state, @through, @now, @version)
127
+ ON CONFLICT (session_id) DO UPDATE SET
128
+ title = excluded.title, summary = excluded.summary, decisions = excluded.decisions,
129
+ pending_items = excluded.pending_items, files_touched = excluded.files_touched,
130
+ turn_count = excluded.turn_count, started_at = excluded.started_at, ended_at = excluded.ended_at,
131
+ kind = excluded.kind, parent_session_id = excluded.parent_session_id,
132
+ summarizer_status = excluded.summarizer_status,
133
+ rollup_state = excluded.rollup_state,
134
+ rolled_up_through_turn_index = excluded.rolled_up_through_turn_index,
135
+ computed_at = excluded.computed_at, rollup_version = excluded.rollup_version`),
136
+ // Conditional on the watermark the caller observed: a second apply
137
+ // of the same turns finds it already advanced and changes nothing.
138
+ updateIfWatermark: db.prepare(`UPDATE session_rollups SET
139
+ title = @title, summary = @summary, decisions = @decisions,
140
+ pending_items = @pending_items, files_touched = @files_touched,
141
+ turn_count = @turn_count, ended_at = @ended_at,
142
+ summarizer_status = @summarizer_status,
143
+ rollup_state = @rollup_state, rolled_up_through_turn_index = @through,
144
+ computed_at = @now, rollup_version = @version
145
+ WHERE session_id = @session_id AND rolled_up_through_turn_index = @expected`),
146
+ markLive: db.prepare(`UPDATE session_rollups SET rollup_state = 'live' WHERE session_id = ? AND rollup_state = 'final'`),
147
+ listByProject: db.prepare('SELECT * FROM session_rollups WHERE project_id = ? ORDER BY ended_at DESC'),
148
+ };
149
+ }
150
+ get(sessionId) {
151
+ return hydrate(this.stmts.get.get(sessionId));
152
+ }
153
+ /**
154
+ * Writes a rollup. `expectedThroughTurnIndex` is the watermark the caller
155
+ * read before computing; the update only lands if it hasn't moved since.
156
+ * Returns false when the write was rejected as already-applied - that is a
157
+ * normal outcome, not an error.
158
+ */
159
+ write(w, expectedThroughTurnIndex) {
160
+ // Security Rule 3 CHOKE POINT, not caller discipline: a caller can
161
+ // forget, a store cannot. Display strings are stripped (the
162
+ // metacharacter carries nothing there); decisions are escaped, because
163
+ // a decision may legitimately need to name the syntax it ruled out.
164
+ const params = {
165
+ session_id: w.sessionId,
166
+ project_id: w.projectId,
167
+ tool: w.tool,
168
+ title: stripShellSyntax(w.title),
169
+ summary: stripShellSyntax(w.summary),
170
+ decisions: JSON.stringify(w.decisions.map((d) => ({
171
+ what: escapeShellSyntax(d.what),
172
+ why: escapeShellSyntax(d.why),
173
+ ...(d.turnIndex !== undefined ? { turnIndex: d.turnIndex } : {}),
174
+ ...(d.at !== undefined ? { at: d.at } : {}),
175
+ }))),
176
+ pending_items: JSON.stringify(w.pendingItems.map(stripShellSyntax)),
177
+ files_touched: JSON.stringify(dedupePaths(w.filesTouched)),
178
+ turn_count: w.turnCount,
179
+ started_at: w.startedAt,
180
+ ended_at: w.endedAt,
181
+ kind: w.kind,
182
+ parent_session_id: w.parentSessionId,
183
+ summarizer_status: w.summarizerStatus,
184
+ rollup_state: w.state,
185
+ through: w.throughTurnIndex,
186
+ now: new Date().toISOString(),
187
+ version: w.rollupVersion ?? ROLLUP_VERSION,
188
+ };
189
+ const run = this.db.transaction(() => {
190
+ if (expectedThroughTurnIndex === undefined) {
191
+ this.stmts.insert.run(params);
192
+ return true;
193
+ }
194
+ return this.stmts.updateIfWatermark.run({ ...params, expected: expectedThroughTurnIndex }).changes > 0;
195
+ });
196
+ return run();
197
+ }
198
+ /** Reopened session: a final rollup returns to live so the next close recomputes it incrementally. */
199
+ markLive(sessionId) {
200
+ this.stmts.markLive.run(sessionId);
201
+ }
202
+ listByProject(projectId) {
203
+ return this.stmts.listByProject.all(projectId).map((r) => hydrate(r));
204
+ }
205
+ /**
206
+ * The listing view: top-level sessions with their sub-agent work attached,
207
+ * rather than sub-agents competing for space as peers. Sub-agents are 36% of
208
+ * Claude Code sessions locally (16 of 44) at a median of one turn each, so
209
+ * listing them flat buries the real sessions.
210
+ *
211
+ * Nothing is deleted from storage, and every child stays reachable through
212
+ * its parent. The served read model computes substance, so this
213
+ * legacy rollup listing no longer reads the inert stored column.
214
+ */
215
+ listSessions(projectId, options = {}) {
216
+ const all = this.listByProject(projectId);
217
+ const childrenByParent = new Map();
218
+ for (const r of all) {
219
+ if (r.parent_session_id == null) {
220
+ continue;
221
+ }
222
+ const bucket = childrenByParent.get(r.parent_session_id);
223
+ if (bucket) {
224
+ bucket.push(r);
225
+ }
226
+ else {
227
+ childrenByParent.set(r.parent_session_id, [r]);
228
+ }
229
+ }
230
+ const entries = [];
231
+ for (const rollup of all) {
232
+ const isChild = rollup.parent_session_id != null;
233
+ // A child whose parent is present in this project is surfaced under
234
+ // the parent. An orphaned child (parent never ingested, or in
235
+ // another project) is still listed on its own - hiding it would
236
+ // lose it entirely.
237
+ const parentPresent = isChild && all.some((r) => r.session_id === rollup.parent_session_id);
238
+ if (parentPresent && !options.includeAll) {
239
+ continue;
240
+ }
241
+ entries.push({ rollup, children: childrenByParent.get(rollup.session_id) ?? [] });
242
+ }
243
+ return entries;
244
+ }
245
+ }
246
+ //# sourceMappingURL=rollup-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rollup-store.js","sourceRoot":"","sources":["../../src/storage/rollup-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /** Reports legacy project rows whose current repository identity can be recovered without writing. */
2
+ export function planRootCommitBackfill(db, resolveGitRoot, resolveGitRootCommit) {
3
+ const projects = db.prepare('SELECT id, path, git_root FROM projects WHERE git_root_commit IS NULL ORDER BY id').all();
4
+ const changes = [];
5
+ let projectsUnresolvable = 0;
6
+ for (const project of projects) {
7
+ const gitRoot = project.git_root || resolveGitRoot(project.path);
8
+ const gitRootCommit = gitRoot ? resolveGitRootCommit(gitRoot) : null;
9
+ if (gitRootCommit === null) {
10
+ projectsUnresolvable++;
11
+ continue;
12
+ }
13
+ changes.push({ projectId: project.id, path: project.path, gitRootCommit });
14
+ }
15
+ return { changes, projectsScanned: projects.length, projectsUnresolvable, projectsSkippedConcurrent: 0 };
16
+ }
17
+ /** Fills only project rows that still need the identity captured by the fresh plan. */
18
+ export function applyRootCommitBackfill(db, resolveGitRoot, resolveGitRootCommit) {
19
+ const plan = planRootCommitBackfill(db, resolveGitRoot, resolveGitRootCommit);
20
+ const apply = db.transaction((changes) => {
21
+ let projectsSkippedConcurrent = 0;
22
+ const update = db.prepare('UPDATE projects SET git_root_commit = ? WHERE id = ? AND git_root_commit IS NULL');
23
+ for (const change of changes) {
24
+ if (update.run(change.gitRootCommit, change.projectId).changes === 0) {
25
+ projectsSkippedConcurrent++;
26
+ }
27
+ }
28
+ return projectsSkippedConcurrent;
29
+ });
30
+ return { ...plan, projectsSkippedConcurrent: apply(plan.changes) };
31
+ }
32
+ //# sourceMappingURL=root-commit-backfill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-commit-backfill.js","sourceRoot":"","sources":["../../src/storage/root-commit-backfill.ts"],"names":[],"mappings":"AAwBA,sGAAsG;AACtG,MAAM,UAAU,sBAAsB,CAClC,EAAY,EACZ,cAA8B,EAC9B,oBAA0C;IAE1C,MAAM,QAAQ,GAAG,EAAE,CAAC,OAAO,CAAC,mFAAmF,CAAC,CAAC,GAAG,EAAmB,CAAC;IACxI,MAAM,OAAO,GAA+B,EAAE,CAAC;IAC/C,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAE7B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,IAAI,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACjE,MAAM,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACrE,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;YACzB,oBAAoB,EAAE,CAAC;YACvB,SAAS;QACb,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,MAAM,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,CAAC,EAAE,CAAC;AAC7G,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,uBAAuB,CACnC,EAAY,EACZ,cAA8B,EAC9B,oBAA0C;IAE1C,MAAM,IAAI,GAAG,sBAAsB,CAAC,EAAE,EAAE,cAAc,EAAE,oBAAoB,CAAC,CAAC;IAC9E,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,OAAmC,EAAE,EAAE;QACjE,IAAI,yBAAyB,GAAG,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,kFAAkF,CAAC,CAAC;QAC9G,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;gBACnE,yBAAyB,EAAE,CAAC;YAChC,CAAC;QACL,CAAC;QACD,OAAO,yBAAyB,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,GAAG,IAAI,EAAE,yBAAyB,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;AACvE,CAAC"}
@@ -0,0 +1,184 @@
1
+ // Security Rule 3 backfill. The choke points in RollupStore.write() and
2
+ // MemoryStore.recordTurn/reingestTurn() clean everything written from now on;
3
+ // this cleans what the store already holds.
4
+ //
5
+ // Rule 3 debt is real, not hypothetical - the live corpus has rollup rows and
6
+ // memory rows carrying backticks and `$(`. Benign today (markdown-ish quoting
7
+ // of config keys), which is exactly the point: the store holds shell-active
8
+ // syntax and until now nothing checked it.
9
+ //
10
+ // Destructive-operation shape: preview the affected rows with
11
+ // before/after text (the list, not just a count - aggregates hide
12
+ // misclassification), back up, run in one transaction, then re-run the
13
+ // detector store-wide and fail loudly if anything survives.
14
+ //
15
+ // files_touched is deliberately NOT sanitized. Those paths are computed
16
+ // deterministically from tool-call arguments, not emitted by the summarizer,
17
+ // and escaping a metacharacter that is genuinely part of a filename would
18
+ // corrupt the path rather than protect anything. A pathological filename is
19
+ // caught instead by the read-time assertion when it reaches an assembled
20
+ // brief.
21
+ import { detectShellSyntax, escapeShellSyntax, stripShellSyntax } from '../security/sanitize.js';
22
+ function mapJsonField(raw, mapper) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(raw);
26
+ }
27
+ catch {
28
+ // A field that does not parse cannot be field-mapped, but it is still
29
+ // stored text served to a model - sanitize the whole blob as a display
30
+ // string rather than leaving it alone. Silently skipping is the
31
+ // failure mode this codebase already paid for.
32
+ return stripShellSyntax(raw);
33
+ }
34
+ return JSON.stringify(mapper(parsed));
35
+ }
36
+ const sanitizeDecisionsJson = (parsed) => {
37
+ if (!Array.isArray(parsed)) {
38
+ return parsed;
39
+ }
40
+ // memories holds string[]; session_rollups holds {what, why}[]. Both are
41
+ // the escape policy - a decision may need to name the syntax it ruled out.
42
+ return parsed.map((d) => {
43
+ if (typeof d === 'string') {
44
+ return escapeShellSyntax(d);
45
+ }
46
+ if (d && typeof d === 'object' && 'what' in d) {
47
+ const rec = d;
48
+ return {
49
+ ...d,
50
+ what: typeof rec.what === 'string' ? escapeShellSyntax(rec.what) : rec.what,
51
+ why: typeof rec.why === 'string' ? escapeShellSyntax(rec.why) : rec.why,
52
+ };
53
+ }
54
+ return d;
55
+ });
56
+ };
57
+ const sanitizeStringsJson = (parsed) => Array.isArray(parsed) ? parsed.map((s) => (typeof s === 'string' ? stripShellSyntax(s) : s)) : parsed;
58
+ export function sanitizeRollupDisplayField(raw) {
59
+ return stripShellSyntax(raw);
60
+ }
61
+ export function sanitizeRollupDecisionsField(raw) {
62
+ return mapJsonField(raw, sanitizeDecisionsJson);
63
+ }
64
+ export function sanitizeRollupPendingItemsField(raw) {
65
+ return mapJsonField(raw, sanitizeStringsJson);
66
+ }
67
+ const ROLLUP_FIELDS = [
68
+ { field: 'title', json: false, transform: sanitizeRollupDisplayField },
69
+ { field: 'summary', json: false, transform: sanitizeRollupDisplayField },
70
+ { field: 'decisions', json: true, transform: sanitizeRollupDecisionsField },
71
+ { field: 'pending_items', json: true, transform: sanitizeRollupPendingItemsField },
72
+ ];
73
+ const MEMORY_FIELDS = [
74
+ { field: 'decisions', json: true, transform: (raw) => mapJsonField(raw, sanitizeDecisionsJson) },
75
+ { field: 'pending_items', json: true, transform: (raw) => mapJsonField(raw, sanitizeStringsJson) },
76
+ ];
77
+ function collect(db, table, idColumn, fields) {
78
+ const columns = fields.map((f) => f.field).join(', ');
79
+ const rows = db.prepare(`SELECT ${idColumn} AS __id, ${columns} FROM ${table}`).all();
80
+ const changes = [];
81
+ for (const row of rows) {
82
+ for (const spec of fields) {
83
+ const before = row[spec.field];
84
+ if (typeof before !== 'string') {
85
+ continue;
86
+ }
87
+ const after = spec.transform(before);
88
+ if (after !== before) {
89
+ changes.push({ table, rowId: Number(row.__id), field: spec.field, before, after });
90
+ }
91
+ }
92
+ }
93
+ return changes;
94
+ }
95
+ /** What the backfill would change, without changing anything. */
96
+ export function planSanitize(db) {
97
+ const changes = [...collect(db, 'session_rollups', 'session_id', ROLLUP_FIELDS), ...collect(db, 'memories', 'id', MEMORY_FIELDS)];
98
+ return {
99
+ changes,
100
+ rollupRows: new Set(changes.filter((c) => c.table === 'session_rollups').map((c) => c.rowId)).size,
101
+ memoryRows: new Set(changes.filter((c) => c.table === 'memories').map((c) => c.rowId)).size,
102
+ };
103
+ }
104
+ /** Applies planSanitize's plan in a single transaction. Returns the plan that was applied, for reporting. */
105
+ export function applySanitize(db) {
106
+ const plan = planSanitize(db);
107
+ const apply = db.transaction(() => {
108
+ for (const c of plan.changes) {
109
+ const idColumn = c.table === 'session_rollups' ? 'session_id' : 'id';
110
+ db.prepare(`UPDATE ${c.table} SET ${c.field} = ? WHERE ${idColumn} = ?`).run(c.after, c.rowId);
111
+ }
112
+ });
113
+ apply();
114
+ return plan;
115
+ }
116
+ /**
117
+ * Post-backfill verification: re-run the detector over every sanitized field
118
+ * in the store. This is the check that turns "the store never holds executable
119
+ * syntax" from a comment into an invariant, so it re-reads from SQL rather
120
+ * than trusting the plan it just applied.
121
+ */
122
+ export function verifySanitize(db) {
123
+ const residue = [];
124
+ const check = (table, idColumn, fields) => {
125
+ const columns = fields.map((f) => f.field).join(', ');
126
+ const rows = db.prepare(`SELECT ${idColumn} AS __id, ${columns} FROM ${table}`).all();
127
+ for (const row of rows) {
128
+ for (const spec of fields) {
129
+ const raw = row[spec.field];
130
+ if (typeof raw !== 'string') {
131
+ continue;
132
+ }
133
+ for (const value of leafStrings(raw, spec.json)) {
134
+ if (detectShellSyntax(value)) {
135
+ residue.push({ table, rowId: Number(row.__id), field: spec.field, text: value });
136
+ }
137
+ }
138
+ }
139
+ }
140
+ };
141
+ check('session_rollups', 'session_id', ROLLUP_FIELDS);
142
+ check('memories', 'id', MEMORY_FIELDS);
143
+ return residue;
144
+ }
145
+ /**
146
+ * The stored strings inside a field, JSON-decoded where the field is JSON.
147
+ *
148
+ * Running the detector on the raw column text instead would report false
149
+ * positives: JSON escapes a backslash as `\\`, so a correctly escaped
150
+ * `` \` `` is stored as `` \\` ``, whose backslash parity reads as
151
+ * "unescaped backtick" to the detector. The invariant is about the values the
152
+ * store hands out, not their transport encoding.
153
+ */
154
+ function leafStrings(raw, isJson) {
155
+ if (!isJson) {
156
+ return [raw];
157
+ }
158
+ let parsed;
159
+ try {
160
+ parsed = JSON.parse(raw);
161
+ }
162
+ catch {
163
+ return [raw]; // unparseable JSON is still stored text; check it as-is
164
+ }
165
+ const out = [];
166
+ const walk = (node) => {
167
+ if (typeof node === 'string') {
168
+ out.push(node);
169
+ }
170
+ else if (Array.isArray(node)) {
171
+ for (const child of node) {
172
+ walk(child);
173
+ }
174
+ }
175
+ else if (node && typeof node === 'object') {
176
+ for (const child of Object.values(node)) {
177
+ walk(child);
178
+ }
179
+ }
180
+ };
181
+ walk(parsed);
182
+ return out;
183
+ }
184
+ //# sourceMappingURL=sanitize-backfill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-backfill.js","sourceRoot":"","sources":["../../src/storage/sanitize-backfill.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ // Segmentation boundary evaluator. Pure decision function - no I/O, no DB
2
+ // access. Callers assemble SegmentBoundaryInput from denormalized
3
+ // trailing_branch/trailing_files columns (see MemoryStore.updateTrailingState)
4
+ // plus the resuming turn's gitBranch/files_touched/resumeMarkerBefore, keeping
5
+ // the decision to one row read per closed turn.
6
+ //
7
+ // - gap >= 7 days -> cut unconditionally; overlap is diagnostically unavailable.
8
+ // - below 7 days, files_touched overlap >= 0.5 vetoes a cut.
9
+ // - otherwise, gap >= 4h AND discontinuity evidence -> cut. Evidence: branch
10
+ // differs, OR files_touched overlap below threshold, OR a Codex resume
11
+ // marker (<environment_context>) immediately preceded the
12
+ // resuming turn. Known-equal branches suppress file overlap when it is the
13
+ // only evidence.
14
+ // - gap >= 4h with no evidence -> no cut (resume-tomorrow guard).
15
+ // - gap < 4h -> never cut, regardless of evidence. The marker does not
16
+ // override this - a 36-second restart to change --model is a real
17
+ // process reattachment inside one episode, not an episode boundary.
18
+ import { SEGMENT_FILE_CONTINUITY_THRESHOLD, SEGMENT_FILE_OVERLAP_THRESHOLD, SEGMENT_MIN_GAP_HOURS, SEGMENT_UNCONDITIONAL_GAP_HOURS, } from '../config/constants.js';
19
+ /**
20
+ * Jaccard overlap (intersection / union) of the two file sets. Returns null
21
+ * when either side is empty - that is "unavailable", never "differs". With
22
+ * nothing on one side there is nothing to compare; treating the arithmetic
23
+ * zero as maximum divergence would turn missing file data into cut evidence,
24
+ * especially for Codex where most turns carry no paths.
25
+ */
26
+ export function fileOverlapRatio(trailingFiles, resumingFiles) {
27
+ if (trailingFiles.length === 0 || resumingFiles.length === 0) {
28
+ return null;
29
+ }
30
+ const trailing = new Set(trailingFiles);
31
+ const resuming = new Set(resumingFiles);
32
+ let intersection = 0;
33
+ for (const f of trailing) {
34
+ if (resuming.has(f)) {
35
+ intersection++;
36
+ }
37
+ }
38
+ const union = new Set([...trailing, ...resuming]).size;
39
+ return union === 0 ? null : intersection / union;
40
+ }
41
+ /**
42
+ * True if branch is known on both sides and they differ. An unknown branch on
43
+ * either side is never treated as "differs" - Codex has no per-turn branch
44
+ * capture (see corpus-discriminators), and the Codex fallback rule ("no cut
45
+ * below 7 days when no evidence exists") depends on that absence not being
46
+ * misread as a positive discontinuity signal.
47
+ */
48
+ function branchDiffers(trailingBranch, resumingBranch) {
49
+ return trailingBranch !== null && resumingBranch !== null && trailingBranch !== resumingBranch;
50
+ }
51
+ /**
52
+ * Diagnostic companion to evaluateSegmentBoundary(). Migration previews need
53
+ * to show why each proposed cut fired, not only the aggregate count. The
54
+ * evaluator delegates to this function so preview evidence cannot drift from
55
+ * the live decision rule.
56
+ */
57
+ export function assessSegmentBoundary(input) {
58
+ if (input.gapHours >= SEGMENT_UNCONDITIONAL_GAP_HOURS) {
59
+ return { cut: true, evidence: ['seven-day-gap'], fileOverlap: null };
60
+ }
61
+ const overlap = fileOverlapRatio(input.trailingFiles, input.resumingFiles);
62
+ if (input.gapHours < SEGMENT_MIN_GAP_HOURS) {
63
+ return { cut: false, evidence: [], fileOverlap: overlap };
64
+ }
65
+ if (overlap !== null && overlap >= SEGMENT_FILE_CONTINUITY_THRESHOLD) {
66
+ return { cut: false, evidence: [], fileOverlap: overlap };
67
+ }
68
+ const evidence = [];
69
+ if (branchDiffers(input.trailingBranch, input.resumingBranch)) {
70
+ evidence.push('branch-change');
71
+ }
72
+ if (overlap !== null && overlap < SEGMENT_FILE_OVERLAP_THRESHOLD) {
73
+ evidence.push('file-overlap');
74
+ }
75
+ if (input.resumeMarkerBefore) {
76
+ evidence.push('resume-marker');
77
+ }
78
+ const branchesMatch = input.trailingBranch !== null && input.resumingBranch !== null && input.trailingBranch === input.resumingBranch;
79
+ if (branchesMatch && evidence.length === 1 && evidence[0] === 'file-overlap') {
80
+ return { cut: false, evidence: [], fileOverlap: overlap };
81
+ }
82
+ return { cut: evidence.length > 0, evidence, fileOverlap: overlap };
83
+ }
84
+ export function evaluateSegmentBoundary(input) {
85
+ return assessSegmentBoundary(input).cut;
86
+ }
87
+ //# sourceMappingURL=segmentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmentation.js","sourceRoot":"","sources":["../../src/storage/segmentation.ts"],"names":[],"mappings":"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"}