@marifold/core 0.60.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 (310) hide show
  1. package/LICENSE +674 -0
  2. package/dist/agent/AgentEvents.d.ts +106 -0
  3. package/dist/agent/AgentEvents.d.ts.map +1 -0
  4. package/dist/agent/AgentEvents.js +3 -0
  5. package/dist/agent/AgentEvents.js.map +1 -0
  6. package/dist/agent/AgentHistory.d.ts +27 -0
  7. package/dist/agent/AgentHistory.d.ts.map +1 -0
  8. package/dist/agent/AgentHistory.js +42 -0
  9. package/dist/agent/AgentHistory.js.map +1 -0
  10. package/dist/agent/AgentRunner.d.ts +149 -0
  11. package/dist/agent/AgentRunner.d.ts.map +1 -0
  12. package/dist/agent/AgentRunner.js +727 -0
  13. package/dist/agent/AgentRunner.js.map +1 -0
  14. package/dist/agent/ApprovalPolicy.d.ts +71 -0
  15. package/dist/agent/ApprovalPolicy.d.ts.map +1 -0
  16. package/dist/agent/ApprovalPolicy.js +29 -0
  17. package/dist/agent/ApprovalPolicy.js.map +1 -0
  18. package/dist/agent/ControlBlockTools.d.ts +9 -0
  19. package/dist/agent/ControlBlockTools.d.ts.map +1 -0
  20. package/dist/agent/ControlBlockTools.js +49 -0
  21. package/dist/agent/ControlBlockTools.js.map +1 -0
  22. package/dist/agent/RunWorkspace.d.ts +92 -0
  23. package/dist/agent/RunWorkspace.d.ts.map +1 -0
  24. package/dist/agent/RunWorkspace.js +328 -0
  25. package/dist/agent/RunWorkspace.js.map +1 -0
  26. package/dist/agent/ScopedProcess.d.ts +35 -0
  27. package/dist/agent/ScopedProcess.d.ts.map +1 -0
  28. package/dist/agent/ScopedProcess.js +280 -0
  29. package/dist/agent/ScopedProcess.js.map +1 -0
  30. package/dist/agent/ToolRegistry.d.ts +80 -0
  31. package/dist/agent/ToolRegistry.d.ts.map +1 -0
  32. package/dist/agent/ToolRegistry.js +45 -0
  33. package/dist/agent/ToolRegistry.js.map +1 -0
  34. package/dist/agent/UserInput.d.ts +59 -0
  35. package/dist/agent/UserInput.d.ts.map +1 -0
  36. package/dist/agent/UserInput.js +203 -0
  37. package/dist/agent/UserInput.js.map +1 -0
  38. package/dist/agent/tools/AskUserTool.d.ts +71 -0
  39. package/dist/agent/tools/AskUserTool.d.ts.map +1 -0
  40. package/dist/agent/tools/AskUserTool.js +70 -0
  41. package/dist/agent/tools/AskUserTool.js.map +1 -0
  42. package/dist/agent/tools/DelegateTool.d.ts +49 -0
  43. package/dist/agent/tools/DelegateTool.d.ts.map +1 -0
  44. package/dist/agent/tools/DelegateTool.js +62 -0
  45. package/dist/agent/tools/DelegateTool.js.map +1 -0
  46. package/dist/agent/tools/InspectAttachmentTool.d.ts +25 -0
  47. package/dist/agent/tools/InspectAttachmentTool.d.ts.map +1 -0
  48. package/dist/agent/tools/InspectAttachmentTool.js +129 -0
  49. package/dist/agent/tools/InspectAttachmentTool.js.map +1 -0
  50. package/dist/agent/tools/PythonPackageTool.d.ts +26 -0
  51. package/dist/agent/tools/PythonPackageTool.d.ts.map +1 -0
  52. package/dist/agent/tools/PythonPackageTool.js +105 -0
  53. package/dist/agent/tools/PythonPackageTool.js.map +1 -0
  54. package/dist/agent/tools/ReadFileTool.d.ts +24 -0
  55. package/dist/agent/tools/ReadFileTool.d.ts.map +1 -0
  56. package/dist/agent/tools/ReadFileTool.js +112 -0
  57. package/dist/agent/tools/ReadFileTool.js.map +1 -0
  58. package/dist/agent/tools/ShellExecTool.d.ts +23 -0
  59. package/dist/agent/tools/ShellExecTool.d.ts.map +1 -0
  60. package/dist/agent/tools/ShellExecTool.js +75 -0
  61. package/dist/agent/tools/ShellExecTool.js.map +1 -0
  62. package/dist/agent/tools/WebSearchTool.d.ts +26 -0
  63. package/dist/agent/tools/WebSearchTool.d.ts.map +1 -0
  64. package/dist/agent/tools/WebSearchTool.js +52 -0
  65. package/dist/agent/tools/WebSearchTool.js.map +1 -0
  66. package/dist/agent/tools/WriteFileTool.d.ts +30 -0
  67. package/dist/agent/tools/WriteFileTool.d.ts.map +1 -0
  68. package/dist/agent/tools/WriteFileTool.js +155 -0
  69. package/dist/agent/tools/WriteFileTool.js.map +1 -0
  70. package/dist/app/AppActionResolver.d.ts +15 -0
  71. package/dist/app/AppActionResolver.d.ts.map +1 -0
  72. package/dist/app/AppActionResolver.js +98 -0
  73. package/dist/app/AppActionResolver.js.map +1 -0
  74. package/dist/app/AppSchema.d.ts +83 -0
  75. package/dist/app/AppSchema.d.ts.map +1 -0
  76. package/dist/app/AppSchema.js +17 -0
  77. package/dist/app/AppSchema.js.map +1 -0
  78. package/dist/app/AppStore.d.ts +14 -0
  79. package/dist/app/AppStore.d.ts.map +1 -0
  80. package/dist/app/AppStore.js +146 -0
  81. package/dist/app/AppStore.js.map +1 -0
  82. package/dist/app/AppValidator.d.ts +10 -0
  83. package/dist/app/AppValidator.d.ts.map +1 -0
  84. package/dist/app/AppValidator.js +503 -0
  85. package/dist/app/AppValidator.js.map +1 -0
  86. package/dist/app/SkillAppCompiler.d.ts +5 -0
  87. package/dist/app/SkillAppCompiler.d.ts.map +1 -0
  88. package/dist/app/SkillAppCompiler.js +632 -0
  89. package/dist/app/SkillAppCompiler.js.map +1 -0
  90. package/dist/app/SkillAppDsl.d.ts +93 -0
  91. package/dist/app/SkillAppDsl.d.ts.map +1 -0
  92. package/dist/app/SkillAppDsl.js +72 -0
  93. package/dist/app/SkillAppDsl.js.map +1 -0
  94. package/dist/app/SkillAppInstanceRegistry.d.ts +25 -0
  95. package/dist/app/SkillAppInstanceRegistry.d.ts.map +1 -0
  96. package/dist/app/SkillAppInstanceRegistry.js +218 -0
  97. package/dist/app/SkillAppInstanceRegistry.js.map +1 -0
  98. package/dist/app/SkillAppResolver.d.ts +12 -0
  99. package/dist/app/SkillAppResolver.d.ts.map +1 -0
  100. package/dist/app/SkillAppResolver.js +89 -0
  101. package/dist/app/SkillAppResolver.js.map +1 -0
  102. package/dist/app/SkillAppSchema.d.ts +116 -0
  103. package/dist/app/SkillAppSchema.d.ts.map +1 -0
  104. package/dist/app/SkillAppSchema.js +5 -0
  105. package/dist/app/SkillAppSchema.js.map +1 -0
  106. package/dist/channels/TelegramBridge.d.ts +103 -0
  107. package/dist/channels/TelegramBridge.d.ts.map +1 -0
  108. package/dist/channels/TelegramBridge.js +565 -0
  109. package/dist/channels/TelegramBridge.js.map +1 -0
  110. package/dist/channels/respond.d.ts +46 -0
  111. package/dist/channels/respond.d.ts.map +1 -0
  112. package/dist/channels/respond.js +41 -0
  113. package/dist/channels/respond.js.map +1 -0
  114. package/dist/config/ChatGptTokenRefresh.d.ts +18 -0
  115. package/dist/config/ChatGptTokenRefresh.d.ts.map +1 -0
  116. package/dist/config/ChatGptTokenRefresh.js +63 -0
  117. package/dist/config/ChatGptTokenRefresh.js.map +1 -0
  118. package/dist/config/ConfigBackup.d.ts +20 -0
  119. package/dist/config/ConfigBackup.d.ts.map +1 -0
  120. package/dist/config/ConfigBackup.js +197 -0
  121. package/dist/config/ConfigBackup.js.map +1 -0
  122. package/dist/config/ConfigLoader.d.ts +28 -0
  123. package/dist/config/ConfigLoader.d.ts.map +1 -0
  124. package/dist/config/ConfigLoader.js +335 -0
  125. package/dist/config/ConfigLoader.js.map +1 -0
  126. package/dist/config/ConfigManager.d.ts +55 -0
  127. package/dist/config/ConfigManager.d.ts.map +1 -0
  128. package/dist/config/ConfigManager.js +707 -0
  129. package/dist/config/ConfigManager.js.map +1 -0
  130. package/dist/config/ConfigSchema.d.ts +231 -0
  131. package/dist/config/ConfigSchema.d.ts.map +1 -0
  132. package/dist/config/ConfigSchema.js +15 -0
  133. package/dist/config/ConfigSchema.js.map +1 -0
  134. package/dist/config/GitHubCopilotAuth.d.ts +7 -0
  135. package/dist/config/GitHubCopilotAuth.d.ts.map +1 -0
  136. package/dist/config/GitHubCopilotAuth.js +40 -0
  137. package/dist/config/GitHubCopilotAuth.js.map +1 -0
  138. package/dist/config/MarifoldOpenAICompatProvider.d.ts +39 -0
  139. package/dist/config/MarifoldOpenAICompatProvider.d.ts.map +1 -0
  140. package/dist/config/MarifoldOpenAICompatProvider.js +158 -0
  141. package/dist/config/MarifoldOpenAICompatProvider.js.map +1 -0
  142. package/dist/config/OpenAICompatUrls.d.ts +7 -0
  143. package/dist/config/OpenAICompatUrls.d.ts.map +1 -0
  144. package/dist/config/OpenAICompatUrls.js +47 -0
  145. package/dist/config/OpenAICompatUrls.js.map +1 -0
  146. package/dist/config/ProviderFactory.d.ts +12 -0
  147. package/dist/config/ProviderFactory.d.ts.map +1 -0
  148. package/dist/config/ProviderFactory.js +61 -0
  149. package/dist/config/ProviderFactory.js.map +1 -0
  150. package/dist/config/ProviderInspector.d.ts +46 -0
  151. package/dist/config/ProviderInspector.d.ts.map +1 -0
  152. package/dist/config/ProviderInspector.js +356 -0
  153. package/dist/config/ProviderInspector.js.map +1 -0
  154. package/dist/config/ProviderRegistry.d.ts +19 -0
  155. package/dist/config/ProviderRegistry.d.ts.map +1 -0
  156. package/dist/config/ProviderRegistry.js +301 -0
  157. package/dist/config/ProviderRegistry.js.map +1 -0
  158. package/dist/config/XaiTokenRefresh.d.ts +15 -0
  159. package/dist/config/XaiTokenRefresh.d.ts.map +1 -0
  160. package/dist/config/XaiTokenRefresh.js +73 -0
  161. package/dist/config/XaiTokenRefresh.js.map +1 -0
  162. package/dist/errors/MarifoldError.d.ts +34 -0
  163. package/dist/errors/MarifoldError.d.ts.map +1 -0
  164. package/dist/errors/MarifoldError.js +100 -0
  165. package/dist/errors/MarifoldError.js.map +1 -0
  166. package/dist/images/ImageOptimizer.d.ts +34 -0
  167. package/dist/images/ImageOptimizer.d.ts.map +1 -0
  168. package/dist/images/ImageOptimizer.js +225 -0
  169. package/dist/images/ImageOptimizer.js.map +1 -0
  170. package/dist/index.d.ts +96 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +193 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/memory/MemoryControls.d.ts +26 -0
  175. package/dist/memory/MemoryControls.d.ts.map +1 -0
  176. package/dist/memory/MemoryControls.js +388 -0
  177. package/dist/memory/MemoryControls.js.map +1 -0
  178. package/dist/memory/MemoryStore.d.ts +122 -0
  179. package/dist/memory/MemoryStore.d.ts.map +1 -0
  180. package/dist/memory/MemoryStore.js +1096 -0
  181. package/dist/memory/MemoryStore.js.map +1 -0
  182. package/dist/profiles/ProfileManager.d.ts +139 -0
  183. package/dist/profiles/ProfileManager.d.ts.map +1 -0
  184. package/dist/profiles/ProfileManager.js +475 -0
  185. package/dist/profiles/ProfileManager.js.map +1 -0
  186. package/dist/profiles/ProfileResolver.d.ts +18 -0
  187. package/dist/profiles/ProfileResolver.d.ts.map +1 -0
  188. package/dist/profiles/ProfileResolver.js +280 -0
  189. package/dist/profiles/ProfileResolver.js.map +1 -0
  190. package/dist/runs/RunRegistry.d.ts +140 -0
  191. package/dist/runs/RunRegistry.d.ts.map +1 -0
  192. package/dist/runs/RunRegistry.js +432 -0
  193. package/dist/runs/RunRegistry.js.map +1 -0
  194. package/dist/runtime/MarifoldRuntime.d.ts +244 -0
  195. package/dist/runtime/MarifoldRuntime.d.ts.map +1 -0
  196. package/dist/runtime/MarifoldRuntime.js +1099 -0
  197. package/dist/runtime/MarifoldRuntime.js.map +1 -0
  198. package/dist/runtime/MarifoldTypes.d.ts +70 -0
  199. package/dist/runtime/MarifoldTypes.d.ts.map +1 -0
  200. package/dist/runtime/MarifoldTypes.js +3 -0
  201. package/dist/runtime/MarifoldTypes.js.map +1 -0
  202. package/dist/schedule/ScheduleStore.d.ts +60 -0
  203. package/dist/schedule/ScheduleStore.d.ts.map +1 -0
  204. package/dist/schedule/ScheduleStore.js +212 -0
  205. package/dist/schedule/ScheduleStore.js.map +1 -0
  206. package/dist/schedule/Scheduler.d.ts +28 -0
  207. package/dist/schedule/Scheduler.d.ts.map +1 -0
  208. package/dist/schedule/Scheduler.js +65 -0
  209. package/dist/schedule/Scheduler.js.map +1 -0
  210. package/dist/search/DuckDuckGoBackend.d.ts +18 -0
  211. package/dist/search/DuckDuckGoBackend.d.ts.map +1 -0
  212. package/dist/search/DuckDuckGoBackend.js +37 -0
  213. package/dist/search/DuckDuckGoBackend.js.map +1 -0
  214. package/dist/search/FirecrawlBackend.d.ts +28 -0
  215. package/dist/search/FirecrawlBackend.d.ts.map +1 -0
  216. package/dist/search/FirecrawlBackend.js +76 -0
  217. package/dist/search/FirecrawlBackend.js.map +1 -0
  218. package/dist/search/SearchBackend.d.ts +18 -0
  219. package/dist/search/SearchBackend.d.ts.map +1 -0
  220. package/dist/search/SearchBackend.js +24 -0
  221. package/dist/search/SearchBackend.js.map +1 -0
  222. package/dist/search/createSearchBackend.d.ts +16 -0
  223. package/dist/search/createSearchBackend.d.ts.map +1 -0
  224. package/dist/search/createSearchBackend.js +30 -0
  225. package/dist/search/createSearchBackend.js.map +1 -0
  226. package/dist/sessions/ResponseMetrics.d.ts +17 -0
  227. package/dist/sessions/ResponseMetrics.d.ts.map +1 -0
  228. package/dist/sessions/ResponseMetrics.js +3 -0
  229. package/dist/sessions/ResponseMetrics.js.map +1 -0
  230. package/dist/sessions/SessionResolver.d.ts +135 -0
  231. package/dist/sessions/SessionResolver.d.ts.map +1 -0
  232. package/dist/sessions/SessionResolver.js +1173 -0
  233. package/dist/sessions/SessionResolver.js.map +1 -0
  234. package/dist/skill/BuiltInSkillManager.d.ts +11 -0
  235. package/dist/skill/BuiltInSkillManager.d.ts.map +1 -0
  236. package/dist/skill/BuiltInSkillManager.js +98 -0
  237. package/dist/skill/BuiltInSkillManager.js.map +1 -0
  238. package/dist/skill/SkillInvocation.d.ts +30 -0
  239. package/dist/skill/SkillInvocation.d.ts.map +1 -0
  240. package/dist/skill/SkillInvocation.js +135 -0
  241. package/dist/skill/SkillInvocation.js.map +1 -0
  242. package/dist/skill/SkillSchema.d.ts +37 -0
  243. package/dist/skill/SkillSchema.d.ts.map +1 -0
  244. package/dist/skill/SkillSchema.js +26 -0
  245. package/dist/skill/SkillSchema.js.map +1 -0
  246. package/dist/skill/SkillStore.d.ts +41 -0
  247. package/dist/skill/SkillStore.d.ts.map +1 -0
  248. package/dist/skill/SkillStore.js +184 -0
  249. package/dist/skill/SkillStore.js.map +1 -0
  250. package/dist/skill/SkillTemplater.d.ts +19 -0
  251. package/dist/skill/SkillTemplater.d.ts.map +1 -0
  252. package/dist/skill/SkillTemplater.js +36 -0
  253. package/dist/skill/SkillTemplater.js.map +1 -0
  254. package/dist/skill/SkillValidator.d.ts +10 -0
  255. package/dist/skill/SkillValidator.d.ts.map +1 -0
  256. package/dist/skill/SkillValidator.js +131 -0
  257. package/dist/skill/SkillValidator.js.map +1 -0
  258. package/dist/skill/SkillVars.d.ts +10 -0
  259. package/dist/skill/SkillVars.d.ts.map +1 -0
  260. package/dist/skill/SkillVars.js +65 -0
  261. package/dist/skill/SkillVars.js.map +1 -0
  262. package/dist/skill/index.d.ts +10 -0
  263. package/dist/skill/index.d.ts.map +1 -0
  264. package/dist/skill/index.js +22 -0
  265. package/dist/skill/index.js.map +1 -0
  266. package/dist/skillapp/SkillAppActionResolver.d.ts +13 -0
  267. package/dist/skillapp/SkillAppActionResolver.d.ts.map +1 -0
  268. package/dist/skillapp/SkillAppActionResolver.js +95 -0
  269. package/dist/skillapp/SkillAppActionResolver.js.map +1 -0
  270. package/dist/skillapp/SkillAppSchema.d.ts +79 -0
  271. package/dist/skillapp/SkillAppSchema.d.ts.map +1 -0
  272. package/dist/skillapp/SkillAppSchema.js +20 -0
  273. package/dist/skillapp/SkillAppSchema.js.map +1 -0
  274. package/dist/skillapp/SkillAppStore.d.ts +11 -0
  275. package/dist/skillapp/SkillAppStore.d.ts.map +1 -0
  276. package/dist/skillapp/SkillAppStore.js +98 -0
  277. package/dist/skillapp/SkillAppStore.js.map +1 -0
  278. package/dist/skillapp/SkillAppValidator.d.ts +10 -0
  279. package/dist/skillapp/SkillAppValidator.d.ts.map +1 -0
  280. package/dist/skillapp/SkillAppValidator.js +522 -0
  281. package/dist/skillapp/SkillAppValidator.js.map +1 -0
  282. package/dist/tasks/TaskStore.d.ts +110 -0
  283. package/dist/tasks/TaskStore.d.ts.map +1 -0
  284. package/dist/tasks/TaskStore.js +371 -0
  285. package/dist/tasks/TaskStore.js.map +1 -0
  286. package/dist/util/fetchRetry.d.ts +11 -0
  287. package/dist/util/fetchRetry.d.ts.map +1 -0
  288. package/dist/util/fetchRetry.js +48 -0
  289. package/dist/util/fetchRetry.js.map +1 -0
  290. package/dist/util/idToken.d.ts +12 -0
  291. package/dist/util/idToken.d.ts.map +1 -0
  292. package/dist/util/idToken.js +32 -0
  293. package/dist/util/idToken.js.map +1 -0
  294. package/dist/util/proxy.d.ts +18 -0
  295. package/dist/util/proxy.d.ts.map +1 -0
  296. package/dist/util/proxy.js +28 -0
  297. package/dist/util/proxy.js.map +1 -0
  298. package/dist/workspace/Workspace.d.ts +9 -0
  299. package/dist/workspace/Workspace.d.ts.map +1 -0
  300. package/dist/workspace/Workspace.js +55 -0
  301. package/dist/workspace/Workspace.js.map +1 -0
  302. package/dist/workspace/WorkspaceInitializer.d.ts +40 -0
  303. package/dist/workspace/WorkspaceInitializer.d.ts.map +1 -0
  304. package/dist/workspace/WorkspaceInitializer.js +212 -0
  305. package/dist/workspace/WorkspaceInitializer.js.map +1 -0
  306. package/dist/workspace/WorkspacePaths.d.ts +11 -0
  307. package/dist/workspace/WorkspacePaths.d.ts.map +1 -0
  308. package/dist/workspace/WorkspacePaths.js +82 -0
  309. package/dist/workspace/WorkspacePaths.js.map +1 -0
  310. package/package.json +36 -0
@@ -0,0 +1,1173 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.SessionResolver = void 0;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
43
+ const core_1 = require("@priest-ai/core");
44
+ const MarifoldError_1 = require("../errors/MarifoldError");
45
+ const ATTACHMENTS_TABLE = 'marifold_turn_attachments';
46
+ const RESPONSE_METRICS_TABLE = 'marifold_response_metrics';
47
+ const SESSION_DISPLAY_TABLE = 'marifold_session_display';
48
+ const PROFILE_DISPLAY_TABLE = 'marifold_profile_display';
49
+ const DEFAULT_IMAGE_MEDIA_TYPE = 'image/jpeg';
50
+ const COMPACTION_METADATA_KEY = '__compaction';
51
+ const SESSION_TITLE_MAX_CHARS = 200;
52
+ class SessionResolver {
53
+ sessionsDb;
54
+ store;
55
+ constructor(sessionsDb) {
56
+ this.sessionsDb = sessionsDb;
57
+ }
58
+ /** Open a connection with crash-resilience pragmas. `journal_mode = WAL` is a
59
+ * persistent file property — set by any connection it sticks and is inherited
60
+ * by every later opener (including priest's session store), making interrupted
61
+ * writes far less likely to corrupt the file. `synchronous` and `busy_timeout`
62
+ * are per-connection, so they are reapplied on every open. */
63
+ open() {
64
+ const db = new better_sqlite3_1.default(this.sessionsDb, { fileMustExist: true });
65
+ db.pragma('journal_mode = WAL');
66
+ db.pragma('synchronous = NORMAL');
67
+ db.pragma('busy_timeout = 5000');
68
+ return db;
69
+ }
70
+ /** Read-only health check. Safe to run when the DB is corrupt (it is the whole
71
+ * point — `marifold doctor` must work when the app won't start), and never
72
+ * throws: failures are returned as `{ ok: false }`. Opens read/write like the
73
+ * app does, so a pass means the app can actually read the DB. */
74
+ checkIntegrity() {
75
+ if (!fs.existsSync(this.sessionsDb))
76
+ return { ok: true, exists: false };
77
+ let db;
78
+ try {
79
+ db = new better_sqlite3_1.default(this.sessionsDb, { fileMustExist: true });
80
+ }
81
+ catch (error) {
82
+ return { ok: false, exists: true, error: `cannot open: ${String(error)}` };
83
+ }
84
+ try {
85
+ const rows = db.pragma('integrity_check');
86
+ const results = rows.map(row => row.integrity_check);
87
+ if (results.length === 1 && results[0] === 'ok') {
88
+ const sessions = db.prepare('SELECT count(*) AS c FROM sessions').get().c;
89
+ const turns = db.prepare('SELECT count(*) AS c FROM turns').get().c;
90
+ return { ok: true, exists: true, sessions, turns };
91
+ }
92
+ return { ok: false, exists: true, error: results.slice(0, 3).join('; ') };
93
+ }
94
+ catch (error) {
95
+ return { ok: false, exists: true, error: String(error) };
96
+ }
97
+ finally {
98
+ db.close();
99
+ }
100
+ }
101
+ openStore() {
102
+ if (!this.store) {
103
+ fs.mkdirSync(path.dirname(this.sessionsDb), { recursive: true });
104
+ this.store = new core_1.SQLiteSessionStore(this.sessionsDb);
105
+ this.store.open();
106
+ }
107
+ return this.store;
108
+ }
109
+ list(limit = 50, profileName, options = {}) {
110
+ if (!fs.existsSync(this.sessionsDb))
111
+ return [];
112
+ const db = this.open();
113
+ try {
114
+ this.ensureSessionDisplayTable(db);
115
+ const search = options.search?.trim().toLowerCase() ?? '';
116
+ const filters = [
117
+ ...(profileName ? ['s.profile_name = ?'] : []),
118
+ ['COALESCE(d.archived, 0) = ?'],
119
+ ...(search ? [`(
120
+ LOWER(COALESCE(d.title, '')) LIKE ?
121
+ OR LOWER(COALESCE((
122
+ SELECT content FROM turns
123
+ WHERE session_id = s.id AND role = 'user'
124
+ ORDER BY id ASC
125
+ LIMIT 1
126
+ ), '')) LIKE ?
127
+ )`] : []),
128
+ ].flat();
129
+ const params = [
130
+ ...(profileName ? [profileName] : []),
131
+ options.archived ? 1 : 0,
132
+ ...(search ? [`%${search}%`, `%${search}%`] : []),
133
+ limit,
134
+ ];
135
+ const orderBy = options.order === 'recent'
136
+ ? 's.updated_at DESC'
137
+ : 'COALESCE(d.pinned, 0) DESC, s.updated_at DESC';
138
+ const rows = db.prepare(`
139
+ SELECT
140
+ s.id AS id,
141
+ s.profile_name AS profileName,
142
+ s.created_at AS createdAt,
143
+ s.updated_at AS updatedAt,
144
+ d.title AS title,
145
+ d.pinned AS pinned,
146
+ d.archived AS archived,
147
+ COUNT(t.id) AS turnCount,
148
+ (
149
+ SELECT content FROM turns
150
+ WHERE session_id = s.id AND role = 'user'
151
+ ORDER BY id ASC
152
+ LIMIT 1
153
+ ) AS preview
154
+ FROM sessions s
155
+ LEFT JOIN turns t ON t.session_id = s.id
156
+ LEFT JOIN ${SESSION_DISPLAY_TABLE} d ON d.session_id = s.id
157
+ WHERE ${filters.join(' AND ')}
158
+ GROUP BY s.id
159
+ ORDER BY ${orderBy}
160
+ LIMIT ?
161
+ `).all(...params);
162
+ return rows.map(row => {
163
+ const preview = row.preview ? sessionPreview(row.preview) : '';
164
+ return {
165
+ id: row.id,
166
+ profileName: row.profileName,
167
+ createdAt: row.createdAt,
168
+ updatedAt: row.updatedAt,
169
+ turnCount: Number(row.turnCount),
170
+ ...(preview ? { preview } : {}),
171
+ ...(row.title ? { title: row.title } : {}),
172
+ ...(row.pinned === 1 ? { pinned: true } : {}),
173
+ ...(row.archived === 1 ? { archived: true } : {}),
174
+ };
175
+ });
176
+ }
177
+ catch (error) {
178
+ throw new MarifoldError_1.MarifoldError('SESSION_STORE_ERROR', `Could not list sessions from ${this.sessionsDb}: ${String(error)}`, { sessionsDb: this.sessionsDb });
179
+ }
180
+ finally {
181
+ db.close();
182
+ }
183
+ }
184
+ /** Contact-list metadata for every profile represented either by a session
185
+ * or a pinned display row. The latest session determines activity/preview. */
186
+ profileActivity() {
187
+ if (!fs.existsSync(this.sessionsDb))
188
+ return [];
189
+ const db = this.open();
190
+ try {
191
+ this.ensureProfileDisplayTable(db);
192
+ const rows = db.prepare(`
193
+ SELECT
194
+ p.profile_name AS profileName,
195
+ p.pinned AS pinned,
196
+ s.updated_at AS updatedAt,
197
+ (
198
+ SELECT content
199
+ FROM turns
200
+ WHERE session_id = s.id AND role = 'assistant'
201
+ ORDER BY id DESC
202
+ LIMIT 1
203
+ ) AS response
204
+ FROM ${PROFILE_DISPLAY_TABLE} p
205
+ LEFT JOIN sessions s ON s.id = (
206
+ SELECT latest.id
207
+ FROM sessions latest
208
+ WHERE latest.profile_name = p.profile_name
209
+ ORDER BY latest.updated_at DESC, latest.id DESC
210
+ LIMIT 1
211
+ )
212
+ UNION ALL
213
+ SELECT
214
+ s.profile_name AS profileName,
215
+ 0 AS pinned,
216
+ s.updated_at AS updatedAt,
217
+ (
218
+ SELECT content
219
+ FROM turns
220
+ WHERE session_id = s.id AND role = 'assistant'
221
+ ORDER BY id DESC
222
+ LIMIT 1
223
+ ) AS response
224
+ FROM sessions s
225
+ WHERE s.id = (
226
+ SELECT latest.id
227
+ FROM sessions latest
228
+ WHERE latest.profile_name = s.profile_name
229
+ ORDER BY latest.updated_at DESC, latest.id DESC
230
+ LIMIT 1
231
+ )
232
+ AND NOT EXISTS (
233
+ SELECT 1
234
+ FROM ${PROFILE_DISPLAY_TABLE} p
235
+ WHERE p.profile_name = s.profile_name
236
+ )
237
+ `).all();
238
+ return rows.map(row => {
239
+ const preview = row.response ? firstLinePreview(row.response) : '';
240
+ return {
241
+ profileName: row.profileName,
242
+ ...(row.pinned === 1 ? { pinned: true } : {}),
243
+ ...(row.updatedAt ? { updatedAt: row.updatedAt } : {}),
244
+ ...(preview ? { preview } : {}),
245
+ };
246
+ });
247
+ }
248
+ catch (error) {
249
+ throw this.storeError(`Could not list profile activity from ${this.sessionsDb}: ${String(error)}`);
250
+ }
251
+ finally {
252
+ db.close();
253
+ }
254
+ }
255
+ setProfilePinned(profileName, pinned) {
256
+ if (!fs.existsSync(this.sessionsDb))
257
+ this.openStore();
258
+ const db = this.open();
259
+ try {
260
+ this.ensureProfileDisplayTable(db);
261
+ db.prepare(`
262
+ INSERT INTO ${PROFILE_DISPLAY_TABLE} (profile_name, pinned)
263
+ VALUES (?, ?)
264
+ ON CONFLICT(profile_name) DO UPDATE SET pinned = excluded.pinned
265
+ `).run(profileName, pinned ? 1 : 0);
266
+ }
267
+ catch (error) {
268
+ throw this.storeError(`Could not update profile '${profileName}' display state: ${String(error)}`);
269
+ }
270
+ finally {
271
+ db.close();
272
+ }
273
+ }
274
+ deleteProfileDisplay(profileName) {
275
+ if (!fs.existsSync(this.sessionsDb))
276
+ return;
277
+ const db = this.open();
278
+ try {
279
+ if (this.hasProfileDisplayTable(db)) {
280
+ db.prepare(`DELETE FROM ${PROFILE_DISPLAY_TABLE} WHERE profile_name = ?`).run(profileName);
281
+ }
282
+ }
283
+ catch (error) {
284
+ throw this.storeError(`Could not remove profile '${profileName}' display state: ${String(error)}`);
285
+ }
286
+ finally {
287
+ db.close();
288
+ }
289
+ }
290
+ latest(profileName) {
291
+ if (!fs.existsSync(this.sessionsDb))
292
+ return undefined;
293
+ const db = this.open();
294
+ let id;
295
+ try {
296
+ const row = db.prepare(`
297
+ SELECT id
298
+ FROM sessions
299
+ ${profileName ? 'WHERE profile_name = ?' : ''}
300
+ ORDER BY updated_at DESC
301
+ LIMIT 1
302
+ `).get(...(profileName ? [profileName] : []));
303
+ id = row?.id;
304
+ }
305
+ catch (error) {
306
+ throw this.storeError(`Could not find the latest session in ${this.sessionsDb}: ${String(error)}`);
307
+ }
308
+ finally {
309
+ db.close();
310
+ }
311
+ // Pinning only affects list presentation. Resume-last must continue to use
312
+ // actual conversation recency.
313
+ return id ? this.get(id) : undefined;
314
+ }
315
+ get(sessionId) {
316
+ if (!fs.existsSync(this.sessionsDb))
317
+ return undefined;
318
+ const db = this.open();
319
+ try {
320
+ this.ensureSessionDisplayTable(db);
321
+ const row = db.prepare(`
322
+ SELECT
323
+ s.id AS id,
324
+ s.profile_name AS profileName,
325
+ s.created_at AS createdAt,
326
+ s.updated_at AS updatedAt,
327
+ d.title AS title,
328
+ d.pinned AS pinned,
329
+ d.archived AS archived,
330
+ COUNT(t.id) AS turnCount,
331
+ (
332
+ SELECT content FROM turns
333
+ WHERE session_id = s.id AND role = 'user'
334
+ ORDER BY id ASC
335
+ LIMIT 1
336
+ ) AS preview
337
+ FROM sessions s
338
+ LEFT JOIN turns t ON t.session_id = s.id
339
+ LEFT JOIN ${SESSION_DISPLAY_TABLE} d ON d.session_id = s.id
340
+ WHERE s.id = ?
341
+ GROUP BY s.id
342
+ `).get(sessionId);
343
+ if (!row)
344
+ return undefined;
345
+ const preview = row.preview ? sessionPreview(row.preview) : '';
346
+ return {
347
+ id: row.id,
348
+ profileName: row.profileName,
349
+ createdAt: row.createdAt,
350
+ updatedAt: row.updatedAt,
351
+ turnCount: Number(row.turnCount),
352
+ ...(preview ? { preview } : {}),
353
+ ...(row.title ? { title: row.title } : {}),
354
+ ...(row.pinned === 1 ? { pinned: true } : {}),
355
+ ...(row.archived === 1 ? { archived: true } : {}),
356
+ turns: this.listTurns(db, row.id),
357
+ };
358
+ }
359
+ catch (error) {
360
+ throw this.storeError(`Could not read session '${sessionId}' from ${this.sessionsDb}: ${String(error)}`);
361
+ }
362
+ finally {
363
+ db.close();
364
+ }
365
+ }
366
+ /** Read one persisted image without including its bytes in the session
367
+ * transcript payload. The service exposes this through an authenticated
368
+ * binary route so clients can fetch only visible/previewed images. */
369
+ getAttachment(sessionId, userTurnIndex, attachmentIndex) {
370
+ if (!Number.isInteger(userTurnIndex) || userTurnIndex < 0
371
+ || !Number.isInteger(attachmentIndex) || attachmentIndex < 0) {
372
+ throw MarifoldError_1.MarifoldError.configInvalid('Attachment coordinates must be non-negative integers.');
373
+ }
374
+ if (!fs.existsSync(this.sessionsDb))
375
+ return undefined;
376
+ const db = this.open();
377
+ try {
378
+ if (!this.hasAttachmentsTable(db))
379
+ return undefined;
380
+ const row = db.prepare(`
381
+ SELECT media_type AS mediaType, data, url
382
+ FROM ${ATTACHMENTS_TABLE}
383
+ WHERE session_id = ? AND user_turn_index = ? AND attachment_index = ?
384
+ `).get(sessionId, userTurnIndex, attachmentIndex);
385
+ if (!row)
386
+ return undefined;
387
+ return {
388
+ mediaType: row.mediaType,
389
+ ...(row.data !== null ? { data: row.data } : {}),
390
+ ...(row.url !== null ? { url: row.url } : {}),
391
+ };
392
+ }
393
+ catch (error) {
394
+ if (error instanceof MarifoldError_1.MarifoldError)
395
+ throw error;
396
+ throw this.storeError(`Could not read an attachment for session '${sessionId}' from ${this.sessionsDb}: ${String(error)}`);
397
+ }
398
+ finally {
399
+ db.close();
400
+ }
401
+ }
402
+ delete(sessionId) {
403
+ if (!fs.existsSync(this.sessionsDb))
404
+ return false;
405
+ const db = this.open();
406
+ try {
407
+ const transaction = db.transaction(() => {
408
+ this.deleteAttachmentsForSession(db, sessionId);
409
+ this.deleteResponseMetricsForSession(db, sessionId);
410
+ this.deleteDisplayForSession(db, sessionId);
411
+ db.prepare('DELETE FROM turns WHERE session_id = ?').run(sessionId);
412
+ return db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId).changes;
413
+ });
414
+ return transaction() > 0;
415
+ }
416
+ catch (error) {
417
+ throw this.storeError(`Could not delete session '${sessionId}' from ${this.sessionsDb}: ${String(error)}`);
418
+ }
419
+ finally {
420
+ db.close();
421
+ }
422
+ }
423
+ /** Update sidebar-only session fields without touching ids, turns, recency,
424
+ * or Priest's metadata column. A separate table prevents a model save that
425
+ * finishes later from overwriting a rename/pin made during the run. */
426
+ updateDisplay(sessionId, update) {
427
+ if (!fs.existsSync(this.sessionsDb))
428
+ return false;
429
+ if (update.title === undefined && update.pinned === undefined && update.archived === undefined) {
430
+ throw MarifoldError_1.MarifoldError.configInvalid('At least one of title, pinned, or archived is required.');
431
+ }
432
+ const title = update.title === null ? null : update.title?.trim();
433
+ if (title !== undefined && title !== null && title.length === 0) {
434
+ throw MarifoldError_1.MarifoldError.configInvalid('Session title cannot be empty.');
435
+ }
436
+ if (title !== undefined && title !== null && title.length > SESSION_TITLE_MAX_CHARS) {
437
+ throw MarifoldError_1.MarifoldError.configInvalid(`Session title cannot exceed ${SESSION_TITLE_MAX_CHARS} characters.`);
438
+ }
439
+ const db = this.open();
440
+ try {
441
+ if (!db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId))
442
+ return false;
443
+ this.ensureSessionDisplayTable(db);
444
+ const current = db.prepare(`
445
+ SELECT title, pinned, archived
446
+ FROM ${SESSION_DISPLAY_TABLE}
447
+ WHERE session_id = ?
448
+ `).get(sessionId);
449
+ const nextTitle = title === undefined ? current?.title ?? null : title;
450
+ const nextPinned = update.pinned === undefined ? current?.pinned === 1 : update.pinned;
451
+ const nextArchived = update.archived === undefined ? current?.archived === 1 : update.archived;
452
+ if (nextTitle === null && !nextPinned && !nextArchived) {
453
+ db.prepare(`DELETE FROM ${SESSION_DISPLAY_TABLE} WHERE session_id = ?`).run(sessionId);
454
+ }
455
+ else {
456
+ db.prepare(`
457
+ INSERT INTO ${SESSION_DISPLAY_TABLE} (session_id, title, pinned, archived)
458
+ VALUES (?, ?, ?, ?)
459
+ ON CONFLICT(session_id) DO UPDATE SET
460
+ title = excluded.title,
461
+ pinned = excluded.pinned,
462
+ archived = excluded.archived
463
+ `).run(sessionId, nextTitle, nextPinned ? 1 : 0, nextArchived ? 1 : 0);
464
+ }
465
+ return true;
466
+ }
467
+ catch (error) {
468
+ if (error instanceof MarifoldError_1.MarifoldError)
469
+ throw error;
470
+ throw this.storeError(`Could not update session '${sessionId}' in ${this.sessionsDb}: ${String(error)}`);
471
+ }
472
+ finally {
473
+ db.close();
474
+ }
475
+ }
476
+ /** Delete one user turn and the entire conversation branch after it. The
477
+ * ordinal is zero-based among user turns, so it remains stable even though
478
+ * Priest rewrites SQLite turn ids whenever a session is saved. */
479
+ truncateFromUserTurn(sessionId, userTurnIndex) {
480
+ if (!Number.isInteger(userTurnIndex) || userTurnIndex < 0) {
481
+ throw MarifoldError_1.MarifoldError.configInvalid('userTurnIndex must be a non-negative integer.');
482
+ }
483
+ if (!fs.existsSync(this.sessionsDb))
484
+ return { found: false, removedTurns: 0 };
485
+ const db = this.open();
486
+ try {
487
+ const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId);
488
+ if (!session)
489
+ return { found: false, removedTurns: 0 };
490
+ const target = db.prepare(`
491
+ SELECT id
492
+ FROM turns
493
+ WHERE session_id = ? AND role = 'user'
494
+ ORDER BY id ASC
495
+ LIMIT 1 OFFSET ?
496
+ `).get(sessionId, userTurnIndex);
497
+ const transaction = db.transaction(() => {
498
+ if (this.hasAttachmentsTable(db)) {
499
+ db.prepare(`
500
+ DELETE FROM ${ATTACHMENTS_TABLE}
501
+ WHERE session_id = ? AND user_turn_index >= ?
502
+ `).run(sessionId, userTurnIndex);
503
+ }
504
+ if (this.hasResponseMetricsTable(db)) {
505
+ db.prepare(`
506
+ DELETE FROM ${RESPONSE_METRICS_TABLE}
507
+ WHERE session_id = ? AND user_turn_index >= ?
508
+ `).run(sessionId, userTurnIndex);
509
+ }
510
+ if (!target)
511
+ return 0;
512
+ const removedTurns = db.prepare(`
513
+ DELETE FROM turns
514
+ WHERE session_id = ? AND id >= ?
515
+ `).run(sessionId, target.id).changes;
516
+ if (removedTurns > 0) {
517
+ db.prepare(`
518
+ UPDATE sessions
519
+ SET updated_at = strftime('%Y-%m-%dT%H:%M:%f000+00:00', 'now')
520
+ WHERE id = ?
521
+ `).run(sessionId);
522
+ }
523
+ return removedTurns;
524
+ });
525
+ return { found: true, removedTurns: transaction() };
526
+ }
527
+ catch (error) {
528
+ if (error instanceof MarifoldError_1.MarifoldError)
529
+ throw error;
530
+ throw this.storeError(`Could not truncate session '${sessionId}' in ${this.sessionsDb}: ${String(error)}`);
531
+ }
532
+ finally {
533
+ db.close();
534
+ }
535
+ }
536
+ /** Return only the completed turns before a zero-based user-turn ordinal.
537
+ * Historical edits use this as model context so later exchanges cannot leak
538
+ * into the regenerated answer. `undefined` means the session/turn is absent. */
539
+ turnsBeforeUserTurn(sessionId, userTurnIndex) {
540
+ if (!Number.isInteger(userTurnIndex) || userTurnIndex < 0) {
541
+ throw MarifoldError_1.MarifoldError.configInvalid('userTurnIndex must be a non-negative integer.');
542
+ }
543
+ const detail = this.get(sessionId);
544
+ if (!detail)
545
+ return undefined;
546
+ const before = [];
547
+ let currentUserIndex = 0;
548
+ for (const turn of detail.turns) {
549
+ if (turn.role === 'user') {
550
+ if (currentUserIndex === userTurnIndex)
551
+ return before;
552
+ currentUserIndex += 1;
553
+ }
554
+ before.push(turn);
555
+ }
556
+ return undefined;
557
+ }
558
+ /** Replace one persisted user→assistant exchange in place. Later exchanges
559
+ * keep their ids/order and therefore remain visible and available as future
560
+ * model context. Attachments are replaced only when `images` is provided. */
561
+ replaceExchange(sessionId, userTurnIndex, userText, assistantText, images, responseMetrics) {
562
+ if (!Number.isInteger(userTurnIndex) || userTurnIndex < 0) {
563
+ throw MarifoldError_1.MarifoldError.configInvalid('userTurnIndex must be a non-negative integer.');
564
+ }
565
+ if (!fs.existsSync(this.sessionsDb))
566
+ return { found: false, replaced: false };
567
+ const db = this.open();
568
+ try {
569
+ const session = db.prepare('SELECT id, metadata FROM sessions WHERE id = ?').get(sessionId);
570
+ if (!session)
571
+ return { found: false, replaced: false };
572
+ const target = db.prepare(`
573
+ SELECT id
574
+ FROM turns
575
+ WHERE session_id = ? AND role = 'user'
576
+ ORDER BY id ASC
577
+ LIMIT 1 OFFSET ?
578
+ `).get(sessionId, userTurnIndex);
579
+ if (!target)
580
+ return { found: true, replaced: false };
581
+ const nextUser = db.prepare(`
582
+ SELECT id
583
+ FROM turns
584
+ WHERE session_id = ? AND role = 'user' AND id > ?
585
+ ORDER BY id ASC
586
+ LIMIT 1
587
+ `).get(sessionId, target.id);
588
+ const assistant = db.prepare(`
589
+ SELECT id
590
+ FROM turns
591
+ WHERE session_id = ? AND role = 'assistant' AND id > ?
592
+ ${nextUser ? 'AND id < ?' : ''}
593
+ ORDER BY id ASC
594
+ LIMIT 1
595
+ `).get(...(nextUser
596
+ ? [sessionId, target.id, nextUser.id]
597
+ : [sessionId, target.id]));
598
+ if (!assistant)
599
+ return { found: true, replaced: false };
600
+ const transaction = db.transaction(() => {
601
+ db.prepare('UPDATE turns SET content = ? WHERE id = ?').run(userText, target.id);
602
+ db.prepare('UPDATE turns SET content = ? WHERE id = ?').run(assistantText, assistant.id);
603
+ if (images !== undefined)
604
+ this.replaceUserTurnAttachments(db, sessionId, userTurnIndex, images);
605
+ if (responseMetrics) {
606
+ this.upsertResponseMetrics(db, sessionId, userTurnIndex, responseMetrics);
607
+ }
608
+ else if (this.hasResponseMetricsTable(db)) {
609
+ // Replacing response content without replacement metrics must not
610
+ // leave the old response's timing/model data attached to new prose.
611
+ db.prepare(`
612
+ DELETE FROM ${RESPONSE_METRICS_TABLE}
613
+ WHERE session_id = ? AND user_turn_index = ?
614
+ `).run(sessionId, userTurnIndex);
615
+ }
616
+ const metadata = JSON.parse(session.metadata);
617
+ delete metadata[COMPACTION_METADATA_KEY];
618
+ db.prepare(`
619
+ UPDATE sessions
620
+ SET updated_at = strftime('%Y-%m-%dT%H:%M:%f000+00:00', 'now'),
621
+ metadata = ?
622
+ WHERE id = ?
623
+ `).run(JSON.stringify(metadata), sessionId);
624
+ });
625
+ transaction();
626
+ return { found: true, replaced: true };
627
+ }
628
+ catch (error) {
629
+ if (error instanceof MarifoldError_1.MarifoldError)
630
+ throw error;
631
+ throw this.storeError(`Could not replace exchange ${userTurnIndex} in session '${sessionId}': ${String(error)}`);
632
+ }
633
+ finally {
634
+ db.close();
635
+ }
636
+ }
637
+ clear(options = {}) {
638
+ if (!fs.existsSync(this.sessionsDb))
639
+ return { count: 0, ids: [] };
640
+ const keepLast = options.keepLast ?? 0;
641
+ if (!Number.isInteger(keepLast) || keepLast < 0) {
642
+ throw MarifoldError_1.MarifoldError.configInvalid('keepLast must be a non-negative integer.');
643
+ }
644
+ const db = this.open();
645
+ try {
646
+ const where = [];
647
+ const params = [];
648
+ if (options.profileName) {
649
+ where.push('profile_name = ?');
650
+ params.push(options.profileName);
651
+ }
652
+ if (options.before) {
653
+ where.push('updated_at < ?');
654
+ params.push(options.before);
655
+ }
656
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
657
+ const rows = db.prepare(`
658
+ SELECT id
659
+ FROM sessions
660
+ ${whereSql}
661
+ ORDER BY updated_at DESC
662
+ `).all(...params);
663
+ const ids = rows.map(row => row.id).slice(keepLast);
664
+ if (ids.length === 0)
665
+ return { count: 0, ids: [] };
666
+ const transaction = db.transaction((sessionIds) => {
667
+ const deleteTurns = db.prepare('DELETE FROM turns WHERE session_id = ?');
668
+ const deleteSession = db.prepare('DELETE FROM sessions WHERE id = ?');
669
+ for (const id of sessionIds) {
670
+ this.deleteAttachmentsForSession(db, id);
671
+ this.deleteResponseMetricsForSession(db, id);
672
+ this.deleteDisplayForSession(db, id);
673
+ deleteTurns.run(id);
674
+ deleteSession.run(id);
675
+ }
676
+ });
677
+ transaction(ids);
678
+ return { count: ids.length, ids };
679
+ }
680
+ catch (error) {
681
+ if (error instanceof MarifoldError_1.MarifoldError)
682
+ throw error;
683
+ throw this.storeError(`Could not clear sessions from ${this.sessionsDb}: ${String(error)}`);
684
+ }
685
+ finally {
686
+ db.close();
687
+ }
688
+ }
689
+ replaceLastAssistantTurn(sessionId, content) {
690
+ if (!fs.existsSync(this.sessionsDb))
691
+ return false;
692
+ const db = this.open();
693
+ try {
694
+ const result = db.prepare(`
695
+ UPDATE turns
696
+ SET content = ?
697
+ WHERE id = (
698
+ SELECT id
699
+ FROM turns
700
+ WHERE session_id = ? AND role = 'assistant'
701
+ ORDER BY id DESC
702
+ LIMIT 1
703
+ )
704
+ `).run(content, sessionId);
705
+ return result.changes > 0;
706
+ }
707
+ catch (error) {
708
+ throw this.storeError(`Could not clean session '${sessionId}' in ${this.sessionsDb}: ${String(error)}`);
709
+ }
710
+ finally {
711
+ db.close();
712
+ }
713
+ }
714
+ replaceLastUserTurn(sessionId, content) {
715
+ if (!fs.existsSync(this.sessionsDb))
716
+ return false;
717
+ const db = this.open();
718
+ try {
719
+ const result = db.prepare(`
720
+ UPDATE turns
721
+ SET content = ?
722
+ WHERE id = (
723
+ SELECT id
724
+ FROM turns
725
+ WHERE session_id = ? AND role = 'user'
726
+ ORDER BY id DESC
727
+ LIMIT 1
728
+ )
729
+ `).run(content, sessionId);
730
+ return result.changes > 0;
731
+ }
732
+ catch (error) {
733
+ throw this.storeError(`Could not clean session '${sessionId}' in ${this.sessionsDb}: ${String(error)}`);
734
+ }
735
+ finally {
736
+ db.close();
737
+ }
738
+ }
739
+ /** Append one clean user→assistant exchange to a session (creating it if
740
+ * needed). Agent runs use this to record the objective + final answer as a
741
+ * single tidy pair, instead of priest's raw per-iteration `Objective:`/tool
742
+ * framing that made resumed transcripts confusing. */
743
+ async appendExchange(sessionId, profileName, userText, assistantText, images, responseMetrics) {
744
+ const store = this.openStore();
745
+ const session = (await store.get(sessionId)) ?? (await store.create(profileName, sessionId));
746
+ session.appendTurn('user', userText);
747
+ session.appendTurn('assistant', assistantText);
748
+ await store.save(session);
749
+ this.saveLastUserTurnAttachments(sessionId, images);
750
+ if (responseMetrics)
751
+ this.saveLastResponseMetrics(sessionId, responseMetrics);
752
+ }
753
+ /** Persist display-only image sources against the newest user turn. Priest
754
+ * intentionally stores text-only session history, so Marifold owns this
755
+ * side table and keeps it out of later model context. Local filesystem paths
756
+ * are deliberately skipped rather than exposed through the service API. */
757
+ saveLastUserTurnAttachments(sessionId, images) {
758
+ const persistable = (images ?? []).filter((image) => Boolean(image.data || image.url));
759
+ if (persistable.length === 0 || !fs.existsSync(this.sessionsDb))
760
+ return;
761
+ const db = this.open();
762
+ try {
763
+ this.ensureAttachmentsTable(db);
764
+ const userTurns = db.prepare(`
765
+ SELECT COUNT(*) AS count
766
+ FROM turns
767
+ WHERE session_id = ? AND role = 'user'
768
+ `).get(sessionId);
769
+ if (userTurns.count === 0)
770
+ return;
771
+ const transaction = db.transaction(() => {
772
+ // Priest rewrites the turns table on every session save, so SQLite
773
+ // turn ids are not stable. The zero-based user-turn ordinal is.
774
+ const userTurnIndex = userTurns.count - 1;
775
+ db.prepare(`
776
+ DELETE FROM ${ATTACHMENTS_TABLE}
777
+ WHERE session_id = ? AND user_turn_index = ?
778
+ `).run(sessionId, userTurnIndex);
779
+ const insert = db.prepare(`
780
+ INSERT INTO ${ATTACHMENTS_TABLE}
781
+ (session_id, user_turn_index, attachment_index, media_type, data, url)
782
+ VALUES (?, ?, ?, ?, ?, ?)
783
+ `);
784
+ for (const [index, image] of persistable.entries()) {
785
+ insert.run(sessionId, userTurnIndex, index, image.mediaType ?? DEFAULT_IMAGE_MEDIA_TYPE, image.data ?? null, image.url ?? null);
786
+ }
787
+ });
788
+ transaction();
789
+ }
790
+ catch (error) {
791
+ throw this.storeError(`Could not save attachments for session '${sessionId}' in ${this.sessionsDb}: ${String(error)}`);
792
+ }
793
+ finally {
794
+ db.close();
795
+ }
796
+ }
797
+ /** Persist content-free completion metadata against the newest user turn.
798
+ * Like attachments, the zero-based user-turn ordinal survives Priest's
799
+ * whole-session turn-row rewrites. */
800
+ saveLastResponseMetrics(sessionId, responseMetrics) {
801
+ if (!fs.existsSync(this.sessionsDb))
802
+ return;
803
+ const db = this.open();
804
+ try {
805
+ const userTurns = db.prepare(`
806
+ SELECT COUNT(*) AS count
807
+ FROM turns
808
+ WHERE session_id = ? AND role = 'user'
809
+ `).get(sessionId);
810
+ if (userTurns.count === 0)
811
+ return;
812
+ this.upsertResponseMetrics(db, sessionId, userTurns.count - 1, responseMetrics);
813
+ }
814
+ catch (error) {
815
+ throw this.storeError(`Could not save response metrics for session '${sessionId}' in ${this.sessionsDb}: ${String(error)}`);
816
+ }
817
+ finally {
818
+ db.close();
819
+ }
820
+ }
821
+ rename(fromSessionId, toSessionId) {
822
+ if (!fs.existsSync(this.sessionsDb))
823
+ return false;
824
+ if (!toSessionId.trim())
825
+ throw MarifoldError_1.MarifoldError.configInvalid('New session id cannot be empty.');
826
+ const db = this.open();
827
+ try {
828
+ const exists = db.prepare('SELECT id FROM sessions WHERE id = ?').get(fromSessionId);
829
+ if (!exists)
830
+ return false;
831
+ const conflict = db.prepare('SELECT id FROM sessions WHERE id = ?').get(toSessionId);
832
+ if (conflict) {
833
+ throw new MarifoldError_1.MarifoldError('SESSION_STORE_ERROR', `Session '${toSessionId}' already exists.`, { sessionsDb: this.sessionsDb });
834
+ }
835
+ const transaction = db.transaction(() => {
836
+ db.prepare(`
837
+ INSERT INTO sessions (id, profile_name, created_at, updated_at, metadata)
838
+ SELECT ?, profile_name, created_at, updated_at, metadata
839
+ FROM sessions
840
+ WHERE id = ?
841
+ `).run(toSessionId, fromSessionId);
842
+ if (this.hasAttachmentsTable(db)) {
843
+ db.prepare(`UPDATE ${ATTACHMENTS_TABLE} SET session_id = ? WHERE session_id = ?`)
844
+ .run(toSessionId, fromSessionId);
845
+ }
846
+ if (this.hasResponseMetricsTable(db)) {
847
+ db.prepare(`UPDATE ${RESPONSE_METRICS_TABLE} SET session_id = ? WHERE session_id = ?`)
848
+ .run(toSessionId, fromSessionId);
849
+ }
850
+ if (this.hasSessionDisplayTable(db)) {
851
+ db.prepare(`UPDATE ${SESSION_DISPLAY_TABLE} SET session_id = ? WHERE session_id = ?`)
852
+ .run(toSessionId, fromSessionId);
853
+ }
854
+ db.prepare('UPDATE turns SET session_id = ? WHERE session_id = ?').run(toSessionId, fromSessionId);
855
+ db.prepare('DELETE FROM sessions WHERE id = ?').run(fromSessionId);
856
+ });
857
+ transaction();
858
+ return true;
859
+ }
860
+ catch (error) {
861
+ if (error instanceof MarifoldError_1.MarifoldError)
862
+ throw error;
863
+ throw this.storeError(`Could not rename session '${fromSessionId}' in ${this.sessionsDb}: ${String(error)}`);
864
+ }
865
+ finally {
866
+ db.close();
867
+ }
868
+ }
869
+ close() {
870
+ this.store?.close();
871
+ this.store = undefined;
872
+ }
873
+ listTurns(db, sessionId) {
874
+ const rows = db.prepare(`
875
+ SELECT role, content, timestamp
876
+ FROM turns
877
+ WHERE session_id = ?
878
+ ORDER BY id ASC
879
+ `).all(sessionId);
880
+ const attachments = this.listAttachments(db, sessionId);
881
+ const responseMetrics = this.listResponseMetrics(db, sessionId);
882
+ let userTurnIndex = -1;
883
+ return rows.map(row => {
884
+ if (row.role === 'user')
885
+ userTurnIndex += 1;
886
+ const turnAttachments = row.role === 'user' ? attachments.get(userTurnIndex) : undefined;
887
+ const turnResponseMetrics = row.role === 'assistant' ? responseMetrics.get(userTurnIndex) : undefined;
888
+ return {
889
+ role: row.role,
890
+ content: row.content,
891
+ timestamp: row.timestamp,
892
+ ...(turnAttachments ? { attachments: turnAttachments } : {}),
893
+ ...(turnResponseMetrics ? { responseMetrics: turnResponseMetrics } : {}),
894
+ };
895
+ });
896
+ }
897
+ ensureAttachmentsTable(db) {
898
+ db.exec(`
899
+ CREATE TABLE IF NOT EXISTS ${ATTACHMENTS_TABLE} (
900
+ session_id TEXT NOT NULL,
901
+ user_turn_index INTEGER NOT NULL,
902
+ attachment_index INTEGER NOT NULL,
903
+ media_type TEXT NOT NULL,
904
+ data TEXT,
905
+ url TEXT,
906
+ PRIMARY KEY (session_id, user_turn_index, attachment_index),
907
+ CHECK ((data IS NOT NULL AND url IS NULL) OR (data IS NULL AND url IS NOT NULL))
908
+ );
909
+ CREATE INDEX IF NOT EXISTS idx_marifold_turn_attachments_session
910
+ ON ${ATTACHMENTS_TABLE} (session_id, user_turn_index);
911
+ `);
912
+ }
913
+ hasAttachmentsTable(db) {
914
+ return db.prepare(`
915
+ SELECT 1
916
+ FROM sqlite_master
917
+ WHERE type = 'table' AND name = ?
918
+ `).get(ATTACHMENTS_TABLE) !== undefined;
919
+ }
920
+ listAttachments(db, sessionId) {
921
+ const byTurn = new Map();
922
+ if (!this.hasAttachmentsTable(db))
923
+ return byTurn;
924
+ const rows = db.prepare(`
925
+ SELECT
926
+ a.user_turn_index AS userTurnIndex,
927
+ a.attachment_index AS attachmentIndex,
928
+ a.media_type AS mediaType,
929
+ a.data IS NOT NULL AS embedded,
930
+ a.url AS url
931
+ FROM ${ATTACHMENTS_TABLE} a
932
+ WHERE a.session_id = ?
933
+ ORDER BY a.user_turn_index ASC, a.attachment_index ASC
934
+ `).all(sessionId);
935
+ for (const row of rows) {
936
+ const current = byTurn.get(row.userTurnIndex) ?? [];
937
+ current.push({
938
+ kind: 'image',
939
+ mediaType: row.mediaType,
940
+ ...(row.embedded === 1 ? {
941
+ ref: {
942
+ userTurnIndex: row.userTurnIndex,
943
+ attachmentIndex: row.attachmentIndex,
944
+ },
945
+ } : {}),
946
+ ...(row.url !== null ? { url: row.url } : {}),
947
+ });
948
+ byTurn.set(row.userTurnIndex, current);
949
+ }
950
+ return byTurn;
951
+ }
952
+ replaceUserTurnAttachments(db, sessionId, userTurnIndex, images) {
953
+ const persistable = images.filter((image) => Boolean(image.data || image.url));
954
+ if (persistable.length > 0)
955
+ this.ensureAttachmentsTable(db);
956
+ if (!this.hasAttachmentsTable(db))
957
+ return;
958
+ db.prepare(`
959
+ DELETE FROM ${ATTACHMENTS_TABLE}
960
+ WHERE session_id = ? AND user_turn_index = ?
961
+ `).run(sessionId, userTurnIndex);
962
+ if (persistable.length === 0)
963
+ return;
964
+ const insert = db.prepare(`
965
+ INSERT INTO ${ATTACHMENTS_TABLE}
966
+ (session_id, user_turn_index, attachment_index, media_type, data, url)
967
+ VALUES (?, ?, ?, ?, ?, ?)
968
+ `);
969
+ for (const [index, image] of persistable.entries()) {
970
+ insert.run(sessionId, userTurnIndex, index, image.mediaType ?? DEFAULT_IMAGE_MEDIA_TYPE, image.data ?? null, image.url ?? null);
971
+ }
972
+ }
973
+ deleteAttachmentsForSession(db, sessionId) {
974
+ if (!this.hasAttachmentsTable(db))
975
+ return;
976
+ db.prepare(`DELETE FROM ${ATTACHMENTS_TABLE} WHERE session_id = ?`).run(sessionId);
977
+ }
978
+ ensureResponseMetricsTable(db) {
979
+ db.exec(`
980
+ CREATE TABLE IF NOT EXISTS ${RESPONSE_METRICS_TABLE} (
981
+ session_id TEXT NOT NULL,
982
+ user_turn_index INTEGER NOT NULL CHECK (user_turn_index >= 0),
983
+ mode TEXT NOT NULL CHECK (mode IN ('agent', 'chat')),
984
+ provider TEXT NOT NULL,
985
+ model TEXT NOT NULL,
986
+ think INTEGER NOT NULL CHECK (think IN (0, 1)),
987
+ started_at TEXT NOT NULL,
988
+ finished_at TEXT NOT NULL,
989
+ latency_ms INTEGER NOT NULL CHECK (latency_ms >= 0),
990
+ input_tokens INTEGER CHECK (input_tokens IS NULL OR input_tokens >= 0),
991
+ output_tokens INTEGER CHECK (output_tokens IS NULL OR output_tokens >= 0),
992
+ total_tokens INTEGER CHECK (total_tokens IS NULL OR total_tokens >= 0),
993
+ cached_input_tokens INTEGER CHECK (cached_input_tokens IS NULL OR cached_input_tokens >= 0),
994
+ reasoning_tokens INTEGER CHECK (reasoning_tokens IS NULL OR reasoning_tokens >= 0),
995
+ estimated_cost_usd REAL CHECK (estimated_cost_usd IS NULL OR estimated_cost_usd >= 0),
996
+ PRIMARY KEY (session_id, user_turn_index)
997
+ );
998
+ CREATE INDEX IF NOT EXISTS idx_marifold_response_metrics_finished
999
+ ON ${RESPONSE_METRICS_TABLE} (finished_at);
1000
+ CREATE INDEX IF NOT EXISTS idx_marifold_response_metrics_provider_model
1001
+ ON ${RESPONSE_METRICS_TABLE} (provider, model, finished_at);
1002
+ `);
1003
+ }
1004
+ hasResponseMetricsTable(db) {
1005
+ return db.prepare(`
1006
+ SELECT 1
1007
+ FROM sqlite_master
1008
+ WHERE type = 'table' AND name = ?
1009
+ `).get(RESPONSE_METRICS_TABLE) !== undefined;
1010
+ }
1011
+ upsertResponseMetrics(db, sessionId, userTurnIndex, metrics) {
1012
+ this.ensureResponseMetricsTable(db);
1013
+ db.prepare(`
1014
+ INSERT INTO ${RESPONSE_METRICS_TABLE} (
1015
+ session_id,
1016
+ user_turn_index,
1017
+ mode,
1018
+ provider,
1019
+ model,
1020
+ think,
1021
+ started_at,
1022
+ finished_at,
1023
+ latency_ms,
1024
+ input_tokens,
1025
+ output_tokens,
1026
+ total_tokens,
1027
+ cached_input_tokens,
1028
+ reasoning_tokens,
1029
+ estimated_cost_usd
1030
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1031
+ ON CONFLICT(session_id, user_turn_index) DO UPDATE SET
1032
+ mode = excluded.mode,
1033
+ provider = excluded.provider,
1034
+ model = excluded.model,
1035
+ think = excluded.think,
1036
+ started_at = excluded.started_at,
1037
+ finished_at = excluded.finished_at,
1038
+ latency_ms = excluded.latency_ms,
1039
+ input_tokens = excluded.input_tokens,
1040
+ output_tokens = excluded.output_tokens,
1041
+ total_tokens = excluded.total_tokens,
1042
+ cached_input_tokens = excluded.cached_input_tokens,
1043
+ reasoning_tokens = excluded.reasoning_tokens,
1044
+ estimated_cost_usd = excluded.estimated_cost_usd
1045
+ `).run(sessionId, userTurnIndex, metrics.mode, metrics.provider, metrics.model, metrics.think ? 1 : 0, metrics.startedAt, metrics.finishedAt, nonNegativeInteger(metrics.latencyMs) ?? 0, nonNegativeInteger(metrics.usage?.inputTokens), nonNegativeInteger(metrics.usage?.outputTokens), nonNegativeInteger(metrics.usage?.totalTokens), nonNegativeInteger(metrics.usage?.cachedInputTokens), nonNegativeInteger(metrics.usage?.reasoningTokens), nonNegativeNumber(metrics.usage?.estimatedCostUSD));
1046
+ }
1047
+ listResponseMetrics(db, sessionId) {
1048
+ const byTurn = new Map();
1049
+ if (!this.hasResponseMetricsTable(db))
1050
+ return byTurn;
1051
+ const rows = db.prepare(`
1052
+ SELECT
1053
+ user_turn_index AS userTurnIndex,
1054
+ mode,
1055
+ provider,
1056
+ model,
1057
+ think,
1058
+ started_at AS startedAt,
1059
+ finished_at AS finishedAt,
1060
+ latency_ms AS latencyMs,
1061
+ input_tokens AS inputTokens,
1062
+ output_tokens AS outputTokens,
1063
+ total_tokens AS totalTokens,
1064
+ cached_input_tokens AS cachedInputTokens,
1065
+ reasoning_tokens AS reasoningTokens,
1066
+ estimated_cost_usd AS estimatedCostUSD
1067
+ FROM ${RESPONSE_METRICS_TABLE}
1068
+ WHERE session_id = ?
1069
+ ORDER BY user_turn_index ASC
1070
+ `).all(sessionId);
1071
+ for (const row of rows) {
1072
+ const usage = {
1073
+ ...(row.inputTokens !== null ? { inputTokens: row.inputTokens } : {}),
1074
+ ...(row.outputTokens !== null ? { outputTokens: row.outputTokens } : {}),
1075
+ ...(row.totalTokens !== null ? { totalTokens: row.totalTokens } : {}),
1076
+ ...(row.cachedInputTokens !== null ? { cachedInputTokens: row.cachedInputTokens } : {}),
1077
+ ...(row.reasoningTokens !== null ? { reasoningTokens: row.reasoningTokens } : {}),
1078
+ ...(row.estimatedCostUSD !== null ? { estimatedCostUSD: row.estimatedCostUSD } : {}),
1079
+ };
1080
+ byTurn.set(row.userTurnIndex, {
1081
+ mode: row.mode,
1082
+ provider: row.provider,
1083
+ model: row.model,
1084
+ think: row.think === 1,
1085
+ startedAt: row.startedAt,
1086
+ finishedAt: row.finishedAt,
1087
+ latencyMs: row.latencyMs,
1088
+ ...(Object.keys(usage).length > 0 ? { usage } : {}),
1089
+ });
1090
+ }
1091
+ return byTurn;
1092
+ }
1093
+ deleteResponseMetricsForSession(db, sessionId) {
1094
+ if (!this.hasResponseMetricsTable(db))
1095
+ return;
1096
+ db.prepare(`DELETE FROM ${RESPONSE_METRICS_TABLE} WHERE session_id = ?`).run(sessionId);
1097
+ }
1098
+ ensureSessionDisplayTable(db) {
1099
+ db.exec(`
1100
+ CREATE TABLE IF NOT EXISTS ${SESSION_DISPLAY_TABLE} (
1101
+ session_id TEXT PRIMARY KEY,
1102
+ title TEXT,
1103
+ pinned INTEGER NOT NULL DEFAULT 0 CHECK (pinned IN (0, 1)),
1104
+ archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1))
1105
+ )
1106
+ `);
1107
+ const columns = db.prepare(`PRAGMA table_info(${SESSION_DISPLAY_TABLE})`).all();
1108
+ if (!columns.some(column => column.name === 'archived')) {
1109
+ db.exec(`ALTER TABLE ${SESSION_DISPLAY_TABLE} ADD COLUMN archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1))`);
1110
+ }
1111
+ }
1112
+ ensureProfileDisplayTable(db) {
1113
+ db.exec(`
1114
+ CREATE TABLE IF NOT EXISTS ${PROFILE_DISPLAY_TABLE} (
1115
+ profile_name TEXT PRIMARY KEY,
1116
+ pinned INTEGER NOT NULL DEFAULT 0 CHECK (pinned IN (0, 1))
1117
+ )
1118
+ `);
1119
+ }
1120
+ hasProfileDisplayTable(db) {
1121
+ return db.prepare(`
1122
+ SELECT 1
1123
+ FROM sqlite_master
1124
+ WHERE type = 'table' AND name = ?
1125
+ `).get(PROFILE_DISPLAY_TABLE) !== undefined;
1126
+ }
1127
+ hasSessionDisplayTable(db) {
1128
+ return db.prepare(`
1129
+ SELECT 1
1130
+ FROM sqlite_master
1131
+ WHERE type = 'table' AND name = ?
1132
+ `).get(SESSION_DISPLAY_TABLE) !== undefined;
1133
+ }
1134
+ deleteDisplayForSession(db, sessionId) {
1135
+ if (!this.hasSessionDisplayTable(db))
1136
+ return;
1137
+ db.prepare(`DELETE FROM ${SESSION_DISPLAY_TABLE} WHERE session_id = ?`).run(sessionId);
1138
+ }
1139
+ storeError(message) {
1140
+ return new MarifoldError_1.MarifoldError('SESSION_STORE_ERROR', message, { sessionsDb: this.sessionsDb });
1141
+ }
1142
+ }
1143
+ exports.SessionResolver = SessionResolver;
1144
+ const PREVIEW_MAX_CHARS = 80;
1145
+ function sessionPreview(content) {
1146
+ const flat = content.replace(/\s+/g, ' ').trim();
1147
+ if (flat.length <= PREVIEW_MAX_CHARS)
1148
+ return flat;
1149
+ return `${flat.slice(0, PREVIEW_MAX_CHARS - 1).trimEnd()}…`;
1150
+ }
1151
+ function nonNegativeInteger(value) {
1152
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
1153
+ ? Math.round(value)
1154
+ : null;
1155
+ }
1156
+ function nonNegativeNumber(value) {
1157
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
1158
+ ? value
1159
+ : null;
1160
+ }
1161
+ function firstLinePreview(content) {
1162
+ const first = content
1163
+ .split(/\r?\n/)
1164
+ .map(line => line.trim())
1165
+ .find(Boolean)
1166
+ ?.replace(/^#{1,6}\s+/, '')
1167
+ .replace(/^[-*+]\s+/, '')
1168
+ .replace(/^\d+[.)]\s+/, '') ?? '';
1169
+ if (first.length <= PREVIEW_MAX_CHARS)
1170
+ return first;
1171
+ return `${first.slice(0, PREVIEW_MAX_CHARS - 1).trimEnd()}…`;
1172
+ }
1173
+ //# sourceMappingURL=SessionResolver.js.map