diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,38 @@
 # Changelog
 
+## 0.5.0.0 — 2026-08-22
+
+### Breaking Changes
+
+- Corrected the released payload of `0011-kioku-memory-space-partition.sql` so its final statement
+  is `RESET search_path;`. Its exact-byte SHA-256 changes from
+  `eee9cd252b32b563c50f8457596347fff1b2e4d3ea4dafe5b45043e991624192` to
+  `6c83d3f01f784d0d9395953d5bb1763b8eea6cd9439073df42f79775a85197a9`. Databases that
+  already applied `0011` under 0.4.0.0 or 0.4.1.0 must run
+  `ledger-fixups/2026-08-19-rebaseline-0011-checksum.sql` once before the corrected `up` or
+  `verify`; databases where `0011` is pending apply it normally. The next release must use the
+  0.5.0.0 series.
+- Raised `keiro-migrations` to `^>=0.14.0.0`. Its appended `keiro/0031` migration adds bounded
+  terminal rejected-outbox audit fields and updates ordering indexes. The composed plan now
+  contains 55 migrations: Kiroku 11, Keiro 31, and Kioku 13.
+
+### Added
+
+- Added `0013-partition-aware-fts-index.sql`. When `btree_gin` is installed or can be installed,
+  it replaces the content-only full-text GIN with an active-only GIN over
+  `(memory_space_id, namespace, content_tsv)`, preventing one space's recall cost from growing
+  with matching content in unrelated spaces. If extension installation is unavailable, the
+  migration succeeds and retains the old index as a correctness-preserving fallback.
+- Added the exact-checksum, idempotent 0.4.x ledger re-baseline at
+  `ledger-fixups/2026-08-19-rebaseline-0011-checksum.sql` and tests for mismatch recovery,
+  repeated execution, and a missing custom ledger configuration.
+
+### Fixed
+
+- Migration `0011` now restores the configured database or role `search_path` before its own
+  transaction commits. The composed-plan regression applies all 55 framework migrations and an
+  unqualified host migration on one connection with a nonstandard `host_app, pg_catalog` default.
+
 ## 0.4.1.0 — 2026-08-18
 
 ### Changed
diff --git a/kioku-migrations.cabal b/kioku-migrations.cabal
--- a/kioku-migrations.cabal
+++ b/kioku-migrations.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               kioku-migrations
-version:            0.4.1.0
+version:            0.5.0.0
 synopsis:           Schema migrations for kioku
 description:
   Owns kioku schema evolution as a native pg-migrate component and composes
@@ -21,6 +21,7 @@
 
 extra-source-files:
   codd-upgrade/*.sql
+  ledger-fixups/*.sql
   migrations/*.sql
   migrations/manifest
   migrations.lock
@@ -58,7 +59,7 @@
     , containers               >=0.6       && <0.8
     , hasql                    >=1.10      && <1.11
     , hasql-transaction        >=1.2       && <1.3
-    , keiro-migrations         ^>=0.13.0.0
+    , keiro-migrations         ^>=0.14.0.0
     , kiroku-store-migrations  ^>=0.4.0.0
     , pg-migrate               ^>=1.1.0.0
     , pg-migrate-embed         ^>=1.1.0.0
@@ -75,7 +76,7 @@
     , base                     >=4.18     && <5
     , ephemeral-pg             ^>=0.2.2.0
     , hasql                    >=1.10     && <1.11
-    , kioku-migrations         ^>=0.4.1.0
+    , kioku-migrations         ^>=0.5.0.0
     , pg-migrate-test-support  ^>=1.1.0.0
     , text                     >=2.0      && <2.2
 
@@ -89,9 +90,11 @@
   -- the bridge must keep working until the last codd-era database crosses over.
   ghc-options:    -threaded -rtsopts -with-rtsopts=-N -Wno-deprecations
   build-depends:
-    , base                           >=4.18     && <5
+    , base                           >=4.18      && <5
+    , containers                     >=0.6       && <0.8
     , hasql                          >=1.6
-    , kioku-migrations               ^>=0.4.1.0
+    , keiro-migrations               ^>=0.14.0.0
+    , kioku-migrations               ^>=0.5.0.0
     , kioku-migrations:test-support
     , kiroku-store-migrations        ^>=0.4.0.0
     , pg-migrate                     ^>=1.1.0.0
@@ -99,4 +102,4 @@
     , pg-migrate-import-codd         ^>=1.1.0.0
     , tasty                          >=1.5
     , tasty-hunit                    >=0.10
-    , text                           >=2.0      && <2.2
+    , text                           >=2.0       && <2.2
diff --git a/ledger-fixups/2026-08-19-rebaseline-0011-checksum.sql b/ledger-fixups/2026-08-19-rebaseline-0011-checksum.sql
new file mode 100644
--- /dev/null
+++ b/ledger-fixups/2026-08-19-rebaseline-0011-checksum.sql
@@ -0,0 +1,84 @@
+-- Ledger re-baseline for the corrected payload of Kioku migration 0011 (BUG-1).
+--
+-- kioku-migrations 0.4.0.0 and 0.4.1.0 shipped a payload whose plain
+-- `SET search_path TO kiroku, pg_catalog` survived transaction commit on the
+-- connection pg-migrate reuses for a composed plan. A later host component on
+-- that connection could therefore fail to resolve its own unqualified tables.
+-- The corrected 0011 explicitly resets the session value before committing.
+--
+-- pg-migrate verifies the exact SHA-256 of every applied payload. Correcting
+-- 0011 therefore changes its checksum, and a database that already applied the
+-- withdrawn payload will report MigrationChecksumMismatch until its stored
+-- checksum is re-baselined. This script performs that one re-baseline and
+-- nothing else.
+--
+-- WHEN TO RUN: after taking a verified backup, once per long-lived database
+-- that ALREADY APPLIED kioku/0011 under kioku-migrations 0.4.0.0 or 0.4.1.0,
+-- BEFORE the first `up` or `verify` using the corrected package. A database
+-- where 0011 is still pending does not need this script; it applies the
+-- corrected payload normally. Fresh and ephemeral databases do not need it.
+--
+-- WHY NO SCHEMA MIGRATION FOLLOWS: the withdrawn and corrected payloads have
+-- identical durable schema and data effects. The only withdrawn effect was a
+-- connection-local search_path value, and that ceased to exist when the old
+-- runner connection closed.
+--
+-- SAFETY: the UPDATE matches only the applied kioku/0011 row carrying the exact
+-- withdrawn checksum. It is idempotent: a second run, a pending migration, an
+-- already-corrected row, or any other checksum changes zero rows. This narrow
+-- correction is not permission to bypass any other checksum mismatch.
+--
+-- LEDGER LOCATION: pg-migrate's default ledger schema is `pgmigrate`. If the
+-- host configured a different schema through LedgerConfig, change the schema
+-- name in the to_regclass call below and nowhere else.
+
+BEGIN;
+
+DO $$
+DECLARE
+  ledger_table regclass;
+  withdrawn_checksum bytea :=
+    decode('eee9cd252b32b563c50f8457596347fff1b2e4d3ea4dafe5b45043e991624192', 'hex');
+  corrected_checksum bytea :=
+    decode('6c83d3f01f784d0d9395953d5bb1763b8eea6cd9439073df42f79775a85197a9', 'hex');
+  rebaselined integer;
+BEGIN
+  ledger_table := to_regclass('pgmigrate.migrations');
+
+  IF ledger_table IS NULL THEN
+    RAISE EXCEPTION
+      'Could not find pgmigrate.migrations; edit this script if LedgerConfig uses a different schema';
+  END IF;
+
+  EXECUTE format(
+    'UPDATE %s SET checksum = $1
+       WHERE component = ''kioku''
+         AND migration = ''0011-kioku-memory-space-partition''
+         AND status = ''applied''
+         AND checksum = $2',
+    ledger_table
+  )
+  USING corrected_checksum, withdrawn_checksum;
+
+  GET DIAGNOSTICS rebaselined = ROW_COUNT;
+
+  IF rebaselined = 0 THEN
+    RAISE NOTICE
+      'no applied kioku/0011 row carried the withdrawn checksum; nothing to re-baseline';
+  ELSIF rebaselined = 1 THEN
+    RAISE NOTICE 're-baselined the applied kioku/0011 checksum';
+  ELSE
+    RAISE EXCEPTION 're-baselined % kioku/0011 rows; expected at most one', rebaselined;
+  END IF;
+END $$;
+
+-- Sanity check: kioku/0011 must now carry the corrected checksum, or be absent
+-- because this database has not reached 0011 yet. Expect one row with ok =
+-- true, or zero rows.
+--   SELECT encode(checksum, 'hex') =
+--          '6c83d3f01f784d0d9395953d5bb1763b8eea6cd9439073df42f79775a85197a9' AS ok
+--   FROM pgmigrate.migrations
+--   WHERE component = 'kioku'
+--     AND migration = '0011-kioku-memory-space-partition';
+
+COMMIT;
diff --git a/migrations/0011-kioku-memory-space-partition.sql b/migrations/0011-kioku-memory-space-partition.sql
--- a/migrations/0011-kioku-memory-space-partition.sql
+++ b/migrations/0011-kioku-memory-space-partition.sql
@@ -224,3 +224,10 @@
 -- kioku_turns_session_idx was of UNIQUE (session_id, turn_index) before the schema-hardening
 -- migration dropped it. Keeping it would be pure write amplification.
 DROP INDEX IF EXISTS kioku_scenes_scope_idx;
+
+-- pg-migrate reuses one connection for the complete composed plan, so the plain SET at the
+-- start of this migration would survive transaction commit. Migration 0010 may already have
+-- supplied the same leaked value, which is why changing only this migration's opening SET to
+-- SET LOCAL would not restore the host configuration. Reset the session value explicitly so
+-- migration 0012 and every later component inherit the configured database or role default.
+RESET search_path;
diff --git a/migrations/0013-partition-aware-fts-index.sql b/migrations/0013-partition-aware-fts-index.sql
new file mode 100644
--- /dev/null
+++ b/migrations/0013-partition-aware-fts-index.sql
@@ -0,0 +1,46 @@
+-- Migration: partition-aware-fts-index
+-- Created: 2026-08-21 UTC
+--
+-- Every full-text recall statement constrains an active row by memory space and namespace
+-- before matching content_tsv. btree_gin supplies GIN operator classes for the two text
+-- columns, so one partial index can enforce that complete candidate boundary instead of
+-- enumerating matches from unrelated tenants through the historical content-only GIN.
+--
+-- This is an optional access-path improvement. Some managed PostgreSQL roles cannot install
+-- extensions, so the extension attempt is isolated in a PL/pgSQL subtransaction. The old
+-- content-only GIN is dropped only after the replacement is visible in the catalog; otherwise
+-- it remains as the correctness-preserving fallback.
+
+DO $$
+DECLARE
+  btree_gin_available boolean := false;
+BEGIN
+  BEGIN
+    CREATE EXTENSION IF NOT EXISTS btree_gin;
+  EXCEPTION
+    WHEN OTHERS THEN
+      RAISE NOTICE
+        'btree_gin extension is unavailable (%: %); retaining kioku_memories_tsv_idx',
+        SQLSTATE,
+        SQLERRM;
+  END;
+
+  SELECT EXISTS (
+    SELECT 1
+    FROM pg_catalog.pg_extension
+    WHERE extname = 'btree_gin'
+  ) INTO btree_gin_available;
+
+  IF btree_gin_available THEN
+    CREATE INDEX IF NOT EXISTS kioku_memories_space_namespace_tsv_idx
+      ON kioku.memories USING gin (memory_space_id, namespace, content_tsv)
+      WHERE status = 'active';
+
+    IF to_regclass('kioku.kioku_memories_space_namespace_tsv_idx') IS NOT NULL THEN
+      DROP INDEX IF EXISTS kioku.kioku_memories_tsv_idx;
+    END IF;
+  ELSE
+    RAISE NOTICE
+      'btree_gin is not installed; retaining kioku_memories_tsv_idx as the full-text fallback';
+  END IF;
+END $$;
diff --git a/migrations/manifest b/migrations/manifest
--- a/migrations/manifest
+++ b/migrations/manifest
@@ -10,3 +10,4 @@
 0010-kioku-scope-identity-recompute.sql
 0011-kioku-memory-space-partition.sql
 0012-relocate-projections-to-kioku-schema.sql
+0013-partition-aware-fts-index.sql
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -16,6 +16,7 @@
 import Data.List (sort)
 import Data.List.NonEmpty (NonEmpty (..))
 import Data.Maybe (mapMaybe)
+import Data.Set qualified as Set
 import Data.Text (Text)
 import Data.Text qualified as Text
 import Data.Text.Encoding qualified as Text.Encoding
@@ -31,10 +32,12 @@
     MigrationPlan,
     MigrationReport (..),
     MigrationResult (..),
+    VerificationIssue (..),
     VerificationReport (..),
     connectionProviderFromSettings,
     defaultImportOptions,
     defaultRunOptions,
+    migrationComponentFromEmbeddedSql,
     migrationId,
     migrationPlan,
     runMigrationPlan,
@@ -51,7 +54,8 @@
 import Hasql.Session (Session)
 import Hasql.Session qualified as Session
 import Hasql.Statement (Statement, preparable)
-import Kioku.Migrations (kiokuMigrationPlan)
+import Keiro.Migrations qualified as KeiroMigrations
+import Kioku.Migrations (kiokuMigrationPlan, kiokuMigrations)
 import Kioku.Migrations.History.Codd
   ( cohortCoddHistoryMappings,
     cohortCoddSourceConfig,
@@ -77,10 +81,24 @@
           testCase "public schema (long-lived dev databases)" (assertRegistryBump "public")
         ],
       testCase "the full migration chain applies to a fresh database" testFreshDatabase,
+      testGroup
+        "migration-session isolation"
+        [testCase "restores the host search path before later components" testHostSearchPathRestored],
+      testGroup
+        "released checksum re-baseline"
+        [ testCase "restores strict verification and is idempotent" testLedgerChecksumRebaseline,
+          testCase "rejects a missing default ledger table" testLedgerFixupRequiresDefaultLedger
+        ],
       testCase "the migration manifest is complete and valid" testManifestIntegrity,
       testCase "the pinned Codd history maps 30 known plan targets" testHistoryMappings,
       testCase "the pre-cutover Codd cohort imports 30 rows and applies only the forward migrations" testCoddCohortImport,
       testGroup
+        "the partition-aware full-text index"
+        [ testCase "a normal database replaces the content-only GIN" testPartitionAwareFtsIndex,
+          testCase "extension failure is handled before the fallback can be dropped" testPartitionAwareFtsFallbackOrdering,
+          testCase "re-applying its body changes nothing" testPartitionAwareFtsIndexIdempotent
+        ],
+      testGroup
         "the memory-space partition migration"
         [ testCase "backfills every pre-partition row into the legacy space" testMemorySpaceBackfill,
           testCase "refuses to finish when a derived row disagrees with its session" testMemorySpaceDriftAborts,
@@ -221,6 +239,261 @@
     E.noParams
     (D.singleRow (D.column (D.nonNullable D.bool)))
 
+-- * Migration-session isolation
+
+-- | A host component that follows Kioku on the same pg-migrate connection must inherit the
+-- database's configured search path, not session state left by a Kioku migration.
+testHostSearchPathRestored :: Assertion
+testHostSearchPathRestored =
+  withBareDatabase \connStr -> do
+    withConnection connStr \conn -> do
+      run conn (Session.script hostSchemaSetup)
+      databaseName <- run conn (Session.statement () quotedCurrentDatabase)
+      run conn (Session.script ("ALTER DATABASE " <> databaseName <> " SET search_path TO host_app, pg_catalog"))
+
+    plan <- hostComposedPlan
+    report <-
+      runMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan
+        >>= either (assertFailure . show) pure
+    hostMigration <- either (assertFailure . show) pure (migrationId "host" "0001-host-table")
+    let MigrationReport {results = migrationResults} = report
+    case reverse (toList migrationResults) of
+      MigrationResult {migration = finalMigration, outcome = finalOutcome} : _ -> do
+        finalMigration @?= hostMigration
+        finalOutcome @?= AppliedNow
+      [] -> assertFailure "the composed migration plan returned no results"
+    query connStr hostMigratedColumnExists >>= (@?= True)
+
+hostComposedPlan :: IO MigrationPlan
+hostComposedPlan = do
+  kiroku <- either (fail . show) pure KirokuMigrations.kirokuMigrations
+  keiro <- either (fail . show) pure KeiroMigrations.keiroMigrations
+  kioku <- either (fail . show) pure kiokuMigrations
+  host <-
+    either
+      (fail . show)
+      pure
+      ( migrationComponentFromEmbeddedSql
+          "host"
+          (Set.singleton "kioku")
+          ( ( "0001-host-table.sql",
+              Text.Encoding.encodeUtf8
+                "ALTER TABLE host_table ADD COLUMN migrated boolean NOT NULL DEFAULT true;"
+            )
+              :| []
+          )
+      )
+  either (fail . show) pure (migrationPlan (kiroku :| [keiro, kioku, host]))
+
+hostSchemaSetup :: Text
+hostSchemaSetup =
+  """
+  CREATE SCHEMA host_app;
+  CREATE TABLE host_app.host_table (id bigint PRIMARY KEY);
+  """
+
+quotedCurrentDatabase :: Statement () Text
+quotedCurrentDatabase =
+  preparable
+    "SELECT quote_ident(current_database())"
+    E.noParams
+    (D.singleRow (D.column (D.nonNullable D.text)))
+
+hostMigratedColumnExists :: Statement () Bool
+hostMigratedColumnExists =
+  preparable
+    """
+    SELECT EXISTS (
+      SELECT 1
+      FROM information_schema.columns
+      WHERE table_schema = 'host_app'
+        AND table_name = 'host_table'
+        AND column_name = 'migrated'
+    )
+    """
+    E.noParams
+    (D.singleRow (D.column (D.nonNullable D.bool)))
+
+-- * Released checksum re-baseline
+
+ledgerFixupPath :: FilePath
+ledgerFixupPath = "ledger-fixups/2026-08-19-rebaseline-0011-checksum.sql"
+
+withdrawnKioku0011Checksum :: Text
+withdrawnKioku0011Checksum = "eee9cd252b32b563c50f8457596347fff1b2e4d3ea4dafe5b45043e991624192"
+
+testLedgerChecksumRebaseline :: Assertion
+testLedgerChecksumRebaseline =
+  withBareDatabase \connStr -> do
+    plan <- either (fail . show) pure kiokuMigrationPlan
+    let settings = Settings.connectionString connStr
+    initial <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
+    length (appliedNow initial) @?= 55
+
+    baselineLedger <- query connStr fullLedgerSnapshot
+    fst baselineLedger @?= 55
+    baselineSchema <- query connStr cohortSchemaSnapshotStatement
+    fixup <- Text.IO.readFile ledgerFixupPath
+
+    withConnection connStr \conn -> run conn (Session.statement () installWithdrawnChecksumFixture)
+    query connStr kioku0011Checksum >>= (@?= withdrawnKioku0011Checksum)
+    query connStr cohortSchemaSnapshotStatement >>= (@?= baselineSchema)
+
+    mismatch <- verifyMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
+    expectedMigration <-
+      either
+        (assertFailure . show)
+        pure
+        (migrationId "kioku" "0011-kioku-memory-space-partition")
+    case issues mismatch of
+      [MigrationChecksumMismatch actualMigration _ _] -> actualMigration @?= expectedMigration
+      other -> assertFailure ("expected one MigrationChecksumMismatch, got: " <> show other)
+
+    withConnection connStr \conn -> run conn (Session.script fixup)
+    query connStr fullLedgerSnapshot >>= (@?= baselineLedger)
+    query connStr cohortSchemaSnapshotStatement >>= (@?= baselineSchema)
+    verified <- verifyMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
+    issues verified @?= []
+
+    repeated <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
+    let MigrationReport {results = repeatedResults} = repeated
+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 55
+    length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0
+
+    withConnection connStr \conn -> run conn (Session.script fixup)
+    query connStr fullLedgerSnapshot >>= (@?= baselineLedger)
+    query connStr cohortSchemaSnapshotStatement >>= (@?= baselineSchema)
+
+testLedgerFixupRequiresDefaultLedger :: Assertion
+testLedgerFixupRequiresDefaultLedger =
+  withBareDatabase \connStr -> do
+    fixup <- Text.IO.readFile ledgerFixupPath
+    withConnection connStr \conn -> do
+      result <- Connection.use conn (Session.script fixup)
+      case result of
+        Left err ->
+          assertBool
+            ("missing-ledger error did not explain how to adapt LedgerConfig: " <> show err)
+            ("Could not find pgmigrate.migrations" `Text.isInfixOf` Text.pack (show err))
+        Right () -> assertFailure "ledger re-baseline unexpectedly accepted a missing ledger table"
+
+installWithdrawnChecksumFixture :: Statement () ()
+installWithdrawnChecksumFixture =
+  preparable
+    """
+    UPDATE pgmigrate.migrations
+       SET checksum = decode('eee9cd252b32b563c50f8457596347fff1b2e4d3ea4dafe5b45043e991624192', 'hex')
+     WHERE component = 'kioku'
+       AND migration = '0011-kioku-memory-space-partition'
+       AND status = 'applied'
+    """
+    E.noParams
+    D.noResult
+
+kioku0011Checksum :: Statement () Text
+kioku0011Checksum =
+  preparable
+    """
+    SELECT encode(checksum, 'hex')
+    FROM pgmigrate.migrations
+    WHERE component = 'kioku'
+      AND migration = '0011-kioku-memory-space-partition'
+    """
+    E.noParams
+    (D.singleRow (D.column (D.nonNullable D.text)))
+
+fullLedgerSnapshot :: Statement () (Int64, Text)
+fullLedgerSnapshot =
+  preparable
+    """
+    SELECT count(*),
+           md5(string_agg(
+             component || '/' || migration || ':' || encode(checksum, 'hex') || ':' || status,
+             E'\n' ORDER BY component COLLATE "C", position))
+    FROM pgmigrate.migrations
+    """
+    E.noParams
+    (D.singleRow ((,) <$> required D.int8 <*> required D.text))
+  where
+    required = D.column . D.nonNullable
+
+-- * The partition-aware full-text index
+
+partitionAwareFtsMigration :: FilePath
+partitionAwareFtsMigration = "0013-partition-aware-fts-index.sql"
+
+-- | The ordinary ephemeral role can install @btree_gin@, so the preferred branch must exist as
+-- a catalog fact: extension present, replacement GIN present, historical GIN absent. The index
+-- definition also proves that all three candidate predicates and the active-row partial clause
+-- are carried by the access path.
+testPartitionAwareFtsIndex :: Assertion
+testPartitionAwareFtsIndex =
+  withKiokuMigratedDatabase \connStr -> do
+    (extensionPresent, replacementPresent, fallbackPresent, definition) <-
+      query connStr partitionAwareFtsIndexLayout
+    extensionPresent @?= True
+    replacementPresent @?= True
+    fallbackPresent @?= False
+    mapM_
+      ( \fragment ->
+          assertBool
+            ("partition-aware GIN definition is missing " <> Text.unpack fragment <> ": " <> Text.unpack definition)
+            (fragment `Text.isInfixOf` definition)
+      )
+      [ "USING gin (memory_space_id, namespace, content_tsv)",
+        "WHERE (status = 'active'::text)"
+      ]
+
+-- | The fallback is a source-order guarantee as well as a happy-path database test. A role that
+-- cannot install @btree_gin@ must reach the exception handler before any statement can remove
+-- the old index, and the replacement receives a catalog check before that removal too.
+testPartitionAwareFtsFallbackOrdering :: Assertion
+testPartitionAwareFtsFallbackOrdering = do
+  migration <- loadMigration partitionAwareFtsMigration
+  let dropStatement = "DROP INDEX IF EXISTS kioku.kioku_memories_tsv_idx"
+      (beforeDrop, dropAndAfter) = Text.breakOn dropStatement migration
+  assertBool "the migration never names the historical fallback index" (not (Text.null dropAndAfter))
+  assertBool
+    "the btree_gin exception handler appears after the fallback drop"
+    ("WHEN OTHERS THEN" `Text.isInfixOf` beforeDrop)
+  assertBool
+    "the fallback drop is not guarded by a catalog-visible replacement"
+    ( "IF to_regclass('kioku.kioku_memories_space_namespace_tsv_idx') IS NOT NULL THEN"
+        `Text.isInfixOf` beforeDrop
+    )
+
+testPartitionAwareFtsIndexIdempotent :: Assertion
+testPartitionAwareFtsIndexIdempotent =
+  withKiokuMigratedDatabase \connStr ->
+    withConnection connStr \conn -> do
+      migration <- loadMigration partitionAwareFtsMigration
+      before <- run conn (Session.statement () partitionAwareFtsIndexLayout)
+      run conn (Session.script migration)
+      run conn (Session.statement () partitionAwareFtsIndexLayout) >>= (@?= before)
+
+partitionAwareFtsIndexLayout :: Statement () (Bool, Bool, Bool, Text)
+partitionAwareFtsIndexLayout =
+  preparable
+    """
+    SELECT EXISTS (
+             SELECT 1 FROM pg_catalog.pg_extension WHERE extname = 'btree_gin'
+           ),
+           to_regclass('kioku.kioku_memories_space_namespace_tsv_idx') IS NOT NULL,
+           to_regclass('kioku.kioku_memories_tsv_idx') IS NOT NULL,
+           coalesce(pg_catalog.pg_get_indexdef(
+             to_regclass('kioku.kioku_memories_space_namespace_tsv_idx')::oid
+           ), '')
+    """
+    E.noParams
+    ( D.singleRow
+        ( (,,,)
+            <$> D.column (D.nonNullable D.bool)
+            <*> D.column (D.nonNullable D.bool)
+            <*> D.column (D.nonNullable D.bool)
+            <*> D.column (D.nonNullable D.text)
+        )
+    )
+
 -- * The memory-space partition migration
 
 -- | Prove the backfill on data that genuinely predates the partition.
@@ -555,7 +828,7 @@
     adoption <- runMigrationPlan defaultRunOptions settings full >>= either (assertFailure . show) pure
     let MigrationReport {results = adoptionResults} = adoption
     length [() | MigrationResult {outcome = AlreadyApplied} <- toList adoptionResults] @?= 11
-    length [() | MigrationResult {outcome = AppliedNow} <- toList adoptionResults] @?= 42
+    length [() | MigrationResult {outcome = AppliedNow} <- toList adoptionResults] @?= 44
 
     -- Verified and skipped, never re-executed: the stored rows keep their checksums and their
     -- original application timestamps.
@@ -568,7 +841,7 @@
     verification <- verifyMigrationPlan defaultRunOptions settings full >>= either (assertFailure . show) pure
     let VerificationReport {issues = adoptionIssues, appliedMigrations, pendingMigrations, unknownMigrations} = verification
     adoptionIssues @?= []
-    length appliedMigrations @?= 53
+    length appliedMigrations @?= 55
     pendingMigrations @?= []
     unknownMigrations @?= []
 
@@ -919,13 +1192,13 @@
     verification <- verifyMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
     let VerificationReport {issues = verificationIssues, appliedMigrations, pendingMigrations, unknownMigrations} = verification
     verificationIssues @?= []
-    length appliedMigrations @?= 53
+    length appliedMigrations @?= 55
     pendingMigrations @?= []
     unknownMigrations @?= []
 
     repeated <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure
     let MigrationReport {results = repeatedResults} = repeated
-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 53
+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 55
     length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0
 
 fixtureMigrationNames :: Text -> [FilePath]
@@ -1012,8 +1285,10 @@
           migrationId "keiro" "0028",
           migrationId "keiro" "0029",
           migrationId "keiro" "0030",
+          migrationId "keiro" "0031",
           migrationId "kioku" "0011-kioku-memory-space-partition",
-          migrationId "kioku" "0012-relocate-projections-to-kioku-schema"
+          migrationId "kioku" "0012-relocate-projections-to-kioku-schema",
+          migrationId "kioku" "0013-partition-aware-fts-index"
         ]
 
 expectRight :: (Show error) => Either error value -> value
