th-memory-mcp 1.2.2 → 2.0.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.
@@ -0,0 +1,125 @@
1
+ import { db, nowISO } from "../db/index.js";
2
+ import { getMemoryById } from "../db/repositories/memories.js";
3
+ import { supersede, reinforce, softDelete } from "../core/lifecycle-engine.js";
4
+ import { embed, cosine, deserialize } from "../lib/embed.js";
5
+ const SIM_DUP = 0.9;
6
+ const SIM_UPDATE = 0.45;
7
+ const SIM_CONTRA = 0.35;
8
+ const RELATED_THRESHOLD = 0.35;
9
+ const NEGATION = [
10
+ /\bnot\b/i,
11
+ /\bnever\b/i,
12
+ /\bno longer\b/i,
13
+ /\bstop\b/i,
14
+ /\bavoid\b/i,
15
+ /\bdon'?t\b/i,
16
+ /\binstead\b/i,
17
+ /\bbut\b/i,
18
+ /\bhowever\b/i,
19
+ /ไม่/i,
20
+ /ห้าม/i,
21
+ /เลิก/i,
22
+ /อย่า/i,
23
+ ];
24
+ export function isContradiction(a, b) {
25
+ const na = NEGATION.some((re) => re.test(a));
26
+ const nb = NEGATION.some((re) => re.test(b));
27
+ if (na === nb)
28
+ return false; // both or neither negated -> not a simple contradiction
29
+ const ta = tokenSet(a);
30
+ const tb = tokenSet(b);
31
+ let overlap = 0;
32
+ for (const t of ta)
33
+ if (tb.has(t))
34
+ overlap++;
35
+ return overlap >= 2;
36
+ }
37
+ function tokenSet(s) {
38
+ return new Set(s.toLowerCase().match(/[a-z0-9ก-์]+/gi) ?? []);
39
+ }
40
+ export function classifyRelationship(candidate, relatedId) {
41
+ const rel = getMemoryById(relatedId);
42
+ if (!rel)
43
+ return "unrelated";
44
+ const relVecRow = db
45
+ .prepare("SELECT vec FROM embeddings WHERE ref_table='memories' AND ref_id=?")
46
+ .get(relatedId);
47
+ if (!relVecRow)
48
+ return "unrelated";
49
+ const score = cosine(embed(candidate.content), deserialize(relVecRow.vec));
50
+ if (score >= SIM_DUP)
51
+ return "duplicate";
52
+ if (isContradiction(candidate.content, rel.content) && score >= SIM_CONTRA) {
53
+ return "contradiction";
54
+ }
55
+ if (score >= SIM_UPDATE)
56
+ return "update";
57
+ return "unrelated";
58
+ }
59
+ // Find best related active/non-archived memory of the same type for a candidate.
60
+ export function findRelated(candidate, threshold = RELATED_THRESHOLD) {
61
+ const vec = embed(candidate.content);
62
+ const rows = db
63
+ .prepare("SELECT ref_id, vec FROM embeddings WHERE ref_table = 'memories'")
64
+ .all();
65
+ let best;
66
+ for (const r of rows) {
67
+ if (candidate.id && r.ref_id === candidate.id)
68
+ continue;
69
+ const m = getMemoryById(r.ref_id);
70
+ if (!m ||
71
+ m.type !== candidate.type ||
72
+ m.status === "deleted" ||
73
+ m.status === "archived")
74
+ continue;
75
+ const score = cosine(vec, deserialize(r.vec));
76
+ if (score >= threshold && (!best || score > best.score)) {
77
+ best = { id: r.ref_id, score };
78
+ }
79
+ }
80
+ return best;
81
+ }
82
+ // Resolve a candidate against existing memories. If candidate.id is set (memory
83
+ // already created), it will be reconciled (superseded / linked / merged).
84
+ export function resolveConflict(candidate) {
85
+ const related = findRelated(candidate);
86
+ if (!related)
87
+ return { relation: "unrelated", score: 0, action: "none" };
88
+ const relation = classifyRelationship(candidate, related.id);
89
+ if (relation === "duplicate" && candidate.id !== undefined) {
90
+ reinforce(related.id);
91
+ softDelete(candidate.id);
92
+ return {
93
+ relation,
94
+ relatedId: related.id,
95
+ score: related.score,
96
+ action: "merged",
97
+ };
98
+ }
99
+ if (relation === "update" && candidate.id !== undefined) {
100
+ supersede(related.id, candidate.id);
101
+ return {
102
+ relation,
103
+ relatedId: related.id,
104
+ score: related.score,
105
+ action: "superseded",
106
+ };
107
+ }
108
+ if (relation === "contradiction" && candidate.id !== undefined) {
109
+ db.prepare(`INSERT INTO memory_links (source_memory_id, relation, target_memory_id, confidence, created_at)
110
+ VALUES (?, 'contradicts', ?, 0.8, ?)
111
+ ON CONFLICT(source_memory_id, relation, target_memory_id) DO UPDATE SET confidence = 0.8`).run(candidate.id, related.id, nowISO());
112
+ return {
113
+ relation,
114
+ relatedId: related.id,
115
+ score: related.score,
116
+ action: "linked_contradiction",
117
+ };
118
+ }
119
+ return {
120
+ relation,
121
+ relatedId: related.id,
122
+ score: related.score,
123
+ action: "none",
124
+ };
125
+ }
@@ -0,0 +1,30 @@
1
+ // Per-type decay rate (lambda) for recency = exp(-lambda * age_days).
2
+ // Lower lambda => slower decay (more durable). Policy classes, not fixed constants.
3
+ export const DECAY_LAMBDA_BY_TYPE = {
4
+ CONSTRAINT: 0.0005,
5
+ DECISION: 0.001,
6
+ LESSON: 0.001,
7
+ PREFERENCE: 0.001,
8
+ FACT: 0.005,
9
+ GOAL: 0.01,
10
+ PROCEDURE: 0.005,
11
+ EPISODE: 0.01,
12
+ RELATION: 0.005,
13
+ PROFILE: 0.0,
14
+ DERIVED: 0.002,
15
+ };
16
+ export function decayLambdaFor(type) {
17
+ return DECAY_LAMBDA_BY_TYPE[type] ?? 0.005;
18
+ }
19
+ export function ageInDays(isoTs, now = new Date()) {
20
+ const t = new Date(isoTs).getTime();
21
+ if (Number.isNaN(t))
22
+ return 0;
23
+ return Math.max(0, (now.getTime() - t) / 86_400_000);
24
+ }
25
+ export function recencyFactor(isoTs, lambda, now = new Date()) {
26
+ return Math.exp(-lambda * ageInDays(isoTs, now));
27
+ }
28
+ export function recencyFactorFor(type, isoTs, now = new Date()) {
29
+ return recencyFactor(isoTs, decayLambdaFor(type), now);
30
+ }
@@ -0,0 +1,51 @@
1
+ import { embed, cosine, deserialize } from "../lib/embed.js";
2
+ import { db } from "../db/index.js";
3
+ export function normalizeText(text) {
4
+ return text
5
+ .toLowerCase()
6
+ .normalize("NFKC")
7
+ .replace(/\s+/g, " ")
8
+ .replace(/[^\p{L}\p{N}\s]/gu, "")
9
+ .trim();
10
+ }
11
+ // Exact / normalized match on content within the same type.
12
+ export function findExactMatch(type, content) {
13
+ const norm = normalizeText(content);
14
+ const rows = db
15
+ .prepare("SELECT id, content FROM memories WHERE type = ? AND status != 'deleted'")
16
+ .all(type);
17
+ for (const r of rows) {
18
+ if (normalizeText(r.content) === norm)
19
+ return r.id;
20
+ }
21
+ return undefined;
22
+ }
23
+ // Semantic similarity against existing memories of the same type.
24
+ export function findSimilar(type, content, threshold = 0.82) {
25
+ const vec = embed(content);
26
+ const rows = db
27
+ .prepare("SELECT ref_id, vec FROM embeddings WHERE ref_table = 'memories'")
28
+ .all();
29
+ let best;
30
+ for (const r of rows) {
31
+ const m = db
32
+ .prepare("SELECT type, status FROM memories WHERE id = ?")
33
+ .get(r.ref_id);
34
+ if (!m || m.type !== type || m.status === "deleted")
35
+ continue;
36
+ const score = cosine(vec, deserialize(r.vec));
37
+ if (score >= threshold && (!best || score > best.score)) {
38
+ best = { id: r.ref_id, score };
39
+ }
40
+ }
41
+ return best;
42
+ }
43
+ export function deduplicate(type, content) {
44
+ const exact = findExactMatch(type, content);
45
+ if (exact !== undefined)
46
+ return { verdict: "duplicate", existingId: exact, score: 1 };
47
+ const sim = findSimilar(type, content);
48
+ if (sim)
49
+ return { verdict: "duplicate", existingId: sim.id, score: sim.score };
50
+ return { verdict: "distinct", score: 0 };
51
+ }
@@ -0,0 +1,44 @@
1
+ import { sourceWeight } from "./source-weights.js";
2
+ import { recencyFactorFor } from "./decay.js";
3
+ export const DEFAULT_SALIENCE_WEIGHTS = {
4
+ semanticRelevance: 0.3,
5
+ importance: 0.2,
6
+ confidence: 0.15,
7
+ recency: 0.15,
8
+ accessFrequency: 0.1,
9
+ projectRelevance: 0.1,
10
+ };
11
+ export function clamp01(x) {
12
+ if (Number.isNaN(x))
13
+ return 0;
14
+ return Math.min(1, Math.max(0, x));
15
+ }
16
+ export function computeSalience(input) {
17
+ const w = { ...DEFAULT_SALIENCE_WEIGHTS, ...(input.weights ?? {}) };
18
+ const raw = w.semanticRelevance * clamp01(input.relevance) +
19
+ w.importance * clamp01(input.importance) +
20
+ w.confidence * clamp01(input.confidence) +
21
+ w.recency * clamp01(input.recency) +
22
+ w.accessFrequency * clamp01(input.accessFrequency) +
23
+ w.projectRelevance * clamp01(input.projectRelevance);
24
+ return clamp01(raw);
25
+ }
26
+ export function computeConfidence(input) {
27
+ const w = sourceWeight(input.source);
28
+ const n = Math.max(0, input.confirmations);
29
+ const next = w + (1 - w) * (1 - 1 / (1 + n));
30
+ return clamp01(next);
31
+ }
32
+ // Convenience: build salience from a memory record + query relevance.
33
+ export function salienceForMemory(params) {
34
+ const recency = recencyFactorFor(params.type, params.updatedAt);
35
+ const accessFrequency = clamp01(params.accessCount / (params.maxAccess ?? 10));
36
+ return computeSalience({
37
+ relevance: params.relevance,
38
+ importance: params.importance,
39
+ confidence: params.confidence,
40
+ recency,
41
+ accessFrequency,
42
+ projectRelevance: params.projectRelevance ?? 0.5,
43
+ });
44
+ }
@@ -0,0 +1,13 @@
1
+ // Recommended source weights for confidence (spec §8). Defaults, not immutable constants.
2
+ export const SOURCE_WEIGHTS = {
3
+ explicit: 1.0,
4
+ corrected: 0.95,
5
+ system: 0.8,
6
+ consolidated: 0.75,
7
+ imported: 0.7,
8
+ inferred: 0.5,
9
+ captured: 0.3,
10
+ };
11
+ export function sourceWeight(source) {
12
+ return SOURCE_WEIGHTS[source] ?? 0.5;
13
+ }
@@ -0,0 +1,41 @@
1
+ export const MEMORY_TYPES = [
2
+ "FACT",
3
+ "PREFERENCE",
4
+ "GOAL",
5
+ "DECISION",
6
+ "CONSTRAINT",
7
+ "LESSON",
8
+ "PROCEDURE",
9
+ "EPISODE",
10
+ "RELATION",
11
+ "PROFILE",
12
+ "DERIVED",
13
+ ];
14
+ export const SOURCE_TYPES = [
15
+ "explicit",
16
+ "corrected",
17
+ "inferred",
18
+ "captured",
19
+ "consolidated",
20
+ "imported",
21
+ "system",
22
+ ];
23
+ export const LIFECYCLE_STATES = [
24
+ "new",
25
+ "active",
26
+ "reinforced",
27
+ "stale",
28
+ "superseded",
29
+ "archived",
30
+ "deleted",
31
+ ];
32
+ export const SCOPES = ["GLOBAL", "USER", "PROJECT", "SESSION"];
33
+ export const LINK_RELATIONS = [
34
+ "supports",
35
+ "contradicts",
36
+ "supersedes",
37
+ "derived_from",
38
+ "related_to",
39
+ "caused_by",
40
+ "depends_on",
41
+ ];
@@ -0,0 +1,22 @@
1
+ import { db, buildFtsMatch } from "../db/index.js";
2
+ export function ftsSearch(query, opts = {}) {
3
+ const limit = Math.min(Math.max(opts.limit ?? 50, 1), 200);
4
+ const statusClause = opts.includeArchived
5
+ ? ""
6
+ : "AND m.status NOT IN ('deleted','archived')";
7
+ const projectClause = opts.projectId
8
+ ? "AND (m.project_id = @projectId OR m.project_id IS NULL)"
9
+ : "";
10
+ const rows = db
11
+ .prepare(`SELECT m.id FROM memories m
12
+ JOIN search_index ON search_index.ref_table = 'memories' AND search_index.ref_id = m.id
13
+ WHERE search_index MATCH @match ${statusClause} ${projectClause}
14
+ ORDER BY rank
15
+ LIMIT @limit`)
16
+ .all({
17
+ match: buildFtsMatch(query),
18
+ limit,
19
+ projectId: opts.projectId,
20
+ });
21
+ return rows.map((r, i) => ({ id: r.id, rank: i + 1 }));
22
+ }
@@ -0,0 +1,11 @@
1
+ // Reciprocal Rank Fusion (spec §13.1): RRF(m) = Σ 1 / (k + rank_i(m))
2
+ export function rrfFuse(lists, k = 60) {
3
+ const scores = new Map();
4
+ for (const list of lists) {
5
+ for (const item of list) {
6
+ const rrf = 1 / (k + item.rank);
7
+ scores.set(item.id, (scores.get(item.id) ?? 0) + rrf);
8
+ }
9
+ }
10
+ return scores;
11
+ }
@@ -0,0 +1,19 @@
1
+ import { clamp01 } from "../memory/scorer.js";
2
+ // spec §13.1: final_score = RRF * confidence * importance_factor * recency_factor * scope_factor
3
+ export function finalScore(input) {
4
+ const importanceFactor = 0.5 + 0.5 * clamp01(input.importance); // 0.5..1.0
5
+ return (input.rrf *
6
+ clamp01(input.confidence) *
7
+ importanceFactor *
8
+ clamp01(input.recency) *
9
+ clamp01(input.scopeFactor));
10
+ }
11
+ export function scopeFactorFor(mem, projectId) {
12
+ if (!projectId)
13
+ return 0.8; // global query: project-scoped memories slightly favored
14
+ if (mem.project_id === projectId)
15
+ return 1.0;
16
+ if (mem.project_id === null)
17
+ return 0.7;
18
+ return 0.3; // different project
19
+ }
@@ -0,0 +1,29 @@
1
+ import { db } from "../db/index.js";
2
+ import { embed, cosine, deserialize } from "../lib/embed.js";
3
+ export function vectorSearch(query, opts = {}) {
4
+ const limit = Math.min(Math.max(opts.limit ?? 50, 1), 200);
5
+ const floor = opts.floor ?? 0.15;
6
+ const topicVec = embed(query);
7
+ const rows = db
8
+ .prepare("SELECT ref_id, vec FROM embeddings WHERE ref_table = 'memories'")
9
+ .all();
10
+ const statusOk = (id) => {
11
+ const m = db
12
+ .prepare("SELECT status, project_id FROM memories WHERE id = ?")
13
+ .get(id);
14
+ if (!m)
15
+ return false;
16
+ if (!opts.includeArchived &&
17
+ (m.status === "deleted" || m.status === "archived"))
18
+ return false;
19
+ if (opts.projectId &&
20
+ !(m.project_id === opts.projectId || m.project_id === null))
21
+ return false;
22
+ return true;
23
+ };
24
+ return rows
25
+ .map((r) => ({ id: r.ref_id, score: cosine(topicVec, deserialize(r.vec)) }))
26
+ .filter((r) => r.score >= floor && statusOk(r.id))
27
+ .sort((a, b) => b.score - a.score)
28
+ .slice(0, limit);
29
+ }
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import { clusterMemories, createDerivedMemory, } from "../core/consolidation-engine.js";
3
+ import { db, ok } from "../db/index.js";
4
+ export const consolidateInput = {
5
+ threshold: z
6
+ .number()
7
+ .min(0)
8
+ .max(1)
9
+ .optional()
10
+ .describe("Cosine similarity threshold for clustering (default 0.7)"),
11
+ projectId: z.string().nullable().optional().describe("Scope to a project"),
12
+ minClusterSize: z
13
+ .number()
14
+ .int()
15
+ .min(2)
16
+ .max(20)
17
+ .optional()
18
+ .describe("Minimum members to report a cluster (default 2)"),
19
+ derive: z
20
+ .boolean()
21
+ .optional()
22
+ .describe("Create a derived memory for each cluster"),
23
+ };
24
+ export function consolidateHandler(args) {
25
+ const clusters = clusterMemories({
26
+ threshold: typeof args.threshold === "number" ? args.threshold : undefined,
27
+ projectId: typeof args.projectId === "string" ? args.projectId : null,
28
+ minClusterSize: typeof args.minClusterSize === "number"
29
+ ? args.minClusterSize
30
+ : undefined,
31
+ });
32
+ const lines = [];
33
+ const derivedIds = [];
34
+ for (const c of clusters) {
35
+ const contents = c.map((id) => {
36
+ const m = db
37
+ .prepare("SELECT content FROM memories WHERE id = ?")
38
+ .get(id);
39
+ return ` - [${id}] ${m?.content ?? "?"}`;
40
+ });
41
+ lines.push(`Cluster (${c.length}):\n${contents.join("\n")}`);
42
+ if (args.derive === true) {
43
+ const summary = c
44
+ .map((id) => {
45
+ const m = db
46
+ .prepare("SELECT content FROM memories WHERE id = ?")
47
+ .get(id);
48
+ return m?.content ?? "";
49
+ })
50
+ .join(" | ");
51
+ const did = createDerivedMemory({
52
+ content: `Consolidated: ${summary}`,
53
+ sourceIds: c,
54
+ });
55
+ derivedIds.push(did);
56
+ lines.push(` => derived memory id=${did}`);
57
+ }
58
+ }
59
+ const header = `Found ${clusters.length} cluster(s)${args.derive === true
60
+ ? `, created ${derivedIds.length} derived memories`
61
+ : ""}`;
62
+ return ok(header + "\n\n" + (lines.join("\n\n") || "(no clusters)"));
63
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ import { getContext } from "../core/context-engine.js";
3
+ import { ok } from "../db/index.js";
4
+ export const contextInput = {
5
+ query: z
6
+ .string()
7
+ .optional()
8
+ .describe("Optional focus query to seed hybrid retrieval"),
9
+ projectId: z
10
+ .string()
11
+ .nullable()
12
+ .optional()
13
+ .describe("Scope context to a project"),
14
+ sessionId: z.string().nullable().optional(),
15
+ limit: z
16
+ .number()
17
+ .int()
18
+ .min(1)
19
+ .max(50)
20
+ .optional()
21
+ .describe("Number of seed memories (default 10)"),
22
+ maxTokens: z
23
+ .number()
24
+ .int()
25
+ .min(100)
26
+ .max(8000)
27
+ .optional()
28
+ .describe("Token budget for assembled context (default 2000)"),
29
+ includeHistory: z
30
+ .boolean()
31
+ .optional()
32
+ .describe("Include superseded/archived memories"),
33
+ includeGraph: z
34
+ .boolean()
35
+ .optional()
36
+ .describe("Expand seeds with memory-graph neighbors"),
37
+ };
38
+ export function contextHandler(args) {
39
+ const res = getContext({
40
+ query: typeof args.query === "string" ? args.query : "",
41
+ projectId: typeof args.projectId === "string" ? args.projectId : null,
42
+ sessionId: typeof args.sessionId === "string" ? args.sessionId : null,
43
+ limit: typeof args.limit === "number" ? args.limit : undefined,
44
+ maxTokens: typeof args.maxTokens === "number" ? args.maxTokens : undefined,
45
+ includeHistory: args.includeHistory === true,
46
+ includeGraph: args.includeGraph === true,
47
+ });
48
+ const lines = res.memories.map((m) => {
49
+ const meta = m.metadata ? ` meta=${m.metadata}` : "";
50
+ const tag = m.viaGraph ? " [graph]" : "";
51
+ return `[${m.id}] (${m.type}/${m.status})${tag} ${m.content}${meta}`;
52
+ });
53
+ const header = `Context for "${res.query || "<no query>"}" — ${res.memories.length} memories, ~${res.tokenEstimate} tokens${res.truncated ? " (truncated to budget)" : ""}`;
54
+ return ok(header + "\n\n" + (lines.join("\n") || "(no memories)"));
55
+ }
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, writeFileSync, statSync } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
3
  import { z } from "zod";
4
- import { db, DB_PATH, nowISO, truncate, ok, err, } from "../db.js";
4
+ import { db, DB_PATH, nowISO, truncate, ok, err, } from "../db/index.js";
5
5
  import { VERSION, EXPORTS_DIRNAME } from "../lib/config.js";
6
6
  export const exportMemoryInput = {
7
7
  includeInteractions: z
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { db, removeSearchIndex, removeEmbedding, ok, err, } from "../db.js";
2
+ import { db, removeSearchIndex, removeEmbedding, ok, err, } from "../db/index.js";
3
3
  export const forgetInput = {
4
4
  target_id: z
5
5
  .number()
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { db, escapeLike, truncate, ok, err, } from "../db.js";
2
+ import { db, escapeLike, truncate, ok, err, } from "../db/index.js";
3
3
  export const searchHistoryInput = {
4
4
  query: z.string().min(1).max(500).describe("Text to search in past prompts"),
5
5
  limit: z
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { db, nowISO, syncSearchIndex, upsertEmbedding, ok, err, } from "../db.js";
2
+ import { db, nowISO, syncSearchIndex, upsertEmbedding, ok, err, } from "../db/index.js";
3
3
  import { embed } from "../lib/embed.js";
4
4
  export const saveLessonInput = {
5
5
  situation: z.string().min(1).max(1000).describe("The original situation/context"),
@@ -1,5 +1,5 @@
1
1
  import { statSync } from "node:fs";
2
- import { db, DB_PATH, truncate, ok, err, } from "../db.js";
2
+ import { db, DB_PATH, truncate, ok, err, } from "../db/index.js";
3
3
  import { CAPTURE_KINDS } from "../lib/capture-core.js";
4
4
  export const STATS_BUDGET = 1500;
5
5
  const kindCounts = db.prepare("SELECT kind, COUNT(*) AS n FROM interactions GROUP BY kind");
@@ -1,4 +1,4 @@
1
- import { db, truncate, ok, err, } from "../db.js";
1
+ import { db, truncate, ok, err, } from "../db/index.js";
2
2
  export const PROFILE_BUDGET = 3000;
3
3
  const PROFILE_SECTION_MAX = 400;
4
4
  const PREF_CATEGORY_MAX = 30;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { db, buildFtsMatch, escapeLike, truncate, getAllEmbeddings, ok, err, } from "../db.js";
2
+ import { db, buildFtsMatch, escapeLike, truncate, getAllEmbeddings, ok, err, } from "../db/index.js";
3
3
  import { embed, cosine, deserialize } from "../lib/embed.js";
4
4
  export const recallInput = {
5
5
  topic: z.string().min(1).max(500).describe("Topic to recall from memory"),
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { db, truncate, ok, err } from "../db.js";
2
+ import { db, truncate, ok, err } from "../db/index.js";
3
3
  import { CAPTURE_KINDS, } from "../lib/capture-core.js";
4
4
  export const recentInteractionsInput = {
5
5
  limit: z
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { db, nowISO, syncSearchIndex, upsertEmbedding, ok, err, } from "../db.js";
2
+ import { db, nowISO, syncSearchIndex, upsertEmbedding, ok, err, } from "../db/index.js";
3
3
  import { embed } from "../lib/embed.js";
4
4
  export const rememberInput = {
5
5
  category: z