om-memory-system 4.24.0-next.147 → 4.25.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 (57) hide show
  1. package/dist/adapters/opencode/user-prompt.js +6 -1
  2. package/dist/config.d.ts +9 -0
  3. package/dist/config.js +5 -44
  4. package/dist/core/retrieval.js +1 -1
  5. package/dist/services/chat/chat-answer.d.ts +56 -0
  6. package/dist/services/chat/chat-answer.js +88 -0
  7. package/dist/services/chat/chat-plan.d.ts +60 -0
  8. package/dist/services/chat/chat-plan.js +92 -0
  9. package/dist/services/chat/chat-retrieve.d.ts +36 -0
  10. package/dist/services/chat/chat-retrieve.js +82 -0
  11. package/dist/services/chat/chat-routes.d.ts +12 -0
  12. package/dist/services/chat/chat-routes.js +145 -0
  13. package/dist/services/chat/chat-runtime.d.ts +21 -0
  14. package/dist/services/chat/chat-runtime.js +104 -0
  15. package/dist/services/chat/chat-service.d.ts +42 -0
  16. package/dist/services/chat/chat-service.js +130 -0
  17. package/dist/services/chat/chat-status.d.ts +16 -0
  18. package/dist/services/chat/chat-status.js +23 -0
  19. package/dist/services/chat/chat-store.d.ts +60 -0
  20. package/dist/services/chat/chat-store.js +352 -0
  21. package/dist/services/chat/chat-types.d.ts +59 -0
  22. package/dist/services/chat/chat-types.js +8 -0
  23. package/dist/services/client.d.ts +1 -0
  24. package/dist/services/client.js +8 -3
  25. package/dist/services/context.d.ts +3 -1
  26. package/dist/services/context.js +4 -2
  27. package/dist/services/global-config-writer.js +8 -1
  28. package/dist/services/privacy.js +2 -2
  29. package/dist/services/settings-snapshot.js +1 -0
  30. package/dist/services/turso/exact-scan.d.ts +6 -0
  31. package/dist/services/turso/exact-scan.js +23 -0
  32. package/dist/services/turso/keyword-search.d.ts +35 -0
  33. package/dist/services/turso/keyword-search.js +153 -0
  34. package/dist/services/turso/rank-fusion.d.ts +34 -0
  35. package/dist/services/turso/rank-fusion.js +45 -0
  36. package/dist/services/turso/tag-match.d.ts +7 -0
  37. package/dist/services/turso/tag-match.js +29 -0
  38. package/dist/services/turso/types.d.ts +2 -0
  39. package/dist/services/turso/vector-search.d.ts +3 -3
  40. package/dist/services/turso/vector-search.js +23 -39
  41. package/dist/services/user-profile/profile-context.d.ts +6 -1
  42. package/dist/services/user-profile/profile-context.js +43 -7
  43. package/dist/services/user-profile/profile-relevance.d.ts +37 -0
  44. package/dist/services/user-profile/profile-relevance.js +75 -0
  45. package/dist/services/web-route-modules.d.ts +8 -0
  46. package/dist/services/web-route-modules.js +15 -0
  47. package/dist/services/web-server.js +5 -4
  48. package/dist/utils/keyword-boost.d.ts +17 -0
  49. package/dist/utils/keyword-boost.js +32 -0
  50. package/dist/utils/reranker-settings.d.ts +13 -0
  51. package/dist/utils/reranker-settings.js +48 -0
  52. package/dist/web/assets/index-DWk9C9Sm.css +2 -0
  53. package/dist/web/assets/index-rHcTKCkC.js +126 -0
  54. package/dist/web/index.html +2 -2
  55. package/package.json +1 -1
  56. package/dist/web/assets/index-B_2GCxJJ.js +0 -127
  57. package/dist/web/assets/index-JAZ-QY-Z.css +0 -2
@@ -0,0 +1,145 @@
1
+ import { tryExtractScopeFromContainerTag } from "../memory-scope.js";
2
+ import { log } from "../logger.js";
3
+ import { chatServiceDeps, chatStore, existingMemoryIds, } from "./chat-runtime.js";
4
+ import { askQuestion } from "./chat-service.js";
5
+ import { chatStatus } from "./chat-status.js";
6
+ import { ChatStoreError } from "./chat-types.js";
7
+ const ok = (data) => ({ status: 200, body: { success: true, data } });
8
+ const fail = (status, code, error) => ({
9
+ status,
10
+ body: { success: false, code, error },
11
+ });
12
+ const STATUS_BY_CODE = { not_found: 404, conflict: 409, invalid: 400, read_only: 409 };
13
+ function scopeTag(value) {
14
+ if (value === null || value === undefined)
15
+ return null;
16
+ // A scope must be a real container tag; anything else could reach a shard path.
17
+ return typeof value === "string" && tryExtractScopeFromContainerTag(value) ? value : undefined;
18
+ }
19
+ function folderId(value) {
20
+ if (value === null)
21
+ return null;
22
+ return typeof value === "string" && value ? value : undefined;
23
+ }
24
+ const CONVERSATION = /^\/api\/chat\/conversations\/([^/]+)$/;
25
+ const MESSAGES = /^\/api\/chat\/conversations\/([^/]+)\/messages$/;
26
+ const FOLDER = /^\/api\/chat\/folders\/([^/]+)$/;
27
+ /**
28
+ * The chat routes under `/api/chat/`. Returns undefined for any other path.
29
+ * Store errors map to 400, 404 or 409; anything else is logged by code only.
30
+ */
31
+ export async function routeChat(path, method, readBody, config) {
32
+ if (!path.startsWith("/api/chat/"))
33
+ return undefined;
34
+ try {
35
+ return await dispatch(path, method, readBody, config);
36
+ }
37
+ catch (error) {
38
+ if (error instanceof ChatStoreError) {
39
+ return fail(STATUS_BY_CODE[error.code], error.code, error.message);
40
+ }
41
+ log("chat route: error", { path: path.replace(/\/[0-9a-f-]{36}/g, "/:id"), method });
42
+ return fail(500, "internal", "Chat request failed");
43
+ }
44
+ }
45
+ async function dispatch(path, method, readBody, config) {
46
+ if (path === "/api/chat/status" && method === "GET")
47
+ return ok(chatStatus(config));
48
+ const store = await chatStore(config);
49
+ if (path === "/api/chat/conversations") {
50
+ if (method === "GET") {
51
+ const [conversations, folders] = await Promise.all([
52
+ store.listConversations(),
53
+ store.listFolders(),
54
+ ]);
55
+ return ok({ conversations, folders, readOnly: store.isReadOnly() });
56
+ }
57
+ if (method === "POST") {
58
+ const body = await readBody();
59
+ const scope = scopeTag(body.scopeTag);
60
+ const folder = body.folderId === undefined ? null : folderId(body.folderId);
61
+ if (scope === undefined || folder === undefined) {
62
+ return fail(400, "invalid", "Invalid scope or folder");
63
+ }
64
+ return ok(await store.createConversation({ title: "", scopeTag: scope, folderId: folder }));
65
+ }
66
+ }
67
+ if (path === "/api/chat/conversations/bulk-delete" && method === "POST") {
68
+ const body = await readBody();
69
+ const ids = Array.isArray(body.ids) ? body.ids.filter((id) => typeof id === "string") : [];
70
+ if (!Array.isArray(body.ids) || ids.length !== body.ids.length) {
71
+ return fail(400, "invalid", "ids must be a list of conversation ids");
72
+ }
73
+ return ok({ deleted: await store.bulkDeleteConversations(ids) });
74
+ }
75
+ const messages = MESSAGES.exec(path);
76
+ if (messages && method === "POST") {
77
+ const body = await readBody();
78
+ const scope = scopeTag(body.scopeTag);
79
+ if (scope === undefined)
80
+ return fail(400, "invalid", "Invalid scope");
81
+ if (typeof body.question !== "string")
82
+ return fail(400, "empty", "A question is required");
83
+ const status = chatStatus(config);
84
+ if (!status.ready) {
85
+ return fail(409, "not_configured", `Missing settings: ${status.missing.join(", ")}`);
86
+ }
87
+ const outcome = await askQuestion(messages[1], { question: body.question, scopeTag: scope, retry: body.retry === true }, await chatServiceDeps(config));
88
+ return outcome.ok
89
+ ? ok({ messages: outcome.messages })
90
+ : fail(outcome.status, outcome.code, outcome.error);
91
+ }
92
+ const conversation = CONVERSATION.exec(path);
93
+ if (conversation) {
94
+ const id = conversation[1];
95
+ if (method === "GET") {
96
+ const found = await store.getConversation(id);
97
+ if (!found)
98
+ return fail(404, "not_found", "Conversation not found");
99
+ const cited = [
100
+ ...new Set(found.messages.flatMap((m) => m.sources.filter((s) => s.kind === "memory").map((s) => s.id))),
101
+ ];
102
+ const existing = await existingMemoryIds(cited).catch(() => new Set(cited));
103
+ return ok({
104
+ ...found,
105
+ missingMemoryIds: cited.filter((memoryId) => !existing.has(memoryId)),
106
+ });
107
+ }
108
+ if (method === "PATCH") {
109
+ const body = await readBody();
110
+ if (typeof body.title === "string")
111
+ await store.renameConversation(id, body.title);
112
+ if (body.folderId !== undefined) {
113
+ const folder = folderId(body.folderId);
114
+ if (folder === undefined)
115
+ return fail(400, "invalid", "Invalid folder");
116
+ await store.moveConversation(id, folder);
117
+ }
118
+ return ok({ id });
119
+ }
120
+ if (method === "DELETE") {
121
+ await store.deleteConversation(id);
122
+ return ok({ id });
123
+ }
124
+ }
125
+ if (path === "/api/chat/folders") {
126
+ if (method === "GET")
127
+ return ok(await store.listFolders());
128
+ if (method === "POST") {
129
+ const body = await readBody();
130
+ return ok(await store.createFolder(typeof body.name === "string" ? body.name : ""));
131
+ }
132
+ }
133
+ const folder = FOLDER.exec(path);
134
+ if (folder) {
135
+ if (method === "PATCH") {
136
+ const body = await readBody();
137
+ await store.renameFolder(folder[1], typeof body.name === "string" ? body.name : "");
138
+ return ok({ id: folder[1] });
139
+ }
140
+ if (method === "DELETE") {
141
+ return ok({ id: folder[1], deletedConversations: await store.deleteFolder(folder[1]) });
142
+ }
143
+ }
144
+ return fail(404, "not_found", "Unknown chat route");
145
+ }
@@ -0,0 +1,21 @@
1
+ import type { ChatServiceDeps } from "./chat-service.js";
2
+ import { ChatStore } from "./chat-store.js";
3
+ /** Each model call gets this long before chat gives up on it. */
4
+ export declare const CHAT_MODEL_TIMEOUT_MS = 60000;
5
+ /** The settings chat reads. The caller passes `CONFIG`. */
6
+ export interface ChatRuntimeConfig {
7
+ storagePath: string;
8
+ memoryProvider: string;
9
+ memoryModel?: string;
10
+ memoryApiUrl?: string;
11
+ memoryApiKey?: string;
12
+ memoryTemperature?: number | false;
13
+ memoryExtraParams?: Record<string, unknown>;
14
+ userEmailOverride?: string;
15
+ }
16
+ /** The chat store for this storage path, opened and migrated once. */
17
+ export declare function chatStore(config: Pick<ChatRuntimeConfig, "storagePath">): Promise<ChatStore>;
18
+ /** The memory ids from `ids` that still exist in any project or user shard. */
19
+ export declare function existingMemoryIds(ids: string[]): Promise<Set<string>>;
20
+ /** Real dependencies for `askQuestion`, built from the settings passed in. */
21
+ export declare function chatServiceDeps(config: ChatRuntimeConfig): Promise<ChatServiceDeps>;
@@ -0,0 +1,104 @@
1
+ import { join } from "node:path";
2
+ import { log } from "../logger.js";
3
+ import { ChatStore } from "./chat-store.js";
4
+ /** Each model call gets this long before chat gives up on it. */
5
+ export const CHAT_MODEL_TIMEOUT_MS = 60_000;
6
+ const stores = new Map();
7
+ /** The chat store for this storage path, opened and migrated once. */
8
+ export function chatStore(config) {
9
+ const path = join(config.storagePath, "chats.db");
10
+ let store = stores.get(path);
11
+ if (!store) {
12
+ const opened = new ChatStore(path);
13
+ store = opened.init().then(() => opened);
14
+ store.catch(() => stores.delete(path));
15
+ stores.set(path, store);
16
+ }
17
+ return store;
18
+ }
19
+ async function callTool(config, system, user, tool) {
20
+ const { AIProviderFactory } = await import("../ai/ai-provider-factory.js");
21
+ const { buildMemoryProviderConfig } = await import("../ai/provider-config.js");
22
+ const { aiSessionManager } = await import("../ai/session/ai-session-manager.js");
23
+ const provider = AIProviderFactory.createProvider(config.memoryProvider, buildMemoryProviderConfig(config, { maxIterations: 1, iterationTimeout: CHAT_MODEL_TIMEOUT_MS }));
24
+ const sessionId = `chat_${crypto.randomUUID()}`;
25
+ try {
26
+ return await provider.executeToolCall(system, user, tool, sessionId);
27
+ }
28
+ finally {
29
+ // Providers keep a session transcript; chat keeps its own history in chats.db.
30
+ await aiSessionManager
31
+ .deleteSession(sessionId, config.memoryProvider)
32
+ .catch(() => undefined);
33
+ }
34
+ }
35
+ async function activeProfileItems(config) {
36
+ const { resolveWebProfileUserId } = await import("../profile-identity.js");
37
+ const { userProfileManager } = await import("../user-profile/user-profile-manager.js");
38
+ const userId = await resolveWebProfileUserId(config);
39
+ const profile = userId ? await userProfileManager.getActiveProfile(userId) : null;
40
+ if (!profile)
41
+ return [];
42
+ const data = JSON.parse(profile.profileData);
43
+ const sections = ["preferences", "patterns", "workflows"];
44
+ return sections.flatMap((section) => (data[section] ?? []).map((item, index) => ({
45
+ section,
46
+ index,
47
+ category: item.category,
48
+ description: String(item.description ?? ""),
49
+ evidence: item.evidence,
50
+ centroid: Array.isArray(item.centroid) ? item.centroid : undefined,
51
+ })));
52
+ }
53
+ /** The memory ids from `ids` that still exist in any project or user shard. */
54
+ export async function existingMemoryIds(ids) {
55
+ const found = new Set();
56
+ if (ids.length === 0)
57
+ return found;
58
+ const { tursoShardManager } = await import("../turso/shard-manager.js");
59
+ const { tursoConnectionManager } = await import("../turso/connection-manager.js");
60
+ const shards = [
61
+ ...(await tursoShardManager.getAllShards("user", "")),
62
+ ...(await tursoShardManager.getAllShards("project", "")),
63
+ ];
64
+ const marks = ids.map(() => "?").join(",");
65
+ for (const shard of shards) {
66
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
67
+ const rows = await db.all(`SELECT id FROM memories WHERE id IN (${marks})`, ids);
68
+ for (const row of rows)
69
+ found.add(String(row.id));
70
+ }
71
+ return found;
72
+ }
73
+ /** Real dependencies for `askQuestion`, built from the settings passed in. */
74
+ export async function chatServiceDeps(config) {
75
+ const store = await chatStore(config);
76
+ return {
77
+ store,
78
+ model: config.memoryModel ?? config.memoryProvider,
79
+ callTool: (system, user, tool) => callTool(config, system, user, tool),
80
+ retrieve: {
81
+ searchMemories: async (query, scopeTag) => {
82
+ const { memoryClient } = await import("../client.js");
83
+ const result = await memoryClient.searchMemories(query, scopeTag ?? "", scopeTag ? "project" : "all-projects");
84
+ return result.success ? result.results : [];
85
+ },
86
+ embed: async (text) => {
87
+ const { embeddingService } = await import("../embedding.js");
88
+ return embeddingService.embedWithTimeout(text, { task: "query" });
89
+ },
90
+ profileItems: () => activeProfileItems(config),
91
+ },
92
+ listTags: async (scopeTag) => {
93
+ const { handleListKeywords } = await import("../api-handlers.js");
94
+ const result = await handleListKeywords(scopeTag ?? undefined);
95
+ return result.success && result.data ? result.data.keywords : [];
96
+ },
97
+ profileCategories: async () => [
98
+ ...new Set((await activeProfileItems(config))
99
+ .map((item) => item.category)
100
+ .filter((category) => !!category)),
101
+ ],
102
+ log: (message, meta) => log(message, meta),
103
+ };
104
+ }
@@ -0,0 +1,42 @@
1
+ import type { ToolCallResult } from "../ai/providers/base-provider.js";
2
+ import { ANSWER_TOOL } from "./chat-answer.js";
3
+ import { PLAN_TOOL, type PlanInput } from "./chat-plan.js";
4
+ import { type RetrieveDeps } from "./chat-retrieve.js";
5
+ import type { ChatStore } from "./chat-store.js";
6
+ import type { ChatMessage } from "./chat-types.js";
7
+ export declare const MAX_QUESTION_CHARS = 4000;
8
+ type Tool = typeof PLAN_TOOL | typeof ANSWER_TOOL;
9
+ export interface ChatServiceDeps {
10
+ store: ChatStore;
11
+ model: string;
12
+ /** One structured model call. Throws or returns `success: false` on failure. */
13
+ callTool: (system: string, user: string, tool: Tool) => Promise<ToolCallResult>;
14
+ retrieve: RetrieveDeps;
15
+ listTags: (scopeTag: string | null) => Promise<PlanInput["tags"]>;
16
+ profileCategories: () => Promise<string[]>;
17
+ /** Metadata only: sizes, counts, codes, timings. Never question or answer text. */
18
+ log: (message: string, meta: Record<string, unknown>) => void;
19
+ }
20
+ export interface AskInput {
21
+ question: string;
22
+ scopeTag: string | null;
23
+ /** Re-run a failed answer instead of adding the question again. */
24
+ retry?: boolean;
25
+ }
26
+ export type AskOutcome = {
27
+ ok: true;
28
+ messages: ChatMessage[];
29
+ } | {
30
+ ok: false;
31
+ status: 400 | 404 | 409;
32
+ code: string;
33
+ error: string;
34
+ };
35
+ /**
36
+ * One question end to end: strip private text, save the question, plan the
37
+ * search, retrieve sources, answer with checked citations, save the answer.
38
+ * A model failure saves an assistant message with an error code so the page
39
+ * can offer a retry.
40
+ */
41
+ export declare function askQuestion(conversationId: string, input: AskInput, deps: ChatServiceDeps): Promise<AskOutcome>;
42
+ export {};
@@ -0,0 +1,130 @@
1
+ import { isFullyPrivate, stripPrivateContent } from "../privacy.js";
2
+ import { ANSWER_TOOL, buildAnswerPrompt, finaliseAnswer, numberSources } from "./chat-answer.js";
3
+ import { PLAN_TOOL, buildPlanPrompt, validatePlan } from "./chat-plan.js";
4
+ import { retrieveSources } from "./chat-retrieve.js";
5
+ export const MAX_QUESTION_CHARS = 4000;
6
+ const TITLE_CHARS = 60;
7
+ const DEFAULT_TITLE = "New chat";
8
+ function fail(status, code, error) {
9
+ return { ok: false, status, code, error };
10
+ }
11
+ /** A short code for a failed model call. Provider error text can echo keys, so it is dropped. */
12
+ function modelErrorCode(result) {
13
+ if (result?.transportError)
14
+ return "model_unreachable";
15
+ if (result?.httpStatus)
16
+ return `model_http_${result.httpStatus}`;
17
+ return "model_failed";
18
+ }
19
+ async function safeCall(deps, system, user, tool) {
20
+ try {
21
+ return await deps.callTool(system, user, tool);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ async function plan(deps, input) {
28
+ const prompt = buildPlanPrompt(input);
29
+ const result = await safeCall(deps, prompt.system, prompt.user, PLAN_TOOL);
30
+ return validatePlan(result?.success ? result.data : null, input);
31
+ }
32
+ /**
33
+ * One question end to end: strip private text, save the question, plan the
34
+ * search, retrieve sources, answer with checked citations, save the answer.
35
+ * A model failure saves an assistant message with an error code so the page
36
+ * can offer a retry.
37
+ */
38
+ export async function askQuestion(conversationId, input, deps) {
39
+ if (isFullyPrivate(input.question)) {
40
+ return fail(400, "private", "The question is fully private");
41
+ }
42
+ let question = stripPrivateContent(input.question).trim();
43
+ if (!question)
44
+ return fail(400, "empty", "A question is required");
45
+ if (question.length > MAX_QUESTION_CHARS) {
46
+ return fail(400, "too_long", `Questions have at most ${MAX_QUESTION_CHARS} characters`);
47
+ }
48
+ if (deps.store.isReadOnly()) {
49
+ return fail(409, "read_only", "chats.db is from a newer OMMS and is read-only");
50
+ }
51
+ const found = await deps.store.getConversation(conversationId);
52
+ if (!found)
53
+ return fail(404, "not_found", "Conversation not found");
54
+ let history = found.messages;
55
+ const last = history[history.length - 1];
56
+ const userTurn = history[history.length - 2];
57
+ if (input.retry && last?.role === "assistant" && last.errorCode && userTurn?.role === "user") {
58
+ await deps.store.deleteMessage(last.id);
59
+ history = history.slice(0, -2);
60
+ // Answer the question that was stored, whatever text the request carries.
61
+ question = userTurn.content;
62
+ }
63
+ else {
64
+ if (found.messages.length === 0 && found.conversation.title === DEFAULT_TITLE) {
65
+ await deps.store.renameConversation(conversationId, question.slice(0, TITLE_CHARS));
66
+ }
67
+ await deps.store.appendMessage(conversationId, { role: "user", content: question });
68
+ }
69
+ if (found.conversation.scopeTag !== input.scopeTag) {
70
+ await deps.store.setConversationScope(conversationId, input.scopeTag);
71
+ }
72
+ const started = Date.now();
73
+ const turns = history
74
+ .filter((message) => !message.errorCode && message.content)
75
+ .map((message) => ({ role: message.role, content: message.content }));
76
+ const [tags, categories] = await Promise.all([
77
+ deps.listTags(input.scopeTag).catch(() => []),
78
+ deps.profileCategories().catch(() => []),
79
+ ]);
80
+ const searchPlan = await plan(deps, { question, turns, tags, categories });
81
+ let sources;
82
+ try {
83
+ sources = numberSources(await retrieveSources(question, searchPlan, input.scopeTag, deps.retrieve));
84
+ }
85
+ catch {
86
+ return save(deps, conversationId, { errorCode: "search_failed", plan: searchPlan }, started);
87
+ }
88
+ if (sources.length === 0) {
89
+ return save(deps, conversationId, { notice: "no_sources", plan: searchPlan }, started);
90
+ }
91
+ const prompt = buildAnswerPrompt(question, turns, sources);
92
+ const result = await safeCall(deps, prompt.system, prompt.user, ANSWER_TOOL);
93
+ const answer = result?.success ? finaliseAnswer(result.data, sources) : null;
94
+ if (!answer) {
95
+ const errorCode = result?.success ? "model_invalid_reply" : modelErrorCode(result);
96
+ return save(deps, conversationId, { errorCode, plan: searchPlan, sources }, started);
97
+ }
98
+ return save(deps, conversationId, {
99
+ content: answer.text,
100
+ sources,
101
+ plan: searchPlan,
102
+ notice: answer.insufficient ? "insufficient" : answer.uncited ? "uncited" : null,
103
+ cited: answer.cited.length,
104
+ }, started);
105
+ }
106
+ async function save(deps, conversationId, out, started) {
107
+ const message = await deps.store.appendMessage(conversationId, {
108
+ role: "assistant",
109
+ content: out.content ?? "",
110
+ sources: out.sources ?? [],
111
+ plan: out.plan,
112
+ model: deps.model,
113
+ errorCode: out.errorCode ?? null,
114
+ notice: out.notice ?? null,
115
+ });
116
+ deps.log("chat: question answered", {
117
+ conversationId,
118
+ outcome: out.errorCode ?? out.notice ?? "answered",
119
+ planSkipped: out.plan.skipped,
120
+ queries: out.plan.queries.length,
121
+ tags: out.plan.tags.length,
122
+ categories: out.plan.profileCategories.length,
123
+ sources: out.sources?.length ?? 0,
124
+ cited: out.cited ?? 0,
125
+ answerChars: out.content?.length ?? 0,
126
+ ms: Date.now() - started,
127
+ });
128
+ const full = await deps.store.getConversation(conversationId);
129
+ return { ok: true, messages: full?.messages ?? [message] };
130
+ }
@@ -0,0 +1,16 @@
1
+ export interface ChatSettings {
2
+ memoryProvider?: string;
3
+ memoryModel?: string;
4
+ memoryApiUrl?: string;
5
+ memoryApiKey?: string;
6
+ }
7
+ export interface ChatStatus {
8
+ ready: boolean;
9
+ /** Names of the settings still needed. Never their values. */
10
+ missing: string[];
11
+ provider: string;
12
+ /** The host that receives questions and memory text, so the page can name it. */
13
+ apiHost: string | null;
14
+ }
15
+ /** Whether chat can call the external memory model. Mirrors `buildMemoryProviderConfig`. */
16
+ export declare function chatStatus(config: ChatSettings): ChatStatus;
@@ -0,0 +1,23 @@
1
+ import { isPlaceholderApiKey } from "../ai/api-key-placeholder.js";
2
+ /** Whether chat can call the external memory model. Mirrors `buildMemoryProviderConfig`. */
3
+ export function chatStatus(config) {
4
+ const provider = config.memoryProvider ?? "openai-chat";
5
+ const presetEndpoint = provider === "orcarouter";
6
+ const missing = [];
7
+ if (!config.memoryModel && !presetEndpoint)
8
+ missing.push("memoryModel");
9
+ if (!config.memoryApiUrl && !presetEndpoint)
10
+ missing.push("memoryApiUrl");
11
+ if (!config.memoryApiKey || isPlaceholderApiKey(config.memoryApiKey)) {
12
+ missing.push("memoryApiKey");
13
+ }
14
+ return { ready: missing.length === 0, missing, provider, apiHost: hostOf(config.memoryApiUrl) };
15
+ }
16
+ function hostOf(url) {
17
+ try {
18
+ return url ? new URL(url).host : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
@@ -0,0 +1,60 @@
1
+ import type { TursoDb } from "../turso/turso-db.js";
2
+ import { type ChatConversation, type ChatFolder, type ChatMessage, type ChatNotice, type ChatPlanRecord, type ChatRole, type ChatSource } from "./chat-types.js";
3
+ /** Bump when the tables change, and add the step to `migrate`. */
4
+ export declare const CHAT_SCHEMA_VERSION = 1;
5
+ export declare const FOLDER_NAME_MAX = 80;
6
+ export declare const BULK_DELETE_MAX = 500;
7
+ type GetConnection = (dbPath: string) => Promise<TursoDb>;
8
+ /**
9
+ * Conversations, messages, and folders in `chats.db`. One instance per file;
10
+ * every write checks the schema version so a file from a newer OMMS stays
11
+ * untouched.
12
+ */
13
+ export declare class ChatStore {
14
+ private readonly dbPath;
15
+ private readonly connect;
16
+ private readOnly;
17
+ private lastStamp;
18
+ constructor(dbPath: string, connect?: GetConnection);
19
+ private db;
20
+ private now;
21
+ init(): Promise<void>;
22
+ isReadOnly(): boolean;
23
+ private writable;
24
+ listConversations(): Promise<ChatConversation[]>;
25
+ getConversation(id: string): Promise<{
26
+ conversation: ChatConversation;
27
+ messages: ChatMessage[];
28
+ } | null>;
29
+ createConversation(input: {
30
+ title: string;
31
+ scopeTag: string | null;
32
+ folderId?: string | null;
33
+ }): Promise<ChatConversation>;
34
+ private updateConversation;
35
+ renameConversation(id: string, title: string): Promise<void>;
36
+ setConversationScope(id: string, scopeTag: string | null): Promise<void>;
37
+ moveConversation(id: string, folderId: string | null): Promise<void>;
38
+ deleteConversation(id: string): Promise<void>;
39
+ /** Deletes every id or none: an unknown id fails the whole request. */
40
+ bulkDeleteConversations(ids: string[]): Promise<number>;
41
+ appendMessage(conversationId: string, input: {
42
+ role: ChatRole;
43
+ content: string;
44
+ sources?: ChatSource[];
45
+ plan?: ChatPlanRecord | null;
46
+ model?: string | null;
47
+ errorCode?: string | null;
48
+ notice?: ChatNotice | null;
49
+ }): Promise<ChatMessage>;
50
+ deleteMessage(id: string): Promise<void>;
51
+ countMessages(): Promise<number>;
52
+ listFolders(): Promise<ChatFolder[]>;
53
+ private requireFolder;
54
+ private assertNameFree;
55
+ createFolder(name: string): Promise<ChatFolder>;
56
+ renameFolder(id: string, name: string): Promise<void>;
57
+ /** Deletes the folder and every conversation in it. Returns how many conversations went. */
58
+ deleteFolder(id: string): Promise<number>;
59
+ }
60
+ export {};