@kernl-sdk/libsql 0.1.36 → 0.1.39
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/.turbo/turbo-build.log +5 -4
- package/CHANGELOG.md +20 -0
- package/README.md +225 -0
- package/dist/__tests__/constraints.test.d.ts +2 -0
- package/dist/__tests__/constraints.test.d.ts.map +1 -0
- package/dist/__tests__/constraints.test.js +97 -0
- package/dist/__tests__/helpers.d.ts +36 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/helpers.js +80 -0
- package/dist/__tests__/memory.create-get.test.d.ts +2 -0
- package/dist/__tests__/memory.create-get.test.d.ts.map +1 -0
- package/dist/__tests__/memory.create-get.test.js +8 -0
- package/dist/__tests__/memory.delete.test.d.ts +2 -0
- package/dist/__tests__/memory.delete.test.d.ts.map +1 -0
- package/dist/__tests__/memory.delete.test.js +6 -0
- package/dist/__tests__/memory.list.test.d.ts +2 -0
- package/dist/__tests__/memory.list.test.d.ts.map +1 -0
- package/dist/__tests__/memory.list.test.js +8 -0
- package/dist/__tests__/memory.update.test.d.ts +2 -0
- package/dist/__tests__/memory.update.test.d.ts.map +1 -0
- package/dist/__tests__/memory.update.test.js +8 -0
- package/dist/__tests__/migrations.test.d.ts +2 -0
- package/dist/__tests__/migrations.test.d.ts.map +1 -0
- package/dist/__tests__/migrations.test.js +68 -0
- package/dist/__tests__/row-codecs.test.d.ts +2 -0
- package/dist/__tests__/row-codecs.test.d.ts.map +1 -0
- package/dist/__tests__/row-codecs.test.js +175 -0
- package/dist/__tests__/sql-utils.test.d.ts +2 -0
- package/dist/__tests__/sql-utils.test.d.ts.map +1 -0
- package/dist/__tests__/sql-utils.test.js +45 -0
- package/dist/__tests__/storage.init.test.d.ts +2 -0
- package/dist/__tests__/storage.init.test.d.ts.map +1 -0
- package/dist/__tests__/storage.init.test.js +63 -0
- package/dist/__tests__/thread.lifecycle.test.d.ts +2 -0
- package/dist/__tests__/thread.lifecycle.test.d.ts.map +1 -0
- package/dist/__tests__/thread.lifecycle.test.js +172 -0
- package/dist/__tests__/transaction.test.d.ts +2 -0
- package/dist/__tests__/transaction.test.d.ts.map +1 -0
- package/dist/__tests__/transaction.test.js +16 -0
- package/dist/__tests__/utils.test.d.ts +2 -0
- package/dist/__tests__/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils.test.js +31 -0
- package/dist/client.d.ts +46 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +46 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/memory/__tests__/create-get.test.d.ts +2 -0
- package/dist/memory/__tests__/create-get.test.d.ts.map +1 -0
- package/dist/memory/__tests__/create-get.test.js +126 -0
- package/dist/memory/__tests__/delete.test.d.ts +2 -0
- package/dist/memory/__tests__/delete.test.d.ts.map +1 -0
- package/dist/memory/__tests__/delete.test.js +96 -0
- package/dist/memory/__tests__/list.test.d.ts +2 -0
- package/dist/memory/__tests__/list.test.d.ts.map +1 -0
- package/dist/memory/__tests__/list.test.js +168 -0
- package/dist/memory/__tests__/sql.test.d.ts +2 -0
- package/dist/memory/__tests__/sql.test.d.ts.map +1 -0
- package/dist/memory/__tests__/sql.test.js +159 -0
- package/dist/memory/__tests__/update.test.d.ts +2 -0
- package/dist/memory/__tests__/update.test.d.ts.map +1 -0
- package/dist/memory/__tests__/update.test.js +113 -0
- package/dist/memory/row.d.ts +11 -0
- package/dist/memory/row.d.ts.map +1 -0
- package/dist/memory/row.js +29 -0
- package/dist/memory/sql.d.ts +34 -0
- package/dist/memory/sql.d.ts.map +1 -0
- package/dist/memory/sql.js +109 -0
- package/dist/memory/store.d.ts +41 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +132 -0
- package/dist/migrations.d.ts +32 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +157 -0
- package/dist/sql.d.ts +28 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +22 -0
- package/dist/storage.d.ts +75 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +123 -0
- package/dist/thread/__tests__/append.test.d.ts +2 -0
- package/dist/thread/__tests__/append.test.d.ts.map +1 -0
- package/dist/thread/__tests__/append.test.js +141 -0
- package/dist/thread/__tests__/history.test.d.ts +2 -0
- package/dist/thread/__tests__/history.test.d.ts.map +1 -0
- package/dist/thread/__tests__/history.test.js +146 -0
- package/dist/thread/__tests__/sql.test.d.ts +2 -0
- package/dist/thread/__tests__/sql.test.d.ts.map +1 -0
- package/dist/thread/__tests__/sql.test.js +129 -0
- package/dist/thread/__tests__/store.test.d.ts +2 -0
- package/dist/thread/__tests__/store.test.d.ts.map +1 -0
- package/dist/thread/__tests__/store.test.js +170 -0
- package/dist/thread/row.d.ts +19 -0
- package/dist/thread/row.d.ts.map +1 -0
- package/dist/thread/row.js +65 -0
- package/dist/thread/sql.d.ts +33 -0
- package/dist/thread/sql.d.ts.map +1 -0
- package/dist/thread/sql.js +112 -0
- package/dist/thread/store.d.ts +67 -0
- package/dist/thread/store.d.ts.map +1 -0
- package/dist/thread/store.js +282 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +21 -0
- package/package.json +15 -11
- package/src/__tests__/constraints.test.ts +123 -0
- package/src/__tests__/helpers.ts +98 -0
- package/src/__tests__/migrations.test.ts +114 -0
- package/src/__tests__/row-codecs.test.ts +201 -0
- package/src/__tests__/sql-utils.test.ts +52 -0
- package/src/__tests__/storage.init.test.ts +92 -0
- package/src/__tests__/thread.lifecycle.test.ts +234 -0
- package/src/__tests__/transaction.test.ts +25 -0
- package/src/__tests__/utils.test.ts +38 -0
- package/src/client.ts +71 -0
- package/src/index.ts +10 -0
- package/src/memory/__tests__/create-get.test.ts +161 -0
- package/src/memory/__tests__/delete.test.ts +124 -0
- package/src/memory/__tests__/list.test.ts +198 -0
- package/src/memory/__tests__/sql.test.ts +186 -0
- package/src/memory/__tests__/update.test.ts +148 -0
- package/src/memory/row.ts +36 -0
- package/src/memory/sql.ts +142 -0
- package/src/memory/store.ts +173 -0
- package/src/migrations.ts +206 -0
- package/src/sql.ts +35 -0
- package/src/storage.ts +170 -0
- package/src/thread/__tests__/append.test.ts +201 -0
- package/src/thread/__tests__/history.test.ts +198 -0
- package/src/thread/__tests__/sql.test.ts +154 -0
- package/src/thread/__tests__/store.test.ts +219 -0
- package/src/thread/row.ts +77 -0
- package/src/thread/sql.ts +153 -0
- package/src/thread/store.ts +381 -0
- package/src/utils.ts +20 -0
- package/LICENSE +0 -201
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { SQL_WHERE, ORDER, SQL_UPDATE } from "../sql";
|
|
4
|
+
|
|
5
|
+
describe("LibSQL memory SQL codecs", () => {
|
|
6
|
+
describe("SQL_WHERE", () => {
|
|
7
|
+
it("returns empty clause when no filters", () => {
|
|
8
|
+
const result = SQL_WHERE.encode({ filter: undefined });
|
|
9
|
+
expect(result.sql).toBe("");
|
|
10
|
+
expect(result.params).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("encodes scope namespace filter", () => {
|
|
14
|
+
const result = SQL_WHERE.encode({
|
|
15
|
+
filter: { scope: { namespace: "default" } },
|
|
16
|
+
});
|
|
17
|
+
expect(result.sql).toBe("namespace = ?");
|
|
18
|
+
expect(result.params).toEqual(["default"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("encodes scope entityId filter", () => {
|
|
22
|
+
const result = SQL_WHERE.encode({
|
|
23
|
+
filter: { scope: { entityId: "user-1" } },
|
|
24
|
+
});
|
|
25
|
+
expect(result.sql).toBe("entity_id = ?");
|
|
26
|
+
expect(result.params).toEqual(["user-1"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("encodes scope agentId filter", () => {
|
|
30
|
+
const result = SQL_WHERE.encode({
|
|
31
|
+
filter: { scope: { agentId: "agent-1" } },
|
|
32
|
+
});
|
|
33
|
+
expect(result.sql).toBe("agent_id = ?");
|
|
34
|
+
expect(result.params).toEqual(["agent-1"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("encodes full scope filter", () => {
|
|
38
|
+
const result = SQL_WHERE.encode({
|
|
39
|
+
filter: {
|
|
40
|
+
scope: {
|
|
41
|
+
namespace: "default",
|
|
42
|
+
entityId: "user-1",
|
|
43
|
+
agentId: "agent-1",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
expect(result.sql).toBe("namespace = ? AND entity_id = ? AND agent_id = ?");
|
|
48
|
+
expect(result.params).toEqual(["default", "user-1", "agent-1"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("encodes collections filter with IN clause", () => {
|
|
52
|
+
const result = SQL_WHERE.encode({
|
|
53
|
+
filter: { collections: ["facts", "preferences"] },
|
|
54
|
+
});
|
|
55
|
+
expect(result.sql).toBe("collection IN (?, ?)");
|
|
56
|
+
expect(result.params).toEqual(["facts", "preferences"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("encodes wmem filter", () => {
|
|
60
|
+
const result = SQL_WHERE.encode({ filter: { wmem: true } });
|
|
61
|
+
expect(result.sql).toBe("wmem = ?");
|
|
62
|
+
expect(result.params).toEqual([1]); // SQLite boolean
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("encodes smem filter (has valid expiration)", () => {
|
|
66
|
+
const before = Date.now();
|
|
67
|
+
const result = SQL_WHERE.encode({ filter: { smem: true } });
|
|
68
|
+
const after = Date.now();
|
|
69
|
+
|
|
70
|
+
// smem=true means has expiration AND not expired
|
|
71
|
+
expect(result.sql).toBe("(smem_expires_at IS NOT NULL AND smem_expires_at > ?)");
|
|
72
|
+
expect(result.params.length).toBe(1);
|
|
73
|
+
expect(result.params[0]).toBeGreaterThanOrEqual(before);
|
|
74
|
+
expect(result.params[0]).toBeLessThanOrEqual(after);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("encodes smem=false filter (no expiration or expired)", () => {
|
|
78
|
+
const before = Date.now();
|
|
79
|
+
const result = SQL_WHERE.encode({ filter: { smem: false } });
|
|
80
|
+
const after = Date.now();
|
|
81
|
+
|
|
82
|
+
// smem=false means no expiration OR already expired
|
|
83
|
+
expect(result.sql).toBe("(smem_expires_at IS NULL OR smem_expires_at <= ?)");
|
|
84
|
+
expect(result.params.length).toBe(1);
|
|
85
|
+
expect(result.params[0]).toBeGreaterThanOrEqual(before);
|
|
86
|
+
expect(result.params[0]).toBeLessThanOrEqual(after);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("encodes timestamp range filters", () => {
|
|
90
|
+
const result = SQL_WHERE.encode({
|
|
91
|
+
filter: { after: 1700000000000, before: 1700100000000 },
|
|
92
|
+
});
|
|
93
|
+
expect(result.sql).toBe("timestamp > ? AND timestamp < ?");
|
|
94
|
+
expect(result.params).toEqual([1700000000000, 1700100000000]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("combines multiple filters with AND", () => {
|
|
98
|
+
const result = SQL_WHERE.encode({
|
|
99
|
+
filter: {
|
|
100
|
+
scope: { namespace: "default" },
|
|
101
|
+
wmem: true,
|
|
102
|
+
collections: ["facts"],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.sql).toContain("namespace = ?");
|
|
107
|
+
expect(result.sql).toContain("wmem = ?");
|
|
108
|
+
expect(result.sql).toContain("collection IN (?)");
|
|
109
|
+
expect(result.sql.match(/AND/g)?.length).toBe(2);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("ORDER", () => {
|
|
114
|
+
it("returns default ordering (desc)", () => {
|
|
115
|
+
const result = ORDER.encode({ order: undefined });
|
|
116
|
+
expect(result).toBe("timestamp DESC");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("encodes asc order", () => {
|
|
120
|
+
const result = ORDER.encode({ order: "asc" });
|
|
121
|
+
expect(result).toBe("timestamp ASC");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("encodes desc order", () => {
|
|
125
|
+
const result = ORDER.encode({ order: "desc" });
|
|
126
|
+
expect(result).toBe("timestamp DESC");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("SQL_UPDATE", () => {
|
|
131
|
+
it("encodes content update with JSON stringify", () => {
|
|
132
|
+
const content = { text: "Updated content" };
|
|
133
|
+
const result = SQL_UPDATE.encode({ patch: { id: "m1", content } });
|
|
134
|
+
expect(result.sql).toContain("content = ?");
|
|
135
|
+
expect(result.params).toContain(JSON.stringify(content));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("encodes wmem flag update", () => {
|
|
139
|
+
const result = SQL_UPDATE.encode({ patch: { id: "m1", wmem: true } });
|
|
140
|
+
expect(result.sql).toContain("wmem = ?");
|
|
141
|
+
expect(result.params).toContain(1); // SQLite boolean
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("encodes smem expiration update", () => {
|
|
145
|
+
const result = SQL_UPDATE.encode({
|
|
146
|
+
patch: { id: "m1", smem: { expiresAt: 1700100000000 } },
|
|
147
|
+
});
|
|
148
|
+
expect(result.sql).toContain("smem_expires_at = ?");
|
|
149
|
+
expect(result.params).toContain(1700100000000);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("encodes metadata update", () => {
|
|
153
|
+
const metadata = { confidence: 0.95 };
|
|
154
|
+
const result = SQL_UPDATE.encode({ patch: { id: "m1", metadata } });
|
|
155
|
+
expect(result.sql).toContain("metadata = ?");
|
|
156
|
+
expect(result.params).toContain(JSON.stringify(metadata));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("always includes updated_at", () => {
|
|
160
|
+
const before = Date.now();
|
|
161
|
+
const result = SQL_UPDATE.encode({ patch: { id: "m1", wmem: false } });
|
|
162
|
+
const after = Date.now();
|
|
163
|
+
|
|
164
|
+
expect(result.sql).toContain("updated_at = ?");
|
|
165
|
+
const updatedAt = result.params.find(
|
|
166
|
+
(p) => typeof p === "number" && p >= before && p <= after,
|
|
167
|
+
);
|
|
168
|
+
expect(updatedAt).toBeDefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("combines multiple updates", () => {
|
|
172
|
+
const result = SQL_UPDATE.encode({
|
|
173
|
+
patch: {
|
|
174
|
+
id: "m1",
|
|
175
|
+
content: { text: "New" },
|
|
176
|
+
wmem: true,
|
|
177
|
+
metadata: { edited: true },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
expect(result.sql).toContain("content = ?");
|
|
181
|
+
expect(result.sql).toContain("wmem = ?");
|
|
182
|
+
expect(result.sql).toContain("metadata = ?");
|
|
183
|
+
expect(result.sql).toContain("updated_at = ?");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import type { Client } from "@libsql/client";
|
|
3
|
+
|
|
4
|
+
import { create_client, create_storage, testid } from "../../__tests__/helpers";
|
|
5
|
+
import { LibSQLStorage } from "../../storage";
|
|
6
|
+
|
|
7
|
+
describe("LibSQLMemoryStore update", () => {
|
|
8
|
+
let client: Client;
|
|
9
|
+
let storage: LibSQLStorage;
|
|
10
|
+
let memId: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
client = create_client();
|
|
14
|
+
storage = create_storage(client);
|
|
15
|
+
await storage.memories.list(); // init
|
|
16
|
+
|
|
17
|
+
memId = testid("mem");
|
|
18
|
+
await storage.memories.create({
|
|
19
|
+
id: memId,
|
|
20
|
+
scope: { namespace: "default", entityId: "user-1" },
|
|
21
|
+
kind: "semantic",
|
|
22
|
+
collection: "facts",
|
|
23
|
+
content: { text: "Original content" },
|
|
24
|
+
wmem: false,
|
|
25
|
+
metadata: { version: 1 },
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
client.close();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("updates content and bumps updated_at", async () => {
|
|
34
|
+
const before = await storage.memories.get(memId);
|
|
35
|
+
const originalUpdatedAt = before!.updatedAt;
|
|
36
|
+
|
|
37
|
+
// Small delay to ensure timestamp difference
|
|
38
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
39
|
+
|
|
40
|
+
const updated = await storage.memories.update(memId, {
|
|
41
|
+
id: memId,
|
|
42
|
+
content: { text: "Updated content" },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(updated.content).toEqual({ text: "Updated content" });
|
|
46
|
+
expect(updated.updatedAt).toBeGreaterThan(originalUpdatedAt);
|
|
47
|
+
|
|
48
|
+
// Verify persisted
|
|
49
|
+
const found = await storage.memories.get(memId);
|
|
50
|
+
expect(found?.content).toEqual({ text: "Updated content" });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("updates wmem flag", async () => {
|
|
54
|
+
const updated = await storage.memories.update(memId, {
|
|
55
|
+
id: memId,
|
|
56
|
+
wmem: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(updated.wmem).toBe(true);
|
|
60
|
+
|
|
61
|
+
const found = await storage.memories.get(memId);
|
|
62
|
+
expect(found?.wmem).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("updates smem expiration", async () => {
|
|
66
|
+
const expiresAt = Date.now() + 7200000;
|
|
67
|
+
|
|
68
|
+
const updated = await storage.memories.update(memId, {
|
|
69
|
+
id: memId,
|
|
70
|
+
smem: { expiresAt },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(updated.smem.expiresAt).toBe(expiresAt);
|
|
74
|
+
|
|
75
|
+
const found = await storage.memories.get(memId);
|
|
76
|
+
expect(found?.smem.expiresAt).toBe(expiresAt);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("clears smem expiration", async () => {
|
|
80
|
+
// First set an expiration
|
|
81
|
+
await storage.memories.update(memId, {
|
|
82
|
+
id: memId,
|
|
83
|
+
smem: { expiresAt: Date.now() + 3600000 },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Then clear it
|
|
87
|
+
const updated = await storage.memories.update(memId, {
|
|
88
|
+
id: memId,
|
|
89
|
+
smem: { expiresAt: null },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(updated.smem.expiresAt).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("updates metadata", async () => {
|
|
96
|
+
const newMetadata = { version: 2, source: "api", edited: true };
|
|
97
|
+
|
|
98
|
+
const updated = await storage.memories.update(memId, {
|
|
99
|
+
id: memId,
|
|
100
|
+
metadata: newMetadata,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(updated.metadata).toEqual(newMetadata);
|
|
104
|
+
|
|
105
|
+
const found = await storage.memories.get(memId);
|
|
106
|
+
expect(found?.metadata).toEqual(newMetadata);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("updates multiple fields at once", async () => {
|
|
110
|
+
const updated = await storage.memories.update(memId, {
|
|
111
|
+
id: memId,
|
|
112
|
+
content: { text: "New content" },
|
|
113
|
+
wmem: true,
|
|
114
|
+
metadata: { version: 3 },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(updated.content).toEqual({ text: "New content" });
|
|
118
|
+
expect(updated.wmem).toBe(true);
|
|
119
|
+
expect(updated.metadata).toEqual({ version: 3 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("throws when memory does not exist", async () => {
|
|
123
|
+
await expect(
|
|
124
|
+
storage.memories.update("nonexistent", {
|
|
125
|
+
id: "nonexistent",
|
|
126
|
+
content: { text: "test" },
|
|
127
|
+
}),
|
|
128
|
+
).rejects.toThrow(/not found/i);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("preserves unchanged fields", async () => {
|
|
132
|
+
const before = await storage.memories.get(memId);
|
|
133
|
+
|
|
134
|
+
await storage.memories.update(memId, {
|
|
135
|
+
id: memId,
|
|
136
|
+
wmem: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const after = await storage.memories.get(memId);
|
|
140
|
+
|
|
141
|
+
// Content should be unchanged
|
|
142
|
+
expect(after?.content).toEqual(before?.content);
|
|
143
|
+
expect(after?.kind).toBe(before?.kind);
|
|
144
|
+
expect(after?.collection).toBe(before?.collection);
|
|
145
|
+
// wmem should be updated
|
|
146
|
+
expect(after?.wmem).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LibSQL row codecs for memory data.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Row } from "@libsql/client";
|
|
6
|
+
import type { Codec } from "@kernl-sdk/shared/lib";
|
|
7
|
+
import type { MemoryDBRecord } from "@kernl-sdk/storage";
|
|
8
|
+
|
|
9
|
+
import { parsejson } from "../utils";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Codec for converting LibSQL rows to MemoryDBRecord.
|
|
13
|
+
*/
|
|
14
|
+
export const RowToMemoryRecord: Codec<Row, MemoryDBRecord> = {
|
|
15
|
+
encode(row: Row): MemoryDBRecord {
|
|
16
|
+
return {
|
|
17
|
+
id: row.id as string,
|
|
18
|
+
namespace: row.namespace as string | null,
|
|
19
|
+
entity_id: row.entity_id as string | null,
|
|
20
|
+
agent_id: row.agent_id as string | null,
|
|
21
|
+
kind: row.kind as "episodic" | "semantic",
|
|
22
|
+
collection: row.collection as string | null,
|
|
23
|
+
content: parsejson<Record<string, unknown>>(row.content) ?? {},
|
|
24
|
+
wmem: Boolean(row.wmem), // Convert SQLite 0/1 to boolean
|
|
25
|
+
smem_expires_at: row.smem_expires_at as number | null,
|
|
26
|
+
timestamp: row.timestamp as number,
|
|
27
|
+
created_at: row.created_at as number,
|
|
28
|
+
updated_at: row.updated_at as number,
|
|
29
|
+
metadata: parsejson<Record<string, unknown>>(row.metadata),
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
decode(): Row {
|
|
34
|
+
throw new Error("RowToMemoryRecord.decode not implemented");
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory SQL conversion codecs for LibSQL.
|
|
3
|
+
*
|
|
4
|
+
* Uses ? placeholders instead of PostgreSQL's $1, $2, etc.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Codec } from "@kernl-sdk/shared/lib";
|
|
8
|
+
import type { MemoryFilter, MemoryRecordUpdate } from "kernl";
|
|
9
|
+
|
|
10
|
+
import { type SQLClause, expandarray } from "../sql";
|
|
11
|
+
|
|
12
|
+
export interface WhereInput {
|
|
13
|
+
filter?: MemoryFilter;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Encode MemoryFilter to SQL WHERE clause with ? placeholders.
|
|
18
|
+
*/
|
|
19
|
+
export const SQL_WHERE: Codec<WhereInput, SQLClause> = {
|
|
20
|
+
encode({ filter }) {
|
|
21
|
+
if (!filter) {
|
|
22
|
+
return { sql: "", params: [] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const conditions: string[] = [];
|
|
26
|
+
const params: unknown[] = [];
|
|
27
|
+
|
|
28
|
+
if (filter.scope?.namespace !== undefined) {
|
|
29
|
+
conditions.push(`namespace = ?`);
|
|
30
|
+
params.push(filter.scope.namespace);
|
|
31
|
+
}
|
|
32
|
+
if (filter.scope?.entityId !== undefined) {
|
|
33
|
+
conditions.push(`entity_id = ?`);
|
|
34
|
+
params.push(filter.scope.entityId);
|
|
35
|
+
}
|
|
36
|
+
if (filter.scope?.agentId !== undefined) {
|
|
37
|
+
conditions.push(`agent_id = ?`);
|
|
38
|
+
params.push(filter.scope.agentId);
|
|
39
|
+
}
|
|
40
|
+
if (filter.collections && filter.collections.length > 0) {
|
|
41
|
+
const { placeholders, params: collectionParams } = expandarray(
|
|
42
|
+
filter.collections,
|
|
43
|
+
);
|
|
44
|
+
conditions.push(`collection IN (${placeholders})`);
|
|
45
|
+
params.push(...collectionParams);
|
|
46
|
+
}
|
|
47
|
+
if (filter.wmem !== undefined) {
|
|
48
|
+
conditions.push(`wmem = ?`);
|
|
49
|
+
params.push(filter.wmem ? 1 : 0); // SQLite uses 0/1 for boolean
|
|
50
|
+
}
|
|
51
|
+
if (filter.smem === true) {
|
|
52
|
+
conditions.push(`(smem_expires_at IS NOT NULL AND smem_expires_at > ?)`);
|
|
53
|
+
params.push(Date.now());
|
|
54
|
+
} else if (filter.smem === false) {
|
|
55
|
+
conditions.push(`(smem_expires_at IS NULL OR smem_expires_at <= ?)`);
|
|
56
|
+
params.push(Date.now());
|
|
57
|
+
}
|
|
58
|
+
if (filter.after !== undefined) {
|
|
59
|
+
conditions.push(`timestamp > ?`);
|
|
60
|
+
params.push(filter.after);
|
|
61
|
+
}
|
|
62
|
+
if (filter.before !== undefined) {
|
|
63
|
+
conditions.push(`timestamp < ?`);
|
|
64
|
+
params.push(filter.before);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
sql: conditions.length > 0 ? conditions.join(" AND ") : "",
|
|
69
|
+
params,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
decode() {
|
|
74
|
+
throw new Error("SQL_WHERE.decode not implemented");
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type OrderDirection = "asc" | "desc";
|
|
79
|
+
|
|
80
|
+
export interface OrderInput {
|
|
81
|
+
order?: OrderDirection;
|
|
82
|
+
defaultColumn?: string;
|
|
83
|
+
defaultDirection?: OrderDirection;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Encode order options to SQL ORDER BY clause.
|
|
88
|
+
*/
|
|
89
|
+
export const ORDER: Codec<OrderInput, string> = {
|
|
90
|
+
encode({ order, defaultColumn = "timestamp", defaultDirection = "desc" }) {
|
|
91
|
+
const dir = (order ?? defaultDirection).toUpperCase();
|
|
92
|
+
return `${defaultColumn} ${dir}`;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
decode() {
|
|
96
|
+
throw new Error("ORDER.decode not implemented");
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export interface PatchInput {
|
|
101
|
+
patch: MemoryRecordUpdate;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Encode MemoryRecordUpdate to SQL SET clause with ? placeholders.
|
|
106
|
+
*/
|
|
107
|
+
export const SQL_UPDATE: Codec<PatchInput, SQLClause> = {
|
|
108
|
+
encode({ patch }) {
|
|
109
|
+
const sets: string[] = [];
|
|
110
|
+
const params: unknown[] = [];
|
|
111
|
+
|
|
112
|
+
if (patch.content !== undefined) {
|
|
113
|
+
sets.push(`content = ?`);
|
|
114
|
+
params.push(JSON.stringify(patch.content));
|
|
115
|
+
}
|
|
116
|
+
if (patch.wmem !== undefined) {
|
|
117
|
+
sets.push(`wmem = ?`);
|
|
118
|
+
params.push(patch.wmem ? 1 : 0); // SQLite uses 0/1 for boolean
|
|
119
|
+
}
|
|
120
|
+
if (patch.smem !== undefined) {
|
|
121
|
+
sets.push(`smem_expires_at = ?`);
|
|
122
|
+
params.push(patch.smem.expiresAt);
|
|
123
|
+
}
|
|
124
|
+
if (patch.metadata !== undefined) {
|
|
125
|
+
sets.push(`metadata = ?`);
|
|
126
|
+
params.push(patch.metadata ? JSON.stringify(patch.metadata) : null);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// always update updated_at
|
|
130
|
+
sets.push(`updated_at = ?`);
|
|
131
|
+
params.push(Date.now());
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
sql: sets.join(", "),
|
|
135
|
+
params,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
decode() {
|
|
140
|
+
throw new Error("SQL_UPDATE.decode not implemented");
|
|
141
|
+
},
|
|
142
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LibSQL Memory store implementation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Client, InValue } from "@libsql/client";
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
MemoryStore,
|
|
9
|
+
MemoryRecord,
|
|
10
|
+
NewMemory,
|
|
11
|
+
MemoryRecordUpdate,
|
|
12
|
+
MemoryListOptions,
|
|
13
|
+
} from "kernl";
|
|
14
|
+
import {
|
|
15
|
+
KERNL_SCHEMA_NAME,
|
|
16
|
+
MemoryRecordCodec,
|
|
17
|
+
NewMemoryCodec,
|
|
18
|
+
} from "@kernl-sdk/storage";
|
|
19
|
+
|
|
20
|
+
import { SQL_WHERE, ORDER, SQL_UPDATE } from "./sql";
|
|
21
|
+
import { RowToMemoryRecord } from "./row";
|
|
22
|
+
import { expandarray } from "../sql";
|
|
23
|
+
|
|
24
|
+
// SQLite doesn't support schemas, so we use table name prefix
|
|
25
|
+
const MEMORIES_TABLE = `${KERNL_SCHEMA_NAME}_memories`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* LibSQL memory store implementation.
|
|
29
|
+
*
|
|
30
|
+
* All async methods call `ensureInit()` before database operations
|
|
31
|
+
* to ensure schema/tables exist.
|
|
32
|
+
*/
|
|
33
|
+
export class LibSQLMemoryStore implements MemoryStore {
|
|
34
|
+
private db: Client;
|
|
35
|
+
private ensureInit: () => Promise<void>;
|
|
36
|
+
|
|
37
|
+
constructor(db: Client, ensureInit: () => Promise<void>) {
|
|
38
|
+
this.db = db;
|
|
39
|
+
this.ensureInit = ensureInit;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a memory by ID.
|
|
44
|
+
*/
|
|
45
|
+
async get(id: string): Promise<MemoryRecord | null> {
|
|
46
|
+
await this.ensureInit();
|
|
47
|
+
|
|
48
|
+
const result = await this.db.execute({
|
|
49
|
+
sql: `SELECT * FROM ${MEMORIES_TABLE} WHERE id = ?`,
|
|
50
|
+
args: [id],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (result.rows.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return MemoryRecordCodec.decode(RowToMemoryRecord.encode(result.rows[0]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List memories matching optional filter criteria.
|
|
62
|
+
*/
|
|
63
|
+
async list(options?: MemoryListOptions): Promise<MemoryRecord[]> {
|
|
64
|
+
await this.ensureInit();
|
|
65
|
+
|
|
66
|
+
const { sql: where, params } = SQL_WHERE.encode({
|
|
67
|
+
filter: options?.filter,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let query = `SELECT * FROM ${MEMORIES_TABLE}`;
|
|
71
|
+
|
|
72
|
+
// build where + order by
|
|
73
|
+
if (where) query += ` WHERE ${where}`;
|
|
74
|
+
query += ` ORDER BY ${ORDER.encode({ order: options?.order })}`;
|
|
75
|
+
|
|
76
|
+
const args = [...params] as InValue[];
|
|
77
|
+
|
|
78
|
+
// add limit + offset
|
|
79
|
+
// SQLite requires LIMIT when using OFFSET, so use -1 for unlimited
|
|
80
|
+
if (options?.limit || options?.offset) {
|
|
81
|
+
query += ` LIMIT ?`;
|
|
82
|
+
args.push(options?.limit ?? -1);
|
|
83
|
+
}
|
|
84
|
+
if (options?.offset) {
|
|
85
|
+
query += ` OFFSET ?`;
|
|
86
|
+
args.push(options.offset);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await this.db.execute({ sql: query, args });
|
|
90
|
+
return result.rows.map((row) =>
|
|
91
|
+
MemoryRecordCodec.decode(RowToMemoryRecord.encode(row)),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a new memory record.
|
|
97
|
+
*/
|
|
98
|
+
async create(memory: NewMemory): Promise<MemoryRecord> {
|
|
99
|
+
await this.ensureInit();
|
|
100
|
+
|
|
101
|
+
const row = NewMemoryCodec.encode(memory);
|
|
102
|
+
|
|
103
|
+
const result = await this.db.execute({
|
|
104
|
+
sql: `INSERT INTO ${MEMORIES_TABLE}
|
|
105
|
+
(id, namespace, entity_id, agent_id, kind, collection, content, wmem, smem_expires_at, timestamp, created_at, updated_at, metadata)
|
|
106
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
107
|
+
RETURNING *`,
|
|
108
|
+
args: [
|
|
109
|
+
row.id,
|
|
110
|
+
row.namespace,
|
|
111
|
+
row.entity_id,
|
|
112
|
+
row.agent_id,
|
|
113
|
+
row.kind,
|
|
114
|
+
row.collection,
|
|
115
|
+
JSON.stringify(row.content),
|
|
116
|
+
row.wmem ? 1 : 0, // SQLite uses 0/1 for boolean
|
|
117
|
+
row.smem_expires_at,
|
|
118
|
+
row.timestamp,
|
|
119
|
+
row.created_at,
|
|
120
|
+
row.updated_at,
|
|
121
|
+
row.metadata ? JSON.stringify(row.metadata) : null,
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return MemoryRecordCodec.decode(RowToMemoryRecord.encode(result.rows[0]));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Update a memory record.
|
|
130
|
+
*/
|
|
131
|
+
async update(id: string, patch: MemoryRecordUpdate): Promise<MemoryRecord> {
|
|
132
|
+
await this.ensureInit();
|
|
133
|
+
|
|
134
|
+
const { sql: updates, params } = SQL_UPDATE.encode({ patch });
|
|
135
|
+
const args = [...params, id] as InValue[];
|
|
136
|
+
|
|
137
|
+
const result = await this.db.execute({
|
|
138
|
+
sql: `UPDATE ${MEMORIES_TABLE} SET ${updates} WHERE id = ? RETURNING *`,
|
|
139
|
+
args,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (result.rows.length === 0) {
|
|
143
|
+
throw new Error(`memory not found: ${id}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return MemoryRecordCodec.decode(RowToMemoryRecord.encode(result.rows[0]));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Delete a memory by ID.
|
|
151
|
+
*/
|
|
152
|
+
async delete(id: string): Promise<void> {
|
|
153
|
+
await this.ensureInit();
|
|
154
|
+
await this.db.execute({
|
|
155
|
+
sql: `DELETE FROM ${MEMORIES_TABLE} WHERE id = ?`,
|
|
156
|
+
args: [id],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Delete multiple memories by ID.
|
|
162
|
+
*/
|
|
163
|
+
async mdelete(ids: string[]): Promise<void> {
|
|
164
|
+
if (ids.length === 0) return;
|
|
165
|
+
await this.ensureInit();
|
|
166
|
+
|
|
167
|
+
const { placeholders, params } = expandarray(ids);
|
|
168
|
+
await this.db.execute({
|
|
169
|
+
sql: `DELETE FROM ${MEMORIES_TABLE} WHERE id IN (${placeholders})`,
|
|
170
|
+
args: params as InValue[],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|