Three lifecycles coexist in Open-Cognition. They are separate on purpose.
Understanding these separately is how you reason about the system without reaching for a more complicated model.
The system_state table holds exactly one row. Its mode column is either
RUNNING or STOPPED.
┌──────────────┐ POST /stop ┌──────────────┐
│ │ ───────────────► │ │
│ RUNNING │ │ STOPPED │
│ (default) │ ◄─────────────── │ │
└──────────────┘ POST /resume └──────────────┘
RUNNING by migrations/001_initial.sql.POST /stop flips to STOPPED, writes an audit log entry with the actor.POST /resume flips back to RUNNING, writes an audit log entry.| Guarantee | RUNNING | STOPPED |
|---|---|---|
POST /canonical accepted |
✓ | ✗ (503) |
POST /reference accepted |
✓ | ✗ (503) |
GET /status responds |
✓ | ✓ |
GET /canonicals, /references, /audit, /reconcile respond |
✓ | ✓ |
| Existing data readable | ✓ | ✓ |
| Audit log appends (mode transitions) | ✓ | ✓ |
STOPPED is a write halt, not a freeze. Reads continue. Operators and
inspectors can do their work while the system is paused.
Agents poll GET /status before each write. If they see STOPPED:
POST /reference or POST /canonical — the server
will reject with 503 anyway, but polite agents don’t retry until they
observe RUNNING.This is a convention, not a technical constraint. The control plane cannot stop a misbehaving agent from retrying; it can only reject the writes.
submitted ──► validated ──► stored ──► recorded
│ │
│ (immutable forever) │
└───────────────────────────┘
/canonical with id (sha256:…),
storage_path, payload (base64), metadata.storage_path. If
the write succeeds but the ledger insert fails, the object is orphaned
in storage. GET /reconcile / make reconcile detects this.canonical_objects; audit log appends
a create_canonical entry.Canonical objects never change. There is no PUT /canonical, no
DELETE /canonical, and no operator workflow for editing one.
What you do instead: If the content was wrong, submit a new canonical object with the corrected bytes. Emit references pointing to the new ID. The old object remains — deleting it would silently rewrite history.
If the original content must actually be removed (GDPR, security incident), operate at the database level. That is an intentional sharp edge: it requires a human with direct SQL access and leaves no audit trail inside the substrate. External operational controls are responsible for recording the reason.
emitted ──► signed ──► verified ──► recorded
│
(immutable; superseded by │
newer references) ▼
referenced / ignored
canonical_object_id, agent_id, context, optional
relevance, trust_weight, time_horizon.{id}:{canonical_object_id}:{agent_id}:{created_at}
with its Ed25519 private key. Attaches signature and public_key
(both base64).agent_keys (TOFU).agent_references; audit log appends
a create_reference entry.References are immutable like canonical objects. An agent that changes
its mind emits a new reference to the same object with different
relevance / trust_weight / context.
Consumers reading references must decide how to resolve multiple references from the same agent to the same object. The substrate does not pick a winner. Typical strategies:
created_at descending, take the first.trust_weight, relevance.None of these are encoded in the control plane. They are consumer logic.
A reference may carry time_horizon (ISO 8601 duration or RFC 3339
timestamp). The substrate stores it. It does not automatically expire
references or hide stale ones. Expiry is consumer logic.
This is intentional: hiding data based on a timestamp is a form of silent mutation. The substrate refuses to do it. A reader sees all references and applies their own horizon policy.
Every write to the substrate appends one row to audit_log:
| Write action | Audit entry |
|---|---|
POST /stop |
actor, action=stop, target_type=system_state |
POST /resume |
actor, action=resume, target_type=system_state |
POST /canonical |
actor=created_by, action=create_canonical, target_id=<hash> |
POST /reference |
actor=agent_id, action=create_reference, target_id=<uuid> |
Rows are append-only at the application level: no handler calls UPDATE
or DELETE on audit_log. A DBA with direct SQL access could still
tamper with the table — the substrate does not protect against that in v0.
See docs/threat-model.md.
GET /audit?limit=N returns recent entries, newest first. The dashboard
and operational tools consume this endpoint. There is no filter DSL —
filtering happens client-side or via direct SQL access.
A deployed Open-Cognition instance goes through a predictable cycle:
make up (starts Postgres, MinIO, control plane).make migrate (applies 001 and 002 SQL).make smoke (round-trip + duplicate rejection).make backup (pg_dump), make export (NDJSON canonicals).make reconcile (every ledger entry exists in storage).POST /stop before destructive operational work.POST /resume once confirmed safe.make down.Steps 5–6 should run on a schedule. The substrate does not schedule them — cron, systemd timers, or a CI job is how an operator wires this up. Scheduling is deliberately out of scope for v0.