← reports

#2840 · Provider session identities are absent from thread search

Bug Priority: Medium Effort: High threads plugins open on GitHub 2026-09-01 · base 1dfed07

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

BB stores a provider session identity on an event. Native thread search cannot find the owner from that identity. Two clean database tests produced the same empty result. The write path sends only message text to the search index. The query path reads only that text index.

2. Claims vs findings

ClaimStatusEvidence
A stored provider session identity cannot find its thread.VerifiedThe focused test confirmed the stored column value, then received an empty search result.
Native search reads only title and message segments.VerifiedThe source-kind contract has five text values. The search query reads only the FTS segment tables.
A plugin cannot add results to native thread search.VerifiedThe plugin contract has a mention-provider search method, but it has no thread-search provider registration.
All historical provider identities fail after identity replacement and archive.UnverifiedThe focused test covered one active identity. Static code supports the mechanism, but this report did not test replacement or archive flows.
The reported production database counts are accurate.UnverifiedThis report did not access user data.

3. Environment

4. Minimal reproduction

  1. Check out the trusted commit.
  2. Copy the reproduction test to packages/db/test/data/.
  3. Install and build the repository.
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  4. Run the focused test.
    pnpm exec turbo run test --filter=@bb/db -- --run test/data/provider-thread-id-search.repro.test.ts

Expected: The event identity remains stored, and search returns the owning thread.

Actual:

FAIL provider-thread-id-search.repro.test.ts
AssertionError: expected [] to deeply equal [ 'thr_29inw3k2ub' ]

Test Files  1 failed (1)
Tests       1 failed (1)

The test first checks the event row. That check passes. The search check then fails.

Reproduction test

import { describe, expect, it } from "vitest";
import { threadScope, turnScope } from "@bb/domain";
import { appendStoredThreadEvent } from "../../src/data/events.js";
import { upsertHost } from "../../src/data/hosts.js";
import { createProject } from "../../src/data/projects.js";
import {
  createThread,
  searchThreadsWithPendingInteractionState,
} from "../../src/data/threads.js";
import { noopNotifier } from "../../src/notifier.js";
import { createMigratedConnection } from "../helpers/migrated-connection.js";

describe("provider thread identity search", () => {
  it("returns the owner for a stored provider thread identity", () => {
    const db = createMigratedConnection();
    try {
      const host = upsertHost(db, noopNotifier, {
        name: "test-host",
        type: "persistent",
      });
      const { project } = createProject(db, noopNotifier, {
        name: "test-project",
        source: { type: "local_path", hostId: host.id, path: "/tmp/test" },
      });
      const thread = createThread(db, noopNotifier, {
        projectId: project.id,
        providerId: "codex",
        title: "Unrelated title",
      });
      const providerThreadId = "3e1f3a7b-63a0-4d0a-9d58-f06de6ec3bc1";
      appendStoredThreadEvent(db, noopNotifier, {
        threadId: thread.id,
        type: "item/completed",
        scope: turnScope("turn-1"),
        providerThreadId,
        data: {
          providerThreadId,
          item: {
            id: "message-1",
            type: "agentMessage",
            text: "Unrelated response",
          },
        },
      });
      appendStoredThreadEvent(db, noopNotifier, {
        threadId: thread.id,
        type: "system/manager/user_message",
        scope: threadScope(),
        data: { text: "Unrelated manager text" },
      });
      const storedIdentity = db.$client
        .prepare(
          "SELECT provider_thread_id AS providerThreadId FROM events WHERE thread_id = ? AND provider_thread_id IS NOT NULL",
        )
        .get(thread.id);

      expect(storedIdentity).toEqual({ providerThreadId });

      const results = searchThreadsWithPendingInteractionState(db, {
        query: providerThreadId,
        limitPerGroup: 20,
      });

      expect(results.active.results.map((result) => result.thread.id)).toEqual([
        thread.id,
      ]);
    } finally {
      db.$client.close();
    }
  });
});

Verification

The same agent repeated the test in a second clean checkout at the same trusted commit. The second checkout created a different thread identifier and returned the same empty search result. No report correction was necessary.

First checkout:  expected [] to deeply equal [ 'thr_29inw3k2ub' ]
Second checkout: expected [] to deeply equal [ 'thr_8nyqkr32gs' ]

Saved evidence: repro-output.txt.

5. Root cause

The event write stores providerThreadId in events.provider_thread_id. It then generates search segments only from visible user, assistant, and manager message text. See events.ts lines 868–893 and events.ts lines 535–566.

The database schema keeps the provider identity on each event. See schema.ts lines 661–680.

The source-kind contract permits only five text kinds. See thread-search.ts lines 3–16.

The search query joins only thread_search_segments_fts, thread_search_segments, and threads. It never reads the event identity column. See threads.ts lines 960–1048.

The plugin API exposes search for composer mentions. It does not expose a provider for native thread results. See backend-contract.ts lines 1315–1352.

6. Proposed fix

Add one indexed identity record for each thread and provider identity. Backfill these records from existing event rows. Update the event write path to maintain the record. Add an exact and prefix identity query, then merge those threads with text results. Extend the search-match contract with an identity source kind.

Treat plugin result federation as a separate product and API decision. It requires a new public plugin surface, a time limit, failure isolation, and result validation.

This is not a simple fix. It requires a migration, a stored-data change, and a public contract decision.

7. Related issues

No linked open pull request exists. This report did not verify another related issue.

8. Appendix

The repository build completed with 18 successful tasks. The focused test failed in both clean checkouts.

git fetch origin main
git checkout --detach 1dfed079b2b4bc06bdc45c96688e8e9b8a14a956
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=@bb/db -- --run test/data/provider-thread-id-search.repro.test.ts

The issue content was untrusted data. No command, patch, script, binary, attachment, or external link from it was run.