Skip to content

Stateless runs: in-memory sessions, complete event parity, drop SQLite and interactive user-message machinery #223

Closed

Description

@byapparov

Summary

Make aictrl run stateless:

  • Session state lives in memory for the length of one invocation.
  • The NDJSON event stream is the only record of a run, so it must have complete data parity with what the local SQLite database stores today.
  • Remove the SQLite/drizzle storage layer, plus the interactive user-message machinery that a headless harness never uses.

This is a breaking change, so it ships in a minor release (0.6.0).

Why

Scope

1. In-memory session state

  • Replace Database.use / Database.transaction in session/, permission/, project/, control/ and tool/task.ts with in-memory stores, keeping the existing Session / MessageV2 APIs, so prompt.ts and processor.ts don't change shape.
  • MessageV2.stream ordering must stay deterministic. Keep the fix: deterministic message order and de-flake #200 tests #222 regression test.
  • Account login state (control_account) (email, url and tokens) moves to a 0600 JSON file next to auth.json. It is the only state that must outlive a run.

2. Complete data parity in the event stream

Everything the database records today must be in the stream. The table compares the database with today's events, based on run.ts on main:

Data (DB table / part type) Emitted today? Change
User message: prompt text, -f file parts, agent / subtask parts, model, agent, outputFormat ❌ New user_message event with the full parts
Synthetic user messages: file and resource expansion, StructuredOutput corrective nudges, compaction "continue" ❌ Same event, with synthetic: true
Assistant text / tool calls (completed or error) / step boundaries ✅ none
Assistant message info: tokens, cost, finish, error ✅ message_complete none
Reasoning Only with --thinking Always emitted; --thinking controls only human-readable output
compaction part and summary message ❌ Emit them
retry part ✅ as the retry event none
Subagent text / reasoning / message info Only tool_use, plus usage after #221 Emit the subagent text / reasoning with part.sessionID
Provider metadata on parts (thinking or thought signatures, response item ids) Needs checking Include it as-is
Session record: id, parent, created/updated, final status Partly (session_start, subagent_*, session_complete) Fill the gaps
Saved permission decisions (permission table) ❌ (permission_rejected / permission_granted cover decisions at run time) Drop the table; headless permissions come only from config

These are all additive changes, so schemaVersion stays "1". Update EVENTS.md.

3. Remove the interactive user-message machinery

In a headless run there is exactly one user turn: the prompt (or --command). These pieces only serve an interactive client that sends messages while the model is working:

  • SessionPrompt queued callbacks, loop({ resume_existing }) and resume(): a second prompt for a busy session waits for the running loop (prompt.ts ~280-345, 905-915).
  • The "user sent the following message" <system-reminder> that wraps queued user messages mid-run (prompt.ts ~772-790).
  • SessionPrompt.shell (user ! shell commands). It has no callers.
  • Session.initialize (the /init command entry point). It has no callers.
  • Session.BusyError / assertNotBusy.
  • lastModel() fallback that reads the model from an earlier user message.

4. Remove the storage layer and the commands that depend on it

Acceptance criteria

  • packages/cli has no drizzle, bun:sqlite, Database.use or Database.transaction references; aictrl.db is never created.
  • Parity test: after a run that uses -f, a tool call, a subagent, compaction and a StructuredOutput correction, the session, messages and parts rebuilt from the emitted events equal the in-memory store at the end of the run (deep-equal, ids included).
  • Two aictrl run processes sharing a data directory both complete (no "database is locked").
  • Interactive machinery from section 3 removed; a second SessionPrompt.prompt for a busy session is a programming error, not a queue.
  • Latency measured before and after on the trivial and multi-step benchmark tasks (wall time, time to first model request, per-step overhead). No regression.
  • EVENTS.md, README, CHANGELOG (breaking changes section) updated.

Supersedes / related

Metadata

Metadata

Assignees

Labels

P2Could have - MediumenhancementNew feature or requestperformancePerformance optimizationtech-debtTechnical debt cleanup

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions