Titentiten.devꦠꦶꦠꦺꦤ꧀
v0.1.1 · npmGitHub
DocsThe memory loopObserve evidence

Observe evidence

Evidence enters Titen as an immutable, content-hashed observation, filed under the trust its credential is allowed to assert.

POST /v1/observationsobservations:writeobs_ + 32 hexappend-only

An observation answers one question: what happened? It is not a conclusion, not a summary, and not an instruction. It is the record that a claim will later have to cite, which is why Titen refuses to edit it.

Append one

observe.tstypescript
import { TitenClient } from 'titen-memory';

const titen = new TitenClient({
  url: 'http://127.0.0.1:8787',
  key: process.env.TITEN_API_KEY!,
});

const obs = await titen.observe({
  subject_id: 'user_rama',
  kind: 'tool_result',
  content: 'Deploy smoke returned 200 for checkout-service.',
  source: { type: 'tool', ref: 'deploy_789#smoke' },
  trust: 'verified',
});
// obs.observation_id → "obs_ed91a2d0f66143fbba66c932494d5e64"

The same call as the raw route, which is all the SDK does:

curl -X POST http://127.0.0.1:8787/v1/observations \
  -H "authorization: Bearer $TITEN_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"subject_id":"user_rama","kind":"tool_result",
       "content":"Deploy smoke returned 200 for checkout-service.",
       "source":{"type":"tool","ref":"deploy_789#smoke"},"trust":"verified"}'
{ "data": { "observation_id": "obs_ed91a2d0f66143fbba66c932494d5e64",
    "subject_id": "user_rama", "project_id": null, "agent_id": null, "run_id": null,
    "kind": "tool_result", "trust": "verified", "visibility": "team",
    "content_hash": "bba5cf65e8aee81fe32d89ba9a5e46518fc17644d488b462a50924d6e1926516",
    "occurred_at": null, "ingested_at": "2026-07-30T10:38:43.062Z" },
  "meta": { "request_id": "req_…", "replayed": false } }

What you send

REQUEST FIELDS

subject_id who or what the evidence is about. Required, up to 200 characters.

kind one of user_statement, tool_result, imported_source, decision, system_event. Required.

content the evidence itself, up to 32000 characters. Required.

source an object with a type and an optional ref. Required — evidence with no origin is not evidence.

trust unverified, asserted, verified or policy_approved. Defaults to asserted.

visibility private, team or organization. Defaults to team.

occurred_at ISO-8601 time of the source event, when you know it.

project_id, agent_id, run_id optional scope dimensions.

There is no organization_id field. Tenant authority comes from the credential and a request body can never widen it.

The content hash is not a deduplication key

content_hash is SHA-256 over content, and it exists so a record can be checked against the bytes it was accepted with. It does not decide identity. Two smoke runs that print the same line are two events, so appending identical content twice produces two observations that share one hash — verified live, obs_ed91a2d0… and obs_c98879c3… both carry bba5cf65e8aee81f….

Retries are idempotent when you ask

Send an Idempotency-Key header and a retry returns the first result rather than a second record. The replay marker is in meta, not in data, so the body you parse is identical either way:

{ "data": { "observation_id": "obs_c98879c3adc44f6a9b2227b821a033bd", "": "" },
  "meta": { "request_id": "req_7fad51e28bee4229b193bb4ddf64392e", "replayed": true } }

A replayed write also returns 200 instead of 201. Reusing one key with a different body is a mistake rather than a second write, and Titen says so:

{ "error": { "code": "CONFLICT",
    "message": "Idempotency-Key was reused with a different request body." },
  "meta": { "request_id": "req_0e44fb86ed814eaa84efaefb656e6c7a" } }

The replay record is inserted in the same atomic batch as the observation, so a duplicate key cannot half-commit.

Trust belongs to the credential

trust is a claim about authority, so Titen checks it against the ceiling stored on your key before anything is written:

{ "error": { "code": "FORBIDDEN",
    "message": "This credential may not assert \"policy_approved\" trust." },
  "meta": { "request_id": "req_7a53014ab8484e32b8f12d7beaa02014" } }

Levels are ordered unverified < asserted < verified < policy_approved. Give a low-trust agent a low ceiling and it cannot promote its own output into verified fact — not by trying harder, not by writing more often. That ordering also caps every claim built on the observation, which is where the ceiling does most of its work.

occurred_at is yours, ingested_at is Titen’s

occurred_at describes the source event. ingested_at describes when Titen learned about it. Titen assigns the second and never infers the first, which is why the capture above shows occurred_at: null — the request did not supply one. Backfilling a month of logs gives old occurred_at values and today’s ingested_at values; collapsing the two would make that import indistinguishable from a month of live traffic.

Visibility today

private is enforced against the creating principal. Retrieval currently admits a record when visibility <> 'private' OR actor_id = you, so team and organization behave identically inside one organization — the membership model that separates them is not wired into the retrieval predicate yet. Write the value you mean; it is recorded faithfully and the predicate is what will tighten.

Why there is no update path

One write commits the observation, its history row, its FTS projection, an index-outbox row and a metadata-only domain event, in a single transaction. None of those is an update.

A claim that cites an observation must still be explainable a year later. If content could be edited, every claim built on it would silently become a claim about something else.

Correction is additive. Append the new evidence and let the claim lifecycle retire the old conclusion.

Forgetting is an authorized lifecycle action, not a model deciding that evidence stopped mattering.

Embedding and vector work is queued to the outbox, never held inside the transaction. The request succeeds when canonical SQL succeeds, so an unreachable embedding endpoint cannot discard evidence you already accepted.

Next

↑↓ navigate↵ open