Skip to content

Fix gitnexus-web server-mode repo switching and query readiness#400

Open
JayceeB1 wants to merge 2 commits intoabhigyanpatwari:mainfrom
JayceeB1:fix/server-mode-worker-hydration
Open

Fix gitnexus-web server-mode repo switching and query readiness#400
JayceeB1 wants to merge 2 commits intoabhigyanpatwari:mainfrom
JayceeB1:fix/server-mode-worker-hydration

Conversation

@JayceeB1
Copy link

Summary

This PR fixes three related problems in gitnexus-web when loading repositories from a local gitnexus serve backend:

  • successful server-mode loads could leave the status bar stuck on Processing...
  • switching to another indexed repo could leave worker-side query features unavailable because the server-loaded graph was never hydrated into BM25/LadybugDB state
  • LadybugDB query handling assumed QueryResult.getAll(), but newer @ladybugdb/wasm-core builds expose getAllObjects() / getAllRows() instead

Changes

  • clear progress after successful server-mode load/switch
  • add worker-side hydrateServerGraph(...) to load backend graph/file contents into local BM25 + LadybugDB state
  • reuse the same worker-state loading path as the local ingestion pipeline
  • make the LadybugDB adapter accept getAllObjects() / getAllRows() with getAll() as fallback
  • update the local type declaration for the current LadybugDB QueryResult shape

Validation

Validated locally in server mode on a non-default indexed repository:

  • repo switch reaches Ready
  • Query panel runs successfully
  • example query:
    MATCH (f:Function) RETURN f.name LIMIT 3

Closes #398

@vercel
Copy link

vercel bot commented Mar 20, 2026

@JayceeB1 is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@abhigyanpatwari
Copy link
Owner

thanks for the PR. can u resolve the conflicts

Copy link

@xkonjin xkonjin left a comment

Choose a reason for hiding this comment

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

Code Review

Overall: Fixes two real bugs: (1) LadybugDB query API compatibility across versions, and (2) server-mode repo switching not hydrating the graph database. Good refactoring of duplicated ingestion logic.

Positives

  • getQueryRows adapter function correctly handles three different QueryResult shapes (getAllObjectsgetAllRowsgetAll), making the code resilient to LadybugDB WASM API changes.
  • loadResultIntoWorkerState deduplicates the graph loading + BM25 indexing that was previously copy-pasted across runPipeline and runPipelineFromFiles. The new hydrateServerGraph reuses it cleanly.
  • The handleServerConnectawait hydrateServerGraph(result) call ensures server-mode connections get a queryable graph, fixing the core bug.
  • Type declaration update (QueryResult making methods optional) matches the runtime adapter pattern.

Issues

  1. getQueryRows preference order may matter: getAllObjects is preferred over getAllRows and getAll. The existing code used getAll() which returns tuple arrays. If getAllObjects returns named-property objects, downstream code that accesses row[0] (tuple style) will break. The fallback chain in callers does handle both (row.cnt ?? row[0]), but executeQuery reconstructs named objects from tuples assuming array rows. If getAllObjects already returns objects, that reconstruction could double-wrap. Worth verifying which shape each method returns and whether the callers handle both consistently.

  2. Error swallowing in hydrateServerGraph: The loadResultIntoWorkerState function catches LadybugDB errors silently. For server mode specifically, if LadybugDB fails to load, the user gets an "exploring" view with no query capability and no indication why. Consider surfacing a non-blocking warning.

  3. Missing setProgress(null) timing: In App.tsx line 159, setProgress(null) is called right after hydrateServerGraph and before view transition. If hydrateServerGraph throws (e.g., corrupt server data), setProgress(null) is never reached and the UI stays stuck on the progress indicator. Wrap in try/finally.

  4. switchRepo dependency array (useAppState.tsx ~1051): hydrateServerGraph was added to the deps, which is correct. But the switchRepo function also calls setProgress(null) in its success path (line 1051) and in its error path (via setTimeout). The success path sets progress to null but never sets it to something first, so the user sees no loading indicator during repo switching. Minor UX gap.

Minor

  • The try { ... } catch { } pattern in loadResultIntoWorkerState (empty catch) should at minimum log to console in dev mode, since the outer callers also have their own try/catch. Silent double-swallow makes debugging harder.

Solid fix. The query adapter pattern is the right long-term approach.

@JayceeB1 JayceeB1 force-pushed the fix/server-mode-worker-hydration branch from 71957f1 to 4f2e4ac Compare March 21, 2026 10:11
@JayceeB1
Copy link
Author

Conflicts resolved. I rebased the branch on top of the current upstream main and force-pushed the updated head.

Validation run locally on the updated branch:

  • cd gitnexus-web && npm run build

The PR is now out of the DIRTY state on my side.

@zander-raycraft
Copy link
Collaborator

@claude this is a bug fix, verify the code fixes the three bugs and give the initial workflow for the original issue this solves, suggest a unit tests to verify it works

@github-actions
Copy link
Contributor

github-actions bot commented Mar 21, 2026

Claude finished @zander-raycraft's task in 1m 42s —— View job


Bug Fix Verification & Analysis

  • Gather context and understand the request
  • Read changed files to verify bug fixes
  • Verify Bug 1: Processing... stuck on successful server-mode load
  • Verify Bug 2: Worker-side query features unavailable after server-mode repo switch
  • Verify Bug 3: LadybugDB getAll() API mismatch
  • Describe original issue workflow
  • Suggest unit tests

Bug Verification

Bug 1 — Status bar stuck on Processing... ✅ Fixed

Root cause: When handleServerConnect completed in App.tsx, it set the view to 'exploring' but never called setProgress(null). The LoadingOverlay renders on viewMode === 'loading' && progress, but the status bar's Processing... message persisted because progress was still set.

How this PR fixes it:

  • App.tsx:164: setProgress(null) is now called right after await hydrateServerGraph(result) before transitioning to 'exploring'.
  • useAppState.tsx:1045: The same setProgress(null) is applied in the switchRepo path.
// App.tsx
await hydrateServerGraph(result);
setViewMode('exploring');
setProgress(null);   // ← new: clears "Processing..." from the status bar

Bug 2 — Worker-side query unavailable after server-mode load/switch ✅ Fixed

Root cause: In server mode, the main thread built a KnowledgeGraph from the server-fetched data for the React UI. But the ingestion worker (which owns LadybugDB and the BM25 index) was never informed — its state stayed empty. Any MATCH ... RETURN query in the Query panel hit throw new Error('Database not ready').

How this PR fixes it:

Two complementary hydration paths are added:

  1. workerApi.hydrateServerGraph (ingestion.worker.ts:345–354) — takes a SerializablePipelineResult, reconstructs the KnowledgeGraph, and calls the shared loadResultIntoWorkerState helper which builds BM25 + loads LadybugDB.

  2. workerApi.hydrateFromServerData (ingestion.worker.ts:221–258) — takes raw nodes/relationships/fileContents, rebuilds the graph in-worker with verbose dev logging, and also calls loadResultIntoWorkerState.

Both paths are triggered in handleServerConnect (App.tsx) and switchRepo (useAppState.tsx):

// First hydration: sets currentGraphResult + BM25 + LadybugDB via the serialized result
await hydrateServerGraph(result);
setViewMode('exploring');

// Second hydration (background): also hydrates + then initializes agent + embeddings
hydrateWorkerFromServer(result.nodes, result.relationships, result.fileContents).then(...)

The loadResultIntoWorkerState helper (ingestion.worker.ts:59–80) deduplicates logic previously copy-pasted across runPipeline and runPipelineFromFiles:

const loadResultIntoWorkerState = async (result: PipelineResult): Promise<void> => {
  currentGraphResult = result;
  storedFileContents = result.fileContents;
  buildBM25Index(storedFileContents);           // BM25 keyword index
  const lbug = await getLbugAdapter();
  await lbug.loadGraphToLbug(result.graph, result.fileContents); // LadybugDB
};

Bug 3 — LadybugDB getAll() API mismatch ✅ Fixed

Root cause: The codebase called result.getAll() on QueryResult, but newer @ladybugdb/wasm-core builds renamed this to getAllObjects() / getAllRows(), causing a runtime TypeError: result.getAll is not a function.

How this PR fixes it:

A new getQueryRows adapter in lbug-adapter.ts:233–245 tries all three shapes in priority order:

const getQueryRows = async (result: any): Promise<any[]> => {
  if (!result) return [];
  if (typeof result.getAllObjects === 'function') return await result.getAllObjects();
  if (typeof result.getAllRows === 'function')   return await result.getAllRows();
  if (typeof result.getAll === 'function')       return await result.getAll();
  throw new Error('Unsupported LadybugDB QueryResult shape');
};

All five call sites that previously used result.getAll() now go through this adapter. The type declaration (lbug-wasm.d.ts) marks all three methods as optional (?) to match.

Note from the existing review: if getAllObjects() returns named-property objects while getAll() returned tuples, the executeQuery reconstruction logic at lbug-adapter.ts:315 (if (Array.isArray(row) && columnNames.length === row.length)) gracefully falls through to rows.push(row) — so already-named objects are passed through unchanged. This is correct.


Original Issue Workflow (Issue #398)

The sequence that reproduced the bugs:

  1. Start backend: npx gitnexus serve (serves one or more indexed repositories via HTTP)
  2. Open gitnexus-web and connect to the server URL
  3. Under the hood, connectToServer() downloads the graph bundle (nodes + relationships + file contents) via HTTP
  4. handleServerConnect() builds the React-side KnowledgeGraph and calls setGraph()/setFileContents()
  5. Bug 1: setProgress(null) was never called → UI showed Processing... indefinitely
  6. Bug 2: The worker's LadybugDB and BM25 were never populated → Query panel threw "Database not ready"
  7. User clicks the repo switcher in the header to load a different indexed repo
  8. switchRepo() re-runs connectToServer() for the new repo — same two bugs reproduce
  9. Bug 3: Any successful query attempt hit result.getAll is not a function on newer WASM builds

Suggested Unit Tests

Test 1 — getQueryRows API adapter (lbug-adapter.test.ts)

import { describe, it, expect, vi } from 'vitest';

// Access the private helper indirectly via executeQuery or mock conn
describe('getQueryRows adapter', () => {
  it('uses getAllObjects() when available', async () => {
    const rows = [{ name: 'foo' }];
    const mockResult = { getAllObjects: vi.fn().mockResolvedValue(rows) };
    // Inject via mock conn.query → verify rows returned correctly
    expect(mockResult.getAllObjects).toHaveBeenCalled();
  });

  it('falls back to getAllRows() when getAllObjects is absent', async () => {
    const rows = [{ name: 'bar' }];
    const mockResult = { getAllRows: vi.fn().mockResolvedValue(rows) };
    // No getAllObjects on the object — should use getAllRows
    expect(mockResult.getAllRows).toHaveBeenCalled();
  });

  it('falls back to getAll() as final fallback', async () => {
    const rows = [['baz']]; // tuple-style
    const mockResult = { getAll: vi.fn().mockResolvedValue(rows) };
    expect(mockResult.getAll).toHaveBeenCalled();
  });

  it('throws when no method is available', async () => {
    const mockResult = {}; // no query result methods
    await expect(getQueryRows(mockResult)).rejects.toThrow('Unsupported LadybugDB QueryResult shape');
  });
});

Test 2 — hydrateServerGraph in the ingestion worker (ingestion.worker.test.ts)

describe('workerApi.hydrateServerGraph', () => {
  it('populates BM25 index and LadybugDB from serialized result', async () => {
    const mockNodes = [{ id: 'func:foo', label: 'Function', properties: { name: 'foo', filePath: 'src/foo.ts' } }];
    const mockRels  = [];
    const mockFiles = { 'src/foo.ts': 'export function foo() {}' };

    await workerApi.hydrateServerGraph({
      nodes: mockNodes,
      relationships: mockRels,
      fileContents: mockFiles,
    });

    // BM25 should be indexed
    expect(isBM25Ready()).toBe(true);

    // LadybugDB should be populated
    const lbug = await getLbugAdapter();
    expect(lbug.isLbugReady()).toBe(true);
    const stats = await lbug.getLbugStats();
    expect(stats.nodes).toBeGreaterThan(0);
  });

  it('makes runQuery usable after hydration', async () => {
    // After hydrateServerGraph, runQuery should not throw "Database not ready"
    const results = await workerApi.runQuery('MATCH (f:Function) RETURN f.name LIMIT 1');
    expect(Array.isArray(results)).toBe(true);
  });
});

Test 3 — setProgress(null) is called after server-mode load (handleServerConnect.test.ts)

describe('handleServerConnect', () => {
  it('clears progress after successful server load', async () => {
    const setProgress = vi.fn();
    const mockResult = buildMockConnectToServerResult();

    // Render with mocked state
    const { result } = renderHook(() => useAppState());

    await act(async () => {
      await result.current.hydrateServerGraph(mockResult);
    });

    // Progress should be null after successful connect
    expect(setProgress).toHaveBeenLastCalledWith(null);
    expect(result.current.viewMode).toBe('exploring');
  });

  it('does not leave viewMode as loading on success', async () => {
    // ...
    expect(result.current.viewMode).not.toBe('loading');
  });
});

Test 4 — switchRepo hydrates the worker for the new repo

describe('switchRepo', () => {
  it('calls hydrateServerGraph for the switched-to repo', async () => {
    const hydrateServerGraph = vi.spyOn(workerApi, 'hydrateServerGraph');
    await switchRepo('other-repo');
    expect(hydrateServerGraph).toHaveBeenCalledWith(expect.objectContaining({
      nodes: expect.any(Array),
      relationships: expect.any(Array),
    }));
  });

  it('clears progress and transitions to exploring on success', async () => {
    await switchRepo('other-repo');
    expect(setProgress).toHaveBeenCalledWith(null);
    expect(viewMode).toBe('exploring');
  });
});

Summary: All three bugs described in the PR are correctly addressed by the changes. The getQueryRows adapter handles the API version skew cleanly, and the hydrateServerGraph/hydrateFromServerData paths ensure the ingestion worker's LadybugDB + BM25 state is populated in both initial server connects and repo switches. The test cases above cover the three bug scenarios and the fallback adapter chain.

@zander-raycraft zander-raycraft added the bug-fix PR tag for bug fixes label Mar 21, 2026
@zander-raycraft
Copy link
Collaborator

@JayceeB1 good work on the bug fixes! Can you integrate these few tests above, test them, and then add that commit to this branch

@JayceeB1
Copy link
Author

Integrated the requested regression tests and pushed them to this branch in a765be ( est: add server-mode hydration regressions).

Added coverage for:

  • getQueryRows fallback order (getAllObjects -> getAllRows -> getAll)
  • serialized server graph reconstruction / hydration delegation used by the worker path

Validation run locally:

  • cd gitnexus-web && npm run test -- src/core/lbug/query-result.test.ts src/workers/server-graph-hydration.test.ts
  • cd gitnexus-web && npm run build

If you want, I can also add a follow-up React-level test for the progress-clearing path, but I kept this commit focused on the two pure regression surfaces with the least test harness overhead.

@zander-raycraft
Copy link
Collaborator

@claude review this and make sure it is ready to merge

@github-actions
Copy link
Contributor

github-actions bot commented Mar 21, 2026

Claude finished @zander-raycraft's task in 2m 48s —— View job


PR Merge Readiness Review

  • Gather context
  • Read changed files
  • Verify all three bug fixes
  • Check reviewer issues (xkonjin)
  • Check tests
  • Check CI status
  • Final merge readiness verdict

Bug Fix Verification

Bug 1 — Status bar stuck on Processing... ✅ Fixed

setProgress(null) is called in both the initial connect path (App.tsx:164) and the switchRepo path (useAppState.tsx:1045) after hydrateServerGraph resolves. Both transitions to 'exploring' are now coupled with the progress clear.

Bug 2 — Worker query unavailable after server-mode load ✅ Fixed

Two complementary hydration paths are in place:

  • workerApi.hydrateServerGraph (synchronous, blocking) — delegates to the shared loadResultIntoWorkerState helper which populates BM25 + LadybugDB
  • hydrateWorkerFromServer (fire-and-forget background) — re-hydrates and then initializes the agent + embeddings

The loadResultIntoWorkerState refactor at ingestion.worker.ts:60–81 cleanly deduplicates what was previously copy-pasted in runPipeline and runPipelineFromFiles.

Bug 3 — LadybugDB getAll() API mismatch ✅ Fixed

query-result.ts introduces a clean getQueryRows adapter that tries getAllObjects → getAllRows → getAll in priority order and throws an explicit error if none are available. All 5 call sites in lbug-adapter.ts now route through it. The lbug-wasm.d.ts type declaration correctly marks all three methods optional.


Reviewer Issues (xkonjin) — Status

# Issue Status
1 getAllObjects shape vs tuple reconstruction ✅ Non-issue — executeQuery at lbug-adapter.ts:302 checks Array.isArray(row) before tuple-to-object conversion; named objects fall through unchanged
2 Silent error swallowing in loadResultIntoWorkerState ⚠️ Partially addressed — DEV-mode logging is present at ingestion.worker.ts:73–76, empty catch in prod is intentional ("LadybugDB is optional")
3 Missing setProgress(null) try/finally ⚠️ Open — if hydrateServerGraph throws on corrupt server data, setProgress(null) is unreachable in handleServerConnect. In the auto-connect path the outer .catch() handles it (with a 3-second delay), but the DropZone path calls handleServerConnect(result) without await at App.tsx:263, meaning a thrown error would be a silent unhandled rejection. Edge case (valid server data won't fail here), but a try/finally would be more robust.
4 No loading indicator during switchRepo ✅ Actually fine — setProgress({ ... 'Switching repository...' }) is set at useAppState.tsx:1001 before the async work begins

Additional Findings

Double hydration (intentional but worth noting): handleServerConnect calls await hydrateServerGraph(result) (synchronous, populates worker state immediately) and then immediately fires hydrateWorkerFromServer(...) as a background task, which re-runs the same BM25 + LadybugDB load. The second pass is needed to kick off agent init + embeddings after the worker is warm. Correct as designed, but LadybugDB gets loaded twice on each server connect/switch.

handleServerConnect not awaited from DropZone (pre-existing + worsened): At App.tsx:263, handleServerConnect(result) is fire-and-forget. Before this PR, the function had no await inside so this was harmless. Now that await hydrateServerGraph(result) is in the body, a failure there becomes an unhandled rejection in the DropZone path. The viewMode would remain 'onboarding' (not stuck on loading), so the user experience degrades gracefully, but it's cleaner to await it.


Tests

The two added test files have solid coverage of the regression surfaces:

  • query-result.test.ts — all four cases: prefers getAllObjects, falls back to getAllRows, falls back to getAll, throws on no method. ✅ Complete
  • server-graph-hydration.test.ts — graph reconstruction (node count, relationship count, file map) and loadResult delegation delegation. ✅ Complete

The author acknowledged the missing React-level setProgress(null) test and noted it was intentionally deferred.


CI Status

The CI run shows action_required — this is the Vercel deployment approval gate (as seen in earlier comments), not a test failure. The actual test suite result is not visible in CI output. Author confirmed npm run build and npm run test pass locally.

The Vercel deployment still needs manual authorization from a team member before it can be previewed.


Merge Readiness Verdict

This PR is ready to merge with one recommended fix before merging:

Recommended (non-blocking for merge): Wrap await hydrateServerGraph(result) + the subsequent state calls in a try/finally in handleServerConnect so setProgress(null) is guaranteed to be called even on unexpected errors. This prevents potential UI stuck states on corrupt server payloads. Fix this →

The three described bugs are correctly fixed, the refactoring is clean, tests cover the key regression paths, and the getQueryRows adapter is a solid long-term solution to the LadybugDB API versioning problem.

@zander-raycraft
Copy link
Collaborator

@JayceeB1 Can you fix that small thing above claude referenced? I will work on merging this after, thank you so much for the fast fix!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix PR tag for bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gitnexus-web: server mode can stay stuck on Processing and leave Query/Processes not ready after repo switch

4 participants