@kisev/memomatic 1.0.0-dev.70.g8d00098df14b → 1.0.0-dev.71.gee4e37a5bb61

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/dist/ingest.js CHANGED
@@ -1,107 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { isAbsolute, join } from "node:path";
3
- import { DatabaseSync } from "node:sqlite";
4
3
  export function opencodeDatabasePath() {
5
- const base = process.env.XDG_DATA_HOME
6
- ? process.env.XDG_DATA_HOME
7
- : join(homedir(), ".local", "share");
4
+ const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local/share");
8
5
  if (!isAbsolute(base))
9
6
  throw new Error("XDG_DATA_HOME must be an absolute path");
10
7
  return join(base, "opencode", "opencode.db");
11
8
  }
12
- function readMessages(db, sessionId) {
13
- const hasParts = db
14
- .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'part'")
15
- .get();
16
- const rows = db
17
- .prepare(`SELECT ${hasParts ? "id, " : ""}data FROM message WHERE session_id = ? ORDER BY time_created ASC;`)
18
- .all(sessionId);
19
- if (!hasParts)
20
- return rows;
21
- const parts = db.prepare("SELECT data FROM part WHERE message_id = ? ORDER BY time_created ASC, id ASC");
22
- return rows.map((row) => {
23
- try {
24
- const message = JSON.parse(row.data);
25
- message.parts = parts.all(row.id).flatMap((part) => {
26
- try {
27
- return [JSON.parse(part.data)];
28
- }
29
- catch {
30
- return [];
31
- }
32
- });
33
- return { data: JSON.stringify(message) };
34
- }
35
- catch {
36
- return row;
37
- }
38
- });
39
- }
40
- export function loadRecentSessions(databaseFile, afterTimeCreated, options = {
41
- maxSessions: 20,
42
- maxCharsPerSession: 24_000,
43
- }) {
44
- let db;
45
- try {
46
- db = new DatabaseSync(databaseFile, { readOnly: true });
47
- }
48
- catch {
49
- return [];
50
- }
51
- try {
52
- const before = options.before ?? Number.MAX_SAFE_INTEGER;
53
- const sessions = db
54
- .prepare(`SELECT id, title, directory, time_created FROM session
55
- WHERE time_created > ? AND time_created < ? ORDER BY time_created ASC LIMIT ?;`)
56
- .all(afterTimeCreated, before, options.maxSessions);
57
- const result = [];
58
- for (const session of sessions) {
59
- const messages = [];
60
- let budget = options.maxCharsPerSession;
61
- let internal = false;
62
- const rows = readMessages(db, session.id);
63
- for (const row of rows) {
64
- if (budget <= 0)
65
- break;
66
- let parsed;
67
- try {
68
- parsed = JSON.parse(row.data);
69
- }
70
- catch {
71
- continue;
72
- }
73
- const texts = [];
74
- for (const part of parsed.parts ?? []) {
75
- const value = part;
76
- if (value.type === "text" && typeof value.text === "string")
77
- texts.push(value.text);
78
- }
79
- const text = texts.join("\n").trim();
80
- if (!text)
81
- continue;
82
- if (text.includes("[memomatic-internal]"))
83
- internal = true;
84
- if (budget - text.length < 0) {
85
- messages.push({ role: parsed.role ?? "unknown", text: text.slice(0, budget) });
86
- budget = 0;
87
- break;
88
- }
89
- budget -= text.length;
90
- messages.push({ role: parsed.role ?? "unknown", text });
91
- }
92
- if (internal || !messages.length)
93
- continue;
94
- result.push({
95
- id: session.id,
96
- title: session.title,
97
- directory: session.directory,
98
- timeCreated: session.time_created,
99
- messages,
100
- });
101
- }
102
- return result;
103
- }
104
- finally {
105
- db.close();
106
- }
107
- }
@@ -0,0 +1,30 @@
1
+ import type { MemoryStore } from "./store.js";
2
+ export type Fragment = {
3
+ id: string;
4
+ sessionId: string;
5
+ messageIds: string[];
6
+ title: string;
7
+ directory: string;
8
+ text: string;
9
+ context: string;
10
+ checkpointKeys: string[];
11
+ seal?: [string, string];
12
+ };
13
+ export type IngestionPlan = {
14
+ fragments: Fragment[];
15
+ sessions: number;
16
+ messages: number;
17
+ cached: number;
18
+ skipped: number;
19
+ characters: number;
20
+ watermark: number;
21
+ cachedSessions: number;
22
+ unscannedSessions: number;
23
+ seals: Array<[string, string]>;
24
+ };
25
+ export declare function planIngestion(file: string, store: MemoryStore, options: {
26
+ before: number;
27
+ maxChars: number;
28
+ maxSessions?: number;
29
+ contextChars?: number;
30
+ }): IngestionPlan;
@@ -0,0 +1,209 @@
1
+ import { createHash } from "node:crypto";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ const digest = (text) => createHash("sha256").update(text).digest("hex");
4
+ export function planIngestion(file, store, options) {
5
+ if (!Number.isSafeInteger(options.maxChars) ||
6
+ options.maxChars < 2 ||
7
+ !Number.isFinite(options.before))
8
+ throw new Error("invalid ingestion limits");
9
+ const result = {
10
+ fragments: [],
11
+ sessions: 0,
12
+ messages: 0,
13
+ cached: 0,
14
+ skipped: 0,
15
+ characters: 0,
16
+ watermark: 0,
17
+ cachedSessions: 0,
18
+ unscannedSessions: 0,
19
+ seals: [],
20
+ };
21
+ const db = new DatabaseSync(file, { readOnly: true });
22
+ try {
23
+ db.exec("BEGIN");
24
+ const hasParts = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE name='part' AND type='table'").get());
25
+ const hasUpdated = db
26
+ .prepare("PRAGMA table_info(session)")
27
+ .all()
28
+ .some((row) => row.name === "time_updated");
29
+ const fastRevision = hasParts &&
30
+ hasUpdated &&
31
+ ["message", "part"].every((table) => db
32
+ .prepare(`PRAGMA table_info(${table})`)
33
+ .all()
34
+ .some((row) => row.name === "time_updated"));
35
+ const partHasSession = hasParts &&
36
+ db
37
+ .prepare("PRAGMA table_info(part)")
38
+ .all()
39
+ .some((row) => row.name === "session_id");
40
+ const sessions = db
41
+ .prepare(`SELECT id,title,directory,time_created,${hasUpdated ? "time_updated" : "time_created"} AS revision FROM session WHERE ${hasUpdated ? "time_updated" : "time_created"} < ? ORDER BY time_created,id`)
42
+ .all(options.before);
43
+ for (const [position, session] of sessions.entries()) {
44
+ if (session.title.startsWith("[memomatic-internal]")) {
45
+ result.skipped++;
46
+ continue;
47
+ }
48
+ const sealKey = `ingest-session-v2:${digest(session.id)}`;
49
+ let seal;
50
+ if (fastRevision) {
51
+ const messages = db
52
+ .prepare("SELECT COUNT(*) AS count,MAX(time_updated) AS updated FROM message WHERE session_id=?")
53
+ .get(session.id);
54
+ const parts = db
55
+ .prepare(`SELECT COUNT(*) AS count,MAX(time_updated) AS updated FROM part WHERE ${partHasSession ? "session_id=?" : "message_id IN (SELECT id FROM message WHERE session_id=?)"}`)
56
+ .get(session.id);
57
+ if (Math.max(Number(messages.updated), Number(parts.updated)) >= options.before) {
58
+ result.skipped++;
59
+ continue;
60
+ }
61
+ const revision = digest(JSON.stringify([session.revision, session.title, session.directory, messages, parts]));
62
+ if (store.getMeta(sealKey) === revision) {
63
+ result.cachedSessions++;
64
+ continue;
65
+ }
66
+ seal = [sealKey, revision];
67
+ }
68
+ const rows = hasParts
69
+ ? db
70
+ .prepare(`SELECT m.id,m.data AS message,p.data AS part FROM message m JOIN part p ON p.message_id=m.id
71
+ WHERE ${partHasSession ? "p" : "m"}.session_id=?
72
+ AND CASE WHEN json_valid(p.data) THEN json_extract(p.data,'$.type') END='text'
73
+ AND CASE WHEN json_valid(m.data) THEN json_extract(m.data,'$.role') END IN ('user','assistant')
74
+ ORDER BY m.time_created,m.id,p.time_created,p.id`)
75
+ .all(session.id)
76
+ : db
77
+ .prepare("SELECT rowid AS id,data FROM message WHERE session_id=? ORDER BY time_created,rowid")
78
+ .all(session.id).flatMap((row) => {
79
+ try {
80
+ const message = JSON.parse(row.data);
81
+ return (message.parts ?? []).map((part) => ({
82
+ id: String(row.id),
83
+ message: row.data,
84
+ part: JSON.stringify(part),
85
+ }));
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ });
91
+ const messages = new Map();
92
+ for (const row of rows) {
93
+ try {
94
+ const message = JSON.parse(row.message);
95
+ const part = JSON.parse(row.part);
96
+ if (!["user", "assistant"].includes(message.role) ||
97
+ part.type !== "text" ||
98
+ typeof part.text !== "string")
99
+ continue;
100
+ const item = messages.get(row.id) ?? {
101
+ role: message.role,
102
+ texts: [],
103
+ };
104
+ item.texts.push(part.text);
105
+ messages.set(row.id, item);
106
+ }
107
+ catch {
108
+ result.skipped++;
109
+ }
110
+ }
111
+ const firstUser = [...messages.values()].find((message) => message.role === "user");
112
+ if (firstUser?.texts.join("\n").trimStart().startsWith("[memomatic-internal]")) {
113
+ result.skipped++;
114
+ continue;
115
+ }
116
+ let batch = [];
117
+ let ids = [];
118
+ let keys = [];
119
+ let context = "";
120
+ let previous = "";
121
+ let length = 0;
122
+ let changed = false;
123
+ const flush = () => {
124
+ if (!batch.length)
125
+ return;
126
+ result.fragments.push({
127
+ id: digest(keys.join("\n")),
128
+ sessionId: session.id,
129
+ messageIds: ids,
130
+ title: session.title,
131
+ directory: session.directory,
132
+ text: batch.join("\n\n"),
133
+ context,
134
+ checkpointKeys: keys,
135
+ });
136
+ batch = [];
137
+ ids = [];
138
+ keys = [];
139
+ length = 0;
140
+ };
141
+ let lastMessage = "";
142
+ for (const [id, message] of messages) {
143
+ const body = message.texts.join("\n").trim();
144
+ if (!body) {
145
+ result.skipped++;
146
+ continue;
147
+ }
148
+ result.messages++;
149
+ const full = `${message.role}: ${body}`;
150
+ // Only adjacent exact duplicate messages with the same role are redundant.
151
+ if (full === lastMessage) {
152
+ result.skipped++;
153
+ continue;
154
+ }
155
+ lastMessage = full;
156
+ for (let offset = 0; offset < body.length;) {
157
+ const start = offset;
158
+ let end = Math.min(body.length, offset + options.maxChars);
159
+ if (end < body.length &&
160
+ /[\uD800-\uDBFF]/.test(body[end - 1]) &&
161
+ /[\uDC00-\uDFFF]/.test(body[end]))
162
+ end--;
163
+ const text = `${message.role}: ${body.slice(start, end)}`;
164
+ offset = end;
165
+ const preceding = Array.from(previous)
166
+ .slice(-(options.contextChars ?? 2000))
167
+ .join("");
168
+ const key = `ingest-v2:${digest(`${session.id}\n${id}\n${start}\n${preceding}\n${text}`)}`;
169
+ if (store.getMeta(key)) {
170
+ result.cached++;
171
+ previous = text;
172
+ continue;
173
+ }
174
+ if (batch.length && length + text.length > options.maxChars)
175
+ flush();
176
+ if (!batch.length)
177
+ context = preceding;
178
+ batch.push(text);
179
+ ids.push(id);
180
+ keys.push(key);
181
+ length += text.length;
182
+ result.characters += text.length;
183
+ changed = true;
184
+ previous = text;
185
+ }
186
+ }
187
+ flush();
188
+ if (seal) {
189
+ if (changed)
190
+ result.fragments.at(-1).seal = seal;
191
+ else
192
+ result.seals.push(seal);
193
+ }
194
+ if (changed) {
195
+ result.sessions++;
196
+ result.watermark = Math.max(result.watermark, session.time_created);
197
+ }
198
+ if (options.maxSessions && result.sessions >= options.maxSessions) {
199
+ result.unscannedSessions = sessions.length - position - 1;
200
+ break;
201
+ }
202
+ }
203
+ db.exec("COMMIT");
204
+ return result;
205
+ }
206
+ finally {
207
+ db.close();
208
+ }
209
+ }
@@ -0,0 +1,7 @@
1
+ import type { Observe } from "./generated/cli.js";
2
+ export type OperationOptions = {
3
+ signal?: AbortSignal;
4
+ observe?: Observe;
5
+ force?: boolean;
6
+ };
7
+ export declare function check(options: OperationOptions): void;
@@ -0,0 +1,3 @@
1
+ export function check(options) {
2
+ options.signal?.throwIfAborted();
3
+ }
package/dist/paths.js CHANGED
@@ -25,7 +25,12 @@ async function assertDirectory(path) {
25
25
  }
26
26
  }
27
27
  export function memomaticPaths() {
28
- const stateRoot = join(safeXdg("XDG_STATE_HOME", ".local/state"), "memomatic");
28
+ const configured = process.env.MEMOMATIC_HOME;
29
+ if (configured && (!isAbsolute(configured) || configured.split(sep).includes("..")))
30
+ throw new Error("MEMOMATIC_HOME must be an absolute safe path");
31
+ const stateRoot = configured
32
+ ? resolve(configured)
33
+ : join(safeXdg("XDG_STATE_HOME", ".local/state"), "memomatic");
29
34
  const configRoot = join(safeXdg("XDG_CONFIG_HOME", ".config"), "memomatic");
30
35
  return {
31
36
  stateRoot,
@@ -34,7 +39,7 @@ export function memomaticPaths() {
34
39
  userFile: join(stateRoot, "USER.md"),
35
40
  dreamsFile: join(stateRoot, "DREAMS.md"),
36
41
  rulesFile: join(configRoot, "MEMORY_RULES.md"),
37
- settingsFile: join(configRoot, "settings.json"),
42
+ settingsFile: process.env.MEMOMATIC_CONFIG ?? join(configRoot, "settings.json"),
38
43
  dailyDir: join(stateRoot, "memory"),
39
44
  archiveDir: join(stateRoot, "archive"),
40
45
  historyDir: join(stateRoot, "history"),
package/dist/search.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type IndexedEntry, MemoryStore } from "./store.js";
2
2
  import type { MemomaticPaths } from "./paths.js";
3
3
  import type { MemomaticSettings } from "./settings.js";
4
+ import { type OperationOptions } from "./operations.js";
4
5
  export type SearchHit = {
5
6
  entry: IndexedEntry;
6
7
  score: number;
@@ -17,7 +18,7 @@ export type SearchHit = {
17
18
  };
18
19
  };
19
20
  export declare function corpusFiles(paths: MemomaticPaths): Promise<string[]>;
20
- export declare function reindex(paths: MemomaticPaths, settings: MemomaticSettings, store: MemoryStore): Promise<number>;
21
+ export declare function reindex(paths: MemomaticPaths, settings: MemomaticSettings, store: MemoryStore, options?: OperationOptions): Promise<number>;
21
22
  export type SearchOptions = {
22
23
  includeArchived?: boolean;
23
24
  markSurfaced?: boolean;
package/dist/search.js CHANGED
@@ -4,6 +4,7 @@ import { cosineSimilarity, stableIdFor } from "./store.js";
4
4
  import { entryKind, parseCorpusEntries } from "./corpus.js";
5
5
  import { entryImportance, entryObservedAt } from "./entries.js";
6
6
  import { embedTexts, embeddingFingerprint } from "./settings.js";
7
+ import { check } from "./operations.js";
7
8
  export async function corpusFiles(paths) {
8
9
  const files = [];
9
10
  for (const name of ["MEMORY.md", "USER.md"]) {
@@ -17,7 +18,9 @@ export async function corpusFiles(paths) {
17
18
  }
18
19
  return files;
19
20
  }
20
- export async function reindex(paths, settings, store) {
21
+ export async function reindex(paths, settings, store, options = {}) {
22
+ check(options);
23
+ options.observe?.({ phase: "index.scan", message: "Reading memory files" });
21
24
  const entries = [];
22
25
  const indexedIds = new Set();
23
26
  for (const file of await corpusFiles(paths)) {
@@ -47,8 +50,55 @@ export async function reindex(paths, settings, store) {
47
50
  });
48
51
  }
49
52
  }
50
- const vectors = await embedTexts(settings, entries.map((entry) => entry.text));
53
+ const prior = new Map(store.allEntries().map((entry) => [entry.stableId, entry.text]));
54
+ const cache = new Map(store.vectors().map((row) => [row.stableId, row.data]));
55
+ const compatible = !options.force && store.getMeta("embeddingFingerprint") === embeddingFingerprint(settings);
56
+ const vectors = settings.embedding ? [] : null;
57
+ const missing = entries
58
+ .map((entry, index) => ({ entry, index }))
59
+ .filter(({ entry, index }) => {
60
+ const cached = compatible && prior.get(entry.stableId) === entry.text
61
+ ? cache.get(entry.stableId)
62
+ : undefined;
63
+ if (cached && vectors) {
64
+ vectors[index] = cached;
65
+ return false;
66
+ }
67
+ return Boolean(vectors);
68
+ });
69
+ options.observe?.({
70
+ phase: "index.embeddings",
71
+ message: "Embedding changed entries",
72
+ total: missing.length,
73
+ completed: 0,
74
+ cached: entries.length - missing.length,
75
+ });
76
+ for (let start = 0; start < missing.length; start += 32) {
77
+ check(options);
78
+ const batch = missing.slice(start, start + 32);
79
+ const computed = await embedTexts(settings, batch.map((item) => item.entry.text), "document", options.signal);
80
+ if (vectors && computed)
81
+ batch.forEach((item, index) => {
82
+ vectors[item.index] = computed[index];
83
+ });
84
+ options.observe?.({
85
+ phase: "index.embeddings",
86
+ message: "Embedding batch complete",
87
+ total: missing.length,
88
+ completed: Math.min(start + 32, missing.length),
89
+ });
90
+ }
91
+ check(options);
92
+ if (vectors?.length && vectors.some((vector) => vector.length !== vectors[0].length))
93
+ throw new Error("embedding dimensions changed; run memomatic index --force");
51
94
  store.replaceIndex(entries, vectors, embeddingFingerprint(settings));
95
+ options.observe?.({
96
+ phase: "index.done",
97
+ message: "Index committed",
98
+ entries: entries.length,
99
+ embedded: missing.length,
100
+ cached: entries.length - missing.length,
101
+ });
52
102
  return entries.length;
53
103
  }
54
104
  function recencyMultiplier(entry, halfLifeDays, now = Date.now()) {
package/dist/service.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type MemoryRules } from "./rules.js";
2
2
  import { type MemomaticPaths } from "./paths.js";
3
3
  import { type MemomaticSettings } from "./settings.js";
4
+ import { type OperationOptions } from "./operations.js";
4
5
  import { type SearchHit, type SearchOptions } from "./search.js";
5
6
  import { MemoryStore } from "./store.js";
6
7
  export type WriteRequest = {
@@ -22,6 +23,7 @@ export type MemomaticContext = {
22
23
  };
23
24
  export declare function openMemomatic(options?: {
24
25
  readOnly?: boolean;
26
+ configure?: (settings: MemomaticSettings) => MemomaticSettings;
25
27
  }): Promise<MemomaticContext>;
26
28
  export declare function writeEntry(context: MemomaticContext, request: WriteRequest): Promise<string>;
27
29
  export declare function searchMemory(context: MemomaticContext, query: string, options?: SearchOptions): Promise<SearchHit[]>;
@@ -57,5 +59,5 @@ export declare function forgetEntry(context: MemomaticContext, target: {
57
59
  file: string;
58
60
  line: number;
59
61
  }): Promise<string>;
60
- export declare function archiveOldEpisodic(context: MemomaticContext): Promise<string[]>;
61
- export declare function rebuildIndex(context: MemomaticContext): Promise<number>;
62
+ export declare function archiveOldEpisodic(context: MemomaticContext, options?: OperationOptions): Promise<string[]>;
63
+ export declare function rebuildIndex(context: MemomaticContext, options?: OperationOptions): Promise<number>;
package/dist/service.js CHANGED
@@ -7,6 +7,7 @@ import { dropToInbox, withRunLock } from "./inbox.js";
7
7
  import { isForbidden, loadRules } from "./rules.js";
8
8
  import { memomaticPaths } from "./paths.js";
9
9
  import { loadSettings } from "./settings.js";
10
+ import { check } from "./operations.js";
10
11
  import { reindex, search } from "./search.js";
11
12
  import { MemoryStore } from "./store.js";
12
13
  import { visibilityForSource } from "./visibility.js";
@@ -20,11 +21,12 @@ async function memoryPath(context, file) {
20
21
  }
21
22
  export async function openMemomatic(options = {}) {
22
23
  const paths = memomaticPaths();
23
- const [settings, rules, store] = await Promise.all([
24
- loadSettings(paths.settingsFile),
25
- loadRules(paths.rulesFile),
26
- options.readOnly ? MemoryStore.preview(paths.indexFile) : MemoryStore.open(paths.indexFile),
27
- ]);
24
+ const loaded = await loadSettings(paths.settingsFile);
25
+ const settings = options.configure ? options.configure(loaded) : loaded;
26
+ const rules = await loadRules(paths.rulesFile);
27
+ const store = options.readOnly
28
+ ? await MemoryStore.preview(paths.indexFile)
29
+ : await MemoryStore.open(paths.indexFile);
28
30
  return { paths, settings, rules, store };
29
31
  }
30
32
  export async function writeEntry(context, request) {
@@ -100,7 +102,7 @@ export async function forgetEntry(context, target) {
100
102
  return file;
101
103
  });
102
104
  }
103
- export async function archiveOldEpisodic(context) {
105
+ export async function archiveOldEpisodic(context, options = {}) {
104
106
  const archived = [];
105
107
  if (!context.rules.autoClean)
106
108
  return archived;
@@ -111,6 +113,7 @@ export async function archiveOldEpisodic(context) {
111
113
  .filter((entry) => entry.kind === "episodic")
112
114
  .map((entry) => entry.file));
113
115
  for (const file of files) {
116
+ check(options);
114
117
  await memoryPath(context, file);
115
118
  const content = await readTextIfExists(file);
116
119
  if (content === undefined)
@@ -141,6 +144,6 @@ export async function archiveOldEpisodic(context) {
141
144
  }
142
145
  return archived;
143
146
  }
144
- export async function rebuildIndex(context) {
145
- return withRunLock(context.paths, () => reindex(context.paths, context.settings, context.store));
147
+ export async function rebuildIndex(context, options = {}) {
148
+ return withRunLock(context.paths, () => reindex(context.paths, context.settings, context.store, options));
146
149
  }
@@ -13,6 +13,13 @@ export type MemomaticSettings = {
13
13
  maxCandidates: number;
14
14
  memoryBudgetLines: number;
15
15
  maxPriorEntryLoss: number;
16
+ timeoutMs: number;
17
+ maxDurationMs: number;
18
+ maxSessions: number;
19
+ maxChars: number;
20
+ retries: number;
21
+ idleMs: number;
22
+ opencodeUrl: string | null;
16
23
  };
17
24
  search: {
18
25
  maxResults: number;
@@ -28,7 +35,7 @@ export type MemomaticSettings = {
28
35
  export declare const defaultSettings: () => MemomaticSettings;
29
36
  export declare function normalizeSettings(raw: unknown): MemomaticSettings;
30
37
  export declare function loadSettings(settingsFile: string): Promise<MemomaticSettings>;
31
- export declare function embedTexts(settings: MemomaticSettings, texts: string[], purpose?: "document" | "query"): Promise<Float32Array[] | null>;
38
+ export declare function embedTexts(settings: MemomaticSettings, texts: string[], purpose?: "document" | "query", signal?: AbortSignal): Promise<Float32Array[] | null>;
32
39
  export declare const MEMORY_QUERY_INSTRUCTION = "Given a search query, retrieve relevant personal memory entries that answer the query.";
33
40
  export declare function embeddingQuery(settings: MemomaticSettings, query: string): string;
34
41
  export declare function embeddingFingerprint(settings: MemomaticSettings): string;
package/dist/settings.js CHANGED
@@ -11,6 +11,13 @@ export const defaultSettings = () => ({
11
11
  maxCandidates: 12,
12
12
  memoryBudgetLines: 80,
13
13
  maxPriorEntryLoss: 0.25,
14
+ timeoutMs: 180000,
15
+ maxDurationMs: 0,
16
+ maxSessions: 0,
17
+ maxChars: 24000,
18
+ retries: 1,
19
+ idleMs: 600000,
20
+ opencodeUrl: null,
14
21
  },
15
22
  search: { maxResults: 6, minScore: 0.35, minSemanticScore: 0.45, halfLifeDays: 30 },
16
23
  archive: { enabled: false, days: 60 },
@@ -30,6 +37,25 @@ export function normalizeSettings(raw) {
30
37
  const source = (raw ?? {});
31
38
  const embedding = source.embedding;
32
39
  const search = mergeSection(defaults.search, source.search);
40
+ const dream = mergeSection(defaults.dream, source.dream);
41
+ for (const key of [
42
+ "timeoutMs",
43
+ "maxDurationMs",
44
+ "maxSessions",
45
+ "maxChars",
46
+ "retries",
47
+ "idleMs",
48
+ ])
49
+ if (!Number.isSafeInteger(dream[key]) || dream[key] < 0)
50
+ throw new Error(`invalid dream.${key}`);
51
+ if (dream.timeoutMs < 1 || dream.maxChars < 256 || dream.maxChars > 200000 || dream.retries > 5)
52
+ throw new Error("invalid Dream request limits");
53
+ if (dream.model !== null && (typeof dream.model !== "string" || !/^[^/]+\/.+/.test(dream.model)))
54
+ throw new Error("model must use provider/model format");
55
+ if (dream.variant !== null && typeof dream.variant !== "string")
56
+ throw new Error("dream.variant must be a string or null");
57
+ if (dream.opencodeUrl !== null && typeof dream.opencodeUrl !== "string")
58
+ throw new Error("dream.opencodeUrl must be a string or null");
33
59
  if (!Number.isInteger(search.maxResults) ||
34
60
  search.maxResults < 1 ||
35
61
  search.maxResults > 200 ||
@@ -47,7 +73,7 @@ export function normalizeSettings(raw) {
47
73
  : {}),
48
74
  }
49
75
  : null,
50
- dream: mergeSection(defaults.dream, source.dream),
76
+ dream,
51
77
  search,
52
78
  archive: mergeSection(defaults.archive, source.archive),
53
79
  };
@@ -62,7 +88,7 @@ export async function loadSettings(settingsFile) {
62
88
  throw error;
63
89
  }
64
90
  }
65
- export async function embedTexts(settings, texts, purpose = "document") {
91
+ export async function embedTexts(settings, texts, purpose = "document", signal) {
66
92
  if (!settings.embedding || !texts.length)
67
93
  return null;
68
94
  const response = await fetch(settings.embedding.url, {
@@ -72,7 +98,9 @@ export async function embedTexts(settings, texts, purpose = "document") {
72
98
  }),
73
99
  headers: { "content-type": "application/json" },
74
100
  method: "POST",
75
- signal: AbortSignal.timeout(30_000),
101
+ signal: signal
102
+ ? AbortSignal.any([signal, AbortSignal.timeout(30_000)])
103
+ : AbortSignal.timeout(30_000),
76
104
  });
77
105
  if (!response.ok)
78
106
  throw new Error(`embedding request failed: ${response.status} ${await response.text()}`);
package/dist/store.js CHANGED
@@ -71,6 +71,8 @@ export class MemoryStore {
71
71
  .run(...Object.values(row));
72
72
  }
73
73
  }
74
+ if (copy.fts)
75
+ copy.db.exec("INSERT INTO entries_fts(stable_id,text) SELECT stable_id,text FROM entries");
74
76
  return copy;
75
77
  }
76
78
  catch (error) {