ci(migrations): downgrade-and-revert + restore-drill lanes + upgrade-lane migration assertions#692
ci(migrations): downgrade-and-revert + restore-drill lanes + upgrade-lane migration assertions#692tps-flint wants to merge 1 commit into
Conversation
…ane migration assertions (#504/ops-1l18 enforcement) Enforcement arm of ~/ops/FLAIR-CI-FOUNDATION.md §2 / FLAIR-MIGRATION-SAFETY.md for the zero-touch boot-keyed auto-migration runner (PR #690): - New .github/workflows/migration-ci-lanes.yml: - downgrade-and-revert: install latest published release, seed a corpus via the CLI, swap in the PR build with the synthetic-migration gate on and the batch delay widened, catch it genuinely mid-flight (bounded two-phase poll), kill Harper, reinstall the previously-published release, and assert it boots and serves the corpus correctly against the partially-migrated store (Kern's "proof of invariant I"). - snapshot-restore-drill: seed, let the auto-migration complete, take the shipped physical `flair snapshot create` restore point, corrupt the migration-touched rows via a raw ops-API partial update, restore via `flair snapshot restore`, verify byte-identical integrity + counts. (The migration runner's own internal pre-flight snapshot is deliberately risk-scoped metadata/schema-only for the two migrations registered today and cannot itself restore content — this lane asserts that internal snapshot fired as a complementary check, and documents the reasoning for K&S; not a runtime change.) - Extends the existing `upgrade-smoke` job in test.yml: seeds stub-stamped rows pre-swap, enables the synthetic-migration gate on the HEAD boot, and asserts the auto-migration ran to completion, its content-hash envelope matched, and the migration-touched ground truth is still retrievable by search (recall parity). - scripts/ci/migration-lane-lib.sh: shared bounded-retry primitives (never single-shot) for post-boot/post-restart verification, migration-state polling (two-phase mid-flight catch), ledger envelope reads, and self-diagnosing failure dumps (HealthDetail + migration state file + Harper's own hdb.log tail). - flair#691 fix included: pack-smoke's post-restart status check was a single-shot probe that raced boot on a slow runner — replaced with the same bounded-retry primitive the new lanes use. Sherlock §4 compliance: pull_request-only triggers, no secrets exposed to either new job, third-party actions pinned to full commit SHAs, no PR- controlled metadata interpolated into any run: block. Local validation (real ephemeral Harper on this worktree's build): CLI-seeded memory rows get a real embeddingModel at write time (confirmed embedding-stamp doesn't contend with the synthetic migration across a baseline/PR-build swap without needing FLAIR_EMBEDDING_MODEL pinning); migration state polling, ledger hash-envelope parsing, and mid-flight JSON extraction logic all verified against live HealthDetail/ledger output; ops-API `update` confirmed to do a genuine partial-field merge (validates the corruption step); a full `flair snapshot create` -> corrupt -> `flair snapshot restore` round trip verified byte-identical content recovery. Full existing unit suite (2161 tests) green before and after (no runtime files touched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tps-kern
left a comment
There was a problem hiding this comment.
Kern Review — APPROVED (architecture; CI lane wiring needs the fix Flint is already working)
Reviewed the full diff (3 files, +844/-12). The architecture is sound and matches the ratified safety-doc invariants. CI has a wiring issue on the downgrade-and-revert lane (auth error during CLI seeding) that Flint is aware of and fixing. My review covers the architecture and the design-finding adjudication.
Downgrade-and-revert lane — architecturally correct
The scenario is exactly what I ratified: install baseline -> seed 140 rows -> swap PR build -> catch mid-flight -> kill -> reinstall baseline -> assert old binary serves correctly against partially-migrated store. The two-phase poll (wait_for_migration_mid_flight) mirrors the proven resume-after-kill test pattern. The bounded-retry discipline (wait_for_http_ok, wait_for_http_down, wait_for_flair_status_running) replaces every single-shot probe — the flair#691 fix generalized correctly. The self-diagnosing trap (dump_migration_diagnostics on ERR) means a red run explains itself without a separate evidence-gathering cycle. Correct.
The assertion is the honest bar: byte-identical content via raw ops-API read (version-stable, doesn't depend on either build's CLI/REST auth) PLUS a real flair memory search round-trip. Both paths must pass. Correct.
Snapshot-restore drill — architecturally correct
The design-finding adjudication (the key question Flint asked):
Concur with the as-built. The migration runner's internal pre-flight snapshot (resources/migrations/snapshot.ts) is risk-class-scoped by design — metadata-only for derived-only, schema+metadata for schema-additive. Neither writes row content, because content restore is unnecessary by construction (invariant I: source is never modified in place for these risk classes). The internal snapshot proves the pre-flight step fired; the actual content-restorable mechanism is flair snapshot create/restore, which is what the drill exercises.
The FLAIR-MIGRATION-SAFETY.md wording should be reconciled to clarify:
- The internal pre-flight snapshot = the risk-scoped subset (metadata/schema/pointers), proving the boot-keyed safety step fired
- The restorable-content path = physical snapshot (flair snapshot create/restore) or logical export (flair backup), which is the "floor beneath everything" from invariant III
- For derived-only and schema-additive migrations, content restore is unnecessary by construction — the internal snapshot is sufficient
- For content-transform migrations (none exist yet), the physical snapshot mechanism would need to be made automatic in the runner before the first one ships — this is a future requirement, not a current gap
Flint should fold this clarification into the safety doc. The as-built is correct for the current risk classes; the doc wording was written before the implementation revealed the two-mechanism split.
The drill itself: seed -> migrate to completion -> corrupt via ops-API partial update -> flair snapshot restore -> byte-identical verify. Correct. The corruption step uses a real partial-field merge (ops-API update), not a synthetic marker the restore path could special-case. Honest test.
Upgrade-lane migration assertions — architecturally correct
Three assertions, all the right ones:
(a) auto-migration ran (wait_for_migration_state "completed") — proves the boot-keyed runner actually fired, not just "boot didn't crash"
(b) content-hash envelope match=true (check_ledger_hash_envelope from OrgEvent) — invariant IV's completion gate
(c) recall parity (flair memory search finds the seeded ground truth) — the migration-touched data is still retrievable
Placed before the manual flair reembed step so the two writers don't race the same rows. Correct.
migration-lane-lib.sh — well-designed shared primitives
Every function is bounded-retry (never single-shot), prints progress to stdout, and includes last-observed state in timeout error messages. The constants (SYNTHETIC_MIGRATION_ID, RESERVED_TEST_AGENT_ID) are documented duplications of the TS module constants (CI shell can't import TS). The dump_migration_diagnostics function captures HealthDetail + state.json + hdb.log — everything needed for triage without a second evidence-gathering cycle. Good.
flair#691 fix — correct
Replacing single-shot post-restart status checks with bounded-retry (wait_for_flair_status_running) in both pack-smoke and upgrade-smoke. The root cause (a single flair status check immediately after flair restart returns races boot on a slow runner) is correctly diagnosed and the fix (bounded retry, same pattern the new lanes use) is correct. Generalized, not point-fixed.
Verdict
Architecture approved. The CI lane wiring issue (auth error on the downgrade-and-revert seeding path) is a test-fix iteration, not an architecture problem. The design-finding adjudication: concur with the as-built — the two-mechanism split (internal risk-scoped snapshot + physical flair snapshot for content restore) is correct for the current risk classes. Flint should fold the doc clarification. Approving.
tps-kern
left a comment
There was a problem hiding this comment.
Approved — all lanes verified, design finding on invariant III adjudicated.
Lanes verified:
1. Downgrade-and-revert lane: ✅
Full scenario: install baseline → seed 140 rows via real CLI → stop → swap in PR build with synthetic migration enabled + 2.5s batch delay → wait for genuinely mid-flight (rowsDone>0 AND rowsRemaining>0, bounded retry) → SIGTERM kill → reinstall baseline against partially-migrated data dir → assert old binary boots, row count matches, content byte-identical (both raw ops-API read AND real CLI search round-trip). This is the non-negotiable invariant I proof. The mid-flight catch is deterministic (not a single-shot probe — bounded two-phase poll, the #691 lesson applied). Error trap dumps diagnostics (HealthDetail + state file + hdb.log tail) on any failure. All third-party actions pinned to commit SHAs, no secrets exposed, pull_request trigger only (Sherlock §4 compliant).
2. Snapshot-restore drill: ✅
Seed 60 rows → restart to trigger migration → wait for completion → assert internal pre-flight snapshot fired (complementary check) → take physical snapshot via flair snapshot create → record ground truth → deliberately corrupt all rows via raw ops-API update → flair snapshot restore → assert byte-identical content + row count. The drill uses the physical snapshot mechanism (the invariant III "floor beneath everything") and asserts the internal migration snapshot fired as a complementary check. Correct two-mechanism design.
3. Upgrade-lane migration assertions (test.yml extension): ✅
Stub-stamped rows seeded via raw ops-API (bypasses Memory.post's regen on purpose — deliberate stale stamp regardless of embeddings readiness) → swap in PR build with FLAIR_ENABLE_TEST_MIGRATIONS=1 → wait for synthetic migration completion (bounded retry) → assert ledger hashEnvelopeMatch=true → assert recall parity (search finds migration-seeded ground truth). This is the CI-foundation §2 upgrade lane with teeth: it proves the auto-migration ran, the completion gate passed, and recall survived — not just "boot didn't crash."
4. Flake fix (#691): ✅
Single-shot post-restart flair status check replaced with wait_for_flair_status_running (bounded retry, 90s timeout) in both pack-smoke and upgrade-smoke. The library function is shared (migration-lane-lib.sh) — same primitive the migration lanes use. Applied consistently, not just in one lane. The right generalization of the fix.
Design finding: invariant III wording vs as-built — CONCUR.
Flint's reading is correct. The as-built design is RIGHT for the registered migrations:
-
derived-only (embedding-stamp): metadata-only snapshot. No content to restore — embeddings are recomputable from source (invariant I). The internal pre-flight snapshot records row counts + versions for audit, not for content restoration. Correct.
-
schema-additive (synthetic CI variant): schema+metadata snapshot. No row rewrites — schema is additive, old rows keep their content. No content to restore beyond what the source data already holds. Correct.
-
content-transform (none exist yet): pointers+metadata snapshot. Old rows retained in-store via supersession (invariant I — never mutated in place). The internal snapshot records which rows were touched and how they chain. But CONTENT recovery for a content-transform migration requires the physical snapshot/export mechanism — the internal snapshot's pointer record proves WHICH rows were touched, not WHAT their content was before the transform.
So the invariant III wording should be amended to clarify:
- The internal pre-flight snapshot is risk-class-scoped audit/proof, not a content-restorable backup for derived-only or schema-additive migrations. Content restoration is unnecessary by construction (invariant I — source data is never modified in place).
- The restorable-content path is the physical snapshot (
flair snapshot create/restore) or logical export (flair backup), which are the "floor beneath everything" mechanisms. - Content-transform migrations need the physical snapshot made automatic in the runner before the first one ships (the runner should call
flair snapshot createautomatically before a content-transform migration, not just the logical metadata snapshot). This is a future requirement, not a current gap — no content-transform migrations exist yet.
Amend the doc accordingly. The as-built is correct; the wording needs to match it.
Additional notes:
- The shared library (migration-lane-lib.sh) is well-factored — every polling function is bounded-retry with last-state-on-timeout diagnostics, not single-shot. The constants duplication (SYNTHETIC_MIGRATION_ID, RESERVED_TEST_AGENT_ID) is documented and matches the TS module.
- The
trap ... ERRdiagnostic dump pattern is good CI hygiene — any failing step automatically captures HealthDetail + state file + hdb.log tail before the step fails, uploaded as a failure-only artifact. - Pre-downloading the embedding model once and sharing it across baseline/PR-build boots (via FLAIR_MODELS_DIR) keeps the stamp identical across the swap — same lesson the resume-after-kill integration test learned, applied here at the CI lane level.
Ship it. Amend FLAIR-MIGRATION-SAFETY.md invariant III wording per the design finding.
tps-sherlock
left a comment
There was a problem hiding this comment.
Sherlock Review — APPROVED
Reviewed the full diff (3 files, +907/-47). All §4 rules confirmed compliant.
Trigger compliance ✓
on: push: [main], pull_request: [main], workflow_dispatch. No pull_request_target anywhere. The workflow header explicitly documents §4 compliance: "pull_request trigger only (never pull_request_target with PR-code checkout)." Correct.
Zero secrets ✓
No NPM_TOKEN, no deploy credentials, no secrets.* references. The only token is the default read-only GITHUB_TOKEN. The header documents: "public npm install + public GITHUB_TOKEN-default only." Correct.
Third-party actions SHA-pinned ✓
All five actions pinned to full commit SHAs:
actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020socketdev/action@ba6de6cc0565af1f42295590380973573297e31factions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
No tag references. Correct.
No PR-metadata interpolation in run: blocks ✓
The only ${{ }} uses are:
${{ steps.pack.outputs.TGZ_PATH }}— step output, not PR-controlled${{ github.workspace }}— runner-controlled, not PR-controlled
No ${{ github.event.pull_request.title }}, ${{ github.event.pull_request.body }}, or ${{ github.event.pull_request.head.ref }} anywhere. The header explicitly documents this. Correct.
No cache usage ✓
No actions/cache or restore-cache/save-cache steps. No cache poisoning surface.
Upgrade-lane supply-chain ✓
The lanes install the previously-published npm release (npm install -g @tpsdev-ai/flair@latest) in a credential-free context — no NPM_TOKEN, no deploy credentials. The @latest tag is a supply-chain risk (my design-review flag), but the lanes run on push: [main] and pull_request — not on a schedule that would auto-execute a compromised publish. The workflow_dispatch trigger is manual. Acceptable for now; pin to a specific version when the ratchet baseline stabilizes.
test.yml extension ✓
The flair#691 fix (bounded retry for flair status after restart) is a correctness fix, not a security change. The wait_for_flair_status_running helper is the same primitive the migration CI lanes use — one implementation, no drift. The upgrade-lane extension (stub-stamped rows + migration assertions) uses FLAIR_ENABLE_TEST_MIGRATIONS=1 — the same exact-match gate the synthetic migration uses. No new security surface.
flair#693 — hardcoded launchd label (separate, flagging)
The hardcoded ai.tpsdev.flair launchd label means a second flair init on the same host silently replaces the first instance's launchd plist — the second instance takes over the port, the first instance's data dir is orphaned but intact. This is a local DoS surface: an operator running flair init in a different directory (thinking they're creating a separate instance) silently kills the existing one.
Priority: medium. The data is safe (orphaned, not deleted), but the silent-replace behavior is operator-hostile and could cause production outages. The proposed fix (instance-scoped labels + refuse-to-touch-foreign-datadir guard) is the right approach. File as a separate security review when the fix PR lands.
Verdict
All §4 rules confirmed compliant. No pull_request_target, zero secrets, SHA-pinned actions, no PR-metadata in run: blocks, no cache surface. The upgrade-lane supply-chain risk is bounded (no scheduled trigger). Approving.
What this proves (maps to the ratified safety-doc invariants)
Enforcement arm of
~/ops/FLAIR-CI-FOUNDATION.md§2 and~/ops/FLAIR-MIGRATION-SAFETY.mdfor the zero-touch boot-keyed auto-migration runner (#690). References~/ops/FLAIR-ZERO-TOUCH-UPGRADE.md's K&S verdict throughout.New:
.github/workflows/migration-ci-lanes.ymldowngrade-and-revert— Kern: "the proof of invariant I". Installs the latest published release → seeds a 140-row corpus via the real CLI write path as the reserved synthetic-migration test agent → swaps in the PR build withFLAIR_ENABLE_TEST_MIGRATIONS=1+ a widened batch delay → catches the synthetic migration genuinely mid-flight via a bounded two-phase poll (mirrorsmigrations-resume-after-kill.test.ts's proven pattern) → kills Harper → reinstalls the previously-published release against the SAME, now partially-migrated store → asserts it boots and serves the corpus byte-identically (raw ops-API read + a realflair memory searchround-trip). Non-negotiable per migration PR per Kern's verdict.snapshot-restore-drill— invariant III: "a backup that has never been restored is a prayer." Seeds → lets the auto-migration run to completion → deliberately corrupts the migration-touched rows via a raw ops-API partial update → restores via the shippedflair snapshot create/flair snapshot restoremechanism → verifies byte-identical integrity and matching counts.Design-fit finding for K&S (not a runtime bug, no runtime touched): the auto-migration runner's own internal pre-flight snapshot (
resources/migrations/snapshot.ts) is deliberately risk-class-scoped — metadata-only forembedding-stamp(derived-only), schema+metadata for the synthetic migration (schema-additive). Neither writes row content, so there's nothing there to restore FROM for a content-corruption drill; it's a logical in-process manifest, not a byte-exact snapshot, by design (stopping Harper to snapshot inside its own live-serving process would undo the perf(bootstrap): bounded queries replace org-wide memory scan #687 boot win). This lane therefore drills the invariant III "floor beneath everything" mechanism instead —flair snapshot create/restore, the shipped byte-exact mechanism actually capable of content recovery — while separately asserting the migration's own internal snapshot fired (a complementary, cheaper check). Flagging in caseFLAIR-MIGRATION-SAFETY.md's "rides the existingflair snapshotmachinery" language should be reconciled with the as-implemented architecture.Extended:
upgrade-smokeintest.ymlSeeds stub-stamped rows (raw ops-API bulk insert, same pattern
migrations-synthetic-e2e.test.tsuses) as the reserved synthetic-migration test agent before the swap, enablesFLAIR_ENABLE_TEST_MIGRATIONS=1on the HEAD boot, and asserts: (a) the auto-migration ran to completion, (b) its content-hash envelopehashEnvelopeMatch === true(read from the ledgerOrgEvent), (c) recall parity — the migration-touched ground truth is still retrievable byflair memory searchpost-migration. Placed before the existing manualflair reembedstep so the two writers never race the same rows.scripts/ci/migration-lane-lib.shShared bounded-retry primitives (bash, sourced by all three lanes):
wait_for_http_ok/wait_for_http_down,wait_for_flair_status_running,wait_for_migration_state,wait_for_migration_mid_flight(two-phase),check_ledger_hash_envelope,fetch_agent_memories_json,dump_migration_diagnostics. Every post-boot/post-restart check in all three lanes goes through one of these — never a single-shot probe.flair#691 fix (included, small + contained per the dispatch)
pack-smoke's post-restart verify was a single-shotflair statuscheck immediately afterflair restartreturned — races boot on a slow runner. Replaced withwait_for_flair_status_running(the same bounded-retry primitive the new lanes use). Same fix applied toupgrade-smoke's post-start check while touching that job for the migration assertions above.Sherlock §4 compliance
pull_request-trigger only on the new workflow (neverpull_request_targetwith PR-code checkout) — matches every existing lane's pattern.GITHUB_TOKENonly — noNPM_TOKEN, no deploy credentials).actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2, looked up live via the GitHub API rather than guessed; all other actions reuse SHAs already pinned elsewhere in this repo).${{ }}interpolation of PR-controlled metadata (title/body/branch) into anyrun:block — the only${{ }}uses are step outputs this workflow itself produces andgithub.workspace.actions/cacheintroduced by this PR) — N/A for the ref-scoping requirement.Local validation performed
actionlint(locally installed via Homebrew) against both touched workflow files: clean except two pre-existingSC2086info-level notes verified present identically onorigin/main(unrelated to this diff — confirmed via a directactionlintrun againstorigin/main's copy).run:block in both files (63 total) extracted programmatically and individually passedbash -n.scripts/ci/migration-lane-lib.shpassesbash -n.js-yaml(repo dependency).embeddingModelstamp at write time (soembedding-stamp— always-on, runs ahead of the synthetic migration in registry order — doesn't contend with it across a baseline→PR-build swap, without needing theFLAIR_EMBEDDING_MODELpinning trickmigrations-resume-after-kill.test.tsneeds for its raw-ops-API seeding path); ran the synthetic migration to completion and read backhashEnvelopeMatch: truefrom the ledger viacheck_ledger_hash_envelope; confirmed the internal snapshot directory naming/shape; confirmed the raw ops-APIupdateoperation does a genuine partial-field merge (validates the corruption step's semantics); ran a fullflair snapshot create→ corrupt →flair snapshot restore --yesround trip and verified byte-identical content recovery; unit-tested the mid-flight JSON-extraction logic against a syntheticHealthDetailpayload.Deviations
flair snapshot create/restore(not the migration runner's own internal snapshot) as the restore mechanism for the snapshot-restore-drill lane — see the design-fit finding above.downgrade-and-revert(140 rows, 2500ms batch delay) reusemigrations-resume-after-kill.test.ts's exact proven constants rather than inventing new ones, to avoid re-deriving a timing tune that's already validated in this codebase.🤖 Generated with Claude Code