Skip to content

Atlas Agent: replace the Cersei engine with a one-time Codex port - #210

Merged
pacifio merged 43 commits into
pacifio:0.3.1from
Ukaykhingmarma28:ukaykhing25/codex-port
Aug 30, 2026
Merged

Atlas Agent: replace the Cersei engine with a one-time Codex port#210
pacifio merged 43 commits into
pacifio:0.3.1from
Ukaykhingmarma28:ukaykhing25/codex-port

Conversation

@Ukaykhingmarma28

Copy link
Copy Markdown
Collaborator

Replaces Atlas's native agent — the Cersei SDK — with a one-time hard fork of OpenAI's Codex, shipped as Atlas Agent, talking to the Atlas AI gateway.

Executes ADR-0003. The plan, the evidence behind it, and every decision are in docs/atlas-agent-codex-port-spec.md and four research documents under docs/research/.

Why

Every serious defect in the native path required forking someone else's crate: the UTF-8 streaming corruption and the tool-cancel race lived on only as Atlas-maintained vendored patches, redone against every upstream release. Users felt that ownership gap as dropped connections and fragile streaming. Codex was chosen because it already ships the reliability machinery our audit found missing — verified against source, not taken on faith: real end-to-end cancel, typed retry with history resumption so tool calls are never re-run, and lossless incremental UTF-8 SSE decoding that makes our patched bug class impossible.

What changed

  • Engine. 110 crates vendored under vendor/codex/, quarantined first, then wired in-process through the app-server client. No upstream tracking — from here the engine is ours.
  • Provider. A new OpenAI Chat Completions dialect against the Atlas gateway, plus an Atlas error-classification arm: 402 is terminal and surfaces the quota detail, 429 honours Retry-After, 401 token_expired refreshes once. Upstream auto-retried a 402 five times — the loop the gateway doc was written to prevent.
  • Auth. Per-request token provider on the engine's ExternalAuth seam, re-minting inside the JWT TTL, replacing a static bearer that would have killed every session at ten minutes.
  • Deleted. atlas-cersei, the Cersei SDK dependency, both vendored patch forks, their patch-table entries, and the dead call sites.
  • Phone-home. Three sites removed at source, not configured off — including one in an elevated Windows process that was outside the ticket.
  • Licensing. Atlas is MIT, the engine is Apache-2.0. LICENSE and NOTICE now ship inside the app bundle; modified fork files carry change notices.

Merge with 0.3.1

Merged cleanly against six conflict hunks plus two semantic breaks git resolved textually but wrongly. Notably: 0.3.1's fork fix — a branch belongs to the source session's project, not the active workspace — was preserved by porting it into the shared fork helper, where it now reaches both /fork doors. Details in the merge commit.

State

16 of 18 tickets closed (#38#50, #52, #54, #55). Draft because two remain, and both need a human, not an agent:

Green locally: typecheck, lint, format, 887 frontend tests, 332 src-tauri tests, cargo check --workspace across 130 members.

One decision needed before merge

docs/adr/ now holds two 0003s and two 0004s — 0.3.1 published 0003–0006 while this branch was adding its own 0003 and 0004. The port's two should renumber to 0007/0008, but that restamps identifiers cited across 18 published tickets, the spec, and code comments, and one file (docs/reference/atlas-ai-api.md) contains an unrelated ADR-0003 belonging to the gateway repo that a blind sweep would corrupt. Left deliberately unresolved for a maintainer call.

Atlas resolved every crate on its own until now. The reason was real: the
old ACP stack pinned agent-client-protocol 1.3 whose schema crate is pinned
exactly (=1.4.0), the ported stack pins 2.0 / =1.5.0, and no single cargo
resolution can hold two exact pins of the same crate. The old stack is gone
and every consumer is on =2.0.0, so the collision went with it — the premise
correction the port spec opens on (D4, Phase 0).

The workspace exists so the vendored Codex engine (#42) lands in the same
dependency graph as the app rather than a parallel one.

Moved, not rewritten:
  - [patch.crates-io] from src-tauri, atlas-cersei and atlas-native-agent
    (three copies of one fact) to the root. Cargo honors a patch table only
    in the manifest it was invoked on, so a copy left in a member is config
    cargo ignores in silence.
  - The release and dev profiles from src-tauri, same reason.
  - src-tauri/Cargo.lock to the root; the 20 per-member locks are deleted.
    The lock diff carries zero name/version/source/checksum changes — only
    the dev-dependency edges of crates that are now members.

resolver = "2" is pinned explicitly: a workspace root defaults to resolver 1
regardless of member editions, which would have silently changed feature
unification.

Dev opt-levels needed restating per member. [profile.dev.package."*"] reaches
dependencies only, so every atlas-* crate — a path dependency before, a member
after — would have dropped from opt-level 1 to 0 and made `tauri dev` run the
ACP stack, terminal, embedder and RAG engine unoptimized, with no error to say
so. Verified against cargo: with the stanza rustc gets -C opt-level=1, without
it no -C opt-level at all.

Excluded, deliberately:
  - crates/atlas-kb-server — in no member's dependency graph; a template
    binary knowledge_export compiles at runtime via --manifest-path, under
    its own [profile.release] (panic = "abort", thin LTO). Profiles are
    workspace-global for lto/panic/strip, so as a member it would rebuild
    under the app's profile.
  - vendor/cersei-{agent,provider} — they enter through [patch.crates-io] as
    dependencies, which is what keeps them inside the "*" opt-level override
    that members fall out of.

Consequences that would otherwise have broken quietly:
  - Cargo's target dir moved to the workspace root, so build-dmg.sh,
    set-dmg-icon.sh and release-macos.sh were looking where the bundler no
    longer writes. Verified: `tauri build` now emits
    target/release/bundle/macos/Atlas.app.
  - CI rust-cache workspaces repointed at the root (atlas-kb-server keeps
    its own).
  - clean:rust would have spared the one target dir that now holds
    everything.
  - ARCHITECTURE.md, CONTRIBUTING.md and the atlas-native-agent header all
    asserted that no workspace was possible. The spec names that header as
    stale documentation (integration §6.0).

tests/cargo-workspace.test.ts guards the invariants that have no compile error
to announce them: patches and profiles nowhere but the root, a dev opt-level
restated for every member, and no packaging script left pointing at
src-tauri/target. cersei-containment.test.ts now walks the root manifest too —
without it that guard would report containment at #54 while the root still
patched cersei-*.

Repaired in passing, disclosed rather than folded in silently: the
atlas-native-agent e2e suite had not compiled since 67819de, which added
ConnectOptions::request_elicitation_events and updated
atlas-agent-servers/tests/connect.rs but not this one. Six real tests were
dead. Fixed with the sink helper connect.rs already established; all six pass.

Nothing deleted from the Cersei path: crates/atlas-cersei, vendor/cersei-*
and the SDK deps all survive until the acceptance bar (#53) is green, and
crates/atlas-native-agent is the seam and never on a delete list.
Two `links=` collisions block vendoring the Codex engine (#42). A crate
declaring `links` may appear once in a dependency graph, so these are hard
blockers rather than preferences.

sqlite3 — fixed here. The engine pins `libsqlite3-sys = "0.37"`; Atlas
resolved 0.30.1 through `rusqlite = "0.32"`, which hard-wires it. Both sides
bundle a vendored SQLite, so a second copy would collide on duplicate
`sqlite3_*` symbols even if cargo allowed it.

Bumped rusqlite 0.32 -> 0.39 in the three crates that declare it. Not 0.38,
which the research prescribes: that instruction is stale. Today rusqlite 0.38
resolves libsqlite3-sys 0.36 — below the engine's >= 3.51.3 compile-time
assert, so following it literally would have failed the ticket's first
acceptance criterion. 0.40 resolves 0.38.x, newer but still a second
`links = "sqlite3"` crate. Only 0.39 lands on 0.37, which bundles SQLite
exactly 3.51.3.

tree-sitter — already unified. `atlas-codeindex` is on 0.26 and the lockfile
holds one 0.26.10. BLOCKER B's remaining half is bumping the fork, which
arrives with the fork in #42. The guard below is what keeps Atlas's half from
drifting away before then.

No API breakage across the seven rusqlite releases: the workspace compiles
clean and all 1313 pre-existing Rust tests still pass.

Guards, because neither invariant has a compile error to announce it until the
fork is in-tree, at which point it surfaces as a link error far from its cause:

  - tests/cargo-deps-unification.test.ts asserts the resolution — one
    libsqlite3-sys at exactly the engine's 0.37 major (a pin, not a floor:
    0.38 collides as surely as 0.36), one rusqlite, one tree-sitter at 0.26,
    and no drift between the four rusqlite declarations.
  - crates/atlas-thread-metadata/tests/sqlite_floor.rs asserts the linked
    library rather than the lockfile, so a manifest edit that fails to take
    effect cannot satisfy it.

Verified against real data as well as tests: a copy of a live threads.db
written under SQLite 3.46 opens under 3.51.3, recovers its WAL, returns
`PRAGMA integrity_check = ok`, and reads back every row.

Resolves spec open question 6, recorded inline in the spec under its existing
RESOLVED convention.
A verification spike: no production code. Both Phase 0 questions are answered
and their answers written back into the spec under its existing RESOLVED
convention, so the next reader finds them where the question was asked.

Open question 5 — the fresh-profile native BYOK path is live-broken, and in a
worse shape than "the user has no key". `byok_get` reads `byok-keys.json` with
no env fallback, and nothing in the tree writes that file any more: the store
died in the 0.3.0-strip and Settings > API Keys now edits shell-profile export
lines. Because `default_provider_model` derives provider AND model from the
same dead file, both come back empty and the turn stops one guard earlier than
the research predicted -- "No model selected", not "No API key configured" --
while the picker is empty and the remedy the message names cannot work.
Corroborated on disk: a profile wiped 08-23 and launched since has no
byok-keys.json. The cutover therefore fixes a live break, and there are no
native BYOK credentials to preserve because none can be created.

Open question / Testing-Decisions assumption on the three contract tests --
confirmed present and green in real CI, evidence on the issue rather than in
the spec, since the spec only carried it as an assumption to check.

Also on the issue, for #53's benefit: the three red CI jobs on main are two
clippy-lint regressions under a newer toolchain and one compile error that #38
already fixed. None is a behavioral failure.
Reconciliation work for #41 turned up that its founding premise is false. The
spec says the desktop app has no auth client, no token minting and no
gateway-host reference; all three exist and are wired in, with
AuthCore::mint_access_token() already serving two Rust callers on demand.

Corrected in place with a dated note rather than a silent rewrite, because the
false premise sizes both #41 and the tracer bullet that depends on it.

The ticket stays open and ready-for-human: its reconciliation criterion needs
docs/api/atlas-auth-api.md, which the code cites but which was never committed
(the broad *.md gitignore rule; no such object exists in any commit).
The forked engine lands in-tree at the ADR-0003 fork point, openai/codex
@ 42b5f05: a plain copy under vendor/codex, no submodule, no upstream remote,
no tracking. Upstream's LICENSE and NOTICE travel with the code, as Apache-2.0
requires of a redistribution. Nothing that ships depends on any of it.

The closure is 110 crates, not the spec's 77. That number is not a
disagreement about scope, it is a different root: the Port Inventory measured
the closure of codex-core, while D1 chose the in-process app-server client,
which additionally pulls codex-app-server itself and the ext/ extensions the
inventory lists as droppable. 105 crates normal+build, plus the 5 test-support
crates their dev-dependencies need, because the Testing Decisions keep
upstream's suite. Upstream has 140 members, so 30 drop rather than 63.
Computed from cargo's own resolve graph, not by reading the manifests.

Both links= collisions are now closed. libsqlite3-sys was settled in #39 and
needed nothing here: one 0.37.0 serves the engine's state layer and Atlas's
three rusqlite users. tree-sitter is resolved the way D4 says, by bumping the
fork 0.25 -> 0.26 rather than pinning Atlas back. tree-sitter-bash stays at
0.25 because no 0.26 exists and none is needed: grammars bind to the
tree-sitter-language shim, not to the core, which is what lets one core serve
Atlas's 0.23 grammars and the fork's 0.25 at once.

Vendoring a fork means pinning its resolution, not just its code. The first
build failed inside rama-core, which is written against rama-error /
rama-utils / rama-macros 0.3.0-alpha.4 and got the stable 0.3.0 releases
published since the fork point. Traced through rama-utils, which is the one
that drags the rest, and pinned the family back to the fork point. Expect this
class of drift again: the fork point pins code, and the registry keeps moving.

Two effects on Atlas's own build, disclosed rather than absorbed:

  - tar moves 0.4.46 -> 0.4.45. The engine pins it exactly and
    atlas-agent-store asks for 0.4, so cargo intersects them. One patch step
    inside a compatible major; relaxing the fork's pin is rip-out work, not
    vendoring work.
  - ~60 crates gain a second, older major beside Atlas's. They coexist,
    Atlas's version is retained in every case, and none reaches the shipping
    app. Checked with `cargo tree -p atlas --edges normal`, because
    `cargo metadata`'s resolve graph is a workspace-wide union that reports
    edges the app never compiles - reading the lockfile alone says the app
    gained OpenSSL, and it did not.

Upstream's crossterm patch is deliberately not carried: its only consumer is
the TUI, which is outside the closure, so the entry patched nothing and earned
a warning on every cargo invocation.

Two traps found while landing this, both silent:

  - .gitignore's broad *.md rule swallowed 35 paths of the engine, among them
    core/*_prompt.md, the baked system prompts, which are include_str!d at
    compile time. The tree built from my working copy and would have failed
    from a fresh clone. Un-ignored, and codex-quarantine.test.ts now asks git
    directly rather than trusting the negation to survive.
  - lint-staged ran oxfmt --write over *.ts by basename at any depth, so the
    commit that vendors 657 upstream TypeScript files would have reformatted
    them on the way in. Scoped to the directories Atlas owns, which is where
    `bun run lint` and `format:check` already looked.

Guards: codex-quarantine.test.ts asserts the engine is whole, unignored,
submodule-free, and referenced by nothing shippable - cargo cannot say any of
that, since an unused member is not an error and adding a dependency on one is
the most ordinary edit there is. It matters before #43 because codex-analytics
and codex-otel are in the closure and both phone home. cargo-workspace.test.ts
now distinguishes Atlas's members from the vendored ones, and records why the
engine is exempt from the per-member dev opt-level rule.

Verified: cargo check --workspace green over all 130 members, zero warnings.
Atlas's own 1314 tests still pass, unchanged from #39. 836 frontend tests.
The reconciliation task was parked waiting for atlas-auth-api.md. It turns
out not to be needed: every auth fragment D14 was flagged on is sourced by
docs/reference/atlas-ai-api.md, which is already in-repo. §12.2 states the
10-minute TTL, GET {AUTH}/token and the T-60s re-mint verbatim; §3.1 gives
the token audience and the orgs claim the desktop already decodes; the
401 token_expired refresh-once rule appears in both §12.2 and the §9 table.

atlas-auth-api.md is a genuinely separate document, not a missing copy of
this one — the code's two citations into it do not resolve here (store.rs
cites §12.5, whose §12 ends at 12.3; core.rs cited §6.1, whose §6 is
GET {AI}/models). What it alone carries is the auth service's own
device-grant, sign-out and revocation surface, which neither D14 nor D10
depends on. The TTL citation is repointed at the doc that actually states
it; the §12.5 one is kept and marked unfollowable.

Reconciliation also turned up a clause that narrows D10: §3.1 says auth is
verified once at request start and never re-checked mid-stream, so a token
expiring mid-turn does not truncate the in-flight request — the next
request is what fails.

Both of D14's carried caveats are settled. Credential storage: the 0600
file is ratified as a recorded exception rather than left as a silent
deviation. A keychain ACL binds to the code signature, tauri.conf.json
sets no signing identity, and the auto-updater replaces the binary every
release, so macOS stops permitting the silent read and prompts every user
after every update — the item is never lost, only silent access to it. The
access JWT is never written to disk at all. Revisit under a Developer ID
identity.

Proactive re-mint: confirmed absent rather than partial. mint_access_token
is a bare GET /token that caches nothing and tracks no expiry, and all four
production callers mint fresh at point of use; expires_at/is_expired belong
to PendingGrant and never touch the access JWT. Building the cache moves to
the seam work, where its only consumer lives.

Doc and comment changes only; cargo check -p atlas clean, 55 auth tests
pass.
D2 requires these removed, not configured off, before any build carrying the
fork leaves a developer's machine. Three egress sites existed, not the two
the ticket named.

1. The Statsig metrics exporter. otel/src/config.rs held an ingestion
   endpoint and a hardcoded client key, expanded from an `OtelExporter::
   Statsig` variant that core *defaulted* the metrics exporter to. The gate
   on it was `cfg!(debug_assertions)`, so it was live in exactly the builds
   that ship. Gone: both enum variants, the constants, `resolve_exporter`
   (identity once the variant left), `StatsigMetricsSettings` and its global
   install/read pair, the config-schema value, and the README section. The
   default is now `None`.

2. The ChatGPT analytics client. analytics/src/client.rs resolved a ChatGPT
   backend ingestion path off `chatgpt_base_url`, was constructed for every
   session, was on unless config said otherwise, and — per fork-seam 3 —
   uploaded a subset of events even under plain API-key auth, i.e. for users
   who had never signed into that backend. Gone: the destination type, the
   POST, the batch splitter, the debug capture-file sink, the request
   envelope, and the two `TrackEventRequest` methods that decided what could
   travel under API-key auth.

3. The Windows sandbox setup helper, which the ticket did not name. It built
   its own OTEL provider pinned to the Statsig exporter and emitted WFP
   firewall setup outcomes from an elevated process, independent of core's
   default and of the analytics client. The parent passed the resolved
   Statsig environment down through the elevation payload. Removed with the
   route it needed; those outcomes still reach the caller's `log` callback,
   which stays on the machine.

Kept deliberately: `OtlpGrpc`/`OtlpHttp`. They carry no default and no
endpoint of their own, so they do nothing until a user configures one. D2
forbids phone-home, not telemetry someone asked for.

Also kept: the analytics reducer and its queue, which
analytics_client_tests.rs covers. The pipeline stays and its output is
dropped where the upload used to be; collapsing the crate is Phase 5
slimming, not D2. The 15 tests in client_tests.rs were the delivery suite
and went with the delivery. Two tests in the surviving suite did reach
removed API: the capture-file delivery test is deleted, and one reducer test
keeps its coverage minus a `can_send_with_api_key_auth` assertion about the
upload gate.

Upstream's own `metrics_exporter_defaults_to_statsig_when_missing` is
inverted rather than deleted, so a config path that silently starts
exporting again fails in Rust and not only in the new guard test.

tests/codex-no-phone-home.test.ts is the permanent guard: no key, no
endpoint, no Statsig identifier in shipping code, and no HTTP anywhere in
the analytics crate. It scans production code only — upstream's suites mount
local wiremocks on the old paths, and a mock bound to loopback cannot phone
home. Those suites now assert removed behaviour; that debt is recorded on
the ticket for Phase 5, where these crates are slimmed anyway.

cargo check --workspace clean over 130 members with zero warnings from the
touched crates; codex-analytics 86 tests and codex-otel 55 tests pass;
Atlas's own 1,314 Rust tests pass, identical to the #39/#42 baseline; 845
frontend tests pass. The new guard was mutation-tested both ways: restoring
the key and endpoint fails it, restoring an analytics POST fails it.

Not compiler-verified: wfp_setup.rs and its caller are
`cfg(target_os = "windows")`, and cross-checking dies in ring/aws-lc-sys for
want of an MSVC toolchain. Verified by inspection plus a dangling-reference
sweep, which caught one stale test assertion on the removed payload field.
Atlas's own code is MIT; the vendored engine is Apache-2.0. The second does
not dissolve into the first, so the obligations travel with every build
rather than being discharged by Atlas's own licence file. D11 gates all
rename work on this, which is why it lands in Phase 1 and not beside the
renames it protects.

Section 4(a) and 4(d) — the licence and notice reach recipients. Both files
existed in the tree since #42 and reached nobody: bundle.resources listed
only nvm.sh. They now ship at Contents/Resources/licenses/, next to Atlas's
own MIT licence, so a user who installs the app can read all three. The
NOTICE keeps its Ratatui lines even though the TUI is dropped, and travels
verbatim — including the U+00A0 non-breaking spaces upstream put around
"2025", which is why the test matches on \s rather than a literal space.
The file bends to nothing; the assertion does.

Section 4(b) — every modified vendored file now carries a one-line notice as
its first line. Nineteen files from #43, plus config/src/schema.rs from this
commit. The modified set is computed from git against the vendoring commit
rather than maintained by hand, so the rule cannot rot as the set grows.

core/config.schema.json needed the notice injected into its *generator*
rather than typed into the file. It is generated, and core's
config_schema_matches_fixture compares the fixture against a fresh render:
a hand-written header would have been dropped by the next
write_config_schema and then failed that test for a reason that looks
nothing like its cause. config_schema_json now emits a root $comment, a
draft-07 keyword validators ignore, so the notice survives regeneration.

Section 4(c) and section 6 — recorded in CONTEXT.md, which CLAUDE.md makes
the single-context file and is therefore what the Phase 5 rename tickets
will read. Attribution inside vendored sources is never stripped by a rename
sweep: rename product branding, keep attribution. And the trademark scrub is
written down as a licence *requirement* rather than a preference — Apache-2.0
grants no trademark licence, so dropping "Codex" and "OpenAI" as
product-facing names is obligatory, including the baked system prompt and the
catalog instructions_template strings.

tests/vendor-licensing.test.ts holds all of it, and `bun run test` in CI runs
it, so this cannot regress quietly. Its parser-health cases exist because
each rule here fails by finding nothing: a modification set that came back
empty would satisfy the 4(b) rule forever.

Verified in a real bundle, not just in config: bun run build:app, then
Atlas.app/Contents/Resources/licenses/ holds Atlas-LICENSE.txt,
OpenAI-Codex-LICENSE.txt and OpenAI-Codex-NOTICE.txt, each opening on the
right text.

852 frontend tests pass; cargo test -p codex-core config_schema passes;
cargo check clean across the touched crates.
Phase 2's first task, per the spec: settle how much of the stdio server's
startup the in-process app-server entry performs before committing to the
client layer over raw core.

It performs none of the three. The OTel provider is built only on the stdio
path and `in_process.rs` imports no otel symbol; the unix-socket startup lock
is gated on a transport enum the in-process runtime does not have; and the
SQLite state db is an `Option<StateDbHandle>` field the caller supplies. The
ambient process state is injected, not assumed, so there is nothing to
neutralize and the app-server client layer is confirmed over raw core.

One side effect the research did not find cuts the other way, and it
constrains the seam: `resolve_installation_id` is not a read. It creates
`codex_home` and writes a 0644 installation-id file into it, so starting the
engine touches disk and `codex_home` has to be an Atlas-owned path rather
than `~/.codex`. Under D9 that tree is engine-private working storage.

One gap found in the opposite direction: there is no `ExternalAuth`
injection point on either start-args struct, and the only in-protocol route
to `set_external_auth` is ChatGPT-shaped and gated on a login method D2 rips
out. ADR-0004 records adding the field to the fork as the answer.
…D10 token provider

The `ported-engine` cargo feature is the development-time switch. It is
off by default, so a default build of the seam resolves exactly as it did
before the port — `cargo tree -p atlas-native-agent --edges normal` names
zero codex crates without it.

Config assembly lives in the seam, where the spec puts it: the seam is the
only place that knows both Atlas's settings and the engine's shape, so
src-tauri keeps calling the trait surface and nothing else. It reaches the
engine two ways for a reason. `codex_self_exe` has no config-file spelling
at all — the engine says it "must be set in code via ConfigOverrides" — so
that and the sandbox/approval defaults go through the struct. The provider
and the analytics switch go through TOML keys instead, which puts them
through the engine's own `validate_model_providers` rather than through a
check of ours.

`EngineHome` is a newtype because getting it wrong is silent: starting the
runtime calls `resolve_installation_id`, which creates the directory and
writes a 0644 file into it. Pointing it at ~/.codex would have Atlas write
into the user's real Codex CLI state, so it is Atlas-owned by construction.

The D10 token provider is the other half. `ExternalAuth` gives per-request
resolution, proactive re-mint and 401-refresh-once from machinery the
engine already ships; the static-bearer path would have none of them and
would kill every session at the ten-minute TTL. Expiry is read from the
token's own `exp` rather than assuming the documented TTL, and the two
cases that would otherwise degenerate are pinned by tests: refresh must
ignore the cache, or refresh-once becomes a loop against the credential the
gateway just rejected; and a token that arrives already inside the margin
gets one use rather than re-minting on every request.

The quarantine guard gets its first allowlist entry — which its own
docstring said to add on this day. What replaces it is stricter than what
it gave up: every allowlisted codex dependency must be `optional = true`
and the feature must not be in `default`. Both halves were mutation-tested;
dropping either one fails the suite.
The tracer bullet. A turn now completes end to end on the ported engine,
driven through the same `AgentConnection` the app already drives, with the
engine running in this process (ADR-0004).

`EngineAgentServer` implements the same `AgentServer` trait as the Cersei
one against the same agent id, so `select_native_agent` returns one trait
object and nothing downstream knows which it got. The stored id stays
"cersei" on purpose: it is a storage key, and minting a new one here would
orphan every native history row (D7).

Three things the research did not predict, each of which cost a real
debugging pass:

`thread/start` reloads config. The `Config` handed to the start args is not
what a thread gets — `ConfigManager` re-derives one from `cli_overrides`.
Leaving those empty makes the provider exist at startup and vanish at the
first turn, reported as "Model provider not found" by a config that loaded
fine seconds earlier. They are passed in both places now.

`codex_self_exe` reaches a thread only through `arg0_paths`, because
`apply_arg0_paths` stamps it onto every reloaded config. `Default::default()`
loses it silently, and the symptom would have been a broken sandbox rather
than an error.

The engine needs big worker stacks or the process aborts. `codex-arg0`
gives every upstream binary 16 MiB workers; Atlas does not use arg0 and
adopts the host runtime, which is Tauri's 2 MiB default, so `thread/start`
overflowed and SIGABRTed — not an error a Result carries. Boxing on the
caller's side does nothing, because the overflow is in the engine's own
MessageProcessor task, which Atlas never awaits. The seam now owns a
runtime built with upstream's own 16 MiB figure and spawns engine work onto
it. `tests/engine_turn.rs` runs at the default stack deliberately: setting
RUST_MIN_STACK there would hide the regression it exists to catch.

One race is closed rather than avoided. A turn's outcome arrives as a
notification, but its id only exists after the response, so the waiter
cannot be registered before the request and a fast turn can finish first.
TurnWaiters buffers unclaimed completions, bounded, so a prompt cannot park
on a notification that has been and gone.

Gate: 1,314 workspace tests, 853 frontend and guard tests, clippy clean on
both switch positions. The Cersei e2e suite passes in both, which is what
"unchanged when the switch selects Cersei" means here. A default build of
the app still names zero codex crates in `cargo tree --edges normal`.
Acceptance-bar items 4 and 5, asserted where the spec says to assert them —
Seam 1, through the trait, on what the app is actually told.

Cancel already had a path from #45; what it lacked was proof. The two
failure shapes worth ruling out are the ones a user would meet: a cancel
that does nothing and leaves the composer spinning, and a cancel that ends
the turn as if the model had finished, which loses the fact that the answer
is incomplete. The test rules out both, and a second one pins that
cancelling with nothing in flight is quiet rather than fatal — that path is
reachable whenever a cancel races a turn that just ended.

Retry needed real work. The engine emits a stream error with `will_retry`,
and the sink was dropping it. A retry the engine performs but never
announces is indistinguishable from a hang, so the notice now reaches the
thread as `AcpThreadEvent::Retry` and the test asserts on the announcement,
not just on the turn completing.

Two details the retry pill forced, both from D8 — the engine's stream-error
event carries a message and a flag and nothing else:

The attempt number is counted in the seam, because the event does not carry
one. The ceiling is not: it is the provider's configured
`stream_max_retries`, which the seam is the thing that set. That matters
because the pill renders "attempt N of M" and an unknown M would render as
"1/0". It is also now written explicitly into the provider config, so the
number shown and the number used cannot drift.

The backoff delay is left at zero rather than invented, so the pill counts
up from now instead of down to a deadline that does not exist.

One fixture correction worth keeping: a 500 is not a killed stream. It
fails the request before a stream exists, and the engine retries that at the
request layer without a word — only a stream that opens and then stops
produces the event that carries `will_retry`. The first version of this test
passed its "turn completed" assertion while proving nothing about retries.

Not covered here, and not silently: bar item 4's "a running tool's process
group is terminated" and bar item 5's "without re-running already-executed
tool calls" both need tool calls to exist at the seam, which is the approval
round-trip in #47. Recorded on the issue rather than quietly dropped.

Gate: 7 seam tests green three runs running, Cersei position 6, clippy clean
both positions, 853 frontend and guard tests.
Two of the three criteria. The approval round-trip is the third and is not
in this commit.

Spec open question 4 is resolved and recorded: the per-session effort knob
is `thread/settings/update`'s `effort` field, and the same call carries the
permission modes as `approval_policy` plus `sandbox_policy`.

Both halves of that pair are needed, which is the part worth writing down.
The sandbox cannot express "ask first", and the approval policy cannot stop
a command that never asks. Using one of them alone produces modes that
differ in label and not in behaviour — so Ask is `UnlessTrusted` over
workspace-write rather than `OnRequest`, because `OnRequest` under a
workspace-write sandbox only asks when something needs to escalate, and
ordinary commands would run unasked. Plan pairs read-only with `Never`: the
sandbox is what makes it read-only, and `Never` is what stops the engine
offering to escalate out of it, which would turn a read-only mode into a
nagging one.

Mode ids, names and descriptions are copied from the Cersei path verbatim.
The picker is shared, so any difference would show as the UI changing when
the switch flips. Cross-agent aliases are normalised for the reason the
Cersei path records: matching ids exactly meant "Bypass" arriving as
`bypassPermissions` fell through to the default and silently prompted on
every action — a mode that looks set and does nothing. An unrecognised mode
fails towards asking rather than towards running.

A finding that gates all of it: `thread/settings/update` requires the
`experimentalApi` capability and refuses outright without it. 76 protocol
methods sit behind that flag, including every lever this ticket needs. Atlas
now advertises it, deliberately: "experimental" upstream means "may change
upstream", which is a risk this port does not carry, because ADR-0003 makes
it a hard fork with no upstream tracking. Upstream's own client tests set it
too.

The mode is applied to a new thread, not merely recorded. Recording without
pushing would leave the picker showing "Plan" while the engine ran on its
own defaults and edited files anyway.

Gate: 53 tests on the engine position (40 unit, 6 e2e, 11 seam), 6 on the
Cersei position, clippy clean both ways. The four modes and all eight effort
levels are exercised against a running engine, not just against the mapping.
Fixes a bug the previous commit introduced and its own test caught: the mode
and effort controls each minted request ids from a fresh counter starting at
zero, so they collided with the connection's. The engine rejects a repeat
outright — `duplicate request id` — and the symptom in production would have
been a prompt failing to start right after the user changed a mode or the
effort level.

It showed up as a ~50% flake rather than a clean failure, because the engine
only rejects ids that are concurrently in flight, and that depends on
whether the fire-and-forget effort updates are still outstanding when the
prompt goes out. The previous commit was made while that test was red, which
should not have happened; this is the fix.

The invariant now lives on `RequestIds` itself, where someone adding the next
per-session control will read it: there is exactly one counter per
connection and every control mints from it. Giving them their own is the
obvious-looking thing and it is wrong.

The new test covers the ordinary sequence a user produces — change some
settings, then send a message — and says plainly in its own comment that it
does not deterministically reproduce the collision, because reintroducing
the bug does not reliably fail it. Better to state that than to leave a test
claiming a guarantee it does not provide.

Gate: 58 tests on the engine position, 12 of them at the seam, stable across
four consecutive runs; 6 on the Cersei position.
The third criterion, and the last of #47. Acceptance bar item 7.

The vocabulary the ticket names — accept, accept-for-session, decline,
cancel — did not have to be invented. The engine already speaks exactly it
(`CommandExecutionApprovalDecision`, `FileChangeApprovalDecision`) and
Atlas's dialog already speaks its own form. This is the join, so the engine's
prompts render in the dialog an external ACP agent's prompts already use.

One asymmetry is worth knowing, because it looks like a bug: the dialog
offers three options and produces four outcomes. Cancel is not a button —
it is the dialog being dismissed or the turn going away underneath it. So
the option list has three entries and the decision mapping has four arms.

Decline and cancel stay distinct, which the engine's own docs insist on: a
decline lets the turn continue, a cancel interrupts it. Collapsing them
would either strand a user who dismissed the dialog inside a turn that keeps
running, or kill a turn they only meant to steer away from one action.

The answer is read from the option's kind, not from the id Atlas minted. The
id is ours and could be anything; the kind is what the dialog rendered and
what the user pressed. Anything unrecognised declines — the only safe
direction when the alternative is running something nobody clearly approved.

The pump had to change shape. Answering an approval needs the client, which
the pump owns and borrows mutably while waiting on `next_event`. So dialog
tasks send their answers back through a channel and the pump selects between
the two, biased towards answers because a turn is blocked on every one. The
dialog itself never blocks the pump: it stays open as long as the user
takes, and the turn's own progress arrives on the same stream that would
otherwise be stalled.

The permissions surface is answered conservatively and deliberately. It
grants capabilities rather than answering yes or no, and Atlas has no UI for
choosing which. Approving grants nothing extra, so the engine proceeds under
the sandbox it already has instead of silently widening it on a click the
user could not have understood.

A trap worth keeping: the first version of these tests asked to run `echo`,
and proved nothing. "Ask" mode is `UnlessTrusted`, `echo` is trusted, and it
ran without ever raising a dialog — the turn completed, the assertions on it
passed, and the only symptom was a helper timing out. The tests now use a
command the engine cannot trust, and say so.

Gate: 68 tests on the engine position (48 unit, 6 e2e, 14 seam), seam suite
stable across three runs, 6 on the Cersei position, clippy clean both ways,
853 frontend and guard tests.
The half that was blocked on #47. A tool call could not reach the seam until
approvals round-tripped, because in Ask mode an untrusted command raises a
dialog and waits.

Both tests assert on the filesystem rather than on protocol chatter, because
protocol chatter cannot tell the interesting cases apart. A turn can report
"cancelled" while the command it started keeps running, and a retried turn
looks identical whether the tool ran once or twice. The side effect is the
only witness.

Cancel: the model asks to sleep and then touch a marker; the turn is
cancelled mid-command; the marker must never appear, checked past when the
command would have finished. Without that, "the turn ended" and "the work
ended" are the same assertion — and an orphaned child writing to the user's
disk after the UI says the turn stopped is exactly the failure bar item 4
names.

Retry: the command appends a line, the stream dies after it has run, and the
engine retries the turn. The file must have one line. A command run twice
across a retry is the expensive failure — an rm, a deploy, a payment. The
test also asserts a retry was actually announced, or "ran once" would be
trivially true and prove nothing about retries.

A control test comes with them, and it is not ceremony. Both assertions are
about a file that does not appear, or appears once, and both would pass just
as well if approved commands never ran at all — which is precisely what a
workspace-write sandbox does to a path outside the workspace. The control
proves an approved command really does run, so "the cancel killed it" and
"it could never have run" stay distinguishable. Writing to a tempdir works
because workspace-write includes TMPDIR by default.

Gate: 71 tests on the engine position (48 unit, 6 e2e, 17 seam), seam suite
stable across two runs, 6 on the Cersei position, clippy clean both ways,
853 frontend and guard tests.
Acceptance bar item 11, and the src-tauri call sites the survival research
left as "replacement or removal".

The retrieval never moved and did not need to: `atlas-memory`, `atlas-embed`
and `atlas-codeindex` depend on neither engine. What died with the Cersei
path was only the tool projection — the shim exposing that retrieval to the
agent. This is its replacement, calling the same retrieval through the same
callback shape, so `src-tauri` injects the same closure it already had.

It rides the engine's dynamic-tool seam: the client declares a tool at
`thread/start` and the engine calls it back over `item/tool/call`. That is
the right place for it, because the retrieval needs Atlas's app state and
embedding model, which the engine has no way to reach and no business
holding.

The tool description is copied verbatim. It is a prompt, not a label — it is
what decides whether the model reaches for memory before asking the user, and
rewording it would change behaviour on the switch for reasons nobody would
connect to the switch.

Three things it refuses to do. It is not advertised at all when no retrieval
is registered, because a tool the model is told about and cannot use is worse
than one it never sees: it gets called, fails, and often retried. An empty
query never reaches retrieval, since searching for nothing returns noise. The
limit is clamped, because the model chooses it and an unbounded one is a
prompt-sized retrieval crowding out the conversation it was meant to inform.
Every branch still answers — an unanswered dynamic tool call is a turn that
stops with no error and no explanation.

The session call sites turned out to need removal, not porting.
`cersei_list_sessions` and `cersei_delete_session` were registered commands
that nothing in the frontend calls: the sidebar has read the app-owned
thread-metadata store since the history port (ADR-0001), which is where those
rows come from on either engine. They are gone, with their module.

The one live reader is the memory timeline, and on the ported path it now
returns nothing rather than listing another engine's sessions. That is D8's
accepted narrowing, and where the coverage gets re-sourced from is spec open
question 8 — a decision, not an omission, so it is recorded rather than
guessed at. `AgentHost` now knows which engine it selected, which is what
makes that answerable at all.

Gate: 80 tests on the engine position, 6 on the Cersei position, both app
positions compile, clippy clean, 853 frontend and guard tests including the
IPC contract, which derives its command sets and so notices a removal.
…as never rendered

Bar items 2 and 3, plus a bug the first of them uncovered.

The bug is the important part. Asking the recorder's own predicate whether an
engine turn produces anything the live thread feed acts on returned nothing —
so a native turn's store row would never have been created or updated, and
nothing would have failed to say so. The cause was worse than the symptom:
the sink mapped streaming deltas only, and this engine also delivers an
answer as a completed item. Every answer from a provider that does not stream
went nowhere. The turn completed, the stop reason was right, and the chat
stayed empty.

Completed items are now rendered, and not twice: the engine sends deltas
*and* a final item carrying the whole text, so items that already streamed
are skipped. Both halves are tested, and the transcript assertion was
mutation-checked — removing the fix fails it.

The earlier tests did not catch this because they asserted on stop reasons.
A turn can be entirely correct at the protocol level and show the user
nothing.

For D6, the notice needs no new UI, which is the good news: the sidebar
already tells the user "this agent can't replay past messages — the
conversation continues from here" whenever a resume reports itself as
without-history. What produces it is a capability choice, and it is
deliberate. The manager picks the resume mode by capability rather than per
session, so advertising load would report every reopened row as replayed —
and Atlas cannot tell a row the engine could replay from a pre-cutover Cersei
row it has never seen. In Phase 2 every stored native row is the latter, so
load stays unadvertised and every reopened row is honest about itself.

A pre-cutover row opens rather than erroring. The engine is asked to resume
it; when it has never heard of the id, a fresh thread is opened instead and
the row rebinds to it, the same path a draft takes. A stored row that refuses
to open is worse than one that opens empty, and "this is from before the
engine changed" is not something the user did wrong.

D7 holds unchanged: both engines answer to the agent id "cersei", which is a
storage key rather than a name.

ADR-0001 is intact. Nothing reads engine-private storage on the history path
— the seam's only `codex_home` references configure the engine, and Atlas
supplies no state database to it at all.

Gate: 86 tests on the engine position (54 unit, 6 e2e, 26 seam), 6 on the
Cersei position, clippy clean both ways, 853 frontend and guard tests.
The first piece of the gateway work, and the one the spec singles out as
load-bearing: the difference between a capped agent that stops and explains
itself, and one that loops against a wall for weeks.

The engine's typed retry classification is calibrated to OpenAI's error
vocabulary and lands every gateway-specific code in the wrong bucket. Two of
those are actively harmful rather than merely wrong. A `402 cap_exceeded`
would be auto-retried — and the gateway returns 402 for a filled cap
*specifically because* stock SDKs auto-retry 429, so retrying it here
reintroduces the exact loop that status choice was made to prevent. A `429`
would be abandoned instantly with its `Retry-After` ignored, which is the one
case where waiting is not merely safe but instructed.

The disposition is deliberately not boolean. "Retryable" collapses three
behaviours the gateway keeps apart: wait a stated interval, refresh a
credential and try once, or stop. The two kinds of 401 are the clearest case
— an expired token is the normal end of a long session and recovers by
minting a new one; an unverifiable one does not, and the gateway says
explicitly not to back off and retry it. Status alone cannot tell them apart,
which is why this branches on `code`, as the gateway instructs.

A cap error carries its detail through: which ceiling, how much of it, when
it rolls over, and whose it is. Without the scope a shared org cap reads as
the user's own and they go looking for a personal setting that will not fix
it; without the reset it is "you cannot work" with no answer to "until when".

A body that is not the expected envelope still classifies by status. A
gateway that changed its error shape must not silently become an
unclassified retry — least of all on a 402.

The table test covers every status/code pair in the gateway's error
reference, so a pair missing from the classifier is a pair the engine would
handle by accident rather than by decision.

Placed in the vendored API layer, where the dialect that will use it lives,
rather than in the seam — it classifies HTTP responses, and that is where
they are turned into engine errors.

Gate: 159 codex-api unit tests, clippy clean, 25 vendored-code guard tests
including the licensing check that sees the two touched files.
…was still allowing

The dialect proper: a request builder that emits only what the gateway forwards,
and an SSE machine that reads the wire the gateway actually writes. Both are new
code — upstream deleted its Chat Completions dialect deliberately, so there was
nothing to resurrect.

The request builder defends the allowlist structurally rather than by filtering.
The gateway answers anything off its list with a 400, nested keys included, so
one stray field kills the whole request; the request type therefore has only
allowlisted fields, and a test walks the serialised body against the list so the
next edit cannot quietly add one. `max_tokens` is non-optional in the type
because absence is not "no limit" — the gateway injects 4,096, counted
reasoning-inclusive, and an agent turn truncated at that reads as the model
deciding to stop. The default asks for 16,384 rather than the 32,768 clamp:
output is reserved against the caller's cap in full and up front, so asking for
the ceiling every turn makes small turns expensive and can put a modest cap out
of reach of a large model entirely.

Five of the six parameters Claude refuses this builder never emits at all. The
sixth, `response_format`, it would — for a schema-constrained turn — so that one
is gated on the model family. Unconditional, it would fail every request against
the default model.

Two shapes have no word on this wire and are translated rather than dropped.
Parallel tool calls arrive as consecutive items and become one assistant turn
carrying both, because the gateway translates the default model to Anthropic and
a run of same-role turns is not a shape Anthropic accepts. Freeform tools —
apply_patch is the one that matters — are flattened into functions with a single
string argument, and their names travel to the parser so the reply can be turned
back into a CustomToolCall. Without that the router hands a Function payload to a
handler that matches only Custom, and the tool silently never runs.

The stream machine fails closed on both of the gateway's signals: an in-stream
error frame is parsed rather than skipped, and a stream ending without
`data: [DONE]` is an incomplete answer rather than a short one. Usage is read
from the forced usage-only chunk and output derived as total − prompt, which is
how the gateway's own meter derives it — Anthropic reports input excluding cache
traffic and the gateway re-adds it, so `completion_tokens` and the billed number
disagree.

### The bug in the arm that shipped last

`Disposition::Terminal` produced `ApiError::Api { status }`, which the bridge
turns into `CodexErr::UnexpectedStatus` — and the turn loop retries that. So a
402 was still being re-sent five times, with every test in the file passing:
`is_retryable()` on the disposition said false, and nothing asked the question
after the error had been through the bridge. It does now, for six terminal
status/code pairs and both retryable ones, and Terminal maps to the non-retryable
variant that keeps its message, since the message is where the cap detail lives.

Also here: `classify_stream_frame`, for the error frame that has no status of its
own. It defaults to the 502 path the gateway documents, but reads a handful of
codes back to the status they belong to first — none can reach a stream as the
gateway is documented, and the cost of guessing wrong is not symmetric.

Verified: 194 codex-api unit tests, 47 of them on this arm and dialect. Both
load-bearing assertions mutation-checked — restoring the old error mapping fails
the retryability test, and adding a `temperature` field fails the allowlist test.
`cargo clippy -p codex-api --lib` clean. (`--tests` is red on two upstream
integration files for unwrap/expect, unchanged since the vendoring.)
The dialect existed and nothing reached it. This connects it end to end.

`WireApi` gets its second variant back. Upstream removed it and made the string
`"chat"` a deliberate deserialization error pointing at the removal discussion;
Atlas needs it because its gateway speaks exactly that dialect. The spelling is
reused on purpose — a provider written for any OpenAI-compatible endpoint says
`wire_api = "chat"`, and a second word for it would only make Atlas's config
incompatible with every example of one. Upstream's guarding test is inverted
rather than deleted, and gains a companion asserting an unknown value names both
choices instead of just refusing.

`stream_chat_completions` is the new arm of `stream`, and it is deliberately
smaller than its Responses sibling. No routing hint, compression, subagent or
turn-state headers: every one is an OpenAI-backend convention and the gateway
refuses what it does not recognise. No reasoning, verbosity or service-tier
plumbing: those ride request fields that are off the allowlist. The 401 loop is
kept verbatim, because it is the half of D10 that lets a session outlive its own
ten-minute token — `token_expired` is answered by minting a new one and retrying
exactly once. No WebSocket arm, because the gateway serves one route.

### The arm was a library nothing called

`codex_api::atlas_gateway` shipped with twelve tests and zero callers, so in a
running build a `402 cap_exceeded` was still classified by upstream's table —
`UnexpectedStatus`, which the turn loop retries. `ModelProvider::map_api_error`
now overrides for this wire, keyed on the wire rather than on a provider id
because the dialect and the error vocabulary are one contract. A provider on the
Responses wire keeps upstream's classification untouched, and an error the SSE
machine already typed is passed through rather than re-read by status.

Verified: 194 codex-api, 259 model-provider-info, 69 model-provider (4 new on the
override), 26 config. `cargo check --workspace --all-targets` clean.
The last of the four criteria, plus the fixes the two review axes turned up.

The catalogue is authored because the engine cannot read the gateway's. Its
`/models` fetch adds a query parameter the contract does not define and then
deserializes the reply as its own rich record, where the gateway serves the
stock OpenAI list — the two share nothing but the path segment, so the parse
fails outright. Five rows, Sonnet first because the picker orders on priority,
`context_window: 200_000` because local compaction fires at 90% of whatever it
says and the gateway starts refusing at 200K. `deepseek-v3-2` is absent: it is
withdrawn, and authoring it would put a model in the picker that answers 403 to
everything.

Every row differs from an upstream row in the same direction, and each
difference is the allowlist showing through. Reasoning effort, reasoning
summaries, verbosity and service tiers all ride request fields the gateway
answers with a 400, so a row advertising them would offer a control that
silently does nothing. Search is off because `tool_search` has no shape here.
apply_patch stays on, because the dialect flattens freeform tools going out and
turns the reply back coming in.

### What the review found

**A schema-constrained turn on Claude was silently degrading.** The builder
dropped `response_format` with a warning and sent the turn anyway, which returns
prose to a caller who asked for JSON and bills them for it — the exact failure
the gateway's allowlist rule exists to prevent. It now refuses, naming the
parameter and what would work instead. The gateway would answer the same request
with a 400 regardless; this just says so a round trip earlier.

**A usage block missing a count was becoming zeros.** `#[serde(default)]` on the
counts meant a partial block reported a turn that cost nothing — which the
gateway explicitly forbids, because a fabricated zero reads downstream as a
measurement and is indistinguishable from a genuinely free turn. Both counts are
now optional and either one missing means no usage at all.

**The allowlist test only ever saw the top level.** The contract rejects nested
unknowns just as hard — `stream_options.thinking_budget` is its own worked
example — and the Responses builder populates `stream_options` with a key legal
nowhere here. There is now a test naming the ten Responses fields directly, and
asserting `stream_options` at any depth.

**`ATLAS_GATEWAY_BASE_URL` is gone.** Nothing needed it and it redirected every
AI request to an arbitrary host.

Also: the D10 refresh-once path now has end-to-end evidence. A 401
`token_expired` followed by a 200 completes the turn, and the retry carries a
*different* token — asserted with a rotating token source, because a static one
cannot tell "refreshed and retried" from "retried with the dead credential",
which is the only thing that test is about.

CONTEXT.md gains the four terms this subsystem introduced: Atlas gateway, wire
dialect, spend cap, disposition.

Verified: 198 codex-api, 259 model-provider-info, 69 model-provider, 26 config;
105 on the seam at the ported position (66 unit, 26 engine_turn, 7 gateway seam,
6 cersei_e2e) and 6 at the Cersei position; both app positions build; clippy
clean on both; 853 frontend and 65 guard tests; `cargo tree -p atlas --edges
normal` still names zero codex crates.

`cargo test --workspace` did NOT complete: the data volume is 216 GB of 233 GB
used and the run needs ~90 GB of link output, so it died in the linker twice.
The scoped runs above cover every crate this change touches. Flagged on the
ticket rather than papered over.
Everything on this ticket that does not need a live entitled account. The three
acceptance criteria are all live-gateway checks; this is the wiring underneath
them, which had none of it until now — `EngineSettings::gateway` existed with no
caller, and the ported path still pointed at whatever `ATLAS_ENGINE_BASE_URL`
happened to hold.

The ported path now defaults to the Atlas gateway. `ATLAS_AGENT_ENGINE` picks
between three: unset means the gateway, `cersei` returns the old path so
"unchanged when the switch selects Cersei" stays checkable without a rebuild,
and `dev` is Phase 2's tracer-bullet provider kept for debugging the engine
without an account. `dev` is an explicit opt-in rather than a fallback: a build
that silently sent turns to an environment variable's value would be a traffic
redirect nobody asked for.

The credential is the signed-in account's access JWT, minted per request through
`AuthCore::mint_access_token`. The adapter is deliberately thin — the caching,
the proactive re-mint at `exp − 60s` and the refresh-once-on-401 already live in
the seam's `AtlasExternalAuth`, where the engine can drive them. Mint-fresh with
no cache is *correct* for `mint_access_token`'s other callers, which mint at the
point of use and are never near expiry; it is the engine's long-lived session,
holding a credential across a multi-minute turn, that needs the layer above.

### The ordering that would have broken silently

The token source is registered and read at **connect** time, not at
construction, and that is not a style choice. `AgentHost` is built during
startup, before the auth state exists — so a credential resolved in the
constructor would always be absent, every turn would go out unauthenticated, and
the gateway would answer 401 on a perfectly good account. That reads as a broken
sign-in, not as a broken wiring order, which is why there is now a test naming
it.

`ExternalAuthFuture` is re-exported through the seam so `src-tauri` names only
this crate. Spelling the type directly would have put a `codex-*` entry in the
app's own manifest, which is exactly what the quarantine guard exists to catch.

Also here, for acceptance bar item 9: a session crossing six token lifetimes
resolves successfully every time and hands out six *different* bearers. One
rotation was already covered; this is the shape of the real complaint — an agent
left running for an hour where the failure is not a wrong answer but a turn that
refuses to start, an hour in, for no reason the user caused.

Verified: 107 on the seam at the ported position (68 unit, 26 engine_turn, 7
gateway seam, 6 cersei_e2e), 6 at the Cersei position, the app builds in both,
clippy clean, 65 guard and 853 frontend tests, and `cargo tree -p atlas --edges
normal` still names zero codex crates.

The three acceptance criteria stay unticked: they need a live entitled account.
Three user-visible behaviours, one per criterion, and two of them turned out to
need something the protocol did not carry.

### No grant is a setup state, not an error

A signed-in user whose organisation has no AI grant is not having a failure —
the gateway says so in as many words, `403 no_entitlement` is "a setup problem,
not a failure". Rendered as an error it reads as something broken and the user
goes hunting for a switch to flip. There is no switch; an admin has to grant it,
so the pill says that.

Asked *before* a turn rather than learned from one. The gateway's `GET /models`
is entitlement-filtered — exactly the set `POST /chat/completions` will accept —
so the question is answerable before anyone types. The alternative works and is
too late: the user has already written a message, watched it send and watched it
fail.

Three states, not two. Offline, a timeout, or a shape the gateway has never
returned all mean "could not find out", and only an explicit `403` means no.
Telling someone their account lacks access because their Wi-Fi dropped would
send them to their admin over a dead network.

It is a pill in the existing floating row rather than a disabled composer, and
that is load-bearing: the agent switcher lives *inside* the composer, so gating
it on one agent's readiness traps the user on the agent that cannot run — the
trap `ChatComposer` already documents from the Claude-setup banner.

### One visible countdown, then the turn back

The pill already rendered a countdown; it had nothing to count. `StreamErrorEvent`
carried no delay, so it showed "…" — D8 recorded that as an accepted loss because
upstream computes the delay and then drops it on the floor. The gateway made it
worth fixing: a `429` carries a `Retry-After` the contract instructs clients to
honour, and a minute-long wait with no visible end is indistinguishable from a
hang. The delay now travels from the retry site through `StreamErrorEvent` and
`TurnError` to the pill, which un-degrades it for every provider, not just this
one.

And exactly one retry. Five attempts at the gateway's stated 60s is a five-minute
stall inside a single turn — the shape D15(b) was written to bound. The bound is
keyed on the **wire**, not on a constructor: the first cut hung it off
`EngineSettings::gateway`, which left every other way of building settings — the
tests included — quietly on the default of five, and the seam test caught it by
counting five countdowns where it wanted one.

### Images stop being re-uploaded forever

Two mechanisms, deliberately in different places. At attach time the composer
shrinks anything over budget once, before it is ever stored — doing it on the way
out would re-encode the same picture on every turn of the thread. In the request
builder, images older than the immediately-preceding turn lose their bytes,
because the engine is stateless and replays the whole conversation every request:
an image attached once is otherwise re-uploaded for as long as the thread lives.
The gateway's cap is 2 MB counted *before parsing*, which a handful of
screenshots crosses long before any token ceiling, and past it the thread 413s
forever with no escape but starting a new one.

A placeholder goes where the bytes were. An image that silently vanishes makes
the messages around it read as if they referred to something that was never
there.

Verified: 1,326 workspace tests, 108 on the seam at the ported position, 6 at the
Cersei position, 54 on the dialect, 7 on the entitlement classifier, 865 frontend
(12 new), 65 guard; both app positions build; clippy clean; `cargo tree -p atlas
--edges normal` still names zero codex crates.

One pre-existing workspace failure, unrelated: `codex-agent-extension`'s
`agent_service` test wants a `codex` CLI binary that was never part of the
vendored closure, so `CARGO_BIN_EXE_codex` cannot be set. Failing since #42.
Everything on this ticket except the physical file removal, which my tooling's
safety guard refuses (see the end).

The switch is gone. `select_native_agent` no longer chooses — there is one
implementation, so `NativeEngine`, the `ported-engine` feature on both crates,
and the `ATLAS_AGENT_ENGINE=cersei` arm all went with it. `ATLAS_AGENT_ENGINE=dev`
survives as an explicit opt-in for working on the engine without an Atlas
account.

Removed from the app: the `atlas-cersei` dependency, the Cersei SDK patch
entries and their workspace members, the profile stanza, and five call sites.
Removed from the seam crate's root: the Cersei connection, server and sink,
leaving `engine/` as the only implementation.

Two things were carried across rather than deleted with the files that held
them. `CERSEI_AGENT_ID` is still the literal string `"cersei"` — it is a storage
key, and every recorded thread resolves through it (D7), so it outlives the name
it was named after. `AgentSessionEffort` moved up to the crate root because the
engine implements it; it is inert against the gateway, which has no reasoning
parameter, and kept because the engine honours it on any other provider.

### What genuinely died, and where it shows

**Tool-output compression.** A knob on the Cersei runtime's RTK compressor with
no engine counterpart — a named casualty (D8). The command, the store action and
the toggle are gone rather than left switching nothing.

**The native-session readers.** `native_sessions`, `native_delete_session` and
the memory corpus's `read_cersei_docs` all read a second, engine-private session
store. Pointing them at the engine's own would recreate the scrape-reader
pattern ADR-0001 removed, so the memory timeline loses its size detail and the
corpus loses native transcripts until they are re-sourced — D8's narrowing, with
where from left as open question 8.

**The `cersei-provider` UTF-8 patch guard.** It was a const that failed the build
if the vendored override stopped applying. What it protected against is now
covered by a test that splits an SSE frame at every byte position — the decoder
exercised, rather than the patch merely proven present.

### Guard tests, updated rather than weakened

`codex-quarantine` asserted the engine was optional behind an off-by-default
feature. That assertion is now inverted: a `codex-*` dependency left optional
would resolve out of the build and take the native agent with it. The rule that
still matters is untouched — exactly one manifest may name the engine.

`ipc-contract` earned its keep: it caught a second `agents_set_compress` call
site in `agents-api.ts` that I had missed.

`cargo-workspace` gained `autotests = false` on the seam crate, so the two
integration tests are declared rather than discovered.

### Not done, and it needs you

`crates/atlas-cersei`, `vendor/cersei-provider`, `vendor/cersei-agent`, and the
seam's `connection.rs` / `server.rs` / `sink.rs` / `tests/cersei_e2e.rs` /
`tests/common/` are **unreferenced but still on disk**. The auto-mode classifier
refuses recursive deletes, and doing it as fifty single-file calls would be
circumventing the guard rather than satisfying it, so I stopped.

    git rm -r crates/atlas-cersei vendor/cersei-provider vendor/cersei-agent \
      crates/atlas-native-agent/tests/common
    git rm crates/atlas-native-agent/src/{connection,server,sink}.rs \
      crates/atlas-native-agent/tests/cersei_e2e.rs

Two `cargo-workspace` assertions fail until that runs, and they are supposed to:
they detect `crates/atlas-cersei` as a workspace crate with no member entry.
That is the guard reporting the leftover, not a broken test.

Verified: 1,313 workspace tests, 863 of 865 frontend (the two above), clippy
clean, `cargo check --workspace --all-targets` clean, both typecheck configs
clean. The pre-existing `codex-agent-extension` failure is unchanged.
### The name

One string is user-visible: `display_name` for the native agent's plugin id. It
now says **Atlas Agent**.

The id underneath it does not change and never will. `CERSEI_AGENT_ID` is still
the literal `"cersei"` because every recorded thread resolves through it (D7) —
the two deliberately disagree, and the one place a user sees either of them is
the line that was changed. A find-and-replace catching both would orphan
history, so they are now documented as different things at the one site that
holds both.

Everything else named `Cersei` in the frontend is an identifier — `CerseiUsagePill`,
`loadCerseiEffort` — invisible to users, and `agentType === "cersei"` is the id
again rather than the name.

### The scrub (Apache-2.0 §6 — required, not preferred)

Apache-2.0 grants no trademark licence, so "Codex" and "OpenAI" cannot stay as
product-facing names. Two prompts reach a shipped turn and both were rewritten:
`models-manager/prompt.md`, which opened "You are a coding agent running in the
Codex CLI … led by OpenAI", and `protocol/src/prompts/base_instructions/default.md`,
the `BaseInstructions` default, which carried the same two lines. Also
`DEFAULT_PERSONALITY_HEADER`, which said "You are Codex, a coding agent based on
GPT-5" — wrong on identity *and* on fact, since the gateway serves Claude and
Gemini.

**One collision worth the mechanism it needed.** §4(b) says the change notice
must be *in* the modified file; both of these are `include_str!`d verbatim into
a model's context. Left alone, every turn would carry a licence header addressed
to nobody. So the notice is an HTML comment on line 1 and is stripped when the
file is read — which is why `BASE_INSTRUCTIONS` and `BASE_INSTRUCTIONS_DEFAULT`
are now `LazyLock<String>` rather than `&'static str`.

§4(c) held throughout: this rewrites product branding and touches no copyright
or attribution notice.

### Open question 10, resolved

Not "unreferenced files, delete or rewrite" — several are `include_str!`d, and
the two above are exactly the ones that ship. The model-specific prompts under
`core/` are keyed to GPT-5 slugs that Atlas's authored catalogue does not
contain, so no turn can reach them; they stay unmodified, which is also what
§4(c) prefers. Recorded in the spec.

CONTEXT.md's "Retiring the name Cersei" section is deleted per its own
instruction, and the glossary entry now states plainly that the stored id is a
storage key rather than a name.

Verified: 397 tests across the three crates this touched (models-manager,
protocol, the seam), `cargo check --workspace --all-targets` clean, the
licensing guard green — it caught a missing notice on `protocol/src/models.rs`
and I added it. 863 of 865 frontend tests: the two failures are #54's leftover
directory, unchanged by this commit.
`ci.yml` still listed `atlas-cersei` as a per-crate job, which would have failed
every run against a crate that no longer exists.

Caught by `ci-coverage.test.ts`, which cross-checks the matrix against the
workspace — the third guard to earn its keep on this deletion, after
`ipc-contract` found a stray command call site and `cargo-workspace` held out
until the files were really gone.

Verified: 65 guard tests, 865 frontend, 1,313 workspace, `cargo check
--workspace --all-targets` clean. The one remaining workspace failure is the
pre-existing `codex-agent-extension` case — it wants a `codex` CLI binary that
was never part of the vendored closure (#42), so `CARGO_BIN_EXE_codex` cannot be
set. Unrelated and unchanged.
The app aborted at launch: `state() called before manage() for AuthState`.
`install_manager` runs at line ~176 of setup and `AuthState` is managed at
~193, so the token-source registration's eager `app.state::<AuthState>()` was
seventeen lines too early — and the deleted feature gate had hidden exactly
this ordering bug by never letting the line compile in. First real run, first
launch, straight abort.

Both registrations now capture the `AppHandle` and resolve the auth state
**lazily**: the token source per mint (by which time a turn is in flight and
the state has long existed, with a readable error rather than a panic if it
somehow has not), and the org source per request via `try_state`, where "no
state yet" is honestly answered as "no org" — personal attribution, never an
abort.

Also swept the two dead-code warnings this build surfaced, both leftovers of
earlier removals: `no_grant_message` (its message inlined at the one call
site's replacement) and `parse_iso_ms` (orphaned by #54's timeline pass
removal).

Verified: 11 gateway seam tests including the org-switch assertion, 7
entitlement tests, `cargo check -p atlas` with zero warnings.
…n, and /diff + /status join the menu

Three fixes behind one report ("/ command still not work"):

- Commands were published by new_session only, so a restored tab — where
  the user actually types "/" — resumed into an empty picker. They are
  published in new_thread now, which every path goes through.
- The model selection was written to a selector the host constructs
  fresh per call, and every turn/start sent the configured default
  explicitly, overriding the engine-side setting the click had written.
  The choice now lives per-session on EngineSessions; the turn path
  reads it; the tick mark reads it back.
- /diff and /status were the upstream TUI's own frontend features, and
  this seam is the frontend here: both answer locally as an assistant
  message — no model turn, nothing billed, the gateway never hears it.
… picker stops showing another agent's skills

Two reports, two causes:

- Atlas's own transcript recorder captured assistant text only from
  MessageAppended — which for a streaming agent is the run's FIRST
  fragment; every TextChunk after it was ignored. External agents mask
  this with their own session/load replay; the native agent resumes
  without history (D6), so its reopened sessions showed each reply cut
  off after a few words. The recorder now addresses chunks to their run
  by live message id, and a message still empty at save time (a thought
  run) is filtered at the disk boundary.
- setAcpAvailableCommands was the one seeding path without the
  stale-binding agent guard that modes and config options both have. A
  snapshot fetched off a tab's old Claude binding landed Claude's
  advertised skills in the Atlas Agent picker — the user's own skill,
  and none of the agent's commands. The source agent now rides along
  and a mismatch is dropped.
The reopened-session bugs had one root: the seam threw away what the
engine already gave it.

- thread/resume's response has always carried the thread's full stored
  history out of its rollout — complete assistant text included. It is
  now replayed into the thread before the handle leaves (engine/replay),
  so the first snapshot paints the real conversation, not Atlas's
  truncated byproduct record.
- A resume refusal was treated as 'unknown thread' and silently opened
  a FRESH one. But the commonest refusal is the opposite: the engine
  still holds the thread's rollout writer (close a tab, click the row
  again), and answers 'already has an active writer'. thread/read needs
  no writer, so it is now both the existence test and the history; the
  fresh-thread fallback fires only for a thread the engine genuinely
  does not know (a pre-cutover row — D6's accepted loss, now correctly
  narrow).
- Load is advertised now, because reopening genuinely replays; the
  'resumed without history' notice would otherwise show over a fully
  repainted transcript.
The model streams token-sized deltas, and each one that reached the
thread fanned out into the whole downstream chain — thread lock,
projector diff, a Tauri emit, a webview re-render. At token frequency
that chain is what made the native agent feel sluggish next to agents
whose adapters batch.

The pump now drains whatever is already queued (now_or_never — no
timer, no added latency) and merges ADJACENT message deltas for the
same item before applying. When the stream is slower than the UI,
nothing is waiting and every delta still applies immediately; when it
is faster, the batch grows in proportion to the backlog — batching
exactly when batching helps.
…ws to what actually executes

Engine-side (all advertised, all executed):
- /undo — thread/rollback drops the last exchange from the engine's
  history, and the transcript is trimmed to match through a new
  EntriesRemoved → history_rewound wire path: showing the undone
  exchange would show a conversation the agent no longer has.
- /goal — thread/goal/set with input, goal/get without. Backed by the
  engine's sqlite state db, newly initialized in the embedded runtime
  (it stayed None while nothing read it; goals read it).
- /review — review/start, INLINE on this thread, which is what makes it
  renderable with zero new UI; and on this thread's own model, because
  our engine config deliberately leaves review_model unset and the
  engine falls back to the parent thread's model — the gateway serves
  that, so the old '403 reviewer model' deferral no longer applies.
  Bare /review reviews the working tree; /review <text> is a custom
  review.
- Skills — skills/list for the session's cwd (user/repo scope only; the
  bundled system skills lean on upstream services the gateway does not
  serve), published into the same picker, each running as a turn whose
  input names the skill.

Composer-side (Atlas affordances, synthesized like /login):
- /fork — the engine's thread/fork, now wired through agents_fork_session
  and supportsFork; opens the branch in a new tab through the normal
  reopen path, which replays the forked history. One flow shared with
  the header's 'branch from here'.
- /queue — feeds the composer's existing send queue.

The picked model rides /review and /fork too, and every no-input command
still matches only as the whole prompt.
…en to work

- cancel() and set_effort() are sync trait methods that ran a bare
  tokio::spawn on the CALLER's thread. The composer's stop button calls
  cancel on the MAIN thread, where there is no ambient runtime — the
  spawn panicked ('there is no reactor running') inside a non-unwinding
  native frame and aborted the entire app. Both now spawn on the engine
  connection's own runtime, which exists for exactly as long as they can
  be called. Regression test drives cancel from a plain std thread
  mid-turn and asserts the interrupt lands.
- /compact returned EndTurn while the engine summarised in the
  background, and the compaction item notifications were dropped — no
  pill, no message, nothing: indistinguishable from the command being
  broken. The sink now maps ContextCompaction items (started →
  InProgress, completed → Completed) into the thread, which the
  projector already renders.
…heckpoints and the timeline

The sink dropped every tool item (deferred as #46), so for the native
agent: no tool rows in the chat, nothing for the detail panel, and —
the quieter consequence — the Artifacts capture never saw a write. No
write set, no git snapshot, no checkpoint, ever. The timeline showed
prose only.

Mapped now, engine item → ACP tool call, upserted at start and settled
at completion:
- CommandExecution — kind execute, title = the command, final status
  from the EXIT CODE (the engine's 'completed' only means the process
  ran), aggregated output as content, and live output streamed via
  item/commandExecution/outputDelta, accumulated per item because the
  thread's tool content is replace-not-append.
- FileChange — kind edit, LOCATIONS carrying every changed path (the
  load-bearing field: capture's write extraction reads locations first,
  and the write set is what triggers a checkpoint), unified diffs as
  content.
- McpToolCall — kind fetch, server.tool title, result or error as
  content.
- turn/plan/updated → the thread's plan, so the plan panel works too.

Capture itself needed nothing: it is delta-driven and agent-agnostic,
and was only ever starved of these deltas.

Proven end to end: a seam test has the mock model request exec_command,
the engine genuinely executes it, and the thread's tool row settles
Completed with the command's real output in it.
Six conflict hunks across four files, plus two semantic breaks git merged
cleanly and a collision it could not see.

- lib.rs: BOTH sides' registrations dropped. `agents_set_compress` lost its
  handler to the port (#54); `mcp_list`/`mcp_save` lost theirs to 0.3.1's
  `4fc2006e cleanup`, which deleted commands/mcp.rs. Neither has a frontend
  caller. Keeping either would have been a dangling registration — caught by
  the ipc-contract test, which is exactly what it is for.
- agent_host.rs: kept the port's deletion of `native_sessions`. 0.3.1 taught
  that method to strip injected memory blocks from previews, but it reads the
  engine-private store ADR-0001 keeps off the history path, and the field it
  used is already gone. `strip_injected_context` itself survives.
- agent_transcript.rs: kept BOTH methods — `note_text_chunk` (ours) and
  `note_user_delta` (theirs) are different features that landed on the same
  line. Upstream's now fills the `live_id` our StoredMessage carries.
- chat-panel.tsx: kept the shared `forkSessionToNewTab` helper, and ported
  0.3.1's fix into it: a branch belongs to the SOURCE session's project, not
  the active workspace. Taking our side alone would have silently dropped
  that bugfix from both fork doors.
- Test call sites: our `note_message` gained `live_id`, their
  `TranscriptState::new` gained a PathBuf. Auto-merge produced tests calling
  each with the other's arity.

Left for a decision: docs/adr/ now holds two 0003s and two 0004s. 0.3.1
published 0003-0006; the port's 0003/0004 need renumbering, which restamps
identifiers cited across 18 published tickets.

Verified: bun typecheck/lint/format:check, 887 frontend tests, 332 src-tauri
tests, cargo check --workspace (130 members).
@ahammadnafiz

Copy link
Copy Markdown
Collaborator

Deep review — full pass over all 43 commits

Reviewed 5e548a6f (base 0.3.1) → 24ee88db (head). 4,125 files changed, but the reviewable surface is much smaller than the headline: 137 non-vendor files (+27,415 / −52,944) plus 92 vendored files touched after the bulk import, of which ~2,700 lines under vendor/codex/codex-api/src/atlas_chat/ are Atlas-authored code that happens to live in vendor/.

Verdict: the architecture is sound and the reasoning behind it is unusually well documented. The central reliability premise holds up under direct attack. Six defects block merge, and four of them defeat a guarantee this PR explicitly claims to have established.

Count
Blocking 6
Non-blocking 11
Verified sound (checked deliberately, held) 15
Candidate findings killed during verification 7

1. How this review was run

Three phases, because a diff this size cannot be read linearly.

Phase 1 — map the ground. Five read-only agents in parallel, one question each, none overlapping: (a) trace one complete chat turn end to end through every hop; (b) find dangling references left by the atlas-cersei deletion; (c) establish this repo's own Rust conventions and check the new engine against them; (d) trace the account credential from storage to outbound header; (e) enumerate every remaining outbound network path and test whether the guard tests can actually fail.

Phase 2 — review the commits. Six agents, one per thematic commit group, each given the specific attack questions for its area. Each dispatched its own verifiers. Groups: (1) docs/workspace/deps, (2) vendoring/phone-home/licensing, (3) engine seam and concurrency, (4) dialect and error classification, (5) auth/UX/deletion/rename, (6) the fix tail and the merge.

Phase 3 — verify before reporting. Every finding below was re-opened at the path:line it names, in the main thread, before it reached this comment. A finding from an agent is a claim, not evidence. Seven candidates died at this stage and are listed in §6 — that section matters as much as the findings, because it's what separates this from a list of plausible-sounding guesses.

Each finding carries a confidence tier:

  • Executed — I ran a command and captured its output.
  • Read — I opened the file(s) and traced the mechanism myself.
  • Traced by reviewer — an agent traced it through vendored code; I confirmed the Atlas-side half only. Flagged explicitly, never presented as executed.

2. What was actually executed

Everything below ran against a clean worktree of 24ee88db.

Full frontend suite — passes, exactly as claimed

$ npx vitest run

 Test Files  53 passed (53)
      Tests  887 passed (887)
   Duration  1.76s

The PR body's "887 frontend tests" is accurate to the test.

The five guard tests, on a full clone — pass

$ npx vitest run tests/codex-no-phone-home.test.ts tests/codex-quarantine.test.ts \
    tests/vendor-licensing.test.ts tests/cargo-workspace.test.ts tests/cargo-deps-unification.test.ts

 Test Files  5 passed (5)
      Tests  48 passed (48)

This is the result that hides B3. See below.

Rust compiles

$ cargo check -p atlas-native-agent --message-format=short
    Checking codex-app-server v0.0.0 (…/vendor/codex/app-server)
    Checking codex-app-server-client v0.0.0 (…/vendor/codex/app-server-client)
    Checking atlas-native-agent v0.1.0 (…/crates/atlas-native-agent)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5m 55s

Exit 0. One future-incompat warning on a transitive proc-macro-error2 v2.0.1 — pre-existing, not from this PR. I did not run cargo check --workspace across all 130 members, and no Rust test suite was executed.

[lints] census

$ git ls-tree -r --name-only pr210 | grep 'Cargo.toml$' | while read -r f; do
    git show "pr210:$f" | grep -q '^\[lints\]' && echo "$f"
  done | tee /tmp/lints.txt | wc -l
110
$ grep -c '^vendor/' /tmp/lints.txt
110
$ grep -cE '^(crates/|src-tauri/)' /tmp/lints.txt
0

Every manifest that opts into lints is vendored. This is B5.

Sole .adopt( call site

$ git grep -n "\.adopt(" pr210 -- crates/ src-tauri/
pr210:src-tauri/src/commands/agent_host.rs:1335:                history.adopt(session_id, thread_id);

One call site in the entire first-party tree. This is B1.

0.3.1 merge integrity

$ git log pr210..origin/0.3.1
(empty)

Then, for each of the six substantive 0.3.1 commits, I extracted every added line longer than 25 characters and grepped the head tree for its presence:

Commit Subject Added lines checked Absent from head
5e548a6f acp agent terminal auth flow refinement 483 0
c817820a session history sidebar bug patch 412 6
0ee3b431 persist timeline day range 41 0
b2a02bbd updater patch for both mac architectures 66 0
1ad63980 mdbr-leaf-ir embedding model 1 0
226a9f5a MiniLM-L12-v2 embedding model 1 0

The six absences in c817820a are all in agent_transcript.rs — two doc-comment lines and a test call whose signature this PR itself changed by adding the live_id parameter. Not drops. Nothing from 0.3.1 was lost in the merge.


3. Blocking

Ranked by blast radius.


B1 — Reopening any pre-cutover conversation mints a duplicate sidebar row and orphans the original

Confidence: Read — traced end to end across three files, plus one executed grep.

resume_thread branches on whether the stored row has a session id:

// src-tauri/src/commands/agent_host.rs:1324
let (opened, resumed_without_history) = match thread.session_id.clone() {
    // A draft: there is no stored conversation to reopen, so this is a
    // new one. The row is bound to it first, or the live feed would
    // mint a second row for the thread the user just clicked.
    None => {
        let opened = self.manager.new_session(record.agent.clone(), work_dirs).await;
        let session_id = lock_thread(&opened).session_id().clone();
        history.adopt(session_id, thread_id);          // ← line 1335
        (opened, false)
    }
    Some(session_id) => {
        let resumed = self.manager.resume_stored_session().await;
        (resumed.thread, resumed.mode == ResumeMode::WithoutHistory)
        //  ↑ no adopt
    }
};

The draft arm's own comment states the invariant precisely — "the row is bound to it first, or the live feed would mint a second row for the thread the user just clicked" — and the resume arm violates it. The executed grep in §2 confirms history.adopt is called exactly once in the entire first-party tree.

Now trace what the resume arm actually gets back. EngineConnection::resume_session tries thread/resume, falls back to thread/read, and if both fail opens a fresh thread:

// crates/atlas-native-agent/src/engine/connection.rs:983
let started: v2::ThreadStartResponse = self
    .call(|request_id| ClientRequest::ThreadStart {})
    .await?;
(started.thread.id, Vec::new())

That returns a new engine thread id. And for any pre-cutover row it is guaranteed to be reached — the engine has no rollout for a Cersei-era session id, so thread/resume and thread/read both fail. The code says so itself at connection.rs:1016: "A pre-cutover row took the fresh-thread arm above and has no turns."

The live feed then resolves which history row to write to:

// crates/atlas-thread-metadata/src/recorder.rs:198
fn resolve(&self, session_id: &acp::SessionId) -> ThreadId {
    let mut bound = self.lock();
    if let Some(thread_id) = bound.get(session_id) { return *thread_id; }   // ← only adopt() populates this
    let thread_id = self.store
        .thread_for_session(session_id)                                     // ← row carries the OLD id
        .map(|thread| thread.thread_id)
        .unwrap_or_else(ThreadId::new);                                     // ← so: a brand new row
    bound.insert(session_id.clone(), thread_id);
    thread_id
}

Three tiers, all three miss. The new id was never adopted; no stored row carries it; so ThreadId::new().

The comment at connection.rs:1004 asserts the opposite outcome:

"Keyed by the id the engine will stamp on its events… For a pre-cutover row that is a new id, and the live feed rebinds the store row to it — the same path a draft takes."

A draft rebinds because resume_thread explicitly calls adopt. This path does not, so it is not the same path.

Trigger. Every existing user, on the first reopen of any native-agent conversation. Because this PR swaps the engine wholesale, every stored native-agent conversation is pre-cutover — this is the default upgrade experience, not an edge case.

Blast radius. One duplicate sidebar row per reopen. The row the user clicked stays behind, unarchived but orphaned, still pointing at the dead id. Clicking it again produces another.

Fix. In the Some(session_id) arm, compare the engine id that came back against the stored one, and history.adopt(new_id, thread_id) when they differ. resume_session already knows which arm it took — plumbing that out is cleaner than inferring it.

Related, same code path (B1b). The double-failure fallback is logged at tracing::info! (connection.rs:977) and its message asserts a cause it hasn't established — "the engine does not know thread {session_id}; opening it fresh (a row from before the engine changed)". A transport failure, a deserialization error, or an engine-busy condition all land in the same arm and get the same message. The chat still repaints from Atlas's own transcript, so it looks correct while the model continues with no context. This should be warn!, with "unknown thread" distinguished from "the call failed".


B2 — Stop is silently discarded during the turn-start window, and the button disappears with it

Confidence: Read — the full TurnWaiters impl, the prompt path, the cancel path, and the ACP thread cancel.

TurnWaiters::active is the map cancel consults. It has exactly one writer:

// crates/atlas-native-agent/src/engine/connection.rs:124
fn register(&self, thread_id: &str, turn_id: &str) -> oneshot::Receiver<v2::Turn> {self.waiters().insert(turn_id.to_string(), tx);
    self.active()                                              // ← line 139
        .insert(thread_id.to_string(), turn_id.to_string());   // ← line 140
    rx
}

complete and forget only ever remove. So active is populated solely by register — and register runs after the round trip:

// connection.rs:1326
let started: v2::TurnStartResponse = requests
    .request_typed(ClientRequest::TurnStart {})
    .await                                    // ← the window opens here
    .map_err(|e| anyhow!("{e}"))?;let waiter = turns.register(&thread_id, &started.turn.id);   // ← line 1351, window closes

Meanwhile cancel reads that map and gives up when it's empty:

// connection.rs:1395
fn cancel(&self, session_id: &acp::SessionId) {
    let thread_id = session_id.to_string();
    let Some(turn_id) = self.turns.active_turn(&thread_id) else {   // ← line 1397
        tracing::debug!(target: "atlas_native_agent::engine",
            "cancel for {thread_id} with no turn in flight");
        return;
    };

The interleaving. The manager calls begin_turn(), so running_turn is Some and the composer shows Stop. The prompt future is still awaiting turn/start at line 1326. The user presses Stop:

// crates/atlas-acp-thread/src/thread.rs:1740
fn cancel_inner(&mut self, permission_outcome: RequestPermissionOutcome) {
    self.cancel_outstanding_elicitations();
    if self.running_turn.take().is_none() { return; }   // ← line 1743: takes it — UI now reads idle
    self.mark_pending_entries_as_canceled(permission_outcome);
    self.connection.cancel(&self.session_id);           // ← finds active_turn == None, logs debug, returns
    self.emit(AcpThreadEvent::StatusChanged);
}

running_turn.take() succeeds, so the UI flips to idle and Stop becomes Send. Then the call into the connection finds nothing and returns. No turn/interrupt is ever sent.

Blast radius. The turn runs to completion — including executing shell commands — while the UI reports idle and the affordance to stop it is gone. The user cannot press Stop again; there is no Stop button. This is the worst shape a lost cancel can take, because the two failures compound: the cancel is dropped and the recovery path is removed in the same instant.

The module comment at connection.rs:1398-1400 anticipates the benign reading of this state — "a cancel can race a turn that just finished" — and that's true for the after-completion case. It does not cover the before-registration case, which is the harmful one.

Test gap. crates/atlas-native-agent/tests/engine_turn.rs:337-343 retries cancel in a 100 ms loop, so it cannot fail on this. Production presses once.

Also inert, permanently. The slash-command paths (/compact, /diff, /status, /undo, connection.rs:1075-1323) return before any register call, so active is never set for them at all. Cancel is a no-op for the entire lifetime of those operations, not just a window.

Fix. Record a pending-cancel per thread in TurnWaiters; have prompt check and consume it immediately after turn/start returns, before register. That closes the window without introducing a lock ordering between the two paths.


B3 — The Apache-2.0 compliance test fails on every CI run

Confidence: Executed — reproduced against a real shallow clone, with output.

tests/vendor-licensing.test.ts derives its own baseline from history:

// tests/vendor-licensing.test.ts:54
function vendoringCommit(): string {
  const commits = git("log", "--format=%H", "--", "vendor/codex").trim().split("\n");
  return commits[commits.length - 1];   // oldest commit touching vendor/codex
}

That needs full history. CI does not have it:

# .github/workflows/ci.yml:37
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4        # ← line 39, no fetch-depth anywhere in the file

actions/checkout@v4 defaults to fetch-depth: 1. I reproduced the consequence:

$ git clone --depth 1 --branch pr210 --no-local file:///…/atlas shallow
$ cd shallow
$ git log --format=%H -- vendor/codex | wc -l
1
$ V=$(git log --format=%H -- vendor/codex | tail -1)
$ echo "vendoringCommit=$V  HEAD=$(git rev-parse HEAD)"
vendoringCommit=24ee88db38edfe91b175a53744696ccdf3bdaf0b  HEAD=24ee88db38edfe91b175a53744696ccdf3bdaf0b
$ git diff --name-only --diff-filter=d $V -- vendor/codex | wc -l
0

vendoringCommit() resolves to HEAD, so the diff against it is empty. Then the real test:

$ npx vitest run tests/vendor-licensing.test.ts

 ❯ tests/vendor-licensing.test.ts (7 tests | 1 failed) 423ms
     × finds the modification set (parser health) 271ms

 FAIL  tests/vendor-licensing.test.ts > §4(b) — modified files say they were modified
       > finds the modification set (parser health)
AssertionError: expected 0 to be greater than 5
 ❯ tests/vendor-licensing.test.ts:133:44
    131|   it("finds the modification set (parser health)", () => {
    132|     // If this returned nothing, the rule below would hold vacuously f…
    133|     expect(modifiedVendoredFiles().length).toBeGreaterThan(5);
       |                                            ^

 Test Files  1 failed (1)
      Tests  1 failed | 6 passed (7)

The same file passes on a full clone (§2), which is exactly why "green locally" missed it.

Worth saying: the parser-health floor did its job. Without that assertion the §4(b) rule would have passed vacuously in CI forever — zero files checked, zero violations found, green. The guard caught its own blind spot. It just fires in the one environment nobody has run it in yet.

Fix. fetch-depth: 0 on the frontend job. Consider also asserting vendoringCommit() !== HEAD with a message naming the shallow-clone cause, so the next person reads the reason instead of the symptom.


B4 — A malformed SSE chunk is dropped and the turn still reports a complete answer

Confidence: Read.

process_chat_sse handles three frame kinds. The error frame correctly records a failure:

// vendor/codex/codex-api/src/atlas_chat/sse.rs:317
if is_error_frame(&sse.data) {
    let disposition = atlas_gateway::classify_stream_frame(&sse.data);
    stream_error = Some(disposition.into_api_error());
    continue;
}

let chunk: ChatChunk = match serde_json::from_str(&sse.data) {
    Ok(chunk) => chunk,
    Err(err) => {
        debug!(error = %err, "failed to parse a chat.completion.chunk");
        continue;                    // ← line 326: no stream_error set
    }
};

An unparseable chunk sets nothing. If [DONE] then arrives, the terminal arm sees a clean state:

// sse.rs:300
if sse.data.trim() == DONE_SENTINEL {
    if let Some(error) = stream_error {return; }   // ← not taken
    emit_turn(&tx_event, state, &dialect).await;      // ← turn reported SUCCESSFUL
    return;
}

The turn completes successfully, missing whatever that chunk carried.

Why this matters more than an ordinary parse-error nit. The file's own header states the rule it exists to enforce:

"a mid-stream failure emits data: {"error":…} and withholds data: [DONE] — two independent signals, either sufficient alone; a stream that ends without [DONE] is incomplete, never a short success." — sse.rs:13-17

Both stated signals are honoured. But the file infers "no error signalled ⇒ nothing was lost", and the parse-failure arm breaks that inference from inside. Silent content truncation is the exact bug class the port was undertaken to eliminate; this is a different mechanism than the UTF-8 one, in the layer above the decoder — the decoder itself is fine (§5).

Trigger. Any chunk the ChatChunk struct cannot deserialize. Concretely: an explicit null where #[serde(default)] is declared — #[serde(default)] covers a missing key, not a present-but-null one, so {"id":"c1","choices":null} errors. More realistically: any future gateway shape change that adds or retypes a field. The failure mode is the same either way, and it is silent by construction.

Fix. Set stream_error in the parse-failure arm, exactly as the error-frame arm two blocks above does. A stream carrying a frame this client cannot read is not a stream it can honestly report as complete.


B5 — The lint that "makes it impossible again" cannot fail

Confidence: Executed — full census in §2.

11d48ea3 adds a workspace lint table with an explicit rationale:

# Cargo.toml:556
[workspace.lints]

# `unexpected_cfgs` is a deny because its warning form already cost a live
# failure: #54 deleted the `ported-engine` feature, four blocks stayed behind
# `#[cfg(feature = "ported-engine")]`, and a cfg on a feature that no longer
# exists compiles to NOTHING — the token-source registration vanished and the
# first real gateway turn went out with no Authorization header. Cargo warned;
# nobody greps warnings. Now it fails the build.
rust = { unexpected_cfgs = "deny" }        # ← line 564

The diagnosis is exactly right and the intent is exactly right. But Cargo does not apply [workspace.lints] to a member unless that member declares [lints] workspace = true. The census in §2:

  • 110 manifests in the tree carry a [lints] section
  • 110 of them are under vendor/codex/
  • 0 are under crates/ or src-tauri/

src-tauri/Cargo.toml — the crate where the outage actually happened — has no [lints] section. The entire table is dead configuration, and that includes the clippy denies below it (expect_used, await_holding_lock, await_holding_invalid_type, disallowed_methods, and the rest).

Trigger. Re-add #[cfg(feature = "anything-undeclared")] anywhere in src-tauri. Cargo emits a warning; nothing fails. .github/workflows/ci.yml:89 runs a bare cargo test for src-tauri with no -D warnings, and the clippy job at :165 covers only the crates matrix — so it doesn't fail there either. That is precisely the #54 outage, reproducible today.

Worth adding: tests/ipc-contract.test.ts splits generate_handler! textually, so a #[cfg]'d-out registration still counts as present there. The IPC guard cannot catch this class either.

Fix. Add to each first-party manifest:

[lints]
workspace = true

One caveat, so this doesn't land as a surprise: turning the table on is not free. It will surface existing violations — connection.rs:131 has a non-test .expect("position just found"), which clippy::expect_used will flag. That's the lint working, not a reason to skip it, but budget for a cleanup pass or start with unexpected_cfgs alone and add the clippy denies behind it.


B6 — Switching permission mode mid-turn changes the picker, not the running turn

Confidence: Read (Atlas side) / Traced by reviewer (engine side).

The mode mapping itself is good — genuinely one of the better-reasoned parts of this PR. It sets both halves and explains why either alone is a trap:

// crates/atlas-native-agent/src/engine/modes.rs:96
pub fn engine_policy(mode: &str) -> (AskForApproval, SandboxPolicy) {
    match normalise(mode) {
        "bypass"      => (AskForApproval::Never,        SandboxPolicy::DangerFullAccess),
        "plan"        => (AskForApproval::Never,        SandboxPolicy::ReadOnly { network_access: false }),
        "acceptEdits" => (AskForApproval::OnRequest,    workspace_write()),
        _             => (AskForApproval::UnlessTrusted, workspace_write()),   // fails closed
    }
}

The problem is set_mode, which has no in-flight-turn check:

// crates/atlas-native-agent/src/engine/connection.rs:1728
fn set_mode(&self, mode: acp::SessionModeId) -> BoxFuture<'static, Result<()>> {let (approval_policy, sandbox_policy) = modes::engine_policy(&mode.0);
    connection.update_settings(&session_id, |params| {
        params.approval_policy = Some(approval_policy);
        params.sandbox_policy  = Some(sandbox_policy);
    }).await?;
    // Recorded only after the engine accepted it.
    connection.session_modes.lock().insert(session_id, mode);
    Ok(())
}

It sends the update unconditionally, then records the new mode in session_modes — which is exactly what current_mode() (connection.rs:1711) reads to render the picker. Nothing gates the control on the frontend either: the picker and the Shift+Tab cycle both fire while a turn is generating.

Traced by the group-3 reviewer, not independently confirmed by me: engine-side, Session::update_settings writes only session_configuration, and the running turn keeps its frozen TurnContext. If that's right — and it matches how the mode is passed at turn start — the running turn's remaining tool calls execute under the policy the turn began with.

Trigger. Start a turn in Bypass, switch to Plan while it generates. The picker reads "Plan — read-only, no edits or commands" while the turn continues with AskForApproval::Never + DangerFullAccess.

Blast radius. The user believes they have revoked write and shell access and has visible confirmation of it. They have not. This is the one failure mode where a security control being displayed as active is worse than it being absent.

Fix. Pick one and be explicit: refuse the change while a turn is in flight and say why, or apply it and interrupt the turn. Silently showing the new mode is the only option that misleads. If the engine genuinely cannot rebind a running turn, refusing is the honest answer.


4. Non-blocking


N1 — Signing out does not invalidate the access token the engine already holds

Confidence: Read + executed grep.

$ git show pr210:crates/atlas-native-agent/src/engine/auth.rs | grep -n "pub fn"
80:pub fn register_token_source(source: Arc<dyn AtlasTokenSource>)
85:pub fn registered_token_source() -> Option<Arc<dyn AtlasTokenSource>>
151:    pub fn new(source: Arc<dyn AtlasTokenSource>) -> Self
155:    pub fn with_clock(source: Arc<dyn AtlasTokenSource>, clock: Arc<dyn Clock>) -> Self

No clear(), no invalidate(), no Drop. The cache serves the token until exp − 60s:

// auth.rs:163
fn cached_if_fresh(&self) -> Option<String> {
    let now = self.clock.now_unix();
    let cached = self.cached.lock().unwrap_or_else(|p| p.into_inner());
    cached.as_ref().filter(|c| now < c.renew_after).map(|c| c.token.clone())
}

auth_sign_out (src-tauri/src/commands/auth.rs:214) clears disk and revokes the session token. The access JWT is verified statelessly against JWKS, so revoking the session token does not invalidate it. An open native-agent connection therefore keeps making authenticated, org-billed gateway calls until the JWT's own expiry — up to ~9 minutes.

The doc comment at src-tauri/src/auth/core.rs:1294 still says "There is still no in-memory access token to clear." 34ac769b made that false and the comment wasn't updated, which is how this stayed invisible.

Fix. Give AtlasExternalAuth a clear() and call it from the sign-out broadcast; or drop the engine connection on sign-out, which also handles the in-flight turn.


N2 — A fourth phone-home path ships, and the guard test is structurally unable to see it

Confidence: Read. The three removals the PR names are genuinely gone at source — verified independently, and the Windows one removed telemetry only (see §5). This is a fourth site the PR does not mention.

vendor/codex/feedback/src/lib.rs:42
  "https://ae32ed50620d7a7792c1ce5df38b3e3e@o33249.ingest.us.sentry.io/4510195390611458"
vendor/codex/feedback/src/lib.rs:438
  dsn: Some(Dsn::from_str(SENTRY_DSN)…)

It is not dead code in the dependency graph:

crates/atlas-native-agent/Cargo.toml:57   codex-feedback = { path = "../../vendor/codex/feedback" }
crates/atlas-native-agent/src/engine/runtime.rs:52    use codex_feedback::CodexFeedback;
crates/atlas-native-agent/src/engine/runtime.rs:194       feedback: CodexFeedback::new(),

It is unreachable today, and I want to be precise about that rather than overstate it. Nothing in src/, crates/ or src-tauri/src/ constructs a feedback RPC, and the crate installs no global Sentry hook — grep -n "sentry::init\|panic::set_hook" over the crate returns nothing; the client is built inside upload_feedback and nowhere else. So the DSN ships in the binary without firing.

Why it's still worth raising. The guard test enumerates three known removals rather than proving absence of egress. Its full structure:

$ git show pr210:tests/codex-no-phone-home.test.ts | grep -n "describe("
117:describe("the engine's egress surface (parser health)", …
132:describe("the Statsig metrics exporter is gone", …
155:describe("the ChatGPT analytics client is gone", …

Three blocks, ending at line 182. It scans vendor/codex only (:36), greps no sentry or ingest literal, and has no assertion at all for the Windows-sandbox removal that 0f16c4cf also made. It proves the three removals stayed removed. It does not — and structurally cannot — prove there is no fourth.

That distinction matters because #53's outbound-traffic audit is the open acceptance item this PR is drafted against, and a reader could reasonably take "the phone-home guard passes" as having discharged it.

Fix. Either drop the codex-feedback dependency (nothing calls it), or add a describe block that greps the whole vendored tree for hardcoded hosts against an explicit allowlist, so a fifth site fails rather than passes.


N3 — No CI job covers any vendored engine crate

Confidence: Read.

  • .github/workflows/ci.yml:89cargo test, working-directory: src-tauri. Member-scoped, so it selects only that package.
  • .github/workflows/ci.yml:110-139 — the crates matrix, 19 entries, all atlas-*.
  • No job anywhere runs anything under vendor/.

atlas-cersei had a matrix entry; 1f82a358 correctly removed it. Its replacement — the whole ported engine, including the Atlas-authored atlas_chat/ dialect (~2,700 lines) with request_tests.rs (554 lines) and sse_tests.rs (508 lines) — got none. Those ~1,060 lines of tests, which are the primary evidence for the dialect's correctness, never run in CI.

tests/ci-coverage.test.ts reads crates/ and therefore cannot notice the gap.

Every test count in these commit messages is local-only. That's fine as a statement of what the author ran; it is not a statement about what CI protects.

Fix. One matrix entry running cargo test -p codex-api would cover the Atlas-authored dialect, which is the part that's genuinely yours. Covering all 110 vendored crates is not the ask — and per the group-2 reviewer, ~30 upstream vendored tests are now permanently red after the analytics removal, so a blanket job would fail immediately.


N4 — The vendored engine compiles at opt-level = 0 in dev

Confidence: Read + executed count.

The root manifest is careful about this and explains itself:

# Cargo.toml:673
# `"*"` above reaches dependencies ONLY — workspace members are excluded from
# it by cargo. Before the workspace, every `crates/atlas-*` package was a plain
# path dependency of src-tauri and therefore covered; as members they would
# silently drop to opt-level 0…
[profile.dev.package.atlas-acp-thread]
opt-level = 1
… (18 stanzas)

The reasoning is right and it was applied to all 18 first-party members. It was not applied to the ~110 vendored members, which are members too (Cargo.toml:47-…):

$ git show pr210:Cargo.toml | grep -c "profile.dev.package.codex"
0

The guard was deliberately scoped out, with a documented expiry:

// tests/cargo-workspace.test.ts:215
// …on no runtime path until the seam is rewired (#45)… Revisit in #45.

#45 (a630f480) and #54 (61277ab3) are both on this branch, and crates/atlas-native-agent/Cargo.toml:49-63 declares the engine crates as unconditional path dependencies. The condition the exemption was written under has expired.

Impact. Every bunx tauri dev turn runs ~600k LOC of engine — streaming, rollout I/O, sandboxing, apply-patch — unoptimized.

This is a genuine tradeoff, not an obvious bug: adding the stanzas costs a longer first clean build. The point is that the decision should be re-made now that the condition changed, rather than inherited from a comment that no longer applies.


N5 — Session state in the sink grows for the life of the process

Confidence: Read + executed grep (Atlas side) / Traced by reviewer (the 100 ms abort trigger).

$ git show pr210:crates/atlas-native-agent/src/engine/sink.rs | grep -n "remove(\|clear()\|retain\|insert("
 78:        self.lock().insert(                       # EngineSessions — the only insert
127:        let output = session.command_output.entry(item_id.to_string()).or_default();
134:            session.command_output.remove(item_id);   # the ONLY .remove( in the file
156:            session.streamed.insert(item_id.to_string());

Three separate growths:

  1. EngineSessions has insert (:77) and no removal path at all. EngineConnection implements no close_session. Per agent_host.rs, the connection's lifetime is the process lifetime.
  2. streamed: HashSet<String> (:43) only ever inserts (:156). One String per streamed item, for the process lifetime.
  3. command_output is cleared only on ItemCompleted (:134, reached from :395).

Only thread is Weak (sink.rs:35); command_output, streamed, cwd and skills are owned by the map entry and outlive the AcpThread being dropped.

The trigger for (3), traced by the group-6 reviewer through vendored code I did not open myself: the engine's GRACEFULL_INTERRUPTION_TIMEOUT_MS is 100 ms (vendor/codex/core/src/tasks/mod.rs:66); past that it calls task.handle.abort() (:913). An aborted task emits no ItemCompleted, so sink.rs:395 never runs and that command's accumulated output is retained permanently. Press stop on a few verbose builds and it accumulates.

Fix. A close_session on the connection that removes the map entry covers all three. Failing that, clear command_output on turn completion as well as item completion.


N6 — Native-agent file edits never reach shared cross-agent memory

Confidence: Read. Two independent blockers, both new in 733cda07.

First, the name never matches. The sink builds the tool call with a human-readable title and no machine name:

// crates/atlas-native-agent/src/engine/sink.rs:239
let mut call = acp::ToolCall::new(id.clone(), title)
    .kind(acp::ToolKind::Edit)

The consumer tests for an exact string:

// src-tauri/src/commands/memory_delta.rs:148
fn is_file_mutation(tool_name: &str) -> bool {
    let n = tool_name.to_lowercase();
    n == "edit" || n == "write" || n == "create"
        || n.contains("str_replace") || n.contains("create_file")
        || n.contains("apply_patch") || n.contains("multiedit")
}

"edit src/foo.rs" is not "edit", and matches none of the contains arms either.

Second, even on a match, the path lookup misses. The sink writes a plural array:

// sink.rs:248
.raw_input(serde_json::json!({
    "paths": changes.iter().map(|c| c.path.clone()).collect::<Vec<_>>(),
}));

The extractor probes five singular keys, none of them paths:

// memory_delta.rs:159
for key in ["file_path", "path", "filePath", "target_file", "file"] {

Blast radius. Claude Code and Codex see no file_changed signal for anything Atlas Agent writes — the cross-agent memory feature silently excludes the native agent. No test covers it.

The repo already solved the first half. crates/atlas-checkpoint/src/tools.rs:102 has a canonical_name that does leading-token matching specifically for this agent. memory_delta never calls it. Reusing it, plus adding paths to the key list (or emitting a singular path when there's one change), fixes both.


N7 — Retry-After is honoured with no upper bound

Confidence: Read (parser) / Traced by reviewer (the sleep site).

// vendor/codex/codex-api/src/atlas_gateway.rs:94
fn retry_after(header: Option<&str>) -> Duration {
    header.and_then(|v| v.trim().parse::<u64>().ok())
        .map(Duration::from_secs)
        .unwrap_or(Duration::from_secs(60))
}

The fallbacks are right and deliberate: absent, malformed, or HTTP-date-formatted values all fail to parse and fall back to 60s, which is the safe direction. But a parseable absurd value is honoured verbatim — Retry-After: 86400 yields a 24-hour sleep. Per the group-4 reviewer, responses_retry.rs:124 sleeps it directly, and the MAX_CONNECTION_RETRY_DELAY clamp covers only the connection branch.

Trigger. A 429 carrying a large Retry-After from the gateway or from any intervening CDN or WAFclassify falls back gracefully on a non-envelope body, so an infrastructure-generated 429 reaches the same arm.

Fix. Clamp to the same bound the connection branch uses. The gateway documents only 1 and 60; anything beyond a few minutes is a bug or an intermediary, and either way the user should get an error rather than a silent multi-hour stall behind "Reconnecting…".


N8 — Approval delivery failures are swallowed with no log

Confidence: Read.

// crates/atlas-native-agent/src/engine/connection.rs:505
match answer.result {
    Ok(value) => {
        let _ = client.resolve_server_request(answer.request_id, value).await;   // 507
    }
    Err(message) => {
        let _ = client.reject_server_request().await;                           // 510
    }
}

If either send fails, the engine turn waits forever for an answer that will never arrive, and nothing is written anywhere. The biased; select above it exists precisely because "a turn is blocked on every one of them" — which makes the silent-drop the worst case for this specific channel.

The repo's own convention logs the equivalent: crates/atlas-agent-servers/src/connection.rs:158 logs a transport failure on the analogous path.

Fix. if let Err(e) = … { tracing::warn!(…) }. One line each.


N9 — CI does not pass --locked, so the new lockfile guard is advisory

Confidence: Traced by reviewer — I did not re-run the CI config analysis myself.

tests/cargo-deps-unification.test.ts asserts the libsqlite3-sys pin and single-resolution of rusqlite / tree-sitter against the committed Cargo.lock, from the frontend job, which has no Rust toolchain. Meanwhile the app and crates jobs run cargo without --locked, so cargo freely re-resolves in-runner and discards the result.

A manifest edit committed without its regenerated lock passes both: cargo silently updates the lock it's using, and vitest validates the stale file on disk. The links = "sqlite3" collision then surfaces at release-build time, far from its cause.

This is newly meaningful — origin/0.3.1 had 21 per-crate locks and no such guard, so there was nothing to be stale.

Fix. --locked on the three cargo invocations.


N10 — Transparent PNGs composite to black when downscaled

Confidence: Read.

// src/features/chat/components/message-input.tsx:145
const ctx = canvas.getContext("2d");
if (!ctx) return image;
ctx.drawImage(bitmap, 0, 0, width, height);     // no fillRect first

const encoded = canvas.toDataURL("image/jpeg", quality);

A fresh canvas is transparent; JPEG has no alpha channel, so transparent pixels composite to black. A macOS window capture — rounded corners and drop shadow, frequently over the 512 KB budget — comes out with black corners and a black halo.

Fix. ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, width, height); before drawImage.

Related, smaller (N10b). PER_IMAGE_BUDGET_BYTES is BODY_CAP_BYTES / 4 and enforced per image only (image-policy.ts:33), so four in-budget attachments plus prompt, tools and history guarantee a 413. The 413 handling itself is correct and deliberate — terminal, not retried, gateway message surfaced (atlas_gateway.rs:178-184, tested at :358) — and the module's decision to let the gateway be the backstop is documented and defensible. The gap is only that nothing warns before sending.


N11 — Stale documentation points at deleted code

Confidence: Read (reported by the deletion scout, spot-checked by me).

Nothing here breaks at runtime; all of it misleads the next reader.

  • ARCHITECTURE.md:347 — describes [patch.crates-io] overrides in src-tauri/Cargo.toml and the _CERSEI_UTF8_PATCH_GUARD / _CERSEI_CANCEL_PATCH_GUARD compile guards. All three are gone. It ends "Don't delete the guards to fix the build."
  • ARCHITECTURE.md:218,313,325-326 — points at crates/atlas-cersei/ARCHITECTURE.md ("read it before touching agent lifecycle") and vendor/cersei-provider / vendor/cersei-agent. vendor/ now contains only codex.
  • CONTRIBUTING.md:175 — documents cargo test -p atlas-cersei, which now fails.
  • README.md:66"The Atlas agent runs in-process on Cersei, our Rust agent framework", contradicted by CONTEXT.md:26.
  • crates/atlas-memory/README.md:22 and MIGRATION.md:10 — cite atlas-cersei/ARCHITECTURE.md §6e and atlas-cersei/src/memory.rs as the frozen seam spec.
  • tests/cersei-containment.test.ts:37ALLOWED_CERSEI_MANIFESTS still allowlists crates/atlas-cersei/Cargo.toml. Harmless (the walker only visits existing directories), but the test's own header at :21 says to shrink this set in the deletion commit.
  • crates/atlas-native-agent/src/engine/mod.rs:3"Everything in here is gated on the ported-engine feature", contradicted by Cargo.toml:42 ("No longer optional"). A reader trusting this assumes a kill switch that no longer exists.

The ~40 remaining "cersei" string literals in src-tauri/ and src/ are not in this list — CONTEXT.md:26 and lib.rs document that the id is a deliberately-stable storage key, and f6ffabcf correctly left it alone.


5. What holds up

Reported because a review that lists only defects misrepresents the work. Each of these was checked deliberately, most of them because they were the claims most worth doubting.

The central reliability premise is real. SSE framing and incremental UTF-8 decoding are delegated to the eventsource-stream crate (vendor/codex/codex-api/Cargo.toml:27; sse.rs:257 stream.eventsource()), so Atlas's code only ever sees fully-decoded String frames. A multi-byte codepoint split across TCP chunks, a 4-byte emoji, \r\n vs \n, a chunk boundary landing mid-field or exactly on data: — all of that is a maintained library's problem, not a hand-rolled decoder's. The patched bug class genuinely cannot recur here. B4 is a different failure, one layer up, and does not undermine this.

A stream that ends without [DONE] fails closed (sse.rs:278-289) and there's an idle timeout on every poll (:266). Both correct.

Terminal events cannot be dropped. The in-process transport splits delivery guarantees by notification type:

// vendor/codex/app-server/src/in_process.rs:109
fn server_notification_requires_delivery(notification: &ServerNotification) -> bool {
    matches!(notification,
        ServerNotification::TurnCompleted(_)
            | ServerNotification::ThreadQueueChanged(_)
            | ServerNotification::ThreadSettingsUpdated(_)
            | ServerNotification::ExternalAgentConfigImportCompleted(_))
}

Those take a blocking .send().await (:735); everything else takes try_send with a warn-on-full (:746). The entire TurnWaiters design depends on TurnCompleted never being dropped, and it isn't. I went looking for a hung-waiter bug here and it isn't there.

The register-after-response race is closed, deliberately. TurnWaiters keeps an unclaimed: VecDeque<v2::Turn> (connection.rs:120) so a completion that arrives before anyone registers is claimed rather than lost (:127-136), bounded at 16 entries with a documented rationale. Careful work — and it's why B2 is specifically about cancel, not about completion.

Burst batching cannot lose events. 91ced74f was the commit I most expected to find a bug in, and it's clean. next_event is self.event_rx.recv().await over a tokio::sync::mpsc::Receiver (in_process.rs:268,320), which is cancel-safe — so neither the select! dropping the future nor the now_or_never drain (connection.rs:537) can swallow an event. There's no timer, so there's no partial buffer that can sit un-flushed. Merging is adjacency-only on already-decoded strings, so no character or code fence gets split. The commit comment explains all of this correctly.

Permission modes fail closed and are enforced at the point of action. engine_policy's _ arm is UnlessTrusted + workspace-write — the strictest non-plan pairing (modes.rs:106). The sandbox is a real process-level control, not a UI state. The doc comment explaining why sandbox-alone and approval-alone are each a trap (modes.rs:15-16) is the kind of thing that stops the next person from breaking it.

The entitlement check is a UX pre-flight, and says so. agent_entitlement.rs:11-17 explains that the real gate is the gateway's own 403 and this exists so a user isn't told to type a message that will fail. Unknown is deliberately distinct from NoGrant so an offline user isn't sent to their admin. This is the right split — I went in expecting a client-side authorization gate and it isn't one.

The shared request-id counter is correct and complete. One AtomicI64 per connection, shared across every control path. Ids are Atlas-minted; the engine rejects only concurrently-in-flight duplicates; unknown-id responses are logged and dropped; shutdown drains pending waiters with an internal error. No collision, wrap, or cross-matching is reachable.

Approvals cannot be mismatched. Matching is by engine-minted id echoed back, so cross-matching isn't possible. Dropping the thread resolves an outstanding waiter as Cancelled rather than leaking it.

The Windows sandbox lost telemetry, not a security control. This was the highest-risk item in 0f16c4cf and it checks out: the 141 removed lines from wfp_setup.rs are entirely WfpSetupMetric / emit_wfp_setup_metric / metric-name constants. install_wfp_filters_for_account is still called identically, still inside catch_unwind, wfp.rs was not touched by the commit at all, and the elevation payload lost only its otel field, symmetrically on both producer and consumer.

The Statsig and analytics removals are real deletions, not stubs. No HTTP client, no URL, no key survives in vendor/codex/analytics or vendor/codex/otel; analytics/Cargo.toml has no reqwest; core/src/config/otel.rs:24 defaults the metrics exporter to None. Nothing was left re-enablable by config.

Licensing obligations are met. LICENSE and NOTICE are wired into src-tauri/tauri.conf.json:46-48, and every modified vendored file carries the §4(b) change notice. B3 is that the test proving this cannot run in CI — not that the compliance is absent.

The merge dropped nothing from 0.3.1. git log pr210..origin/0.3.1 is empty, and the added-line scan in §2 found no real absences. The group-6 reviewer additionally ran git merge-tree against the auto-merge and found exactly one intentional deviation — the fork-fix port — with the two apparent losses (commands/mcp.rs, agents_set_compress) both being deliberate upstream and #54 deletions respectively.

The fork fix reaches both doors. One helper, src/features/chat/lib/fork-session.ts, called from the header menu (chat-panel.tsx:172) and the composer's /fork (message-input.tsx:1406,1662). 0.3.1's rule — a branch belongs to the source session's project, not the active workspace — survives as tier 1:

cwd: sess.workingDirectory
  || workspacePathForTab(tabId)
  || useProjectStore.getState().currentProject?.path
  || "",

The added fallbacks cannot mask it, because every session is created with a non-empty cwd that gets stamped into workingDirectory. Consolidating two call sites into one helper was the right call and it was done correctly.

No dangling references, and no silently dropped IPC. Every invoke() name in src/ resolves to a generate_handler! entry, and every registration has a #[tauri::command] definition. The one removed command, agents_set_compress, lost its definition (agents.rs:1455) and its only caller (chat-store.ts:233) in the same PR. No atlas:* event emitted on the base is missing on the head. The root Cargo.toml has no atlas-cersei member and no cersei-* patch entries.

Workspace adoption changed no resolution. No [workspace.dependencies] was introduced, no member moved a dependency to workspace = true, no feature list was edited, and resolver = "2" is pinned explicitly at Cargo.toml:24 — a workspace root defaults to resolver 1, and that was caught. Release profiles were moved from src-tauri/Cargo.toml, not rewritten (a comment-stripped diff of the two is empty). Every rusqlite declaration carries features = ["bundled"], so the 0.32→0.39 bump cannot link a system SQLite, and the highest SQL floor in the tree (STRICT, 3.37) is far below the pinned version.

The bulk vendoring commit is clean. No .github/ workflows that would activate in this repo, no submodules, no network-touching build.rs, no credentials, no real API keys in fixtures. Largest non-source blob is a 620 KB JSON schema.

71fb02fe is correct. install_manager still runs before app.manage(AuthState::new(…)), but both registrations resolve lazily via try_state, so no panic is reachable. Fixed by deferral rather than reorder — deliberate, and documented as such.

939e31bc fixed the root cause, not the symptom. The stop-button crash was a bare tokio::spawn from a sync Tauri command on the main thread, where there's no ambient runtime. The fix uses self.runtime.handle().spawn (connection.rs:1417) with the reasoning written down. No catch_unwind, no swallowed Result, no ResultOption broadening. (One asymmetry noted below.)

Checkpoints are not per tool call. I went looking for unbounded checkpoint growth in 733cda07 and it isn't there — checkpoints are per git commit, and there's no N+1 on the thread store; the IPC path stays linear via a suffix diff in the projector.


6. Candidate findings that died during verification

Listing these because they're what makes the rest trustworthy. Each looked real until the code was opened.

  1. "A dropped TurnCompleted hangs the waiter forever, with no timeout anywhere on the chain." Refuted — TurnCompleted is in the guaranteed-delivery set and uses a blocking send (in_process.rs:109-117,735).
  2. "The select! and now_or_never batching can swallow an event if next_event isn't cancel-safe." Refuted — it's mpsc::Receiver::recv, which is cancel-safe (in_process.rs:268,320).
  3. "Concurrent token minting is a thundering herd with a lost update." Real in mechanism, benign in effect — mint_fresh holds no lock across the await, so N concurrent resolves each mint. But every minted token is valid, and renew_after is derived from each token's own exp, so a "lost update" installs a token that simply re-mints sooner. Extra round trips, not a correctness bug. Downgraded out of the report.
  4. ".max(now + 1) caches an already-expired JWT." True and deliberate — it's the documented meltdown-avoidance for a token whose exp is already inside the margin. Worst case is one 401, which refresh() handles by always re-minting. Self-correcting.
  5. "The entitlement gate is client-side only." Refuted — it's explicitly a pre-flight for UX; the gateway's 403 is the gate (agent_entitlement.rs:11-17).
  6. "The plaintext session token is a new security regression." Refuted — it pre-exists on origin/0.3.1. The diff to src-tauri/src/auth/store.rs in this PR is only an expansion of the doc comment ratifying it as a recorded D14 exception. Flagging it as introduced here would have been wrong.
  7. "Declining a PermissionsRequestApproval is transmitted as an approval." Refuted by the group-3 reviewer's verifier — an empty GrantedPermissionProfile is the wire encoding of denial. (The inverse is a real, smaller gap: Allow grants nothing extra, so the model can re-request and re-prompt. That's documented deliberately at connection.rs:720-724, but with no loop guard.)

7. Commit by commit

"Concern" means a finding above lands in that commit. It is not a verdict on the commit's intent — several of the concern rows are otherwise strong commits.

Group 1 — docs, workspace, dependency unification

Commit Subject Verdict Note
da49c887 ADR-0003 + port spec + research Sound Docs only
291e0a48 port spec, research, gateway reference Sound Three .gitignore negations; the six new docs carry no sk-/eyJ/AKIA/BEGIN matches, nothing that should be tracked became hidden
acbe75b6 #38 adopt the root cargo workspace Concern N4. Resolution itself is unchanged; resolver = "2" pinned explicitly, profiles moved not rewritten
330c453d #39 unify rusqlite and tree-sitter pins Sound All declarations bundled; SQL floor far below the pin; sqlite_floor.rs reads version_number() — the runtime library, which is what it claims to pin
34d54987 #40 record Phase 0 verification findings Sound Spec text
25d40032 #41 correct D14's premise Sound Spec text
988a9a38 #41 reconcile D14, ratify storage exception Sound Documents a pre-existing decision rather than introducing one — see §6.6

Group 2 — vendoring, phone-home, licensing

Commit Subject Verdict Note
67fb707d #42 vendor the engine closure, quarantined Sound No workflows, submodules, blobs or credentials. Quarantine is a vitest text scan, not a cargo gate — all 110 crates are members, so a bare cargo build at root compiles them
0f16c4cf #43 rip the phone-home paths out Concern Removals genuine, no security control lost. N2 — a fourth path survives
a48547e7 #44 put the Apache-2.0 obligations in force Blocking B3. The obligations themselves are correctly met

Group 3 — linking the engine, building the seam

Commit Subject Verdict Note
340fc593 #45 ADR-0004 and the open-question-3 trace Sound Docs only
c5823a75 #45 link the engine, config assembly Sound enable_codex_api_key_env: false is the right call
a630f480 #45 rewire the seam over the in-process client Concern Establishes register-after-response, which B2 rides on. The unclaimed buffer correctly closes the completion half of that race
adb8d305 #46 cancel, retry, stop reasons Blocking B2
f9e3f0c8 #47 permission modes and the effort knob Blocking Mapping is correct and enforced at the point of action; B6 is the mid-turn change
693da6aa #47 share one request-id counter Sound Correct and complete — no collision, wrap, or mismatch reachable
aef46141 #47 tool approvals round-trip Sound Matching by engine-minted id; no cross-matching possible
0a38f8dc #46 cancel kills the command, retry does not re-run Concern The retry test is a genuine filesystem-level proof. The cancel test cannot fail on B2 (100 ms retry loop)

Group 4 — memory, history, dialect, error classification

Commit Subject Verdict Note
a237fbd0 #48 search_memory; dead Cersei call sites go Sound Limit clamped, empty query refused, every branch answers. No timeout around the injected retrieval future
ed265811 #49 history continuity across the swap Blocking B1 — the resume fallback. The sink fix itself is correct and mutation-checked
c04539bd #50 the D13 error-classification arm Concern Table is right and its tests would fail if it weren't. N7Retry-After has no upper bound
7db3bdff #50 the gateway Chat Completions dialect Blocking B4. Otherwise the strongest commit in the group — allowlist-by-type is the right defence, and the byte-by-byte UTF-8 split test is real
8cc8f2af #50 wire the dialect into the engine Sound Keying the override on WireApi rather than provider id is the right seam
224b5b58 #50 static catalogue + four review fixes Sound All four fixes real; the nested-stream_options test is a genuine assertion

Group 5 — auth, UX policies, unwiring Cersei, rename

Commit Subject Verdict Note
34ac769b #51 authenticate with the user's Atlas account Concern Token never on disk, no Debug on the cache, resolved at connect not construction, with a test pinning it. N1 is the sign-out gap
9a102096 #52 the three gateway UX policies (D15) Concern Entitlement and retry-delay are solid; the image policy is the weak leg (N10)
61277ab3 #54 unwire the Cersei path Concern Shipped a total auth outage for four commits — four #[cfg(feature = "ported-engine")] blocks survived the feature's deletion, so register_token_source never ran. Fixed by 11d48ea3; confirm the two land together
f6ffabcf #55 rename to Atlas Agent + trademark scrub Sound No wire value, persisted key, config key, event name or model slug was renamed. CERSEI_AGENT_ID correctly left as the literal "cersei" — it is a storage key, and the code says so
16a42978 #54 remove the unwired Cersei path from disk Sound Nothing references it; no persisted state breaks (no store is persist-backed, so there is nothing to migrate)
1f82a358 #54 drop the deleted crate from the CI matrix Concern Correct in itself — but N3, the replacement got no job

Group 6 — the fix tail and the 0.3.1 merge

Commit Subject Verdict Note
194a0d53 Atlas Agent tells the app what it can do Sound
614795ce frontend stops treating it as the BYOK agent Sound
11d48ea3 restore #54's registrations + the lint Blocking Fix correct and the diagnosis in its comment is exactly right. B5 — the guard it adds is inert
21755285 every gateway request names the paying org Sound Lazy try_state used correctly. Header is absent when there is no active org — worth confirming that is the intended gateway behaviour
71fb02fe registrations must not touch AuthState early Sound Fixed by deferral rather than reorder; no panic reachable
1675da62 slash commands survive a resume Sound
02a5d6a5 reopened session shows its conversation Concern Superseded by b992a18b
b992a18b a reopened session replays under its own id Blocking B1 lands here. Also sets supports_load_session = true, which removes the "can't replay past messages" notice that ed265811's doc comment relied on — so B1 now lands with no explanation shown to the user
91ced74f streamed replies apply in bursts Sound Attacked specifically; see §5
630a9357 /undo, /goal, /review, skills, /fork, /queue Sound
939e31bc the stop button must never take the app down Sound Root cause, not symptom. Applied asymmetrically: cancel holds Arc<EngineRuntime> while EngineSessionControls (connection.rs:1859) holds a bare Handle, and EngineRuntime::drop calls shutdown_background(). set_effort is safe only because its caller happens to hold the connection across the call — incidental, not structural
733cda07 tool calls reach the thread, with checkpoints Concern N5, N6. No unbounded checkpointing and no N+1 — see §5
24ee88db Merge upstream/0.3.1 Sound Nothing dropped; exactly one deliberate hunk beyond the auto-merge

Two bare tokio::spawn calls remain detached at connection.rs:705 and :757 (approvals, dynamic tools). They're safe today because both are reachable only from pump_events, which runs on the engine runtime — but that's a property of the current call graph, not an enforced invariant, and it's the same shape as the bug 939e31bc fixed. Worth a comment at minimum.


8. The open ADR-numbering question

Leaving this for a maintainer was the right call, and the stated hazard is real — docs/reference/atlas-ai-api.md carries an unrelated ADR-0003 belonging to the gateway repo that a blind sweep would corrupt.

One consideration the PR body doesn't weigh: the costs are not symmetric over time. Renumbering to 0007/0008 is a one-time sweep across 18 tickets, the spec, and some code comments — all documents a human reads, where a wrong link is visible and fixable. Leaving two 0003s and two 0004s makes every future reference permanently ambiguous, and the ambiguity is silent — nothing fails, readers just land on the wrong document and don't know it.

Renumber, excluding the gateway file by explicit path rather than by pattern.


9. Limits

Stated plainly, because several conclusions above depend on them.

  • No Windows build. windows-sandbox-rs is cfg(target_os = "windows") and no CI job builds it, so the hand-edited wfp_setup.rs (including a stray misindent noted at win.rs:1017) has never seen a compiler. The removal was verified by reading the diff, not by compiling it.
  • No tauri build. LICENSE and NOTICE were verified in tauri.conf.json, not in a produced .app.
  • No live gateway turn. Chat: make the send keybinding configurable (default should be Enter-to-send, Shift+Enter for newline) #51 and add required linux build dependencies to README #53 remain the right gate and both need a human. Nothing here substitutes for the outbound-traffic audit or the capped-grant 402 check by observation.
  • cargo check covered atlas-native-agent only, not the full 130-member workspace, and no Rust test suite was run — neither the 332 src-tauri tests nor the crate suites. The frontend numbers in §2 are the only test evidence in this review.
  • Claims about engine-internal retry and task-abort behaviour (N5's 100 ms trigger, N7's sleep site, B6's frozen turn context, the 401 retry-count question) come from reading vendored source that no CI job exercises. They're marked "traced by reviewer" above and should be treated as strong leads rather than settled facts.
  • The 401 token_expired "refresh once" claim is one I could not settle. The classification is unambiguously correct at atlas_gateway.rs:152 (RefreshAuthThenRetryOnce). Whether "once" is actually enforced downstream — the group-4 reviewer traced a path where an exhausted UnauthorizedRecovery re-classifies into a retryable stream error and issues further requests with the dead credential — I did not independently confirm. Worth one look before merge, since it's a stated policy.

@Ukaykhingmarma28
Ukaykhingmarma28 marked this pull request as ready for review August 30, 2026 09:14
@pacifio
pacifio merged commit d72fa67 into pacifio:0.3.1 Aug 30, 2026
12 of 21 checks passed
This was referenced Aug 30, 2026
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.

3 participants