Skip to content

Upgrade the Pi SDK and refresh its model catalog - #867

Merged
SawyerHood merged 7 commits into
ymichael:mainfrom
amadad:fix/pi-082-model-catalog
Jul 28, 2026
Merged

Upgrade the Pi SDK and refresh its model catalog#867
SawyerHood merged 7 commits into
ymichael:mainfrom
amadad:fix/pi-082-model-catalog

Conversation

@amadad

@amadad amadad commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Move BB's Pi bridge and server inference helpers from @mariozechner/pi-* 0.70.5 to @earendil-works/pi-* 0.82.0.
  • Use one shared ModelRuntime for model resolution, authentication, session creation, and catalog refresh. The picker now reflects each model's supported reasoning levels, including max.
  • Keep retryable Pi failures inside the active BB turn by honoring agent_end.willRetry.
  • Bundle Pi's jiti/static dependency and register Pi's static OAuth loaders so the standalone bridge can authenticate without filesystem-only dynamic imports.
  • Raise the supported Node.js floor to 22.19 and remove the Node 20 compatibility job and shim.

Why

BB was pinned to Pi 0.70.5 under the retired package namespace. Its static model APIs could not expose models added through Pi's current catalog. The new runtime API keeps session and catalog behavior under one owner while preserving BB's existing bridge protocol.

The server pins @opentelemetry/api to the workspace version so Pi AI and Drizzle resolve one shared type identity. This pin is a build-time type-identity workaround expressed as a runtime dependency; see the "Pinned Dependencies" table in docs/repository-overview.md before changing it.

Compatibility

  • Minimum Node.js is 22.19, which is Pi's own engines.node floor. 22.19, 24, and 26 are the tested lines. install-machine.sh gates on the floor only rather than an allow-list of tested majors, so a release line we have not tested yet still installs instead of hard-failing the day it ships. The bb-app npm engines field lists the tested lines, which npm surfaces as a warning rather than an install failure.
  • Pi remains an SDK bundled with BB. This does not switch the provider to an independently installed Pi CLI.
  • The server-to-daemon wire contract gains max as a Pi reasoning level, so HOST_DAEMON_PROTOCOL_VERSION moves 65 → 66.

Review follow-ups included in this PR

  • Node range. The install script had moved from an open-ended floor to a closed 22/24/26 allow-list, which would hard-fail installs on Node 25 today and on every future line at release. Restored an open-ended floor.
  • Catalog refresh error handling. ModelsRefreshResult carries per-provider errors and an aborted flag; both were discarded, so a persistently failing refresh was invisible and one cold-start timeout pinned the bridge to a stale catalog for the life of the process. Now logged, with bounded retries.
  • Refresh concurrency and latency. The once-per-process latch was set before its await, so concurrent model/list calls could read a half-refreshed snapshot. Replaced with a memoized promise. Only the first attempt blocks a caller; retries run in the background, so a host with no route to pi.dev pays one timeout rather than one per early picker render.
  • PI_OFFLINE. ModelRuntime derives its network policy from this variable, and passing allowNetwork: true unconditionally overrode it. Now honored.
  • Bundled OAuth is now guarded. registerBunOAuthFlows() reaches Pi's static loaders because the bridge ships as a single file; an upstream rename would otherwise surface only at login time inside the packaged bridge. check-bundles asserts the flows landed in the bundle.
  • agent_settled. Pi 0.82 added AgentSession._emitAgentSettled, an event that does not exist in 0.70.5. BB's adapter did not translate it, so it fell through to provider/unhandled and rendered a literal "Unhandled Pi event" row in the transcript on every turn. Added an explicit ignore list.
  • Lazy inference registry. builtinModels() at module scope turned any failure inside it into a server import failure rather than a failure of the one inference call.
  • Reasoning reconciliation. A stored reasoning override is now reconciled whenever the effective model no longer advertises it, not only on a model switch. Pi reports provider-verified thinking levels, so a model can lose a level between catalog refreshes.

Verification

  • pnpm install --frozen-lockfile
  • pnpm exec turbo run build typecheck lint --concurrency=1 --force (68 tasks)
  • Agent runtime: 41 files, 808 tests · Server: 148 files, 1,257 tests · bb-app: 2 files, 47 tests
  • pnpm exec turbo run smoke:tarball --filter=bb-app --force
  • Host-daemon bundles build and pass check-bundles, including the new OAuth-literal assertion. Verified non-vacuous: removing the registration and rebuilding fails the check.

End-to-end against real openai-codex OAuth credentials

  • Bundled bridge, direct JSON-RPC: three-message conversation on openai-codex/gpt-5.6-sol — plain reply, real tool use (read, correct file contents), and multi-turn session memory. Same at max reasoning.
  • Auth gating: empty auth.json yields 0 models; with credentials, 7 models and only the authenticated provider. Confirms removing the explicit hasAuth filter did not widen the picker.
  • Full product stack (CLI → server → daemon → bridge → OAuth → provider): thread spawned at max reasoning replies correctly; the composer renders "GPT-5.6 Sol Max"; the GPT-5.6 family advertises [low, medium, high, xhigh, max] while 5.3/5.4/5.5 stop at xhigh.
  • Refresh timeout forced with a blackhole proxy: model/list still returns a usable catalog, the failure is logged, and the retry no longer blocks later callers (second call 5004ms → 2ms).
  • agent_settled: a fresh Pi thread after the fix records zero provider/unhandled events, versus one per turn before.

@amadad

amadad commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Live testing exposed one bundle-only OAuth failure: Pi’s lazy OAuth loader resolved ./openai-codex.js relative to the generated single-file bridge. Commit 5e157ef7 registers Pi’s static OAuth loaders at bridge startup.

Proof: the regression test failed before the change and passes after it; all 803 agent-runtime tests pass; the tarball smoke passes; and a bundled openai-codex/gpt-5.6-luna turn completed successfully with stored OAuth credentials.

@amadad

amadad commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main and added one commit.

fix: refresh the Pi model catalog once per bridge process — as originally filed this PR shipped a regression it introduced. listPiBridgeModels called modelRuntime.refresh({ allowNetwork: true }) on every invocation, so each model list fanned out one network request per configured provider to pi.dev. It runs on every picker render and on thread-detail bootstrap with nothing cached between the browser and the bridge, so a stalled provider endpoint burned the full 5s abort budget. Measured per call: ~8-9ms warm, 274-375ms cold, 5,010ms on 2 of 6 cold runs. The refresh is now attempted at most once per bridge process; later calls resolve from the persisted store plus the bundled static catalog and return the same model set. It also stops memoizing a rejected ModelRuntime.create(), which previously cached a single transient failure for the life of the process and left every model list and session start broken until the bridge restarted.

Two things that may make the Node floor less of a decision than it looks:

  • @mariozechner/pi-ai is deprecated on npm — npm view @mariozechner/pi-ai@0.73.1 deprecated returns "please use @earendil-works/pi-ai instead going forward". The current pin is on a retired namespace, and the newer releases under it carry the same notice.
  • Node 20 was already best-effort here before this PR: docs/platform-support.md said "Node.js 20 is best-effort only because it is end-of-life upstream", packages/bb-app/README.md says "Node 20.19.x is best-effort only", and the Node 20 CI job already ran with continue-on-error: true. This PR removes a non-blocking job and a support tier the docs had already marked EOL.

Happy to split this differently if you'd rather review the namespace migration and the runtime unification separately — say the word and I'll restructure.

amadad added 3 commits July 28, 2026 00:19
listPiBridgeModels called modelRuntime.refresh({ allowNetwork: true }) on
every invocation, so each model list fanned out one network request per
configured provider to pi.dev. It runs on every picker render and on
thread-detail bootstrap, and nothing caches the result between the browser
and the bridge. When a provider endpoint stalls the call burns the full 5s
abort budget before returning.

Measured cost of one listPiBridgeModels call: ~8-9ms warm, 274-375ms cold,
and 5,010ms on 2 of 6 cold runs.

Attempt the network refresh at most once per bridge process. The bridge is
long-lived, so later calls resolve from the persisted store plus the bundled
static catalog and return the same model set.

Also stop memoizing a rejected ModelRuntime.create(): a single transient
failure was otherwise cached for the life of the process, leaving every
model list and session start broken until the bridge restarted.
@amadad

amadad commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

CI caught a real defect in the previous push — fixed and force-pushed.

The Node 20 cleanup had removed the globalThis.navigator shim in packages/plugin-build/scripts/generate-runtime-export-manifest.mjs, which broke runtime-export-manifest.test.ts > runs when Node does not provide a global Navigator. That was wrong on my part: the test deletes globalThis.navigator explicitly to prove export introspection survives without it, so the shim guards a scenario the supported-Node floor has no bearing on — @pierre/diffs reads navigator.userAgent during module initialization regardless of Node version. The shim is restored verbatim, and this PR no longer touches packages/plugin-build at all.

Verified locally on the pushed commit, replicating the CI shard:

  • pnpm exec turbo run test --filter='!@bb/server' --filter='!@bb/app' --filter='!@bb/integration-tests' — 48/48 packages passing, including @bb/plugin-build 6/6
  • One further exclusion: @bb/host-daemon's command-dispatch.test.ts > reuses a busy runtime when thread.start carries a changed skill catalog fails on my machine with "Codex 0.101.0 is too old for this bb version". It reproduces identically on a clean origin/main checkout, so it is a local Codex version artifact and unrelated to this branch.

@amadad
amadad force-pushed the fix/pi-082-model-catalog branch from 9b352a3 to 9ce7023 Compare July 28, 2026 00:24
@amadad

amadad commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Resolved the max reasoning blocker called out in #875.

The owning invariant is now consistent end to end: Pi's model catalog, server provider policy, and daemon bridge all accept max. The fix updates PI_SERVER_CAPABILITIES rather than adding a caller bypass, adds a server-boundary regression that failed with the prior HTTP 400, and bumps HOST_DAEMON_PROTOCOL_VERSION to 66 so an older Pi runtime cannot remain connected after the expanded server→daemon value set.

Proof on dc0a40a9:

  • focused server tests: 57/57
  • full server suite: 1,256/1,256
  • agent runtime: 803/803
  • agent providers: 9/9
  • host-daemon contract: 50/50
  • protocol self-update: 10/10
  • relevant package typechecks: clean
  • live packaged turn after deploying protocol 66: openai-codex/gpt-5.6-luna, reasoning maxPI_MAX_LIVE_OK

The full local host-daemon suite still has the previously documented environment-only failure because this machine's Codex 0.101.0 is below bb's 0.136.0 minimum; the protocol-focused host-daemon test passes and GitHub CI is running on the pushed commit.

@SawyerHood

Copy link
Copy Markdown
Collaborator

Hey @amadad I think how I'm going to solve this is:

  • Move pi to ACP so you are using the installed version on your machine
  • Remove the bundled pi dependency all together

Does that make sense to you?

@amadad

amadad commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Yeah @SawyerHood. Unbundling deletes the class of problems this PR works around (static Auth loaders, jiti/static, per-call catalog refresh all exist because F ships inside bb).

FWIW, bb already works this way with Codex, and I hit Codex 0.101.0 is too old for this bb version twice on this branch, reproducing on a clean origin/main It surfaces late and reads like a bb bug rather than "update Codex" - worth getting the version handshake right for Pi from the start.

Happy to close this. The max reasoning fix (PI_SERVER_CAPABILITIES + protocol 66) is server-side and survives unbundling - it closes #875 - so I can send it standalone if useful.

@SawyerHood

Copy link
Copy Markdown
Collaborator

I think I will end up going with your approach for now since it more or less keeps the status quo. I just dug into the pi acp adapter and it has some issues.

I'll take a pass to do some deeper review and testing today

SawyerHood and others added 3 commits July 28, 2026 16:54
Follow-up fixes to the Pi SDK upgrade, all in areas the upgrade itself
introduced or changed.

- Stop rejecting untested Node release lines. `install-machine.sh` moved
  from an open-ended floor to a closed allow-list of 22/24/26, which hard
  -fails installs on Node 25 today and on every future line the day it
  ships. Pi only requires >=22.19, so gate on that floor and keep the
  tested-lines list as documentation.
- Handle the model catalog network refresh instead of discarding it.
  `ModelsRefreshResult` carries per-provider errors and an aborted flag;
  both were dropped silently, so a persistently failing refresh was
  invisible and a single cold-start timeout pinned the bridge to the
  stale catalog for the life of the process. Log the failures and allow
  a bounded number of retries.
- Hold the refresh as a promise rather than a boolean. The latch was set
  before the await, so concurrent `model/list` calls raced past it and
  could read a half-refreshed snapshot.
- Honor `PI_OFFLINE`. `ModelRuntime` disables catalog network access when
  it is set; passing `allowNetwork: true` unconditionally overrode that.
- Guard the bundled OAuth registration. `registerBunOAuthFlows()` reaches
  Pi's static loaders because the bridge ships as one file; a rename
  upstream would otherwise surface only at login time inside the packaged
  bridge. `check-bundles` now asserts the flows landed in the bundle.
- Build the server inference registry lazily. `builtinModels()` at module
  scope turned any failure inside it into a server import failure.
- Reconcile a stored reasoning override whenever the effective model no
  longer advertises it, not only on a model switch. Pi reports
  provider-verified thinking levels, so a model can lose a level between
  catalog refreshes.
- Document the `@opentelemetry/api` pin, which is invisible from
  package.json alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verified against the real bundled bridge: with pi.dev unreachable, the
first two model/list calls each paid the full 5s timeout because every
caller awaited the in-flight retry.

Only the first attempt blocks now. Once it settles, later calls start a
background retry and answer immediately from the current catalog, so a
host with no route to pi.dev pays one timeout instead of one per early
picker render. Measured on the bundle: second call went from 5004ms to
2ms, with the retry still running and still logged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pi 0.82 added AgentSession._emitAgentSettled, which emits `agent_settled`
on the session event stream after every agent run. Pi 0.70.5 had no such
event. BB's adapter does not translate it, so it fell through to the
`provider/unhandled` fallback and rendered as a literal "Unhandled Pi
event" row in the user's transcript on every single turn.

Found by driving a real thread through the dev app with the browser, not
by reading the diff: the row is visible in the UI and the event is
persisted to the thread event log.

Add an explicit ignore list rather than translating it. BB already
derives turn completion from `agent_end` plus its `willRetry` flag, so
the settle signal carries nothing extra. The list gives the next Pi bump
a named place to classify new events instead of leaking them to the UI.

Verified: a fresh pi thread after the fix records 13 events and zero
`provider/unhandled`, versus one per turn before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SawyerHood

Copy link
Copy Markdown
Collaborator

Reviewed this and pushed three follow-up commits to the branch rather than leaving a long list of review notes. Summary of what changed and why, since a couple of these were only findable by running the thing:

b2cd3b682 — the review findings. The one I'd flag hardest is the Node range: install-machine.sh had gone from an open-ended floor to a closed 22 || 24 || 26 allow-list, which hard-fails installs on Node 25 today and on every future release line at ship time. Pi itself only declares >=22.19.0, so I restored an open-ended floor and left the tested-lines list as documentation. The rest: the catalog refresh was discarding ModelsRefreshResult.errors and .aborted, the once-per-process latch was set before its await (concurrent model/list could read a half-refreshed snapshot), and allowNetwork: true was overriding the user's PI_OFFLINE.

a7ff72eee — my own fix in the previous commit was wrong under load. I forced the refresh timeout with a blackhole proxy and found every caller was awaiting the in-flight retry, so a host with no route to pi.dev paid 5s on calls 1 and 2 — worse than the original, which paid it once. Only the first attempt blocks now; retries run in the background. Second call went 5004ms → 2ms.

4b78be19e — this one is a real regression and I only caught it in the browser. Pi 0.82 added AgentSession._emitAgentSettled, which emits agent_settled after every agent run. That event does not exist in 0.70.5 at all (0 occurrences; agent_end appears 10 times in the same file). BB's adapter doesn't translate it, so it fell through to provider/unhandled and rendered a literal "Unhandled Pi event" row in the transcript on every single turn. Fixed with an explicit ignore list, which also gives the next Pi bump a named place to classify new events instead of leaking them to the UI.

I also updated the PR body — the compatibility section described the old closed Node range, and since this repo squash-merges, that text becomes the permanent commit message.

Verified end to end against real openai-codex OAuth credentials: tool use, multi-turn memory, max reasoning through the picker, and auth gating (empty auth.json → 0 models, credentialed → 7 from the authenticated provider only). Nice work on the upgrade — the ModelRuntime consolidation is a clear improvement over the static-API approach.

@SawyerHood
SawyerHood merged commit 2e22553 into ymichael:main Jul 28, 2026
9 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

Development

Successfully merging this pull request may close these issues.

2 participants