title: Spatial persistence and hydration status: partially implemented audience:
- humans
- AI agents last_reviewed: 2026-08-22 ---
Spatial persistence and hydration
Purpose
Yuu should become a local-first spatial context system rather than a process that loads one complete scene into memory. The camera is both a viewport and an AI-context lens:
- geometry determines what can become visible;
- lightweight metadata explains what exists nearby;
- entity-specific hydrators load only the state needed for rendering or AI;
- semantic regions and conversations group entities independently of position;
- camera checkpoints restore where the user left off;
- a consistent SQLite copy can be uploaded to object storage for backup, transfer, or later server ingestion.
SQLite is the first source of truth because it is portable, transactional, fast on local machines, and easy to move as one artifact. The storage API must not expose SQLite details to canvas interaction or rendering code, so a hosted PostgreSQL/PostGIS implementation can implement the same contract later.
Implementation status
The first isolated storage slice is implemented in
//yuu:canvas_store_sqlite:
- the vendored SQLite target enables R-tree;
- schema initialization creates workspaces, entity catalog rows, R-tree bounds, and named camera checkpoints;
- entity metadata and bounds are upserted in one transaction;
- viewport overlap queries return catalog metadata in z-order;
- movement is represented by updating the same entity's catalog revision and R-tree bounds;
- discrete component state is bounded to one fixed-size row per entity
(
activeand integervalue), while hover, animation progress, selection, and focus remain transient; - focused tests cover negative coordinates, overlap/exclusion, movement, truncation, z-order, camera round trips, and rollback after a foreign-key failure.
The native //yuu:dev target now provides the first frame-loop
integration:
- it opens existing workspace state without automatically creating entities;
- an explicit Seed 100 developer action resets and creates deterministic test data when requested;
- restores the
last-viewcamera checkpoint before the initial query; - queries a 1.75x preload rectangle and materializes matching rows into
Canvas_Scene; - refreshes after the viewport leaves an inner guard rectangle;
- reconciles rows by stable ID so entities shared by adjacent viewports do not restart animation or flicker;
- shows resident count and load duration in the developer panel;
- saves camera target, zoom, and viewport size after an idle debounce and on clean shutdown.
- treats developer
Clearas a durable workspace mutation: R-tree rows are removed before entity rows in one transaction, and normal startup preserves the intentionally empty workspace.
This is intentionally a development proving path. Runtime entity edits are not
yet synchronized back to stable catalog rows, and each refresh rebuilds the
small resident scene synchronously. Asynchronous query generations, typed
payload hydration, .zmap migration, and database backup/export remain planned
phases below.
Goals
- Open a workspace without decoding every entity payload.
- Query IDs and small metadata intersecting a camera viewport.
- Hydrate different entity types through different mechanisms.
- Keep rendering residency separate from AI-context hydration.
- Persist movement, resizing, z-order, semantic membership, and camera state.
- Preserve stable domain IDs across process restarts and database transfers.
- Export a consistent database image without uploading a live WAL pair.
- Allow future remote storage without rewriting
canvas.c.
Non-goals for the first implementation
- Multi-user concurrent editing.
- Transparent merging of two independently edited database copies.
- Loading arbitrary executable entity code from a database.
- Treating spatial proximity as authoritative conversation membership.
- Replacing the existing render and input model in the same milestone.
Core model
The persistent catalog and the resident Raylib scene are different layers:
SQLite workspace database
├── workspace and camera checkpoints
├── entity catalog and stable IDs
├── R-tree world bounds
├── summaries and hydration descriptors
├── type-specific payloads or external references
└── semantic regions and relationships
│ viewport query
▼
Resident set
├── nearby catalog entries
├── visible summary/full entities
└── active external resources
│ materialize
▼
Canvas_Scene
└── hot Raylib/CEF entities used by the current frame
Canvas_Scene should remain the hot retained render state. It should not become
the durable database model or contain every entity in an unbounded workspace.
Stable identity
Separate three identities:
| Identity | Scope | Purpose |
|---|---|---|
entity_pk |
One SQLite database | Integer key required by SQLite R-tree |
entity_id |
Stable across exports/imports | Public opaque 128-bit ID |
| domain key | Entity-type-specific | Conversation, asset, document, or component ID |
The current numeric Canvas_Entity.id is a process-local rendering handle.
During migration it may temporarily mirror entity_pk, but persisted
relationships and agent contracts must use stable entity_id or a typed domain
key.
Proposed SQLite schema
The exact migration SQL belongs beside the future store implementation. This shape defines ownership and query behavior:
CREATE TABLE schema_meta (
schema_version INTEGER NOT NULL,
created_at_ms INTEGER NOT NULL,
application_id TEXT NOT NULL
);
CREATE TABLE workspace (
workspace_id BLOB PRIMARY KEY,
title TEXT NOT NULL,
revision INTEGER NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
);
CREATE TABLE camera_checkpoint (
workspace_id BLOB NOT NULL,
checkpoint_name TEXT NOT NULL,
target_x REAL NOT NULL,
target_y REAL NOT NULL,
zoom REAL NOT NULL,
viewport_width INTEGER NOT NULL,
viewport_height INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (workspace_id, checkpoint_name)
);
CREATE TABLE entity (
entity_pk INTEGER PRIMARY KEY,
entity_id BLOB NOT NULL UNIQUE,
workspace_id BLOB NOT NULL,
entity_type INTEGER NOT NULL,
domain_kind INTEGER NOT NULL,
domain_id BLOB,
title TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
z_order INTEGER NOT NULL,
flags INTEGER NOT NULL,
payload_kind INTEGER NOT NULL,
payload_version INTEGER NOT NULL,
payload_bytes INTEGER NOT NULL DEFAULT 0,
external_uri TEXT,
revision INTEGER NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
);
CREATE VIRTUAL TABLE entity_bounds USING rtree(
entity_pk,
min_x, max_x,
min_y, max_y
);
CREATE TABLE entity_runtime_state (
entity_pk INTEGER PRIMARY KEY,
active INTEGER NOT NULL,
value INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
FOREIGN KEY (entity_pk) REFERENCES entity(entity_pk) ON DELETE CASCADE
);
CREATE TABLE entity_payload (
entity_pk INTEGER PRIMARY KEY,
codec INTEGER NOT NULL,
payload BLOB NOT NULL,
checksum BLOB NOT NULL,
FOREIGN KEY (entity_pk) REFERENCES entity(entity_pk) ON DELETE CASCADE
);
CREATE TABLE region (
region_id BLOB PRIMARY KEY,
workspace_id BLOB NOT NULL,
title TEXT NOT NULL,
summary TEXT NOT NULL,
instructions TEXT NOT NULL DEFAULT '',
revision INTEGER NOT NULL
);
CREATE TABLE region_entity (
region_id BLOB NOT NULL,
entity_id BLOB NOT NULL,
role INTEGER NOT NULL,
PRIMARY KEY (region_id, entity_id)
);
The vendored SQLite target enables SQLITE_ENABLE_RTREE, and focused tests
create/query the virtual table through Deita.
Database access should use //deita:deita; missing backup or transaction
features should be added to Deita rather than bypassing the first-party
abstraction throughout Yuu.
Viewport query
The basic intersection query returns catalog rows, not full payloads:
SELECT
e.entity_pk,
e.entity_id,
e.entity_type,
e.domain_kind,
e.domain_id,
e.title,
e.summary,
e.z_order,
e.flags,
e.payload_kind,
e.payload_version,
e.payload_bytes,
e.external_uri,
b.min_x,
b.max_x,
b.min_y,
b.max_y
FROM entity_bounds AS b
JOIN entity AS e ON e.entity_pk = b.entity_pk
WHERE e.workspace_id = ?
AND b.max_x >= ?
AND b.min_x <= ?
AND b.max_y >= ?
AND b.min_y <= ?
ORDER BY e.z_order;
Query a preload rectangle larger than the visible viewport. The visible viewport is the hot region; the surrounding margin is warm metadata. A query should run when the camera exits an inner guard rectangle, changes zoom enough to alter desired detail, or the workspace revision changes. It should not run for every subpixel camera movement.
Each asynchronous query carries a generation number. Results from an older camera generation are discarded rather than materialized after the user has moved elsewhere.
Residency states
An entity progresses independently through these states:
ABSENT
-> CATALOG
-> SUMMARY
-> FULL
-> ACTIVE_RESOURCE
Any loading state may become FAILED with an explicit retryable error.
| State | Resident data |
|---|---|
ABSENT |
Stable ID may be known through a relationship only |
CATALOG |
Type, bounds, title, flags, payload descriptor |
SUMMARY |
AI-readable summary and cheap visual placeholder |
FULL |
Complete persistent payload required by the entity |
ACTIVE_RESOURCE |
Runtime-only CEF browser, GPU texture, SDK handle, or stream |
Downgrading releases runtime resources first, then full payloads. Catalog rows remain cheap and may stay cached across nearby camera movement.
Hydration contract
Hydration must be selected by entity type and payload kind, not implemented as one generic JSON decoder:
typedef struct {
boolean (*load_summary)(...);
boolean (*load_full)(...);
boolean (*activate)(...);
void (*deactivate)(...);
void (*release_full)(...);
boolean (*append_context)(...);
} Canvas_Entity_Hydrator;
The eventual API should be exposed through a small store/residency boundary, for example:
Canvas_Store_Open_WorkspaceCanvas_Store_Query_ViewportCanvas_Store_Load_PayloadCanvas_Store_Upsert_EntityCanvas_Store_Update_BoundsCanvas_Store_Save_CameraCanvas_Store_Create_ExportCanvas_Residency_UpdateCanvas_Hydrator_For_Type
Hydration work must not block the Raylib frame. Database reads, file decoding, network fetches, and browser creation run asynchronously. The main thread applies bounded completed work between input and rendering.
Entity-specific hydration
| Entity family | Catalog/summary | Full hydration | Active resource |
|---|---|---|---|
| Shapes, lines, buttons, switches | All visual state inline | Usually unnecessary | None |
| Text and text areas | Title and excerpt | Full text payload | Editing buffers already in scene |
| Conversation | Title, latest-turn summary, unread/working state | Requested transcript window | Copilot session only while needed |
| Image | Dimensions, media type, thumbnail reference | Original asset metadata | Decoded texture or CEF image view |
| Browser | URL, title, favicon/preview | Navigation/session metadata | Pooled CEF browser |
| Calendar/table/card | Compact semantic summary | Type-specific rows/items | None unless externally backed |
| Generated component | Schema name and safe summary | Versioned component data | Allowlisted renderer only |
Rendering hydration and context hydration are related but not identical. An off-screen conversation may need transcript hydration because the user asked about it explicitly. A visible image may need a texture for rendering but only caption metadata for AI context.
Movement and writes
Entity movement, resize, and z-order updates use one transaction:
BEGIN IMMEDIATE
UPDATE entity revision/z_order/updated_at
UPDATE entity_bounds min/max coordinates
INSERT append-only mutation record when history is enabled
COMMIT
Do not update the R-tree only after a drag finishes if other viewport or context queries can run during dragging. The UI may coalesce writes to one transaction every short interval and always flush at drag completion.
Persistent semantic membership is separate:
- geometry answers where the entity is;
region_entityanswers which conversation or context region owns it;- proximity may produce a suggestion, never an automatic authoritative move.
Camera checkpoints
Persist one reserved last-view checkpoint per workspace and allow named
viewpoints later.
- Update the in-memory checkpoint continuously.
- Flush after a short idle debounce, on workspace switch, and on clean exit.
- Store target, zoom, and viewport size.
- On startup, restore the camera first, query the preload rectangle, then progressively hydrate results.
- If no checkpoint exists, fit the camera to a bounded initial entity query.
The checkpoint is user/workspace state, not part of an entity payload.
Context planning
The camera query supplies candidates; it must not blindly concatenate every full payload.
Context assembly should proceed in budgeted layers:
- Workspace and active region summaries.
- Stable ID, type, bounds, title, and summary for visible entities.
- Type-specific semantic state for the most relevant visible entities.
- Full payload excerpts requested by the orchestrator or selected by policy.
- Explicitly referenced off-screen entities and relationships.
Every context entry includes its stable ID and hydration level. If more detail exists but was not loaded, expose a retrieval capability instead of pretending the summary is complete. Parked or archived conversations remain excluded by policy even if their geometry intersects a large viewport.
The context planner owns token and byte budgets. The renderer does not decide which transcript window or document section an agent receives.
Cache and eviction policy
Begin with deterministic limits rather than a complex adaptive cache:
- maximum resident catalog entries;
- maximum full-payload bytes;
- maximum hydrations started per frame;
- maximum completed entities materialized per frame;
- existing maximum active CEF views;
- grace period before evicting an entity that just left the preload bounds.
Eviction must never discard dirty state. Dirty entities flush successfully or surface an error before their full payload is released.
Database export and bucket upload
Do not upload the live database file while SQLite is using WAL mode. A valid export flow is:
- Flush dirty entity and camera writes.
- Use SQLite's backup API to write a temporary standalone database.
- Run
PRAGMA quick_checkon the copy. - Record schema version, workspace revision, byte length, and checksum in a small manifest.
- Optionally encrypt the database copy before it leaves the machine.
- Upload to an immutable object key containing workspace ID, revision, and checksum.
- Update a small
latestpointer using a conditional write.
Credentials, Copilot tokens, CEF caches, decoded textures, and other machine-local runtime state must never enter the workspace database.
The first synchronization model is single-writer backup/restore. If a database has changed locally and remotely, preserve both copies and require an explicit choice. Multi-writer merge requires stable operation IDs, an append-only change log, and conflict semantics; it is a later architecture milestone.
Relationship to .zmap
The current .zmap format remains useful for:
- deterministic tests;
- explicit user snapshots;
- import/export compatibility;
- fast rollback while the database store is introduced.
It should not become the long-term query engine. Migration should decode a
validated .zmap, assign stable entity IDs, and insert catalog, bounds, and
payload rows in one transaction. Database exports supersede .zmap for full
workspace backup after parity is proven.
Module ownership
Proposed first-party targets:
| Target | Responsibility |
|---|---|
//yuu:canvas_store |
Backend-neutral workspace/catalog API |
//yuu:canvas_store_sqlite |
Deita/SQLite schema, queries, migrations, export |
//yuu:canvas_residency |
Camera query generations, hot/warm sets, eviction |
//yuu:canvas_hydration |
Hydrator registry and completion queue |
//yuu:canvas_context |
Budgeted AI context planning |
//yuu:scene_store |
Existing .zmap snapshot compatibility |
Keep SQL and database handles out of canvas.c, main.c, and entity drawing
functions. Those files consume store results and report mutations through the
interface.
Delivery phases
Phase 0: contracts and measurements
- Define stable IDs, store structs, hydration states, and error semantics.
- Add frame, query, hydration, and resident-byte counters.
- Preserve current behavior behind an in-memory store implementation.
Phase 1: SQLite catalog and camera
- [x] Enable and test SQLite R-tree.
- [x] Add the initial versioned schema through
canvas_store_sqlite. - [x] Persist workspace, entity catalog metadata, bounds, and
last-view. - [ ] Persist type-specific payload bodies.
- [ ] Add forward schema migration steps beyond version 1.
- Initially hydrate all queried entities fully to prove persistence parity.
Phase 2: viewport residency
- [x] Query preload bounds in the native development target.
- [x] Requery after the viewport leaves an inner guard rectangle.
- [x] Preserve database z-order while materializing resident entities.
- [ ] Move queries off the frame thread.
- [ ] Discard stale asynchronous results using query generations.
- [ ] Apply large result sets within bounded per-frame work.
- Persist movement and resize transactions.
Phase 3: entity-specific hydration
- Introduce summary/full/active-resource transitions.
- Move browser, image, conversation, and large text loading behind hydrators.
- Add byte and resource budgets plus eviction grace periods.
Phase 4: context planner
- Build context from catalog summaries first.
- Retrieve full entity details within explicit token/byte budgets.
- Add semantic regions and explicit region membership.
- Allow agents to request hydration by stable ID.
Phase 5: export and restore
- Add consistent SQLite backup creation and validation.
- Add manifests, checksums, optional encryption, and bucket upload integration.
- Exercise restore into a temporary path before atomic activation.
Phase 6: hosted backend
- Implement the same store contract with PostgreSQL/PostGIS.
- Keep local SQLite as an offline cache or authoritative personal workspace.
- Add operation logs and conflict semantics only when multi-writer editing is an actual requirement.
Acceptance criteria
- Startup work is proportional to the initial viewport, not workspace size.
- Camera movement never blocks a frame on database or payload I/O.
- Stale viewport query results cannot overwrite the current resident set.
- Moving/resizing an entity survives restart with matching bounds and z-order.
- Different entity types demonstrably use different hydration paths.
- AI context identifies incomplete hydration and can retrieve more by stable ID.
- Exported databases pass integrity and checksum validation before activation.
- A workspace with many distant entities stays within configured resident entity, payload-byte, and active-resource budgets.