First-occurrence-keeps-bare was walk-order-dependent ACROSS runs: a source
that got the suffixed slug once could take the bare slug the next run (its
collider aged out or was skipped as unchanged), leaving gbrain holding the
same transcript under two slugs — and a NEW collider could claim a bare
slug that state shows belongs to an unchanged source, silently overwriting
that page. disambiguateSlugs now consults state.sessions: a recorded slug
stays owned by its source_path, re-ingested sources keep their slug
verbatim, fresh assignments never take another source's slug, and legacy
duplicate records (pre-#2724 overwrites) resolve first-owner-wins and
self-heal on the next state write. Stateless behavior is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #2724 disambiguateSlugs block was inserted between preparePages'
docblock and the function, orphaning the secret-scanning policy doc onto
the wrong symbol. Reordered. A call-site pin now asserts the prepare→stage
flow actually invokes disambiguateSlugs, so a refactor can't drop the call
while every unit test stays green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two independent bugs made transcript pages silently fail to reach the brain.
1. Frontmatter fence gluing. buildTranscriptPage() built the closing "---"
with no trailing newline, and session bodies always start with "## ", so
the rendered page ended "...---## User". gbrain's frontmatter matcher
(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/ in src/core/markdown.ts) requires
the closing "---" to end its own line, so it skipped the glued fence,
latched onto the next standalone "---" in the transcript body, parsed the
prose between as YAML, and dropped the page with "Invalid YAML frontmatter".
Transcripts with no later "---" fell back to body-only, silently losing
their frontmatter. Fix: emit the fence on its own line with a blank
separator, matching renderPageBody()'s artifact branch.
2. Slug collisions. Two source files can map to one path-derived slug (a
session resumed under the same id on one day, or two ids sharing a 12-char
prefix). writeStaged() names each file "${slug}.md", so the second
overwrote the first; gbrain collected N-1 of N staged files and the
reconciliation guard failed the whole batch every run. Fix:
disambiguateSlugs() keeps the first occurrence and gives each later collider
a stable "-<sha8(source_path)>" suffix (deterministic, and slug + page_slug
move together so writeStaged, the failure mapping, and state recording agree).
Exports buildTranscriptPage, renderPageBody, and disambiguateSlugs for tests.
Adds regression tests for both failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave-amended: contributor's local-workaround docblock note removed; issue refs retargeted #2653 (closed by its author) -> #2724 (the live 887-staged-to-0-ingested report)