personal memory agent

feat(schemas): name-first relational layer for story + entity_observer master

Add a bounded relational layer to two generation schemas. Models emit entity names; Python resolves them to entity IDs after generation. An unresolved name never becomes a fabricated ID. story.schema.json gains a required top-level `relations` array ({from, to, kind, note, quote}) and a required nullable `counterparty` on decisions. story.py resolves relation endpoints and decision counterparties through the existing find_matching_entity(fuzzy_threshold=90) path, leaving *_entity_id as None on a miss while the name and note/quote evidence survive to disk. Relations with a kind outside the closed 8-value enum, or kind "other" with no note, are skipped with a warning -- mirroring the existing ALLOWED_RESOLUTIONS precedent. entity_observer operations gain an optional nullable `relation` component with a model-emitted target_name. An unresolvable target drops the whole op, logs a warning, and increments a `relation_unresolved` counter surfaced in <day>_observer_outcome.json. A relation-bearing op lands complete or not at all. Relation persistence stays with the L2 write owners: activity records via activities.py::merge_story_fields, observations via entities/observations.py::_new_observation. The app hook writes neither. The relation-kind vocabulary lives in exactly one Python frozenset (story.ALLOWED_RELATION_KINDS) plus the two JSON enums, with parity tests. Both schemas are now fully bounded (maxItems on every array, maxLength on every free-text string, including nullable ones), so their check_schema_bounds.py allowlist entries are deleted -- the guard fails CI on stale entries. max_output_tokens=12288 on the three story talents is derived from the bounded schema's 31192-char theoretical maximum (~8912 tokens, within 0.8x the budget); the resulting LLM request timeout is 286s. test_schema_prep.py's byte-identity snapshot asserted that no shipped schema carries provider-stripped keywords -- a claim the schema-bounds ratchet is designed to falsify. Replace it with the durable invariant: prep always yields a provider-supported subset, is a no-op exactly when the schema has no unsupported keywords, and demonstrably rewrites the schema when it does. Add explicit provider-behavior tests for the newly bounded schemas (local keeps bounds; openai/google lose maxLength; anthropic loses maxLength and maxItems), guarded against passing vacuously. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>