← reports

#2319 · provider-claude-code: unknown custom entry type agent-session-bridge-state floods sdk/unknown log

Bug Priority: Low Effort: — providers provider-claude-code open on GitHub 2026-08-24 · base 494f66526

Verdict: REPRODUCED — but in provider-pi, not provider-claude-code · Root-cause confidence: high

1. TL;DR

The reporter sees a stream of "Unhandled … event / SDK Unknown / Raw event: sdk/unknown" rows whose payload is a { type: "custom", customType: "agent-session-bridge-state", … } entry written by the agent-session-bridge extension. The issue blames plugins/provider-claude-code/src/visibility.ts. That attribution is wrong: agent-session-bridge-state is written by the bridge's Pi extension via pi.appendEntry(...), the Claude Agent SDK has no type: "custom" message at all, and the Claude parser would label such a payload sdk/custom, never sdk/unknown.

The real mechanism: since Pi 0.84 every pi.appendEntry(customType, data) call makes Pi emit an entry_appended session event on its RPC stream, with the custom entry nested as entry. bb's Pi bridge forwards every RPC event verbatim as an sdk/message, and plugins/provider-pi/src/visibility.ts has no case for entry_appended, so toPiSdkEventType() returns "unknown", the event is classified { kind: "sdk/unknown", coverage: "unknown" }, and the translator persists a provider/unhandled thread event for each one. The bridge extension persists its state on every message_end and turn_end, so a one-line prompt produces three such rows. Reproduced end to end on a live bb instance with a real Pi turn (3 provider/unhandled rows per turn) and with a unit test at the exact code path. Harmless for correctness, but it pollutes the event log, the timeline (always in dev builds; behind "Show unhandled provider events" in release builds) and every plugin subscribed to thread events.

2. Claims vs findings

Claim from the issueStatusEvidence
visibility.ts in provider-claude-code parses every sdk/message; an unrecognized type falls through to { kind: "sdk/unknown" }Refuted (wrong provider)Claude Code's default branch returns { kind: "sdk/unknown", sdkType: type } and describeParsedClaudeRawEvent turns that into kind: "sdk/custom" (L400-405, L437-441). Cross-check test issue-2319.crosscheck.test.ts passes: the quoted payload yields { kind: "sdk/custom", coverage: "unknown" }. The literal text "SDK Unknown / Raw event: sdk/unknown" can only come from the Pi parser, whose unknown branch carries no type (pi visibility L271-272, L298-299).
Symptom: high-frequency "SDK Unknown / Raw event: sdk/unknown" noise, harmlessVerified (Pi provider)Live Pi thread thr_w3xqexe3gn: the prompt "Reply only with ok." produced 3 provider/unhandled events with rawType: "sdk/unknown" (event sequence, screenshot). The turn itself completed normally ("ok").
The payload is params.message = { type: "custom", customType: "agent-session-bridge-state", … }Partially verified (shape differs)Pi 0.84.x does not send the custom entry as the message itself; it sends { type: "entry_appended", entry: { type: "custom", customType: "agent-session-bridge-state", data: {…}, id, parentId, timestamp } } (raw pi RPC stdout, dist/core/agent-session.js:1954-1959 of pi-coding-agent 0.84.3). The issue most likely abbreviated the nesting; either shape ends in sdk/unknown today.
agent-session-bridge-state is emitted by the session-bridge extension running inside Claude Code threads; it mirrors sessions to local JSONL (mirrorPath)Refuted (it is a Pi extension)packages/pi/index.ts of agent-session-bridge: const CUSTOM_TYPE = "agent-session-bridge-state", pi.appendEntry(CUSTOM_TYPE, serializePiBridgeState(state)) on message_end and turn_end (copy: agent-session-bridge-pi-index.ts). The bridge's Claude Code package is a hook CLI that writes files; it injects no SDK messages (hook-cli.ts). mirrorPath is the Claude JSONL the Pi session is mirrored to, which explains the confusion.
It fires frequently on thread/agent state changesVerifiedOnce per message_end (user and assistant) plus once per turn_end: 3 entries for a single-turn prompt, more with tool calls.
Handle it like command_lifecycle / assistant in the Claude parserRefuted (wrong file)Adding a case to the Claude parser changes nothing for this event. The fix belongs in plugins/provider-pi/src/visibility.ts (see §6; verified with a 4-line patch).
"logged heavily"Unverified as bb log outputbb's server and host-daemon logs contain zero "unhandled" lines for the repro threads. The noise is persisted provider/unhandled thread events, rendered as timeline rows (dev builds always, release builds behind Settings → "Show unhandled provider events", data.ts L333-335), by bb thread log, and delivered to plugins subscribed to thread events — presumably the reporter's own plugin log.
No changes needed in bb-plugin-prompt-suggesterVerifiedRoot cause is entirely in the provider parser; the plugin (server.ts) neither produces nor special-cases these events.

3. Environment

4. Minimal reproduction

4a. Unit level (fastest, no accounts)

  1. Save issue-2319.repro.test.ts as plugins/provider-pi/src/issue-2319.repro.test.ts and run it from plugins/provider-pi:
    pnpm exec vitest run src/issue-2319.repro.test.ts
    
    expected (desired behaviour): the two "wanted:" cases pass, the two "(the bug)" cases fail
    actual on 494f66526:
     Test Files  1 passed (1)
          Tests  2 passed | 2 expected fail (4)
    The two "(the bug)" cases pass, i.e. describeRawEvent returns { kind: "sdk/unknown", coverage: "unknown" } and the translator+assembler emit exactly one provider/unhandled (rawType: "sdk/unknown") per entry_appended, both mid-turn and idle. The two it.fails cases encode the desired behaviour and fail because the bug exists (log).
/**
 * Repro for get-bb/bb#2319.
 *
 * A Pi extension that calls `pi.appendEntry("agent-session-bridge-state", …)`
 * (bohdanpodvirnyi/agent-session-bridge, packages/pi/index.ts) makes Pi emit
 * an `entry_appended` AgentSessionEvent whose `entry` is the custom session
 * entry `{ type: "custom", customType, data }`. bb's Pi bridge forwards every
 * RPC event verbatim as an `sdk/message` envelope; `parsePiRawEvent` has no
 * case for `entry_appended`, so the envelope is classified
 * `{ kind: "sdk/unknown", coverage: "unknown" }` and becomes a persisted
 * `provider/unhandled` event (the "SDK Unknown / Raw event: sdk/unknown" row).
 *
 * The two `it.fails` cases document the desired behaviour (the entry is
 * inert noise); they fail on 494f66526 because the bug exists.
 */
import { describe, expect, it } from "vitest";
import { createDeltaAssembler } from "@bb/provider-bridge-protocol/assembler";
import { createPiDeltaTranslator } from "./delta-translation.js";
import { piVisibilityMetadata } from "./visibility.js";

const BRIDGE_STATE_ENTRY = {
  type: "custom",
  id: "entry-7",
  parentId: "entry-6",
  timestamp: "2026-08-24T19:05:11.123Z",
  customType: "agent-session-bridge-state",
  data: {
    mirrorSessionId: "11111111-2222-3333-4444-555555555555",
    mirrorPath:
      "/Users/example/.claude/projects/-tmp-qa/11111111-2222-3333-4444-555555555555.jsonl",
    updatedAt: "2026-08-24T19:05:11.123Z",
  },
};

/** Exactly what `pi --mode rpc` prints after `pi.appendEntry(...)`. */
const ENTRY_APPENDED_ENVELOPE = {
  jsonrpc: "2.0" as const,
  method: "sdk/message",
  params: {
    threadId: "pi-thread-1",
    message: { type: "entry_appended", entry: BRIDGE_STATE_ENTRY },
  },
};

/** The shape quoted in the issue body (the entry itself as the message). */
const ISSUE_QUOTED_ENVELOPE = {
  jsonrpc: "2.0" as const,
  method: "sdk/message",
  params: { threadId: "pi-thread-1", message: BRIDGE_STATE_ENTRY },
};

describe("#2319 pi custom session entries", () => {
  it("classifies entry_appended as sdk/unknown with unknown coverage (the bug)", () => {
    expect(piVisibilityMetadata.describeRawEvent(ENTRY_APPENDED_ENVELOPE)).toEqual({
      kind: "sdk/unknown",
      coverage: "unknown",
    });
    expect(piVisibilityMetadata.describeRawEvent(ISSUE_QUOTED_ENVELOPE)).toEqual({
      kind: "sdk/unknown",
      coverage: "unknown",
    });
  });

  it("persists one provider/unhandled row per appendEntry, mid-turn and idle (the bug)", () => {
    const translator = createPiDeltaTranslator({
      resolveModelContextWindow: () => null,
    });
    const assembler = createDeltaAssembler({
      providerId: "pi",
      entropyPrefix: "pi-2319",
      textDeltaFlushMs: 0,
    });
    const assemble = (event: unknown) =>
      assembler.assemble({
        threadId: "bb-thread-1",
        deltas: translator.translate(event, { threadId: "bb-thread-1" }),
      });

    // Idle thread (no open turn): the extension persists state on turn_end
    // after the turn settled.
    const idle = assemble(ENTRY_APPENDED_ENVELOPE);
    expect(idle.map((event) => event.type)).toEqual(["provider/unhandled"]);
    expect(idle[0]).toMatchObject({
      type: "provider/unhandled",
      providerId: "pi",
      rawType: "sdk/unknown",
      rawEvent: ENTRY_APPENDED_ENVELOPE,
    });

    // Mid-turn: message_end fires inside the run, so the row is turn-scoped.
    assemble({
      jsonrpc: "2.0",
      method: "sdk/message",
      params: { threadId: "pi-thread-1", message: { type: "agent_start" } },
    });
    const midTurn = assemble(ENTRY_APPENDED_ENVELOPE);
    expect(midTurn.map((event) => event.type)).toEqual(["provider/unhandled"]);
    expect(midTurn[0]).toMatchObject({ rawType: "sdk/unknown" });
  });

  it.fails("wanted: entry_appended is classified as noise", () => {
    expect(piVisibilityMetadata.describeRawEvent(ENTRY_APPENDED_ENVELOPE).coverage).toBe(
      "noise",
    );
  });

  it.fails("wanted: entry_appended translates to no thread events", () => {
    const translator = createPiDeltaTranslator({
      resolveModelContextWindow: () => null,
    });
    expect(
      translator.translate(ENTRY_APPENDED_ENVELOPE, { threadId: "bb-thread-1" }),
    ).toEqual([]);
  });
});

4b. What Pi actually sends (no bb involved)

  1. Save the 15-line stand-in extension bridge-state.ts (same pi.appendEntry("agent-session-bridge-state", …) calls on message_end/turn_end as the real extension) to /tmp/bb-2319-ext/bridge-state.ts.
  2. Run run-pi-rpc.sh: it pipes one prompt command into pi --mode rpc --no-session --extension /tmp/bb-2319-ext/bridge-state.ts --model anthropic/claude-haiku-4-5 and captures stdout.
    bash /tmp/bb-2319-ext/run-pi-rpc.sh
    
    pi exit=0
          13 /tmp/bb-reports/issues/2319/repro/pi-rpc-stdout.jsonl
       3 "type":"entry_appended"
       3 "type":"custom"
       2 "type":"message_start"
       2 "type":"message_end"
       1 "type":"turn_start"
       1 "type":"turn_end"
       1 "type":"agent_start"
       1 "type":"agent_settled"
       1 "type":"agent_end"
    --- entry_appended lines:
    5:{"type":"entry_appended","entry":{"type":"custom","customType":"agent-session-bridge-state","data":{"mirrorSessionId":"11111111-2222-3333-4444-555555555555","mirrorPath":"/Users/example/.claude/projects/-tmp-qa/11111111-2222-3333-4444-555555555555.jsonl","updatedAt":"2026-08-24T19:05:00.144Z"},"id":"dd6b3a53","parentId":"280d04b1","timestamp":"2026-08-24T19:05:00.144Z"}}
    8:{"type":"entry_appended","entry":{"type":"custom","customType":"agent-session-bridge-state", …
    10:{"type":"entry_appended","entry":{"type":"custom","customType":"agent-session-bridge-state", …
    Full capture: pi-rpc-stdout.jsonl. (This run hit the expired Anthropic OAuth token, so the assistant message is an error — the three entry_appended events are emitted regardless.)

4c. End to end in bb (live instance, real Pi turn)

  1. Start your own dev instance with the stand-in extension injected into bb's Pi child (the bridge reads these two variables from its own environment, rpc-child.ts L94-113):
    export BB_PI_BRIDGE_COMMAND=pi
    export BB_PI_BRIDGE_ARGS='["--extension","/tmp/bb-2319-ext/bridge-state.ts"]'
    scripts/bb-dev-app current          # prints App/Server/Host daemon URLs
    eval "$(scripts/bb-dev-app env)"
  2. Create a scratch repo and project, then spawn a Pi thread with a one-word prompt:
    mkdir -p /tmp/bb-2319-qa && cd /tmp/bb-2319-qa && git init -q && echo "# qa" > README.md && git add . && git commit -qm init
    curl -s -X POST $BB_SERVER_URL/api/v1/projects -H 'content-type: application/json' \
      -d '{"name":"qa","source":{"type":"local_path","path":"/tmp/bb-2319-qa","hostId":"host_jtb266n7z8"}}'
    pnpm bb:dev thread spawn --json --project proj_isxu4c4j25 --environment /tmp/bb-2319-qa --machine host_jtb266n7z8 \
      --provider pi --model github-copilot/claude-haiku-4.5 --title "2319 repro (copilot)" --prompt "Reply only with ok."
    pnpm bb:dev thread wait thr_w3xqexe3gn --status idle
    pnpm bb:dev thread log thr_w3xqexe3gn --format minimal
  3. Expected: User → "ok", nothing else. Actual (full output):
    ── User ────────────────────────────────────────────────────
    Reply only with ok.
    ── Worked for (2s) ─────────────────────────────────────────
    ── Assistant ───────────────────────────────────────────────
    ok
    ── Unhandled Pi event ──────────────────────────────────────
      SDK Unknown
      Raw event: sdk/unknown
      Payload:
      {
        "jsonrpc": "2.0",
        "method": "sdk/message",
        "params": {
          "message": {
            "type": "entry_appended",
            "entry": {
              "type": "custom",
              "customType": "agent-session-bridge-state",
              "data": {
                "mirrorSessionId": "11111111-2222-3333-4444-555555555555",
                "mirrorPath": "/Users/example/.claude/projects/-tmp-qa/11111111-2222-3333-4444-555555555555.jsonl",
                "updatedAt": "2026-08-24T19:09:43.241Z"
              },
              "id": "ce9379fb",
              "parentId": "a1f798e3",
              "timestamp": "2026-08-24T19:09:43.241Z"
            }
          },
          "threadId": "thr_w3xqexe3gn"
        }
      }
    ── Unhandled Pi event ──────────────────────────────────────
      … (two more identical rows)
    Raw events (bb thread log --json --all, json, seq summary): three persisted provider/unhandled events with data.providerId: "pi", data.rawType: "sdk/unknown", all turn-scoped (seq 6, 13, 14 of 18).
bb thread view after the Pi turn: the assistant reply 'ok' followed by two 'Unhandled Pi event' rows
Thread 2319 repro (copilot) right after the turn: under the reply "ok", the timeline shows "Unhandled Pi event" rows (dev build; release builds show them when Settings → "Show unhandled provider events" is on).
First unhandled row expanded: SDK Unknown, Raw event: sdk/unknown, payload with type entry_appended and customType agent-session-bridge-state
The first row expanded: "SDK Unknown / Raw event: sdk/unknown" with the entry_appended payload carrying customType: "agent-session-bridge-state" and mirrorPath — the exact text the issue quotes.

The same three rows appeared on the first attempt with anthropic/claude-haiku-4-5, where the model call failed on an expired OAuth token (seq, verbose log): the noise is independent of the turn's success.

Repro files: 2319/repro/

5. Root cause

  1. Pi emits a session event for every custom entry. In pi-coding-agent 0.84.x, the extension API's appendEntry appends the entry and then this._emit({ type: "entry_appended", entry }) (dist/core/agent-session.js:1954-1959); the AgentSessionEvent union in the SDK bb vendors (0.84.0) declares entry_appended alongside session_info_changed, thinking_level_changed, queue_update, summarization_retry_* and bash_execution_update. RPC mode prints every session event on stdout unchanged (dist/modes/rpc/rpc-mode.js:265-270, toJsonEvent only rewrites message_update).
  2. bb's Pi bridge forwards everything. PiRpcSession.handleEvent accepts any object with a string type (rpc-session.ts L596-640) and createOnPiEvent wraps it as sdk/message { threadId, message: event } (bridge.ts L404-418).
  3. The translator has no case, so visibility decides. translate() only short-circuits agent_settled (PI_IGNORED_EVENT_TYPES); piEventTypeSchema (L54-69) rejects entry_appended, the recursive call returns [], and the envelope falls into unhandledDeltas() (L583-608, L517-530), which emits an unhandled delta whenever coverage is "unknown".
  4. Visibility's event list is stale. toPiSdkEventType() maps anything outside its 14 known types to "unknown" (visibility.ts L115-135); parsePiRawEvent returns { kind: "sdk/unknown" } with no type attached (L271-272) and describeParsedPiRawEvent rates it { kind: "sdk/unknown", coverage: "unknown" } (L298-299). That kind becomes rawType, which the UI humanises to "SDK Unknown" and prints as "Raw event: sdk/unknown" (provider-unhandled-detail.ts L38-47, parse-operation-message.ts L475-488).
  5. The assembler persists it. The unhandled delta becomes a stored provider/unhandled thread event (turn-scoped when a turn is open), so it is in the DB, in bb thread log, in the timeline (dev builds unconditionally, release builds behind the "Show unhandled provider events" setting), and in every plugin's thread-event subscription.

Why the issue points at Claude Code. The extension's state is literally the path of a Claude Code JSONL mirror (mirrorPath), the repo has a packages/claude-code hook package, and the Claude and Pi visibility files are near-identical in structure. But the Claude path cannot produce the quoted label: its unknown branch keeps sdkType and would print "SDK Custom / Raw event: sdk/custom" (cross-check test, log), and the Claude Agent SDK never emits type: "custom".

Deeper issue. Every Pi session-level event added since the visibility list was written — entry_appended, session_info_changed, thinking_level_changed, queue_update, summarization_retry_scheduled/_attempt_start/_finished, bash_execution_update — lands in the same sdk/unknown bucket. entry_appended is just the one that fires on every turn for anyone with a state-persisting extension. The visibility file has no test that pins the Pi SDK's AgentSessionEvent union against toPiSdkEventType, so Pi upgrades drift silently.

6. Proposed fix (first principles)

Classify entry_appended as known noise in plugins/provider-pi/src/visibility.ts: add it to PiSdkEventType, toPiSdkEventType, the sdk/simple branch of parsePiRawEvent, and the noise group of describeParsedPiRawEvent. No translator change is needed — the translator already yields nothing for it, and the bridge/host-daemon wire is untouched (no HOST_DAEMON_PROTOCOL_VERSION bump). Verified: with the patch below (proposed-fix-visibility.diff) the two "wanted" cases of the repro test pass and the two "(the bug)" cases fail (log), and the existing 57 Pi visibility/translation tests still pass (log). The patch was reverted from the worktree afterwards.

--- a/plugins/provider-pi/src/visibility.ts
+++ b/plugins/provider-pi/src/visibility.ts
@@ -36,6 +36,7 @@ type PiSdkEventType =
   | "auto_retry_start"
   | "compaction_end"
   | "compaction_start"
+  | "entry_appended"
   | "message_end"
@@ -120,6 +121,7 @@ function toPiSdkEventType(type: string | undefined): PiSdkEventType {
     case "compaction_end":
     case "compaction_start":
+    case "entry_appended":
     case "message_end":
@@ -259,6 +261,7 @@ function parsePiRawEvent(event: JsonRpcMessage): PiRawEvent {
     case "compaction_end":
     case "compaction_start":
+    case "entry_appended":
     case "tool_execution_end":
@@ -309,6 +312,10 @@ function describeParsedPiRawEvent(
         case "auto_retry_end":
         case "auto_retry_start":
+        // `pi.appendEntry(customType, data)`: an extension persisted private
+        // state in the session file. Custom entries never enter the LLM
+        // context and carry nothing for bb's transcript.
+        case "entry_appended":
         case "turn_end":
         case "turn_start":
           return { kind: `sdk/${event.sdkType}`, coverage: "noise" };

Do the same for the other Pi 0.84 session events in the same change (session_info_changed, thinking_level_changed, queue_update, summarization_retry_*, bash_execution_update — the last one only when the translator does not want to render it), and add a test that feeds each member of the vendored SDK's AgentSessionEvent["type"] union through toPiSdkEventType so the next Pi bump fails loudly instead of leaking sdk/unknown rows. The Pi-side delta-translation.test.ts case "scopes unknown sdk envelopes to the active turn" still covers genuinely unknown types (future_event).

What could go wrong: nothing user-visible is lost — custom entries are explicitly "not in LLM context" per Pi's docs — but if a future bb feature wants to surface extension-persisted state (e.g. a thread/extensionState/updated bridge), it would need to translate entry_appended rather than rely on the unhandled fallback. Do not add the suggested case to the Claude parser: it is dead code there.

Housekeeping for the issue: retitle/relabel from provider-claude-code to provider-pi.

7. PR review

No pull request is linked to this issue (searched gh search prs --repo get-bb/bb 2319 and agent-session-bridge-state: none).

8. Related issues

9. Appendix

Raw pi RPC stream (direct run, first 13 lines, truncated)

{"id":"req-1","type":"response","command":"prompt","success":true}
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"Reply only with ok."}],"timestamp":1787598300142}}
{"type":"entry_appended","entry":{"type":"custom","customType":"agent-session-bridge-state","data":{…},"id":"dd6b3a53","parentId":"280d04b1","timestamp":"2026-08-24T19:05:00.144Z"}}
{"type":"message_end","message":{"role":"user",…}}
{"type":"message_start","message":{"role":"assistant",…,"stopReason":"error","errorMessage":"OAuth refresh failed for anthropic: …"}}
{"type":"entry_appended","entry":{"type":"custom","customType":"agent-session-bridge-state",…}}
{"type":"message_end","message":{"role":"assistant",…}}
{"type":"entry_appended","entry":{"type":"custom","customType":"agent-session-bridge-state",…}}
{"type":"turn_end",…}
{"type":"agent_end",…}
{"type":"agent_settled"}

Persisted event (bb thread log --json, thread thr_z2u6srjejq, seq 10)

{
  "id": "evt_…",
  "scope": { "kind": "turn", "turnId": "dad65b6d76-t1" },
  "threadId": "thr_z2u6srjejq",
  "seq": 10,
  "type": "provider/unhandled",
  "data": {
    "providerThreadId": "thr_z2u6srjejq",
    "providerId": "pi",
    "rawType": "sdk/unknown",
    "rawEvent": {
      "jsonrpc": "2.0",
      "method": "sdk/message",
      "params": {
        "message": {
          "type": "entry_appended",
          "entry": {
            "type": "custom",
            "customType": "agent-session-bridge-state",
            "data": { "mirrorSessionId": "1111…", "mirrorPath": "/Users/example/.claude/projects/-tmp-qa/1111….jsonl", "updatedAt": "2026-08-24T19:08:20.099Z" },
            "id": "dabc6bdf", "parentId": "8fef396f", "timestamp": "2026-08-24T19:08:20.099Z"
          }
        },
        "threadId": "thr_z2u6srjejq"
      }
    }
  }
}

Event sequence, successful thread thr_w3xqexe3gn

seq  type                               scope  turn           message.type
1    client/turn/requested              thread
2    client/thread/start                thread
3    thread/identity                    thread
4    turn/started                       turn   dad65b6d76-t2
5    turn/input/accepted                turn   dad65b6d76-t2
6    provider/unhandled                 turn   dad65b6d76-t2  entry_appended
7    item/started                       turn   dad65b6d76-t2
8    item/reasoning/textDelta           turn   dad65b6d76-t2
10   item/completed                     turn   dad65b6d76-t2
11   item/started                       turn   dad65b6d76-t2
12   item/agentMessage/delta            turn   dad65b6d76-t2
13   provider/unhandled                 turn   dad65b6d76-t2  entry_appended
14   provider/unhandled                 turn   dad65b6d76-t2  entry_appended
15   thread/contextWindowUsage/updated  turn   dad65b6d76-t2
16   item/completed                     turn   dad65b6d76-t2
17   thread/tokenUsage/updated          turn   dad65b6d76-t2
18   turn/completed                     turn   dad65b6d76-t2

Evidence the Claude SDK has no custom message

$ grep -n "type SDKMessage =" node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts
3727: export declare type SDKMessage = SDKAssistantMessage | SDKUserMessage | SDKUserMessageReplay | SDKResultMessage
      | SDKSystemMessage | SDKPartialAssistantMessage | SDKCompactBoundaryMessage | SDKStatusMessage | SDKAPIRetryMessage
      | … | SDKPromptSuggestionMessage | SDKMirrorErrorMessage | SDKInformationalMessage;
$ grep -n "customType" sdk.d.ts agentSdkTypes.d.ts sdk-tools.d.ts      # no matches
$ grep -ao '.{80}customType.{80}' "$(which claude)" | head -3          # only mermaid gitgraph code (…type:si.MERGE,customType:i…)

Pi source (0.84.3) that emits the event

// dist/core/agent-session.js
appendEntry: (customType, data) => {
    const entryId = this.sessionManager.appendCustomEntry(customType, data);
    const entry = this.sessionManager.getEntry(entryId);
    if (entry) {
        this._emit({ type: "entry_appended", entry });
    }
},
// dist/modes/rpc/rpc-mode.js
unsubscribe = session.subscribe((event) => {
    output(toJsonEvent(event));
    …
});

Commands run (abridged)

gh issue view 2319 --repo get-bb/bb --json …
pnpm install --frozen-lockfile --prefer-offline && pnpm exec turbo run build
git fetch origin main && git log 494f66526..origin/main --oneline -- plugins/provider-pi plugins/provider-claude-code   # empty
gh api -X GET search/code -f q='"agent-session-bridge-state"'   # → bohdanpodvirnyi/agent-session-bridge packages/pi/index.ts
npm pack @earendil-works/pi-coding-agent@0.84.3 (inspected dist/core/agent-session.js, dist/modes/rpc/rpc-mode.js, docs/extensions.md)
pnpm exec vitest run src/issue-2319.repro.test.ts            (plugins/provider-pi)
pnpm exec vitest run src/issue-2319.crosscheck.test.ts       (plugins/provider-claude-code)
bash /tmp/bb-2319-ext/run-pi-rpc.sh
BB_PI_BRIDGE_COMMAND=pi BB_PI_BRIDGE_ARGS='["--extension","/tmp/bb-2319-ext/bridge-state.ts"]' scripts/bb-dev-app current
curl -s -X POST $BB_SERVER_URL/api/v1/projects …
pnpm bb:dev thread spawn … --provider pi --model github-copilot/claude-haiku-4.5 --prompt "Reply only with ok."
pnpm bb:dev thread wait thr_w3xqexe3gn --status idle
pnpm bb:dev thread log thr_w3xqexe3gn --json --all / --format minimal / --format verbose
doobie --headless < shot4.js   (screenshots)
pnpm dev:stop; rm -rf data dir, /tmp/pi-2319, /tmp/bb-2319-*