References Bind To Stable IDs, Not Handles
Why every stored reference points at a stable identity with the handle as a display hint, and why the Obsidian-style wikilink model was abandoned
Decision
Every stored reference binds to a stable identity. The handle is a display hint, never the pointer.
Terminology — 2026-08-15
An authored @ token is a Mention. Resolving that Mention against the
current document produces a Resolved Mention: a contextual projection with
the target's current name and content, not a second stored object. “Reference”
remains a useful generic word for relationships elsewhere in the system, but
it does not name this authored-token-to-context pipeline.
The stored form is <@id|hint>, parsed and rendered in
packages/domain/src/mention/mention-language.ts, and carries
{ id, hint, modifier }. The app resolves
by id. The hint carries the authored handle so stored content stays
human-readable and greppable, and so a rename can refresh the displayed text
without touching the binding.
This applies anywhere a handle is stored as a pointer. A handle used as a
display value, or as the address a human types, is unaffected — authors go on
writing @executor_details, and the UI goes on showing it.
Context
The earlier intent was to run the whole product offline over plain files with
skills, using Obsidian-style [[wikilink]] references: the human-readable
name was the pointer. That model is attractive on paper — files stay
portable, and any tool that can read text can follow a link.
It does not survive rename. A handle that appears in a dozen references becomes a dozen breakages the moment it changes, and every one of those sites must be found and rewritten for the document to stay correct. Correctness that depends on a rewrite sweep completing is not correctness.
The priority is that the app works seamlessly. That requires that a correct reference stays correct when a handle is renamed, which requires binding to something that does not change. Offline is now a secondary venue.
What this supersedes
Code written under the wikilink model still exists and reads as deliberate. Two known sites:
parseFlowFacetinpackages/domain/src/block/block-facet-authoring.tsstrips the@fromafter:and stores the bare handle, commented as intentional — "both name the same sibling. Storage is the plain handle." That comment records the superseded model.updateBlockLabelused to rewrite siblings'flow.afterwhen a block was renamed. Gone (verified 2026-08-02). The About cutover (7165737) replaced the three-way Label/Display/Flow dispatch withupdateBlockAboutand took the rename sweep with it. The only surviving mention ofupdateBlockLabelis a fixture string insrc/lib/block-notification.node.test.ts:106. This bullet is kept because the reasoning still explains why the sweep existed — not because the code does.
Treat a handle-storage comment as evidence of the old model, not as a constraint to preserve.
Amendment, 2026-07-29 — two writers still mint handles
An architecture review found that this decision landed in only one of the
three sites that write flow.after. updateBlockFlow resolves to
blockKey; addDataBlock (convex/documents.ts:1358-1360) and
blockReuse.ts:110-112 both still store a handle.
The consequence is sharper than drift: a backfill of existing handle pointers cannot converge while those two keep writing the old form. The rename sweep above therefore cannot simply be deleted in favour of a one-shot backfill — the writers must be fixed first.
The settled order is recorded in Block Edits Run Inside One
Envelope: extract resolveFlowAfter
as a pure function and route all three writers through it, then backfill,
then delete the sweep and this section's second bullet with it.
Status, 2026-08-04 — migration and anchor retirement complete
Re-verified against the tree:
-
Step 1 (fix the writers) is done.
resolveFlowAfteris extracted atconvex/blockFold.ts:335, andblockReuse.ts:131routes through it. The blocking claim above — that two writers still mint handles — no longer holds. -
Step 3 (delete the sweep) is done, though it landed out of order: the About cutover removed
updateBlockLabelwholesale rather than the backfill clearing the way for it. -
Step 2 has now run on dev (2026-08-02).
backfillFlowAfterKeys(convex/documents.ts:1228) rewrote 7,441 handle-form pointers across 120 documents with 0 dangling. It isrouteKey-scoped, so it is run per document.A second migration was needed alongside it and is easy to miss:
backfillFlowAfterKeysonly rewrites pointers, and does not writeorder.position.blockOrderMigration.migrateBlockOrderdoes that — on dev it positioned 1,142 modules and 8,606 blocks that were still sorting via the anchor fallback. Run that one first; it derives positions from the anchor chain, so it needs the relationships intact.Both are idempotent — a second full pass reported zero writes and skipped everything.
Production is empty and needs no data rewrite. On 2026-08-04 the current Convex functions were deployed to production, then the formal
migrateBlockOrderdry run scanned zero document versions and reported zero module or block writes with no anchor problems. A read-only count confirmed zerodocumentVersions,moduleVersions, andblockVersions, so there are norouteKeyvalues forbackfillFlowAfterKeysto process. Neither non-dry migration was run.Two
blockVersionsremain without a position permanently: they carry nomoduleVersionKey, so no parent module or document owns them and no migration can reach them. Orphaned rows, not a migration defect — butverifyBlockOrderMigrationwill always reportblocksWithoutPosition: 2.Do not read
blockFold.ts:269's note about the fold tolerating bare handles as evidence the backfill is unwritten. That comment describes the reader, and the reader keeps its legacy fallback until the rows are known clean on every deployment that matters.
The out-of-order deletion is worth naming: this section warned that deleting the sweep before backfilling "makes the fold's tolerance of handle pointers permanent." That has effectively happened — the tolerance is now load-bearing until the backfill runs, rather than a temporary accommodation. It is not broken, but the safety margin the ordering was protecting is spent.
The completed migration gate allowed the anchor machinery to be retired on
2026-08-04. Current reads sort only by order.position; current insertion
paths write scalar positions; the migration functions and pointer backfill are
gone. The detail above is retained as migration history.
Relationship to the no-runtime local venue
Deterministic Write-Safety For Hosted App Flow Conditions states a target of no-runtime local execution: an agent reads authored module files directly, without Convex or an executable resolver. That target is not withdrawn, but it is now explicitly secondary to the hosted app.
The hint is what keeps it reachable. A stored reference remains legible
without a database, so a local agent can read and reason about authored files.
What is not guaranteed is that the hint alone resolves unambiguously — no
dual-resolution machinery is built, and handle uniqueness is not strengthened
to support binding by hint. If the local venue later needs guaranteed
resolution without identity lookup, that is a new decision.
Import-reconciliation sketch, 2026-08-08 (design intent, unbuilt)
When the local round trip is built, the owner-settled sketch is:
- Export carries keys. The YAML exports each block/module/field's stable key beside its handle (the Identifier Model already promises "UI never shows it, export does"). On re-import, a known id is an update to that thing regardless of its current handle; the handle is a display hint catching up.
- Offline minting uses ULID-shaped keys. An unfamiliar well-formed id imports as new. A familiar id whose row contradicts the import's claim (wrong kind, wrong scope) is refused — that is duplication or tampering, not a collision.
- The ordering invariant that must not be violated: resolve identities
first; then compile every reference within the import's own namespace
(an offline-authored
@executor_detailsbinds to the import's block of that handle before anything else is consulted); only then reconcile handle collisions. Suffixing (executor_details_2) is safe only after binding, when a handle is purely a display hint — suffixing before compilation silently re-points intra-import references at pre-existing blocks, which is the silent-wrong-output failure class. Every suffix is reported, never silent; the interactive refuse-don't-suffix rule stays, since import has no human at write time and suffix-and-report is its batch equivalent.
Consequences
venue: "after"inReferenceCompileAdapterbecomes meaningful. It is declared but never read; it was placed for this and never wired.- The temporary
flow.afteridentity migration is complete and its runtime resolver has been deleted. Stable-ID binding remains the rule for references; ordering no longer stores a pointer at all. - The field-identity backfill already landed for exactly this reason: field
declarations carry stable
fld_identities before any reference binds to them. This decision generalizes that invariant beyond fields.
The Side-Panel Goldie Is Maker Only
Owner ruling 2026-09-15 — the interview lives in the side panel and the side panel is drawn only for a Maker; in Builder mode Goldie is the in-folio agent on the card, proposing edits to that folio and its children and proposing new folios below.
The Folio Carries the Product
Owner sittings 2026-08-28 — the farm bet is two stakes, the Folio carries all five jobs and the Stack is replaceable furniture; AI parity is the defensible core; the Builder authors a kit, the Maker is interviewed, and the interview is a projection of the same folios the document prints from