Skip to content

State schema

kiki state lives in two SQLite databases. All tables are versioned via migrations. Recovery on daemon restart reads from sqlite; threads survive crashes and reboots.

Database split

PathScopeContents
~/.config/kiki/state.dbper-userrepo registry, daemon meta, presenters, unscoped audit
~/.config/kiki/repos/<repo_id>/state.dbper-repothreads, agent sessions, transcripts, cascades, audit

Both databases live under ~/.config/kiki/ — kiki's centralized state directory. The source repo's own filesystem holds no kiki state; the per-repo state.db lives at ~/.config/kiki/repos/<repo_id>/state.db, where <repo_id> is a UUID assigned at kk init and recorded in the per-user repos table. The user-level database knows which repos kkd watches; the per-repo database holds everything that lives or dies with the repo.

The <repo_id> UUID gives the centralized directory a stable, opaque name during one registration; it is not an identity that survives the source repo being moved out of band. With no breadcrumb inside the source repo, kiki's only handle on a registered repo is its canonical_path. If the user mvs the tree, kk invoked from the new location reports the repo as unregistered (because no canonical_path matches). The prior <repo_id> and its centralized state remain in ~/.config/kiki/repos/<old_repo_id>/ until kk repo unregister is run against the original path. The v1 recoveries are: move the source tree back so the registered canonical_path matches; or kk repo unregister followed by kk init at the new location, which starts a fresh registration with a new repo_id. By default, kk repo unregister removes both the registry row and the centralized state directory; --keep-state retains the directory on disk for manual inspection but offers no kiki-managed recovery — a subsequent kk init at the same canonical path mints a fresh <repo_id> with no link to the preserved bytes. See Commands · Repo registry. A move-aware kk repo relocate command is not in v1.

Tables

The list below is table-level. Column-level definitions are derived from the behavioral chapters.

Per-user (~/.config/kiki/state.db)

  • repos(repo_id, canonical_path, registered_at, opt_in_metadata). repo_id is a UUID assigned at kk init and keys the per-repo state directory at ~/.config/kiki/repos/<repo_id>/. canonical_path is the realpath of the registered source repo at registration time and is the only handle kiki has for matching a future invocation back to this row. repo_id is stable for the life of the registration (so the centralized directory's name does not churn while kkd is running); it is not preserved across kk repo unregister + kk init, which is a fresh registration with a new UUID. An out-of-band mv of the source tree cannot be auto-detected — see the move semantics in the section above.
  • daemon_meta — daemon startup info, last-known socket path, schema version.
  • approval_presenters — foreground-channel enrollments (presenter_id, frontend_kind, credential_hash, channel_binding, enrolled_at, last_seen_at, revoked_at?). Enrollment requires bootstrap Admin, but enrollment is not approval for an operation. A presenter proves that a confirmation came from a registered interactive CLI or TUI channel; it cannot mint a capability without completing a persisted challenge.
  • user_approval_challenges and user_human_approvals — the same two-phase challenge and one-use-capability records described below, for operations with no repository target. Repository-scoped confirmations are stored only in the target repository database.
  • user_approved_operations and user_approved_operation_steps — durable claim envelopes and idempotent step journals for approved operations whose authority must survive without a repo database, including registration and unregister. Their shape matches the per-repo tables below; unregister completes its user-level journal only after the target repo state is deleted.
  • user_audit_log — append-only attempts that have no valid repository target, including bootstrap, ListRepos, registration, malformed repo selectors, and authorization failures before a repo can be resolved. Its row shape matches per-repo audit_log and permits null credential, approval, and presenter identities.

Per-repo (~/.config/kiki/repos/<repo_id>/state.db)

  • threads — per-thread row carrying stable identity; bookmark, workspace, tmux, and harness projection references; thread_head_commit_id; checkpoint_commit_id; owned_stack_base_commit_id; and a lifecycle column with values Creating | Active | CreateFailed | ClosePreflight | CloseCommit | CloseFailed | Closed | ProjectionDiverged | DestroyCommit | DestroyFailed | Destroyed (see Threads). projection_resume_lifecycle is null except in ProjectionDiverged, where it records whether successful repair must restore Active or Closed invariants. For an active coherent thread, the managed workspace's exact @ is the live head and thread_head_commit_id is its last-observed exact value. checkpoint_commit_id is the expected bookmark target used for publish and reopen; it may legitimately trail the live head between explicit checkpoints. owned_stack_base_commit_id survives follows-edge deletion and advances only with a completed reconciliation, so detach and later destroy validation retain an exact structural base. Cascade progress is not projected onto this row; it is derived from sync_intents.
  • thread_links — directed follows edges, DAG-validated at insert time, including last_synchronized_parent_commit_id. The initial parent anchor comes from the parent's exact live head in a pinned, revalidated operation view, never its bookmark or a stale cache. Immediately before explicit rebase, every commit after the thread's exact owned-stack base through its exact live head must form one strict single-parent chain and be owned by that child. Merge commits, multiple roots, conflicts, or foreign commits produce TopologyDiverged; kiki never infers a broader source revset or trusts a remembered “root” identity in place of validating the path.
  • thread_lifecycle_ops — durable creation, close, reopen, checkpoint, repair, and later destroy sagas. Each row carries (lifecycle_op_id, thread_id, kind, state, started_at, updated_at, completed_at?, last_error?, context_json). context_json contains pinned ids and fingerprints needed to retry but is not a substitute for normalized thread or step state.
  • thread_lifecycle_steps — append/update journal keyed by (lifecycle_op_id, ordinal) carrying (name, state: Pending | Started | Completed | Compensating | Compensated | Failed, attempt, started_at?, completed_at?, input_fingerprint?, result_json?, error?). A step is marked Started before its external side effect and records the stable external identity required to detect already-completed work. Creation and reopen compensation run completed steps in reverse order. Close records freeze, final proof, checkpoint, each child reconcile/checkpoint/detach, session kill, workspace forget, and directory delete separately; child work completes while the parent remains frozen and before session kill. Destroy separately records bookmark deletion, optional exact revision abandonment, projection cleanup, and tombstoning so failure after an irreversible step cannot masquerade as an intact lifecycle.
  • thread_projection_issues — repair ledger (issue_id, thread_id, kind, expected_json, observed_json, detected_at, notified_at?, resolved_at?, resolution?). Kinds include WorkspaceDirectoryMissing | WorkspaceRecordMissing | WorkspacePathMismatch | BookmarkMissing | BookmarkMoved | SessionMissing. An unresolved issue places an otherwise coherent active or closed thread in ProjectionDiverged, except for a temporary mismatch owned by an in-progress lifecycle journal. Issue effects are derived from kind: bookmark-only issues block checkpoint, publish, and close but not unambiguous workspace-head observation or follows classification; SessionMissing permits head observation but blocks managed boundary mutation; workspace identity or path ambiguity blocks both. Only identity-preserving, unique normalization may resolve automatically; adopt/recreate/move/restart choices require a recorded kk repair resolution.
  • agent_sessions — one row per runtime process incarnation carrying (agent_session_id, harness_session_id, started_at, retired_at?, launch_gate_id?, gate_activated_at?, harness_started_at?, ready_fingerprint?, exclusive_hook_fingerprint?, exclusive_hook_proven_at?, delivered_intent_id?, startup_intent_id?, startup_intent_epoch?, startup_delivery_id?, startup_payload_sha256?, startup_proven_at?). agent_session_id changes on every process start even when --resume reuses harness_session_id; every incarnation must independently prove exclusive hook control before it is ready or may mutate a managed workspace. During creation, a launcher blocks on launch_gate_id; activation authorizes exec but leaves the thread Creating. A validated ready handshake records the harness and hook fingerprints, and only that transaction completes the saga and enters Active. Delivery pointers name a synthetic result awaiting acknowledgement either through a soft batch barrier or as the byte-stable, one-use-tagged first startup message of a replacement process.
  • metadata_writes — kk-ownership content-hash ledger for descriptions and bookmark names.
  • sync_intents — the sole durable reconciliation, embedded-outbox, and delivery authority. Each row carries (intent_id, thread_id, seq, kind, from_base_commit_id, to_base_commit_id, classification_op_id, state, planned_op_id?, result_op_id?, result_workspace_commit_id?, payload?, anchor_change_id?, anchor_commit_id?, anchor_op_id?, prepared_at?, delivery_mode?, delivered_at?, acknowledged_at?, transcript_row_id?, barrier_incarnation_id?, barrier_model_turn_id?, barrier_tool_batch_id?, barrier_bound_at?, batch_completed_at?, soft_redelivery_count, restart_incarnation_id?, restart_startup_delivery_id?, restart_startup_proven_at?, delivery_exhausted_at?, delivery_epoch, delivery_rearmed_at?, recovery_reason?, recovery_bundle_path?, recovery_fingerprint?, recovery_details?), where kind is NativeRewrite | ParentAdvance, delivery_mode is SoftBatch | RestartStartup, and state is Detected | Reconciling | Materialized | Delivered | Acknowledged | RecoveryRequired | TopologyDiverged | Superseded. Soft-barrier and restart-startup proof fields are embedded on the intent rather than stored in another progress table. Delivery-attempt bounds are durable on the same row: soft_redelivery_count (default 0) counts barrier rebinds after contrary delivery receipts, replacement attempts are counted from the persisted agent_sessions rows whose (startup_intent_id, startup_intent_epoch) match the intent and its current delivery_epoch, and delivery_exhausted_at freezes the intent for human attention. UNIQUE (thread_id, seq) orders intents; sequence allocation and insertion occur in one transaction. Recovery bundles live under ~/.config/kiki/repos/<repo_id>/recovery/<intent_id>/, outside the source workspace. The runtime lock remains an in-memory tokio::sync::Mutex keyed by thread_id; restart recovery resumes from intent state, exact operation ids, and durable delivery proof. See Cascade and the embedded-outbox design note.
  • sync_intent_triggers — normalized (intent_id, op_id) rows with UNIQUE (intent_id, op_id). Coalescing adds observed operation ids here and updates classification_op_id to the exact view used for reclassification only while the intent remains pre-materialization; it does not impose a false linear “through” order on jj's operation DAG.
  • op_attribution — kk-initiated op-id dedupe so the watcher does not react to its own ops.
  • op_history — operation-node cache (repo_id, op_id, committed_at, observed_at, processed_at?) with UNIQUE (repo_id, op_id). A row is processed only after every relevant workspace transition for that node has been classified durably.
  • op_history_parents — normalized operation-DAG edges (repo_id, op_id, parent_op_id) with UNIQUE (repo_id, op_id, parent_op_id). No code path may collapse an operation with multiple parents to one cursor predecessor.
  • op_workspace_heads — per-view workspace projection (repo_id, op_id, workspace_id, change_id, commit_id) with UNIQUE (repo_id, op_id, workspace_id). The workspace key is load-bearing because @ is workspace-local.
  • op_watch_frontier — the current known set of operation heads (repo_id, op_id, observed_at) with UNIQUE (repo_id, op_id). Restart catch-up walks every not-yet-processed ancestor reachable from this frontier in topological order; it is not a singular “last cursor to head” scan.
  • pr_links — thread → PR.
  • thread_config — per-thread config layer key/value/updated_at.
  • credentials(cred_id, kind: 'Admin' | 'ThreadScoped' | 'RegistrationScoped', thread_id?, repo_id?, issued_at, revoked_at?). Admin is accepted only by bootstrap and approval-presenter enrollment endpoints; operational calls cannot substitute it for human approval.
  • approval_challenges — pending foreground confirmations (challenge_id, requester_cred_id, presenter_id?, method, repo_id, thread_id?, args_digest, rendered_plan, nonce, created_at, expires_at, confirmed_at?, confirmed_terminal_binding?, cancelled_at?). BeginApproval persists the exact plan before returning it for display. Only an enrolled foreground presenter may bind itself and confirm the nonce and digest; presenter binding, confirmation evidence, and capability issuance occur in one target-database transaction.
  • human_approvals — one-use capabilities (approval_id, challenge_id, requesting_cred_id, approving_presenter_id, method, repo_id?, thread_id?, args_digest, issued_at, expires_at, claimed_at?, operation_id?, consumed_at?, consuming_audit_id?). Claim and creation of the exact approved_operations envelope occur atomically before external effects. A claimed approval can resume only that operation id and cannot authorize another plan; completion marks it consumed. Expired, replayed, method-mismatched, target-mismatched, argument-drifted, unconfirmed, or channel-mismatched approvals fail closed.
  • approved_operations — generic approval-claim envelope (operation_id, approval_id UNIQUE, kind, thread_id?, plan_digest, state: Claimed | Running | Completed | Failed | OutcomeUnknown, claimed_at, updated_at, completed_at?, result_digest?, last_error?). Lifecycle operations link their envelope to thread_lifecycle_ops; publish, parent-merge remote updates, sensitive snapshots, and other consequential methods use this envelope directly. One approval creates one immutable envelope, and recovery may resume only its stored plan.
  • approved_operation_steps — idempotent external-effect journal (operation_id, ordinal, name, state, input_fingerprint, result_json?, error?), unique by (operation_id, ordinal). Remote push and PR-base mutation are separate steps; an unknown remote result becomes OutcomeUnknown until observation proves success or failure. Read-only approved snapshots record their repository observation and result digest as one completed step rather than leaving an unjournaled reusable capability.
  • audit_log — append-only (audit_id, timestamp, cred_id?, approval_id?, presenter_id?, transport_peer, declared_scope?, method, args_summary, outcome). Null identities explicitly represent unidentified or rejected callers. Parseable attempts with a resolved repo go here; attempts without a resolvable repo go to per-user user_audit_log, so there is exactly one authoritative row per attempt.
  • thread_messages — per-thread transcript rows. (thread_id, change_id?, commit_id?, op_id?, session_id, seq, captured_at, author, direction, text, dedup_key, synthesized, anchor_unknown) with UNIQUE (thread_id, dedup_key) and an FTS5 virtual table over text. CHECK constraint binding anchor_unknown to nullability of the (change_id, commit_id, op_id) triple. See Transcript.
  • thread_changes — per-(thread, change_id) row carrying tombstoned_at? (abandon) and redirected_to? (squash).
  • transcript_offsets — per-(thread, session_id) (byte_offset, last_row_uuid) for crash-safe tail resumption. Both fields advance in the same transaction as the corresponding thread_messages insert.
  • pending_kkd_prepends — TTL-bounded (prepend_id PRIMARY KEY, thread_id, text_sha256, inserted_at). Duplicate (thread_id, text_sha256) rows are allowed and consumed FIFO. Match key omits session_id so fresh-session reopens match correctly.
  • provider_egress_consents — remembered, revocable grants (consent_id, thread_id, provider_identity, purpose: 'catch_up' | 'transcript_mcp', disclosure_version, granted_at, granted_by_presenter_id, revoked_at?). Consent is valid only for the exact thread, configured provider identity, purpose, and disclosed policy version. Local preview is not provider egress; returning transcript-derived text through a hosted-model MCP call is and must check an active transcript_mcp grant before reading rows.

v2-only

  • causal_chains, causal_chain_visits — deferred until the broader MCP substrate ships. See Roadmap.

Constraints and transactional invariants worth calling out

  • threads: thread_head_commit_id, checkpoint_commit_id, and owned_stack_base_commit_id are required in Active, ClosePreflight, CloseCommit, CloseFailed, Closed, and ProjectionDiverged; destroy states retain every still-valid id until the corresponding journaled deletion or abandonment step. They may be null only while the corresponding creation step has not completed or after its exact destructive step is recorded. checkpoint_commit_id is the expected bookmark target, but need not equal thread_head_commit_id while active. Entering Closed requires equality because close checkpoints before forgetting the workspace. Active requires no unresolved thread_projection_issues and no unfinished lifecycle operation. Closed may have one unfinished or failed Reopen operation because reopen does not expose an active thread until its ready handshake commits; other lifecycle kinds are disallowed there. ProjectionDiverged requires at least one unresolved projection issue and projection_resume_lifecycle IN ('Active', 'Closed'); other states require it to be null. CreateFailed, CloseFailed, or DestroyFailed requires the corresponding failed unfinished lifecycle operation. A thread cannot return to Active from failed close until the journal records a verified process/session resume or an explicit restart repair; after destroy's first irreversible step, it cannot return to Active, Closed, or ProjectionDiverged by inference. Projection mismatches caused inside Creating, CloseCommit, reopen, or DestroyCommit belong to that lifecycle journal and fail the owning saga rather than manufacturing an invalid projection-resume state.
  • thread_lifecycle_steps: (lifecycle_op_id, ordinal) is unique and step transitions are monotonic except that a failed idempotent step may return to Started with an incremented attempt. A lifecycle operation may have only one in-flight step. Completed requires result_json sufficient to recognize or compensate the external effect. Gate activation requires the credential, hook configuration, workspace head, bookmark checkpoint, and tmux projections to be complete, but does not enter Active. Only a ready-handshake transaction with a matching incarnation and settings fingerprint completes creation and enters Active; an activated gate alone is never success evidence.
  • thread_messages: (anchor_unknown=FALSE AND change_id IS NOT NULL AND commit_id IS NOT NULL AND op_id IS NOT NULL) OR (anchor_unknown=TRUE AND change_id IS NULL AND commit_id IS NULL AND op_id IS NULL). The two states cannot drift apart.
  • approved_operations: approval_id is unique and immutable; plan_digest must equal the claimed approval's argument/rendered-plan digest. External steps cannot start before state = Claimed, and completion consumes the approval in the same transaction that commits the final result digest. Failed and OutcomeUnknown retain the claim and can only retry or observe the same step plan; they never release the approval for reuse.
  • sync_intents: both exact base commits are required. Materialized, Delivered, and Acknowledged require a result workspace commit, byte-stable payload, complete anchor triple, and prepared_at. Before any soft-path workspace mutation, the transaction sets delivery_mode = SoftBatch and the complete (barrier_incarnation_id, barrier_model_turn_id, barrier_tool_batch_id, barrier_bound_at) tuple; partial tuples are invalid. batch_completed_at requires that tuple and may be written only by an idempotent completion report carrying the same identities. Soft acknowledgement requires both that completion and a later model-turn identity. If MarkDelivered was lost, that exact later-turn proof may atomically repair delivered_at and advance Materialized -> Acknowledged; a different batch or stdout alone is insufficient. RestartStartup clears the retired soft tuple without claiming acknowledgement, requires the complete (restart_incarnation_id, restart_startup_delivery_id) pair, and acknowledges only when that replacement's first managed boundary causally echoes the one-use id attached to the byte-stable startup message. RecoveryRequired requires recovery_reason, recovery_bundle_path, and recovery_fingerprint. Only a pre-materialization intent may become Superseded. A barrier tuple may be rebound to a new batch after a contrary delivery receipt only in a transaction that increments soft_redelivery_count; retry bounds are evaluated against that column and the persisted replacement-incarnation rows, never an in-memory counter. delivery_exhausted_at may be set only by the transaction that abandons the final permitted attempt; while it is set, exactly two writes are valid, both human-directed kk repair plans. A retry plan clears it, increments delivery_epoch (an integer starting at 0, monotonic per intent), records delivery_rearmed_at for display, and resets soft_redelivery_count in one journaled transaction. Every replacement attempt is stamped with the intent's delivery_epoch at creation, and attempt bounds count only rows whose stamp matches the current epoch — an exact id comparison, never a timestamp one, so clock skew or rollback cannot move an attempt between epochs. A re-armed intent therefore gets exactly one fresh cycle and may exhaust again. A one-shot-approved discard plan is the sole path to Acknowledged without delivery proof and must record the human resolution in recovery_details. Absent either plan, no new barrier tuple or replacement incarnation may reference the intent, restart recovery re-surfaces the blocked thread instead of retrying, and the thread's derived cascade status is conflicted.
  • agent_sessions: delivered_intent_id, when non-null, must reference a same-thread Materialized or Delivered intent assigned to that incarnation's delivery mode. A partial unique index allows at most one non-retired incarnation to claim an intent. Starting a replacement process retires the old incarnation, records startup_intent_id, startup_intent_epoch copied from that intent's current delivery_epoch, and the saved payload hash before exec, and supplies that payload as the first startup message. Delivery-attempt bounds count only rows whose stamped epoch matches the intent's current one. Its first managed boundary must re-prove exclusive hooks and the startup proof before atomically acknowledging and clearing both pointers. If the process exits earlier, the next replacement receives the same saved payload; no interrupted tool is replayed automatically.

Migrations

Every schema change ships as a migration. Migrations are forward-only. Once a release is cut, schema changes must preserve upgrade compatibility. The migration runner is part of kiki-core.

docs/reference · built with VitePress