talon-agent 4.3.3 → 4.5.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/package.json +1 -1
- package/src/app.ts +1 -1
- package/src/backend/claude-sdk/state.ts +1 -1
- package/src/backend/codex/init.ts +1 -1
- package/src/backend/codex/state.ts +1 -1
- package/src/backend/openai-agents/init.ts +1 -1
- package/src/backend/openai-agents/state.ts +1 -1
- package/src/backend/remote-server/factory.ts +1 -1
- package/src/backend/remote-server/mcp.ts +1 -1
- package/src/backend/remote-server/server-bindings.ts +1 -1
- package/src/backend/remote-server/state.ts +1 -1
- package/src/backend/shared/system-prompt.ts +1 -1
- package/src/bootstrap.ts +2 -2
- package/src/cli/chat.ts +1 -1
- package/src/cli/config.ts +2 -2
- package/src/cli/index.ts +8 -0
- package/src/cli/memory.ts +197 -0
- package/src/cli/setup.ts +1 -1
- package/src/core/agent-runtime/backend-registry.ts +1 -1
- package/src/core/agent-runtime/model-ref.ts +2 -2
- package/src/{util/config.ts → core/config/index.ts} +8 -8
- package/src/core/engine/backend-controller/legacy.ts +1 -1
- package/src/core/engine/backend-controller/pool.ts +1 -1
- package/src/core/engine/backend-controller/rebind.ts +1 -1
- package/src/core/engine/backend-controller/state.ts +1 -1
- package/src/core/engine/gateway-actions/plugins.ts +1 -1
- package/src/core/engine/model-audit.ts +1 -1
- package/src/core/frontend-runtime/capabilities.ts +1 -1
- package/src/core/memory/import.ts +380 -0
- package/src/core/memory/render.ts +243 -0
- package/src/core/models/active-model.ts +1 -1
- package/src/core/plugin/builtins.ts +2 -2
- package/src/core/plugin/manage.ts +1 -1
- package/src/core/prompt/memory-view.ts +8 -8
- package/src/frontend/discord/admin.ts +1 -1
- package/src/frontend/discord/callbacks/components/index.ts +1 -1
- package/src/frontend/discord/callbacks/components/settings.ts +1 -1
- package/src/frontend/discord/callbacks/components/types.ts +1 -1
- package/src/frontend/discord/callbacks/modals.ts +1 -1
- package/src/frontend/discord/commands/admin.ts +1 -1
- package/src/frontend/discord/commands/definitions.ts +1 -1
- package/src/frontend/discord/commands/info.ts +1 -1
- package/src/frontend/discord/commands/router.ts +1 -1
- package/src/frontend/discord/commands/session.ts +1 -1
- package/src/frontend/discord/commands/settings.ts +1 -1
- package/src/frontend/discord/handlers/delivery.ts +1 -1
- package/src/frontend/discord/handlers/messages.ts +1 -1
- package/src/frontend/discord/handlers/queue.ts +1 -1
- package/src/frontend/discord/handlers/state.ts +1 -1
- package/src/frontend/discord/index.ts +1 -1
- package/src/frontend/discord/middleware.ts +1 -1
- package/src/frontend/discord/runtime.ts +1 -1
- package/src/frontend/native/extensions.ts +4 -1
- package/src/frontend/native/handlers.ts +5 -0
- package/src/frontend/native/index.ts +1 -1
- package/src/frontend/native/memory.ts +112 -0
- package/src/frontend/native/protocol.ts +45 -0
- package/src/frontend/native/routes/host.ts +9 -0
- package/src/frontend/native/routes/index.ts +2 -0
- package/src/frontend/native/routes/memory.ts +34 -0
- package/src/frontend/native/routes/table.ts +7 -0
- package/src/frontend/native/runtime.ts +1 -1
- package/src/frontend/native/settings.ts +1 -1
- package/src/frontend/shared/model-commands.ts +1 -1
- package/src/frontend/shared/plan-usage-report.ts +1 -1
- package/src/frontend/shared/reasoning-levels.ts +1 -1
- package/src/frontend/shared/session-status.ts +1 -1
- package/src/frontend/teams/index.ts +1 -1
- package/src/frontend/teams/runtime.ts +1 -1
- package/src/frontend/telegram/actions/chat-info.ts +1 -1
- package/src/frontend/telegram/actions/coerce.ts +15 -0
- package/src/frontend/telegram/actions/index.ts +1 -1
- package/src/frontend/telegram/actions/media.ts +1 -1
- package/src/frontend/telegram/actions/messaging.ts +3 -4
- package/src/frontend/telegram/actions/moderation/chat.ts +1 -1
- package/src/frontend/telegram/actions/moderation/index.ts +1 -1
- package/src/frontend/telegram/actions/moderation/topics.ts +1 -1
- package/src/frontend/telegram/actions/rich-messages.ts +51 -0
- package/src/frontend/telegram/actions/{shared.ts → send.ts} +9 -59
- package/src/frontend/telegram/admin/sessions.ts +1 -1
- package/src/frontend/telegram/admin.ts +1 -1
- package/src/frontend/telegram/callbacks/auth.ts +1 -1
- package/src/frontend/telegram/callbacks/effort.ts +1 -1
- package/src/frontend/telegram/callbacks/index.ts +3 -3
- package/src/frontend/telegram/callbacks/metrics.ts +1 -1
- package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
- package/src/frontend/telegram/callbacks/model/control.ts +1 -1
- package/src/frontend/telegram/callbacks/model/select.ts +1 -1
- package/src/frontend/telegram/callbacks/model/types.ts +1 -1
- package/src/frontend/telegram/callbacks/model/views.ts +1 -1
- package/src/frontend/telegram/callbacks/model.ts +1 -1
- package/src/frontend/telegram/callbacks/pulse.ts +1 -1
- package/src/frontend/telegram/callbacks/{shared.ts → query.ts} +3 -2
- package/src/frontend/telegram/callbacks/settings.ts +1 -1
- package/src/frontend/telegram/callbacks/whatsapp.ts +1 -1
- package/src/frontend/telegram/commands/admin.ts +1 -1
- package/src/frontend/telegram/commands/definitions.ts +4 -0
- package/src/frontend/telegram/commands/index.ts +4 -1
- package/src/frontend/telegram/commands/info.ts +3 -0
- package/src/frontend/telegram/commands/memory.ts +112 -0
- package/src/frontend/telegram/commands/state.ts +1 -1
- package/src/frontend/telegram/handlers/context.ts +1 -1
- package/src/frontend/telegram/handlers/delivery.ts +2 -2
- package/src/frontend/telegram/handlers/messages.ts +1 -1
- package/src/frontend/telegram/handlers/queue.ts +1 -1
- package/src/frontend/telegram/handlers/state.ts +1 -1
- package/src/frontend/telegram/index.ts +1 -1
- package/src/frontend/telegram/middleware.ts +1 -1
- package/src/frontend/telegram/model-menu.ts +1 -1
- package/src/frontend/terminal/command-registry.ts +1 -1
- package/src/frontend/terminal/index.ts +1 -1
- package/src/frontend/whatsapp/index.ts +1 -1
- package/src/frontend/whatsapp/runtime.ts +1 -1
- package/src/storage/memory.ts +513 -0
- package/src/storage/repositories/memory-repo.ts +308 -0
- package/src/storage/sql/memory.sql +95 -0
- package/src/storage/sql/schema.sql +81 -0
- package/src/storage/sql/statements.generated.ts +146 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory repository — executes the statements in sql/memory.sql against
|
|
3
|
+
* the `memory` / `memory_fts` / `memory_history` tables; no SQL text
|
|
4
|
+
* lives here. The public store (storage/memory.ts) holds the domain API,
|
|
5
|
+
* validation and the transactional lifecycle rules; this module owns
|
|
6
|
+
* statement execution, the row↔domain mapping and the content hash.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { getDatabase } from "../db.js";
|
|
11
|
+
import { memorySql } from "../sql/statements.generated.js";
|
|
12
|
+
|
|
13
|
+
/** Kinds are lifecycles, not labels — see docs/memory-persona-plan.md §3.1. */
|
|
14
|
+
export type MemoryKind =
|
|
15
|
+
"directive" | "fact" | "state" | "episode" | "relationship" | "reflection";
|
|
16
|
+
|
|
17
|
+
/** Where a claim came from, and therefore how far it is allowed to rise. */
|
|
18
|
+
export type MemoryTrust = "operator" | "agent" | "user_claim" | "group_chat";
|
|
19
|
+
|
|
20
|
+
/** The mutation vocabulary recorded in memory_history. */
|
|
21
|
+
export type MemoryOp =
|
|
22
|
+
"assert" | "supersede" | "drop" | "merge" | "pin" | "unpin" | "replace_state";
|
|
23
|
+
|
|
24
|
+
/** Provenance of a claim: which frontend, chat, actor and turn asserted it. */
|
|
25
|
+
export type MemorySource = {
|
|
26
|
+
frontend?: string;
|
|
27
|
+
chat?: string;
|
|
28
|
+
actor?: string;
|
|
29
|
+
turn?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** One memory row as the domain sees it. */
|
|
33
|
+
export type MemoryRow = {
|
|
34
|
+
id: number;
|
|
35
|
+
kind: MemoryKind;
|
|
36
|
+
subject: string;
|
|
37
|
+
/** Present only for kind='state' — the key whose single live row this is. */
|
|
38
|
+
key?: string;
|
|
39
|
+
text: string;
|
|
40
|
+
source: MemorySource;
|
|
41
|
+
trust: MemoryTrust;
|
|
42
|
+
/** 0..1. */
|
|
43
|
+
confidence: number;
|
|
44
|
+
createdAt: number;
|
|
45
|
+
lastSeenAt: number;
|
|
46
|
+
hitCount: number;
|
|
47
|
+
salience: number;
|
|
48
|
+
pinned: boolean;
|
|
49
|
+
/** Id of the row that replaced this one; absent while live. */
|
|
50
|
+
supersededBy?: number;
|
|
51
|
+
/** When this row was dropped to the graveyard; absent while live. */
|
|
52
|
+
droppedAt?: number;
|
|
53
|
+
/** sha256 of kind|subject|key|text — the import idempotency key. */
|
|
54
|
+
contentHash: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** One audit entry. */
|
|
58
|
+
export type MemoryHistoryRow = {
|
|
59
|
+
id: number;
|
|
60
|
+
memoryId: number;
|
|
61
|
+
op: MemoryOp;
|
|
62
|
+
beforeText?: string;
|
|
63
|
+
afterText?: string;
|
|
64
|
+
reason?: string;
|
|
65
|
+
at: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** A new claim, as callers supply it (the store fills in the defaults). */
|
|
69
|
+
export type MemoryInput = {
|
|
70
|
+
kind: MemoryKind;
|
|
71
|
+
subject: string;
|
|
72
|
+
key?: string;
|
|
73
|
+
text: string;
|
|
74
|
+
source?: MemorySource;
|
|
75
|
+
trust: MemoryTrust;
|
|
76
|
+
confidence?: number;
|
|
77
|
+
salience?: number;
|
|
78
|
+
pinned?: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The fully-defaulted row the repository inserts. */
|
|
82
|
+
export type MemoryInsert = Omit<MemoryRow, "id" | "supersededBy" | "droppedAt">;
|
|
83
|
+
|
|
84
|
+
/** Filters for `list`; everything is optional and narrows the result. */
|
|
85
|
+
export type MemoryListFilter = {
|
|
86
|
+
kind?: MemoryKind;
|
|
87
|
+
subject?: string;
|
|
88
|
+
includeSuperseded?: boolean;
|
|
89
|
+
includeDropped?: boolean;
|
|
90
|
+
limit?: number;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type Row = {
|
|
94
|
+
id: number;
|
|
95
|
+
kind: string;
|
|
96
|
+
subject: string;
|
|
97
|
+
key: string | null;
|
|
98
|
+
text: string;
|
|
99
|
+
source_frontend: string | null;
|
|
100
|
+
source_chat: string | null;
|
|
101
|
+
source_actor: string | null;
|
|
102
|
+
source_turn: string | null;
|
|
103
|
+
trust: string;
|
|
104
|
+
confidence: number;
|
|
105
|
+
created_at: number;
|
|
106
|
+
last_seen_at: number;
|
|
107
|
+
hit_count: number;
|
|
108
|
+
salience: number;
|
|
109
|
+
pinned: number;
|
|
110
|
+
superseded_by: number | null;
|
|
111
|
+
dropped_at: number | null;
|
|
112
|
+
content_hash: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
type HistoryRow = {
|
|
116
|
+
id: number;
|
|
117
|
+
memory_id: number;
|
|
118
|
+
op: string;
|
|
119
|
+
before_text: string | null;
|
|
120
|
+
after_text: string | null;
|
|
121
|
+
reason: string | null;
|
|
122
|
+
at: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function rowToMemory(row: Row): MemoryRow {
|
|
126
|
+
const source: MemorySource = {
|
|
127
|
+
...(row.source_frontend !== null ? { frontend: row.source_frontend } : {}),
|
|
128
|
+
...(row.source_chat !== null ? { chat: row.source_chat } : {}),
|
|
129
|
+
...(row.source_actor !== null ? { actor: row.source_actor } : {}),
|
|
130
|
+
...(row.source_turn !== null ? { turn: row.source_turn } : {}),
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
id: Number(row.id),
|
|
134
|
+
kind: row.kind as MemoryKind,
|
|
135
|
+
subject: row.subject,
|
|
136
|
+
...(row.key !== null ? { key: row.key } : {}),
|
|
137
|
+
text: row.text,
|
|
138
|
+
source,
|
|
139
|
+
trust: row.trust as MemoryTrust,
|
|
140
|
+
confidence: row.confidence,
|
|
141
|
+
createdAt: row.created_at,
|
|
142
|
+
lastSeenAt: row.last_seen_at,
|
|
143
|
+
hitCount: row.hit_count,
|
|
144
|
+
salience: row.salience,
|
|
145
|
+
pinned: row.pinned !== 0,
|
|
146
|
+
...(row.superseded_by !== null
|
|
147
|
+
? { supersededBy: Number(row.superseded_by) }
|
|
148
|
+
: {}),
|
|
149
|
+
...(row.dropped_at !== null ? { droppedAt: row.dropped_at } : {}),
|
|
150
|
+
contentHash: row.content_hash,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function rowToHistory(row: HistoryRow): MemoryHistoryRow {
|
|
155
|
+
return {
|
|
156
|
+
id: Number(row.id),
|
|
157
|
+
memoryId: Number(row.memory_id),
|
|
158
|
+
op: row.op as MemoryOp,
|
|
159
|
+
...(row.before_text !== null ? { beforeText: row.before_text } : {}),
|
|
160
|
+
...(row.after_text !== null ? { afterText: row.after_text } : {}),
|
|
161
|
+
...(row.reason !== null ? { reason: row.reason } : {}),
|
|
162
|
+
at: row.at,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The idempotency key for PR 5's import: identical claims hash
|
|
168
|
+
* identically regardless of provenance or ranking, so re-importing the
|
|
169
|
+
* same `memory.md` produces no new rows.
|
|
170
|
+
*/
|
|
171
|
+
export function contentHash(
|
|
172
|
+
kind: string,
|
|
173
|
+
subject: string,
|
|
174
|
+
key: string | undefined,
|
|
175
|
+
text: string,
|
|
176
|
+
): string {
|
|
177
|
+
return createHash("sha256")
|
|
178
|
+
.update(`${kind}|${subject}|${key ?? ""}|${text}`)
|
|
179
|
+
.digest("hex");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Insert a fully-defaulted row; returns the new id. */
|
|
183
|
+
export function insert(row: MemoryInsert): number {
|
|
184
|
+
const inserted = getDatabase()
|
|
185
|
+
.prepare(memorySql.insert)
|
|
186
|
+
.get(
|
|
187
|
+
row.kind,
|
|
188
|
+
row.subject,
|
|
189
|
+
row.key ?? null,
|
|
190
|
+
row.text,
|
|
191
|
+
row.source.frontend ?? null,
|
|
192
|
+
row.source.chat ?? null,
|
|
193
|
+
row.source.actor ?? null,
|
|
194
|
+
row.source.turn ?? null,
|
|
195
|
+
row.trust,
|
|
196
|
+
row.confidence,
|
|
197
|
+
row.createdAt,
|
|
198
|
+
row.lastSeenAt,
|
|
199
|
+
row.hitCount,
|
|
200
|
+
row.salience,
|
|
201
|
+
row.pinned ? 1 : 0,
|
|
202
|
+
row.contentHash,
|
|
203
|
+
) as { id: number };
|
|
204
|
+
return Number(inserted.id);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function get(id: number): MemoryRow | undefined {
|
|
208
|
+
const row = getDatabase().prepare(memorySql.get).get(id) as Row | undefined;
|
|
209
|
+
return row ? rowToMemory(row) : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Filtered listing, pinned first then salience then recency. */
|
|
213
|
+
export function list(filter: MemoryListFilter = {}): MemoryRow[] {
|
|
214
|
+
const kind = filter.kind ?? null;
|
|
215
|
+
const subject = filter.subject ?? null;
|
|
216
|
+
const rows = getDatabase()
|
|
217
|
+
.prepare(memorySql.list)
|
|
218
|
+
.all(
|
|
219
|
+
kind,
|
|
220
|
+
kind,
|
|
221
|
+
subject,
|
|
222
|
+
subject,
|
|
223
|
+
filter.includeSuperseded ? 1 : 0,
|
|
224
|
+
filter.includeDropped ? 1 : 0,
|
|
225
|
+
filter.limit ?? 100,
|
|
226
|
+
) as Row[];
|
|
227
|
+
return rows.map(rowToMemory);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The single live `state` row for a key, if there is one. */
|
|
231
|
+
export function liveStateByKey(key: string): MemoryRow | undefined {
|
|
232
|
+
const row = getDatabase().prepare(memorySql.liveStateByKey).get(key) as
|
|
233
|
+
Row | undefined;
|
|
234
|
+
return row ? rowToMemory(row) : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** FTS5 search over live rows, best match first. `match` must be quoted. */
|
|
238
|
+
export function searchFts(
|
|
239
|
+
match: string,
|
|
240
|
+
kind: MemoryKind | undefined,
|
|
241
|
+
limit: number,
|
|
242
|
+
): MemoryRow[] {
|
|
243
|
+
const k = kind ?? null;
|
|
244
|
+
const rows = getDatabase()
|
|
245
|
+
.prepare(memorySql.searchFts)
|
|
246
|
+
.all(match, k, k, limit) as Row[];
|
|
247
|
+
return rows.map(rowToMemory);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Near-duplicate candidates for a fresh assert. `match` must be quoted. */
|
|
251
|
+
export function similar(
|
|
252
|
+
match: string,
|
|
253
|
+
kind: MemoryKind,
|
|
254
|
+
subject: string,
|
|
255
|
+
excludeId: number,
|
|
256
|
+
limit: number,
|
|
257
|
+
): MemoryRow[] {
|
|
258
|
+
const rows = getDatabase()
|
|
259
|
+
.prepare(memorySql.similar)
|
|
260
|
+
.all(match, kind, subject, excludeId, limit) as Row[];
|
|
261
|
+
return rows.map(rowToMemory);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function setSupersededBy(id: number, newId: number): void {
|
|
265
|
+
getDatabase().prepare(memorySql.setSupersededBy).run(newId, id);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function setDropped(id: number, at: number): void {
|
|
269
|
+
getDatabase().prepare(memorySql.setDropped).run(at, id);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function setPinned(id: number, pinned: boolean): void {
|
|
273
|
+
getDatabase()
|
|
274
|
+
.prepare(memorySql.setPinned)
|
|
275
|
+
.run(pinned ? 1 : 0, id);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function touch(id: number, at: number): void {
|
|
279
|
+
getDatabase().prepare(memorySql.touch).run(at, id);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Append one audit entry — every mutation writes exactly one. */
|
|
283
|
+
export function insertHistory(entry: {
|
|
284
|
+
memoryId: number;
|
|
285
|
+
op: MemoryOp;
|
|
286
|
+
beforeText?: string;
|
|
287
|
+
afterText?: string;
|
|
288
|
+
reason?: string;
|
|
289
|
+
at: number;
|
|
290
|
+
}): void {
|
|
291
|
+
getDatabase()
|
|
292
|
+
.prepare(memorySql.insertHistory)
|
|
293
|
+
.run(
|
|
294
|
+
entry.memoryId,
|
|
295
|
+
entry.op,
|
|
296
|
+
entry.beforeText ?? null,
|
|
297
|
+
entry.afterText ?? null,
|
|
298
|
+
entry.reason ?? null,
|
|
299
|
+
entry.at,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function historyFor(id: number): MemoryHistoryRow[] {
|
|
304
|
+
const rows = getDatabase()
|
|
305
|
+
.prepare(memorySql.historyFor)
|
|
306
|
+
.all(id) as HistoryRow[];
|
|
307
|
+
return rows.map(rowToHistory);
|
|
308
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- Statements for the memory tables (see repositories/memory-repo.ts for
|
|
2
|
+
-- the parameter order and row↔domain mapping). "Live" everywhere means
|
|
3
|
+
-- superseded_by IS NULL AND dropped_at IS NULL: a row that has been
|
|
4
|
+
-- replaced or dropped stays in the table (stable ids, revertible
|
|
5
|
+
-- history) but is invisible to listings and search.
|
|
6
|
+
--
|
|
7
|
+
-- The optional filters bind the same value twice — `(? IS NULL OR
|
|
8
|
+
-- col = ?)` — so the statement text is constant and nothing is
|
|
9
|
+
-- interpolated into SQL at the call site.
|
|
10
|
+
|
|
11
|
+
-- name: insert
|
|
12
|
+
-- RETURNING id so the caller gets the new row id without a second
|
|
13
|
+
-- round trip through last_insert_rowid().
|
|
14
|
+
INSERT INTO memory
|
|
15
|
+
(kind, subject, key, text, source_frontend, source_chat, source_actor,
|
|
16
|
+
source_turn, trust, confidence, created_at, last_seen_at, hit_count,
|
|
17
|
+
salience, pinned, superseded_by, dropped_at, content_hash)
|
|
18
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?)
|
|
19
|
+
RETURNING id
|
|
20
|
+
|
|
21
|
+
-- name: get
|
|
22
|
+
SELECT id, kind, subject, key, text, source_frontend, source_chat,
|
|
23
|
+
source_actor, source_turn, trust, confidence, created_at,
|
|
24
|
+
last_seen_at, hit_count, salience, pinned, superseded_by,
|
|
25
|
+
dropped_at, content_hash
|
|
26
|
+
FROM memory WHERE id = ?
|
|
27
|
+
|
|
28
|
+
-- name: list
|
|
29
|
+
SELECT id, kind, subject, key, text, source_frontend, source_chat,
|
|
30
|
+
source_actor, source_turn, trust, confidence, created_at,
|
|
31
|
+
last_seen_at, hit_count, salience, pinned, superseded_by,
|
|
32
|
+
dropped_at, content_hash
|
|
33
|
+
FROM memory
|
|
34
|
+
WHERE (? IS NULL OR kind = ?)
|
|
35
|
+
AND (? IS NULL OR subject = ?)
|
|
36
|
+
AND (? = 1 OR superseded_by IS NULL)
|
|
37
|
+
AND (? = 1 OR dropped_at IS NULL)
|
|
38
|
+
ORDER BY pinned DESC, salience DESC, last_seen_at DESC, id DESC
|
|
39
|
+
LIMIT ?
|
|
40
|
+
|
|
41
|
+
-- name: liveStateByKey
|
|
42
|
+
SELECT id, kind, subject, key, text, source_frontend, source_chat,
|
|
43
|
+
source_actor, source_turn, trust, confidence, created_at,
|
|
44
|
+
last_seen_at, hit_count, salience, pinned, superseded_by,
|
|
45
|
+
dropped_at, content_hash
|
|
46
|
+
FROM memory
|
|
47
|
+
WHERE kind = 'state' AND key = ?
|
|
48
|
+
AND superseded_by IS NULL AND dropped_at IS NULL
|
|
49
|
+
ORDER BY id DESC LIMIT 1
|
|
50
|
+
|
|
51
|
+
-- name: searchFts
|
|
52
|
+
-- The match param must already be a valid FTS5 expression
|
|
53
|
+
-- (see memory.ts ftsQuery). Live rows only, best match first.
|
|
54
|
+
SELECT m.id, m.kind, m.subject, m.key, m.text, m.source_frontend,
|
|
55
|
+
m.source_chat, m.source_actor, m.source_turn, m.trust, m.confidence,
|
|
56
|
+
m.created_at, m.last_seen_at, m.hit_count, m.salience, m.pinned,
|
|
57
|
+
m.superseded_by, m.dropped_at, m.content_hash
|
|
58
|
+
FROM memory m JOIN memory_fts ON memory_fts.rowid = m.id
|
|
59
|
+
WHERE memory_fts MATCH ?
|
|
60
|
+
AND m.superseded_by IS NULL AND m.dropped_at IS NULL
|
|
61
|
+
AND (? IS NULL OR m.kind = ?)
|
|
62
|
+
ORDER BY bm25(memory_fts) LIMIT ?
|
|
63
|
+
|
|
64
|
+
-- name: similar
|
|
65
|
+
-- Near-duplicate candidates for a fresh assert: live rows of the same
|
|
66
|
+
-- kind + subject that match the new text, the new row itself excluded.
|
|
67
|
+
SELECT m.id, m.kind, m.subject, m.key, m.text, m.source_frontend,
|
|
68
|
+
m.source_chat, m.source_actor, m.source_turn, m.trust, m.confidence,
|
|
69
|
+
m.created_at, m.last_seen_at, m.hit_count, m.salience, m.pinned,
|
|
70
|
+
m.superseded_by, m.dropped_at, m.content_hash
|
|
71
|
+
FROM memory m JOIN memory_fts ON memory_fts.rowid = m.id
|
|
72
|
+
WHERE memory_fts MATCH ?
|
|
73
|
+
AND m.kind = ? AND m.subject = ? AND m.id <> ?
|
|
74
|
+
AND m.superseded_by IS NULL AND m.dropped_at IS NULL
|
|
75
|
+
ORDER BY bm25(memory_fts) LIMIT ?
|
|
76
|
+
|
|
77
|
+
-- name: setSupersededBy
|
|
78
|
+
UPDATE memory SET superseded_by = ? WHERE id = ?
|
|
79
|
+
|
|
80
|
+
-- name: setDropped
|
|
81
|
+
UPDATE memory SET dropped_at = ? WHERE id = ?
|
|
82
|
+
|
|
83
|
+
-- name: setPinned
|
|
84
|
+
UPDATE memory SET pinned = ? WHERE id = ?
|
|
85
|
+
|
|
86
|
+
-- name: touch
|
|
87
|
+
UPDATE memory SET hit_count = hit_count + 1, last_seen_at = ? WHERE id = ?
|
|
88
|
+
|
|
89
|
+
-- name: insertHistory
|
|
90
|
+
INSERT INTO memory_history (memory_id, op, before_text, after_text, reason, at)
|
|
91
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
92
|
+
|
|
93
|
+
-- name: historyFor
|
|
94
|
+
SELECT id, memory_id, op, before_text, after_text, reason, at
|
|
95
|
+
FROM memory_history WHERE memory_id = ? ORDER BY id
|
|
@@ -52,6 +52,87 @@ CREATE TRIGGER IF NOT EXISTS history_au AFTER UPDATE OF text, sender_name ON his
|
|
|
52
52
|
VALUES (new.id, new.text, new.sender_name);
|
|
53
53
|
END;
|
|
54
54
|
|
|
55
|
+
-- Typed memory: one row per claim, with an FTS5 index over subject +
|
|
56
|
+
-- text. Kinds are lifecycles, not labels (docs/memory-persona-plan.md
|
|
57
|
+
-- §3.1): `directive` is durable human intent, `fact` is durable and
|
|
58
|
+
-- supersedable, `state` is keyed (a write replaces the row for that
|
|
59
|
+
-- key), `episode` decays fast, `relationship` and `reflection` are the
|
|
60
|
+
-- persona layer. Rows are never physically deleted — `superseded_by`
|
|
61
|
+
-- points at the replacement and `dropped_at` is the graveyard, so ids
|
|
62
|
+
-- stay stable and every change is revertible.
|
|
63
|
+
CREATE TABLE IF NOT EXISTS memory (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
-- directive | fact | state | episode | relationship | reflection
|
|
66
|
+
kind TEXT NOT NULL,
|
|
67
|
+
-- Who or what the claim is about; the retrieval scope.
|
|
68
|
+
subject TEXT NOT NULL,
|
|
69
|
+
-- Required for kind='state' (e.g. 'heartbeat.health'), NULL otherwise.
|
|
70
|
+
-- One live row per key is enforced by the store's replaceStateKey,
|
|
71
|
+
-- which supersedes the previous row inside a transaction.
|
|
72
|
+
key TEXT,
|
|
73
|
+
text TEXT NOT NULL,
|
|
74
|
+
-- Provenance: which frontend/chat/actor/turn asserted this.
|
|
75
|
+
source_frontend TEXT,
|
|
76
|
+
source_chat TEXT,
|
|
77
|
+
source_actor TEXT,
|
|
78
|
+
source_turn TEXT,
|
|
79
|
+
-- operator | agent | user_claim | group_chat — the trust tier.
|
|
80
|
+
-- user_claim and group_chat can never be pinned (plan §5).
|
|
81
|
+
trust TEXT NOT NULL,
|
|
82
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
83
|
+
created_at INTEGER NOT NULL,
|
|
84
|
+
last_seen_at INTEGER NOT NULL,
|
|
85
|
+
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
salience REAL NOT NULL DEFAULT 0,
|
|
87
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
88
|
+
-- memory.id of the row that replaced this one; NULL while live.
|
|
89
|
+
superseded_by INTEGER,
|
|
90
|
+
-- Soft delete: the graveyard, not oblivion.
|
|
91
|
+
dropped_at INTEGER,
|
|
92
|
+
-- sha256 of kind|subject|key|text — the idempotency key for import.
|
|
93
|
+
content_hash TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_memory_kind_subject ON memory(kind, subject);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_memory_key ON memory(key) WHERE key IS NOT NULL;
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_memory_salience ON memory(salience);
|
|
98
|
+
|
|
99
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
100
|
+
subject,
|
|
101
|
+
text,
|
|
102
|
+
content='memory',
|
|
103
|
+
content_rowid='id'
|
|
104
|
+
);
|
|
105
|
+
CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON memory BEGIN
|
|
106
|
+
INSERT INTO memory_fts(rowid, subject, text)
|
|
107
|
+
VALUES (new.id, new.subject, new.text);
|
|
108
|
+
END;
|
|
109
|
+
CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON memory BEGIN
|
|
110
|
+
INSERT INTO memory_fts(memory_fts, rowid, subject, text)
|
|
111
|
+
VALUES ('delete', old.id, old.subject, old.text);
|
|
112
|
+
END;
|
|
113
|
+
CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE OF subject, text ON memory BEGIN
|
|
114
|
+
INSERT INTO memory_fts(memory_fts, rowid, subject, text)
|
|
115
|
+
VALUES ('delete', old.id, old.subject, old.text);
|
|
116
|
+
INSERT INTO memory_fts(rowid, subject, text)
|
|
117
|
+
VALUES (new.id, new.subject, new.text);
|
|
118
|
+
END;
|
|
119
|
+
|
|
120
|
+
-- The audit log: one row per mutation, so every change is diffable and
|
|
121
|
+
-- revertible (`/memory diff`, `/memory undo`). Ops are the reconcile
|
|
122
|
+
-- vocabulary of plan §3.3 plus the store's own replace_state. A touch
|
|
123
|
+
-- (hit_count / last_seen_at) changes no content and is not audited.
|
|
124
|
+
CREATE TABLE IF NOT EXISTS memory_history (
|
|
125
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
126
|
+
memory_id INTEGER NOT NULL,
|
|
127
|
+
-- assert | supersede | drop | merge | pin | unpin | replace_state
|
|
128
|
+
op TEXT NOT NULL,
|
|
129
|
+
before_text TEXT,
|
|
130
|
+
after_text TEXT,
|
|
131
|
+
reason TEXT,
|
|
132
|
+
at INTEGER NOT NULL
|
|
133
|
+
);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS idx_memory_history_memory ON memory_history(memory_id, id);
|
|
135
|
+
|
|
55
136
|
-- Sessions: real columns rather than a JSON blob. The store's hot
|
|
56
137
|
-- paths (recordUsage, incrementTurns, setSessionId) accumulate into
|
|
57
138
|
-- individual fields per turn, and the usage counters are numeric
|
|
@@ -57,6 +57,87 @@ CREATE TRIGGER IF NOT EXISTS history_au AFTER UPDATE OF text, sender_name ON his
|
|
|
57
57
|
VALUES (new.id, new.text, new.sender_name);
|
|
58
58
|
END;
|
|
59
59
|
|
|
60
|
+
-- Typed memory: one row per claim, with an FTS5 index over subject +
|
|
61
|
+
-- text. Kinds are lifecycles, not labels (docs/memory-persona-plan.md
|
|
62
|
+
-- §3.1): \`directive\` is durable human intent, \`fact\` is durable and
|
|
63
|
+
-- supersedable, \`state\` is keyed (a write replaces the row for that
|
|
64
|
+
-- key), \`episode\` decays fast, \`relationship\` and \`reflection\` are the
|
|
65
|
+
-- persona layer. Rows are never physically deleted — \`superseded_by\`
|
|
66
|
+
-- points at the replacement and \`dropped_at\` is the graveyard, so ids
|
|
67
|
+
-- stay stable and every change is revertible.
|
|
68
|
+
CREATE TABLE IF NOT EXISTS memory (
|
|
69
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
+
-- directive | fact | state | episode | relationship | reflection
|
|
71
|
+
kind TEXT NOT NULL,
|
|
72
|
+
-- Who or what the claim is about; the retrieval scope.
|
|
73
|
+
subject TEXT NOT NULL,
|
|
74
|
+
-- Required for kind='state' (e.g. 'heartbeat.health'), NULL otherwise.
|
|
75
|
+
-- One live row per key is enforced by the store's replaceStateKey,
|
|
76
|
+
-- which supersedes the previous row inside a transaction.
|
|
77
|
+
key TEXT,
|
|
78
|
+
text TEXT NOT NULL,
|
|
79
|
+
-- Provenance: which frontend/chat/actor/turn asserted this.
|
|
80
|
+
source_frontend TEXT,
|
|
81
|
+
source_chat TEXT,
|
|
82
|
+
source_actor TEXT,
|
|
83
|
+
source_turn TEXT,
|
|
84
|
+
-- operator | agent | user_claim | group_chat — the trust tier.
|
|
85
|
+
-- user_claim and group_chat can never be pinned (plan §5).
|
|
86
|
+
trust TEXT NOT NULL,
|
|
87
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
88
|
+
created_at INTEGER NOT NULL,
|
|
89
|
+
last_seen_at INTEGER NOT NULL,
|
|
90
|
+
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
91
|
+
salience REAL NOT NULL DEFAULT 0,
|
|
92
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
93
|
+
-- memory.id of the row that replaced this one; NULL while live.
|
|
94
|
+
superseded_by INTEGER,
|
|
95
|
+
-- Soft delete: the graveyard, not oblivion.
|
|
96
|
+
dropped_at INTEGER,
|
|
97
|
+
-- sha256 of kind|subject|key|text — the idempotency key for import.
|
|
98
|
+
content_hash TEXT NOT NULL
|
|
99
|
+
);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_memory_kind_subject ON memory(kind, subject);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_memory_key ON memory(key) WHERE key IS NOT NULL;
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_memory_salience ON memory(salience);
|
|
103
|
+
|
|
104
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
105
|
+
subject,
|
|
106
|
+
text,
|
|
107
|
+
content='memory',
|
|
108
|
+
content_rowid='id'
|
|
109
|
+
);
|
|
110
|
+
CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON memory BEGIN
|
|
111
|
+
INSERT INTO memory_fts(rowid, subject, text)
|
|
112
|
+
VALUES (new.id, new.subject, new.text);
|
|
113
|
+
END;
|
|
114
|
+
CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON memory BEGIN
|
|
115
|
+
INSERT INTO memory_fts(memory_fts, rowid, subject, text)
|
|
116
|
+
VALUES ('delete', old.id, old.subject, old.text);
|
|
117
|
+
END;
|
|
118
|
+
CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE OF subject, text ON memory BEGIN
|
|
119
|
+
INSERT INTO memory_fts(memory_fts, rowid, subject, text)
|
|
120
|
+
VALUES ('delete', old.id, old.subject, old.text);
|
|
121
|
+
INSERT INTO memory_fts(rowid, subject, text)
|
|
122
|
+
VALUES (new.id, new.subject, new.text);
|
|
123
|
+
END;
|
|
124
|
+
|
|
125
|
+
-- The audit log: one row per mutation, so every change is diffable and
|
|
126
|
+
-- revertible (\`/memory diff\`, \`/memory undo\`). Ops are the reconcile
|
|
127
|
+
-- vocabulary of plan §3.3 plus the store's own replace_state. A touch
|
|
128
|
+
-- (hit_count / last_seen_at) changes no content and is not audited.
|
|
129
|
+
CREATE TABLE IF NOT EXISTS memory_history (
|
|
130
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
131
|
+
memory_id INTEGER NOT NULL,
|
|
132
|
+
-- assert | supersede | drop | merge | pin | unpin | replace_state
|
|
133
|
+
op TEXT NOT NULL,
|
|
134
|
+
before_text TEXT,
|
|
135
|
+
after_text TEXT,
|
|
136
|
+
reason TEXT,
|
|
137
|
+
at INTEGER NOT NULL
|
|
138
|
+
);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_memory_history_memory ON memory_history(memory_id, id);
|
|
140
|
+
|
|
60
141
|
-- Sessions: real columns rather than a JSON blob. The store's hot
|
|
61
142
|
-- paths (recordUsage, incrementTurns, setSessionId) accumulate into
|
|
62
143
|
-- individual fields per turn, and the usage counters are numeric
|
|
@@ -488,6 +569,71 @@ ORDER BY timestamp ASC, rowid ASC LIMIT 1`,
|
|
|
488
569
|
countByFilePath: `SELECT COUNT(*) AS n FROM media_index WHERE file_path = ?`,
|
|
489
570
|
} as const;
|
|
490
571
|
|
|
572
|
+
export const memorySql = {
|
|
573
|
+
insert: `-- RETURNING id so the caller gets the new row id without a second
|
|
574
|
+
-- round trip through last_insert_rowid().
|
|
575
|
+
INSERT INTO memory
|
|
576
|
+
(kind, subject, key, text, source_frontend, source_chat, source_actor,
|
|
577
|
+
source_turn, trust, confidence, created_at, last_seen_at, hit_count,
|
|
578
|
+
salience, pinned, superseded_by, dropped_at, content_hash)
|
|
579
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?)
|
|
580
|
+
RETURNING id`,
|
|
581
|
+
get: `SELECT id, kind, subject, key, text, source_frontend, source_chat,
|
|
582
|
+
source_actor, source_turn, trust, confidence, created_at,
|
|
583
|
+
last_seen_at, hit_count, salience, pinned, superseded_by,
|
|
584
|
+
dropped_at, content_hash
|
|
585
|
+
FROM memory WHERE id = ?`,
|
|
586
|
+
list: `SELECT id, kind, subject, key, text, source_frontend, source_chat,
|
|
587
|
+
source_actor, source_turn, trust, confidence, created_at,
|
|
588
|
+
last_seen_at, hit_count, salience, pinned, superseded_by,
|
|
589
|
+
dropped_at, content_hash
|
|
590
|
+
FROM memory
|
|
591
|
+
WHERE (? IS NULL OR kind = ?)
|
|
592
|
+
AND (? IS NULL OR subject = ?)
|
|
593
|
+
AND (? = 1 OR superseded_by IS NULL)
|
|
594
|
+
AND (? = 1 OR dropped_at IS NULL)
|
|
595
|
+
ORDER BY pinned DESC, salience DESC, last_seen_at DESC, id DESC
|
|
596
|
+
LIMIT ?`,
|
|
597
|
+
liveStateByKey: `SELECT id, kind, subject, key, text, source_frontend, source_chat,
|
|
598
|
+
source_actor, source_turn, trust, confidence, created_at,
|
|
599
|
+
last_seen_at, hit_count, salience, pinned, superseded_by,
|
|
600
|
+
dropped_at, content_hash
|
|
601
|
+
FROM memory
|
|
602
|
+
WHERE kind = 'state' AND key = ?
|
|
603
|
+
AND superseded_by IS NULL AND dropped_at IS NULL
|
|
604
|
+
ORDER BY id DESC LIMIT 1`,
|
|
605
|
+
searchFts: `-- The match param must already be a valid FTS5 expression
|
|
606
|
+
-- (see memory.ts ftsQuery). Live rows only, best match first.
|
|
607
|
+
SELECT m.id, m.kind, m.subject, m.key, m.text, m.source_frontend,
|
|
608
|
+
m.source_chat, m.source_actor, m.source_turn, m.trust, m.confidence,
|
|
609
|
+
m.created_at, m.last_seen_at, m.hit_count, m.salience, m.pinned,
|
|
610
|
+
m.superseded_by, m.dropped_at, m.content_hash
|
|
611
|
+
FROM memory m JOIN memory_fts ON memory_fts.rowid = m.id
|
|
612
|
+
WHERE memory_fts MATCH ?
|
|
613
|
+
AND m.superseded_by IS NULL AND m.dropped_at IS NULL
|
|
614
|
+
AND (? IS NULL OR m.kind = ?)
|
|
615
|
+
ORDER BY bm25(memory_fts) LIMIT ?`,
|
|
616
|
+
similar: `-- Near-duplicate candidates for a fresh assert: live rows of the same
|
|
617
|
+
-- kind + subject that match the new text, the new row itself excluded.
|
|
618
|
+
SELECT m.id, m.kind, m.subject, m.key, m.text, m.source_frontend,
|
|
619
|
+
m.source_chat, m.source_actor, m.source_turn, m.trust, m.confidence,
|
|
620
|
+
m.created_at, m.last_seen_at, m.hit_count, m.salience, m.pinned,
|
|
621
|
+
m.superseded_by, m.dropped_at, m.content_hash
|
|
622
|
+
FROM memory m JOIN memory_fts ON memory_fts.rowid = m.id
|
|
623
|
+
WHERE memory_fts MATCH ?
|
|
624
|
+
AND m.kind = ? AND m.subject = ? AND m.id <> ?
|
|
625
|
+
AND m.superseded_by IS NULL AND m.dropped_at IS NULL
|
|
626
|
+
ORDER BY bm25(memory_fts) LIMIT ?`,
|
|
627
|
+
setSupersededBy: `UPDATE memory SET superseded_by = ? WHERE id = ?`,
|
|
628
|
+
setDropped: `UPDATE memory SET dropped_at = ? WHERE id = ?`,
|
|
629
|
+
setPinned: `UPDATE memory SET pinned = ? WHERE id = ?`,
|
|
630
|
+
touch: `UPDATE memory SET hit_count = hit_count + 1, last_seen_at = ? WHERE id = ?`,
|
|
631
|
+
insertHistory: `INSERT INTO memory_history (memory_id, op, before_text, after_text, reason, at)
|
|
632
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
633
|
+
historyFor: `SELECT id, memory_id, op, before_text, after_text, reason, at
|
|
634
|
+
FROM memory_history WHERE memory_id = ? ORDER BY id`,
|
|
635
|
+
} as const;
|
|
636
|
+
|
|
491
637
|
export const scriptsSql = {
|
|
492
638
|
upsert: `INSERT OR REPLACE INTO scripts
|
|
493
639
|
(id, name, description, language, script_path, created_at,
|