03 — Delivery, lifecycle, and concurrency
Defines memory APIs, daemon-global quota storage, database lifecycle, concurrency ownership, trimming, and unresolved storage policy.
Summary
Host-owned APIs resolve and audit memory without exposing storage files to agents. Per-instance state and daemon-global account quota use separate ownership and concurrency models.
Question and scope
How should persistent state be delivered, migrated, shared, trimmed, and protected across host processes and containerized agents?
Method
The analysis applies the storage model to Capsule, daemon, telemetry, workflow, migration, instance-lifecycle, and multi-agent concurrency boundaries.
Findings
Memory delivery and external backends
jackin❯ should expose the memory layer through a narrow internal API:
memory.resolve(scope, workflow, role, agent, run) -> MemoryBrief
memory.propose(scope, kind, title, body, provenance) -> pending item
memory.append_run_summary(run, session, body) -> memory item
memory.hide/supersede(id, reason) -> operator actionjackin❯ can also expose an MCP server with equivalent operations so agents can query or propose memories during a session. The MCP server must enforce the same scope, read policy, write policy, and audit rules as the host CLI; it must not become arbitrary file access to the operator's host. External systems such as OpenMemory/Mem0, Letta, or Zep/Graphiti can become optional backends behind the same API only when they preserve the same local policy model.
Daemon-global account cache
Provider account quota snapshots do not belong in each per-instance
database. The Token & Cost Telemetry
plan makes the host daemon the single owner of provider refresh, then fans
read-only account state out to Desktop, jackin console, Capsule, and CLI
surfaces. The daemon-global cache keeps that state from diverging across role
containers.
Location and lifecycle. The DB lives at ~/.jackin/data/daemon/accounts.db —
outside any ~/.jackin/data/<container>/ tree so it survives jackin purge
on individual instances. The daemon creates and
migrates this file on first start; CLI / jackin console / Capsule do not
write to it, and the daemon process is the single writer. CLI tools and the
console open the file read-only. WAL mode handles the daemon-write /
multi-reader-process pattern
(this table's concurrency model is different from the per-instance
tables described in the "Concurrency model" section below — those assume
single-process single-writer; the daemon-global cache assumes single-writer
across all host processes, enforced by the daemon's per-operator-user
singleton).
jackin usage <instance> accounts --sync-host-cache performs explicit,
operator-triggered synchronization from one selected Capsule daemon. Automatic
provider refresh belongs exclusively to the host daemon.
CREATE TABLE account_usage_snapshots (
id INTEGER PRIMARY KEY,
provider TEXT NOT NULL,
account_key_hash TEXT NOT NULL,
account_label TEXT NOT NULL,
source TEXT NOT NULL,
confidence TEXT NOT NULL,
window_kind TEXT NOT NULL,
used_amount INTEGER,
used_unit TEXT,
limit_amount INTEGER,
limit_unit TEXT,
resets_at INTEGER,
fetched_at INTEGER NOT NULL,
expires_at INTEGER, -- nullable: not every provider returns an expiry
status TEXT NOT NULL,
last_error TEXT,
UNIQUE(provider, account_key_hash, source, window_kind)
);Lifecycle
- Created on first instance load. If the file doesn't exist, run
the migration suite and set
_meta.schema_version. - Migrated on subsequent loads. If
_meta.schema_version< current, run pending migrations. - Kept with the data dir. The DB lives under the same
~/.jackin/data/<container>/tree as the instance manifest and durable agent home, so normal restore and purge flows treat it as part of the instance. - Deleted with
purge. Standard data-dir teardown.
Concurrency model
Single writer (the running jackin process for that instance), many
readers. Console subscribers may read concurrently. SQLite's WAL mode
handles this; turn it on at DB creation time. No multi-writer scenarios
exist because each instance has at most one running jackin❯
process.
Memory introduces a second concurrency question: multiple agents inside one Capsule-managed container may read the same memory brief and propose new memory during the same run. should serialize writes through the workflow runner or daemon-owned storage handle, not let each in-container agent open the database directly. Agents submit proposals through the control/MCP surface; the host-side storage owner stamps provenance and writes the row. This keeps the database private to jackin❯ and prevents an agent from rewriting durable memory with ordinary filesystem access.
Trimming
Tables with rolling-window semantics (status_log, usage_samples,
tool_history) get trimmed on every Nth insert (cheap, bounded), not
on a background timer. Counts above are starting points; tune after
observing real footprint.
Implications for jackin❯
Keep storage host-owned: agents use narrow control or MCP operations, instance state follows instance lifecycle, and account quota state remains daemon-global with one writer.
Limitations and unknowns
- Turso production posture. Turso's SQLite-compatible Rust engine remains pre-1.0 and labeled beta. The storage ADR must cover build cost, migration tooling, multi-process behaviour, backup/export shape, and the rationale for using Turso with operator data.
- Schema in code vs schema in
.sqlfiles. Diesel-style embedded migrations are clean but add a new toolchain dependency. An in-code migration list avoids an additional toolchain dependency for the current table count. - Workspace memory across many instances. Per-instance DBs are purge-friendly, but workspace memory must survive instance purge and serve new containers. The storage ADR must select a workspace-level database or another single durable authority while the API remains workspace-scoped.
- Memory retrieval semantics. Explicit scope filtering and operator-curated titles/kinds are the baseline. Keyword, embedding, or graph retrieval requires representative memory data and measured quality.
- Agent write authority. Run summaries and review findings may use workflow-append policy; operator preferences and project facts remain operator-only until the operator accepts a proposal.