Global lessons added since the day-one seed batch do not RETRIEVE — two rows queried near-verbatim are absent from the ranked set, and every global row `search_lessons` returns carries a `cmr11*` id
Type: code · Executor: coding_agent · Repo pinned motir-ai — where the seed source, the migration generator and embedMissingLessons all live. If the diagnosis lands in motir-core's search seam instead, re-pin before building; the pin is a claim, not a finding.
Found 2026-08-29 while running MOTIR-3876, whose planning defect is described by a curated global lesson that the run could not retrieve. Filed here because it carries no dependency edge into that story — the project owes this fix, that story does not.
The measurement (two rows, near-verbatim queries, reproducible)
motir-ai src/seed/lessons.base.ts on origin/main holds 221 scope: 'global' rows.
search_lessons(projectKey: 'MOTIR', limit: 50), queried with the near-verbatim body of the row whosesourceRefisMOTIR-3794("A container cannot hold the card that verifies the deployment its own merge produces…") returned 50 rows. The target was absent. Worst distance in the window:0.751— so it was not merely ranked below the cut, it was not in the ranked set at all.- Of those 50: 45
globalrows, and every single one carries acmr11…id — the original day-one seed batch. The other 5 aretenantrows, three of them recent (cmta2k…,cmt9xy…,cmteai…), which are written throughadd_lesson/ the capture path. - Repeated at
limit: 5with the near-verbatim title of theMOTIR-3805row ("A precondition that is a RELEASE cannot be carried by a dependency edge…"): five rows, allcmr11…, target absent, best distance0.474on an unrelated row.
So: a row written through the MCP retrieves; a row written through the seed migration does not. That is the shape of the defect, and it is the shape of a lot of work — every global lesson authored since the day-one batch, which is what the sourceRef: 'MOTIR-<n>' convention exists for.
What is NOT established, and must be the first deliverable
The CAUSE. Two candidates, and this card must distinguish them before it changes anything:
- The rows are in the store without embeddings.
scripts/generate-lessons-migration.tssays the release runssrc/seed/embedMissingLessons.ts"immediately after", andembedMissingLessons.ts's own header calls an unembedded row a silent failure — so the failure mode is anticipated in the code and the backfill may simply not be running, or may be failing without failing the release. - The migrations have not been applied. Five
*_lessons_base_datamigrations landed between 2026-08-28 18:19 and 2026-08-29 00:01;fly.toml'srelease_commandapplies them, so this requires a release not to have happened — check the deployment, not the repository.
Read it from the deployed database, inside a machine (DATABASE_URL_UNPOOLED): count Lesson rows by scope, and count how many have a null embedding. That single query separates the two candidates and is the reproduction this card is built on.
Why it matters more than a missing row
The corpus has two halves, and only one of them is working. Lessons are still being WRITTEN — carefully, with a curation discipline, a totality suite and a per-row routing decision — and every one of them since the seed batch has been written into a corpus that no planner can read. Every motir run and every planning pass is calling search_lessons, getting outcome: "matched" and a plausible ranked list, and being handed the day-one corpus while believing it has been handed the whole one. A retrieval that returns the wrong rows is indistinguishable from one that returns the right ones, which is why this stood.
It has already cost at least one concrete failure: the MOTIR-3794 row states the exact defect that shipped into MOTIR-3876 a day later — the container that cannot hold the card verifying its own merge's deployment — and neither the plan pass that authored the story nor the run that refused it could retrieve it. See MOTIR-3904.
Acceptance criteria
- The reproduction runs first and is quoted: the deployed-database counts (total
Lessonrows, by scope, and how many carry no embedding), read inside a machine, with the timestamp — before any fix. The two candidate causes above are decided by that reading, and the losing one is stated as excluded. - The fix makes a row delivered by a generated
*_lessons_base_datamigration retrievable bysearch_lessons, whatever the cause turns out to be — the backfill actually running on release, or the release actually happening, or both. - A regression guard exists that fails when a lesson is reachable in the store but not in a search. Not a test that the backfill function works in isolation: the property that broke is written ⇒ retrievable, and it broke while every unit around it stayed green.
- The existing backlog is backfilled, and the proof is a query, not a run of the script:
search_lessonsreturns theMOTIR-3794row for a query naming a container that cannot hold a post-deploy child, and theMOTIR-3805row for one naming a precondition that is a release. Both are quoted in the close-out. - The close-out states how many global rows were unretrievable at the time of the fix, so the blast radius is on the record rather than inferred from this card's title.
- ≥90% coverage on the files this PR touches.
Context refs
motir-ai/src/seed/lessons.base.ts— the 221 global rows and thesourceRef: 'MOTIR-<n>'convention for new onesmotir-ai/src/seed/embedMissingLessons.ts— the backfill, and its own note that an unembedded row is a silent failuremotir-ai/scripts/generate-lessons-migration.ts— the generator, and its claim that the release runs the backfill immediately aftermotir-ai/prisma/migrations/*_lessons_base_data/— five between 2026-08-28 and 2026-08-29motir-ai/fly.toml— therelease_commandthat applies themmotir-corelib/mcp/tools/searchLessons.ts→POST /v1/lessons/search— the read path, if the cause is on that side- MOTIR-3904 — the planning bug this was found beside; its defect is the one the unreachable row describes
- MOTIR-3466 — the story that shipped
search_lessons
Comments (0)
No comments yet — be the first to weigh in.