feat: stable symbol handles across reindexing and agent context compaction #37

Closed
opened 2026-07-21 14:26:17 +02:00 by buildagent · 2 comments
Member

Product

Give agents a durable symbol identity that survives daemon restarts, ordinary reindexing, edits that move a symbol, and context compaction. Numeric symbol_id remains the fast project-local lookup key, but must not be the durable agent contract.

Why

Long-running coding agents pass symbol handles between planning, editing, review, subagents, and later sessions. Current numeric IDs may change after a rebuild or migration. A stale ID can fail closed, or worse, identify a different row. This becomes a prerequisite for trustworthy context_pack (#35), durable review findings (#34), and saved evidence trails.

Proposed handle

A versioned opaque encoding derived from:

  • project identity
  • normalized repository-relative path
  • qualified symbol name
  • kind
  • signature/structure fingerprint

The wire representation should be opaque and versioned so its internals can evolve.

Resolution contract

Add stable_handle to symbol-bearing responses and a resolver that reports one of:

  • exact
  • moved_or_changed with explicit evidence
  • ambiguous with candidates
  • missing

Never silently choose an ambiguous candidate. Project routing must be explicit, and old handle versions must either remain readable or return a structured upgrade error.

Boundaries

  • Handles identify source structure, not semantic equivalence.
  • Renames are not exact matches unless corroborated and must carry confidence.
  • Generated/duplicate definitions require honest ambiguity.
  • Numeric IDs remain valid within the current index generation for efficient drill-down.

Acceptance

  • unchanged symbols resolve after full rebuild with different row IDs
  • moved-line/body-only edits preserve resolution
  • rename/signature/path changes produce moved_or_changed or ambiguity, never false exactness
  • deletion produces missing
  • duplicate qualified names do not resolve arbitrarily
  • multi-project links cannot cross-route silently
  • handles round-trip through context_pack, review_diff, and existing symbol drill-down tools
  • migration and fresh-index paths are equivalent
## Product Give agents a durable symbol identity that survives daemon restarts, ordinary reindexing, edits that move a symbol, and context compaction. Numeric `symbol_id` remains the fast project-local lookup key, but must not be the durable agent contract. ## Why Long-running coding agents pass symbol handles between planning, editing, review, subagents, and later sessions. Current numeric IDs may change after a rebuild or migration. A stale ID can fail closed, or worse, identify a different row. This becomes a prerequisite for trustworthy `context_pack` (#35), durable review findings (#34), and saved evidence trails. ## Proposed handle A versioned opaque encoding derived from: - project identity - normalized repository-relative path - qualified symbol name - kind - signature/structure fingerprint The wire representation should be opaque and versioned so its internals can evolve. ## Resolution contract Add `stable_handle` to symbol-bearing responses and a resolver that reports one of: - `exact` - `moved_or_changed` with explicit evidence - `ambiguous` with candidates - `missing` Never silently choose an ambiguous candidate. Project routing must be explicit, and old handle versions must either remain readable or return a structured upgrade error. ## Boundaries - Handles identify source structure, not semantic equivalence. - Renames are not exact matches unless corroborated and must carry confidence. - Generated/duplicate definitions require honest ambiguity. - Numeric IDs remain valid within the current index generation for efficient drill-down. ## Acceptance - unchanged symbols resolve after full rebuild with different row IDs - moved-line/body-only edits preserve resolution - rename/signature/path changes produce `moved_or_changed` or ambiguity, never false exactness - deletion produces `missing` - duplicate qualified names do not resolve arbitrarily - multi-project links cannot cross-route silently - handles round-trip through `context_pack`, `review_diff`, and existing symbol drill-down tools - migration and fresh-index paths are equivalent
Author
Member

Triage: P1 prerequisite for the agent workflow

Land before, or as the first track of, context_pack (#35). Keep the first version deliberately structural and deterministic. A handle must fail honestly under ambiguity rather than becoming a fuzzy search token.

Implementation should separate the stable external identity from the current-generation numeric ID so existing hot paths remain cheap.

### Triage: P1 prerequisite for the agent workflow Land before, or as the first track of, `context_pack` (#35). Keep the first version deliberately structural and deterministic. A handle must fail honestly under ambiguity rather than becoming a fuzzy search token. Implementation should separate the stable external identity from the current-generation numeric ID so existing hot paths remain cheap.
Author
Member

Shipped in v0.7.0 (mission I031).

What landed

Versioned durable handle cih1_<base64url(payload)> — the payload is a unit-separated join of five stored-column-derived fields: project | path | identity_name | kind | fingerprint. Because it's derived from existing columns there is no schema change, so a healed DB and a fresh index emit byte-identical handles (migration ≡ fresh-index, trivially). The codec lives dependency-free in code_index_core::handle (FNV-1a + base64url inlined) and is shared by both the MCP emit/route side and the daemon resolve side.

  • identity_name = qualified_name when present, else the bare name (resolver applies the same fallback so legacy NULL-qualified_name rows round-trip).
  • fingerprint = 16 hex of FNV-1a over the whitespace-normalized signature. It is signature-shaped, not body-shaped: survives body-only edits and line moves, flips on a signature/shape change.

Resolution contract (resolve_handle → daemon resolve_symbol_identity, run against exactly one project, routed by the key inside the handle — a handle for project A can never resolve against project B):

  • exact — identity set of one, fingerprint matches (survives body edits + line moves).
  • moved_or_changed — with explicit evidence: signature_changed, path_changed, or renamed. Renames require a non-empty fingerprint corroboration (a NULL-signature symbol can never vacuously "rename-match").
  • ambiguous — ≥2 identity candidates, or multiple relaxed matches; returns candidates and never picks.
  • missing — nothing, or the handle's project is absent (with a routing note, never a cross-route).
  • Unknown prefix → unsupported_handle_version; corrupt body → malformed_handle. Never a guess.

stable_handle is now stamped at the MCP boundary (where routing is authoritative) on search_symbols, get_symbol, and file_outline, plus context_pack targets/dependents. The numeric id stays as the cheap generation-local drill-down key.

Acceptance criteria — all met

  • Unchanged symbol resolves exact after a full rebuild with different numeric ids
  • Body-only edit and moved lines still resolve exact
  • Rename / signature change / path move → moved_or_changed with explicit evidence, never a false exact
  • Deletion → missing
  • Duplicate qualified names → ambiguous, never arbitrary
  • A handle carrying project A never resolves against project B
  • Old/unknown handle version → structured error, not a guess
  • Handles round-trip through drill-down (resolve_handle → symbol_id → get_symbol/find_callers)
  • Full gates; migration ≡ fresh-index

Review

Deep adversarial review (13 agents: 7 confirmed / 2 refuted), all confirmed findings fixed with reproduction-backed regression tests. Headline catch — the exact hazard this system exists to prevent: get_symbol(path:line) / file_outline were stamping handles with the requested project key while absolute paths auto-route to linked projects, so a handle decoded project="primary" and resolved against the wrong index. Fixed: routed_index_for_path now returns the resolved project key and both stamp sites use it. Also fixed: the vacuous NULL-signature false-rename above.

Deferred (honest): stable_handle is not yet stamped on change_impact / review_diff / changed_symbols / refactor responses — those still carry numeric ids + paths. That stamping is purely additive and non-blocking for this issue; tracked as a follow-up.

Closing as done.

Shipped in **v0.7.0** (mission I031). ## What landed **Versioned durable handle** `cih1_<base64url(payload)>` — the payload is a unit-separated join of five *stored-column-derived* fields: `project | path | identity_name | kind | fingerprint`. Because it's derived from existing columns there is **no schema change**, so a healed DB and a fresh index emit byte-identical handles (migration ≡ fresh-index, trivially). The codec lives dependency-free in `code_index_core::handle` (FNV-1a + base64url inlined) and is shared by both the MCP emit/route side and the daemon resolve side. - `identity_name` = `qualified_name` when present, else the bare `name` (resolver applies the same fallback so legacy NULL-`qualified_name` rows round-trip). - `fingerprint` = 16 hex of FNV-1a over the **whitespace-normalized signature**. It is *signature-shaped, not body-shaped*: survives body-only edits and line moves, flips on a signature/shape change. **Resolution contract** (`resolve_handle` → daemon `resolve_symbol_identity`, run against exactly one project, routed by the key inside the handle — a handle for project A can never resolve against project B): - **exact** — identity set of one, fingerprint matches (survives body edits + line moves). - **moved_or_changed** — with explicit evidence: `signature_changed`, `path_changed`, or `renamed`. Renames require a *non-empty* fingerprint corroboration (a NULL-signature symbol can never vacuously "rename-match"). - **ambiguous** — ≥2 identity candidates, or multiple relaxed matches; returns candidates and **never picks**. - **missing** — nothing, or the handle's project is absent (with a routing note, never a cross-route). - Unknown prefix → `unsupported_handle_version`; corrupt body → `malformed_handle`. Never a guess. `stable_handle` is now stamped at the MCP boundary (where routing is authoritative) on `search_symbols`, `get_symbol`, and `file_outline`, plus context_pack targets/dependents. The numeric `id` stays as the cheap generation-local drill-down key. ## Acceptance criteria — all met - [x] Unchanged symbol resolves `exact` after a full rebuild with **different numeric ids** - [x] Body-only edit and moved lines still resolve `exact` - [x] Rename / signature change / path move → `moved_or_changed` with explicit evidence, never a false `exact` - [x] Deletion → `missing` - [x] Duplicate qualified names → `ambiguous`, never arbitrary - [x] A handle carrying project A never resolves against project B - [x] Old/unknown handle version → structured error, not a guess - [x] Handles round-trip through drill-down (resolve_handle → symbol_id → get_symbol/find_callers) - [x] Full gates; migration ≡ fresh-index ## Review Deep adversarial review (13 agents: 7 confirmed / 2 refuted), all confirmed findings fixed with reproduction-backed regression tests. Headline catch — the exact hazard this system exists to prevent: `get_symbol(path:line)` / `file_outline` were stamping handles with the *requested* project key while absolute paths auto-route to linked projects, so a handle decoded `project="primary"` and resolved against the wrong index. Fixed: `routed_index_for_path` now returns the resolved project key and both stamp sites use it. Also fixed: the vacuous NULL-signature false-rename above. **Deferred (honest):** `stable_handle` is not yet stamped on `change_impact` / `review_diff` / `changed_symbols` / `refactor` responses — those still carry numeric ids + paths. That stamping is purely additive and non-blocking for this issue; tracked as a follow-up. Closing as done.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
h-dv/code-index#37
No description provided.