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,196 @@
1
+ // Backfill: re-derives surface/git_branch/kind/trailing_branch/
2
+ // trailing_files (sessions) and has_external_content (memories) for rows
3
+ // that predate capture-at-ingestion, from transcripts
4
+ // still on disk. Structural only - re-walks each session's file through its
5
+ // adapter's parseTurns(), which never calls a SummarizationProvider. This is
6
+ // NOT `elepha reingest`: no model call, no decisions/pending_items rewrite.
7
+ //
8
+ // Destructive-operation shape: preview (the list, not a
9
+ // count) -> --apply gate -> DB backup -> single transaction -> re-verify.
10
+ // A session whose source_path is outside its provider store, no longer exists,
11
+ // or exists but cannot actually be read (corrupted, permission-denied, deleted
12
+ // in a race after the readability check, a path pointing at a directory, ...), keeps every
13
+ // new field NULL - never a manufactured value. Both cases collapse into the
14
+ // same `transcriptMissing: true` outcome: the caller-facing behavior (NULL,
15
+ // shown in preview, exempted from the CLI verify check) is identical either
16
+ // way, and distinguishing "absent" from "present but unreadable" for
17
+ // reporting isn't worth the extra surface.
18
+ import { claudeCodeSurface, codexSurface, toSessionRowKind } from '../adapters/discriminators.js';
19
+ import { TRAILING_FILES_CAP } from '../config/constants.js';
20
+ import { dedupePaths, isReadableProviderSource } from '../config/paths.js';
21
+ import { applyBackfill, planBackfill } from './backfill-runner.js';
22
+ /** The NULL-preserving result: reused for both "doesn't exist" and "exists but couldn't actually be read". */
23
+ function unreadableResult(session) {
24
+ return {
25
+ transcriptMissing: true,
26
+ turns: [],
27
+ after: {
28
+ surface: session.surface,
29
+ git_branch: session.git_branch,
30
+ kind: session.kind,
31
+ trailing_branch: session.trailing_branch,
32
+ trailing_files: session.trailing_files,
33
+ },
34
+ };
35
+ }
36
+ async function deriveForSession(session, adapter) {
37
+ if (!isReadableProviderSource(session.tool, session.source_path)) {
38
+ return unreadableResult(session);
39
+ }
40
+ // The guarded file exists, but "exists" is not "readable":
41
+ // classifySession/parseTurns can still throw - EISDIR (source_path points
42
+ // at a directory), EACCES (permission denied), or a delete-after-exists
43
+ // race. base.ts's parseTurns opens the file with `fs.promises.open`
44
+ // outside any try/catch, so that throw is uncaught and would otherwise
45
+ // propagate out of this async generator loop, out of buildPlan's
46
+ // per-session loop, and abort the entire plan/apply run for every other
47
+ // session in the batch - not just this one. Treat it exactly like a
48
+ // missing file: NULL-preserving, reported, never a manufactured value.
49
+ let classification;
50
+ const turns = [];
51
+ try {
52
+ classification = await adapter.classifySession(session.source_path);
53
+ for await (const turn of adapter.parseTurns(session.source_path, undefined, { closeTrailingOnIdle: true })) {
54
+ turns.push(turn);
55
+ }
56
+ }
57
+ catch {
58
+ return unreadableResult(session);
59
+ }
60
+ const kind = toSessionRowKind(classification.kind);
61
+ const firstWithSurface = turns.find((t) => t.surface !== undefined);
62
+ const firstWithBranch = turns.find((t) => t.gitBranch !== undefined);
63
+ const lastWithBranch = [...turns].reverse().find((t) => t.gitBranch !== undefined);
64
+ const rawSurface = firstWithSurface?.surface;
65
+ const surface = session.tool === 'claude-code' ? claudeCodeSurface(rawSurface) : codexSurface(rawSurface);
66
+ // trailing_files: most-recent-first across the re-parsed turns, same cap
67
+ // and dedupe as the live updateTrailingState path (MemoryStore.recordTurn).
68
+ const filesNewestFirst = [...turns].reverse().flatMap((t) => t.toolCalls.flatMap((c) => c.filePaths));
69
+ const trailingFiles = dedupePaths(filesNewestFirst).slice(0, TRAILING_FILES_CAP);
70
+ return {
71
+ transcriptMissing: false,
72
+ turns,
73
+ after: {
74
+ surface,
75
+ git_branch: firstWithBranch?.gitBranch ?? null,
76
+ kind,
77
+ trailing_branch: lastWithBranch?.gitBranch ?? null,
78
+ trailing_files: JSON.stringify(trailingFiles),
79
+ },
80
+ };
81
+ }
82
+ const deriver = {
83
+ load(db) {
84
+ return {
85
+ sessions: db
86
+ .prepare('SELECT id, tool, native_id, source_path, surface, git_branch, kind, trailing_branch, trailing_files FROM sessions')
87
+ .all(),
88
+ state: undefined,
89
+ };
90
+ },
91
+ async derive({ db, adapters, session }) {
92
+ const { after, turns, transcriptMissing } = await deriveForSession(session, adapters[session.tool]);
93
+ const before = {
94
+ surface: session.surface,
95
+ git_branch: session.git_branch,
96
+ kind: session.kind,
97
+ trailing_branch: session.trailing_branch,
98
+ trailing_files: session.trailing_files,
99
+ };
100
+ let memoryFlagsChanged = 0;
101
+ if (!transcriptMissing) {
102
+ const existingMemories = db
103
+ .prepare('SELECT turn_index, has_external_content FROM memories WHERE session_id = ?')
104
+ .all(session.id);
105
+ const byIndex = new Map(existingMemories.map((memory) => [memory.turn_index, memory.has_external_content]));
106
+ for (const turn of turns) {
107
+ const want = turn.hasExternalContent ? 1 : 0;
108
+ const have = byIndex.get(turn.turnIndex);
109
+ if (have !== undefined && have !== want) {
110
+ memoryFlagsChanged++;
111
+ }
112
+ }
113
+ }
114
+ // transcriptMissing sessions are always reported, even though `after`
115
+ // mirrors `before` verbatim (there's nothing to derive without the
116
+ // file) - the operator needs visibility into "this session can never
117
+ // be backfilled from disk" as distinct from "nothing to do here".
118
+ // deriveForSession's NULL-preserving branch is what keeps the actual
119
+ // apply() a no-op for these rows; this only affects what gets listed.
120
+ const changed = before.surface !== after.surface ||
121
+ before.git_branch !== after.git_branch ||
122
+ before.kind !== after.kind ||
123
+ before.trailing_branch !== after.trailing_branch ||
124
+ before.trailing_files !== after.trailing_files ||
125
+ memoryFlagsChanged > 0 ||
126
+ transcriptMissing;
127
+ if (!changed) {
128
+ return undefined;
129
+ }
130
+ return {
131
+ sessionId: session.id,
132
+ nativeId: session.native_id,
133
+ tool: session.tool,
134
+ sourcePath: session.source_path,
135
+ transcriptMissing,
136
+ before,
137
+ after,
138
+ memoryFlagsChanged,
139
+ };
140
+ },
141
+ shouldWrite: () => true,
142
+ write(db, change) {
143
+ db.prepare('UPDATE sessions SET surface = ?, git_branch = ?, kind = ?, trailing_branch = ?, trailing_files = ? WHERE id = ?').run(change.after.surface, change.after.git_branch, change.after.kind, change.after.trailing_branch, change.after.trailing_files, change.sessionId);
144
+ return {};
145
+ },
146
+ async afterApply({ db, adapters, plan }) {
147
+ // has_external_content flips need the actual per-turn values, not just a
148
+ // count - re-derive per changed session inside its own pass rather than
149
+ // carrying every ParsedTurn through the plan object (memory footprint on
150
+ // a large corpus). Still one DB transaction per session's memory rows.
151
+ for (const change of plan.changes) {
152
+ if (change.memoryFlagsChanged === 0 || change.transcriptMissing) {
153
+ continue;
154
+ }
155
+ const session = db
156
+ .prepare('SELECT id, tool, native_id, source_path FROM sessions WHERE id = ?')
157
+ .get(change.sessionId);
158
+ const adapter = adapters[session.tool];
159
+ const updateFlag = db.prepare('UPDATE memories SET has_external_content = ? WHERE session_id = ? AND turn_index = ?');
160
+ const writeFlags = db.transaction((turns) => {
161
+ for (const turn of turns) {
162
+ updateFlag.run(turn.hasExternalContent ? 1 : 0, change.sessionId, turn.turnIndex);
163
+ }
164
+ });
165
+ // Same failure mode as deriveForSession's parseTurns call - a file
166
+ // that became unreadable between phase 1 (buildPlan, above) and this
167
+ // second pass (a delete/permission race, or a directory path) must
168
+ // not abort the rest of the batch. The sessions-table fields for this
169
+ // session were already written by apply() above; skipping just this
170
+ // session's has_external_content flips leaves them stale rather than
171
+ // manufacturing anything.
172
+ const turns = [];
173
+ if (!isReadableProviderSource(session.tool, session.source_path)) {
174
+ continue;
175
+ }
176
+ try {
177
+ for await (const turn of adapter.parseTurns(session.source_path, undefined, { closeTrailingOnIdle: true })) {
178
+ turns.push(turn);
179
+ }
180
+ }
181
+ catch {
182
+ continue;
183
+ }
184
+ writeFlags(turns);
185
+ }
186
+ },
187
+ };
188
+ /** What the backfill would change, without changing anything. */
189
+ export async function planSessionFieldsBackfill(db, adapters) {
190
+ return planBackfill(db, adapters, deriver);
191
+ }
192
+ /** Applies the plan in a single transaction. Re-derives turns a second time inside the transaction closure is avoided by computing the plan first, then writing it - matches planPurge/applyPurge's shape. */
193
+ export async function applySessionFieldsBackfill(db, adapters) {
194
+ return applyBackfill(db, adapters, deriver);
195
+ }
196
+ //# sourceMappingURL=session-fields-backfill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-fields-backfill.js","sourceRoot":"","sources":["../../src/storage/session-fields-backfill.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ // Canonical hydrated read model for served sessions. SQL storage details stay
2
+ // here so serving consumers share one shape instead of re-declaring row types.
3
+ const warnedDegradedFields = new WeakMap();
4
+ export function safeStringArray(value, onDropped) {
5
+ try {
6
+ const parsed = JSON.parse(value);
7
+ if (!Array.isArray(parsed)) {
8
+ onDropped?.();
9
+ return [];
10
+ }
11
+ const strings = parsed.filter((item) => typeof item === 'string');
12
+ if (strings.length !== parsed.length) {
13
+ onDropped?.();
14
+ }
15
+ return strings;
16
+ }
17
+ catch {
18
+ onDropped?.();
19
+ return [];
20
+ }
21
+ }
22
+ function warnDegradedField(db, sessionId, column) {
23
+ let warned = warnedDegradedFields.get(db);
24
+ if (warned === undefined) {
25
+ warned = new Set();
26
+ warnedDegradedFields.set(db, warned);
27
+ }
28
+ const key = `${sessionId}:${column}`;
29
+ if (warned.has(key)) {
30
+ return;
31
+ }
32
+ warned.add(key);
33
+ console.warn(`[elepha] degraded session read: session ${sessionId} ${column} contained invalid data; dropped invalid values`);
34
+ }
35
+ function hydrateServedSession(db, row) {
36
+ return {
37
+ ...row,
38
+ trailing_files: safeStringArray(row.trailing_files, () => warnDegradedField(db, row.id, 'trailing_files')),
39
+ };
40
+ }
41
+ export function jsonArrayLength(value) {
42
+ if (value === null) {
43
+ return null;
44
+ }
45
+ try {
46
+ const decoded = JSON.parse(value);
47
+ return Array.isArray(decoded) ? decoded.length : 0;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /**
54
+ * Canonical read-time substantive predicate. A rollup with decisions
55
+ * or files is substantive; otherwise stored capture is substantive from two
56
+ * turns onward or when a turn touched files.
57
+ */
58
+ export function isSubstantive(session) {
59
+ if (session.rollup_state !== null) {
60
+ return (jsonArrayLength(session.rollup_decisions) ?? 0) > 0 || session.has_files_touched === 1;
61
+ }
62
+ return session.turn_count >= 2 || session.has_files_touched === 1;
63
+ }
64
+ // One SELECT shared by every served-session reader: a narrow lookup that
65
+ // diverged from the project query would silently split the canonical shape.
66
+ const SERVED_SESSION_SELECT = `SELECT s.*, r.title AS rollup_title, r.summary AS rollup_summary, r.decisions AS rollup_decisions,
67
+ r.pending_items AS rollup_pending_items, r.files_touched AS rollup_files_touched, r.rollup_state,
68
+ COUNT(m.id) AS turn_count, MAX(CASE WHEN m.files_touched <> '[]' THEN 1 ELSE 0 END) AS has_files_touched,
69
+ MAX(CASE WHEN m.has_external_content = 1 THEN 1 ELSE 0 END) AS has_external_content
70
+ FROM sessions s LEFT JOIN session_rollups r ON r.session_id = s.id LEFT JOIN memories m ON m.session_id = s.id`;
71
+ /** The single project-session query used by serving readers, newest activity first. */
72
+ export function readProjectSessions(db, projectIds) {
73
+ const placeholders = projectIds.map(() => '?').join(',');
74
+ const rows = db
75
+ .prepare(`${SERVED_SESSION_SELECT}
76
+ WHERE s.project_id IN (${placeholders}) GROUP BY s.id
77
+ ORDER BY COALESCE(s.last_turn_at, s.last_ingested_at, s.started_at) DESC, s.id DESC`)
78
+ .all(...projectIds);
79
+ return rows.map((row) => hydrateServedSession(db, row));
80
+ }
81
+ /** One grouped read for list-project aggregate inputs, ordered like the canonical session list. */
82
+ export function readProjectSessionAggregates(db, projectIds) {
83
+ if (projectIds.length === 0) {
84
+ return [];
85
+ }
86
+ const placeholders = projectIds.map(() => '?').join(',');
87
+ const rows = db
88
+ .prepare(`WITH ranked AS (
89
+ SELECT s.project_id, s.tool, s.surface, s.last_ingested_at,
90
+ COALESCE(s.last_turn_at, s.last_ingested_at, s.started_at) AS activity, s.id,
91
+ ROW_NUMBER() OVER (
92
+ PARTITION BY s.project_id, s.tool, s.surface
93
+ ORDER BY COALESCE(s.last_turn_at, s.last_ingested_at, s.started_at) DESC, s.id DESC
94
+ ) AS activity_rank
95
+ FROM sessions s
96
+ WHERE s.project_id IN (${placeholders})
97
+ )
98
+ SELECT project_id, tool, surface, MAX(last_ingested_at) AS last_ingested_at,
99
+ COUNT(*) AS work_episodes,
100
+ MAX(CASE WHEN activity_rank = 1 THEN activity END) AS newest_activity,
101
+ MAX(CASE WHEN activity_rank = 1 THEN id END) AS newest_id
102
+ FROM ranked
103
+ GROUP BY project_id, tool, surface
104
+ ORDER BY newest_activity DESC, newest_id DESC`)
105
+ .all(...projectIds);
106
+ return rows.map(({ project_id, tool, surface, last_ingested_at, work_episodes }) => ({
107
+ project_id,
108
+ tool,
109
+ surface,
110
+ last_ingested_at,
111
+ work_episodes,
112
+ }));
113
+ }
114
+ /** Indexed session-id lookup sharing the exact hydrated shape used by project reads. */
115
+ export function readSessionById(db, id) {
116
+ const row = db.prepare(`${SERVED_SESSION_SELECT} WHERE s.id = ? GROUP BY s.id`).get(id);
117
+ return row === undefined ? undefined : hydrateServedSession(db, row);
118
+ }
119
+ /**
120
+ * Indexed lookup on sessions.UNIQUE(tool, native_id, segment_index), for
121
+ * callers that need exactly one session. Consent-independent by design; the
122
+ * caller owns any consent gate.
123
+ */
124
+ export function readSessionByNaturalKey(db, key) {
125
+ const row = db
126
+ .prepare(`${SERVED_SESSION_SELECT} WHERE s.tool = ? AND s.native_id = ? AND s.segment_index = ? GROUP BY s.id`)
127
+ .get(key.tool, key.nativeId, key.segmentIndex);
128
+ return row === undefined ? undefined : hydrateServedSession(db, row);
129
+ }
130
+ //# sourceMappingURL=session-read-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-read-model.js","sourceRoot":"","sources":["../../src/storage/session-read-model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,200 @@
1
+ import { TRAILING_FILES_CAP } from '../config/constants.js';
2
+ import { dedupePaths } from '../config/paths.js';
3
+ import { gitRevListCountHead } from '../security/subprocess-allowlist.js';
4
+ import { titleForTurn } from './session-title.js';
5
+ /**
6
+ * `trailing_files` is stored as JSON text in SQLite but exposed as
7
+ * `string[]` on SessionRow, matching how MemoryRow.decisions/files_touched
8
+ * already hydrate JSON columns (see listMemoriesForSession). Every read path
9
+ * for SessionRow must route through this.
10
+ */
11
+ export function hydrateSessionRow(raw) {
12
+ return {
13
+ ...raw,
14
+ trailing_files: typeof raw.trailing_files === 'string' ? JSON.parse(raw.trailing_files) : [],
15
+ };
16
+ }
17
+ /** Recent-files window kept on sessions.trailing_files: boundary evaluation needs "recent enough to signal overlap", not full history. */
18
+ export class SessionStore {
19
+ db;
20
+ getProjectById;
21
+ resolveGitCommitCount;
22
+ stmts;
23
+ constructor(db, getProjectById, resolveGitCommitCount = gitRevListCountHead) {
24
+ this.db = db;
25
+ this.getProjectById = getProjectById;
26
+ this.resolveGitCommitCount = resolveGitCommitCount;
27
+ this.stmts = {
28
+ findSession: db.prepare('SELECT * FROM sessions WHERE tool = ? AND native_id = ? ORDER BY segment_index DESC LIMIT 1'),
29
+ findSessionSegment: db.prepare('SELECT * FROM sessions WHERE tool = ? AND native_id = ? AND segment_index = ?'),
30
+ insertSession: db.prepare(`INSERT INTO sessions (tool, native_id, segment_index, project_id, source_path, cursor, started_at, last_ingested_at, surface, git_branch, kind, last_turn_at, trailing_branch, trailing_files, title, custom_title, git_commit_count)
31
+ VALUES (@tool, @native_id, @segment_index, @project_id, @source_path, NULL, @now, @now, @surface, @git_branch, @kind, NULL, NULL, '[]', NULL, @custom_title, @git_commit_count)`),
32
+ updateSessionCursor: db.prepare('UPDATE sessions SET cursor = ?, last_ingested_at = ? WHERE id = ?'),
33
+ listSessionsWithMemoriesSince: db.prepare(`SELECT DISTINCT s.* FROM sessions s
34
+ JOIN memories m ON m.session_id = s.id
35
+ WHERE m.turn_started_at >= ?`),
36
+ };
37
+ }
38
+ upsertSession(tool, nativeId, projectId, sourcePath, meta, gitCommitCount) {
39
+ const existing = this.stmts.findSession.get(tool, nativeId);
40
+ if (existing) {
41
+ if (meta?.customTitle !== undefined) {
42
+ this.updateCustomTitle(tool, nativeId, meta.customTitle);
43
+ return { ...hydrateSessionRow(existing), custom_title: meta.customTitle };
44
+ }
45
+ return hydrateSessionRow(existing);
46
+ }
47
+ const now = new Date().toISOString();
48
+ const project = this.getProjectById(projectId);
49
+ const info = this.stmts.insertSession.run({
50
+ tool,
51
+ native_id: nativeId,
52
+ project_id: projectId,
53
+ source_path: sourcePath,
54
+ segment_index: 0,
55
+ now,
56
+ surface: meta?.surface ?? null,
57
+ git_branch: meta?.gitBranch ?? null,
58
+ kind: meta?.kind ?? null,
59
+ custom_title: meta?.customTitle ?? null,
60
+ git_commit_count: gitCommitCount !== undefined
61
+ ? gitCommitCount
62
+ : project === undefined
63
+ ? null
64
+ : this.resolveGitCommitCount(project.git_root ?? project.path),
65
+ });
66
+ return hydrateSessionRow(this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(info.lastInsertRowid));
67
+ }
68
+ findSession(tool, nativeId) {
69
+ const row = this.stmts.findSession.get(tool, nativeId);
70
+ return row ? hydrateSessionRow(row) : undefined;
71
+ }
72
+ /** User-set custom titles remain native-transcript metadata; list_sessions uses the per-segment title field. */
73
+ updateCustomTitle(tool, nativeId, customTitle) {
74
+ this.db.prepare('UPDATE sessions SET custom_title = ? WHERE tool = ? AND native_id = ?').run(customTitle, tool, nativeId);
75
+ }
76
+ /**
77
+ * Opens the segment immediately after `previous`. Its trailing window and
78
+ * cursor start empty by construction; recordTurn() fills them only after
79
+ * the resuming turn has actually been stored. The exact-index lookup makes
80
+ * this safe if an already-decided cut is retried after a partial failure.
81
+ */
82
+ startNextSegment(previous, projectId, sourcePath, meta, gitCommitCount) {
83
+ const segmentIndex = previous.segment_index + 1;
84
+ const existing = this.stmts.findSessionSegment.get(previous.tool, previous.native_id, segmentIndex);
85
+ if (existing) {
86
+ return hydrateSessionRow(existing);
87
+ }
88
+ const now = new Date().toISOString();
89
+ const project = this.getProjectById(projectId);
90
+ const info = this.stmts.insertSession.run({
91
+ tool: previous.tool,
92
+ native_id: previous.native_id,
93
+ segment_index: segmentIndex,
94
+ project_id: projectId,
95
+ source_path: sourcePath,
96
+ now,
97
+ surface: meta?.surface ?? null,
98
+ git_branch: meta?.gitBranch ?? null,
99
+ kind: meta?.kind ?? null,
100
+ custom_title: meta?.customTitle ?? previous.custom_title,
101
+ git_commit_count: gitCommitCount !== undefined
102
+ ? gitCommitCount
103
+ : project === undefined
104
+ ? null
105
+ : this.resolveGitCommitCount(project.git_root ?? project.path),
106
+ });
107
+ return hydrateSessionRow(this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(info.lastInsertRowid));
108
+ }
109
+ gitCommitCount(projectPath) {
110
+ return this.resolveGitCommitCount(projectPath);
111
+ }
112
+ /**
113
+ * Sessions whose rollup is missing or was written by an older
114
+ * `rollup_version` - the `elepha rollup --rebuild` target set.
115
+ *
116
+ * Distinct from listOpenSessions() on purpose: that one deliberately skips
117
+ * `final` rollups, which is right for the idle-close sweep and wrong for a
118
+ * version bump. A rebuild that silently cannot reach finalized rollups
119
+ * would rebuild the 14 rollups nobody cared about and leave the 62 that
120
+ * matter untouched, while reporting success.
121
+ */
122
+ listSessionsForRollupRebuild(currentVersion) {
123
+ return this.db
124
+ .prepare(`SELECT s.* FROM sessions s
125
+ LEFT JOIN session_rollups r ON r.session_id = s.id
126
+ WHERE r.session_id IS NULL OR r.rollup_version <> ?
127
+ ORDER BY s.id`)
128
+ .all(currentVersion)
129
+ .map((row) => hydrateSessionRow(row));
130
+ }
131
+ /** Sessions with no rollup yet, or whose rollup is still 'live' - the idle-close sweep candidates. */
132
+ listOpenSessions() {
133
+ return this.db
134
+ .prepare(`SELECT s.* FROM sessions s
135
+ LEFT JOIN session_rollups r ON r.session_id = s.id
136
+ WHERE r.session_id IS NULL OR r.rollup_state = 'live'`)
137
+ .all()
138
+ .map((row) => hydrateSessionRow(row));
139
+ }
140
+ getSessionCursor(tool, nativeId) {
141
+ const row = this.stmts.findSession.get(tool, nativeId);
142
+ return row ? (hydrateSessionRow(row).cursor ?? undefined) : undefined;
143
+ }
144
+ /** Advances an already-known session after a deliberately dropped turn. */
145
+ advanceSessionCursor(sessionDbId, cursor) {
146
+ this.advanceSessionCursorAt(sessionDbId, cursor, new Date().toISOString());
147
+ }
148
+ advanceSessionCursorAt(sessionDbId, cursor, now) {
149
+ this.stmts.updateSessionCursor.run(cursor, now, sessionDbId);
150
+ }
151
+ /** Quote-back runs before project/session persistence, so it can only advance an existing session. */
152
+ advanceExistingSessionCursor(tool, nativeId, cursor) {
153
+ const session = this.findSession(tool, nativeId);
154
+ if (!session) {
155
+ return false;
156
+ }
157
+ this.advanceSessionCursor(session.id, cursor);
158
+ return true;
159
+ }
160
+ /** Writes a sanitized title in the same transaction as the turn that establishes it. */
161
+ updateSessionTitle(sessionDbId, turn) {
162
+ const row = this.db.prepare('SELECT title, segment_index FROM sessions WHERE id = ?').get(sessionDbId);
163
+ if (!row) {
164
+ return;
165
+ }
166
+ const title = titleForTurn(row.title, turn, row.segment_index === 0);
167
+ if (title !== row.title) {
168
+ this.db.prepare('UPDATE sessions SET title = ? WHERE id = ?').run(title, sessionDbId);
169
+ }
170
+ }
171
+ /**
172
+ * Denormalized "recent state" on the session row, refreshed on every
173
+ * turn close: this exists
174
+ * so boundary evaluation is one row read per closed turn, not a query
175
+ * into turn history on a 200-turn rollout. trailing_branch is the raw
176
+ * per-turn value (drifts freely, unlike sessions.git_branch which is the
177
+ * creation-time anchor); trailing_files is capped, most-recent-first,
178
+ * case-insensitively deduped the same way memories.files_touched already is.
179
+ * last_turn_at is the prior-close timestamp used for the next gap test;
180
+ * unlike last_ingested_at it is transcript time, not daemon wall time.
181
+ */
182
+ updateTrailingState(sessionDbId, turn) {
183
+ const current = this.db.prepare('SELECT trailing_branch, trailing_files FROM sessions WHERE id = ?').get(sessionDbId);
184
+ if (!current) {
185
+ return;
186
+ }
187
+ const newBranch = turn.gitBranch ?? current.trailing_branch;
188
+ const existingFiles = JSON.parse(current.trailing_files);
189
+ const turnFiles = turn.toolCalls.flatMap((c) => c.filePaths);
190
+ const merged = dedupePaths([...turnFiles, ...existingFiles]).slice(0, TRAILING_FILES_CAP);
191
+ this.db
192
+ .prepare('UPDATE sessions SET last_turn_at = ?, trailing_branch = ?, trailing_files = ? WHERE id = ?')
193
+ .run(turn.endedAt, newBranch, JSON.stringify(merged), sessionDbId);
194
+ }
195
+ /** Sessions with at least one memory row in [sinceIso, now) - the reingest target set. Returned rows carry source_path + tool so the caller can re-walk each file with the right adapter. */
196
+ listSessionsWithMemoriesSince(sinceIso) {
197
+ return this.stmts.listSessionsWithMemoriesSince.all(sinceIso).map((row) => hydrateSessionRow(row));
198
+ }
199
+ }
200
+ //# sourceMappingURL=session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.js","sourceRoot":"","sources":["../../src/storage/session-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ // Re-derives the stored list_sessions title for every existing segment from
2
+ // its already-persisted turn indexes. This never changes segment boundaries,
3
+ // memories, rollups, rendered statistics, or custom-title metadata.
4
+ import { isReadableProviderSource } from '../config/paths.js';
5
+ import { applyBackfill, planBackfill } from './backfill-runner.js';
6
+ import { titleForSegment } from './session-title.js';
7
+ async function turnsForSession(db, session, adapter) {
8
+ if (!isReadableProviderSource(session.tool, session.source_path)) {
9
+ return undefined;
10
+ }
11
+ const indexes = new Set(db.prepare('SELECT turn_index FROM memories WHERE session_id = ? ORDER BY turn_index').all(session.id).map((row) => row.turn_index));
12
+ const turns = [];
13
+ try {
14
+ for await (const turn of adapter.parseTurns(session.source_path, undefined, { closeTrailingOnIdle: true })) {
15
+ if (indexes.has(turn.turnIndex)) {
16
+ turns.push(turn);
17
+ }
18
+ }
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ return turns;
24
+ }
25
+ const deriver = {
26
+ load(db) {
27
+ return {
28
+ sessions: db
29
+ .prepare('SELECT id, tool, native_id, source_path, segment_index, title FROM sessions ORDER BY id')
30
+ .all(),
31
+ state: undefined,
32
+ };
33
+ },
34
+ async derive({ db, adapters, session }) {
35
+ const turns = await turnsForSession(db, session, adapters[session.tool]);
36
+ if (turns === undefined) {
37
+ return {
38
+ sessionId: session.id,
39
+ nativeId: session.native_id,
40
+ tool: session.tool,
41
+ sourcePath: session.source_path,
42
+ before: session.title,
43
+ after: session.title,
44
+ transcriptMissing: true,
45
+ };
46
+ }
47
+ const after = titleForSegment(turns, session.segment_index === 0);
48
+ if (session.title === after) {
49
+ return undefined;
50
+ }
51
+ return {
52
+ sessionId: session.id,
53
+ nativeId: session.native_id,
54
+ tool: session.tool,
55
+ sourcePath: session.source_path,
56
+ before: session.title,
57
+ after,
58
+ transcriptMissing: false,
59
+ };
60
+ },
61
+ shouldWrite: (change) => !change.transcriptMissing,
62
+ write(db, change) {
63
+ db.prepare('UPDATE sessions SET title = ? WHERE id = ?').run(change.after, change.sessionId);
64
+ return {};
65
+ },
66
+ };
67
+ /** Shows title changes without writing. */
68
+ export async function planSessionTitleBackfill(db, adapters) {
69
+ return planBackfill(db, adapters, deriver);
70
+ }
71
+ /** Applies only readable sessions' derived titles in one transaction. */
72
+ export async function applySessionTitleBackfill(db, adapters) {
73
+ return applyBackfill(db, adapters, deriver);
74
+ }
75
+ //# sourceMappingURL=session-title-backfill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-title-backfill.js","sourceRoot":"","sources":["../../src/storage/session-title-backfill.ts"],"names":[],"mappings":"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"}