Repository navigation
feat(storage): migrate remaining stores to SQLite and remove the dual-track writer #1649
Copy link
Copy link
Closed
Description
Problem
Maka's durable storage is mid-migration and currently dual-tracked:
- Already SQLite (canonical, but gated):
runtime_events(runtime.sqlite) andsession_metadata+ agent-graph tables (sessions.sqlite). - Still file/JSONL: run headers and the
AgentRunEventstream (FileAgentRunStore,writeAtomic+appendJsonl),interaction,shell-run,task-ledger,artifact,pricing,automation,message-receipt,plan,deep-research(allFile*Store). - The
sqliteCanonicalswitch is conditional: CLI usesagentGraphEnabled || MAKA_RUNTIME_SQLITE_CANONICAL=1; Desktop uses only the env flag and defaults to JSONL for runtime events.
On a shared root, Desktop (JSONL) and CLI (SQLite) can diverge and rely on import-on-open to reconcile. This is a migration state, not a steady state, and it is the kind of debt that gets harder to remove as users accumulate.
Desired outcome
One SQLite authority is the single canonical writer for every structured durable store. Specifically:
- run header,
AgentRunEventstream,interaction,shell-run,task-ledger,artifact,pricing,automation,message-receipt,plan, anddeep-researchare backed by SQLite; - the
sqliteCanonicalswitch and every legacy JSONL / file write path are removed; - a one-time, idempotent import-on-open remains so existing roots migrate their legacy data into SQLite without losing sessions;
- CLI, Desktop, and Headless all use the same SQLite-backed store implementations;
- the existing
importLegacyRuntimeEventJsonlTreeandimportLegacySessionMetadataTreeimporters are kept (and extended to the newly migrated stores) as the only legacy read path.
This is a breaking change to storage internals. It is acceptable now while the user base is small; it gets harder later.
Open questions to resolve in this issue
- Session transcript bodies.
#1370deliberately leftStoredMessagetranscript payloads as JSONL (non-goal). Should this issue override that and move transcript bodies into SQLite too, or keep them as the one remaining append-only file store? Decide with a concrete plan for compaction, large-text row storage, and DB size/vacuum if moving them. - Single workspace database.
runtime.sqliteandsessions.sqliteare two separate SQLite authorities today.#1370already flagged "avoid two independently coordinated SQLite authorities." Decide whether the remaining stores extend one of these or a single renamed workspace-state database, and document the cutover.
Proposed scope
- Add SQLite schemas and transactional read/write APIs for each remaining store, preserving current API semantics for callers.
- Remove the
sqliteCanonicalflag and all ongoing JSONL / file write paths for migrated data. - Extend the one-time import-on-open to each newly migrated store; keep it idempotent, restart-safe, and fail-closed on corrupt input.
- Wire CLI, Desktop, and Headless through the same SQLite-backed implementations.
- Resolve the two open questions above.
Acceptance criteria
- run header,
AgentRunEvent,interaction,shell-run,task-ledger,artifact,pricing,automation,message-receipt,plan, anddeep-researchread/write through SQLite. - The
sqliteCanonicalswitch and all legacy JSONL / file write paths for migrated data are removed. - Existing roots import their legacy data into SQLite once, idempotently, and remain readable after restart.
- CLI, Desktop, and Headless share one SQLite-backed store implementation per domain.
- Crash/failpoint tests cover migration and multi-row mutations for each migrated store.
- Open questions (transcript, single-DB) are decided and documented.
- No production dual-writer ambiguity remains for migrated data.
Non-goals
- Solving the multi-open ownership race. Two processes sharing one SQLite DB still produce double terminal facts, double turns, and broken ledger invariants, because that is an application-layer ownership problem, not a storage problem. That is tracked by the Runtime Host work (
#853,#1167). - Changing the
RuntimeEventor Tool Journal schema semantics. - Remote/distributed storage or cloud CAS.
Context
#1370/#1371established the SQLite metadata foundation and explicitly deferred transcript bodies and the single-DB question.- This issue closes the remaining dual-track debt while the user base is small.
简体中文
问题
Maka 的持久化存储处于迁移中途,目前是双轨:
- 已 SQLite(canonical,但有开关):
runtime_events(runtime.sqlite)和session_metadata+ agent-graph 表(sessions.sqlite)。 - 仍是文件/JSONL: run header 与
AgentRunEvent流(FileAgentRunStore,writeAtomic+appendJsonl),以及interaction、shell-run、task-ledger、artifact、pricing、automation、message-receipt、plan、deep-research(全是File*Store)。 sqliteCanonical开关是条件性的: CLI 用agentGraphEnabled || MAKA_RUNTIME_SQLITE_CANONICAL=1;Desktop 只认 env,默认仍走 JSONL。
同一 root 上 Desktop(JSONL)和 CLI(SQLite)会分叉,靠打开时 import 对齐。这是迁移态,不是稳态,且越晚越难删。
期望结果
单一 SQLite authority 作为所有结构化持久 store 的唯一 canonical writer:
- run header、
AgentRunEvent流、interaction、shell-run、task-ledger、artifact、pricing、automation、message-receipt、plan、deep-research都走 SQLite; - 删除
sqliteCanonical开关和所有遗留 JSONL / 文件写路径; - 保留一次性、幂等的 import-on-open,让老 root 的遗留数据迁进 SQLite、不丢会话;
- CLI、Desktop、Headless 统一用同一套 SQLite-backed store 实现;
- 现有的
importLegacyRuntimeEventJsonlTree和importLegacySessionMetadataTree保留(并扩展到新迁的 store)作为唯一遗留读路径。
这是存储内部的 breaking change。趁现在用户少可以做,越晚越难。
需在本 issue 内解决的 open question
- session transcript 正文。
#1370有意把StoredMessagetranscript 正文留 JSONL(列为 non-goal)。本 issue 要不要覆盖该决定、把正文也搬进 SQLite,还是保留它作为唯一剩下的 append-only 文件 store?若搬,需给出 compaction、大文本行存、DB 体积/vacuum 的具体方案。 - 单一 workspace DB。 当前
runtime.sqlite和sessions.sqlite是两个独立 SQLite authority。#1370已提"避免两个独立协调的 SQLite authority"。决定剩余 store 是扩展其中一个、还是用单个重命名的 workspace-state DB,并记录切换方式。
范围
- 为每个剩余 store 增加 SQLite schema 与事务读写 API,保持对外 API 语义不变。
- 删除
sqliteCanonical开关和所有已迁数据的 ongoing JSONL / 文件写路径。 - 把一次性 import-on-open 扩展到每个新迁 store;保持幂等、可重启、对损坏输入 fail-closed。
- CLI、Desktop、Headless 统一走同一套 SQLite-backed 实现。
- 解决上述两个 open question。
验收标准
- run header、
AgentRunEvent、interaction、shell-run、task-ledger、artifact、pricing、automation、message-receipt、plan、deep-research经 SQLite 读写。 -
sqliteCanonical开关和所有已迁数据的遗留 JSONL / 文件写路径已删除。 - 老 root 的遗留数据一次性、幂等导入 SQLite,重启后仍可读。
- CLI、Desktop、Headless 每个域共享同一套 SQLite-backed store 实现。
- crash/failpoint 测试覆盖每个新迁 store 的迁移与多行 mutation。
- 两个 open question(transcript、单 DB)已决定并记录。
- 已迁数据不再有 production 双写歧义。
不在范围
- 解决多开 ownership 竞态。两个进程共享同一 SQLite DB 仍会产生双终态、双 turn 和破裂的 ledger 不变量,因为这是应用层 ownership 问题,不是存储问题。由 Runtime Host 工作跟踪(
#853、#1167)。 - 改变
RuntimeEvent或 Tool Journal schema 语义。 - 远程/分布式存储或云 CAS。
背景
#1370/#1371建立了 SQLite metadata 基础,并明确推迟了 transcript 正文与单 DB 问题。- 本 issue 在用户少时清掉剩余双轨债。
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
likun666661 commentedon Jul 30, 2026
I ran a read-only Graph planning pass against current
main. The direction in this issue looks right, but I recommend treating it as an umbrella and landing it as a sequence of reviewable cutovers rather than one large storage PR.Recommended architecture decisions
runtime.sqliteunderstateRootas the unified operational database instead of introducing a third database name. Importsessions.sqliteinto it and retire the latter after validation.OperationalStateStore/ database owner. It should own the connection lifecycle, pragmas, schema migrations, transactions, backup/snapshot API, and repositories. Pointing independently opened stores at the same SQLite file is not sufficient to create one transactional authority.stateRoot/configRootboundary explicit. Session/execution state, usage/pricing state, and artifact metadata belong understateRoot. Credentials, model connections, settings, skills, device identity, and automation configuration remainconfigRoot; automation should either stay out of this operational migration or move in a separate config-database issue.StoredMessagetranscript bodies as append-only JSONL for now, but make them compatibility/import-export material rather than a second structured authority. Runtime facts, run state, receipts, projections, and metadata should be SQLite-only.Proposed PR sequence
PR 1 — remove the current RuntimeEvent writer divergence
This should be the smallest first PR:
openRuntimeEventPersistencealways create/openruntime.sqlite;sqliteCanonical, the CLI graph/env gating, and the Desktop-specific choice;This eliminates the immediate dual-writer risk without waiting for every other store migration.
PR 2 — establish the single operational DB owner and cutover protocol
cutover_journalcontaining store name, source identity/hash, state (started/completed), timestamps, and validation result;sessions.sqliteintoruntime.sqlite;PR 3 — migrate core execution state
Migrate session metadata, AgentRun headers/events/projections, root-turn admissions/proofs, interaction, message receipts/host epochs, and shell-run records. Update
execution-stores.tsso Headless readers/writers use the same repositories as CLI/Desktop. Preserve public store APIs and error semantics during the cutover.PR 4 — migrate workflow and artifact metadata stores
Migrate task ledger, plan, deep-research, pricing/usage state, and artifact metadata. Keep artifact payloads in the existing artifact tree and make filesystem publication recoverable against the SQLite metadata transaction. Also explicitly classify currently omitted stores such as agent mailbox and plan reminders so “all structured durable stores” has a closed scope.
PR 5 — bundle, export, import, and backup
Replace the current special handling of
runtime.sqliteplus protection ofsessions.sqlitewith a transactionally consistent export API. A selected-session bundle should contain only that session’s relational closure plus referenced artifact payloads. Full backup should use a WAL-safe SQLite snapshot and a verified artifact manifest;configRootsecrets must remain excluded.PR 6 — remove legacy production writers
After every domain has completed cutover:
Cutover invariant
Each import should run under the canonical storage-root writer lease plus a SQLite transaction:
startedwith source hash/fingerprint;completed.An unchanged completed source is a no-op. A changed legacy source after cutover should fail closed and require an explicit repair path; it must not silently overwrite newer SQLite state. Crash/failpoint tests should cover every transition.
The highest-risk gates are RuntimeEvent/receipt loss or duplication, cross-session leakage in filtered bundles, Headless recovery parity, artifact metadata/payload crash windows, and backup/downgrade behavior.
feat(core): add canonical ModelCallAttempt accounting contract
feat(core): add canonical ModelCallAttempt accounting contract
feat(core): add canonical ModelCallAttempt accounting contract (#1687)
likun666661 commentedon Aug 1, 2026
Closing this umbrella after the staged operational-state cutover completed.
Final state
runtime.sqliteis the unconditional canonical operational database, owned through one sharedOperationalStateStoretransaction authority.sqliteCanonicalandMAKA_RUNTIME_SQLITE_CANONICALselection paths are gone.sessions.sqliteinputs are retained only as fingerprinted import/cutover evidence. Import is journaled, idempotent, restart-safe, and fails closed on corrupt or changed post-cutover evidence.Landed sequence
Final scope decisions
StoredMessagetranscript bodies remain the intentional append-only JSONL exception. They are a presentation/compatibility transcript, not a second operational fact authority.configRootconfiguration state and are outside this operational-state migration.These decisions supersede the broader wording in the original unchecked checklist. The production dual-writer migration goal is complete.
Non-blocking follow-ups are removal of test-only legacy writer fixtures after the compatibility support window, and the separate multi-open ownership work tracked by #853 and #1167.