Repository navigation
Closed
Labels
P2Could have - MediumCould have - MediumenhancementNew feature or requestNew feature or requestperformancePerformance optimizationPerformance optimizationtech-debtTechnical debt cleanupTechnical debt cleanup
Description
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
- The database doesn't earn its keep. Every run writes
aictrl.db(sessions, messages, parts, permissions, projects, accounts), and nothing headless reads it back afterwards.- The executor runs one invocation per container and throws the file away.
- From 2026-10-05 to 2026-10-19 prod ran 634 executor jobs with 0 container-level failures, so durable restart would rescue nothing.
- It costs us anyway. It brings migrations, drizzle advisories (Bump drizzle-orm to 1.0.0-beta.21+ for GHSA-gpj5-g38j-94v9 (handle migrate() + transaction() API drift) #61/fix(deps): bump drizzle-orm/drizzle-kit to 1.0.0-beta.20 (GHSA-gpj5-g38j-94v9) #220), a WAL "database is locked" failure when parallel runs share a data directory, schema drift (Reconcile drizzle schema with migrations: drop orphaned todo and workspace tables #185), leftover data (Reclaim disk left by removed worktree snapshots ($DATA/snapshot, storage/session_diff) #186, Scrub leftover session-share secrets from local storage after #189 #198), and a SQLite write on every part update. The run loop also re-reads its whole history from SQLite on every step (
MessageV2.stream; fix: deterministic message order and de-flake #200 tests #222 had to fix its ordering). - Decided against:
- Resume or durable restart: the complexity outweighs the benefit.
- Writing to an external database such as Postgres: that couples the CLI to one backend and puts database credentials inside the sandbox.
- Consumers that want history store the event stream themselves.
Scope
1. In-memory session state
- Replace
Database.use/Database.transactioninsession/,permission/,project/,control/andtool/task.tswith in-memory stores, keeping the existingSession/MessageV2APIs, soprompt.tsandprocessor.tsdon't change shape. MessageV2.streamordering 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 a0600JSON file next toauth.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:
SessionPromptqueued callbacks,loop({ resume_existing })andresume(): 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/initcommand 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
- Remove
storage/db.ts,storage/schema.sql.ts,*.sql.tstable definitions,migration/,storage/json-migration.tsandstorage/migration-journal.ts(fix(deps): bump drizzle-orm/drizzle-kit to 1.0.0-beta.20 (GHSA-gpj5-g38j-94v9) #220). - Remove the
drizzle-orm/drizzle-kitdependencies, thedbscript, andbun:sqliteusage. - Remove the
aictrl session,export,import,statsanddbcommands, and the--continue/--session/--forkrun flags (and--share, if it outlived Remove session sharing (sync to upstream opncd.ai) #189). - No flag or config restores SQLite.
- Delete the stale
aictrl.db*files under the data directory on first run, or document the manual cleanup. Decide in the PR.
Acceptance criteria
-
packages/clihas nodrizzle,bun:sqlite,Database.useorDatabase.transactionreferences;aictrl.dbis 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 runprocesses sharing a data directory both complete (no "database is locked"). - Interactive machinery from section 3 removed; a second
SessionPrompt.promptfor 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
- Supersedes Abstract Database layer to support external persistence (Postgres/Redis) #21 (external persistence) and Support multi-tenant session namespacing (user_id/project_id) #23 (multi-tenant session namespacing).
- Makes Reconcile drizzle schema with migrations: drop orphaned todo and workspace tables #185 and Reclaim disk left by removed worktree snapshots ($DATA/snapshot, storage/session_diff) #186 moot: the schema is gone and its leftovers are cleaned up.
- Moots Scrub leftover session-share secrets from local storage after #189 #198 and Session: track title provenance instead of matching the default-title pattern #163.
- Closes the
storage/db.tsPRAGMA ordering follow-up raised in fix: deterministic message order and de-flake #200 tests #222. - Related: Bump drizzle-orm to 1.0.0-beta.21+ for GHSA-gpj5-g38j-94v9 (handle migrate() + transaction() API drift) #61 / fix(deps): bump drizzle-orm/drizzle-kit to 1.0.0-beta.20 (GHSA-gpj5-g38j-94v9) #220, fix: deterministic message order and de-flake #200 tests #222.
Metadata
Metadata
Assignees
Labels
P2Could have - MediumCould have - MediumenhancementNew feature or requestNew feature or requestperformancePerformance optimizationPerformance optimizationtech-debtTechnical debt cleanupTechnical debt cleanup
Activity
refactor(session): reject busy loop() asynchronously (#225 review)
fix(session): only the prompt that claims a session frees it (#225 re…
feat(run): emit a user_message NDJSON event
test(run): group the throwing-handler test with the #223 suite (#224 …
Merge pull request #225 from aictrl-dev/refactor/issue-223-interactive