Skip to content

feat(coding-agent)!: adopt pi's ModelRuntime architecture for full model-provider parity - #2064

Merged
flora131 merged 15 commits into
mainfrom
feat/pi-model-runtime-parity
Jul 29, 2026
Merged

feat(coding-agent)!: adopt pi's ModelRuntime architecture for full model-provider parity#2064
flora131 merged 15 commits into
mainfrom
feat/pi-model-runtime-parity

Conversation

@flora131

@flora131 flora131 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces Atomic's fork-era ModelRegistry engine with upstream pi's model-provider architecture, ported from pi @ 027a5847 (coding-agent 0.82.1). Stacked on #2059 (its credential-survival invariant is now guaranteed structurally by pi's login() shape rather than by Atomic-specific glue).

Changes

  • Architecture: pi's ModelRuntime (snapshot-based availability, RuntimeCredentials, ModelConfig, provider-composer, pi's remote-catalog-provider) is now the engine; ModelRegistry shrinks to pi's thin synchronous extension facade. 48 call-site files (AgentSession, SDK, RPC, interactive, isolated-engine) consume the runtime directly.
  • Breaking removals (strict parity, per maintainer decision): legacy extension OAuth registration bridge, layered .pi+.atomic models.json merging, custom API-key login surfaces beyond pi's provider contract, AuthStorage sync compatibility API (now pi's async CredentialStore), and ModelRegistry/CreateAgentSessionOptions surface not present in pi. See changelog Breaking Changes for the migration guide.
  • Refresh semantics: pi-exact — modelRefreshTimeoutMs bounds only runtime creation; the /model selector owns its 15 s timeout, renders cached models immediately, and reports timeout/errors with pi's exact strings and surfaces.
  • llama.cpp extension: ported to pi's provider-owned registration. Fixes unconfigured users seeing Could not refresh llama.cpp; showing cached models. — the provider now stays dormant (no refresh attempt) until LLAMA_BASE_URL or a stored login configures a server; /login prompts for server URL + optional key like pi.
  • Tests: pi's llama-extension.test.ts ported (one assertion adapted to Atomic's unified pending-registration list); root parity tests rewritten against the native surface incl. a dormant-until-configured regression; selector tests updated to pi semantics with a fake-timer timeout case; fix(auth): never roll back OAuth credentials on failed post-login model refresh #2059 credential-survival regressions preserved.

Breaking Changes

Documented in packages/coding-agent/CHANGELOG.md [Unreleased]Breaking Changes (OAuth bridge removal, single-file ModelConfig, async AuthStorage, facade-only ModelRegistry, modelRuntime session option).

Verification

  • bun run typecheck, bun run lint, bun run check:file-length — clean
  • bunx vitest --run (packages/coding-agent): 2881 passed / 0 failed (369 files)
  • bun run test:unit (root, incl. workflows/subagents): 4407 passed / 0 failed
  • Grep gates: zero source references to registerOAuthProvider/registerLegacyOAuthProvider/getLegacyOAuthProvider/refreshLegacyOAuth

Notes

Produced by a bounded goal-workflow run (10 orchestrator turns with reviewer gates) plus two follow-up parity fixes found in post-run audit: the selector refresh port and the llama.cpp provider port.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

flora131 added 14 commits July 28, 2026 21:37
…el refresh

Match upstream pi's invariant: once fresh OAuth tokens are persisted, the
catalog-refresh outcome must not fail the login or touch the stored
credential.

Previously, a timed-out (aborted) or partially failed model-catalog
refresh after a successful /login threw 'Model refresh aborted after
OAuth login' and rolled the fresh tokens back to the previous
credential. Because providers rotate refresh tokens, the rollback could
re-install a server-side-invalidated credential, permanently stranding
the user with 'invalid_grant' on every send while every re-login was
rolled back again. Machines with slow routes to catalog endpoints hit
this deterministically.

- rpc-provider-auth: persist the credential, run the refresh
  best-effort, and drop the rollback and aborted/error classification
  (a thrown refresh still propagates but no longer unpersists tokens)
- interactive-auth-login: drop the provider-error and aborted throws
  from the post-login refresh; models fall back to the cached snapshot
- update RPC and interactive regression tests to lock in credential
  survival; add an aborted-refresh interactive case

Assistant-model: Claude Fable 5
Replace the fork-era registry engine with pi's provider-owned runtime and thin extension facade. Remove legacy OAuth registration and layered models configuration while preserving Atomic path, auth, RPC, and isolated-engine compatibility.

Assistant-model: Claude Opus 5
Persist the full RPC credential union through ModelRuntime and refresh the catalog before returning. Restore Atomic-specific retry, compaction, footer, OAuth cancellation, migration, and usage-accounting regression coverage removed during the runtime parity port.

Assistant-model: Claude Opus 5
Restore isolated RPC model refresh credential reload, explicit force and timeout forwarding, auth-storage transactional behavior, and SDK/RPC regression coverage while documenting pi parity changes.

Assistant-model: Claude Opus 5
Reload frontend credential state after isolated login and logout so auth selectors reflect stored credentials. Transport and honor OAuth login labels and callback-server metadata, restoring reachable manual-code input.

Restore credential reload regression coverage, isolate RPC persistence tests from ambient auth, and document the AuthStorage and ModelRegistry API reductions.

Assistant-model: Claude Opus 5
Restore callback-server and login-label metadata for builtin OAuth providers so manual redirect login remains reachable. Keep logout and isolated auth status aligned with stored credentials and engine-published availability, and restore deterministic RPC prompt-lock coverage.

Assistant-model: Claude Opus 5
Correct the models.json migration docs, preserve refresh option coverage, label logout credentials by stored type, and honor truthy offline flags.

Assistant-model: Claude Opus 5
Fix the tautological restored-model provider guard and restore the positive registered-provider regression coverage. Preserve readStoredCredential's non-throwing malformed-file contract and split ModelRuntime snapshot helpers for file-length headroom. Correct models.json migration documentation and project-scoped configuration coverage.

Assistant-model: Claude Opus 5
Route session restoration through the restored-model resolver and narrow
unknown-model reconstruction to supported dynamic and configured providers.
Restore discriminating SDK and retired-model regression coverage.

Use the extension-facing model registry for workflow catalogs, guard the
advisory Anthropic auth check from stale credential failures, and restore the
full RPC OAuth login coverage including correlated callback dispatch.

Assistant-model: Claude Opus 5
Show the Anthropic subscription warning once from stored OAuth status without triggering refresh-capable auth lookup, while keeping lookup failures non-fatal. Label login status from the stored credential type and restore regression assertions for both behaviors.

Document the removed CreateAgentSessionOptions authStorage and modelRegistry overrides.

Assistant-model: Claude Opus 5
Keep thrown post-login catalog refresh failures visible without rolling back persisted OAuth credentials, while preserving successful handling of returned provider errors and aborted results. Restore the PR #2059 regression assertion and propagate interactive refresh failures.

Disable network catalog refresh during --api-key startup, migrate shipped SDK examples and documentation to ModelRuntime, and restore session-service startup timing spans.

Assistant-model: Claude Opus 5
The migrated selector diverged from pi: it dropped pi's selector-owned
15s refresh timeout, routed failures through the status line with
'showing available models' wording, rendered the snapshot through an
async promise chain, and passed allowNetwork although the runtime owns
offline gating. Port pi's implementation verbatim: synchronous snapshot
render, selector-owned timeout with 'Model refresh timed out; showing
cached models.', failures on the error surface, muted status color, and
a signal-only refresh call. Tests updated to pi semantics plus a fake-
timer timeout case and a runtime-owned network-gating assertion.

Assistant-model: Claude Fable 5
The migration left the bundled llama.cpp extension on Atomic's legacy
ProviderConfig registration with a hardcoded 'local' API key, so the new
ModelRuntime treated the provider as configured for everyone: every
/model refresh attempted http://127.0.0.1:8080 and unconfigured users
saw 'Could not refresh llama.cpp; showing cached models.'

Port pi's provider.ts verbatim: native Provider registration with
provider-owned auth whose check/resolve return undefined until
LLAMA_BASE_URL or a stored login configures a server, so refresh skips
the provider entirely when unconfigured. Includes pi's /login flow
(server URL + optional API key). Ports pi's llama-extension test suite
(one assertion adapted to Atomic's unified pending-registration list),
rewrites the root parity tests against the native surface with a new
dormant-until-configured regression, and corrects the changelog's
refresh-timeout entry to pi's real semantics (selector-owned 15s
timeout).

Assistant-model: Claude Fable 5
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown

Too many files changed for review. (226 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@flora131
flora131 changed the base branch from fix/oauth-login-refresh-parity to main July 29, 2026 17:46
All five conflicts resolve to the branch side: the ModelRuntime
migration supersedes the #2059 versions of these files while keeping
its credential-survival invariant and changelog entry.

Assistant-model: Claude Fable 5
@flora131
flora131 merged commit 5ef323e into main Jul 29, 2026
8 of 9 checks passed
@mintlify

mintlify Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
bastani 🟢 Ready View Preview Jul 29, 2026, 5:48 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

flora131 pushed a commit that referenced this pull request Jul 29, 2026
…rewrite

#2064 rewrote these assertions and dropped the platform-aware handling the
previous versions had, so all three fail on Windows only:

- footer-width: `formatCwdForFooter` joins with `sep`, so the expectation is
  `~\project` on Windows. The rewrite hardcoded `~/project`
  (was: `` `~${sep}project` ``).
- sdk-session-manager: the session file separator check hardcoded `/`
  (was: `` `${expectedSessionDir}${sep}` ``).
- sdk-session-manager: the system prompt always renders cwd with POSIX
  separators (`system-prompt.ts` applies `.replace(/\\/g, "/")`), so the
  Windows expectation needs the same normalization
  (was: `` sessionCwd.replaceAll("\\", "/") ``).

Restore all three. Production code is unchanged and was correct throughout.

This is what exhausted the Windows job budget: the three failures triggered
the bounded retry, and two full ~170s suite runs pushed the job past its 15m
cap, where GitHub reported the kill as `cancelled`.

Assistant-model: Claude Opus 5
flora131 added a commit that referenced this pull request Jul 29, 2026
The Windows `test` job has been killed at its 15m `timeout-minutes` budget on
every run since #2064, always inside the coding-agent vitest step. The step
log was empty for the entire 10m before the kill, so the cancellation carried
no evidence about which test stalled.

The cause of the blackout is `run-flaky-test-suite.ts`: it collected the whole
child stdout/stderr with `new Response(child.stdout).text()` and only wrote it
after the child exited. A child killed by the job timeout never exits normally,
so every collected line was discarded.

Tee both streams as they arrive instead. The retry, deterministic-file, and
diagnostics-artifact contracts are unchanged; a timed-out attempt now leaves
its progress in the step log.

Also raise the Windows budget to 30m temporarily so the suite can run to
completion and report its real failures. Restored once the stall is fixed.

Assistant-model: Claude Opus 5
flora131 added a commit that referenced this pull request Jul 29, 2026
…rewrite

#2064 rewrote these assertions and dropped the platform-aware handling the
previous versions had, so all three fail on Windows only:

- footer-width: `formatCwdForFooter` joins with `sep`, so the expectation is
  `~\project` on Windows. The rewrite hardcoded `~/project`
  (was: `` `~${sep}project` ``).
- sdk-session-manager: the session file separator check hardcoded `/`
  (was: `` `${expectedSessionDir}${sep}` ``).
- sdk-session-manager: the system prompt always renders cwd with POSIX
  separators (`system-prompt.ts` applies `.replace(/\\/g, "/")`), so the
  Windows expectation needs the same normalization
  (was: `` sessionCwd.replaceAll("\\", "/") ``).

Restore all three. Production code is unchanged and was correct throughout.

This is what exhausted the Windows job budget: the three failures triggered
the bounded retry, and two full ~170s suite runs pushed the job past its 15m
cap, where GitHub reported the kill as `cancelled`.

Assistant-model: Claude Opus 5
flora131 added a commit that referenced this pull request Jul 29, 2026
All 374 Windows test files now pass in 191s, but the process then sits for
~412s until the job cap kills it. The last green Windows run (pre-#2064)
exited 0.8s after printing its summary, so the exit hang is new and is what
still exhausts the budget.

Add vitest's hanging-process reporter to name the handle. Removed once the
leak is fixed.

Assistant-model: Claude Opus 5
flora131 added a commit that referenced this pull request Jul 29, 2026
…le (#2066)

* ci: stream test-suite output so timed-out Windows jobs stay diagnosable

The Windows `test` job has been killed at its 15m `timeout-minutes` budget on
every run since #2064, always inside the coding-agent vitest step. The step
log was empty for the entire 10m before the kill, so the cancellation carried
no evidence about which test stalled.

The cause of the blackout is `run-flaky-test-suite.ts`: it collected the whole
child stdout/stderr with `new Response(child.stdout).text()` and only wrote it
after the child exited. A child killed by the job timeout never exits normally,
so every collected line was discarded.

Tee both streams as they arrive instead. The retry, deterministic-file, and
diagnostics-artifact contracts are unchanged; a timed-out attempt now leaves
its progress in the step log.

Also raise the Windows budget to 30m temporarily so the suite can run to
completion and report its real failures. Restored once the stall is fixed.

Assistant-model: Claude Opus 5

* ci: keep the Windows budget at its contracted 15m

The deterministic CI contract pins the Windows timeout at 15m, so the
temporary diagnostic bump failed 'Deterministic CI and release contracts'
before the vitest step could run. Streamed output alone now carries the
evidence: the killed step reports progress up to the kill point.

Assistant-model: Claude Opus 5

* fix(test): restore Windows path handling dropped by the ModelRuntime rewrite

#2064 rewrote these assertions and dropped the platform-aware handling the
previous versions had, so all three fail on Windows only:

- footer-width: `formatCwdForFooter` joins with `sep`, so the expectation is
  `~\project` on Windows. The rewrite hardcoded `~/project`
  (was: `` `~${sep}project` ``).
- sdk-session-manager: the session file separator check hardcoded `/`
  (was: `` `${expectedSessionDir}${sep}` ``).
- sdk-session-manager: the system prompt always renders cwd with POSIX
  separators (`system-prompt.ts` applies `.replace(/\\/g, "/")`), so the
  Windows expectation needs the same normalization
  (was: `` sessionCwd.replaceAll("\\", "/") ``).

Restore all three. Production code is unchanged and was correct throughout.

This is what exhausted the Windows job budget: the three failures triggered
the bounded retry, and two full ~170s suite runs pushed the job past its 15m
cap, where GitHub reported the kill as `cancelled`.

Assistant-model: Claude Opus 5

* fix(test): make two Windows-only suite failures portable

With the separator fixes in place the Windows suite ran far enough to expose
two more failures, both from tests that assume POSIX process and filesystem
behavior:

- bash-session-metadata: the detached-async-job test leaves a shell running
  with the temp dir as its cwd. Windows locks a directory in that state, so
  `afterEach` cleanup failed with EBUSY and failed the test. Retry the removal
  and ignore a residual failure; reclaiming an OS temp dir is never the
  assertion.
- sdk-session-manager: the cwd check compared `pwd` output through
  `realpathSync`. On Windows the bash tool resolves to Git Bash, which reports
  MSYS paths (`/tmp/...`) that Node resolves against the current drive, so the
  lstat failed with ENOENT on `C:\tmp\...`. Prove the working directory by
  reading a marker file only resolvable from sessionCwd, which tests the same
  contract without depending on how a shell spells a path.

Assistant-model: Claude Opus 5

* test: temporarily report hanging handles on the Windows suite

All 374 Windows test files now pass in 191s, but the process then sits for
~412s until the job cap kills it. The last green Windows run (pre-#2064)
exited 0.8s after printing its summary, so the exit hang is new and is what
still exhausts the budget.

Add vitest's hanging-process reporter to name the handle. Removed once the
leak is fixed.

Assistant-model: Claude Opus 5

* test: dump the surviving process tree at Windows suite teardown

The Windows suite now passes all 374 files in ~200s, then sits until the job
cap kills it. vitest's hanging-process reporter printed nothing, because the
suite runs under `bun --bun` where that detection does not apply.

Swap it for a global teardown that dumps the live process tree on Windows, to
name whatever still holds the event loop open. The failure path exits in 0.2s
while the success path hangs, which points at a surviving child rather than a
reporting bug. Removed once the leak is fixed.

Assistant-model: Claude Opus 5

* test: probe surviving handles after the Windows suite finishes

The teardown process-tree dump showed no surviving bash/sleep/git children and
no leftover vitest workers, so the handle that blocks exit lives inside
vitest's own process rather than in spawned test children.

Replace the one-shot dump with unref'd probes at +15s and +45s that report
process.getActiveResourcesInfo(). Unref'd timers fire only while the loop is
still alive, so a healthy run exits before they run and pays nothing.

Assistant-model: Claude Opus 5

* test: name the Windows survivors holding the suite's stdio pipes

* test: widen the Windows teardown probe to every recent process

* fix(test): stop the Windows suite from launching a real browser

The interactive OAuth callback test drives a real LoginDialogComponent
through onAuth, and showAuthUrl calls openBrowser. On Windows that spawns
a detached `rundll32 url.dll,FileProtocolHandler https://corp.invalid/login`,
which inherits the test process's stdout/stderr write handles. The CI
runner has no browser to hand off to, so rundll32 never exits: the vitest
process, `bun run`, and every descendant terminate, but the step's pipes
never reach EOF and the job hangs until its 15-minute budget expires.

CI evidence (run 30495631917, Windows job): all 2887 tests pass, the child
exits code=0 at 22:25:38, and a Win32_Process table taken every 30s after
that shows exactly one non-infrastructure survivor, pid 2508
`rundll32 url.dll,FileProtocolHandler https://corp.invalid/login`, started
22:24:23 with a dead parent, still alive when the job is cancelled.

5ef323e added this test, which is why the bisect lands there. Mock the
launcher exactly as pi's login-dialog suites do, and assert the dialog
still hands the URL to it so the launch path stays covered without
spawning a process.

Assistant-model: Claude Opus 5

* test: remove the Windows teardown diagnostic scaffolding

The probes named the leaking process, so drop them: the vitest globalSetup
that dumped the surviving process tree, its config entry, and the process
table the flaky-suite wrapper printed after the child exited. The streamed
tee stays, since a timed-out attempt still needs a self-describing log.

Assistant-model: Claude Opus 5
sina85 added a commit to sina85/atomic that referenced this pull request Aug 3, 2026
Bring the node-card model/thinking/fast + durable thinkingLevel persistence
up to date with upstream's tabs reformat, the ModelRuntime refactor (bastani-inc#2064),
and the reshaped graph node cards (bastani-inc#2140). Re-applied thinkingLevel across
the durable serialize/restore path, integrated the model row into the
reshaped card, and preserved the replay model-identity fix. NODE_H stays 6
for the model row; updated the bastani-inc#2100 graph-perf composed-row expectation to
match the taller card.
@flora131
flora131 deleted the feat/pi-model-runtime-parity branch August 14, 2026 01:16
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