@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.
- package/LICENSE +674 -0
- package/dist/agent/AgentEvents.d.ts +106 -0
- package/dist/agent/AgentEvents.d.ts.map +1 -0
- package/dist/agent/AgentEvents.js +3 -0
- package/dist/agent/AgentEvents.js.map +1 -0
- package/dist/agent/AgentHistory.d.ts +27 -0
- package/dist/agent/AgentHistory.d.ts.map +1 -0
- package/dist/agent/AgentHistory.js +42 -0
- package/dist/agent/AgentHistory.js.map +1 -0
- package/dist/agent/AgentRunner.d.ts +149 -0
- package/dist/agent/AgentRunner.d.ts.map +1 -0
- package/dist/agent/AgentRunner.js +727 -0
- package/dist/agent/AgentRunner.js.map +1 -0
- package/dist/agent/ApprovalPolicy.d.ts +71 -0
- package/dist/agent/ApprovalPolicy.d.ts.map +1 -0
- package/dist/agent/ApprovalPolicy.js +29 -0
- package/dist/agent/ApprovalPolicy.js.map +1 -0
- package/dist/agent/ControlBlockTools.d.ts +9 -0
- package/dist/agent/ControlBlockTools.d.ts.map +1 -0
- package/dist/agent/ControlBlockTools.js +49 -0
- package/dist/agent/ControlBlockTools.js.map +1 -0
- package/dist/agent/RunWorkspace.d.ts +92 -0
- package/dist/agent/RunWorkspace.d.ts.map +1 -0
- package/dist/agent/RunWorkspace.js +328 -0
- package/dist/agent/RunWorkspace.js.map +1 -0
- package/dist/agent/ScopedProcess.d.ts +35 -0
- package/dist/agent/ScopedProcess.d.ts.map +1 -0
- package/dist/agent/ScopedProcess.js +280 -0
- package/dist/agent/ScopedProcess.js.map +1 -0
- package/dist/agent/ToolRegistry.d.ts +80 -0
- package/dist/agent/ToolRegistry.d.ts.map +1 -0
- package/dist/agent/ToolRegistry.js +45 -0
- package/dist/agent/ToolRegistry.js.map +1 -0
- package/dist/agent/UserInput.d.ts +59 -0
- package/dist/agent/UserInput.d.ts.map +1 -0
- package/dist/agent/UserInput.js +203 -0
- package/dist/agent/UserInput.js.map +1 -0
- package/dist/agent/tools/AskUserTool.d.ts +71 -0
- package/dist/agent/tools/AskUserTool.d.ts.map +1 -0
- package/dist/agent/tools/AskUserTool.js +70 -0
- package/dist/agent/tools/AskUserTool.js.map +1 -0
- package/dist/agent/tools/DelegateTool.d.ts +49 -0
- package/dist/agent/tools/DelegateTool.d.ts.map +1 -0
- package/dist/agent/tools/DelegateTool.js +62 -0
- package/dist/agent/tools/DelegateTool.js.map +1 -0
- package/dist/agent/tools/InspectAttachmentTool.d.ts +25 -0
- package/dist/agent/tools/InspectAttachmentTool.d.ts.map +1 -0
- package/dist/agent/tools/InspectAttachmentTool.js +129 -0
- package/dist/agent/tools/InspectAttachmentTool.js.map +1 -0
- package/dist/agent/tools/PythonPackageTool.d.ts +26 -0
- package/dist/agent/tools/PythonPackageTool.d.ts.map +1 -0
- package/dist/agent/tools/PythonPackageTool.js +105 -0
- package/dist/agent/tools/PythonPackageTool.js.map +1 -0
- package/dist/agent/tools/ReadFileTool.d.ts +24 -0
- package/dist/agent/tools/ReadFileTool.d.ts.map +1 -0
- package/dist/agent/tools/ReadFileTool.js +112 -0
- package/dist/agent/tools/ReadFileTool.js.map +1 -0
- package/dist/agent/tools/ShellExecTool.d.ts +23 -0
- package/dist/agent/tools/ShellExecTool.d.ts.map +1 -0
- package/dist/agent/tools/ShellExecTool.js +75 -0
- package/dist/agent/tools/ShellExecTool.js.map +1 -0
- package/dist/agent/tools/WebSearchTool.d.ts +26 -0
- package/dist/agent/tools/WebSearchTool.d.ts.map +1 -0
- package/dist/agent/tools/WebSearchTool.js +52 -0
- package/dist/agent/tools/WebSearchTool.js.map +1 -0
- package/dist/agent/tools/WriteFileTool.d.ts +30 -0
- package/dist/agent/tools/WriteFileTool.d.ts.map +1 -0
- package/dist/agent/tools/WriteFileTool.js +155 -0
- package/dist/agent/tools/WriteFileTool.js.map +1 -0
- package/dist/app/AppActionResolver.d.ts +15 -0
- package/dist/app/AppActionResolver.d.ts.map +1 -0
- package/dist/app/AppActionResolver.js +98 -0
- package/dist/app/AppActionResolver.js.map +1 -0
- package/dist/app/AppSchema.d.ts +83 -0
- package/dist/app/AppSchema.d.ts.map +1 -0
- package/dist/app/AppSchema.js +17 -0
- package/dist/app/AppSchema.js.map +1 -0
- package/dist/app/AppStore.d.ts +14 -0
- package/dist/app/AppStore.d.ts.map +1 -0
- package/dist/app/AppStore.js +146 -0
- package/dist/app/AppStore.js.map +1 -0
- package/dist/app/AppValidator.d.ts +10 -0
- package/dist/app/AppValidator.d.ts.map +1 -0
- package/dist/app/AppValidator.js +503 -0
- package/dist/app/AppValidator.js.map +1 -0
- package/dist/app/SkillAppCompiler.d.ts +5 -0
- package/dist/app/SkillAppCompiler.d.ts.map +1 -0
- package/dist/app/SkillAppCompiler.js +632 -0
- package/dist/app/SkillAppCompiler.js.map +1 -0
- package/dist/app/SkillAppDsl.d.ts +93 -0
- package/dist/app/SkillAppDsl.d.ts.map +1 -0
- package/dist/app/SkillAppDsl.js +72 -0
- package/dist/app/SkillAppDsl.js.map +1 -0
- package/dist/app/SkillAppInstanceRegistry.d.ts +25 -0
- package/dist/app/SkillAppInstanceRegistry.d.ts.map +1 -0
- package/dist/app/SkillAppInstanceRegistry.js +218 -0
- package/dist/app/SkillAppInstanceRegistry.js.map +1 -0
- package/dist/app/SkillAppResolver.d.ts +12 -0
- package/dist/app/SkillAppResolver.d.ts.map +1 -0
- package/dist/app/SkillAppResolver.js +89 -0
- package/dist/app/SkillAppResolver.js.map +1 -0
- package/dist/app/SkillAppSchema.d.ts +116 -0
- package/dist/app/SkillAppSchema.d.ts.map +1 -0
- package/dist/app/SkillAppSchema.js +5 -0
- package/dist/app/SkillAppSchema.js.map +1 -0
- package/dist/channels/TelegramBridge.d.ts +103 -0
- package/dist/channels/TelegramBridge.d.ts.map +1 -0
- package/dist/channels/TelegramBridge.js +565 -0
- package/dist/channels/TelegramBridge.js.map +1 -0
- package/dist/channels/respond.d.ts +46 -0
- package/dist/channels/respond.d.ts.map +1 -0
- package/dist/channels/respond.js +41 -0
- package/dist/channels/respond.js.map +1 -0
- package/dist/config/ChatGptTokenRefresh.d.ts +18 -0
- package/dist/config/ChatGptTokenRefresh.d.ts.map +1 -0
- package/dist/config/ChatGptTokenRefresh.js +63 -0
- package/dist/config/ChatGptTokenRefresh.js.map +1 -0
- package/dist/config/ConfigBackup.d.ts +20 -0
- package/dist/config/ConfigBackup.d.ts.map +1 -0
- package/dist/config/ConfigBackup.js +197 -0
- package/dist/config/ConfigBackup.js.map +1 -0
- package/dist/config/ConfigLoader.d.ts +28 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +335 -0
- package/dist/config/ConfigLoader.js.map +1 -0
- package/dist/config/ConfigManager.d.ts +55 -0
- package/dist/config/ConfigManager.d.ts.map +1 -0
- package/dist/config/ConfigManager.js +707 -0
- package/dist/config/ConfigManager.js.map +1 -0
- package/dist/config/ConfigSchema.d.ts +231 -0
- package/dist/config/ConfigSchema.d.ts.map +1 -0
- package/dist/config/ConfigSchema.js +15 -0
- package/dist/config/ConfigSchema.js.map +1 -0
- package/dist/config/GitHubCopilotAuth.d.ts +7 -0
- package/dist/config/GitHubCopilotAuth.d.ts.map +1 -0
- package/dist/config/GitHubCopilotAuth.js +40 -0
- package/dist/config/GitHubCopilotAuth.js.map +1 -0
- package/dist/config/MarifoldOpenAICompatProvider.d.ts +39 -0
- package/dist/config/MarifoldOpenAICompatProvider.d.ts.map +1 -0
- package/dist/config/MarifoldOpenAICompatProvider.js +158 -0
- package/dist/config/MarifoldOpenAICompatProvider.js.map +1 -0
- package/dist/config/OpenAICompatUrls.d.ts +7 -0
- package/dist/config/OpenAICompatUrls.d.ts.map +1 -0
- package/dist/config/OpenAICompatUrls.js +47 -0
- package/dist/config/OpenAICompatUrls.js.map +1 -0
- package/dist/config/ProviderFactory.d.ts +12 -0
- package/dist/config/ProviderFactory.d.ts.map +1 -0
- package/dist/config/ProviderFactory.js +61 -0
- package/dist/config/ProviderFactory.js.map +1 -0
- package/dist/config/ProviderInspector.d.ts +46 -0
- package/dist/config/ProviderInspector.d.ts.map +1 -0
- package/dist/config/ProviderInspector.js +356 -0
- package/dist/config/ProviderInspector.js.map +1 -0
- package/dist/config/ProviderRegistry.d.ts +19 -0
- package/dist/config/ProviderRegistry.d.ts.map +1 -0
- package/dist/config/ProviderRegistry.js +301 -0
- package/dist/config/ProviderRegistry.js.map +1 -0
- package/dist/config/XaiTokenRefresh.d.ts +15 -0
- package/dist/config/XaiTokenRefresh.d.ts.map +1 -0
- package/dist/config/XaiTokenRefresh.js +73 -0
- package/dist/config/XaiTokenRefresh.js.map +1 -0
- package/dist/errors/MarifoldError.d.ts +34 -0
- package/dist/errors/MarifoldError.d.ts.map +1 -0
- package/dist/errors/MarifoldError.js +100 -0
- package/dist/errors/MarifoldError.js.map +1 -0
- package/dist/images/ImageOptimizer.d.ts +34 -0
- package/dist/images/ImageOptimizer.d.ts.map +1 -0
- package/dist/images/ImageOptimizer.js +225 -0
- package/dist/images/ImageOptimizer.js.map +1 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +193 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/MemoryControls.d.ts +26 -0
- package/dist/memory/MemoryControls.d.ts.map +1 -0
- package/dist/memory/MemoryControls.js +388 -0
- package/dist/memory/MemoryControls.js.map +1 -0
- package/dist/memory/MemoryStore.d.ts +122 -0
- package/dist/memory/MemoryStore.d.ts.map +1 -0
- package/dist/memory/MemoryStore.js +1096 -0
- package/dist/memory/MemoryStore.js.map +1 -0
- package/dist/profiles/ProfileManager.d.ts +139 -0
- package/dist/profiles/ProfileManager.d.ts.map +1 -0
- package/dist/profiles/ProfileManager.js +475 -0
- package/dist/profiles/ProfileManager.js.map +1 -0
- package/dist/profiles/ProfileResolver.d.ts +18 -0
- package/dist/profiles/ProfileResolver.d.ts.map +1 -0
- package/dist/profiles/ProfileResolver.js +280 -0
- package/dist/profiles/ProfileResolver.js.map +1 -0
- package/dist/runs/RunRegistry.d.ts +140 -0
- package/dist/runs/RunRegistry.d.ts.map +1 -0
- package/dist/runs/RunRegistry.js +432 -0
- package/dist/runs/RunRegistry.js.map +1 -0
- package/dist/runtime/MarifoldRuntime.d.ts +244 -0
- package/dist/runtime/MarifoldRuntime.d.ts.map +1 -0
- package/dist/runtime/MarifoldRuntime.js +1099 -0
- package/dist/runtime/MarifoldRuntime.js.map +1 -0
- package/dist/runtime/MarifoldTypes.d.ts +70 -0
- package/dist/runtime/MarifoldTypes.d.ts.map +1 -0
- package/dist/runtime/MarifoldTypes.js +3 -0
- package/dist/runtime/MarifoldTypes.js.map +1 -0
- package/dist/schedule/ScheduleStore.d.ts +60 -0
- package/dist/schedule/ScheduleStore.d.ts.map +1 -0
- package/dist/schedule/ScheduleStore.js +212 -0
- package/dist/schedule/ScheduleStore.js.map +1 -0
- package/dist/schedule/Scheduler.d.ts +28 -0
- package/dist/schedule/Scheduler.d.ts.map +1 -0
- package/dist/schedule/Scheduler.js +65 -0
- package/dist/schedule/Scheduler.js.map +1 -0
- package/dist/search/DuckDuckGoBackend.d.ts +18 -0
- package/dist/search/DuckDuckGoBackend.d.ts.map +1 -0
- package/dist/search/DuckDuckGoBackend.js +37 -0
- package/dist/search/DuckDuckGoBackend.js.map +1 -0
- package/dist/search/FirecrawlBackend.d.ts +28 -0
- package/dist/search/FirecrawlBackend.d.ts.map +1 -0
- package/dist/search/FirecrawlBackend.js +76 -0
- package/dist/search/FirecrawlBackend.js.map +1 -0
- package/dist/search/SearchBackend.d.ts +18 -0
- package/dist/search/SearchBackend.d.ts.map +1 -0
- package/dist/search/SearchBackend.js +24 -0
- package/dist/search/SearchBackend.js.map +1 -0
- package/dist/search/createSearchBackend.d.ts +16 -0
- package/dist/search/createSearchBackend.d.ts.map +1 -0
- package/dist/search/createSearchBackend.js +30 -0
- package/dist/search/createSearchBackend.js.map +1 -0
- package/dist/sessions/ResponseMetrics.d.ts +17 -0
- package/dist/sessions/ResponseMetrics.d.ts.map +1 -0
- package/dist/sessions/ResponseMetrics.js +3 -0
- package/dist/sessions/ResponseMetrics.js.map +1 -0
- package/dist/sessions/SessionResolver.d.ts +135 -0
- package/dist/sessions/SessionResolver.d.ts.map +1 -0
- package/dist/sessions/SessionResolver.js +1173 -0
- package/dist/sessions/SessionResolver.js.map +1 -0
- package/dist/skill/BuiltInSkillManager.d.ts +11 -0
- package/dist/skill/BuiltInSkillManager.d.ts.map +1 -0
- package/dist/skill/BuiltInSkillManager.js +98 -0
- package/dist/skill/BuiltInSkillManager.js.map +1 -0
- package/dist/skill/SkillInvocation.d.ts +30 -0
- package/dist/skill/SkillInvocation.d.ts.map +1 -0
- package/dist/skill/SkillInvocation.js +135 -0
- package/dist/skill/SkillInvocation.js.map +1 -0
- package/dist/skill/SkillSchema.d.ts +37 -0
- package/dist/skill/SkillSchema.d.ts.map +1 -0
- package/dist/skill/SkillSchema.js +26 -0
- package/dist/skill/SkillSchema.js.map +1 -0
- package/dist/skill/SkillStore.d.ts +41 -0
- package/dist/skill/SkillStore.d.ts.map +1 -0
- package/dist/skill/SkillStore.js +184 -0
- package/dist/skill/SkillStore.js.map +1 -0
- package/dist/skill/SkillTemplater.d.ts +19 -0
- package/dist/skill/SkillTemplater.d.ts.map +1 -0
- package/dist/skill/SkillTemplater.js +36 -0
- package/dist/skill/SkillTemplater.js.map +1 -0
- package/dist/skill/SkillValidator.d.ts +10 -0
- package/dist/skill/SkillValidator.d.ts.map +1 -0
- package/dist/skill/SkillValidator.js +131 -0
- package/dist/skill/SkillValidator.js.map +1 -0
- package/dist/skill/SkillVars.d.ts +10 -0
- package/dist/skill/SkillVars.d.ts.map +1 -0
- package/dist/skill/SkillVars.js +65 -0
- package/dist/skill/SkillVars.js.map +1 -0
- package/dist/skill/index.d.ts +10 -0
- package/dist/skill/index.d.ts.map +1 -0
- package/dist/skill/index.js +22 -0
- package/dist/skill/index.js.map +1 -0
- package/dist/skillapp/SkillAppActionResolver.d.ts +13 -0
- package/dist/skillapp/SkillAppActionResolver.d.ts.map +1 -0
- package/dist/skillapp/SkillAppActionResolver.js +95 -0
- package/dist/skillapp/SkillAppActionResolver.js.map +1 -0
- package/dist/skillapp/SkillAppSchema.d.ts +79 -0
- package/dist/skillapp/SkillAppSchema.d.ts.map +1 -0
- package/dist/skillapp/SkillAppSchema.js +20 -0
- package/dist/skillapp/SkillAppSchema.js.map +1 -0
- package/dist/skillapp/SkillAppStore.d.ts +11 -0
- package/dist/skillapp/SkillAppStore.d.ts.map +1 -0
- package/dist/skillapp/SkillAppStore.js +98 -0
- package/dist/skillapp/SkillAppStore.js.map +1 -0
- package/dist/skillapp/SkillAppValidator.d.ts +10 -0
- package/dist/skillapp/SkillAppValidator.d.ts.map +1 -0
- package/dist/skillapp/SkillAppValidator.js +522 -0
- package/dist/skillapp/SkillAppValidator.js.map +1 -0
- package/dist/tasks/TaskStore.d.ts +110 -0
- package/dist/tasks/TaskStore.d.ts.map +1 -0
- package/dist/tasks/TaskStore.js +371 -0
- package/dist/tasks/TaskStore.js.map +1 -0
- package/dist/util/fetchRetry.d.ts +11 -0
- package/dist/util/fetchRetry.d.ts.map +1 -0
- package/dist/util/fetchRetry.js +48 -0
- package/dist/util/fetchRetry.js.map +1 -0
- package/dist/util/idToken.d.ts +12 -0
- package/dist/util/idToken.d.ts.map +1 -0
- package/dist/util/idToken.js +32 -0
- package/dist/util/idToken.js.map +1 -0
- package/dist/util/proxy.d.ts +18 -0
- package/dist/util/proxy.d.ts.map +1 -0
- package/dist/util/proxy.js +28 -0
- package/dist/util/proxy.js.map +1 -0
- package/dist/workspace/Workspace.d.ts +9 -0
- package/dist/workspace/Workspace.d.ts.map +1 -0
- package/dist/workspace/Workspace.js +55 -0
- package/dist/workspace/Workspace.js.map +1 -0
- package/dist/workspace/WorkspaceInitializer.d.ts +40 -0
- package/dist/workspace/WorkspaceInitializer.d.ts.map +1 -0
- package/dist/workspace/WorkspaceInitializer.js +212 -0
- package/dist/workspace/WorkspaceInitializer.js.map +1 -0
- package/dist/workspace/WorkspacePaths.d.ts +11 -0
- package/dist/workspace/WorkspacePaths.d.ts.map +1 -0
- package/dist/workspace/WorkspacePaths.js +82 -0
- package/dist/workspace/WorkspacePaths.js.map +1 -0
- 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
|