Titentiten.devꦠꦶꦠꦺꦤ꧀
v0.1.1 · npmGitHub
DocsReferenceData model

Data model

The entities behind the API, the fields that carry meaning, and which state transitions the implementation actually allows.

9 migrations28 tablesbun:sqlite + D1append-only evidence

One schema, two runtimes

Nine forward-only migrations build 28 tables and two FTS5 virtual tables, plus a titen_migrations bookkeeping table. The same SQL runs on bun:sqlite and on Cloudflare D1, which constrains how migrations are written: one statement per array item, no PRAGMA (D1 rejects most of them), parents before children so foreign keys hold, and nothing destructive without its own work item.

GET /readyz compares the applied version against the expected one and refuses to report ready when they differ. A deployment running old code against a new database, or the reverse, is a state you find out about at startup rather than at the first failed query.

Identifiers, tenancy, authority

Every external id is a prefix plus 32 lowercase hex characters — obs_, claim_, ctx_, fb_, org_, key_, and req_ for request ids. The prefix is a reading aid, never authority: the code comment on the generator says exactly that, and a record is resolved with an org_id filter before anything reads or writes it, regardless of how unique the id looks.

Top-level tenant records carry org_id. Link tables — claim_sources, context_run_items, channel_release_items, federation_filters — reach the tenant through their parent.

Organization comes from the credential. No request body field can move a record into another tenant, and no route accepts one.

subject_id, agent_id, project_id and run_id are scope dimensions, not owners. They narrow retrieval; they never grant access.

An index is not an authorization control. Every route resolves a record with an org_id filter before touching it, even when the primary key alone would find the row.

Evidence

observations is append-only. There is no content update path and no delete route.

Field group Columns
identity id, org_id, actor_id
scope subject_id, project_id, agent_id, run_id
evidence kind, content, content_hash, source_type, source_ref
authority trust, visibility
time occurred_at (when it happened, nullable), ingested_at (when Titen received it)

kind is checked in the database: user_statement, tool_result, imported_source, decision, system_event. trust is unverified, asserted, verified or policy_approved, and visibility is private, team or organization — both enforced by CHECK constraints, not only at the API boundary.

Two observations with identical content are not automatically the same event. Sameness is decided by the idempotency key and a hash of the request body, never by text similarity, because the same tool output at two different times is two facts.

Claims and their sources

A claims row is the current head: kind, statement, confidence (greater than 0 and at most 1, enforced by CHECK), trust, visibility, status, version, valid_from, valid_to, and superseded_by. Kinds are semantic_fact, episodic_event, preference, procedural, decision, relationship.

claim_sources is the append-only link to evidence, keyed on (claim_id, observation_id, relation) with relation in supports, contradicts, qualifies. relation being part of the key is the point: one observation can carry more than one relation to the same claim, and every link survives. At least one supports source is mandatory, so no claim exists without evidence under it.

Trust and visibility are derived from that evidence rather than declared. A claim defaults to the highest trust among its supporting observations and to the narrowest visibility among all its sources, and a request may lower either but never raise it. confidence defaults to 0.8 when omitted.

WHY THE STATEMENT IS NOT THE EVIDENCE

A claim stores a statement, not a copy of the observation content it came from. The text an agent reads in a compiled pack and the text a tool actually produced are different records with different mutability, so a claim can be rewritten by supersession without any chance of rewriting history.

Claim states

From To Trigger Retrievable after?
active consolidation with only supports / qualifies sources yes
disputed consolidation where any source relation is contradicts yes
active, disputed superseded POST /v1/claims/:id/supersede with an active replacement no
active, disputed expired POST /v1/claims/:id/expire, which also sets valid_to no
active, disputed, expired revoked POST /v1/claims/:id/revoke no

Retrieval selects status IN ('active','disputed') and applies the temporal window, so the three terminal states drop out of compiled context without anything being deleted. Every transition bumps version and appends a record_history row.

Nothing in the implementation moves a claim back to active. A dispute is resolved by writing a better claim and superseding the old one — which is why supersession demands that the replacement already be active. Revoking a superseded claim is rejected with a message telling you to revoke the replacement instead, because the superseded row is no longer the one anyone reads.

Context and feedback

THREE TABLES, ONE COMPILE

context_runs what was asked and under what budget: task_hash rather than the raw prompt, max_tokens, used_tokens, policy_snapshot, degraded

context_run_items the ordered selection: position, claim_id, score, score_components — why each claim was chosen, without copying its content

context_feedback one outcome per run or item: used, useful, irrelevant, incorrect, harmful, with an optional reason_code and client_mutation_id

The task is stored hashed. A compiled pack is a record of a decision, not an archive of prompts, and storing the prompt would quietly turn the context log into a second copy of everything an agent was ever told.

Feedback influences ranking utility. It never mutates evidence, trust, visibility or authorization — a harmful outcome makes a claim less likely to be selected, and changes nothing about whether the claim is true or who may read it.

Execution state

Table Key Lifecycle
checkpoints one row per (org_id, subject_id, agent_id, kind) Upserted in place with a state_hash and expires_at. Reads filter on expires_at > now. DELETE removes the row — the one place a record is physically deleted, because task state is not evidence.
leases one unreleased row per (org_id, resource_type, resource_id) Live while released_at IS NULL. A competing acquire is 409 until expires_at passes; then the stale row is released and the new holder takes it.
handoffs pendingaccepted or rejected Only to_principal may resolve. GET /v1/handoffs lists just the caller’s pending ones.
memberships unique on (org_id, workspace_id, principal_id) Removal sets removed_at. Listings skip it; provenance and audit entries stay intact.
api_keys unique on key_hash Revocation sets revoked_at. Authentication reads the row every request, so revocation takes effect immediately.

There is no status column on checkpoints or leases. Expiry is a timestamp comparison, which means a crashed agent releases its lease without anyone running a cleanup job.

Governance and delivery

policies records retention, approval_required, visibility_default or trust_ceiling with a JSON config. channel_releases is versioned per (org_id, channel, audience) and moves draftactiverevoked; channel_release_items holds the claim ids and an optional redacted_statement, so what a channel serves can differ from what the claim says internally. customer_assertions is created by migration 4, but no route reads or writes it yet. audit_log is metadata only: actor, action, resource type and id, optional detail, ip_hint, timestamp.

webhooks moves activepausedactive, and reaches disabled after 10 consecutive failures. webhook_deliveries carries pendingsuccess or failed, with a unique index on (webhook_id, event_id) added in migration 9 — before that, event_id held the event kind, so deduplication matched on kind and only the first event of each kind was ever delivered. federation_peers moves activesuspended.

Five enum values are reserved by CHECK constraints but never written by any route today: expired on handoffs and on webhook_deliveries, revoked on federation_peers, failed on index_outbox, and error on federation_log.

Derived, and therefore rebuildable

Table Holds If you lose it
observations_fts, claims_fts FTS5 search text, written in the same transaction as the canonical row Rebuild from observations / claims
index_outbox pending embedding work: record type, id, operation, state, attempts Re-queue; POST /v1/index/drain consumes it
the vector store opaque claim ids and embeddings — no canonical content Re-embed. Vectors are an index, never the source of truth
events metadata-only domain history, written in the canonical batch Not reconstructible, but nothing reads it as authority
record_history record_type, record_id, version, change_kind, actor_id, snapshot_hash Not reconstructible; it is the provenance trail, and it stores a hash rather than content
idempotency (org_id, endpoint, key_hash) → stored status and response Losing it means a replayed key writes again

Where the design document runs ahead of the schema

docs/reference/data-model.md is the logical design and names entities the migrations have not created. The mapping, so neither page misleads you:

Design document Today
principals No table. Principal identity lives on api_keys (principal_id, principal_kind) and memberships.
subjects, subject_references No table. subject_id is a scope column; there is no alias management.
claim_versions No table. record_history carries the version, change kind, actor and snapshot hash.
record_events Implemented as record_history; audit_events as audit_log.
vector_outbox, event_outbox Implemented as index_outbox and events + webhook_deliveries.
webhook_subscriptions Implemented as webhooks.
channels No table. channel and audience are columns on channel_releases.
tags, record_tags Not created. Tag filtering has not entered a shipped slice.
schema_meta Implemented as titen_migrations, holding the applied version only.
claim kind procedural_guidance The enforced kind is procedural.
release states approved, suspended, replaced, expired The CHECK allows draft, active, revoked.
checkpoint states open, in_progress, blocked No status column. A checkpoint is current until expires_at.
handoff states offered, declined, completed The CHECK uses pending, accepted, rejected, expired.

Next

↑↓ navigate↵ open