add source_uris provenance to private memory + temporal render tail
problem: observations and interactions were floating claims — no way to
tell where a belief came from or how recently phi extracted it. stale
observations rendered identically to fresh ones; uncited claims rendered
identically to well-sourced ones. the "memory without sequence is just
assertion" lesson from the gullibility incident wasn't being applied to
phi's own observation rendering.
fix: cite sources + surface age.
schema
- new source_uris: list[AtUri] field on Observation (pydantic-validated
via the SDK's atproto_client.models.string_formats.AtUri type)
- new source_uris: []string column on USER_NAMESPACE_SCHEMA + EPISODIC_SCHEMA
- one field because the URI itself encodes author DID, collection NSID,
and TID timestamp — no separate source_kind/source_handle/source_at
write paths
- store_interaction / store_observations / _write_observation /
store_episodic_memory / after_interaction all accept + persist source_uris
- reconciliation UPDATE unions old + new sources (preserves pedigree)
- DELETE+ADD inherits new sources only; supersedes link gives audit trail
extraction
- get_unprocessed_interactions returns typed InteractionRow carrying URIs
- process_extraction attributes every observation in a batch to the full
set of URIs that fed it (always-true: each claim was justified by
something in this batch)
callers
- reply_to captures bot-post URI from create_post() and threads
[parent_uri, bot_post_uri] into after_interaction
- note tool gains optional source_uri param with docstring nudge
role inference (match/case)
- _source_role(uri, phi_did, owner_did) classifies into phi-post /
operator-liked / their-post / essay / card / liked-by-other / other /
unknown via match on host + collection NSID
- helper for future per-URI trust weighting; not surfaced in default
render yet
temporal render
- _citation_tail(source_uris, created_at) renders compact provenance:
"(3 sources, 2w ago)" / "(1 source)" / "(2w ago)" / ""
- build_user_context observation query now fetches created_at; renders
both citation count AND age so phi sees two trust signals
(how-anchored + how-aged) on every observation
- interactions were already fetching created_at but dropping it — now
rendered as "(2d ago)" tail
helper consolidation
- new bot/utils/time.py:relative_when — single canonical implementation.
granularity slides s → m → h (decimal<10) → d (decimal<10) → mo → y
- core/recent_operations.py and core/self_state.py delete local copies,
import from utils.time instead (was three near-identical implementations)
typing
- ObservationRow / InteractionRow / _InteractionDisplay TypedDicts at
module scope — no bare dicts in the new read/extraction paths
tests
- test_source_uris.py: Observation.source_uris validation, _source_role
match/case classification, _citation_tail formatting
- test_relative_when.py: granularity boundaries, decimal behavior, future
timestamps, invalid inputs, Z-suffix parsing
101 tests pass (was 85 before).
loq.toml: relaxed namespace_memory.py from 945 → 1033 for the added
helpers and typed dict scaffolding.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>