@pcircle/footprint 1.5.0 → 1.7.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 (249) hide show
  1. package/README.md +301 -132
  2. package/SKILL.md +72 -28
  3. package/bin/footprint.js +16 -0
  4. package/dist/src/adapters/claude.d.ts +2 -0
  5. package/dist/src/adapters/claude.d.ts.map +1 -0
  6. package/dist/src/adapters/claude.js +7 -0
  7. package/dist/src/adapters/claude.js.map +1 -0
  8. package/dist/src/adapters/codex.d.ts +2 -0
  9. package/dist/src/adapters/codex.d.ts.map +1 -0
  10. package/dist/src/adapters/codex.js +7 -0
  11. package/dist/src/adapters/codex.js.map +1 -0
  12. package/dist/src/adapters/gemini.d.ts +2 -0
  13. package/dist/src/adapters/gemini.d.ts.map +1 -0
  14. package/dist/src/adapters/gemini.js +7 -0
  15. package/dist/src/adapters/gemini.js.map +1 -0
  16. package/dist/src/adapters/index.d.ts +5 -0
  17. package/dist/src/adapters/index.d.ts.map +1 -0
  18. package/dist/src/adapters/index.js +12 -0
  19. package/dist/src/adapters/index.js.map +1 -0
  20. package/dist/src/adapters/structured-prefix.d.ts +10 -0
  21. package/dist/src/adapters/structured-prefix.d.ts.map +1 -0
  22. package/dist/src/adapters/structured-prefix.js +59 -0
  23. package/dist/src/adapters/structured-prefix.js.map +1 -0
  24. package/dist/src/adapters/types.d.ts +32 -0
  25. package/dist/src/adapters/types.d.ts.map +1 -0
  26. package/dist/src/adapters/types.js +2 -0
  27. package/dist/src/adapters/types.js.map +1 -0
  28. package/dist/src/cli/context-flow.d.ts +92 -0
  29. package/dist/src/cli/context-flow.d.ts.map +1 -0
  30. package/dist/src/cli/context-flow.js +724 -0
  31. package/dist/src/cli/context-flow.js.map +1 -0
  32. package/dist/src/cli/history-display.d.ts +27 -0
  33. package/dist/src/cli/history-display.d.ts.map +1 -0
  34. package/dist/src/cli/history-display.js +167 -0
  35. package/dist/src/cli/history-display.js.map +1 -0
  36. package/dist/src/cli/index.js +924 -0
  37. package/dist/src/cli/index.js.map +1 -1
  38. package/dist/src/cli/launch-spec.d.ts +31 -0
  39. package/dist/src/cli/launch-spec.d.ts.map +1 -0
  40. package/dist/src/cli/launch-spec.js +182 -0
  41. package/dist/src/cli/launch-spec.js.map +1 -0
  42. package/dist/src/cli/live-demo.d.ts +34 -0
  43. package/dist/src/cli/live-demo.d.ts.map +1 -0
  44. package/dist/src/cli/live-demo.js +254 -0
  45. package/dist/src/cli/live-demo.js.map +1 -0
  46. package/dist/src/cli/pty-transcript.d.ts +34 -0
  47. package/dist/src/cli/pty-transcript.d.ts.map +1 -0
  48. package/dist/src/cli/pty-transcript.js +174 -0
  49. package/dist/src/cli/pty-transcript.js.map +1 -0
  50. package/dist/src/cli/session-display.d.ts +74 -0
  51. package/dist/src/cli/session-display.d.ts.map +1 -0
  52. package/dist/src/cli/session-display.js +922 -0
  53. package/dist/src/cli/session-display.js.map +1 -0
  54. package/dist/src/cli/session-execution.d.ts +55 -0
  55. package/dist/src/cli/session-execution.d.ts.map +1 -0
  56. package/dist/src/cli/session-execution.js +817 -0
  57. package/dist/src/cli/session-execution.js.map +1 -0
  58. package/dist/src/cli/session-runtime.d.ts +5 -0
  59. package/dist/src/cli/session-runtime.d.ts.map +1 -0
  60. package/dist/src/cli/session-runtime.js +11 -0
  61. package/dist/src/cli/session-runtime.js.map +1 -0
  62. package/dist/src/cli/setup.d.ts.map +1 -1
  63. package/dist/src/cli/setup.js +2 -0
  64. package/dist/src/cli/setup.js.map +1 -1
  65. package/dist/src/index.d.ts +4 -0
  66. package/dist/src/index.d.ts.map +1 -1
  67. package/dist/src/index.js +148 -7
  68. package/dist/src/index.js.map +1 -1
  69. package/dist/src/ingestion/deterministic.d.ts +3 -0
  70. package/dist/src/ingestion/deterministic.d.ts.map +1 -0
  71. package/dist/src/ingestion/deterministic.js +862 -0
  72. package/dist/src/ingestion/deterministic.js.map +1 -0
  73. package/dist/src/ingestion/index.d.ts +5 -0
  74. package/dist/src/ingestion/index.d.ts.map +1 -0
  75. package/dist/src/ingestion/index.js +27 -0
  76. package/dist/src/ingestion/index.js.map +1 -0
  77. package/dist/src/ingestion/semantic.d.ts +6 -0
  78. package/dist/src/ingestion/semantic.d.ts.map +1 -0
  79. package/dist/src/ingestion/semantic.js +627 -0
  80. package/dist/src/ingestion/semantic.js.map +1 -0
  81. package/dist/src/ingestion/types.d.ts +10 -0
  82. package/dist/src/ingestion/types.d.ts.map +1 -0
  83. package/dist/src/ingestion/types.js +2 -0
  84. package/dist/src/ingestion/types.js.map +1 -0
  85. package/dist/src/lib/context-memory.d.ts +140 -0
  86. package/dist/src/lib/context-memory.d.ts.map +1 -0
  87. package/dist/src/lib/context-memory.js +974 -0
  88. package/dist/src/lib/context-memory.js.map +1 -0
  89. package/dist/src/lib/history-handoff.d.ts +43 -0
  90. package/dist/src/lib/history-handoff.d.ts.map +1 -0
  91. package/dist/src/lib/history-handoff.js +179 -0
  92. package/dist/src/lib/history-handoff.js.map +1 -0
  93. package/dist/src/lib/observability.d.ts +3 -0
  94. package/dist/src/lib/observability.d.ts.map +1 -0
  95. package/dist/src/lib/observability.js +63 -0
  96. package/dist/src/lib/observability.js.map +1 -0
  97. package/dist/src/lib/session-artifacts.d.ts +51 -0
  98. package/dist/src/lib/session-artifacts.d.ts.map +1 -0
  99. package/dist/src/lib/session-artifacts.js +132 -0
  100. package/dist/src/lib/session-artifacts.js.map +1 -0
  101. package/dist/src/lib/session-filters.d.ts +11 -0
  102. package/dist/src/lib/session-filters.d.ts.map +1 -0
  103. package/dist/src/lib/session-filters.js +16 -0
  104. package/dist/src/lib/session-filters.js.map +1 -0
  105. package/dist/src/lib/session-history.d.ts +50 -0
  106. package/dist/src/lib/session-history.d.ts.map +1 -0
  107. package/dist/src/lib/session-history.js +73 -0
  108. package/dist/src/lib/session-history.js.map +1 -0
  109. package/dist/src/lib/session-trends.d.ts +129 -0
  110. package/dist/src/lib/session-trends.d.ts.map +1 -0
  111. package/dist/src/lib/session-trends.js +361 -0
  112. package/dist/src/lib/session-trends.js.map +1 -0
  113. package/dist/src/lib/storage/database.d.ts +212 -1
  114. package/dist/src/lib/storage/database.d.ts.map +1 -1
  115. package/dist/src/lib/storage/database.js +1694 -114
  116. package/dist/src/lib/storage/database.js.map +1 -1
  117. package/dist/src/lib/storage/export-sessions.d.ts +33 -0
  118. package/dist/src/lib/storage/export-sessions.d.ts.map +1 -0
  119. package/dist/src/lib/storage/export-sessions.js +525 -0
  120. package/dist/src/lib/storage/export-sessions.js.map +1 -0
  121. package/dist/src/lib/storage/index.d.ts +7 -6
  122. package/dist/src/lib/storage/index.d.ts.map +1 -1
  123. package/dist/src/lib/storage/index.js +6 -5
  124. package/dist/src/lib/storage/index.js.map +1 -1
  125. package/dist/src/lib/storage/schema.d.ts +6 -1
  126. package/dist/src/lib/storage/schema.d.ts.map +1 -1
  127. package/dist/src/lib/storage/schema.js +337 -2
  128. package/dist/src/lib/storage/schema.js.map +1 -1
  129. package/dist/src/lib/storage/types.d.ts +122 -0
  130. package/dist/src/lib/storage/types.d.ts.map +1 -1
  131. package/dist/src/prompts/skill-prompt.d.ts.map +1 -1
  132. package/dist/src/prompts/skill-prompt.js +13 -0
  133. package/dist/src/prompts/skill-prompt.js.map +1 -1
  134. package/dist/src/tools/confirm-context-link.d.ts +62 -0
  135. package/dist/src/tools/confirm-context-link.d.ts.map +1 -0
  136. package/dist/src/tools/confirm-context-link.js +36 -0
  137. package/dist/src/tools/confirm-context-link.js.map +1 -0
  138. package/dist/src/tools/context-schemas.d.ts +694 -0
  139. package/dist/src/tools/context-schemas.d.ts.map +1 -0
  140. package/dist/src/tools/context-schemas.js +171 -0
  141. package/dist/src/tools/context-schemas.js.map +1 -0
  142. package/dist/src/tools/export-sessions.d.ts +111 -0
  143. package/dist/src/tools/export-sessions.d.ts.map +1 -0
  144. package/dist/src/tools/export-sessions.js +136 -0
  145. package/dist/src/tools/export-sessions.js.map +1 -0
  146. package/dist/src/tools/get-context.d.ts +208 -0
  147. package/dist/src/tools/get-context.d.ts.map +1 -0
  148. package/dist/src/tools/get-context.js +27 -0
  149. package/dist/src/tools/get-context.js.map +1 -0
  150. package/dist/src/tools/get-history-handoff.d.ts +109 -0
  151. package/dist/src/tools/get-history-handoff.d.ts.map +1 -0
  152. package/dist/src/tools/get-history-handoff.js +85 -0
  153. package/dist/src/tools/get-history-handoff.js.map +1 -0
  154. package/dist/src/tools/get-history-trends.d.ts +155 -0
  155. package/dist/src/tools/get-history-trends.d.ts.map +1 -0
  156. package/dist/src/tools/get-history-trends.js +123 -0
  157. package/dist/src/tools/get-history-trends.js.map +1 -0
  158. package/dist/src/tools/get-session-artifacts.d.ts +151 -0
  159. package/dist/src/tools/get-session-artifacts.d.ts.map +1 -0
  160. package/dist/src/tools/get-session-artifacts.js +184 -0
  161. package/dist/src/tools/get-session-artifacts.js.map +1 -0
  162. package/dist/src/tools/get-session-decisions.d.ts +69 -0
  163. package/dist/src/tools/get-session-decisions.d.ts.map +1 -0
  164. package/dist/src/tools/get-session-decisions.js +99 -0
  165. package/dist/src/tools/get-session-decisions.js.map +1 -0
  166. package/dist/src/tools/get-session-messages.d.ts +55 -0
  167. package/dist/src/tools/get-session-messages.d.ts.map +1 -0
  168. package/dist/src/tools/get-session-messages.js +89 -0
  169. package/dist/src/tools/get-session-messages.js.map +1 -0
  170. package/dist/src/tools/get-session-narrative.d.ts +72 -0
  171. package/dist/src/tools/get-session-narrative.d.ts.map +1 -0
  172. package/dist/src/tools/get-session-narrative.js +106 -0
  173. package/dist/src/tools/get-session-narrative.js.map +1 -0
  174. package/dist/src/tools/get-session-timeline.d.ts +55 -0
  175. package/dist/src/tools/get-session-timeline.d.ts.map +1 -0
  176. package/dist/src/tools/get-session-timeline.js +93 -0
  177. package/dist/src/tools/get-session-timeline.js.map +1 -0
  178. package/dist/src/tools/get-session-trends.d.ts +108 -0
  179. package/dist/src/tools/get-session-trends.d.ts.map +1 -0
  180. package/dist/src/tools/get-session-trends.js +130 -0
  181. package/dist/src/tools/get-session-trends.js.map +1 -0
  182. package/dist/src/tools/get-session.d.ts +251 -0
  183. package/dist/src/tools/get-session.d.ts.map +1 -0
  184. package/dist/src/tools/get-session.js +290 -0
  185. package/dist/src/tools/get-session.js.map +1 -0
  186. package/dist/src/tools/index.d.ts +22 -0
  187. package/dist/src/tools/index.d.ts.map +1 -1
  188. package/dist/src/tools/index.js +22 -0
  189. package/dist/src/tools/index.js.map +1 -1
  190. package/dist/src/tools/list-contexts.d.ts +50 -0
  191. package/dist/src/tools/list-contexts.d.ts.map +1 -0
  192. package/dist/src/tools/list-contexts.js +28 -0
  193. package/dist/src/tools/list-contexts.js.map +1 -0
  194. package/dist/src/tools/list-sessions.d.ts +86 -0
  195. package/dist/src/tools/list-sessions.d.ts.map +1 -0
  196. package/dist/src/tools/list-sessions.js +97 -0
  197. package/dist/src/tools/list-sessions.js.map +1 -0
  198. package/dist/src/tools/merge-contexts.d.ts +58 -0
  199. package/dist/src/tools/merge-contexts.d.ts.map +1 -0
  200. package/dist/src/tools/merge-contexts.js +27 -0
  201. package/dist/src/tools/merge-contexts.js.map +1 -0
  202. package/dist/src/tools/move-session-context.d.ts +62 -0
  203. package/dist/src/tools/move-session-context.d.ts.map +1 -0
  204. package/dist/src/tools/move-session-context.js +33 -0
  205. package/dist/src/tools/move-session-context.js.map +1 -0
  206. package/dist/src/tools/reingest-session.d.ts +31 -0
  207. package/dist/src/tools/reingest-session.d.ts.map +1 -0
  208. package/dist/src/tools/reingest-session.js +43 -0
  209. package/dist/src/tools/reingest-session.js.map +1 -0
  210. package/dist/src/tools/reject-context-link.d.ts +58 -0
  211. package/dist/src/tools/reject-context-link.d.ts.map +1 -0
  212. package/dist/src/tools/reject-context-link.js +26 -0
  213. package/dist/src/tools/reject-context-link.js.map +1 -0
  214. package/dist/src/tools/resolve-context.d.ts +287 -0
  215. package/dist/src/tools/resolve-context.d.ts.map +1 -0
  216. package/dist/src/tools/resolve-context.js +35 -0
  217. package/dist/src/tools/resolve-context.js.map +1 -0
  218. package/dist/src/tools/search-history.d.ts +86 -0
  219. package/dist/src/tools/search-history.d.ts.map +1 -0
  220. package/dist/src/tools/search-history.js +103 -0
  221. package/dist/src/tools/search-history.js.map +1 -0
  222. package/dist/src/tools/session-ui-metadata.d.ts +15 -0
  223. package/dist/src/tools/session-ui-metadata.d.ts.map +1 -0
  224. package/dist/src/tools/session-ui-metadata.js +15 -0
  225. package/dist/src/tools/session-ui-metadata.js.map +1 -0
  226. package/dist/src/tools/set-active-context.d.ts +58 -0
  227. package/dist/src/tools/set-active-context.d.ts.map +1 -0
  228. package/dist/src/tools/set-active-context.js +26 -0
  229. package/dist/src/tools/set-active-context.js.map +1 -0
  230. package/dist/src/tools/split-context.d.ts +62 -0
  231. package/dist/src/tools/split-context.d.ts.map +1 -0
  232. package/dist/src/tools/split-context.js +36 -0
  233. package/dist/src/tools/split-context.js.map +1 -0
  234. package/dist/src/tools/verify-footprint.js +1 -1
  235. package/dist/src/tools/verify-footprint.js.map +1 -1
  236. package/dist/src/types.d.ts +2 -0
  237. package/dist/src/types.d.ts.map +1 -1
  238. package/dist/src/ui/register.d.ts +6 -1
  239. package/dist/src/ui/register.d.ts.map +1 -1
  240. package/dist/src/ui/register.js +60 -16
  241. package/dist/src/ui/register.js.map +1 -1
  242. package/dist/ui/dashboard.html +239 -868
  243. package/dist/ui/detail.html +107 -248
  244. package/dist/ui/export.html +115 -298
  245. package/dist/ui/session-dashboard-live.html +264 -0
  246. package/dist/ui/session-dashboard.html +329 -0
  247. package/dist/ui/session-detail-live.html +336 -0
  248. package/dist/ui/session-detail.html +355 -0
  249. package/package.json +34 -9
@@ -1,15 +1,66 @@
1
1
  /* global Buffer, crypto */
2
2
  import Database from "better-sqlite3";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { getArtifactSearchableText, parseArtifactMetadata, } from "../session-artifacts.js";
6
+ import { traceSyncOperation } from "../observability.js";
3
7
  import { createSchema } from "./schema.js";
4
8
  function escapeLikePattern(pattern) {
5
9
  return pattern.replace(/[%_\\]/g, "\\$&");
6
10
  }
11
+ function normalizeWorkspaceKeyForMatching(value) {
12
+ const resolved = path.resolve(value);
13
+ try {
14
+ return fs.realpathSync.native(resolved);
15
+ }
16
+ catch {
17
+ return resolved;
18
+ }
19
+ }
20
+ function matchesWorkspaceKey(session, workspaceKey) {
21
+ const normalizedWorkspaceKey = normalizeWorkspaceKeyForMatching(workspaceKey);
22
+ return (normalizeWorkspaceKeyForMatching(session.projectRoot) ===
23
+ normalizedWorkspaceKey ||
24
+ normalizeWorkspaceKeyForMatching(session.cwd) === normalizedWorkspaceKey);
25
+ }
26
+ /**
27
+ * Append LIMIT/OFFSET clause to SQL query string.
28
+ * Mutates the params array by pushing limit/offset values.
29
+ * @returns The query string with pagination appended.
30
+ */
31
+ function appendPaginationClause(query, params, limit, offset) {
32
+ const off = offset ?? 0;
33
+ if (limit !== undefined) {
34
+ query += " LIMIT ?";
35
+ params.push(limit);
36
+ if (off > 0) {
37
+ query += " OFFSET ?";
38
+ params.push(off);
39
+ }
40
+ }
41
+ else if (off > 0) {
42
+ query += " LIMIT -1 OFFSET ?";
43
+ params.push(off);
44
+ }
45
+ return query;
46
+ }
47
+ function formatDbError(action, error) {
48
+ return new Error(`Failed to ${action}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
49
+ }
50
+ const TREND_FAILED_OUTCOME_PATTERN = /\b(?:fail|failed|error|timeout|timed-out|interrupted|non-zero)\b/i;
51
+ const TREND_SUCCEEDED_OUTCOME_PATTERN = /\b(?:success|succeeded|passed|completed|captured|ok)\b/i;
52
+ const SESSION_HISTORY_CACHE_VERSION_KEY = "session_history_cache_version";
53
+ const SESSION_TREND_CACHE_VERSION_KEY = "session_trend_cache_version";
54
+ const CURRENT_SESSION_HISTORY_CACHE_VERSION = 1;
55
+ const CURRENT_SESSION_TREND_CACHE_VERSION = 1;
7
56
  /**
8
57
  * Evidence database with CRUD operations
9
58
  * Manages encrypted evidence storage with SQLite backend
10
59
  */
11
60
  export class EvidenceDatabase {
12
61
  db;
62
+ sessionHistoryCacheBackfilled = false;
63
+ sessionTrendAttemptsBackfilled = false;
13
64
  /**
14
65
  * Creates or opens an evidence database
15
66
  * @param dbPath - Path to SQLite database file
@@ -19,12 +70,101 @@ export class EvidenceDatabase {
19
70
  this.db = new Database(dbPath);
20
71
  try {
21
72
  createSchema(this.db);
73
+ this.initializeMaterializedCaches();
22
74
  }
23
75
  catch (error) {
24
76
  // Clean up database connection on any initialization failure
25
77
  this.db.close();
26
- throw new Error(`Failed to initialize database: ${error instanceof Error ? error.message : String(error)}`);
78
+ throw formatDbError("initialize database", error);
79
+ }
80
+ }
81
+ getMetadataValue(key) {
82
+ const row = this.db
83
+ .prepare(`
84
+ SELECT value
85
+ FROM metadata
86
+ WHERE key = ?
87
+ `)
88
+ .get(key);
89
+ return row?.value ?? null;
90
+ }
91
+ getMetadataVersion(key) {
92
+ const value = this.getMetadataValue(key);
93
+ const parsed = Number.parseInt(value ?? "", 10);
94
+ return Number.isFinite(parsed) ? parsed : 0;
95
+ }
96
+ setMetadataValue(key, value) {
97
+ this.db
98
+ .prepare(`
99
+ INSERT INTO metadata (key, value)
100
+ VALUES (?, ?)
101
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
102
+ `)
103
+ .run(key, value);
104
+ }
105
+ getAllSessionIds() {
106
+ return this.db
107
+ .prepare(`
108
+ SELECT id
109
+ FROM sessions
110
+ ORDER BY startedAt ASC, id ASC
111
+ `)
112
+ .all().map((row) => row.id);
113
+ }
114
+ initializeMaterializedCaches() {
115
+ if (this.getMetadataVersion(SESSION_HISTORY_CACHE_VERSION_KEY) <
116
+ CURRENT_SESSION_HISTORY_CACHE_VERSION) {
117
+ this.rebuildAllSessionHistoryCaches();
118
+ this.setMetadataValue(SESSION_HISTORY_CACHE_VERSION_KEY, String(CURRENT_SESSION_HISTORY_CACHE_VERSION));
119
+ this.sessionHistoryCacheBackfilled = true;
120
+ }
121
+ else {
122
+ this.sessionHistoryCacheBackfilled = false;
123
+ this.ensureSessionHistoryCacheBackfilled();
124
+ }
125
+ if (this.getMetadataVersion(SESSION_TREND_CACHE_VERSION_KEY) <
126
+ CURRENT_SESSION_TREND_CACHE_VERSION) {
127
+ this.rebuildAllSessionTrendAttempts();
128
+ this.setMetadataValue(SESSION_TREND_CACHE_VERSION_KEY, String(CURRENT_SESSION_TREND_CACHE_VERSION));
129
+ this.sessionTrendAttemptsBackfilled = true;
130
+ }
131
+ else {
132
+ this.sessionTrendAttemptsBackfilled = false;
133
+ this.ensureSessionTrendAttemptsBackfilled();
134
+ }
135
+ }
136
+ dbOp(action, fn) {
137
+ try {
138
+ return fn();
139
+ }
140
+ catch (error) {
141
+ throw formatDbError(action, error);
142
+ }
143
+ }
144
+ resolveActiveContextIdOrThrow(contextId) {
145
+ let currentId = contextId.trim();
146
+ const visited = new Set();
147
+ while (currentId) {
148
+ if (visited.has(currentId)) {
149
+ throw new Error(`Context merge loop detected for ${contextId}`);
150
+ }
151
+ visited.add(currentId);
152
+ const row = this.db
153
+ .prepare(`
154
+ SELECT id, status, mergedIntoContextId
155
+ FROM contexts
156
+ WHERE id = ?
157
+ `)
158
+ .get(currentId);
159
+ if (!row) {
160
+ throw new Error(`Context not found: ${contextId}`);
161
+ }
162
+ if (row.status !== "merged" || !row.mergedIntoContextId) {
163
+ return row.id;
164
+ }
165
+ currentId = row.mergedIntoContextId;
27
166
  }
167
+ throw new Error(`Context not found: ${contextId}`);
28
168
  }
29
169
  /**
30
170
  * Creates a new evidence record
@@ -34,7 +174,7 @@ export class EvidenceDatabase {
34
174
  create(evidence) {
35
175
  const id = crypto.randomUUID();
36
176
  const now = new Date().toISOString();
37
- try {
177
+ return this.dbOp("create evidence", () => {
38
178
  const stmt = this.db.prepare(`
39
179
  INSERT INTO evidences (
40
180
  id, timestamp, conversationId, llmProvider,
@@ -44,10 +184,7 @@ export class EvidenceDatabase {
44
184
  `);
45
185
  stmt.run(id, evidence.timestamp, evidence.conversationId, evidence.llmProvider, Buffer.from(evidence.encryptedContent), Buffer.from(evidence.nonce), evidence.contentHash, evidence.messageCount, evidence.gitCommitHash, evidence.gitTimestamp, evidence.tags, now, now);
46
186
  return id;
47
- }
48
- catch (error) {
49
- throw new Error(`Failed to create evidence: ${error instanceof Error ? error.message : String(error)}`);
50
- }
187
+ });
51
188
  }
52
189
  /**
53
190
  * Finds evidence by ID
@@ -55,7 +192,7 @@ export class EvidenceDatabase {
55
192
  * @returns Evidence or null if not found
56
193
  */
57
194
  findById(id) {
58
- try {
195
+ return this.dbOp("find evidence by ID", () => {
59
196
  const stmt = this.db.prepare(`
60
197
  SELECT * FROM evidences WHERE id = ?
61
198
  `);
@@ -64,11 +201,7 @@ export class EvidenceDatabase {
64
201
  return null;
65
202
  }
66
203
  return this.rowToEvidence(row);
67
- }
68
- catch (error) {
69
- // Don't swallow database errors - re-throw with context
70
- throw new Error(`Failed to find evidence by ID: ${error instanceof Error ? error.message : String(error)}`);
71
- }
204
+ });
72
205
  }
73
206
  /**
74
207
  * Finds all evidences for a conversation
@@ -76,7 +209,7 @@ export class EvidenceDatabase {
76
209
  * @returns Array of evidences (empty if none found)
77
210
  */
78
211
  findByConversationId(conversationId) {
79
- try {
212
+ return this.dbOp("find evidences by conversationId", () => {
80
213
  const stmt = this.db.prepare(`
81
214
  SELECT * FROM evidences
82
215
  WHERE conversationId = ?
@@ -84,10 +217,7 @@ export class EvidenceDatabase {
84
217
  `);
85
218
  const rows = stmt.all(conversationId);
86
219
  return rows.map((row) => this.rowToEvidence(row));
87
- }
88
- catch (error) {
89
- throw new Error(`Failed to find evidences by conversationId: ${error instanceof Error ? error.message : String(error)}`);
90
- }
220
+ });
91
221
  }
92
222
  /**
93
223
  * Lists evidences with pagination
@@ -95,31 +225,1407 @@ export class EvidenceDatabase {
95
225
  * @returns Array of evidences
96
226
  */
97
227
  list(options) {
228
+ return this.dbOp("list evidences", () => {
229
+ const params = [];
230
+ const query = appendPaginationClause("SELECT * FROM evidences ORDER BY timestamp DESC", params, options?.limit, options?.offset);
231
+ const stmt = this.db.prepare(query);
232
+ const rows = stmt.all(...params);
233
+ return rows.map((row) => this.rowToEvidence(row));
234
+ });
235
+ }
236
+ static joinSearchParts(parts) {
237
+ return parts
238
+ .map((part) => part.trim())
239
+ .filter(Boolean)
240
+ .join("\n");
241
+ }
242
+ static appendSearchPart(existing, addition) {
243
+ const normalizedAddition = addition.trim();
244
+ if (!normalizedAddition) {
245
+ return existing;
246
+ }
247
+ return existing.trim()
248
+ ? `${existing}\n${normalizedAddition}`
249
+ : normalizedAddition;
250
+ }
251
+ static normalizeTrendOutcome(value) {
252
+ if (!value) {
253
+ return "other";
254
+ }
255
+ if (TREND_FAILED_OUTCOME_PATTERN.test(value)) {
256
+ return "failed";
257
+ }
258
+ if (TREND_SUCCEEDED_OUTCOME_PATTERN.test(value)) {
259
+ return "succeeded";
260
+ }
261
+ return "other";
262
+ }
263
+ buildArtifactHistoryCache(artifacts) {
264
+ const issueKeys = new Set();
265
+ const text = EvidenceDatabase.joinSearchParts(artifacts.flatMap((artifact) => {
266
+ const metadata = parseArtifactMetadata(artifact.metadata);
267
+ if (metadata.issueKey) {
268
+ issueKeys.add(metadata.issueKey);
269
+ }
270
+ return getArtifactSearchableText(artifact);
271
+ }));
272
+ return {
273
+ text,
274
+ issueKeys: Array.from(issueKeys).sort(),
275
+ };
276
+ }
277
+ buildNarrativeHistoryCache(narratives) {
278
+ return EvidenceDatabase.joinSearchParts(narratives.map((narrative) => narrative.content));
279
+ }
280
+ buildDecisionHistoryCache(decisions) {
281
+ return EvidenceDatabase.joinSearchParts(decisions.map((decision) => decision.summary));
282
+ }
283
+ ensureSessionHistoryCacheRow(sessionId) {
284
+ this.db
285
+ .prepare(`
286
+ INSERT INTO session_history_cache (
287
+ sessionId, titleText, metadataText, messagesText,
288
+ artifactsText, narrativesText, decisionsText, updatedAt
289
+ )
290
+ SELECT
291
+ id,
292
+ COALESCE(title, ''),
293
+ COALESCE(metadata, ''),
294
+ '',
295
+ '',
296
+ '',
297
+ '',
298
+ ?
299
+ FROM sessions
300
+ WHERE id = ?
301
+ ON CONFLICT(sessionId) DO NOTHING
302
+ `)
303
+ .run(new Date().toISOString(), sessionId);
304
+ }
305
+ updateSessionHistoryCache(sessionId, updates) {
306
+ this.ensureSessionHistoryCacheRow(sessionId);
307
+ const current = this.db
308
+ .prepare(`
309
+ SELECT * FROM session_history_cache
310
+ WHERE sessionId = ?
311
+ `)
312
+ .get(sessionId);
313
+ if (!current) {
314
+ return;
315
+ }
316
+ const next = {
317
+ ...current,
318
+ ...updates,
319
+ sessionId,
320
+ updatedAt: new Date().toISOString(),
321
+ };
322
+ this.db
323
+ .prepare(`
324
+ UPDATE session_history_cache
325
+ SET titleText = ?,
326
+ metadataText = ?,
327
+ messagesText = ?,
328
+ artifactsText = ?,
329
+ narrativesText = ?,
330
+ decisionsText = ?,
331
+ updatedAt = ?
332
+ WHERE sessionId = ?
333
+ `)
334
+ .run(next.titleText, next.metadataText, next.messagesText, next.artifactsText, next.narrativesText, next.decisionsText, next.updatedAt, sessionId);
335
+ }
336
+ replaceSessionIssueKeys(sessionId, issueKeys) {
337
+ this.db
338
+ .prepare(`DELETE FROM session_issue_keys WHERE sessionId = ?`)
339
+ .run(sessionId);
340
+ const insertIssueKey = this.db.prepare(`
341
+ INSERT INTO session_issue_keys (sessionId, issueKey)
342
+ VALUES (?, ?)
343
+ `);
344
+ for (const issueKey of issueKeys) {
345
+ insertIssueKey.run(sessionId, issueKey);
346
+ }
347
+ }
348
+ rebuildSessionHistoryCache(sessionId) {
349
+ const session = this.findSessionById(sessionId);
350
+ if (!session) {
351
+ return;
352
+ }
353
+ const messagesText = EvidenceDatabase.joinSearchParts(this.getSessionMessages(sessionId).map((message) => message.content));
354
+ const artifacts = this.getSessionArtifacts(sessionId);
355
+ const artifactCache = this.buildArtifactHistoryCache(artifacts);
356
+ const narrativesText = this.buildNarrativeHistoryCache(this.getSessionNarratives(sessionId));
357
+ const decisionsText = this.buildDecisionHistoryCache(this.getSessionDecisions(sessionId));
358
+ this.updateSessionHistoryCache(sessionId, {
359
+ titleText: session.title ?? "",
360
+ metadataText: session.metadata ?? "",
361
+ messagesText,
362
+ artifactsText: artifactCache.text,
363
+ narrativesText,
364
+ decisionsText,
365
+ });
366
+ this.replaceSessionIssueKeys(sessionId, artifactCache.issueKeys);
367
+ }
368
+ ensureSessionHistoryCacheBackfilled() {
369
+ if (this.sessionHistoryCacheBackfilled) {
370
+ return;
371
+ }
372
+ const missingRows = this.db
373
+ .prepare(`
374
+ SELECT s.id
375
+ FROM sessions s
376
+ LEFT JOIN session_history_cache cache ON cache.sessionId = s.id
377
+ WHERE cache.sessionId IS NULL
378
+ ORDER BY s.startedAt ASC, s.id ASC
379
+ `)
380
+ .all();
381
+ for (const row of missingRows) {
382
+ this.rebuildSessionHistoryCache(row.id);
383
+ }
384
+ this.sessionHistoryCacheBackfilled = true;
385
+ }
386
+ rebuildAllSessionHistoryCaches() {
387
+ for (const sessionId of this.getAllSessionIds()) {
388
+ this.rebuildSessionHistoryCache(sessionId);
389
+ }
390
+ }
391
+ fetchTrendSeenAtByEventIds(sessionId, eventIds) {
392
+ if (eventIds.length === 0) {
393
+ return new Map();
394
+ }
395
+ const placeholders = eventIds.map(() => "?").join(", ");
396
+ const rows = this.db
397
+ .prepare(`
398
+ SELECT id, COALESCE(endedAt, startedAt) as seenAt
399
+ FROM timeline_events
400
+ WHERE sessionId = ? AND id IN (${placeholders})
401
+ `)
402
+ .all(sessionId, ...eventIds);
403
+ return new Map(rows.map((row) => [row.id, row.seenAt]));
404
+ }
405
+ buildSessionTrendAttempts(sessionId, artifacts) {
406
+ const eventIds = [
407
+ ...new Set(artifacts
408
+ .map((artifact) => artifact.eventId)
409
+ .filter((eventId) => Boolean(eventId))),
410
+ ];
411
+ const seenAtByEventId = this.fetchTrendSeenAtByEventIds(sessionId, eventIds);
412
+ return artifacts.flatMap((artifact) => {
413
+ const metadata = parseArtifactMetadata(artifact.metadata);
414
+ const eventType = typeof metadata.details.eventType === "string"
415
+ ? metadata.details.eventType
416
+ : null;
417
+ const outcome = metadata.outcome ?? metadata.status ?? "captured";
418
+ const outcomeCategory = EvidenceDatabase.normalizeTrendOutcome(outcome);
419
+ if (!artifact.eventId || !metadata.issueKey) {
420
+ return [];
421
+ }
422
+ if (!eventType ||
423
+ (!eventType.startsWith("command.") && !eventType.startsWith("test."))) {
424
+ return [];
425
+ }
426
+ if (outcomeCategory === "other") {
427
+ return [];
428
+ }
429
+ return [
430
+ {
431
+ artifactId: artifact.id,
432
+ sessionId,
433
+ issueKey: metadata.issueKey,
434
+ issueLabel: metadata.issueLabel ?? metadata.issueKey,
435
+ kind: metadata.intent ?? metadata.category,
436
+ issueFamilyKey: metadata.issueFamilyKey,
437
+ issueFamilyLabel: metadata.issueFamilyLabel ??
438
+ metadata.issueFamilyKey ??
439
+ metadata.issueLabel,
440
+ outcome,
441
+ outcomeCategory,
442
+ seenAt: seenAtByEventId.get(artifact.eventId) ?? artifact.createdAt,
443
+ createdAt: artifact.createdAt,
444
+ },
445
+ ];
446
+ });
447
+ }
448
+ markSessionTrendAttemptsFresh(sessionId) {
449
+ this.db
450
+ .prepare(`
451
+ INSERT INTO session_trend_cache_state (sessionId, updatedAt)
452
+ VALUES (?, ?)
453
+ ON CONFLICT(sessionId) DO UPDATE SET updatedAt = excluded.updatedAt
454
+ `)
455
+ .run(sessionId, new Date().toISOString());
456
+ }
457
+ replaceSessionTrendAttempts(sessionId, artifacts) {
458
+ this.db
459
+ .prepare(`DELETE FROM session_trend_attempts WHERE sessionId = ?`)
460
+ .run(sessionId);
461
+ const attempts = this.buildSessionTrendAttempts(sessionId, artifacts);
462
+ if (attempts.length === 0) {
463
+ this.markSessionTrendAttemptsFresh(sessionId);
464
+ return;
465
+ }
466
+ const insertAttempt = this.db.prepare(`
467
+ INSERT INTO session_trend_attempts (
468
+ artifactId, sessionId, issueKey, issueLabel, kind,
469
+ issueFamilyKey, issueFamilyLabel, outcome, outcomeCategory,
470
+ seenAt, createdAt
471
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
472
+ `);
473
+ for (const attempt of attempts) {
474
+ insertAttempt.run(attempt.artifactId, attempt.sessionId, attempt.issueKey, attempt.issueLabel, attempt.kind, attempt.issueFamilyKey, attempt.issueFamilyLabel, attempt.outcome, attempt.outcomeCategory, attempt.seenAt, attempt.createdAt);
475
+ }
476
+ this.markSessionTrendAttemptsFresh(sessionId);
477
+ }
478
+ insertSessionTrendAttempts(sessionId, artifacts) {
479
+ const attempts = this.buildSessionTrendAttempts(sessionId, artifacts);
480
+ if (attempts.length === 0) {
481
+ this.markSessionTrendAttemptsFresh(sessionId);
482
+ return;
483
+ }
484
+ const insertAttempt = this.db.prepare(`
485
+ INSERT INTO session_trend_attempts (
486
+ artifactId, sessionId, issueKey, issueLabel, kind,
487
+ issueFamilyKey, issueFamilyLabel, outcome, outcomeCategory,
488
+ seenAt, createdAt
489
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
490
+ ON CONFLICT(artifactId) DO UPDATE SET
491
+ sessionId = excluded.sessionId,
492
+ issueKey = excluded.issueKey,
493
+ issueLabel = excluded.issueLabel,
494
+ kind = excluded.kind,
495
+ issueFamilyKey = excluded.issueFamilyKey,
496
+ issueFamilyLabel = excluded.issueFamilyLabel,
497
+ outcome = excluded.outcome,
498
+ outcomeCategory = excluded.outcomeCategory,
499
+ seenAt = excluded.seenAt,
500
+ createdAt = excluded.createdAt
501
+ `);
502
+ for (const attempt of attempts) {
503
+ insertAttempt.run(attempt.artifactId, attempt.sessionId, attempt.issueKey, attempt.issueLabel, attempt.kind, attempt.issueFamilyKey, attempt.issueFamilyLabel, attempt.outcome, attempt.outcomeCategory, attempt.seenAt, attempt.createdAt);
504
+ }
505
+ this.markSessionTrendAttemptsFresh(sessionId);
506
+ }
507
+ rebuildSessionTrendAttempts(sessionId) {
508
+ const session = this.findSessionById(sessionId);
509
+ if (!session) {
510
+ return;
511
+ }
512
+ this.replaceSessionTrendAttempts(sessionId, this.getSessionArtifacts(sessionId));
513
+ }
514
+ ensureSessionTrendAttemptsBackfilled() {
515
+ if (this.sessionTrendAttemptsBackfilled) {
516
+ return;
517
+ }
518
+ const missingRows = this.db
519
+ .prepare(`
520
+ SELECT s.id
521
+ FROM sessions s
522
+ LEFT JOIN session_trend_cache_state state ON state.sessionId = s.id
523
+ WHERE state.sessionId IS NULL
524
+ ORDER BY s.startedAt ASC, s.id ASC
525
+ `)
526
+ .all();
527
+ for (const row of missingRows) {
528
+ this.rebuildSessionTrendAttempts(row.id);
529
+ }
530
+ this.sessionTrendAttemptsBackfilled = true;
531
+ }
532
+ rebuildAllSessionTrendAttempts() {
533
+ for (const sessionId of this.getAllSessionIds()) {
534
+ this.rebuildSessionTrendAttempts(sessionId);
535
+ }
536
+ }
537
+ querySessionsByHistory(options) {
538
+ return traceSyncOperation("db.query-sessions-by-history", {
539
+ host: options.host,
540
+ status: options.status,
541
+ hasQuery: Boolean(options.query?.trim()),
542
+ issueKey: options.issueKey?.trim() || undefined,
543
+ sessionIds: options.sessionIds?.length ?? 0,
544
+ limit: options.limit,
545
+ offset: options.offset,
546
+ }, () => this.dbOp("query sessions by history", () => {
547
+ this.ensureSessionHistoryCacheBackfilled();
548
+ const conditions = [];
549
+ const params = [];
550
+ if (options.host) {
551
+ conditions.push(`s.host = ?`);
552
+ params.push(options.host);
553
+ }
554
+ if (options.status) {
555
+ conditions.push(`s.status = ?`);
556
+ params.push(options.status);
557
+ }
558
+ if (options.query && options.query.trim()) {
559
+ const pattern = `%${escapeLikePattern(options.query.trim())}%`;
560
+ conditions.push(`
561
+ (
562
+ cache.titleText LIKE ? ESCAPE '\\'
563
+ OR cache.metadataText LIKE ? ESCAPE '\\'
564
+ OR cache.messagesText LIKE ? ESCAPE '\\'
565
+ OR cache.artifactsText LIKE ? ESCAPE '\\'
566
+ OR cache.narrativesText LIKE ? ESCAPE '\\'
567
+ OR cache.decisionsText LIKE ? ESCAPE '\\'
568
+ )
569
+ `);
570
+ params.push(pattern, pattern, pattern, pattern, pattern, pattern);
571
+ }
572
+ if (options.issueKey && options.issueKey.trim()) {
573
+ conditions.push(`
574
+ EXISTS (
575
+ SELECT 1
576
+ FROM session_issue_keys issue_keys
577
+ WHERE issue_keys.sessionId = s.id
578
+ AND issue_keys.issueKey = ?
579
+ )
580
+ `);
581
+ params.push(options.issueKey.trim());
582
+ }
583
+ if (options.sessionIds && options.sessionIds.length > 0) {
584
+ const normalizedIds = options.sessionIds
585
+ .map((sessionId) => sessionId.trim())
586
+ .filter(Boolean);
587
+ if (normalizedIds.length === 0) {
588
+ return {
589
+ sessions: [],
590
+ total: 0,
591
+ };
592
+ }
593
+ conditions.push(`s.id IN (${normalizedIds.map(() => "?").join(", ")})`);
594
+ params.push(...normalizedIds);
595
+ }
596
+ const whereSql = conditions.length
597
+ ? `WHERE ${conditions.join(" AND ")}`
598
+ : "";
599
+ const fromSql = `
600
+ FROM sessions s
601
+ LEFT JOIN session_history_cache cache ON cache.sessionId = s.id
602
+ ${whereSql}
603
+ `;
604
+ const total = this.db
605
+ .prepare(`SELECT COUNT(*) as total ${fromSql}`)
606
+ .get(...params)?.total ?? 0;
607
+ let query = `
608
+ SELECT s.*
609
+ ${fromSql}
610
+ ORDER BY s.startedAt DESC, s.id DESC
611
+ `;
612
+ const pageParams = [...params];
613
+ query = appendPaginationClause(query, pageParams, options.limit, options.offset);
614
+ const rows = this.db
615
+ .prepare(query)
616
+ .all(...pageParams);
617
+ return {
618
+ sessions: rows.map((row) => this.rowToSession(row)),
619
+ total,
620
+ };
621
+ }));
622
+ }
623
+ querySessionTrendAttempts(options) {
624
+ return traceSyncOperation("db.query-session-trend-attempts", {
625
+ host: options?.host,
626
+ status: options?.status,
627
+ sessionIds: options?.sessionIds?.length ?? 0,
628
+ }, () => this.dbOp("query session trend attempts", () => {
629
+ this.ensureSessionTrendAttemptsBackfilled();
630
+ const conditions = [];
631
+ const params = [];
632
+ if (options?.host) {
633
+ conditions.push(`s.host = ?`);
634
+ params.push(options.host);
635
+ }
636
+ if (options?.status) {
637
+ conditions.push(`s.status = ?`);
638
+ params.push(options.status);
639
+ }
640
+ if (options?.sessionIds && options.sessionIds.length > 0) {
641
+ const normalizedIds = options.sessionIds
642
+ .map((sessionId) => sessionId.trim())
643
+ .filter(Boolean);
644
+ if (normalizedIds.length === 0) {
645
+ return [];
646
+ }
647
+ conditions.push(`ta.sessionId IN (${normalizedIds.map(() => "?").join(", ")})`);
648
+ params.push(...normalizedIds);
649
+ }
650
+ const whereSql = conditions.length
651
+ ? `WHERE ${conditions.join(" AND ")}`
652
+ : "";
653
+ return this.db
654
+ .prepare(`
655
+ SELECT
656
+ ta.*,
657
+ s.host,
658
+ s.status,
659
+ s.cwd,
660
+ s.startedAt,
661
+ s.endedAt,
662
+ s.title
663
+ FROM session_trend_attempts ta
664
+ INNER JOIN sessions s ON s.id = ta.sessionId
665
+ ${whereSql}
666
+ ORDER BY ta.seenAt DESC, ta.artifactId DESC
667
+ `)
668
+ .all(...params);
669
+ }));
670
+ }
671
+ querySessionTrendContextAttempts(sessionId) {
672
+ return this.dbOp("query session trend context attempts", () => {
673
+ const normalizedSessionId = sessionId.trim();
674
+ if (!normalizedSessionId) {
675
+ return [];
676
+ }
677
+ this.ensureSessionTrendAttemptsBackfilled();
678
+ return this.db
679
+ .prepare(`
680
+ WITH target_issue_keys AS (
681
+ SELECT DISTINCT issueKey
682
+ FROM session_trend_attempts
683
+ WHERE sessionId = ?
684
+ )
685
+ SELECT
686
+ ta.*,
687
+ s.host,
688
+ s.status,
689
+ s.cwd,
690
+ s.startedAt,
691
+ s.endedAt,
692
+ s.title
693
+ FROM session_trend_attempts ta
694
+ INNER JOIN target_issue_keys tik ON tik.issueKey = ta.issueKey
695
+ INNER JOIN sessions s ON s.id = ta.sessionId
696
+ ORDER BY ta.issueKey ASC, ta.seenAt DESC, ta.artifactId DESC
697
+ `)
698
+ .all(normalizedSessionId);
699
+ });
700
+ }
701
+ getSessionFollowUpMessages(sessionIds, options) {
702
+ return traceSyncOperation("db.get-session-follow-up-messages", {
703
+ sessionIds: sessionIds.length,
704
+ limit: options?.limit,
705
+ offset: options?.offset,
706
+ }, () => this.dbOp("get session follow-up messages", () => {
707
+ const normalizedIds = sessionIds
708
+ .map((sessionId) => sessionId.trim())
709
+ .filter(Boolean);
710
+ if (normalizedIds.length === 0) {
711
+ return [];
712
+ }
713
+ const params = [...normalizedIds];
714
+ let query = `
715
+ SELECT id, sessionId, content, capturedAt, seq
716
+ FROM messages
717
+ WHERE sessionId IN (${normalizedIds.map(() => "?").join(", ")})
718
+ AND (
719
+ content LIKE '%?%'
720
+ OR lower(content) LIKE 'next:%'
721
+ )
722
+ ORDER BY capturedAt DESC, sessionId DESC, seq DESC, id DESC
723
+ `;
724
+ query = appendPaginationClause(query, params, options?.limit, options?.offset);
725
+ const rows = this.db
726
+ .prepare(query)
727
+ .all(...params);
728
+ return rows.map((row) => ({
729
+ sessionId: row.sessionId,
730
+ content: row.content,
731
+ capturedAt: row.capturedAt,
732
+ }));
733
+ }));
734
+ }
735
+ createSession(session) {
736
+ const id = crypto.randomUUID();
737
+ const now = new Date().toISOString();
738
+ return this.dbOp("create session", () => {
739
+ const stmt = this.db.prepare(`
740
+ INSERT INTO sessions (
741
+ id, host, projectRoot, cwd, title, status,
742
+ startedAt, endedAt, metadata, createdAt, updatedAt
743
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
744
+ `);
745
+ stmt.run(id, session.host, session.projectRoot, session.cwd, session.title, session.status, session.startedAt, session.endedAt, session.metadata, now, now);
746
+ this.db
747
+ .prepare(`
748
+ INSERT INTO session_history_cache (
749
+ sessionId, titleText, metadataText, messagesText,
750
+ artifactsText, narrativesText, decisionsText, updatedAt
751
+ ) VALUES (?, ?, ?, '', '', '', '', ?)
752
+ `)
753
+ .run(id, session.title ?? "", session.metadata ?? "", now);
754
+ this.markSessionTrendAttemptsFresh(id);
755
+ return id;
756
+ });
757
+ }
758
+ appendMessage(message) {
759
+ const id = crypto.randomUUID();
760
+ return this.dbOp("append message", () => {
761
+ const stmt = this.db.prepare(`
762
+ INSERT INTO messages (
763
+ id, sessionId, seq, role, source, content, capturedAt, metadata
764
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
765
+ `);
766
+ stmt.run(id, message.sessionId, message.seq, message.role, message.source, message.content, message.capturedAt, message.metadata);
767
+ const currentCache = this.db
768
+ .prepare(`
769
+ SELECT messagesText FROM session_history_cache
770
+ WHERE sessionId = ?
771
+ `)
772
+ .get(message.sessionId);
773
+ this.updateSessionHistoryCache(message.sessionId, {
774
+ messagesText: EvidenceDatabase.appendSearchPart(currentCache?.messagesText ?? "", message.content),
775
+ });
776
+ return id;
777
+ });
778
+ }
779
+ appendTimelineEvent(event) {
780
+ const id = crypto.randomUUID();
781
+ return this.dbOp("append timeline event", () => {
782
+ const stmt = this.db.prepare(`
783
+ INSERT INTO timeline_events (
784
+ id, sessionId, seq, eventType, eventSubType, source,
785
+ summary, payload, startedAt, endedAt, status, relatedMessageId
786
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
787
+ `);
788
+ stmt.run(id, event.sessionId, event.seq, event.eventType, event.eventSubType, event.source, event.summary, event.payload, event.startedAt, event.endedAt, event.status, event.relatedMessageId);
789
+ return id;
790
+ });
791
+ }
792
+ updateSessionTitle(id, title) {
793
+ this.dbOp("update session title", () => {
794
+ const stmt = this.db.prepare(`
795
+ UPDATE sessions
796
+ SET title = ?,
797
+ updatedAt = ?
798
+ WHERE id = ?
799
+ `);
800
+ const result = stmt.run(title, new Date().toISOString(), id);
801
+ if (result.changes === 0) {
802
+ throw new Error(`Session with id ${id} not found`);
803
+ }
804
+ this.updateSessionHistoryCache(id, {
805
+ titleText: title,
806
+ });
807
+ });
808
+ }
809
+ finalizeSession(id, updates) {
810
+ this.dbOp("finalize session", () => {
811
+ const stmt = this.db.prepare(`
812
+ UPDATE sessions
813
+ SET status = ?,
814
+ endedAt = ?,
815
+ title = COALESCE(?, title),
816
+ updatedAt = ?
817
+ WHERE id = ?
818
+ `);
819
+ const result = stmt.run(updates.status, updates.endedAt, updates.title, new Date().toISOString(), id);
820
+ if (result.changes === 0) {
821
+ throw new Error(`Session with id ${id} not found`);
822
+ }
823
+ });
824
+ }
825
+ findSessionById(id) {
826
+ return this.dbOp("find session by ID", () => {
827
+ const row = this.db
828
+ .prepare(`SELECT * FROM sessions WHERE id = ?`)
829
+ .get(id);
830
+ return row ? this.rowToSession(row) : null;
831
+ });
832
+ }
833
+ listSessions(options) {
834
+ return this.dbOp("list sessions", () => {
835
+ const params = [];
836
+ const query = appendPaginationClause("SELECT * FROM sessions ORDER BY startedAt DESC", params, options?.limit, options?.offset);
837
+ const rows = this.db.prepare(query).all(...params);
838
+ return rows.map((row) => this.rowToSession(row));
839
+ });
840
+ }
841
+ createContext(context) {
842
+ const id = crypto.randomUUID();
843
+ const now = new Date().toISOString();
844
+ return this.dbOp("create context", () => {
845
+ this.db
846
+ .prepare(`
847
+ INSERT INTO contexts (
848
+ id, label, workspaceKey, status, mergedIntoContextId,
849
+ metadata, createdAt, updatedAt
850
+ ) VALUES (?, ?, ?, 'active', NULL, ?, ?, ?)
851
+ `)
852
+ .run(id, context.label, context.workspaceKey, context.metadata ?? null, now, now);
853
+ return {
854
+ id,
855
+ label: context.label,
856
+ workspaceKey: context.workspaceKey,
857
+ status: "active",
858
+ mergedIntoContextId: null,
859
+ metadata: context.metadata ?? null,
860
+ createdAt: now,
861
+ updatedAt: now,
862
+ };
863
+ });
864
+ }
865
+ findContextById(id) {
866
+ return this.dbOp("find context by ID", () => {
867
+ const row = this.db
868
+ .prepare(`SELECT * FROM contexts WHERE id = ?`)
869
+ .get(id);
870
+ return row ? this.rowToContext(row) : null;
871
+ });
872
+ }
873
+ resolveContextById(id) {
98
874
  try {
99
- const limit = options?.limit;
100
- const offset = options?.offset ?? 0;
101
- let query = "SELECT * FROM evidences ORDER BY timestamp DESC";
875
+ const activeId = this.resolveActiveContextIdOrThrow(id);
876
+ return this.findContextById(activeId);
877
+ }
878
+ catch {
879
+ return null;
880
+ }
881
+ }
882
+ listContexts(options) {
883
+ return this.dbOp("list contexts", () => {
884
+ const conditions = [];
885
+ const params = [];
886
+ if (options?.workspaceKey) {
887
+ conditions.push("workspaceKey = ?");
888
+ params.push(options.workspaceKey);
889
+ }
890
+ if (options?.status) {
891
+ conditions.push("status = ?");
892
+ params.push(options.status);
893
+ }
894
+ else if (!options?.includeMerged) {
895
+ conditions.push("status = 'active'");
896
+ }
897
+ const whereSql = conditions.length
898
+ ? `WHERE ${conditions.join(" AND ")}`
899
+ : "";
900
+ const query = appendPaginationClause(`
901
+ SELECT *
902
+ FROM contexts
903
+ ${whereSql}
904
+ ORDER BY updatedAt DESC, createdAt DESC, id DESC
905
+ `, params, options?.limit, options?.offset);
906
+ const rows = this.db.prepare(query).all(...params);
907
+ return rows.map((row) => this.rowToContext(row));
908
+ });
909
+ }
910
+ getContextCount(options) {
911
+ return this.dbOp("count contexts", () => {
912
+ const conditions = [];
913
+ const params = [];
914
+ if (options?.workspaceKey) {
915
+ conditions.push("workspaceKey = ?");
916
+ params.push(options.workspaceKey);
917
+ }
918
+ if (options?.status) {
919
+ conditions.push("status = ?");
920
+ params.push(options.status);
921
+ }
922
+ else if (!options?.includeMerged) {
923
+ conditions.push("status = 'active'");
924
+ }
925
+ const whereSql = conditions.length
926
+ ? `WHERE ${conditions.join(" AND ")}`
927
+ : "";
928
+ const row = this.db
929
+ .prepare(`SELECT COUNT(*) as count FROM contexts ${whereSql}`)
930
+ .get(...params);
931
+ return row?.count ?? 0;
932
+ });
933
+ }
934
+ listSessionsForContext(contextId) {
935
+ return this.dbOp("list sessions for context", () => {
936
+ const activeId = this.resolveActiveContextIdOrThrow(contextId);
937
+ const rows = this.db
938
+ .prepare(`
939
+ SELECT s.*
940
+ FROM context_session_links links
941
+ INNER JOIN sessions s ON s.id = links.sessionId
942
+ WHERE links.contextId = ?
943
+ ORDER BY s.startedAt ASC, s.id ASC
944
+ `)
945
+ .all(activeId);
946
+ return rows.map((row) => this.rowToSession(row));
947
+ });
948
+ }
949
+ listUnlinkedSessions(options) {
950
+ return this.dbOp("list unlinked sessions", () => {
951
+ const conditions = [
952
+ "NOT EXISTS (SELECT 1 FROM context_session_links links WHERE links.sessionId = s.id)",
953
+ ];
102
954
  const params = [];
103
- if (limit !== undefined) {
104
- query += " LIMIT ?";
105
- params.push(limit);
106
- if (offset > 0) {
107
- query += " OFFSET ?";
108
- params.push(offset);
955
+ let query = `
956
+ SELECT s.*
957
+ FROM sessions s
958
+ WHERE ${conditions.join(" AND ")}
959
+ ORDER BY s.startedAt ASC, s.id ASC
960
+ `;
961
+ if (!options?.workspaceKey) {
962
+ query = appendPaginationClause(query, params, options?.limit, options?.offset);
963
+ }
964
+ const rows = this.db.prepare(query).all(...params);
965
+ let sessions = rows.map((row) => this.rowToSession(row));
966
+ if (options?.workspaceKey) {
967
+ sessions = sessions.filter((session) => matchesWorkspaceKey(session, options.workspaceKey));
968
+ const off = options.offset ?? 0;
969
+ if (off > 0) {
970
+ sessions = sessions.slice(off);
971
+ }
972
+ if (options.limit !== undefined) {
973
+ sessions = sessions.slice(0, options.limit);
109
974
  }
110
975
  }
111
- else if (offset > 0) {
112
- query += " LIMIT -1 OFFSET ?";
113
- params.push(offset);
976
+ return sessions;
977
+ });
978
+ }
979
+ findContextLinkForSession(sessionId) {
980
+ return this.dbOp("find context link for session", () => {
981
+ const row = this.db
982
+ .prepare(`
983
+ SELECT *
984
+ FROM context_session_links
985
+ WHERE sessionId = ?
986
+ `)
987
+ .get(sessionId);
988
+ return row ? this.rowToContextSessionLink(row) : null;
989
+ });
990
+ }
991
+ listContextSessionLinks(contextId) {
992
+ return this.dbOp("list context session links", () => {
993
+ const activeId = this.resolveActiveContextIdOrThrow(contextId);
994
+ const rows = this.db
995
+ .prepare(`
996
+ SELECT *
997
+ FROM context_session_links
998
+ WHERE contextId = ?
999
+ ORDER BY updatedAt DESC, sessionId DESC
1000
+ `)
1001
+ .all(activeId);
1002
+ return rows.map((row) => this.rowToContextSessionLink(row));
1003
+ });
1004
+ }
1005
+ assignSessionToContext(input) {
1006
+ const now = new Date().toISOString();
1007
+ return this.dbOp("assign session to context", () => {
1008
+ const session = this.findSessionById(input.sessionId);
1009
+ if (!session) {
1010
+ throw new Error(`Session not found: ${input.sessionId}`);
1011
+ }
1012
+ const activeContextId = this.resolveActiveContextIdOrThrow(input.contextId);
1013
+ this.db
1014
+ .prepare(`
1015
+ INSERT INTO context_session_links (
1016
+ sessionId, contextId, linkSource, createdAt, updatedAt
1017
+ ) VALUES (?, ?, ?, ?, ?)
1018
+ ON CONFLICT(sessionId) DO UPDATE SET
1019
+ contextId = excluded.contextId,
1020
+ linkSource = excluded.linkSource,
1021
+ updatedAt = excluded.updatedAt
1022
+ `)
1023
+ .run(input.sessionId, activeContextId, input.linkSource, now, now);
1024
+ this.db
1025
+ .prepare(`
1026
+ DELETE FROM context_link_rejections
1027
+ WHERE sessionId = ? AND contextId = ?
1028
+ `)
1029
+ .run(input.sessionId, activeContextId);
1030
+ this.db
1031
+ .prepare(`
1032
+ UPDATE contexts
1033
+ SET updatedAt = ?
1034
+ WHERE id = ?
1035
+ `)
1036
+ .run(now, activeContextId);
1037
+ return {
1038
+ sessionId: input.sessionId,
1039
+ contextId: activeContextId,
1040
+ linkSource: input.linkSource,
1041
+ createdAt: now,
1042
+ updatedAt: now,
1043
+ };
1044
+ });
1045
+ }
1046
+ rejectContextForSession(sessionId, contextId) {
1047
+ const now = new Date().toISOString();
1048
+ return this.dbOp("reject context for session", () => {
1049
+ if (!this.findSessionById(sessionId)) {
1050
+ throw new Error(`Session not found: ${sessionId}`);
1051
+ }
1052
+ const activeContextId = this.resolveActiveContextIdOrThrow(contextId);
1053
+ this.db
1054
+ .prepare(`
1055
+ INSERT INTO context_link_rejections (sessionId, contextId, createdAt)
1056
+ VALUES (?, ?, ?)
1057
+ ON CONFLICT(sessionId, contextId) DO NOTHING
1058
+ `)
1059
+ .run(sessionId, activeContextId, now);
1060
+ return {
1061
+ sessionId,
1062
+ contextId: activeContextId,
1063
+ createdAt: now,
1064
+ };
1065
+ });
1066
+ }
1067
+ listContextRejectionsForSession(sessionId) {
1068
+ return this.dbOp("list context rejections", () => {
1069
+ const rows = this.db
1070
+ .prepare(`
1071
+ SELECT *
1072
+ FROM context_link_rejections
1073
+ WHERE sessionId = ?
1074
+ ORDER BY createdAt DESC, contextId DESC
1075
+ `)
1076
+ .all(sessionId);
1077
+ return rows.map((row) => this.rowToContextLinkRejection(row));
1078
+ });
1079
+ }
1080
+ setWorkspacePreferredContext(workspaceKey, contextId) {
1081
+ const now = new Date().toISOString();
1082
+ return this.dbOp("set preferred context", () => {
1083
+ const activeContextId = this.resolveActiveContextIdOrThrow(contextId);
1084
+ this.db
1085
+ .prepare(`
1086
+ INSERT INTO context_workspace_preferences (
1087
+ workspaceKey, contextId, createdAt, updatedAt
1088
+ ) VALUES (?, ?, ?, ?)
1089
+ ON CONFLICT(workspaceKey) DO UPDATE SET
1090
+ contextId = excluded.contextId,
1091
+ updatedAt = excluded.updatedAt
1092
+ `)
1093
+ .run(workspaceKey, activeContextId, now, now);
1094
+ return {
1095
+ workspaceKey,
1096
+ contextId: activeContextId,
1097
+ createdAt: now,
1098
+ updatedAt: now,
1099
+ };
1100
+ });
1101
+ }
1102
+ getWorkspacePreferredContext(workspaceKey) {
1103
+ return this.dbOp("get preferred context", () => {
1104
+ const row = this.db
1105
+ .prepare(`
1106
+ SELECT *
1107
+ FROM context_workspace_preferences
1108
+ WHERE workspaceKey = ?
1109
+ `)
1110
+ .get(workspaceKey);
1111
+ if (!row) {
1112
+ return null;
1113
+ }
1114
+ try {
1115
+ const activeContextId = this.resolveActiveContextIdOrThrow(row.contextId);
1116
+ if (activeContextId !== row.contextId) {
1117
+ const now = new Date().toISOString();
1118
+ this.db
1119
+ .prepare(`
1120
+ UPDATE context_workspace_preferences
1121
+ SET contextId = ?, updatedAt = ?
1122
+ WHERE workspaceKey = ?
1123
+ `)
1124
+ .run(activeContextId, now, workspaceKey);
1125
+ return this.rowToContextWorkspacePreference({
1126
+ ...row,
1127
+ contextId: activeContextId,
1128
+ updatedAt: now,
1129
+ });
1130
+ }
1131
+ }
1132
+ catch {
1133
+ return this.rowToContextWorkspacePreference(row);
1134
+ }
1135
+ return this.rowToContextWorkspacePreference(row);
1136
+ });
1137
+ }
1138
+ mergeContexts(sourceContextId, targetContextId) {
1139
+ this.dbOp("merge contexts", () => {
1140
+ const sourceId = this.resolveActiveContextIdOrThrow(sourceContextId);
1141
+ const targetId = this.resolveActiveContextIdOrThrow(targetContextId);
1142
+ if (sourceId === targetId) {
1143
+ throw new Error("Cannot merge a context into itself");
1144
+ }
1145
+ const now = new Date().toISOString();
1146
+ const transaction = this.db.transaction(() => {
1147
+ this.db
1148
+ .prepare(`
1149
+ UPDATE context_session_links
1150
+ SET contextId = ?, linkSource = 'merge', updatedAt = ?
1151
+ WHERE contextId = ?
1152
+ `)
1153
+ .run(targetId, now, sourceId);
1154
+ this.db
1155
+ .prepare(`
1156
+ UPDATE context_workspace_preferences
1157
+ SET contextId = ?, updatedAt = ?
1158
+ WHERE contextId = ?
1159
+ `)
1160
+ .run(targetId, now, sourceId);
1161
+ this.db
1162
+ .prepare(`
1163
+ UPDATE contexts
1164
+ SET status = 'merged',
1165
+ mergedIntoContextId = ?,
1166
+ updatedAt = ?
1167
+ WHERE id = ?
1168
+ `)
1169
+ .run(targetId, now, sourceId);
1170
+ this.db
1171
+ .prepare(`
1172
+ UPDATE contexts
1173
+ SET updatedAt = ?
1174
+ WHERE id = ?
1175
+ `)
1176
+ .run(now, targetId);
1177
+ });
1178
+ transaction();
1179
+ });
1180
+ }
1181
+ countSessionMessages(sessionId) {
1182
+ return this.dbOp("count session messages", () => {
1183
+ const row = this.db
1184
+ .prepare(`SELECT COUNT(*) as total FROM messages WHERE sessionId = ?`)
1185
+ .get(sessionId);
1186
+ return row?.total ?? 0;
1187
+ });
1188
+ }
1189
+ getSessionMessageStats(sessionId) {
1190
+ return this.dbOp("summarize session messages", () => {
1191
+ const summaryRow = this.db
1192
+ .prepare(`
1193
+ SELECT
1194
+ COUNT(*) as total,
1195
+ COALESCE(SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END), 0) as userCount,
1196
+ COALESCE(SUM(CASE WHEN role = 'assistant' THEN 1 ELSE 0 END), 0) as assistantCount,
1197
+ COALESCE(SUM(CASE WHEN role = 'system' THEN 1 ELSE 0 END), 0) as systemCount,
1198
+ MIN(capturedAt) as firstCapturedAt,
1199
+ MAX(capturedAt) as lastCapturedAt
1200
+ FROM messages
1201
+ WHERE sessionId = ?
1202
+ `)
1203
+ .get(sessionId);
1204
+ const previewRow = this.db
1205
+ .prepare(`
1206
+ SELECT content FROM messages
1207
+ WHERE sessionId = ?
1208
+ ORDER BY seq ASC, capturedAt ASC
1209
+ LIMIT 1
1210
+ `)
1211
+ .get(sessionId);
1212
+ return {
1213
+ total: summaryRow?.total ?? 0,
1214
+ byRole: {
1215
+ user: summaryRow?.userCount ?? 0,
1216
+ assistant: summaryRow?.assistantCount ?? 0,
1217
+ system: summaryRow?.systemCount ?? 0,
1218
+ },
1219
+ firstCapturedAt: summaryRow?.firstCapturedAt ?? null,
1220
+ lastCapturedAt: summaryRow?.lastCapturedAt ?? null,
1221
+ previewContent: previewRow?.content ?? null,
1222
+ };
1223
+ });
1224
+ }
1225
+ getSessionMessages(sessionId, options) {
1226
+ return this.dbOp("get session messages", () => {
1227
+ const params = [sessionId];
1228
+ const query = appendPaginationClause("SELECT * FROM messages WHERE sessionId = ? ORDER BY seq ASC, capturedAt ASC", params, options?.limit, options?.offset);
1229
+ const rows = this.db.prepare(query).all(...params);
1230
+ return rows.map((row) => this.rowToMessage(row));
1231
+ });
1232
+ }
1233
+ countSessionTimeline(sessionId) {
1234
+ return this.dbOp("count session timeline", () => {
1235
+ const row = this.db
1236
+ .prepare(`SELECT COUNT(*) as total FROM timeline_events WHERE sessionId = ?`)
1237
+ .get(sessionId);
1238
+ return row?.total ?? 0;
1239
+ });
1240
+ }
1241
+ getSessionTimelineStats(sessionId) {
1242
+ return this.dbOp("summarize session timeline", () => {
1243
+ const summaryRow = this.db
1244
+ .prepare(`
1245
+ SELECT
1246
+ COUNT(*) as total,
1247
+ MIN(startedAt) as firstStartedAt,
1248
+ MAX(COALESCE(endedAt, startedAt)) as lastEndedAt
1249
+ FROM timeline_events
1250
+ WHERE sessionId = ?
1251
+ `)
1252
+ .get(sessionId);
1253
+ const eventTypeRows = this.db
1254
+ .prepare(`
1255
+ SELECT eventType FROM timeline_events
1256
+ WHERE sessionId = ?
1257
+ GROUP BY eventType
1258
+ ORDER BY MIN(seq) ASC, MIN(startedAt) ASC
1259
+ `)
1260
+ .all(sessionId);
1261
+ const statusRows = this.db
1262
+ .prepare(`
1263
+ SELECT status FROM timeline_events
1264
+ WHERE sessionId = ? AND status IS NOT NULL
1265
+ GROUP BY status
1266
+ ORDER BY MIN(seq) ASC, MIN(startedAt) ASC
1267
+ `)
1268
+ .all(sessionId);
1269
+ return {
1270
+ total: summaryRow?.total ?? 0,
1271
+ eventTypes: eventTypeRows.map((row) => row.eventType),
1272
+ statuses: statusRows.map((row) => row.status),
1273
+ firstStartedAt: summaryRow?.firstStartedAt ?? null,
1274
+ lastEndedAt: summaryRow?.lastEndedAt ?? null,
1275
+ };
1276
+ });
1277
+ }
1278
+ getSessionTimeline(sessionId, options) {
1279
+ return this.dbOp("get session timeline", () => {
1280
+ const params = [sessionId];
1281
+ const query = appendPaginationClause(`
1282
+ SELECT * FROM timeline_events
1283
+ WHERE sessionId = ?
1284
+ ORDER BY seq ASC, startedAt ASC
1285
+ `, params, options?.limit, options?.offset);
1286
+ const rows = this.db.prepare(query).all(...params);
1287
+ return rows.map((row) => this.rowToTimelineEvent(row));
1288
+ });
1289
+ }
1290
+ createArtifact(artifact) {
1291
+ const id = crypto.randomUUID();
1292
+ const createdAt = new Date().toISOString();
1293
+ return this.dbOp("create artifact", () => {
1294
+ this.db
1295
+ .prepare(`
1296
+ INSERT INTO artifacts (
1297
+ id, sessionId, eventId, artifactType, path, metadata, createdAt
1298
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1299
+ `)
1300
+ .run(id, artifact.sessionId, artifact.eventId, artifact.artifactType, artifact.path, artifact.metadata, createdAt);
1301
+ const artifactRecord = {
1302
+ id,
1303
+ sessionId: artifact.sessionId,
1304
+ eventId: artifact.eventId,
1305
+ artifactType: artifact.artifactType,
1306
+ path: artifact.path,
1307
+ metadata: artifact.metadata,
1308
+ createdAt,
1309
+ };
1310
+ const artifactCache = this.buildArtifactHistoryCache([artifactRecord]);
1311
+ const currentCache = this.db
1312
+ .prepare(`
1313
+ SELECT artifactsText FROM session_history_cache
1314
+ WHERE sessionId = ?
1315
+ `)
1316
+ .get(artifact.sessionId);
1317
+ this.updateSessionHistoryCache(artifact.sessionId, {
1318
+ artifactsText: EvidenceDatabase.appendSearchPart(currentCache?.artifactsText ?? "", artifactCache.text),
1319
+ });
1320
+ for (const issueKey of artifactCache.issueKeys) {
1321
+ this.db
1322
+ .prepare(`
1323
+ INSERT INTO session_issue_keys (sessionId, issueKey)
1324
+ VALUES (?, ?)
1325
+ ON CONFLICT(sessionId, issueKey) DO NOTHING
1326
+ `)
1327
+ .run(artifact.sessionId, issueKey);
1328
+ }
1329
+ this.insertSessionTrendAttempts(artifact.sessionId, [artifactRecord]);
1330
+ return artifactRecord;
1331
+ });
1332
+ }
1333
+ replaceArtifactsForSession(sessionId, artifacts) {
1334
+ return this.dbOp("replace artifacts", () => {
1335
+ const transaction = this.db.transaction(() => {
1336
+ this.db
1337
+ .prepare(`DELETE FROM artifacts WHERE sessionId = ?`)
1338
+ .run(sessionId);
1339
+ const insertArtifact = this.db.prepare(`
1340
+ INSERT INTO artifacts (
1341
+ id, sessionId, eventId, artifactType, path, metadata, createdAt
1342
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1343
+ `);
1344
+ const createdArtifacts = artifacts.map((artifact) => {
1345
+ const id = crypto.randomUUID();
1346
+ const createdAt = new Date().toISOString();
1347
+ insertArtifact.run(id, sessionId, artifact.eventId, artifact.artifactType, artifact.path, artifact.metadata, createdAt);
1348
+ return {
1349
+ id,
1350
+ sessionId,
1351
+ eventId: artifact.eventId,
1352
+ artifactType: artifact.artifactType,
1353
+ path: artifact.path,
1354
+ metadata: artifact.metadata,
1355
+ createdAt,
1356
+ };
1357
+ });
1358
+ const artifactCache = this.buildArtifactHistoryCache(createdArtifacts);
1359
+ this.updateSessionHistoryCache(sessionId, {
1360
+ artifactsText: artifactCache.text,
1361
+ });
1362
+ this.replaceSessionIssueKeys(sessionId, artifactCache.issueKeys);
1363
+ this.replaceSessionTrendAttempts(sessionId, createdArtifacts);
1364
+ return createdArtifacts;
1365
+ });
1366
+ return transaction();
1367
+ });
1368
+ }
1369
+ countSessionArtifacts(sessionId, options) {
1370
+ return this.dbOp("count session artifacts", () => {
1371
+ const row = (options?.artifactType
1372
+ ? this.db
1373
+ .prepare(`SELECT COUNT(*) as count FROM artifacts WHERE sessionId = ? AND artifactType = ?`)
1374
+ .get(sessionId, options.artifactType)
1375
+ : this.db
1376
+ .prepare(`SELECT COUNT(*) as count FROM artifacts WHERE sessionId = ?`)
1377
+ .get(sessionId));
1378
+ return row?.count ?? 0;
1379
+ });
1380
+ }
1381
+ getSessionArtifactSummary(sessionId) {
1382
+ return this.dbOp("summarize session artifacts", () => {
1383
+ const row = this.db
1384
+ .prepare(`
1385
+ SELECT
1386
+ COUNT(*) as total,
1387
+ COALESCE(SUM(CASE WHEN artifactType = 'file-change' THEN 1 ELSE 0 END), 0) as fileChange,
1388
+ COALESCE(SUM(CASE WHEN artifactType = 'command-output' THEN 1 ELSE 0 END), 0) as commandOutput,
1389
+ COALESCE(SUM(CASE WHEN artifactType = 'test-result' THEN 1 ELSE 0 END), 0) as testResult,
1390
+ COALESCE(SUM(CASE WHEN artifactType = 'git-commit' THEN 1 ELSE 0 END), 0) as gitCommit
1391
+ FROM artifacts
1392
+ WHERE sessionId = ?
1393
+ `)
1394
+ .get(sessionId);
1395
+ return {
1396
+ total: row?.total ?? 0,
1397
+ byType: {
1398
+ fileChange: row?.fileChange ?? 0,
1399
+ commandOutput: row?.commandOutput ?? 0,
1400
+ testResult: row?.testResult ?? 0,
1401
+ gitCommit: row?.gitCommit ?? 0,
1402
+ },
1403
+ };
1404
+ });
1405
+ }
1406
+ getSessionArtifacts(sessionId, options) {
1407
+ return this.dbOp("get session artifacts", () => {
1408
+ const conditions = ["sessionId = ?"];
1409
+ const parameters = [sessionId];
1410
+ if (options?.artifactType) {
1411
+ conditions.push("artifactType = ?");
1412
+ parameters.push(options.artifactType);
1413
+ }
1414
+ const query = appendPaginationClause(`SELECT * FROM artifacts WHERE ${conditions.join(" AND ")} ORDER BY createdAt ASC, id ASC`, parameters, options?.limit, options?.offset);
1415
+ const rows = this.db.prepare(query).all(...parameters);
1416
+ return rows.map((row) => this.rowToArtifact(row));
1417
+ });
1418
+ }
1419
+ replaceDecisionsForSession(sessionId, decisions) {
1420
+ return this.dbOp("replace decisions", () => {
1421
+ const transaction = this.db.transaction(() => {
1422
+ this.db
1423
+ .prepare(`DELETE FROM decisions WHERE sessionId = ?`)
1424
+ .run(sessionId);
1425
+ const createdDecisions = decisions.map((decision) => {
1426
+ const id = crypto.randomUUID();
1427
+ const createdAt = new Date().toISOString();
1428
+ this.db
1429
+ .prepare(`
1430
+ INSERT INTO decisions (
1431
+ id, sessionId, title, summary, rationale, status, sourceRefs, createdAt
1432
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1433
+ `)
1434
+ .run(id, sessionId, decision.title, decision.summary, decision.rationale, decision.status, decision.sourceRefs, createdAt);
1435
+ return {
1436
+ id,
1437
+ sessionId,
1438
+ title: decision.title,
1439
+ summary: decision.summary,
1440
+ rationale: decision.rationale,
1441
+ status: decision.status,
1442
+ sourceRefs: decision.sourceRefs,
1443
+ createdAt,
1444
+ };
1445
+ });
1446
+ this.updateSessionHistoryCache(sessionId, {
1447
+ decisionsText: this.buildDecisionHistoryCache(createdDecisions),
1448
+ });
1449
+ return createdDecisions;
1450
+ });
1451
+ return transaction();
1452
+ });
1453
+ }
1454
+ countSessionDecisions(sessionId) {
1455
+ return this.dbOp("count session decisions", () => {
1456
+ const row = this.db
1457
+ .prepare(`SELECT COUNT(*) as count FROM decisions WHERE sessionId = ?`)
1458
+ .get(sessionId);
1459
+ return row?.count ?? 0;
1460
+ });
1461
+ }
1462
+ getSessionDecisions(sessionId, options) {
1463
+ return this.dbOp("get session decisions", () => {
1464
+ const params = [sessionId];
1465
+ const query = appendPaginationClause(`
1466
+ SELECT * FROM decisions
1467
+ WHERE sessionId = ?
1468
+ ORDER BY createdAt ASC, id ASC
1469
+ `, params, options?.limit, options?.offset);
1470
+ const rows = this.db.prepare(query).all(...params);
1471
+ return rows.map((row) => this.rowToDecision(row));
1472
+ });
1473
+ }
1474
+ replaceNarrativesForSession(sessionId, narratives) {
1475
+ return this.dbOp("replace narratives", () => {
1476
+ const transaction = this.db.transaction(() => {
1477
+ this.db
1478
+ .prepare(`DELETE FROM narratives WHERE sessionId = ?`)
1479
+ .run(sessionId);
1480
+ const createdNarratives = narratives.map((narrative) => {
1481
+ const id = crypto.randomUUID();
1482
+ const now = new Date().toISOString();
1483
+ this.db
1484
+ .prepare(`
1485
+ INSERT INTO narratives (
1486
+ id, sessionId, kind, content, sourceRefs, createdAt, updatedAt
1487
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1488
+ `)
1489
+ .run(id, sessionId, narrative.kind, narrative.content, narrative.sourceRefs, now, now);
1490
+ return {
1491
+ id,
1492
+ sessionId,
1493
+ kind: narrative.kind,
1494
+ content: narrative.content,
1495
+ sourceRefs: narrative.sourceRefs,
1496
+ createdAt: now,
1497
+ updatedAt: now,
1498
+ };
1499
+ });
1500
+ this.updateSessionHistoryCache(sessionId, {
1501
+ narrativesText: this.buildNarrativeHistoryCache(createdNarratives),
1502
+ });
1503
+ return createdNarratives;
1504
+ });
1505
+ return transaction();
1506
+ });
1507
+ }
1508
+ countSessionNarratives(sessionId, options) {
1509
+ return this.dbOp("count session narratives", () => {
1510
+ const row = (options?.kind
1511
+ ? this.db
1512
+ .prepare(`SELECT COUNT(*) as count FROM narratives WHERE sessionId = ? AND kind = ?`)
1513
+ .get(sessionId, options.kind)
1514
+ : this.db
1515
+ .prepare(`SELECT COUNT(*) as count FROM narratives WHERE sessionId = ?`)
1516
+ .get(sessionId));
1517
+ return row?.count ?? 0;
1518
+ });
1519
+ }
1520
+ getSessionNarratives(sessionId, options) {
1521
+ return this.dbOp("get session narratives", () => {
1522
+ const conditions = ["sessionId = ?"];
1523
+ const params = [sessionId];
1524
+ if (options?.kind) {
1525
+ conditions.push("kind = ?");
1526
+ params.push(options.kind);
1527
+ }
1528
+ let query = `
1529
+ SELECT * FROM narratives
1530
+ WHERE ${conditions.join(" AND ")}
1531
+ ORDER BY CASE kind
1532
+ WHEN 'journal' THEN 1
1533
+ WHEN 'project-summary' THEN 2
1534
+ WHEN 'handoff' THEN 3
1535
+ ELSE 99
1536
+ END, createdAt ASC, id ASC
1537
+ `;
1538
+ query = appendPaginationClause(query, params, options?.limit, options?.offset);
1539
+ const rows = this.db.prepare(query).all(...params);
1540
+ return rows.map((row) => this.rowToNarrative(row));
1541
+ });
1542
+ }
1543
+ hasNarrativesForSession(sessionId) {
1544
+ return this.dbOp("check session narratives", () => {
1545
+ const row = this.db
1546
+ .prepare(`SELECT EXISTS(SELECT 1 FROM narratives WHERE sessionId = ?) as present`)
1547
+ .get(sessionId);
1548
+ return Boolean(row?.present);
1549
+ });
1550
+ }
1551
+ createIngestionRun(run) {
1552
+ const id = crypto.randomUUID();
1553
+ const startedAt = new Date().toISOString();
1554
+ return this.dbOp("create ingestion run", () => {
1555
+ this.db
1556
+ .prepare(`
1557
+ INSERT INTO ingestion_runs (
1558
+ id, sessionId, stage, status, error, startedAt, endedAt
1559
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1560
+ `)
1561
+ .run(id, run.sessionId, run.stage, run.status, run.error ?? null, startedAt, null);
1562
+ return {
1563
+ id,
1564
+ sessionId: run.sessionId,
1565
+ stage: run.stage,
1566
+ status: run.status,
1567
+ error: run.error ?? null,
1568
+ startedAt,
1569
+ endedAt: null,
1570
+ };
1571
+ });
1572
+ }
1573
+ completeIngestionRun(id, status, error = null) {
1574
+ try {
1575
+ const endedAt = new Date().toISOString();
1576
+ const result = this.db
1577
+ .prepare(`
1578
+ UPDATE ingestion_runs
1579
+ SET status = ?, error = ?, endedAt = ?
1580
+ WHERE id = ?
1581
+ `)
1582
+ .run(status, error, endedAt, id);
1583
+ if (result.changes === 0) {
1584
+ throw new Error(`Ingestion run with id ${id} not found`);
114
1585
  }
115
- const stmt = this.db.prepare(query);
116
- const rows = stmt.all(...params);
117
- return rows.map((row) => this.rowToEvidence(row));
118
1586
  }
119
- catch (error) {
120
- throw new Error(`Failed to list evidences: ${error instanceof Error ? error.message : String(error)}`);
1587
+ catch (cause) {
1588
+ throw new Error(`Failed to complete ingestion run: ${cause instanceof Error ? cause.message : String(cause)}`);
121
1589
  }
122
1590
  }
1591
+ getSessionIngestionRuns(sessionId) {
1592
+ return this.dbOp("get ingestion runs", () => {
1593
+ const rows = this.db
1594
+ .prepare(`
1595
+ SELECT * FROM ingestion_runs
1596
+ WHERE sessionId = ?
1597
+ ORDER BY startedAt ASC
1598
+ `)
1599
+ .all(sessionId);
1600
+ return rows.map((row) => this.rowToIngestionRun(row));
1601
+ });
1602
+ }
1603
+ getSessionDetail(sessionId) {
1604
+ // Wrap all reads in a single transaction for atomic snapshot
1605
+ const readAll = this.db.transaction(() => {
1606
+ const session = this.findSessionById(sessionId);
1607
+ if (!session) {
1608
+ return null;
1609
+ }
1610
+ const messages = this.getSessionMessages(sessionId);
1611
+ const timeline = this.getSessionTimeline(sessionId);
1612
+ const artifacts = this.getSessionArtifacts(sessionId);
1613
+ const narratives = this.getSessionNarratives(sessionId);
1614
+ const decisions = this.getSessionDecisions(sessionId);
1615
+ const ingestionRuns = this.getSessionIngestionRuns(sessionId);
1616
+ return {
1617
+ session,
1618
+ messages,
1619
+ timeline,
1620
+ artifacts,
1621
+ narratives,
1622
+ decisions,
1623
+ ingestionRuns,
1624
+ hasNarratives: narratives.length > 0,
1625
+ };
1626
+ });
1627
+ return readAll();
1628
+ }
123
1629
  /**
124
1630
  * Updates git commit information for an evidence
125
1631
  * @param id - Evidence UUID
@@ -127,7 +1633,7 @@ export class EvidenceDatabase {
127
1633
  * @param gitTimestamp - Git commit timestamp (ISO 8601)
128
1634
  */
129
1635
  updateGitInfo(id, gitCommitHash, gitTimestamp) {
130
- try {
1636
+ this.dbOp("update git info", () => {
131
1637
  const stmt = this.db.prepare(`
132
1638
  UPDATE evidences
133
1639
  SET gitCommitHash = ?,
@@ -139,10 +1645,7 @@ export class EvidenceDatabase {
139
1645
  if (result.changes === 0) {
140
1646
  throw new Error(`Evidence with id ${id} not found`);
141
1647
  }
142
- }
143
- catch (error) {
144
- throw new Error(`Failed to update git info: ${error instanceof Error ? error.message : String(error)}`);
145
- }
1648
+ });
146
1649
  }
147
1650
  /**
148
1651
  * Adds tags to an evidence (appends to existing tags)
@@ -151,7 +1654,7 @@ export class EvidenceDatabase {
151
1654
  * @throws Error if tags array is empty or all tags are whitespace
152
1655
  */
153
1656
  addTags(id, tags) {
154
- try {
1657
+ this.dbOp("add tags", () => {
155
1658
  if (tags.length === 0) {
156
1659
  throw new Error("Tags array cannot be empty");
157
1660
  }
@@ -185,10 +1688,7 @@ export class EvidenceDatabase {
185
1688
  stmt.run(mergedTags.join(","), new Date().toISOString(), id);
186
1689
  });
187
1690
  transaction();
188
- }
189
- catch (error) {
190
- throw new Error(`Failed to add tags: ${error instanceof Error ? error.message : String(error)}`);
191
- }
1691
+ });
192
1692
  }
193
1693
  /**
194
1694
  * Builds a WHERE clause from search/filter options
@@ -227,23 +1727,20 @@ export class EvidenceDatabase {
227
1727
  * @returns Number of matching evidences
228
1728
  */
229
1729
  getFilteredCount(options) {
230
- try {
1730
+ return this.dbOp("get filtered count", () => {
231
1731
  const { sql: whereClause, params } = this.buildWhereClause(options);
232
1732
  const row = this.db
233
1733
  .prepare(`SELECT COUNT(*) as count FROM evidences${whereClause}`)
234
1734
  .get(...params);
235
1735
  return row.count;
236
- }
237
- catch (error) {
238
- throw new Error(`Failed to get filtered count: ${error instanceof Error ? error.message : String(error)}`);
239
- }
1736
+ });
240
1737
  }
241
1738
  /**
242
1739
  * Search evidences and return both paginated results and total matching count
243
1740
  * in a single pass (builds WHERE clause once instead of twice)
244
1741
  */
245
1742
  searchWithCount(options) {
246
- try {
1743
+ return this.dbOp("search evidences", () => {
247
1744
  const { limit, offset = 0 } = options;
248
1745
  const { sql: whereClause, params: baseParams } = this.buildWhereClause(options);
249
1746
  // Wrap both queries in a transaction for consistent snapshot
@@ -254,19 +1751,7 @@ export class EvidenceDatabase {
254
1751
  .get(...baseParams);
255
1752
  // Build paginated query (clone params since we append to it)
256
1753
  const searchParams = [...baseParams];
257
- let sql = `SELECT * FROM evidences${whereClause} ORDER BY timestamp DESC`;
258
- if (limit !== undefined) {
259
- sql += " LIMIT ?";
260
- searchParams.push(limit);
261
- if (offset > 0) {
262
- sql += " OFFSET ?";
263
- searchParams.push(offset);
264
- }
265
- }
266
- else if (offset > 0) {
267
- sql += " LIMIT -1 OFFSET ?";
268
- searchParams.push(offset);
269
- }
1754
+ const sql = appendPaginationClause(`SELECT * FROM evidences${whereClause} ORDER BY timestamp DESC`, searchParams, limit, offset);
270
1755
  const rows = this.db.prepare(sql).all(...searchParams);
271
1756
  return {
272
1757
  evidences: rows.map((row) => this.rowToEvidence(row)),
@@ -274,10 +1759,7 @@ export class EvidenceDatabase {
274
1759
  };
275
1760
  });
276
1761
  return query();
277
- }
278
- catch (error) {
279
- throw new Error(`Failed to search evidences: ${error instanceof Error ? error.message : String(error)}`);
280
- }
1762
+ });
281
1763
  }
282
1764
  /**
283
1765
  * Search and filter evidences by various criteria
@@ -285,31 +1767,17 @@ export class EvidenceDatabase {
285
1767
  * @returns Array of matching evidences
286
1768
  */
287
1769
  search(options) {
288
- try {
1770
+ return this.dbOp("search evidences", () => {
289
1771
  const { limit, offset = 0 } = options;
290
1772
  const { sql: whereClause, params } = this.buildWhereClause(options);
291
1773
  // Build final query
292
1774
  let sql = `SELECT * FROM evidences${whereClause} ORDER BY timestamp DESC`;
293
1775
  // Add pagination
294
- if (limit !== undefined) {
295
- sql += " LIMIT ?";
296
- params.push(limit);
297
- if (offset > 0) {
298
- sql += " OFFSET ?";
299
- params.push(offset);
300
- }
301
- }
302
- else if (offset > 0) {
303
- sql += " LIMIT -1 OFFSET ?";
304
- params.push(offset);
305
- }
1776
+ sql = appendPaginationClause(sql, params, limit, offset);
306
1777
  const stmt = this.db.prepare(sql);
307
1778
  const rows = stmt.all(...params);
308
1779
  return rows.map((row) => this.rowToEvidence(row));
309
- }
310
- catch (error) {
311
- throw new Error(`Failed to search evidences: ${error instanceof Error ? error.message : String(error)}`);
312
- }
1780
+ });
313
1781
  }
314
1782
  /**
315
1783
  * Deletes evidence by ID
@@ -317,14 +1785,11 @@ export class EvidenceDatabase {
317
1785
  * @returns true if deleted, false if not found
318
1786
  */
319
1787
  delete(id) {
320
- try {
1788
+ return this.dbOp("delete evidence", () => {
321
1789
  const stmt = this.db.prepare(`DELETE FROM evidences WHERE id = ?`);
322
1790
  const result = stmt.run(id);
323
1791
  return result.changes > 0;
324
- }
325
- catch (error) {
326
- throw new Error(`Failed to delete evidence: ${error instanceof Error ? error.message : String(error)}`);
327
- }
1792
+ });
328
1793
  }
329
1794
  /**
330
1795
  * Deletes multiple evidences by IDs
@@ -334,7 +1799,7 @@ export class EvidenceDatabase {
334
1799
  deleteMany(ids) {
335
1800
  if (ids.length === 0)
336
1801
  return 0;
337
- try {
1802
+ return this.dbOp("delete evidences", () => {
338
1803
  // Batch deletions to stay under SQLite's 999 parameter limit
339
1804
  const BATCH_SIZE = 999;
340
1805
  let totalDeleted = 0;
@@ -346,10 +1811,7 @@ export class EvidenceDatabase {
346
1811
  totalDeleted += result.changes;
347
1812
  }
348
1813
  return totalDeleted;
349
- }
350
- catch (error) {
351
- throw new Error(`Failed to delete evidences: ${error instanceof Error ? error.message : String(error)}`);
352
- }
1814
+ });
353
1815
  }
354
1816
  /**
355
1817
  * Updates tags for an evidence (replaces existing tags)
@@ -358,7 +1820,7 @@ export class EvidenceDatabase {
358
1820
  * @returns true if updated, false if not found
359
1821
  */
360
1822
  updateTags(id, tags) {
361
- try {
1823
+ return this.dbOp("update tags", () => {
362
1824
  const stmt = this.db.prepare(`
363
1825
  UPDATE evidences
364
1826
  SET tags = ?,
@@ -367,10 +1829,7 @@ export class EvidenceDatabase {
367
1829
  `);
368
1830
  const result = stmt.run(tags, new Date().toISOString(), id);
369
1831
  return result.changes > 0;
370
- }
371
- catch (error) {
372
- throw new Error(`Failed to update tags: ${error instanceof Error ? error.message : String(error)}`);
373
- }
1832
+ });
374
1833
  }
375
1834
  /**
376
1835
  * Renames a tag across all evidences atomically using transaction
@@ -380,7 +1839,7 @@ export class EvidenceDatabase {
380
1839
  * @throws Error if transaction fails (no partial updates)
381
1840
  */
382
1841
  renameTag(oldTag, newTag) {
383
- try {
1842
+ return this.dbOp("rename tag", () => {
384
1843
  // Wrap in transaction for atomic updates
385
1844
  const transaction = this.db.transaction(() => {
386
1845
  // Get all evidences with this tag
@@ -398,10 +1857,7 @@ export class EvidenceDatabase {
398
1857
  return updatedCount;
399
1858
  });
400
1859
  return transaction();
401
- }
402
- catch (error) {
403
- throw new Error(`Failed to rename tag: ${error instanceof Error ? error.message : String(error)}`);
404
- }
1860
+ });
405
1861
  }
406
1862
  /**
407
1863
  * Removes a tag from all evidences atomically using transaction
@@ -410,7 +1866,7 @@ export class EvidenceDatabase {
410
1866
  * @throws Error if transaction fails (no partial updates)
411
1867
  */
412
1868
  removeTag(tag) {
413
- try {
1869
+ return this.dbOp("remove tag", () => {
414
1870
  // Wrap in transaction for atomic updates
415
1871
  const transaction = this.db.transaction(() => {
416
1872
  // Get all evidences with this tag
@@ -431,17 +1887,14 @@ export class EvidenceDatabase {
431
1887
  return updatedCount;
432
1888
  });
433
1889
  return transaction();
434
- }
435
- catch (error) {
436
- throw new Error(`Failed to remove tag: ${error instanceof Error ? error.message : String(error)}`);
437
- }
1890
+ });
438
1891
  }
439
1892
  /**
440
1893
  * Gets all unique tags with their counts
441
1894
  * @returns Map of tag to count
442
1895
  */
443
1896
  getTagCounts() {
444
- try {
1897
+ return this.dbOp("get tag counts", () => {
445
1898
  const stmt = this.db.prepare(`SELECT tags FROM evidences WHERE tags IS NOT NULL AND tags != ''`);
446
1899
  const rows = stmt.all();
447
1900
  const tagCounts = new Map();
@@ -455,10 +1908,7 @@ export class EvidenceDatabase {
455
1908
  }
456
1909
  }
457
1910
  return tagCounts;
458
- }
459
- catch (error) {
460
- throw new Error(`Failed to get tag counts: ${error instanceof Error ? error.message : String(error)}`);
461
- }
1911
+ });
462
1912
  }
463
1913
  /**
464
1914
  * Gets total count of all evidence records
@@ -470,6 +1920,12 @@ export class EvidenceDatabase {
470
1920
  .get();
471
1921
  return row.count;
472
1922
  }
1923
+ getSessionCount() {
1924
+ const row = this.db
1925
+ .prepare("SELECT COUNT(*) as count FROM sessions")
1926
+ .get();
1927
+ return row.count;
1928
+ }
473
1929
  /**
474
1930
  * Gets count of evidence records matching a search query
475
1931
  * @param query - Search text to match against conversationId and tags
@@ -517,5 +1973,129 @@ export class EvidenceDatabase {
517
1973
  updatedAt: row.updatedAt,
518
1974
  };
519
1975
  }
1976
+ rowToSession(row) {
1977
+ return {
1978
+ id: row.id,
1979
+ host: row.host,
1980
+ projectRoot: row.projectRoot,
1981
+ cwd: row.cwd,
1982
+ title: row.title,
1983
+ status: row.status,
1984
+ startedAt: row.startedAt,
1985
+ endedAt: row.endedAt,
1986
+ metadata: row.metadata,
1987
+ createdAt: row.createdAt,
1988
+ updatedAt: row.updatedAt,
1989
+ };
1990
+ }
1991
+ rowToContext(row) {
1992
+ return {
1993
+ id: row.id,
1994
+ label: row.label,
1995
+ workspaceKey: row.workspaceKey,
1996
+ status: row.status,
1997
+ mergedIntoContextId: row.mergedIntoContextId,
1998
+ metadata: row.metadata,
1999
+ createdAt: row.createdAt,
2000
+ updatedAt: row.updatedAt,
2001
+ };
2002
+ }
2003
+ rowToContextSessionLink(row) {
2004
+ return {
2005
+ sessionId: row.sessionId,
2006
+ contextId: row.contextId,
2007
+ linkSource: row.linkSource,
2008
+ createdAt: row.createdAt,
2009
+ updatedAt: row.updatedAt,
2010
+ };
2011
+ }
2012
+ rowToContextLinkRejection(row) {
2013
+ return {
2014
+ sessionId: row.sessionId,
2015
+ contextId: row.contextId,
2016
+ createdAt: row.createdAt,
2017
+ };
2018
+ }
2019
+ rowToContextWorkspacePreference(row) {
2020
+ return {
2021
+ workspaceKey: row.workspaceKey,
2022
+ contextId: row.contextId,
2023
+ createdAt: row.createdAt,
2024
+ updatedAt: row.updatedAt,
2025
+ };
2026
+ }
2027
+ rowToMessage(row) {
2028
+ return {
2029
+ id: row.id,
2030
+ sessionId: row.sessionId,
2031
+ seq: row.seq,
2032
+ role: row.role,
2033
+ source: row.source,
2034
+ content: row.content,
2035
+ capturedAt: row.capturedAt,
2036
+ metadata: row.metadata,
2037
+ };
2038
+ }
2039
+ rowToTimelineEvent(row) {
2040
+ return {
2041
+ id: row.id,
2042
+ sessionId: row.sessionId,
2043
+ seq: row.seq,
2044
+ eventType: row.eventType,
2045
+ eventSubType: row.eventSubType,
2046
+ source: row.source,
2047
+ summary: row.summary,
2048
+ payload: row.payload,
2049
+ startedAt: row.startedAt,
2050
+ endedAt: row.endedAt,
2051
+ status: row.status,
2052
+ relatedMessageId: row.relatedMessageId,
2053
+ };
2054
+ }
2055
+ rowToArtifact(row) {
2056
+ return {
2057
+ id: row.id,
2058
+ sessionId: row.sessionId,
2059
+ eventId: row.eventId,
2060
+ artifactType: row.artifactType,
2061
+ path: row.path,
2062
+ metadata: row.metadata,
2063
+ createdAt: row.createdAt,
2064
+ };
2065
+ }
2066
+ rowToDecision(row) {
2067
+ return {
2068
+ id: row.id,
2069
+ sessionId: row.sessionId,
2070
+ title: row.title,
2071
+ summary: row.summary,
2072
+ rationale: row.rationale,
2073
+ status: row.status,
2074
+ sourceRefs: row.sourceRefs,
2075
+ createdAt: row.createdAt,
2076
+ };
2077
+ }
2078
+ rowToNarrative(row) {
2079
+ return {
2080
+ id: row.id,
2081
+ sessionId: row.sessionId,
2082
+ kind: row.kind,
2083
+ content: row.content,
2084
+ sourceRefs: row.sourceRefs,
2085
+ createdAt: row.createdAt,
2086
+ updatedAt: row.updatedAt,
2087
+ };
2088
+ }
2089
+ rowToIngestionRun(row) {
2090
+ return {
2091
+ id: row.id,
2092
+ sessionId: row.sessionId,
2093
+ stage: row.stage,
2094
+ status: row.status,
2095
+ error: row.error,
2096
+ startedAt: row.startedAt,
2097
+ endedAt: row.endedAt,
2098
+ };
2099
+ }
520
2100
  }
521
2101
  //# sourceMappingURL=database.js.map