Skip to content

fix(graph): pack symbol-graph upserts by bytes and surface persist failures - #92

Merged
giancarloerra merged 6 commits into
mainfrom
fix/symbol-graph-byte-aware-upserts
Jul 28, 2026
Merged

fix(graph): pack symbol-graph upserts by bytes and surface persist failures#92
giancarloerra merged 6 commits into
mainfrom
fix/symbol-graph-byte-aware-upserts

Conversation

@giancarloerra

@giancarloerra giancarloerra commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Fixes #89.

The bug

On repos with large source files, codebase_graph_build reported success while the symbol graph silently held nothing, so codebase_impact answered "0 callers" for every symbol. Three separate defects stacked up.

1. Root cause: upserts were batched by count, not size. saveFilePayloads sent a fixed 50 points per Qdrant request regardless of how big they were. A per-file payload scales with the source file (a ~900 KB Java file yields a ~6 MB point), so a few large files in one batch pushed the body past Qdrant's service.max_request_size_mb ceiling (32 MiB by default) and the server rejected the entire batch.

Reproduced against real Qdrant v1.17.0, six ~6 MB payloads in one request:

Bad Request | Payload error: JSON payload (36001578 bytes) is larger than allowed (limit: 33554432 bytes)

2. The real reason was thrown away. The Qdrant client puts the server's explanation in err.data.status.error while its message is only the HTTP status text, so the log said Bad Request and nothing else.

3. The failure was recorded as a success. The catch logged, did not rethrow, and the build then wrote an unqualified completion record. The persist also writes payloads, then shards, then metadata last with no rollback, so a mid-way failure leaves committed points but no metadata, which is what makes impact return 0 rather than error.

The fix

  • Byte-aware packing. Requests are now packed up to a byte budget (24 MiB, headroom under the 32 MiB ceiling) as well as the existing 50-point cap. Ordinary repos batch exactly as before; large files simply take more requests.
  • Loud failure instead of an anonymous 400. A point that cannot fit even alone throws SymbolGraphPointTooLargeError naming the file or shard and the server knob that raises the limit, rather than being dropped. Name shards, the one structure that grows unbounded with repo size, are size-checked for the same reason.
  • Real reason surfaced. describeQdrantError digs the server's explanation out of err.data.status.error (through a cause chain), the build records it on GraphBuildCompleted.symbolGraphError, and codebase_graph_status reports that symbol-level tools are incomplete until a rebuild succeeds.

Backward compatibility (verified three ways)

  1. Stored data is byte-identical. Point ids and payload shapes are untouched; only how points are split across HTTP requests changed. I wrote the same workload (120 files + a name shard + a reverse shard) on main and on this branch against a real Qdrant, dumped every persisted point with vectors and payloads, and diffed: identical. Nothing to migrate, existing graphs keep working.
  2. Ordinary batching is unchanged. A regression test pins the historic 50/50/20 split for 120 small files.
  3. No API break. saveFilePayloads keeps its signature; symbolGraphError is an optional additive field; the build still returns and saves the file-import graph when the symbol half fails, exactly as before.

Deliberately not done

Splitting an oversized shard across multiple points would change the on-disk layout, so it is out of scope here; such a shard now fails with a named, actionable error instead. Happy to do it as a follow-up if you want it.

Testing

  • npx tsc --noEmit clean, npm run lint (biome) clean, npm run build succeeds
  • Unit 983/983, integration 167/167 (real Qdrant + Ollama)
  • New unit coverage: byte packing, the oversized-point error, the ordinary-batching regression guard, and describeQdrantError (message/cause/no-duplicate/fallback)
  • Mutation-checked: reverting the byte budget to count-only fails exactly the two tests that matter, so they genuinely guard the fix
  • End-to-end against Qdrant v1.17.0: the old single request fails with the error above, the new path stores all 6 points
  • CodeRabbit local review: no findings

Note, unrelated to this PR: npm audit now reports 3 new advisories (@hono/node-server path traversal, brace-expansion DoS) that were published after the 1.9.1 lockfile refresh. They are present identically on main; this PR changes no dependencies. Worth a separate lockfile refresh.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented whole-request failures during symbol-graph persistence by batching symbol upserts using a byte-budget (while preserving the legacy point-count cap).
    • Added clearer, targeted errors when an individual symbol point or name/reverse shard exceeds Qdrant limits.
    • Improved rebuild transparency: symbol-graph persistence failures now surface as warnings and in codebase_graph_status, including structured failure reasons.
  • Tests
    • Added unit tests for byte-aware batching/splitting, oversized point/shard handling, and Qdrant error reason formatting.
  • Documentation
    • Updated developer guidance for the new byte-budget behavior and half-built graph/persistence reporting.

…ilures

buildCodeGraph reported success while the symbol graph silently held nothing
on repos containing large source files, leaving codebase_impact answering
"0 callers" for every symbol.

saveFilePayloads batched a fixed 50 points per Qdrant request with no regard
for their size. A per-file payload scales with the source file (a ~900 KB Java
file yields a ~6 MB point), so a handful of large files in one batch pushed the
request body past Qdrant's service.max_request_size_mb ceiling (32 MiB by
default) and the server rejected the whole batch with HTTP 400. Reproduced
against Qdrant v1.17.0: six ~6 MB payloads in one request return "Payload
error: JSON payload (36001578 bytes) is larger than allowed (limit: 33554432
bytes)".

Pack each request up to a byte budget as well as the existing point cap, so
ordinary repos batch exactly as before and large files simply take more
requests. Point ids and payload shapes are untouched: the stored data is
byte-identical, verified by writing the same workload on both sides and
diffing every persisted point.

A point that cannot fit even alone now throws SymbolGraphPointTooLargeError
naming the file or shard and the server knob that raises the limit, instead of
being dropped or producing an anonymous 400. Name shards, the one structure
that grows unbounded with repo size, are size-checked for the same reason.

The failure was also invisible twice over: the Qdrant client puts the server's
real explanation in err.data.status.error while its message is the bare HTTP
status text, and the catch logged only that message, did not rethrow, and let
the build record an unqualified success. describeQdrantError now recovers the
real reason (through a cause chain), the build records it on
GraphBuildCompleted.symbolGraphError, and codebase_graph_status reports that
symbol-level tools are incomplete until a rebuild succeeds.

Refs #89
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Symbol-graph persistence now batches Qdrant requests by byte budget and point count, rejects oversized points explicitly, and records enriched persistence failures in graph-build status and codebase_graph_status.

Changes

Symbol Graph Persistence Reliability

Layer / File(s) Summary
Byte-aware symbol-graph upserts
src/constants.ts, src/services/symbol-graph-store.ts, tests/unit/symbol-graph-upsert-budget.test.ts
Qdrant writes enforce byte and point-count limits, validate individual points and shards, preserve ordinary batching, and cover oversized and split-request behavior.
Persistence failure reporting
src/services/qdrant.ts, src/services/code-graph.ts, src/tools/graph-tools.ts, DEVELOPER.md, tests/unit/qdrant-error-wrapping.test.ts, tests/unit/graph-symbol-error-record.test.ts
Qdrant server reasons are decoded through nested causes, symbol-graph failures are stored on completed builds, status and symbol tools report incomplete symbol data, and documentation describes the behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant doRebuildGraph
  participant SymbolGraphStore
  participant Qdrant
  participant codebase_graph_status
  doRebuildGraph->>SymbolGraphStore: persist symbol graph
  SymbolGraphStore->>Qdrant: send byte-bounded upserts
  Qdrant-->>SymbolGraphStore: return success or detailed error
  SymbolGraphStore-->>doRebuildGraph: return persistence result
  doRebuildGraph->>codebase_graph_status: record symbolGraphError when needed
  codebase_graph_status-->>doRebuildGraph: report graph status
Loading

Possibly related PRs

Suggested reviewers: shaitourchin

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: byte-based symbol-graph upserts and surfaced persistence failures.
Description check ✅ Passed Covers summary, changes, testing, and the linked issue; only some template checkbox formatting is omitted.
Linked Issues check ✅ Passed The PR addresses #89 by batching by bytes, surfacing Qdrant errors, and recording symbol-graph failures instead of reporting success.
Out of Scope Changes check ✅ Passed The changes are tightly scoped to Qdrant sizing, error reporting, graph status, docs, and tests for the reported bug.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/symbol-graph-byte-aware-upserts

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/unit/symbol-graph-upsert-budget.test.ts (1)

16-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the always-false-branch conditional type.

CapturedUpsert["points"] extends never ? never : { points: CapturedUpsert["points"] } always resolves to { points: CapturedUpsert["points"] } since the array type is never never. Can be simplified without behavior change.

♻️ Proposed simplification
-const mockUpsert = vi.fn(async (collName: string, body: CapturedUpsert["points"] extends never ? never : { points: CapturedUpsert["points"] }) => {
+const mockUpsert = vi.fn(async (collName: string, body: { points: CapturedUpsert["points"] }) => {
   upserts.push({ collName, points: body.points });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/symbol-graph-upsert-budget.test.ts` around lines 16 - 18, Update
the mockUpsert parameter type in mockUpsert to remove the redundant
CapturedUpsert["points"] extends never conditional and use the equivalent object
type containing points directly, preserving the existing upserts.push behavior.
src/services/symbol-graph-store.ts (1)

160-176: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Flush pending batch before throwing on an oversized point.

When a point exceeds QDRANT_MAX_REQUEST_BYTES (line 163-165), the function throws immediately without flushing whatever is already queued in batch. Those already-validated, budget-compliant points are then silently discarded instead of being persisted, even though they were ready to send.

♻️ Proposed fix
     const bytes = pointBytes(point);
     if (bytes > QDRANT_MAX_REQUEST_BYTES) {
+      await flush();
       throw new SymbolGraphPointTooLargeError(describe(i), bytes);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/symbol-graph-store.ts` around lines 160 - 176, Update the
point-processing loop around pointBytes and SymbolGraphPointTooLargeError to
flush any non-empty pending batch before throwing for a point exceeding
QDRANT_MAX_REQUEST_BYTES. Preserve the existing error and index/size details,
and keep the final flush behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/services/symbol-graph-store.ts`:
- Around line 160-176: Update the point-processing loop around pointBytes and
SymbolGraphPointTooLargeError to flush any non-empty pending batch before
throwing for a point exceeding QDRANT_MAX_REQUEST_BYTES. Preserve the existing
error and index/size details, and keep the final flush behavior unchanged.

In `@tests/unit/symbol-graph-upsert-budget.test.ts`:
- Around line 16-18: Update the mockUpsert parameter type in mockUpsert to
remove the redundant CapturedUpsert["points"] extends never conditional and use
the equivalent object type containing points directly, preserving the existing
upserts.push behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 90226b44-c205-4e68-bfa5-2d0a32444591

📥 Commits

Reviewing files that changed from the base of the PR and between ae6616c and 1fb7928.

📒 Files selected for processing (8)
  • DEVELOPER.md
  • src/constants.ts
  • src/services/code-graph.ts
  • src/services/qdrant.ts
  • src/services/symbol-graph-store.ts
  • src/tools/graph-tools.ts
  • tests/unit/qdrant-error-wrapping.test.ts
  • tests/unit/symbol-graph-upsert-budget.test.ts

…on-oversize choice

Addresses both CodeRabbit nitpicks on #92. The mock's parameter type carried a
conditional that always resolved to the same object type. The upsert loop's
decision not to flush a queued batch before throwing is deliberate, so the
reason is now stated where the throw happens rather than left to be
rediscovered: the throw aborts persistSymbolGraph, no metadata is written, and
the next successful build rewrites every payload, so flushing would only add
points nothing reads to a collection already known to be incomplete.
@giancarloerra

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@DEVELOPER.md`:
- Line 886: Update the documentation text to identify doRebuildGraph as the
function that saves the file-import graph and records
GraphBuildCompleted.symbolGraphError via describeQdrantError; do not refer to
buildCodeGraph for this flow, since it only performs per-file parsing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5777377-f80c-4117-9642-a1e3b146407c

📥 Commits

Reviewing files that changed from the base of the PR and between ae6616c and d1d3a6d.

📒 Files selected for processing (8)
  • DEVELOPER.md
  • src/constants.ts
  • src/services/code-graph.ts
  • src/services/qdrant.ts
  • src/services/symbol-graph-store.ts
  • src/tools/graph-tools.ts
  • tests/unit/qdrant-error-wrapping.test.ts
  • tests/unit/symbol-graph-upsert-budget.test.ts

Comment thread DEVELOPER.md Outdated
…ror recorder

buildCodeGraph only parses files; the persist call, its catch, and the
GraphBuildCompleted record all live in doRebuildGraph. Caught in review.
@giancarloerra

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@giancarloerra giancarloerra self-assigned this Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/code-graph.ts (1)

234-263: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

symbolGraphError is silently cleared by skip-symbol-graph rebuilds, undermining the "incomplete until successful rebuild" guarantee.

symbolGraphError (line 234) is re-initialized to undefined on every call and only ever set inside if (!opts.skipSymbolGraph). Per DEVELOPER.md, rebuildGraph(path, { skipSymbolGraph: true }) is the fully-wired watcher/Phase F path, triggered on ordinary file changes. When that path runs after a prior symbol-graph persistence failure, line 263 overwrites lastGraphBuildCompleted with symbolGraphError: undefined, so codebase_graph_status reports a clean build again — even though the symbol graph was never actually rebuilt or fixed. This defeats the PR's stated goal of not reporting incomplete builds as successful "until a successful rebuild."

Preserve the prior failure across skip-symbol-graph calls instead of resetting it unconditionally:

🐛 Proposed fix
     // Build & persist symbol graph (resolution + sharded persistence) — unless
     // the caller asked to skip it (Phase F watcher path).
-    let symbolGraphError: string | undefined;
+    // A skip-symbol-graph rebuild doesn't touch the symbol graph at all, so it
+    // must not clear a previously-recorded failure — only an actual attempt
+    // (success or new failure) below should change this.
+    let symbolGraphError: string | undefined = opts.skipSymbolGraph
+      ? lastGraphBuildCompleted.get(resolvedPath)?.symbolGraphError
+      : undefined;
     if (!opts.skipSymbolGraph) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/code-graph.ts` around lines 234 - 263, Preserve an existing
symbol-graph failure when rebuildGraph is called with opts.skipSymbolGraph
enabled. Initialize symbolGraphError from the prior lastGraphBuildCompleted
entry for resolvedPath, and only clear or replace it after a non-skipped
symbol-graph rebuild succeeds or fails; keep the existing error recording
behavior in the catch block and ensure skip-only rebuilds do not report a clean
status.
🧹 Nitpick comments (2)
src/services/symbol-graph-store.ts (1)

144-185: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Batching logic verified against test scenarios — correct.

Hand-traced the byte-budget/point-count loop against the "splits by BYTES", "oversized-but-writable", and "keeps historic 50-points" test cases; batch boundaries match expected output in all three.

Minor nit: UPSERT_MAX_POINTS (line 184) is referenced inside upsertWithinBudget (line 174) before its declaration. Safe today (function bodies only evaluate free variables when called), but consider moving the constant above the function for readability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/symbol-graph-store.ts` around lines 144 - 185, Move the
UPSERT_MAX_POINTS constant above the upsertWithinBudget function so its
declaration appears before the function that references it, without changing the
batching logic or constant value.
src/constants.ts (1)

115-115: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Ceiling is hardcoded to Qdrant's default, not the deployed server's actual limit.

The comment correctly notes self-hosted deployments may set service.max_request_size_mb differently, but QDRANT_MAX_REQUEST_BYTES (and therefore QDRANT_UPSERT_BUDGET_BYTES) is a fixed default. If an operator lowers the server limit (or a proxy imposes a stricter one), this fix still produces requests that exceed it and reproduces issue #89 at a different threshold. Consider deriving this from an env var (e.g. QDRANT_MAX_REQUEST_SIZE_MB), consistent with the existing QDRANT_URL/QDRANT_HOST env-driven config pattern in src/services/qdrant.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/constants.ts` at line 115, Update QDRANT_MAX_REQUEST_BYTES to derive from
an environment-configured request-size limit, using the existing
QDRANT_URL/QDRANT_HOST configuration pattern in the qdrant service and
preserving the current default when the variable is absent or invalid. Ensure
QDRANT_UPSERT_BUDGET_BYTES continues to use the derived byte limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/services/code-graph.ts`:
- Around line 234-263: Preserve an existing symbol-graph failure when
rebuildGraph is called with opts.skipSymbolGraph enabled. Initialize
symbolGraphError from the prior lastGraphBuildCompleted entry for resolvedPath,
and only clear or replace it after a non-skipped symbol-graph rebuild succeeds
or fails; keep the existing error recording behavior in the catch block and
ensure skip-only rebuilds do not report a clean status.

---

Nitpick comments:
In `@src/constants.ts`:
- Line 115: Update QDRANT_MAX_REQUEST_BYTES to derive from an
environment-configured request-size limit, using the existing
QDRANT_URL/QDRANT_HOST configuration pattern in the qdrant service and
preserving the current default when the variable is absent or invalid. Ensure
QDRANT_UPSERT_BUDGET_BYTES continues to use the derived byte limit.

In `@src/services/symbol-graph-store.ts`:
- Around line 144-185: Move the UPSERT_MAX_POINTS constant above the
upsertWithinBudget function so its declaration appears before the function that
references it, without changing the batching logic or constant value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 762f38e9-e849-4d75-8202-34b2ff5167d6

📥 Commits

Reviewing files that changed from the base of the PR and between ae6616c and 0a4f9e0.

📒 Files selected for processing (8)
  • DEVELOPER.md
  • src/constants.ts
  • src/services/code-graph.ts
  • src/services/qdrant.ts
  • src/services/symbol-graph-store.ts
  • src/tools/graph-tools.ts
  • tests/unit/qdrant-error-wrapping.test.ts
  • tests/unit/symbol-graph-upsert-budget.test.ts

…ebuilds

A build that passes skipSymbolGraph never attempts symbol-graph persistence, so
it has no standing to declare the symbol graph healthy — but it still wrote the
completion record, clearing symbolGraphError. indexer.ts drives exactly that on
the watcher path (rebuildGraph with skipSymbolGraph: useIncremental), so a
single edited file after a failed persist reported a clean build while the
symbol graph was still broken, which is the reporting gap this change set out
to close.

Carry the recorded failure forward when the symbol graph is skipped; only an
actual successful persist clears it. Covered by a test that fails without the
carry-forward, plus cases pinning the unwrapped server reason and the clear-on-
success path.

Caught in review of #92.
@giancarloerra

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 12 minutes.

@giancarloerra

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/code-graph.ts (1)

234-291: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Total rebuild failure silently clears a previously recorded symbolGraphError.

The outer catch (lines 278-286) overwrites lastGraphBuildCompleted without symbolGraphError, dropping any prior symbol-graph failure. If this build fails before reaching the symbol-graph phase (e.g. transient Qdrant outage during saveGraphData), and a subsequent incremental rebuild runs with skipSymbolGraph: true, it reads symbolGraphError as undefined (line 261) and silently reports the symbol graph as healthy even though it was never actually fixed — exactly the false-success reporting this PR sets out to prevent.

🐛 Proposed fix
   } catch (err) {
     const message = err instanceof Error ? err.message : String(err);
     progress.error = message;
     lastGraphBuildCompleted.set(resolvedPath, {
       completedAt: Date.now(),
       durationMs: Date.now() - progress.startedAt,
       filesProcessed: progress.filesProcessed,
       filesSkipped: progress.filesSkipped,
       nodesCreated: 0,
       edgesCreated: 0,
       error: message,
+      symbolGraphError: lastGraphBuildCompleted.get(resolvedPath)?.symbolGraphError,
     });
     throw err;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/code-graph.ts` around lines 234 - 291, Preserve the previously
recorded symbolGraphError in the outer failure handler of the build flow. When
updating lastGraphBuildCompleted in the catch block, carry forward the value
from the existing record for resolvedPath unless the current build has
explicitly established a new symbol-graph error; keep the existing error field
and failure metadata unchanged.
🧹 Nitpick comments (2)
DEVELOPER.md (1)

1190-1199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

codebase_graph_status reference doc doesn't mention the new symbol-graph-failure line.

The "If ready" bullet doesn't note the new Symbol graph FAILED to persist: ... warning added in src/tools/graph-tools.ts (lines 317-325).

📝 Proposed fix
   If ready: Status READY with node/edge count, last built time, cache status, last build duration, and files skipped when non-zero
+    (plus a "Symbol graph FAILED to persist" warning when the last symbol-graph persist failed)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DEVELOPER.md` around lines 1190 - 1199, Update the codebase_graph_status “If
ready” documentation to mention that the response may include the warning
“Symbol graph FAILED to persist: ...”, matching the behavior implemented in the
graph status tool.
tests/unit/symbol-graph-upsert-budget.test.ts (1)

112-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for saveFilePayload and saveReverseShard size guards.

This suite tests the new assertPointFits guard via saveNameShard, but saveFilePayload (singular) and saveReverseShard gained the identical guard (src/services/symbol-graph-store.ts lines 266-273, 398-404) with no equivalent test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/symbol-graph-upsert-budget.test.ts` around lines 112 - 133, Add
tests covering the size guards in saveFilePayload and saveReverseShard,
mirroring the oversized-point rejection and zero-upsert assertions already used
for saveNameShard. Verify each rejects with SymbolGraphPointTooLargeError and
includes the relevant file or reverse-shard identifier in the error message,
while preserving existing success-path coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/services/code-graph.ts`:
- Around line 234-291: Preserve the previously recorded symbolGraphError in the
outer failure handler of the build flow. When updating lastGraphBuildCompleted
in the catch block, carry forward the value from the existing record for
resolvedPath unless the current build has explicitly established a new
symbol-graph error; keep the existing error field and failure metadata
unchanged.

---

Nitpick comments:
In `@DEVELOPER.md`:
- Around line 1190-1199: Update the codebase_graph_status “If ready”
documentation to mention that the response may include the warning “Symbol graph
FAILED to persist: ...”, matching the behavior implemented in the graph status
tool.

In `@tests/unit/symbol-graph-upsert-budget.test.ts`:
- Around line 112-133: Add tests covering the size guards in saveFilePayload and
saveReverseShard, mirroring the oversized-point rejection and zero-upsert
assertions already used for saveNameShard. Verify each rejects with
SymbolGraphPointTooLargeError and includes the relevant file or reverse-shard
identifier in the error message, while preserving existing success-path
coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e50e29e-3094-4bb9-9c72-2350d6eb1519

📥 Commits

Reviewing files that changed from the base of the PR and between ae6616c and 79034e3.

📒 Files selected for processing (9)
  • DEVELOPER.md
  • src/constants.ts
  • src/services/code-graph.ts
  • src/services/qdrant.ts
  • src/services/symbol-graph-store.ts
  • src/tools/graph-tools.ts
  • tests/unit/graph-symbol-error-record.test.ts
  • tests/unit/qdrant-error-wrapping.test.ts
  • tests/unit/symbol-graph-upsert-budget.test.ts

…ure too

The skipSymbolGraph carry-forward closed one route to false success; the outer
catch was the other. A build that dies before the symbol-graph phase (a
transient Qdrant outage during saveGraphData, say) rewrote the completion
record without symbolGraphError, erasing the failure the previous build
recorded. The next incremental build then carried that blank forward and
reported the symbol graph healthy while it was still broken.

Preserve the recorded failure there as well, read back from the map since the
catch sits outside the scope of the try's local. Covered by a test that drives
a rejected saveGraphData and asserts both the build error and the retained
symbol-graph reason; it fails without the fix.

Caught in review of #92.
@giancarloerra

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/unit/symbol-graph-upsert-budget.test.ts (1)

1-134: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Solid coverage of the byte-budget batching contract.

Batching, oversized-point-of-its-own, and hard-failure paths are all exercised and match the traced implementation.

One minor gap: saveReverseShard's oversized-shard path (mirroring the saveNameShard test at Line 112) isn't directly tested, though it shares the same assertPointFits guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/symbol-graph-upsert-budget.test.ts` around lines 1 - 134, Add a
unit test covering saveReverseShard’s oversized-shard failure path, mirroring
the existing saveNameShard test and using a payload exceeding
QDRANT_MAX_REQUEST_BYTES. Assert that it rejects with
SymbolGraphPointTooLargeError, identifies the reverse shard, and performs no
upserts.
src/tools/graph-tools.ts (1)

343-352: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Symbol-level query tools don't surface symbolGraphError, so a stale/incomplete cache can still answer silently.

Only codebase_graph_status warns about lastBuild.symbolGraphError. codebase_impact, codebase_flow, and codebase_symbol answer straight from getSymbolGraphCache without checking it, so if the latest symbol-graph persist failed (e.g. a single oversized point/shard) but an older successful cache is still present, these tools would keep answering with stale data and no indication anything is wrong — the same "0 callers, no explanation" pattern issue #89 is about, just for the narrower case the byte-budget batching in this PR doesn't eliminate.

Consider prepending the same caveat used in status when getLastGraphBuildCompleted(resolved)?.symbolGraphError is set.

Also applies to: 370-375, 429-437

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/graph-tools.ts` around lines 343 - 352, The symbol-level query
tools do not report a recent symbol-graph persistence error when using an older
cache. In the handlers for codebase_impact, codebase_flow, and codebase_symbol,
call getLastGraphBuildCompleted with the resolved project context and prepend
the same caveat used by codebase_graph_status when symbolGraphError is set,
while preserving the existing query results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/tools/graph-tools.ts`:
- Around line 343-352: The symbol-level query tools do not report a recent
symbol-graph persistence error when using an older cache. In the handlers for
codebase_impact, codebase_flow, and codebase_symbol, call
getLastGraphBuildCompleted with the resolved project context and prepend the
same caveat used by codebase_graph_status when symbolGraphError is set, while
preserving the existing query results.

In `@tests/unit/symbol-graph-upsert-budget.test.ts`:
- Around line 1-134: Add a unit test covering saveReverseShard’s oversized-shard
failure path, mirroring the existing saveNameShard test and using a payload
exceeding QDRANT_MAX_REQUEST_BYTES. Assert that it rejects with
SymbolGraphPointTooLargeError, identifies the reverse shard, and performs no
upserts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fbce55ce-a9eb-4587-b8b0-51450d3c2d94

📥 Commits

Reviewing files that changed from the base of the PR and between ae6616c and 4cd0f48.

📒 Files selected for processing (9)
  • DEVELOPER.md
  • src/constants.ts
  • src/services/code-graph.ts
  • src/services/qdrant.ts
  • src/services/symbol-graph-store.ts
  • src/tools/graph-tools.ts
  • tests/unit/graph-symbol-error-record.test.ts
  • tests/unit/qdrant-error-wrapping.test.ts
  • tests/unit/symbol-graph-upsert-budget.test.ts

…o persist

codebase_graph_status reported a failed symbol-graph persist, but the tools that
actually answer from it did not. codebase_impact, codebase_flow, codebase_symbol
and codebase_symbols read whatever cache survived, so after a failed persist
they answer from stale or absent data with nothing on screen to say why, which
is the report behind #89 in the narrower case where an older cache is still
present.

Wrap the dispatch and prepend the recorded failure for those four tools. Also
pin saveReverseShard's oversized-shard guard, which shared assertPointFits with
the name-shard case but was reached by a separate call path and untested.

Both raised in review of #92.
@giancarloerra

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/code-graph.ts`:
- Around line 243-261: Update the incremental watcher flow around
updateChangedFilesSymbolGraph() so Phase F failures are captured and propagated
into symbolGraphError before the fallback full rebuild records
GraphBuildCompleted. Preserve the failure detail from indexer.ts rather than
leaving the field empty, ensuring the subsequent lastGraphBuildCompleted status
retains shard or payload errors even when the fallback rebuild succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ac85e83-37cc-4581-b2c7-8d549861e810

📥 Commits

Reviewing files that changed from the base of the PR and between ae6616c and 73f2ab6.

📒 Files selected for processing (9)
  • DEVELOPER.md
  • src/constants.ts
  • src/services/code-graph.ts
  • src/services/qdrant.ts
  • src/services/symbol-graph-store.ts
  • src/tools/graph-tools.ts
  • tests/unit/graph-symbol-error-record.test.ts
  • tests/unit/qdrant-error-wrapping.test.ts
  • tests/unit/symbol-graph-upsert-budget.test.ts

Comment thread src/services/code-graph.ts
@giancarloerra
giancarloerra merged commit d6dc17a into main Jul 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant