Two persona-infrastructure correctness fixes that go together because
the second one (FK with CASCADE) requires the first (preventing the
persona row from being mutated out from under its facts).
1. update_persona handler refuses name/systemPrompt edits to built-ins
(409). includeAllMemories stays editable — that's a per-user
preference, not the persona's identity. Mirrors the existing
delete_persona guard. The DAO is intentionally permissive so the
guard sits at the HTTP layer; persona_dao test pins that contract.
2. Migration 2026-05-10 adds user_id to entity_facts and a composite
FK (user_id, persona_id) -> personas(user_id, persona_id) ON DELETE
CASCADE. This closes two issues at once:
- Persona orphans: deleting a custom persona used to leave its
facts dangling forever, readable only via PersonaFilter::All.
CASCADE now wipes them with the persona row.
- Multi-user fact leakage: PersonaFilter::Single("default") used
to surface every user's default-scoped facts. PersonaFilter is
now { user_id, persona_id } and all read paths
(get_facts_for_entity, list_facts, get_recent_activity) filter
on user_id first. upsert_fact's dedup key extends to user_id so
identical claims under shared persona names from different
users no longer corroborate-bump each other's confidence.
- user_id threads from Claims.sub.parse::<i32>().unwrap_or(1) at
the chat / insight handlers through ChatTurnRequest, the
streaming agentic loop, execute_tool, and into the leaf tools
(tool_store_fact, tool_recall_facts_for_photo). The ".unwrap_or(1)"
accommodates Apollo's service token whose sub is non-numeric on
legacy mints.
- Backfill picks the smallest user_id matching each legacy fact's
persona_id so the FK holds for already-stored rows.
Five new knowledge_dao tests with FK-on connection: persona scoping
isolation, All-variant union per-user, dedup not crossing users,
CASCADE delete, FK rejection of unknown personas. Plus
dao_update_does_not_block_built_ins documenting where the
HTTP-layer guard lives.
Apollo coordinates separately — the matching changes there add the
/api/personas proxy and start sending persona_id on photo-chat turns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
2.0 KiB
SQL
48 lines
2.0 KiB
SQL
-- Reverse 2026-05-10-000000_entity_facts_persona_fk: drop the
|
|
-- composite FK and the user_id column via the same rebuild pattern.
|
|
|
|
DROP INDEX IF EXISTS idx_entity_facts_user_persona;
|
|
DROP INDEX IF EXISTS idx_entity_facts_persona;
|
|
DROP INDEX IF EXISTS idx_entity_facts_source_photo;
|
|
DROP INDEX IF EXISTS idx_entity_facts_status;
|
|
DROP INDEX IF EXISTS idx_entity_facts_predicate;
|
|
DROP INDEX IF EXISTS idx_entity_facts_subject;
|
|
|
|
ALTER TABLE entity_facts RENAME TO entity_facts_old;
|
|
|
|
CREATE TABLE entity_facts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
subject_entity_id INTEGER NOT NULL,
|
|
predicate TEXT NOT NULL,
|
|
object_entity_id INTEGER,
|
|
object_value TEXT,
|
|
source_photo TEXT,
|
|
source_insight_id INTEGER,
|
|
confidence REAL NOT NULL DEFAULT 0.6,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at BIGINT NOT NULL,
|
|
persona_id TEXT NOT NULL DEFAULT 'default',
|
|
CONSTRAINT fk_ef_subject FOREIGN KEY (subject_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_ef_object FOREIGN KEY (object_entity_id) REFERENCES entities(id) ON DELETE SET NULL,
|
|
CONSTRAINT fk_ef_insight FOREIGN KEY (source_insight_id) REFERENCES photo_insights(id) ON DELETE SET NULL,
|
|
CHECK (object_entity_id IS NOT NULL OR object_value IS NOT NULL)
|
|
);
|
|
|
|
INSERT INTO entity_facts
|
|
(id, subject_entity_id, predicate, object_entity_id, object_value,
|
|
source_photo, source_insight_id, confidence, status, created_at,
|
|
persona_id)
|
|
SELECT
|
|
id, subject_entity_id, predicate, object_entity_id, object_value,
|
|
source_photo, source_insight_id, confidence, status, created_at,
|
|
persona_id
|
|
FROM entity_facts_old;
|
|
|
|
DROP TABLE entity_facts_old;
|
|
|
|
CREATE INDEX idx_entity_facts_subject ON entity_facts(subject_entity_id);
|
|
CREATE INDEX idx_entity_facts_predicate ON entity_facts(predicate);
|
|
CREATE INDEX idx_entity_facts_status ON entity_facts(status);
|
|
CREATE INDEX idx_entity_facts_source_photo ON entity_facts(source_photo);
|
|
CREATE INDEX idx_entity_facts_persona ON entity_facts(persona_id);
|