Skip to content

fix(providers/pi): lazy-load Pi SDK to unbreak compiled archon binary#1355

Merged
Wirasm merged 2 commits intodevfrom
fix/pi-lazy-load
Apr 22, 2026
Merged

fix(providers/pi): lazy-load Pi SDK to unbreak compiled archon binary#1355
Wirasm merged 2 commits intodevfrom
fix/pi-lazy-load

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented Apr 22, 2026

Summary

Fixes the other half of the v0.3.7 binary-release blocker (#1354 fixed the --bytecode compile flag; this fixes the runtime crash).

@mariozechner/pi-coding-agent/dist/config.js runs readFileSync(getPackageJsonPath(), 'utf-8') at module top-level. Inside a compiled archon binary, getPackageJsonPath() resolves to dirname(process.execPath) + '/package.json' — a path that doesn't exist next to /usr/local/bin/archon. Result: archon crashes at ENOENT before any command runs, including archon version. Reproduces natively on darwin-arm64 after building locally.

Change

Convert all Pi SDK value imports and imports of Pi-dependent helper modules (options-translator, resource-loader, session-resolver, ui-context-stub, event-bridge) in `provider.ts` to dynamic imports inside `PiProvider.sendQuery()`. Type-only imports stay static — TS erases them, no runtime resolution.

Effect: registerCommunityProviders() + getAgentProvider('pi') no longer loads the Pi SDK. Load happens only when a Pi workflow actually runs.

Why Pi and not Claude/Codex

Claude and Codex providers keep static imports. Their SDKs have no module-init side effects that fail inside a compiled binary. Pi is a deliberate outlier because of the upstream behaviour; a comment in `provider.ts`'s header makes this explicit so future maintainers don't "normalize" it by reintroducing static imports.

Nits

  • Class constructors (`AuthStorage`, `ModelRegistry`, `SettingsManager`) accessed via `piCodingAgent.X` rather than destructured — destructuring PascalCase bindings trips eslint's naming-convention rule, and a disable here would just be noise.
  • `lookupPiModel` now takes `getModel` as a parameter (previously a closure over a static import).

Regression test

New `provider-lazy-load.test.ts` mocks both Pi SDK packages, walks the exact path the CLI and server take (`registerCommunityProviders()` → `getAgentProvider('pi')`), and asserts neither SDK module factory fired. Runs in its own `bun test` invocation — Bun's `mock.module` is process-wide and would poison `provider.test.ts`.

If a future change reintroduces a static Pi SDK import in the chain reachable from the registry, the counters tip to `true` and this test fails with a pointer back to this PR.

Verified locally

  • `bun run validate` passes (type-check, lint, format, all tests)
  • `bun build --compile --minify --target=bun-darwin-arm64` produces a binary whose `archon version` runs cleanly and reports `Build: binary`
  • Before this change (on this same branch with just the bytecode fix from fix(build): drop --bytecode from compiled-binary build #1354), the same binary crashed at startup with the ENOENT described above

Depends on

Test plan

  • CI green on `bun run validate`
  • CI green on release-workflow dry run (or confirmed when v0.3.8 is cut)
  • Manual: `./dist/binaries/archon-darwin-arm64 version` runs on macOS
  • Manual: `./dist/binaries/archon-linux-x64 version` runs on Linux (covered by `/test-release curl-mac` + `curl-vps` once 0.3.8 ships)

Summary by CodeRabbit

  • Tests

    • Added a regression test to ensure the Pi community provider defers loading of its SDK during registration.
  • Bug Fixes

    • Prevented startup crashes in compiled binaries caused by eager Pi provider initialization.
    • Fixed release binary generation to avoid producing broken bytecode.
  • Refactor

    • Pi provider updated to lazily load its SDK at query time to improve stability.

Pi's @mariozechner/pi-coding-agent/dist/config.js runs
`readFileSync(getPackageJsonPath(), 'utf-8')` at module top level. Inside
a compiled archon binary `getPackageJsonPath()` resolves to
`dirname(process.execPath) + '/package.json'`, which doesn't exist next
to `/usr/local/bin/archon` — so archon crashes with ENOENT at startup
before any command runs. v0.3.7's release binary build appeared to
compile clean (CI fell over first on an unrelated --bytecode issue) but
even the fixed-bytecode binary fails the same way locally.

Convert all Pi SDK value imports and Pi-dependent helper imports to
dynamic imports inside `PiProvider.sendQuery()`. Type-only imports stay
static (erased by TS). Effect: registering the Pi provider and creating
an instance no longer loads Pi's SDK — load happens only when a Pi
workflow actually runs. Claude and Codex providers keep their static
import style (their SDKs have no module-init side effects that fail in
a binary); see the file header comment in provider.ts for why Pi is the
deliberate outlier.

Class constructors (AuthStorage, ModelRegistry, SettingsManager) are
accessed via `piCodingAgent.X` rather than destructured to keep
eslint's naming-convention rule happy without a disable.

Add a regression test (provider-lazy-load.test.ts) that mocks
@mariozechner/pi-coding-agent and @mariozechner/pi-ai, walks the same
registerCommunityProviders() → getAgentProvider('pi') path the CLI and
server take, and asserts neither SDK module was loaded. Runs in its own
`bun test` invocation (mock.module is process-wide).

Verified locally: `bun build --compile --minify --target=bun-darwin-arm64`
produces a binary whose `archon version` runs cleanly and reports
Build: binary, where previously every command crashed at boot.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bfe11ab-d876-4ba7-9cee-74150495eb0c

📥 Commits

Reviewing files that changed from the base of the PR and between fb0f8b7 and cff6247.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • packages/providers/src/community/pi/provider-lazy-load.test.ts
  • packages/providers/src/community/pi/provider.ts
  • packages/providers/src/community/pi/ui-context-stub.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/providers/src/community/pi/ui-context-stub.ts
  • packages/providers/src/community/pi/provider-lazy-load.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/providers/src/community/pi/provider.ts

📝 Walkthrough

Walkthrough

This PR makes the Pi community provider lazy-load its SDK and helper modules at runtime (inside sendQuery), adds a regression test to ensure no eager resolution during registration, updates a test script, and tweaks a type-only import in the Pi UI context stub and the CHANGELOG.

Changes

Cohort / File(s) Summary
Build Configuration
packages/providers/package.json
Updated test script to include the new regression test src/community/pi/provider-lazy-load.test.ts.
Pi Provider Implementation
packages/providers/src/community/pi/provider.ts
Removed static imports of @mariozechner/pi-coding-agent and @mariozechner/pi-ai and related helper modules; moved to dynamic import() calls inside sendQuery(); changed lookupPiModel to accept a getModel function parameter.
Pi Provider Regression Test
packages/providers/src/community/pi/provider-lazy-load.test.ts
Added Bun regression test that mocks Pi SDK module resolution, registers community providers, obtains the Pi provider, and asserts the Pi SDK modules remain unloaded during registration/instantiation.
UI Context Stub
packages/providers/src/community/pi/ui-context-stub.ts
Converted Theme import from a value import to a type-only import from @mariozechner/pi-coding-agent to avoid runtime import.
Changelog
CHANGELOG.md
Documented fix: Pi provider lazy-loading prevents startup crashes in compiled binaries and noted related build script change (removal of --bytecode).

Sequence Diagram

sequenceDiagram
    participant Client
    participant Registry
    participant PiProvider
    participant PiSDK as Pi SDK Modules

    Note over Client,PiSDK: Old Flow (Static Imports)
    Client->>PiProvider: Import provider module
    PiProvider->>PiSDK: Static import (eager load)
    Note over PiSDK: Modules loaded at import time

    Note over Client,PiSDK: New Flow (Dynamic/Lazy)
    Client->>Registry: registerCommunityProviders()
    Registry->>PiProvider: Request provider metadata (no Pi SDK import)
    Client->>PiProvider: sendQuery(...)
    PiProvider->>PiSDK: Dynamic import() of SDK & helpers
    PiSDK->>PiProvider: Return runtime bindings
    PiProvider->>PiProvider: Create agent session / handle query
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly Related PRs

Poem

🐇 I nibble code with careful paws,
No Pi SDK wakes without a cause.
When queries call, I'll fetch on cue,
Till then those modules sleep—shh!—true.
Hooray for lazy loads and tests anew!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: converting Pi SDK to lazy loading to fix compiled binary crashes.
Description check ✅ Passed The description covers all critical sections: problem statement with root cause (module-level readFileSync), solution (dynamic imports), scope boundaries (why Pi vs Claude/Codex), regression test approach, local verification evidence, and dependencies.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/pi-lazy-load

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@Wirasm
Copy link
Copy Markdown
Collaborator Author

Wirasm commented Apr 22, 2026

PR Review Summary — multi-agent

Ran code-reviewer, docs-impact, pr-test-analyzer, comment-analyzer, silent-failure-hunter, and code-simplifier in parallel. PR is small (3 files, +115/-23) and the core fix is correct — converting static Pi SDK imports in provider.ts to dynamic imports inside sendQuery() unblocks the compiled binary.

Critical issues

None — the lazy-load mechanism is structurally sound.

Important issues

Agent Issue Location
comment-analyzer Docstring says the mocks make SDK resolution throw, but both mock.module factories set a boolean and return {}. The inline comment two blocks below (lines 21–27) correctly explains the counter approach, so the top-level docstring contradicts the code directly beneath it. packages/providers/src/community/pi/provider-lazy-load.test.ts:11-13
comment-analyzer "v1 capabilities are all false" docblock is factually wrong — PI_CAPABILITIES has 7 flags set to true (sessionResume, skills, toolRestrictions, structuredOutput, envInjection, effortControl, thinkingControl). Pre-existing, but the PR diff touches this class docblock's neighborhood. packages/providers/src/community/pi/provider.ts:103-107
docs-impact No CHANGELOG.md entry for this bug fix. This is a user-visible fix (compiled binary crashed at startup before any command ran on v0.3.7); belongs in [Unreleased] ### Fixed. CHANGELOG.md

Suggestions

Agent Suggestion Location
code-simplifier Promise.all of 7 local-filesystem dynamic imports implies parallelism that doesn't exist (Bun's module loader is sync for disk-local files). A 7-tuple positional destructure is harder to audit than 7 sequential await import() declarations. Same runtime semantics, easier to diff. packages/providers/src/community/pi/provider.ts:116-140
code-reviewer / code-simplifier lookupPiModel(getModel: unknown, …) — since there is exactly one call site and the cast is a single expression, the wrapper no longer pulls its weight. Either inline it at the call site with a comment, or at minimum tighten the parameter type via a local type GetModelFn = (p: string, m: string) => Model<Api> | undefined (type-only imports of Api/Model are already present). packages/providers/src/community/pi/provider.ts:65-71
silent-failure-hunter The Promise.all import block has no classified error path — if a user runs a Pi workflow without the optional @mariozechner/pi-coding-agent dep installed, they get a raw Cannot find module with no hint to install it. Provider is explicitly builtIn: false, so wrapping in a try/catch with an install-hint message would be friendlier. packages/providers/src/community/pi/provider.ts:116-140
code-reviewer ui-context-stub.ts:7 has a non-type import { Theme } from '@mariozechner/pi-coding-agent'. In practice Theme appears only in type positions (as Theme, return types), so TS erasure likely removes it — but if Theme is ever a runtime class in a future Pi release, it would reintroduce the eager-load via this sibling module (which is now dynamically imported, so safe today). Cheap fix: import type { Theme }. packages/providers/src/community/pi/ui-context-stub.ts:7
comment-analyzer Inline comment claims Bun's mock.module factory "runs even for type-only imports during dependency resolution." TS erases import type, so Bun shouldn't see it at all — the real justification for counters-over-throw is simpler (throwing inside a factory produces a crash at resolution time with no assertion context). Minor but misleading for future debugging. packages/providers/src/community/pi/provider-lazy-load.test.ts:21-27
docs-impact Pre-existing gap, not introduced by this PR: CLAUDE.md test-isolation table omits @archon/providers (15 invocations after this PR, 14 before). The table says "See each package's package.json for the exact splits" so it's not misleading — just incomplete. CLAUDE.md:131

Strengths

  • Header block on provider.ts:1-26 is precise: names the upstream function (getPackageJsonPath), the failure mode (dirname(process.execPath) + '/package.json'), and the guardrail (no static value imports).
  • Regression test correctly mirrors the exact path the CLI and server take (registerCommunityProviders() + getAgentProvider('pi')) rather than reaching into internals.
  • Separate bun test invocation for the new test is necessary and correct — provider.test.ts installs a benign mock.module stub for the same packages, which would collide process-wide (per the mock.module isolation rules in CLAUDE.md).
  • Auth-failure path (lines 221–230) sets the bar for classified errors — structured message + install/login hints.
  • piCodingAgent.X property access (vs destructuring PascalCase constructors) is justified by the eslint naming-convention rule, not aesthetics — documented inline.

Test coverage verdict — ADEQUATE

  • No additional tests needed. The counter-based detection is the right choice given Bun's factory-runs-during-resolution behavior.
  • One small coverage caveat worth documenting in the test header: the test only detects direct eager loads of @mariozechner/pi-coding-agent and @mariozechner/pi-ai. The five sibling helpers (event-bridge, options-translator, resource-loader, session-resolver, ui-context-stub) are not themselves mocked — if one were statically re-imported into provider.ts, the regression test would still catch it transitively (because those siblings have value imports from Pi), but a contrived future refactor that added a Pi-free sibling that then re-imports Pi dynamically inside itself could slip through. Unlikely in practice.

Verdict

NEEDS MINOR FIXES — two clear, cheap wins before merge:

  1. Rewrite the test docstring at provider-lazy-load.test.ts:11-13 to describe counter-based detection instead of throwing.
  2. Add a CHANGELOG.md entry under [Unreleased] ### Fixed.

Everything else is suggestion-grade. The Promise.all → sequential-awaits simplification and the stale "v1 capabilities are all false" docblock are nice-to-have follow-ups but not release-blocking. The core fix is correct and the regression test, while narrow, reliably guards the specific regression this PR targets.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/providers/src/community/pi/provider.ts (1)

65-71: Optional: tighten getModel typing using the existing type-only import.

Api and Model are already imported as types on Line 2, so getModel can be typed using typeof import(...) without triggering any runtime module resolution. This preserves the dynamic-load property while removing the unknown hop and the internal cast.

♻️ Proposed refactor
-function lookupPiModel(
-  getModel: unknown,
-  provider: string,
-  modelId: string
-): Model<Api> | undefined {
-  return (getModel as (p: string, m: string) => Model<Api> | undefined)(provider, modelId);
-}
+function lookupPiModel(
+  getModel: typeof import('@mariozechner/pi-ai').getModel,
+  provider: string,
+  modelId: string
+): Model<Api> | undefined {
+  // Pi's getModel constrains TModelId to keyof MODELS[TProvider]; not knowable
+  // from a runtime string, so the cast-through-unknown escape hatch remains.
+  return (getModel as unknown as (p: string, m: string) => Model<Api> | undefined)(
+    provider,
+    modelId
+  );
+}

Purely cosmetic — current form is also correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/community/pi/provider.ts` around lines 65 - 71,
Summary: tighten the getModel parameter typing in lookupPiModel to avoid the
unknown + cast. Change lookupPiModel's signature so getModel is typed as (p:
string, m: string) => Model<Api> | undefined (using the existing type-only
imports Model and Api) and then return getModel(provider, modelId) directly
without the runtime cast; update references to the getModel parameter in
lookupPiModel accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/providers/src/community/pi/provider.ts`:
- Around line 65-71: Summary: tighten the getModel parameter typing in
lookupPiModel to avoid the unknown + cast. Change lookupPiModel's signature so
getModel is typed as (p: string, m: string) => Model<Api> | undefined (using the
existing type-only imports Model and Api) and then return getModel(provider,
modelId) directly without the runtime cast; update references to the getModel
parameter in lookupPiModel accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8269882e-fbba-4952-b206-4f21c4ff70e1

📥 Commits

Reviewing files that changed from the base of the PR and between 48c81d3 and fb0f8b7.

📒 Files selected for processing (3)
  • packages/providers/package.json
  • packages/providers/src/community/pi/provider-lazy-load.test.ts
  • packages/providers/src/community/pi/provider.ts

…mport

From the multi-agent review on #1355:

- Add CHANGELOG.md entries under [Unreleased] ### Fixed for this PR's Pi
  lazy-load fix and the --bytecode removal from #1354 — both user-visible
  fixes (compiled binary unusable without them).
- Rewrite the test-header docstring in provider-lazy-load.test.ts to
  describe counter-based detection instead of "mocks throw" (contradicted
  the actual code directly below it).
- Tighten `lookupPiModel`'s first parameter from `unknown` to a local
  `GetModelFn` alias, moving the runtime-string cast to the single call
  site with a pointer to the docblock.
- Update the class docblock on `PiProvider` — "v1 capabilities are all
  false" was stale; PI_CAPABILITIES has seven flags set to true.
- `ui-context-stub.ts` imports `Theme` as a non-type value even though
  every usage is in a type position. Fold it into the existing
  `import type {…}` block so a future runtime-class `Theme` in Pi can't
  reintroduce an eager module load via this sibling.

No behavior change. Type-check, lint, format, tests, and a local
darwin-arm64 compile + version smoke all clean.
@Wirasm
Copy link
Copy Markdown
Collaborator Author

Wirasm commented Apr 22, 2026

Thanks — pushed cff6247 addressing the review.

Must-fixes (both done):

  • Rewrote provider-lazy-load.test.ts docstring to describe counter-based detection (the "mocks throw" claim contradicted the code directly below it)
  • Added CHANGELOG.md entries under [Unreleased] ### Fixed — one for this PR's Pi lazy-load fix, one for the --bytecode removal from fix(build): drop --bytecode from compiled-binary build #1354 (also user-visible, also had no entry)

Nice-to-haves addressed:

  • Tightened lookupPiModel's first parameter from unknown to a local GetModelFn alias, cast moved to the call site
  • Fixed the stale "v1 capabilities are all false" docblock on PiProviderPI_CAPABILITIES has 7 flags true
  • Folded import { Theme } into the existing import type {...} block in ui-context-stub.ts
  • Fixed the misleading inline comment about Bun's mock.module behavior (covered by the rewritten docstring)

Intentionally skipped:

  • Promise.all → sequential awaits: same runtime semantics on Bun's sync module loader, and I think the 7-tuple is fine — kept as-is.
  • Silent-failure wrap on the import block: @mariozechner/pi-coding-agent is a hard dependencies entry in packages/providers/package.json, so a "Cannot find module" genuinely means the user's install is broken (npm run without bun install, or a pathological bundler). A catch-and-rewrap would mask the root cause. Left to surface as-is.
  • CLAUDE.md test-isolation table update for @archon/providers: pre-existing gap, out of scope for this PR per the reviewer's own note.

Verified locally after the cleanups: bun run validate clean, bun build --compile --minify --target=bun-darwin-arm64 produces a working binary (archon version runs, reports Build: binary).

@Wirasm Wirasm merged commit 5294fcd into dev Apr 22, 2026
4 checks passed
popemkt pushed a commit to popemkt/Archon that referenced this pull request Apr 22, 2026
Single conflict in packages/providers/package.json test script — resolved
as union (all pi/ tests from upstream including new provider-lazy-load.test.ts
plus the copilot/ test files from this branch).

Upstream's pi-provider lazy-load refactor (coleam00#1355) auto-merged cleanly with
this branch's pi → shared/ extraction because the extracted shared modules
have no Pi SDK deps, so the compiled-binary contract is preserved.
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.

1 participant