Skip to content

ADR-0005: One service layer behind every surface, with optimistic concurrency

  • Status: Accepted
  • Date: 2026-10-19
  • Deciders: Jian (owner)

Context

REST, CLI, MCP and the dashboard all write the same data, and one of the writers is an agent that may overwrite a human's fix.

Decision

All surfaces call one service layer; no surface writes the database directly. Entries carry a version. Writes state the expected version; a stale write returns 409 with the current entry. Entries can be pinned; agent writes to a pinned entry fail unless forced, and forcing is logged. The CLI is a thin HTTP client with --json and stable exit codes. Agent-facing lists return a lean projection (no bodies).

Amendment: entry history (SEM-66, 2026-10-21)

Every write to an entry (authored, answered from the queue, edited, pinned, imported, forced) stores a snapshot in entry_revision in the same transaction: version, time, actor (agent or human), a few words on what changed, whether an agent forced past a pin, and the responses, selection and pin as the write left them. Reading it is GET /api/v1/entries/{id}/history (?version=N for one revision with its snapshot), semula entry history, MCP entry_history. The latest 50 per entry are kept; deleting the entry (or its project) deletes its history; export and import carry none, and an import starts the target's history with an imported revision. Reads, hits and re-keys are not writes and leave none. Entries that existed before the table got one before history revision each. A version_conflict still returns the current entry; the history is what to compare it with.

Alternatives considered

Option Why not
Last write wins An agent re-record could silently destroy a human fix.
Locks Overkill for a local tool; versions are simpler and work across surfaces.

Consequences

Buys one behaviour to test and no drift between CLI and MCP. Costs clients handling 409s, which agents must be told how to do (tool descriptions).