nemoris 0.1.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/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { buildResultSignature } from "./memory-signature.js";
|
|
4
|
+
import { createRuntimeId } from "../utils/ids.js";
|
|
5
|
+
import { hammingDistance, hexToSimhash } from "./simhash.js";
|
|
6
|
+
|
|
7
|
+
function uniqueId(prefix) {
|
|
8
|
+
return createRuntimeId(prefix);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeQuery(query) {
|
|
12
|
+
return String(query || "")
|
|
13
|
+
.split(/[^a-z0-9]+/i)
|
|
14
|
+
.map((token) => token.trim())
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.map((token) => `"${token.replace(/"/g, "")}"`)
|
|
17
|
+
.join(" OR ");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toRow(record) {
|
|
21
|
+
return {
|
|
22
|
+
entryId: record.entryId || null,
|
|
23
|
+
entrySignature: record.entrySignature || buildResultSignature(record),
|
|
24
|
+
type: record.type,
|
|
25
|
+
category: record.category || null,
|
|
26
|
+
title: record.title || null,
|
|
27
|
+
content: record.content || null,
|
|
28
|
+
summary: record.summary || null,
|
|
29
|
+
reason: record.reason || null,
|
|
30
|
+
timestamp: record.timestamp || null,
|
|
31
|
+
salience: record.salience ?? null,
|
|
32
|
+
accessCount: record.accessCount ?? 0,
|
|
33
|
+
expiresAt: record.expiresAt || null,
|
|
34
|
+
sourceKind: record.sourceKind || null,
|
|
35
|
+
dedupeKey: record.dedupeKey || null,
|
|
36
|
+
simhash: record.simhash || null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fromRow(row) {
|
|
41
|
+
return {
|
|
42
|
+
entryId: row.entry_id,
|
|
43
|
+
entrySignature: row.entry_signature,
|
|
44
|
+
type: row.type,
|
|
45
|
+
category: row.category,
|
|
46
|
+
title: row.title,
|
|
47
|
+
content: row.content,
|
|
48
|
+
summary: row.summary,
|
|
49
|
+
reason: row.reason,
|
|
50
|
+
timestamp: row.timestamp,
|
|
51
|
+
salience: row.salience,
|
|
52
|
+
accessCount: row.access_count ?? 0,
|
|
53
|
+
expiresAt: row.expires_at,
|
|
54
|
+
sourceKind: row.source_kind,
|
|
55
|
+
dedupeKey: row.dedupe_key,
|
|
56
|
+
simhash: row.simhash
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function cosineSimilarity(a, b) {
|
|
61
|
+
let dot = 0;
|
|
62
|
+
let normA = 0;
|
|
63
|
+
let normB = 0;
|
|
64
|
+
const length = Math.min(a.length, b.length);
|
|
65
|
+
for (let i = 0; i < length; i += 1) {
|
|
66
|
+
dot += a[i] * b[i];
|
|
67
|
+
normA += a[i] * a[i];
|
|
68
|
+
normB += b[i] * b[i];
|
|
69
|
+
}
|
|
70
|
+
if (normA === 0 || normB === 0) return 0;
|
|
71
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildDuplicateSignature(record, kind) {
|
|
75
|
+
if (kind === "summary") {
|
|
76
|
+
return [record.title, record.category, record.summary, record.content].map((value) => String(value || "")).join("::");
|
|
77
|
+
}
|
|
78
|
+
if (kind === "fact") {
|
|
79
|
+
return [record.title, record.category, record.content, record.reason].map((value) => String(value || "")).join("::");
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class SqliteActiveStore {
|
|
85
|
+
constructor({ dbPath }) {
|
|
86
|
+
this.dbPath = dbPath;
|
|
87
|
+
this.walPath = `${dbPath}-wal`;
|
|
88
|
+
this.db = new DatabaseSync(dbPath, {
|
|
89
|
+
timeout: 5000
|
|
90
|
+
});
|
|
91
|
+
this.db.exec("pragma busy_timeout = 5000;");
|
|
92
|
+
this.db.exec("pragma journal_mode = wal;");
|
|
93
|
+
this.db.exec("pragma synchronous = normal;");
|
|
94
|
+
this.ensureSchema();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
ensureSchema() {
|
|
98
|
+
this.db.exec(`
|
|
99
|
+
create table if not exists memory_entries (
|
|
100
|
+
rowid integer primary key autoincrement,
|
|
101
|
+
entry_id text not null unique,
|
|
102
|
+
entry_signature text not null,
|
|
103
|
+
type text not null,
|
|
104
|
+
category text,
|
|
105
|
+
title text,
|
|
106
|
+
content text,
|
|
107
|
+
summary text,
|
|
108
|
+
reason text,
|
|
109
|
+
timestamp text,
|
|
110
|
+
salience real,
|
|
111
|
+
access_count integer default 0,
|
|
112
|
+
expires_at text,
|
|
113
|
+
source_kind text,
|
|
114
|
+
dedupe_key text,
|
|
115
|
+
duplicate_signature text
|
|
116
|
+
);
|
|
117
|
+
create unique index if not exists idx_memory_entries_dedupe_key on memory_entries(dedupe_key) where dedupe_key is not null;
|
|
118
|
+
create unique index if not exists idx_memory_entries_duplicate_signature on memory_entries(type, duplicate_signature) where duplicate_signature is not null;
|
|
119
|
+
create index if not exists idx_memory_entries_expires_at on memory_entries(expires_at);
|
|
120
|
+
create virtual table if not exists memory_entries_fts using fts5(title, content, summary, reason, category);
|
|
121
|
+
create table if not exists memory_embeddings (
|
|
122
|
+
entry_id text primary key,
|
|
123
|
+
entry_signature text not null,
|
|
124
|
+
entry_json text not null,
|
|
125
|
+
vector_json text not null,
|
|
126
|
+
updated_at text,
|
|
127
|
+
provider text,
|
|
128
|
+
model text,
|
|
129
|
+
dimensions integer,
|
|
130
|
+
freshness_status text not null default 'fresh',
|
|
131
|
+
embedded_signature text
|
|
132
|
+
);
|
|
133
|
+
create table if not exists embedding_meta (
|
|
134
|
+
key text primary key,
|
|
135
|
+
value text
|
|
136
|
+
);
|
|
137
|
+
create table if not exists runtime_meta (
|
|
138
|
+
key text primary key,
|
|
139
|
+
value text
|
|
140
|
+
);
|
|
141
|
+
create table if not exists memory_locks (
|
|
142
|
+
lock_key text primary key,
|
|
143
|
+
owner_id text not null,
|
|
144
|
+
acquired_at text not null,
|
|
145
|
+
expires_at text not null
|
|
146
|
+
);
|
|
147
|
+
`);
|
|
148
|
+
this.ensureColumn("memory_entries", "entry_signature", "text");
|
|
149
|
+
this.ensureEmbeddingSchema();
|
|
150
|
+
this.backfillEntrySignatures();
|
|
151
|
+
this.db.exec("create index if not exists idx_memory_entries_entry_signature on memory_entries(entry_signature);");
|
|
152
|
+
this.db.exec("create index if not exists idx_memory_embeddings_signature on memory_embeddings(entry_signature);");
|
|
153
|
+
this.ensureColumn("memory_entries", "simhash", "text");
|
|
154
|
+
this.db.exec("create index if not exists idx_memory_entries_simhash on memory_entries(simhash);");
|
|
155
|
+
this.ensureColumn("memory_entries", "last_accessed", "text");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
ensureColumn(tableName, columnName, definition) {
|
|
159
|
+
const columns = this.db.prepare(`pragma table_info(${tableName})`).all();
|
|
160
|
+
if (columns.some((column) => column.name === columnName)) return;
|
|
161
|
+
this.db.exec(`alter table ${tableName} add column ${columnName} ${definition}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
updateAccessTime(entryIds) {
|
|
165
|
+
if (!entryIds.length) return;
|
|
166
|
+
const now = new Date().toISOString();
|
|
167
|
+
const stmt = this.db.prepare("UPDATE memory_entries SET last_accessed = ? WHERE entry_id = ?");
|
|
168
|
+
for (const id of entryIds) {
|
|
169
|
+
stmt.run(now, id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ensureEmbeddingSchema() {
|
|
174
|
+
const columns = this.db.prepare("pragma table_info(memory_embeddings)").all();
|
|
175
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
176
|
+
const needsReset =
|
|
177
|
+
columnNames.size > 0 &&
|
|
178
|
+
(!columnNames.has("entry_id") ||
|
|
179
|
+
!columnNames.has("freshness_status") ||
|
|
180
|
+
!columnNames.has("embedded_signature"));
|
|
181
|
+
|
|
182
|
+
if (!needsReset) return;
|
|
183
|
+
|
|
184
|
+
this.db.exec("drop table if exists memory_embeddings;");
|
|
185
|
+
this.db.exec("drop table if exists embedding_meta;");
|
|
186
|
+
this.db.exec(`
|
|
187
|
+
create table if not exists memory_embeddings (
|
|
188
|
+
entry_id text primary key,
|
|
189
|
+
entry_signature text not null,
|
|
190
|
+
entry_json text not null,
|
|
191
|
+
vector_json text not null,
|
|
192
|
+
updated_at text,
|
|
193
|
+
provider text,
|
|
194
|
+
model text,
|
|
195
|
+
dimensions integer,
|
|
196
|
+
freshness_status text not null default 'fresh',
|
|
197
|
+
embedded_signature text
|
|
198
|
+
);
|
|
199
|
+
create table if not exists embedding_meta (
|
|
200
|
+
key text primary key,
|
|
201
|
+
value text
|
|
202
|
+
);
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
backfillEntrySignatures() {
|
|
207
|
+
const rows = this.db
|
|
208
|
+
.prepare(`
|
|
209
|
+
select entry_id, type, category, title, content, summary, reason, timestamp, salience, access_count, expires_at, source_kind, dedupe_key
|
|
210
|
+
from memory_entries
|
|
211
|
+
where entry_signature is null or entry_signature = ''
|
|
212
|
+
`)
|
|
213
|
+
.all();
|
|
214
|
+
const update = this.db.prepare("update memory_entries set entry_signature = ? where entry_id = ?");
|
|
215
|
+
for (const row of rows) {
|
|
216
|
+
update.run(buildResultSignature(fromRow(row)), row.entry_id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
runTransaction(fn) {
|
|
221
|
+
this.db.exec("begin immediate");
|
|
222
|
+
try {
|
|
223
|
+
const result = fn();
|
|
224
|
+
this.db.exec("commit");
|
|
225
|
+
return result;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
try {
|
|
228
|
+
this.db.exec("rollback");
|
|
229
|
+
} catch {}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
findNearDuplicates(type, simhashHex, maxDistance = 3) {
|
|
235
|
+
if (!simhashHex) return [];
|
|
236
|
+
const targetHash = hexToSimhash(simhashHex);
|
|
237
|
+
const rows = this.db.prepare(
|
|
238
|
+
"select entry_id, simhash, salience, access_count from memory_entries where type = ? and simhash is not null"
|
|
239
|
+
).all(type);
|
|
240
|
+
|
|
241
|
+
if (rows.length > 1000) {
|
|
242
|
+
console.warn(`[Nemoris] simhash: ${rows.length} entries for type "${type}" — consider compaction`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const matches = [];
|
|
246
|
+
for (const row of rows) {
|
|
247
|
+
const dist = hammingDistance(hexToSimhash(row.simhash), targetHash);
|
|
248
|
+
if (dist <= maxDistance) {
|
|
249
|
+
matches.push({
|
|
250
|
+
entryId: row.entry_id,
|
|
251
|
+
distance: dist,
|
|
252
|
+
salience: row.salience,
|
|
253
|
+
accessCount: row.access_count
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return matches;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
boostSalience(entryId, amount = 0.15) {
|
|
261
|
+
this.db.prepare(`
|
|
262
|
+
update memory_entries
|
|
263
|
+
set salience = min(1.0, salience + ?),
|
|
264
|
+
access_count = access_count + 1,
|
|
265
|
+
timestamp = ?
|
|
266
|
+
where entry_id = ?
|
|
267
|
+
`).run(amount, new Date().toISOString(), entryId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
close() {
|
|
271
|
+
this.db.close();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
getCheckpointStats(mode = "passive") {
|
|
275
|
+
const normalizedMode = String(mode || "passive").toLowerCase();
|
|
276
|
+
const row = this.db.prepare(`pragma wal_checkpoint(${normalizedMode})`).get();
|
|
277
|
+
return {
|
|
278
|
+
mode: normalizedMode,
|
|
279
|
+
busy: Number(row?.busy ?? 0),
|
|
280
|
+
log: Number(row?.log ?? 0),
|
|
281
|
+
checkpointed: Number(row?.checkpointed ?? 0)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async getWalStatus(mode = "passive") {
|
|
286
|
+
let walBytes;
|
|
287
|
+
try {
|
|
288
|
+
walBytes = (await stat(this.walPath)).size;
|
|
289
|
+
} catch {
|
|
290
|
+
walBytes = 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const checkpoint = this.getCheckpointStats(mode);
|
|
294
|
+
return {
|
|
295
|
+
dbPath: this.dbPath,
|
|
296
|
+
walPath: this.walPath,
|
|
297
|
+
walBytes,
|
|
298
|
+
checkpoint
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async manageWal(options = {}) {
|
|
303
|
+
const thresholdBytes = Number(options.thresholdBytes ?? 64 * 1024 * 1024);
|
|
304
|
+
const passive = await this.getWalStatus("passive");
|
|
305
|
+
const needsCheckpoint =
|
|
306
|
+
passive.walBytes >= thresholdBytes ||
|
|
307
|
+
(passive.checkpoint.log > 0 && passive.checkpoint.checkpointed < passive.checkpoint.log);
|
|
308
|
+
|
|
309
|
+
if (!needsCheckpoint) {
|
|
310
|
+
return {
|
|
311
|
+
...passive,
|
|
312
|
+
action: "none"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const restart = await this.getWalStatus("restart");
|
|
317
|
+
const truncatable = restart.checkpoint.busy === 0;
|
|
318
|
+
const finalStatus = truncatable ? await this.getWalStatus("truncate") : restart;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
...finalStatus,
|
|
322
|
+
action: truncatable ? "truncate" : "restart"
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
count() {
|
|
327
|
+
return this.db.prepare("select count(*) as count from memory_entries").get().count;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
clearAll() {
|
|
331
|
+
this.db.exec("delete from memory_entries; delete from memory_entries_fts; delete from memory_embeddings; delete from embedding_meta; delete from runtime_meta;");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
hasEventDedupe(dedupeKey) {
|
|
335
|
+
if (!dedupeKey) return false;
|
|
336
|
+
return Boolean(
|
|
337
|
+
this.db
|
|
338
|
+
.prepare("select 1 from memory_entries where dedupe_key = ? limit 1")
|
|
339
|
+
.get(String(dedupeKey))
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
hasDuplicateSignature(type, duplicateSignature) {
|
|
344
|
+
if (!duplicateSignature) return false;
|
|
345
|
+
return Boolean(
|
|
346
|
+
this.db
|
|
347
|
+
.prepare("select 1 from memory_entries where type = ? and duplicate_signature = ? limit 1")
|
|
348
|
+
.get(type, duplicateSignature)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
insertUnchecked(record, options = {}) {
|
|
353
|
+
const entryId = options.entryId || uniqueId(record.type || "entry");
|
|
354
|
+
const entrySignature = options.entrySignature || buildResultSignature(record);
|
|
355
|
+
const duplicateSignature = options.duplicateSignature || null;
|
|
356
|
+
const row = toRow(record);
|
|
357
|
+
const insertEntry = this.db.prepare(`
|
|
358
|
+
insert into memory_entries (
|
|
359
|
+
entry_id, entry_signature, type, category, title, content, summary, reason, timestamp, salience, access_count, expires_at, source_kind, dedupe_key, duplicate_signature, simhash
|
|
360
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
361
|
+
`);
|
|
362
|
+
const insertFts = this.db.prepare(`
|
|
363
|
+
insert into memory_entries_fts(rowid, title, content, summary, reason, category) values (?, ?, ?, ?, ?, ?)
|
|
364
|
+
`);
|
|
365
|
+
|
|
366
|
+
const result = insertEntry.run(
|
|
367
|
+
entryId,
|
|
368
|
+
entrySignature,
|
|
369
|
+
row.type,
|
|
370
|
+
row.category,
|
|
371
|
+
row.title,
|
|
372
|
+
row.content,
|
|
373
|
+
row.summary,
|
|
374
|
+
row.reason,
|
|
375
|
+
row.timestamp,
|
|
376
|
+
row.salience,
|
|
377
|
+
row.accessCount,
|
|
378
|
+
row.expiresAt,
|
|
379
|
+
row.sourceKind,
|
|
380
|
+
row.dedupeKey,
|
|
381
|
+
duplicateSignature,
|
|
382
|
+
row.simhash
|
|
383
|
+
);
|
|
384
|
+
insertFts.run(result.lastInsertRowid, row.title, row.content, row.summary, row.reason, row.category);
|
|
385
|
+
this.markEmbeddingStateUnchecked(entryId, entrySignature, {
|
|
386
|
+
status: "missing",
|
|
387
|
+
preserveVector: false
|
|
388
|
+
});
|
|
389
|
+
return entryId;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
insert(record, options = {}) {
|
|
393
|
+
return this.runTransaction(() => this.insertUnchecked(record, options));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
upsertScratchpad(scratchpadId, record) {
|
|
397
|
+
const entryId = `scratchpad:${scratchpadId}`;
|
|
398
|
+
const existing = this.db.prepare("select rowid from memory_entries where entry_id = ?").get(entryId);
|
|
399
|
+
const row = toRow(record);
|
|
400
|
+
const entrySignature = buildResultSignature(record);
|
|
401
|
+
|
|
402
|
+
this.runTransaction(() => {
|
|
403
|
+
if (existing?.rowid) {
|
|
404
|
+
const previous = this.db.prepare("select entry_signature from memory_entries where rowid = ?").get(existing.rowid);
|
|
405
|
+
this.db.prepare("delete from memory_entries_fts where rowid = ?").run(existing.rowid);
|
|
406
|
+
this.db.prepare(`
|
|
407
|
+
update memory_entries
|
|
408
|
+
set entry_signature = ?, type = ?, category = ?, title = ?, content = ?, summary = ?, reason = ?, timestamp = ?, salience = ?, access_count = ?, expires_at = ?, source_kind = ?, dedupe_key = ?, duplicate_signature = null
|
|
409
|
+
where rowid = ?
|
|
410
|
+
`).run(
|
|
411
|
+
entrySignature,
|
|
412
|
+
row.type,
|
|
413
|
+
row.category,
|
|
414
|
+
row.title,
|
|
415
|
+
row.content,
|
|
416
|
+
row.summary,
|
|
417
|
+
row.reason,
|
|
418
|
+
row.timestamp,
|
|
419
|
+
row.salience,
|
|
420
|
+
row.accessCount,
|
|
421
|
+
row.expiresAt,
|
|
422
|
+
row.sourceKind,
|
|
423
|
+
row.dedupeKey,
|
|
424
|
+
existing.rowid
|
|
425
|
+
);
|
|
426
|
+
this.db.prepare(`
|
|
427
|
+
insert into memory_entries_fts(rowid, title, content, summary, reason, category) values (?, ?, ?, ?, ?, ?)
|
|
428
|
+
`).run(existing.rowid, row.title, row.content, row.summary, row.reason, row.category);
|
|
429
|
+
this.markEmbeddingStateUnchecked(entryId, entrySignature, {
|
|
430
|
+
status: previous?.entry_signature === entrySignature ? "fresh" : "stale",
|
|
431
|
+
preserveVector: previous?.entry_signature === entrySignature
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
this.insertUnchecked(record, {
|
|
437
|
+
entryId,
|
|
438
|
+
entrySignature
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
return entryId;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
deleteByEntryId(entryId) {
|
|
445
|
+
const existing = this.db.prepare("select rowid from memory_entries where entry_id = ?").get(entryId);
|
|
446
|
+
if (!existing?.rowid) return false;
|
|
447
|
+
this.runTransaction(() => {
|
|
448
|
+
this.db.prepare("delete from memory_entries_fts where rowid = ?").run(existing.rowid);
|
|
449
|
+
this.db.prepare("delete from memory_entries where rowid = ?").run(existing.rowid);
|
|
450
|
+
this.db.prepare("delete from memory_embeddings where entry_id = ?").run(entryId);
|
|
451
|
+
});
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
listAll(nowIso = null) {
|
|
456
|
+
const rows = nowIso
|
|
457
|
+
? this.db
|
|
458
|
+
.prepare("select * from memory_entries where expires_at is null or expires_at >= ? order by rowid asc")
|
|
459
|
+
.all(nowIso)
|
|
460
|
+
: this.db.prepare("select * from memory_entries order by rowid asc").all();
|
|
461
|
+
return rows.map(fromRow);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
listExpiredScratchpads(nowIso) {
|
|
465
|
+
return this.db
|
|
466
|
+
.prepare("select entry_id from memory_entries where type = 'scratchpad' and expires_at is not null and expires_at < ?")
|
|
467
|
+
.all(nowIso)
|
|
468
|
+
.map((row) => row.entry_id);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
queryCandidates(query, limit = 32, nowIso = null) {
|
|
472
|
+
const match = normalizeQuery(query);
|
|
473
|
+
if (!match) {
|
|
474
|
+
const rows = nowIso
|
|
475
|
+
? this.db
|
|
476
|
+
.prepare("select * from memory_entries where expires_at is null or expires_at >= ? order by rowid desc limit ?")
|
|
477
|
+
.all(nowIso, limit)
|
|
478
|
+
: this.db.prepare("select * from memory_entries order by rowid desc limit ?").all(limit);
|
|
479
|
+
return rows.map(fromRow);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const rows = nowIso
|
|
483
|
+
? this.db
|
|
484
|
+
.prepare(`
|
|
485
|
+
select e.* from memory_entries_fts f
|
|
486
|
+
join memory_entries e on e.rowid = f.rowid
|
|
487
|
+
where f.memory_entries_fts match ?
|
|
488
|
+
and (e.expires_at is null or e.expires_at >= ?)
|
|
489
|
+
limit ?
|
|
490
|
+
`)
|
|
491
|
+
.all(match, nowIso, limit)
|
|
492
|
+
: this.db
|
|
493
|
+
.prepare(`
|
|
494
|
+
select e.* from memory_entries_fts f
|
|
495
|
+
join memory_entries e on e.rowid = f.rowid
|
|
496
|
+
where f.memory_entries_fts match ?
|
|
497
|
+
limit ?
|
|
498
|
+
`)
|
|
499
|
+
.all(match, limit);
|
|
500
|
+
|
|
501
|
+
if (rows.length >= limit) {
|
|
502
|
+
return rows.map(fromRow);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const existingIds = new Set(rows.map((row) => row.entry_id));
|
|
506
|
+
const fallback = (nowIso
|
|
507
|
+
? this.db
|
|
508
|
+
.prepare("select * from memory_entries where (expires_at is null or expires_at >= ?) order by rowid desc limit ?")
|
|
509
|
+
.all(nowIso, limit)
|
|
510
|
+
: this.db.prepare("select * from memory_entries order by rowid desc limit ?").all(limit)
|
|
511
|
+
).filter((row) => !existingIds.has(row.entry_id));
|
|
512
|
+
|
|
513
|
+
return [...rows, ...fallback].slice(0, limit).map(fromRow);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
replaceEmbeddings({ items, config = null, updatedAt = null }) {
|
|
517
|
+
const normalizedUpdatedAt = updatedAt || new Date().toISOString();
|
|
518
|
+
const provider = config?.provider || null;
|
|
519
|
+
const model = config?.model || null;
|
|
520
|
+
const dimensions = Number(config?.dimensions ?? 0) || null;
|
|
521
|
+
|
|
522
|
+
this.runTransaction(() => {
|
|
523
|
+
this.db.prepare("delete from memory_embeddings").run();
|
|
524
|
+
this.db.prepare("delete from embedding_meta").run();
|
|
525
|
+
|
|
526
|
+
const insert = this.db.prepare(`
|
|
527
|
+
insert into memory_embeddings (
|
|
528
|
+
entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
|
|
529
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
530
|
+
on conflict(entry_id) do update set
|
|
531
|
+
entry_signature = excluded.entry_signature,
|
|
532
|
+
entry_json = excluded.entry_json,
|
|
533
|
+
vector_json = excluded.vector_json,
|
|
534
|
+
updated_at = excluded.updated_at,
|
|
535
|
+
provider = excluded.provider,
|
|
536
|
+
model = excluded.model,
|
|
537
|
+
dimensions = excluded.dimensions,
|
|
538
|
+
freshness_status = excluded.freshness_status,
|
|
539
|
+
embedded_signature = excluded.embedded_signature
|
|
540
|
+
`);
|
|
541
|
+
const upsertMeta = this.db.prepare(`
|
|
542
|
+
insert into embedding_meta(key, value) values (?, ?)
|
|
543
|
+
on conflict(key) do update set value = excluded.value
|
|
544
|
+
`);
|
|
545
|
+
|
|
546
|
+
const seenEntryIds = new Set();
|
|
547
|
+
for (const item of items || []) {
|
|
548
|
+
const entryId = item.entryId || item.item?.entryId;
|
|
549
|
+
if (!entryId) continue;
|
|
550
|
+
seenEntryIds.add(entryId);
|
|
551
|
+
const entrySignature = item.signature || item.item?.entrySignature || buildResultSignature(item.item || {});
|
|
552
|
+
insert.run(
|
|
553
|
+
entryId,
|
|
554
|
+
entrySignature,
|
|
555
|
+
JSON.stringify(item.item || {}),
|
|
556
|
+
JSON.stringify(item.vector || []),
|
|
557
|
+
normalizedUpdatedAt,
|
|
558
|
+
provider,
|
|
559
|
+
model,
|
|
560
|
+
dimensions,
|
|
561
|
+
"fresh",
|
|
562
|
+
entrySignature
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const existingRows = this.db.prepare("select entry_id from memory_entries").all();
|
|
567
|
+
for (const row of existingRows) {
|
|
568
|
+
if (seenEntryIds.has(row.entry_id)) continue;
|
|
569
|
+
const entry = this.db.prepare("select entry_signature from memory_entries where entry_id = ?").get(row.entry_id);
|
|
570
|
+
this.markEmbeddingStateUnchecked(row.entry_id, entry?.entry_signature || null, {
|
|
571
|
+
status: "missing",
|
|
572
|
+
preserveVector: false
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
upsertMeta.run("updatedAt", normalizedUpdatedAt);
|
|
577
|
+
upsertMeta.run("config", JSON.stringify(config || null));
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
updatedAt: normalizedUpdatedAt,
|
|
582
|
+
config,
|
|
583
|
+
items
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
getEmbeddingIndex() {
|
|
588
|
+
const rows = this.db
|
|
589
|
+
.prepare(`
|
|
590
|
+
select entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
|
|
591
|
+
from memory_embeddings
|
|
592
|
+
order by entry_id asc
|
|
593
|
+
`)
|
|
594
|
+
.all();
|
|
595
|
+
const updatedAt = this.db.prepare("select value from embedding_meta where key = 'updatedAt'").get()?.value || null;
|
|
596
|
+
const configRaw = this.db.prepare("select value from embedding_meta where key = 'config'").get()?.value || "null";
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
updatedAt,
|
|
600
|
+
config: JSON.parse(configRaw),
|
|
601
|
+
items: rows.map((row) => ({
|
|
602
|
+
entryId: row.entry_id,
|
|
603
|
+
signature: row.entry_signature,
|
|
604
|
+
item: JSON.parse(row.entry_json),
|
|
605
|
+
vector: JSON.parse(row.vector_json),
|
|
606
|
+
updatedAt: row.updated_at,
|
|
607
|
+
provider: row.provider,
|
|
608
|
+
model: row.model,
|
|
609
|
+
dimensions: row.dimensions,
|
|
610
|
+
freshnessStatus: row.freshness_status,
|
|
611
|
+
embeddedSignature: row.embedded_signature
|
|
612
|
+
}))
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
getEmbeddingHealth() {
|
|
617
|
+
const freshnessRows = this.db
|
|
618
|
+
.prepare(`
|
|
619
|
+
select freshness_status, count(*) as count
|
|
620
|
+
from memory_embeddings
|
|
621
|
+
group by freshness_status
|
|
622
|
+
`)
|
|
623
|
+
.all();
|
|
624
|
+
const freshness = Object.fromEntries(
|
|
625
|
+
freshnessRows.map((row) => [row.freshness_status, Number(row.count || 0)])
|
|
626
|
+
);
|
|
627
|
+
const lastError = this.getRuntimeMeta("embedding_last_error");
|
|
628
|
+
const lastErrorAt = this.getRuntimeMeta("embedding_last_error_at");
|
|
629
|
+
const lastProvider = this.getRuntimeMeta("embedding_last_provider");
|
|
630
|
+
const lastModel = this.getRuntimeMeta("embedding_last_model");
|
|
631
|
+
const lastSuccessAt = this.getRuntimeMeta("embedding_last_success_at");
|
|
632
|
+
const lastMode = this.getRuntimeMeta("embedding_last_query_mode");
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
totalTrackedCount: freshnessRows.reduce((sum, row) => sum + Number(row.count || 0), 0),
|
|
636
|
+
freshCount: freshness.fresh || 0,
|
|
637
|
+
staleCount: freshness.stale || 0,
|
|
638
|
+
missingCount: freshness.missing || 0,
|
|
639
|
+
failedCount: freshness.failed || 0,
|
|
640
|
+
lastError,
|
|
641
|
+
lastErrorAt,
|
|
642
|
+
lastProvider,
|
|
643
|
+
lastModel,
|
|
644
|
+
lastSuccessAt,
|
|
645
|
+
lastQueryMode: lastMode,
|
|
646
|
+
degraded: Boolean(lastError) || (freshness.fresh || 0) === 0
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
queryEmbeddingSimilarities(queryVector, limit = 8, options = {}) {
|
|
651
|
+
const nowIso = options.nowIso || null;
|
|
652
|
+
const candidateEntryIds = options.candidateEntryIds || null;
|
|
653
|
+
const params = [];
|
|
654
|
+
let sql = `
|
|
655
|
+
select e.entry_id, e.entry_signature, e.type, e.category, e.title, e.content, e.summary, e.reason, e.timestamp, e.salience, e.access_count, e.expires_at, e.source_kind, e.dedupe_key,
|
|
656
|
+
m.vector_json, m.freshness_status, m.updated_at, m.provider, m.model, m.dimensions
|
|
657
|
+
from memory_embeddings m
|
|
658
|
+
join memory_entries e on e.entry_id = m.entry_id
|
|
659
|
+
where m.freshness_status = 'fresh'
|
|
660
|
+
`;
|
|
661
|
+
if (nowIso) {
|
|
662
|
+
sql += " and (e.expires_at is null or e.expires_at >= ?)";
|
|
663
|
+
params.push(nowIso);
|
|
664
|
+
}
|
|
665
|
+
if (candidateEntryIds?.length) {
|
|
666
|
+
sql += ` and e.entry_id in (${candidateEntryIds.map(() => "?").join(", ")})`;
|
|
667
|
+
params.push(...candidateEntryIds);
|
|
668
|
+
}
|
|
669
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
670
|
+
|
|
671
|
+
return rows
|
|
672
|
+
.map((row) => ({
|
|
673
|
+
entryId: row.entry_id,
|
|
674
|
+
signature: row.entry_signature,
|
|
675
|
+
similarity: cosineSimilarity(queryVector, JSON.parse(row.vector_json)),
|
|
676
|
+
freshnessStatus: row.freshness_status,
|
|
677
|
+
item: fromRow(row)
|
|
678
|
+
}))
|
|
679
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
680
|
+
.slice(0, limit);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
getEmbeddingState(entryId) {
|
|
684
|
+
return this.db
|
|
685
|
+
.prepare(`
|
|
686
|
+
select entry_id, entry_signature, updated_at, provider, model, dimensions, freshness_status, embedded_signature
|
|
687
|
+
from memory_embeddings where entry_id = ?
|
|
688
|
+
`)
|
|
689
|
+
.get(entryId) || null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
queryRetrievalCandidates(query, options = {}) {
|
|
693
|
+
const nowIso = options.nowIso || null;
|
|
694
|
+
const limit = Math.max(Number(options.limit ?? 8), 1);
|
|
695
|
+
const candidateMultiplier = Math.max(Number(options.candidateMultiplier ?? 6), 2);
|
|
696
|
+
const lexicalLimit = Math.max(limit * candidateMultiplier, 24);
|
|
697
|
+
const queryVector = Array.isArray(options.queryVector) ? options.queryVector : null;
|
|
698
|
+
|
|
699
|
+
const lexicalRows = this.queryCandidates(query, lexicalLimit, nowIso);
|
|
700
|
+
const lexicalMap = new Map(lexicalRows.map((item, index) => [item.entryId, { item, lexicalRank: index }]));
|
|
701
|
+
const semanticRows = queryVector
|
|
702
|
+
? this.queryEmbeddingSimilarities(queryVector, lexicalLimit, {
|
|
703
|
+
nowIso,
|
|
704
|
+
candidateEntryIds: lexicalRows.map((item) => item.entryId)
|
|
705
|
+
})
|
|
706
|
+
: [];
|
|
707
|
+
const semanticMap = new Map(semanticRows.map((row, index) => [row.entryId, { ...row, semanticRank: index }]));
|
|
708
|
+
|
|
709
|
+
const candidateEntryIds = new Set([...lexicalMap.keys(), ...semanticMap.keys()]);
|
|
710
|
+
const results = [];
|
|
711
|
+
|
|
712
|
+
for (const entryId of candidateEntryIds) {
|
|
713
|
+
const lexical = lexicalMap.get(entryId) || null;
|
|
714
|
+
const semantic = semanticMap.get(entryId) || null;
|
|
715
|
+
const baseItem = semantic?.item || lexical?.item;
|
|
716
|
+
if (!baseItem) continue;
|
|
717
|
+
const embeddingState = this.getEmbeddingState(entryId);
|
|
718
|
+
results.push({
|
|
719
|
+
item: baseItem,
|
|
720
|
+
lexicalMatch: Boolean(lexical),
|
|
721
|
+
lexicalRank: lexical?.lexicalRank ?? null,
|
|
722
|
+
embeddingSimilarity: semantic?.similarity ?? 0,
|
|
723
|
+
semanticRank: semantic?.semanticRank ?? null,
|
|
724
|
+
embeddingFreshness: embeddingState?.freshness_status || "missing",
|
|
725
|
+
embeddingProvider: embeddingState?.provider || null,
|
|
726
|
+
embeddingModel: embeddingState?.model || null,
|
|
727
|
+
retrievalSources: [
|
|
728
|
+
lexical ? "lexical" : null,
|
|
729
|
+
semantic ? "semantic" : null
|
|
730
|
+
].filter(Boolean)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return results;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
markEmbeddingState(entryId, entrySignature, options = {}) {
|
|
738
|
+
return this.runTransaction(() => this.markEmbeddingStateUnchecked(entryId, entrySignature, options));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
markEmbeddingStateUnchecked(entryId, entrySignature, options = {}) {
|
|
742
|
+
const existing = this.db.prepare("select * from memory_embeddings where entry_id = ?").get(entryId);
|
|
743
|
+
const status = options.status || "missing";
|
|
744
|
+
const preserveVector = options.preserveVector ?? status === "fresh";
|
|
745
|
+
const updatedAt = options.updatedAt || existing?.updated_at || null;
|
|
746
|
+
const provider = options.provider || existing?.provider || null;
|
|
747
|
+
const model = options.model || existing?.model || null;
|
|
748
|
+
const dimensions = options.dimensions ?? existing?.dimensions ?? null;
|
|
749
|
+
const embeddedSignature =
|
|
750
|
+
status === "fresh"
|
|
751
|
+
? entrySignature
|
|
752
|
+
: options.embeddedSignature ?? (preserveVector ? existing?.embedded_signature : null);
|
|
753
|
+
const entryJson = options.entryJson ?? existing?.entry_json ?? JSON.stringify({});
|
|
754
|
+
const vectorJson = preserveVector ? existing?.vector_json || JSON.stringify([]) : JSON.stringify([]);
|
|
755
|
+
|
|
756
|
+
this.db.prepare(`
|
|
757
|
+
insert into memory_embeddings (
|
|
758
|
+
entry_id, entry_signature, entry_json, vector_json, updated_at, provider, model, dimensions, freshness_status, embedded_signature
|
|
759
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
760
|
+
on conflict(entry_id) do update set
|
|
761
|
+
entry_signature = excluded.entry_signature,
|
|
762
|
+
entry_json = excluded.entry_json,
|
|
763
|
+
vector_json = excluded.vector_json,
|
|
764
|
+
updated_at = excluded.updated_at,
|
|
765
|
+
provider = excluded.provider,
|
|
766
|
+
model = excluded.model,
|
|
767
|
+
dimensions = excluded.dimensions,
|
|
768
|
+
freshness_status = excluded.freshness_status,
|
|
769
|
+
embedded_signature = excluded.embedded_signature
|
|
770
|
+
`).run(
|
|
771
|
+
entryId,
|
|
772
|
+
entrySignature,
|
|
773
|
+
entryJson,
|
|
774
|
+
vectorJson,
|
|
775
|
+
updatedAt,
|
|
776
|
+
provider,
|
|
777
|
+
model,
|
|
778
|
+
dimensions,
|
|
779
|
+
status,
|
|
780
|
+
embeddedSignature
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
setRuntimeMeta(key, value) {
|
|
785
|
+
this.db.prepare(`
|
|
786
|
+
insert into runtime_meta(key, value) values (?, ?)
|
|
787
|
+
on conflict(key) do update set value = excluded.value
|
|
788
|
+
`).run(String(key), value == null ? null : String(value));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
getRuntimeMeta(key) {
|
|
792
|
+
const row = this.db.prepare("select value from runtime_meta where key = ?").get(String(key));
|
|
793
|
+
return row?.value ?? null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
clearRuntimeMeta(key) {
|
|
797
|
+
this.db.prepare("delete from runtime_meta where key = ?").run(String(key));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
tryAcquireLock(lockKey, { ownerId, acquiredAt, expiresAt }) {
|
|
801
|
+
return this.runTransaction(() => {
|
|
802
|
+
const existing = this.db.prepare("select * from memory_locks where lock_key = ?").get(lockKey);
|
|
803
|
+
if (!existing) {
|
|
804
|
+
this.db
|
|
805
|
+
.prepare("insert into memory_locks(lock_key, owner_id, acquired_at, expires_at) values (?, ?, ?, ?)")
|
|
806
|
+
.run(lockKey, ownerId, acquiredAt, expiresAt);
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (existing.owner_id === ownerId || existing.expires_at <= acquiredAt) {
|
|
811
|
+
this.db
|
|
812
|
+
.prepare("update memory_locks set owner_id = ?, acquired_at = ?, expires_at = ? where lock_key = ?")
|
|
813
|
+
.run(ownerId, acquiredAt, expiresAt, lockKey);
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return false;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
releaseLock(lockKey, ownerId) {
|
|
822
|
+
this.db.prepare("delete from memory_locks where lock_key = ? and owner_id = ?").run(lockKey, ownerId);
|
|
823
|
+
}
|
|
824
|
+
}
|