Repository navigation
Expand file tree
/
Copy pathschema.sql
More file actions
680 lines (604 loc) · 28.5 KB
/
Copy pathschema.sql
File metadata and controls
680 lines (604 loc) · 28.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT OR REPLACE INTO meta(key, value) VALUES ('schema_version', '1');
CREATE TABLE IF NOT EXISTS souls (
name TEXT PRIMARY KEY,
file_path TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0,
description TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_souls_enabled ON souls(enabled, sort_order);
CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
ts TEXT NOT NULL,
content TEXT NOT NULL,
importance REAL DEFAULT 0.5,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_posts_ts ON posts(ts DESC);
CREATE INDEX IF NOT EXISTS idx_posts_importance ON posts(importance DESC);
CREATE TABLE IF NOT EXISTS calendar_accounts (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
display_name TEXT NOT NULL,
created_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS schedule_events (
id TEXT PRIMARY KEY,
account_id TEXT,
subject TEXT NOT NULL DEFAULT '',
body_preview TEXT,
start_ts REAL NOT NULL,
end_ts REAL NOT NULL,
start_local TEXT NOT NULL,
end_local TEXT NOT NULL,
all_day INTEGER NOT NULL DEFAULT 0,
location TEXT,
web_link TEXT,
series_master_id TEXT,
is_cancelled INTEGER NOT NULL DEFAULT 0,
change_key TEXT,
synced_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_schedule_events_start ON schedule_events(start_ts);
CREATE INDEX IF NOT EXISTS idx_schedule_events_account ON schedule_events(account_id);
CREATE TABLE IF NOT EXISTS post_soul_orders (
post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
soul_name TEXT NOT NULL REFERENCES souls(name) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at REAL NOT NULL,
PRIMARY KEY (post_id, soul_name)
);
CREATE INDEX IF NOT EXISTS idx_post_soul_orders_post_order
ON post_soul_orders(post_id, sort_order, soul_name);
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
content,
content='posts',
content_rowid='rowid'
);
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts_trigram USING fts5(
content,
tokenize='trigram',
content='posts',
content_rowid='rowid'
);
CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN
INSERT INTO posts_fts(rowid, content) VALUES (new.rowid, new.content);
INSERT INTO posts_fts_trigram(rowid, content) VALUES (new.rowid, new.content);
END;
CREATE TRIGGER IF NOT EXISTS posts_ad AFTER DELETE ON posts BEGIN
INSERT INTO posts_fts(posts_fts, rowid, content)
VALUES ('delete', old.rowid, old.content);
INSERT INTO posts_fts_trigram(posts_fts_trigram, rowid, content)
VALUES ('delete', old.rowid, old.content);
END;
CREATE TRIGGER IF NOT EXISTS posts_au AFTER UPDATE ON posts BEGIN
INSERT INTO posts_fts(posts_fts, rowid, content)
VALUES ('delete', old.rowid, old.content);
INSERT INTO posts_fts(rowid, content) VALUES (new.rowid, new.content);
INSERT INTO posts_fts_trigram(posts_fts_trigram, rowid, content)
VALUES ('delete', old.rowid, old.content);
INSERT INTO posts_fts_trigram(rowid, content) VALUES (new.rowid, new.content);
END;
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
soul_name TEXT NOT NULL REFERENCES souls(name) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'assistant' CHECK(role IN ('assistant', 'user')),
content TEXT NOT NULL,
seq INTEGER NOT NULL DEFAULT 0,
metadata TEXT,
created_at REAL NOT NULL,
edited_at REAL,
rerun_at REAL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_comments_conversation_seq
ON comments(post_id, soul_name, seq);
CREATE INDEX IF NOT EXISTS idx_comments_post_soul
ON comments(post_id, soul_name, seq);
CREATE INDEX IF NOT EXISTS idx_comments_soul_created
ON comments(soul_name, created_at DESC);
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
file_path TEXT NOT NULL,
mime_type TEXT NOT NULL CHECK(mime_type IN ('image/jpeg', 'image/png')),
file_size INTEGER NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
sha256 TEXT NOT NULL,
original_filename TEXT,
linked_at REAL,
created_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_attachments_sha256 ON attachments(sha256);
CREATE INDEX IF NOT EXISTS idx_attachments_linked_created ON attachments(linked_at, created_at);
CREATE TABLE IF NOT EXISTS vision_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
attachment_id TEXT NOT NULL REFERENCES attachments(id) ON DELETE CASCADE,
model TEXT NOT NULL,
prompt_version TEXT NOT NULL,
description TEXT,
visible_text TEXT,
uncertainties TEXT,
status TEXT NOT NULL CHECK(status IN ('ok', 'failed', 'skipped')),
error TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL,
UNIQUE(attachment_id, model, prompt_version)
);
CREATE INDEX IF NOT EXISTS idx_vision_cache_attachment
ON vision_cache(attachment_id);
CREATE INDEX IF NOT EXISTS idx_vision_cache_status
ON vision_cache(status, updated_at);
CREATE TABLE IF NOT EXISTS post_attachments (
post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
attachment_id TEXT NOT NULL REFERENCES attachments(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (post_id, attachment_id)
);
CREATE INDEX IF NOT EXISTS idx_post_attachments_post
ON post_attachments(post_id, sort_order);
CREATE TABLE IF NOT EXISTS comment_attachments (
comment_id INTEGER NOT NULL REFERENCES comments(id) ON DELETE CASCADE,
attachment_id TEXT NOT NULL REFERENCES attachments(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (comment_id, attachment_id)
);
CREATE INDEX IF NOT EXISTS idx_comment_attachments_comment
ON comment_attachments(comment_id, sort_order);
CREATE TABLE IF NOT EXISTS chat_message_attachments (
message_id INTEGER NOT NULL REFERENCES chat_messages(id) ON DELETE CASCADE,
attachment_id TEXT NOT NULL REFERENCES attachments(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (message_id, attachment_id)
);
CREATE INDEX IF NOT EXISTS idx_chat_message_attachments_message
ON chat_message_attachments(message_id, sort_order);
CREATE TABLE IF NOT EXISTS chat_threads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
soul_name TEXT NOT NULL REFERENCES souls(name) ON DELETE CASCADE,
title TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL,
last_message_at REAL,
-- Unread is derived, never stored: assistant messages created after this
-- watermark are unread. A counter column would drift the moment a message
-- is edited, rerun or truncated, all of which this schema allows.
last_read_at REAL
);
CREATE INDEX IF NOT EXISTS idx_chat_threads_soul ON chat_threads(soul_name, last_message_at DESC);
CREATE TABLE IF NOT EXISTS chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
thread_id INTEGER NOT NULL REFERENCES chat_threads(id) ON DELETE CASCADE,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at REAL NOT NULL,
edited_at REAL,
rerun_at REAL,
metadata TEXT,
client_request_id TEXT
);
CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id, created_at);
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_request_role
ON chat_messages(thread_id, client_request_id, role)
WHERE client_request_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS soul_letters (
message_id INTEGER PRIMARY KEY REFERENCES chat_messages(id) ON DELETE CASCADE,
sent_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_soul_letters_sent
ON soul_letters(sent_at DESC);
CREATE TABLE IF NOT EXISTS soul_message_sources (
message_id INTEGER NOT NULL REFERENCES soul_letters(message_id) ON DELETE CASCADE,
post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
PRIMARY KEY (message_id, post_id)
);
CREATE INDEX IF NOT EXISTS idx_soul_message_sources_post
ON soul_message_sources(post_id);
CREATE TABLE IF NOT EXISTS goals (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
detail TEXT,
horizon TEXT NOT NULL CHECK(horizon IN ('short', 'long')),
status TEXT NOT NULL DEFAULT 'active'
CHECK(status IN ('active', 'done', 'abandoned', 'paused')),
source TEXT NOT NULL DEFAULT 'user'
CHECK(source IN ('user', 'suggested_accepted')),
focus INTEGER NOT NULL DEFAULT 0 CHECK(focus IN (0, 1)),
last_progress_at REAL,
schedule_expectation TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_goals_status_horizon
ON goals(status, horizon, id);
CREATE TABLE IF NOT EXISTS goal_schedule_links (
goal_id TEXT NOT NULL,
event_id TEXT NOT NULL,
created_at REAL NOT NULL,
PRIMARY KEY (goal_id, event_id)
);
CREATE TABLE IF NOT EXISTS goal_schedule_assessments (
event_id TEXT PRIMARY KEY,
assessed_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS goal_activities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
kind TEXT NOT NULL
CHECK(kind IN ('commitment','progress','blocked','milestone','scheduled')),
source TEXT NOT NULL CHECK(source IN ('auto','manual','schedule')),
evidence_ref TEXT NOT NULL, -- post:{id} | comment:{id} | schedule:{event_id}
evidence_span TEXT, -- 引用的原文片段;source=schedule 时为 NULL
confidence REAL, -- source=manual/schedule 时为 NULL
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','rejected')),
created_at REAL NOT NULL,
decided_at REAL, -- 置 rejected 的时刻
UNIQUE(goal_id, evidence_ref)
);
CREATE INDEX IF NOT EXISTS idx_goal_activities_goal
ON goal_activities(goal_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_goal_activities_ref
ON goal_activities(evidence_ref);
CREATE TABLE IF NOT EXISTS suggestions (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL CHECK(kind IN ('goal', 'schedule')),
payload_json TEXT NOT NULL,
evidence_ref TEXT,
confidence REAL NOT NULL DEFAULT 0.6 CHECK(confidence >= 0.0 AND confidence <= 1.0),
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending', 'accepted', 'dismissed')),
normalized_key TEXT,
created_at REAL NOT NULL,
decided_at REAL
);
CREATE INDEX IF NOT EXISTS idx_suggestions_kind_status
ON suggestions(kind, status, id);
CREATE INDEX IF NOT EXISTS idx_suggestions_normkey
ON suggestions(normalized_key);
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
status TEXT NOT NULL,
payload_json TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 1,
error TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL,
started_at REAL,
finished_at REAL
);
CREATE INDEX IF NOT EXISTS idx_jobs_status_created
ON jobs(status, created_at);
CREATE INDEX IF NOT EXISTS idx_jobs_type_status
ON jobs(type, status);
CREATE TABLE IF NOT EXISTS post_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
job_id INTEGER REFERENCES jobs(id) ON DELETE SET NULL,
event_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_post_events_post_id
ON post_events(post_id, id);
CREATE TABLE IF NOT EXISTS vector_docs (
doc_id TEXT PRIMARY KEY,
doc_type TEXT NOT NULL,
source_table TEXT NOT NULL,
source_id TEXT NOT NULL,
content TEXT NOT NULL,
content_hash TEXT NOT NULL,
metadata_json TEXT NOT NULL,
source_revision INTEGER NOT NULL,
updated_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_vector_docs_revision
ON vector_docs(source_revision);
CREATE INDEX IF NOT EXISTS idx_vector_docs_source
ON vector_docs(source_table, source_id);
CREATE TABLE IF NOT EXISTS vector_doc_tombstones (
doc_id TEXT PRIMARY KEY,
deleted_revision INTEGER NOT NULL,
deleted_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_vector_doc_tombstones_revision
ON vector_doc_tombstones(deleted_revision);
CREATE TABLE IF NOT EXISTS evidence_feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel TEXT NOT NULL,
message_id INTEGER NOT NULL,
doc_id TEXT NOT NULL,
verdict TEXT NOT NULL DEFAULT 'irrelevant',
created_at REAL NOT NULL,
UNIQUE(channel, message_id, doc_id)
);
CREATE TABLE IF NOT EXISTS vector_index_collections (
collection_name TEXT PRIMARY KEY,
embedding_config_hash TEXT NOT NULL,
embedding_model TEXT NOT NULL,
embedding_base_url TEXT NOT NULL,
synced_revision INTEGER NOT NULL DEFAULT 0,
ready INTEGER NOT NULL DEFAULT 0,
last_audited_at REAL,
audit_status TEXT NOT NULL DEFAULT 'unknown',
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS vector_index_items (
collection_name TEXT NOT NULL REFERENCES vector_index_collections(collection_name) ON DELETE CASCADE,
doc_id TEXT NOT NULL,
content_hash TEXT NOT NULL,
source_revision INTEGER NOT NULL,
indexed_at REAL NOT NULL,
dim INTEGER,
embedding BLOB,
PRIMARY KEY (collection_name, doc_id)
);
CREATE INDEX IF NOT EXISTS idx_vector_index_items_doc
ON vector_index_items(doc_id);
CREATE TABLE IF NOT EXISTS vector_outbox (
id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_name TEXT NOT NULL REFERENCES vector_index_collections(collection_name) ON DELETE CASCADE,
doc_id TEXT NOT NULL,
op TEXT NOT NULL CHECK(op IN ('upsert', 'delete')),
target_hash TEXT,
source_revision INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'succeeded', 'failed')),
attempts INTEGER NOT NULL DEFAULT 0,
error TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL,
finished_at REAL
);
CREATE INDEX IF NOT EXISTS idx_vector_outbox_collection_status
ON vector_outbox(collection_name, status, id);
CREATE INDEX IF NOT EXISTS idx_vector_outbox_doc_status
ON vector_outbox(collection_name, doc_id, status);
-- ---------------------------------------------------------------------------
-- memory v2: append-only evidence event ledger
-- Every create/edit/rerun/delete on a business row (post/comment/chat) appends
-- an immutable evidence event in the SAME transaction. memory units bind to
-- these event versions (not to mutable source rows), so edits never silently
-- rewrite history. `id` is the monotonic consumption cursor for reconcile.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS memory_ingest_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_scope TEXT NOT NULL, -- 'global' | 'soul:<name>'
visibility_scope TEXT NOT NULL, -- 'public' | 'thread:<post_id>' | 'private:soul:<name>'
source_channel TEXT NOT NULL
CHECK(source_channel IN ('post','comment','chat')),
source_type TEXT NOT NULL
CHECK(source_type IN ('post','post_vision','comment_message','comment_relationship','chat_message')),
source_id TEXT NOT NULL,
source_revision INTEGER NOT NULL, -- monotonic from 1 per source_id
op TEXT NOT NULL
CHECK(op IN ('create','edit','rerun','delete')),
author TEXT -- 'user' | 'assistant' | NULL(unknown)
CHECK(author IS NULL OR author IN ('user','assistant')),
content_snapshot TEXT, -- version at the time; delete may be NULL
content_hash TEXT, -- sha256(content_snapshot)
occurred_at REAL NOT NULL, -- business action time
created_at REAL NOT NULL, -- ledger insert time
UNIQUE(source_type, source_id, source_revision)
);
CREATE INDEX IF NOT EXISTS idx_memory_events_boundary_id
ON memory_ingest_events(owner_scope, visibility_scope, id);
CREATE INDEX IF NOT EXISTS idx_memory_events_source
ON memory_ingest_events(source_type, source_id, source_revision);
CREATE TABLE IF NOT EXISTS memory_reconcile_cursors (
owner_scope TEXT NOT NULL,
visibility_scope TEXT NOT NULL,
last_event_id INTEGER NOT NULL DEFAULT 0,
updated_at REAL NOT NULL,
PRIMARY KEY(owner_scope, visibility_scope)
);
-- ---------------------------------------------------------------------------
-- memory v2: structured belief layer (memory units) + audit + view objects
-- A memory unit is a first-class cross-evidence belief: stable id, confidence,
-- evidence chain, status, time. Reconcile writes units (not md). owner_scope =
-- who manages it; visibility_scope = where it may be used (orthogonal). CHECK
-- enums are written in full now (SQLite cannot ALTER a CHECK) to avoid future
-- table rebuilds.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS memory_units (
id TEXT PRIMARY KEY, -- mu_<ulid>
owner_scope TEXT NOT NULL, -- 'global' | 'soul:<name>'
visibility_scope TEXT NOT NULL, -- 'public' | 'thread:<post_id>' | 'private:soul:<name>'
source_channel TEXT NOT NULL
CHECK(source_channel IN ('post','comment','chat','user')),
prompt_policy TEXT NOT NULL DEFAULT 'allow'
CHECK(prompt_policy IN ('allow','no_prompt')),
type TEXT NOT NULL, -- identity/preference/goal/state/relationship/insight/freeform
content TEXT NOT NULL, -- cross-evidence abstraction, NOT a single raw transcription
confidence REAL NOT NULL DEFAULT 0.6,
source TEXT NOT NULL DEFAULT 'reflected'
CHECK(source IN ('reflected','user_authored')),
status TEXT NOT NULL DEFAULT 'active'
CHECK(status IN ('active','pending','dormant',
'retracted_by_model','retracted_by_user',
'superseded','challenged')),
retraction_reason TEXT
CHECK(retraction_reason IS NULL OR
retraction_reason IN ('false','outdated')),
tier TEXT NOT NULL DEFAULT 'contextual'
CHECK(tier IN ('core','contextual','episodic')),
portrait_policy TEXT NOT NULL DEFAULT 'auto'
CHECK(portrait_policy IN ('auto','force_include','force_exclude')),
importance REAL NOT NULL DEFAULT 0.5,
sensitivity TEXT NOT NULL DEFAULT 'normal'
CHECK(sensitivity IN ('high','normal','low')),
in_portrait INTEGER NOT NULL DEFAULT 0, -- selector result cache
normalized_claim TEXT, -- canonical assertion: tombstone dedup + linker key
superseded_by TEXT REFERENCES memory_units(id) ON DELETE SET NULL,
-- set when other-bucket evidence contradicts this unit (P1). The mark is
-- attribution-free by design: read paths hedge the unit ("不太确定") and the
-- portrait excludes it, but nothing user- or model-visible says WHY. Fresh
-- same-bucket evidence (confirm/revise) clears it.
contested_at REAL,
first_seen REAL NOT NULL,
last_confirmed REAL NOT NULL,
retrieval_count INTEGER NOT NULL DEFAULT 0, -- reserved for later phases
metadata TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_units_boundary_status
ON memory_units(owner_scope, visibility_scope, status, prompt_policy);
CREATE INDEX IF NOT EXISTS idx_units_portrait
ON memory_units(owner_scope, visibility_scope, in_portrait, status);
-- FTS5 over unit content (keyword side of memory-v2 unit retrieval). Mirrors the
-- posts_fts pair: a default-tokenizer table for non-CJK and a trigram table for
-- CJK substring matching. External-content, keyed on the (implicit) integer rowid.
-- Triggers keep it in sync; a pre-existing DB seeds its historical rows once with
-- INSERT INTO memory_units_fts(memory_units_fts) VALUES('rebuild') (+ the trigram table).
CREATE VIRTUAL TABLE IF NOT EXISTS memory_units_fts USING fts5(
content,
content='memory_units',
content_rowid='rowid'
);
CREATE VIRTUAL TABLE IF NOT EXISTS memory_units_fts_trigram USING fts5(
content,
tokenize='trigram',
content='memory_units',
content_rowid='rowid'
);
CREATE TRIGGER IF NOT EXISTS memory_units_ai AFTER INSERT ON memory_units BEGIN
INSERT INTO memory_units_fts(rowid, content) VALUES (new.rowid, new.content);
INSERT INTO memory_units_fts_trigram(rowid, content) VALUES (new.rowid, new.content);
END;
CREATE TRIGGER IF NOT EXISTS memory_units_ad AFTER DELETE ON memory_units BEGIN
INSERT INTO memory_units_fts(memory_units_fts, rowid, content)
VALUES ('delete', old.rowid, old.content);
INSERT INTO memory_units_fts_trigram(memory_units_fts_trigram, rowid, content)
VALUES ('delete', old.rowid, old.content);
END;
CREATE TRIGGER IF NOT EXISTS memory_units_au AFTER UPDATE ON memory_units BEGIN
INSERT INTO memory_units_fts(memory_units_fts, rowid, content)
VALUES ('delete', old.rowid, old.content);
INSERT INTO memory_units_fts(rowid, content) VALUES (new.rowid, new.content);
INSERT INTO memory_units_fts_trigram(memory_units_fts_trigram, rowid, content)
VALUES ('delete', old.rowid, old.content);
INSERT INTO memory_units_fts_trigram(rowid, content) VALUES (new.rowid, new.content);
END;
-- Cross-bucket relations between units (P1: link, never merge). Buckets stay
-- isolated — a link is metadata about two beliefs, it never moves content or
-- rewrites either side. same_fact drives read-time folding (inject one copy in
-- private scenes); contradicts marks the more-public side contested (hedged,
-- out of portrait); context_variant records "both true, different contexts" —
-- deliberately NOT a contradiction. Pairs are stored with a_unit_id < b_unit_id.
CREATE TABLE IF NOT EXISTS memory_unit_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
a_unit_id TEXT NOT NULL REFERENCES memory_units(id) ON DELETE CASCADE,
b_unit_id TEXT NOT NULL REFERENCES memory_units(id) ON DELETE CASCADE,
relation TEXT NOT NULL
CHECK(relation IN ('same_fact','contradicts','context_variant')),
created_by TEXT NOT NULL DEFAULT 'linker', -- 'linker' | 'user'
created_at REAL NOT NULL,
UNIQUE(a_unit_id, b_unit_id, relation)
);
CREATE INDEX IF NOT EXISTS idx_unit_links_a ON memory_unit_links(a_unit_id);
CREATE INDEX IF NOT EXISTS idx_unit_links_b ON memory_unit_links(b_unit_id);
CREATE TABLE IF NOT EXISTS memory_unit_evidence (
unit_id TEXT NOT NULL REFERENCES memory_units(id) ON DELETE CASCADE,
event_id INTEGER NOT NULL REFERENCES memory_ingest_events(id) ON DELETE RESTRICT,
relation TEXT NOT NULL DEFAULT 'supports'
CHECK(relation IN ('supports','contradicts','revises','source')),
-- 1 while awaiting AI re-link after a user edit: the link is NOT counted as
-- current support and does NOT trigger challenge until the judge keeps/drops it.
review_pending INTEGER NOT NULL DEFAULT 0,
created_at REAL NOT NULL,
PRIMARY KEY (unit_id, event_id, relation)
);
CREATE INDEX IF NOT EXISTS idx_unit_evidence_event ON memory_unit_evidence(event_id);
CREATE TABLE IF NOT EXISTS memory_unit_reconcile_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
unit_id TEXT NOT NULL REFERENCES memory_units(id) ON DELETE CASCADE,
trigger_event_id INTEGER NOT NULL REFERENCES memory_ingest_events(id) ON DELETE RESTRICT,
reason TEXT NOT NULL CHECK(reason IN ('edit','delete')),
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending','resolved')),
created_at REAL NOT NULL,
resolved_at REAL,
UNIQUE(unit_id, trigger_event_id)
);
CREATE INDEX IF NOT EXISTS idx_unit_reconcile_queue_pending
ON memory_unit_reconcile_queue(status, unit_id, id);
CREATE INDEX IF NOT EXISTS idx_unit_reconcile_queue_trigger
ON memory_unit_reconcile_queue(trigger_event_id, status);
-- Dedicated queue for the AI re-link pass after a user edits a unit. Separate
-- from memory_unit_reconcile_queue because re-link is a different task (narrow
-- "does this evidence still support the new content" judgment, no trigger event,
-- no challenge decision). unit_version records unit.updated_at at enqueue time so
-- a stale judge result from before a newer edit cannot overwrite the new content.
CREATE TABLE IF NOT EXISTS memory_unit_relink_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
unit_id TEXT NOT NULL REFERENCES memory_units(id) ON DELETE CASCADE,
unit_version REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending','resolved')),
created_at REAL NOT NULL,
resolved_at REAL
);
CREATE INDEX IF NOT EXISTS idx_unit_relink_queue_pending
ON memory_unit_relink_queue(status, unit_id, id);
CREATE TABLE IF NOT EXISTS memory_reconcile_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_type TEXT NOT NULL,
owner_scope TEXT NOT NULL,
visibility_scope TEXT NOT NULL,
trigger TEXT NOT NULL,
event_id_start INTEGER,
event_id_end INTEGER,
event_count INTEGER NOT NULL DEFAULT 0,
summary TEXT NOT NULL DEFAULT '',
metadata_json TEXT,
created_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_memory_reconcile_runs_boundary
ON memory_reconcile_runs(owner_scope, visibility_scope, id DESC);
CREATE INDEX IF NOT EXISTS idx_memory_reconcile_runs_created
ON memory_reconcile_runs(created_at DESC);
CREATE TABLE IF NOT EXISTS memory_unit_ops (
id INTEGER PRIMARY KEY AUTOINCREMENT,
unit_id TEXT NOT NULL,
related_unit_id TEXT,
op TEXT NOT NULL, -- add/challenge/retain/confirm/revise/retract/supersede/
-- user_create/user_edit/user_delete
actor TEXT NOT NULL, -- 'reconciler' | 'user'
before_json TEXT,
after_json TEXT,
reconcile_run_id INTEGER REFERENCES memory_reconcile_runs(id) ON DELETE SET NULL,
created_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_unit_ops_unit ON memory_unit_ops(unit_id, id);
CREATE INDEX IF NOT EXISTS idx_unit_ops_reconcile_run ON memory_unit_ops(reconcile_run_id);
CREATE TABLE IF NOT EXISTS memory_views (
id TEXT PRIMARY KEY, -- mv_<ulid>
owner_scope TEXT NOT NULL,
visibility_scope TEXT NOT NULL,
view_type TEXT NOT NULL, -- 'user_portrait' | 'soul_relationship_memory'
content_md TEXT NOT NULL,
source_unit_set_hash TEXT NOT NULL,
renderer_version TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'fresh'
CHECK(status IN ('fresh','stale','failed')),
generated_at REAL NOT NULL,
updated_at REAL NOT NULL,
metadata TEXT,
UNIQUE(owner_scope, visibility_scope, view_type)
);
CREATE TABLE IF NOT EXISTS memory_view_units (
view_id TEXT NOT NULL REFERENCES memory_views(id) ON DELETE CASCADE,
unit_id TEXT NOT NULL REFERENCES memory_units(id) ON DELETE CASCADE,
order_index INTEGER NOT NULL,
PRIMARY KEY (view_id, unit_id)
);