Skip to content

feat(store): one-time legacy v11 store import into the control database - #375

Closed
Bnjoroge1 wants to merge 4 commits into
Bnjoroge/lock-refactorfrom
Bnjoroge/legacy-db-import
Closed

Bnjoroge1 wants to merge 4 commits into
Bnjoroge/lock-refactorfrom
Bnjoroge/legacy-db-import

Conversation

@Bnjoroge1

@Bnjoroge1 Bnjoroge1 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

One-time legacy v11 store import into the control database

Base: Bnjoroge/lock-refactor · Head: Bnjoroge/legacy-db-import · Draft — superseded by the integration branch Bnjoroge/control-import-e2e (stacks on #372, adds the missing read paths, then gets the end-to-end test on a real database). This PR is pushed for visibility; review the follow-up for the merge vehicle.

What it does

preloop store import-legacy --source <old preloop.db> --target <new control.db> [--state-dir DIR --key FILE --active refuse|requeue|cancel --json] performs the one-time, explicit, offline move from a released v11 durable-state store (the pre-ControlBackend schema) to a fresh control SQLite database:

  • Source is opened read-only, sha256-hashed before and after, data_version re-checked; a changed source aborts.
  • Everything is decoded/audited before a byte is written: wrong key, unknown user_version/table set, unknown meta keys, duplicate unique-index keys, unreadable blobs all refuse with a report.
  • Target is built in <target>.importing in one transaction, verified (PRAGMA foreign_key_check, row counts, schema check), then published atomically and never overwriting (hard_link, AlreadyExists on a racing target); a failure removes staging + staged sidecars, so retry is safe.
  • In-flight work (claimed attempts, session bindings, live concurrency gates) refuses by default and is listed; --active=requeue|cancel resolves it explicitly. Never a silent rerun.
  • Mapped: runs/jobs/specs/needs/messages/attempts/leases/steps, logs → live-log segments, run-tier secrets (sealed, values never enter the control DB, post-strip canary scan), counters/check ids/outputs/annotations, runners/sessions, webhooks, timelines, assignments/cancellations/token requests, OIDC grants, control_events → outbox (ids/order/payload preserved), per-attempt frames verified or reconstructing terminal-job templates, finalized artifact registries (meta + artifact_v2_registry.json).
  • Fixture SQL for tests is the released v11 migration chain copied verbatim with a commit reference and per-migration sha256 pins.

Known gaps (being completed in the follow-up branch)

  • v1 artifact GET/list and runs/:id/events still read node-local in-memory maps, so imported artifacts/events are durable but not yet served; the follow-up wires the backend catalog + outbox-backed event snapshot, plus public-endpoint smokes.
  • target.rs still initializes via the old lite schema; the follow-up swaps to the embedded refinery migrations (needed for the boot ledger guard).

Verification

No build/test has been run on this commit (delegated work, gates owned by the integration branch). Tests exist for the full import path (tests/legacy_import.rs + module tests) and will be executed on Bnjoroge/control-import-e2e; CI is the backstop.


Summary by cubic

Adds preloop store import-legacy, a one-time offline command that moves a released v11/v12 preloop.db (the durable-state store the control backend replaced) into a fresh control SQLite database. The source is opened read-only and hashed before and after; the target is built in <target>.importing, verified, and atomically published, so a failure leaves no target and retry is safe.

Key behaviors

  • Everything is decoded and audited before any write: wrong key, unknown source version, unreadable blobs, and duplicate unique keys abort with a report.
  • In-flight work (claimed attempts, session bindings, concurrency gates) refuses by default and is listed; --active=requeue|cancel drains it explicitly.
  • Secrets move to the SecretProvider run tier (sealed, never written to control SQL) and stored messages are reduced to secret-free templates.
  • The legacy event log is carried into the outbox with ids and order preserved.
  • v12 sources are handled too: message payloads use the row-bound AAD, a terminal status overrides a stale recorded conclusion, and settled attempts drop their lease.
  • Verified end to end against a real 37 MB v12 database (264 runs / 1736 attempts / 754 steps) with the source digest unchanged; tests use a fixture built from the released migration chain.
  • The import lands on the refactored lock base; the merge repaired the legacy writer for the base's type and borrow drift without changing behavior.

Written for commit 92bb3d0. Summary will update on new commits.

Review in cubic Turn on auto-fix

…rol DB

Adds `preloop store import-legacy` (library: `preloop_runner_server::
legacy_import`): an explicit, offline, fail-closed import of a released v11
durable-state SQLite store into a fresh control SQLite database. Nothing
runs from `preloop serve`, and the legacy file is opened read-only.

Source handling
- Reads exactly the released v11 format: PRAGMA user_version 11 (12 with
  the message-payload marker), the audited migration chain, and the full
  table set. Any other version, a missing table, a key that cannot decrypt
  a blob, an unknown metadata key, or a referentially inconsistent record
  aborts before a byte is written.
- Decodes the sealed blobs with the existing envelope (runs via the ported
  run_record_value shape, queued jobs, request snapshots, step names, the
  runtime metadata snapshot) and re-hashes the source before/after; the
  target is never published if the source changed underneath the import.

Target handling
- Builds `<target>.importing` in one transaction through the same row
  vocabulary as a live submit, verifies row counts, `PRAGMA
  foreign_key_check` and the schema ledger, then atomically renames it into
  place. A failure removes the staging file and leaves no target.
- Secrets move to the SecretProvider run tier (`run-secrets/<run_id>`)
  sealed with the cluster key; stored job messages are reduced to the
  secret-free template (`strip_template`) and scanned so no secret value
  can enter the control SQL. Log bytes are published in the live-log
  layout, counters/check ids/outputs/timeline records/webhook queue state
  and runner rows are carried over.

In-flight work
- Claimed-but-unfinished attempts, session bindings and live concurrency
  gates refuse by default (`--active=refuse`); `--active=requeue|cancel`
  drains them explicitly and records what was released. Transient families
  (protocol frames, node-local caches, the legacy event audit log) are
  listed in the report with counts and reasons, never dropped silently.

Tests build a populated fixture from the released migration chain (under
`test-support`) and cover full recovery, source immutability, wrong key,
active-claim refusal/drain, version refusal, unknown metadata refusal, and
failure/retry safety. `preloop serve` and the base worktree are untouched.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Bnjoroge1

Copy link
Copy Markdown
Collaborator Author

Superseded for merge by #376 (Bnjoroge/control-import-e2e), which stacks this import on the migration runner, fixes the compile/test fallout, adds the artifact/event read paths, and is verified end-to-end on a real 260-run v12 database.

…us-derived conclusions, settled-attempt leases

Verified end to end against a real 37 MB v12 preloop.db (264 runs / 1736
attempts / 754 steps): source sha256 unchanged, every run served through a
live engine matches the legacy status, no foreign-key or integrity errors.

- decode job_request_messages with the v12 row-bound AAD (the released store
  seals those rows; the importer only tried the unbound envelope and refused
  every real store with 'envelope authentication failed'). Covered by a unit
  test; the fixture now writes sealed frames too.
- run conclusion: a terminal status now wins over a stale recorded
  conclusion (real stores held status=cancelled with conclusion="success").
- a settled attempt no longer keeps a job_leases row (the live server drops
  the lease when it records a result).
- legacy log keys without plan/job identity are reported as size-only
  metadata, not 'unmapped log files'.
- fixture: create the schema_migrations audit table, gate the RUN_ACTIVE
  step on include_active_claim (FK), and expose base_record.
- lite queue_stats: GROUP BY 1 so the CASE alias wins over the jobs.kind
  column (mixed queue states collapsed into one bucket).
- repair the branch's never-compiled legacy import tests against the merged
  backend API (Option<RunRecord>, claim-through-session, QUEUE_KIND order).
- webhooks.rs: add the git_forge_url field #352 introduced.
@Bnjoroge1
Bnjoroge1 force-pushed the Bnjoroge/legacy-db-import branch from 9af3896 to e3bda8b Compare October 6, 2026 20:05
@Bnjoroge1

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by #376, which is the merge vehicle for the import work (fixes the compile errors in this PR).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant