jeo-code 0.8.5 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.8.8] - 2026-07-08
10
+ _"실동작검증을 통해서 유효성 평가하고 개선해줘" — live-verified the v0.8.5 fallback mechanism using active Claude Code OAuth credentials on this machine (confirmed 200 OK for both Fable 5 and Opus 4.8 via the proxy). Identified two critical improvement vectors during verification: (1) server-side fallback was previously scoped strictly to API-key credentials, leaving OAuth users (who also hit `reasoning_extraction` refusals) with today's reactive-only recovery; (2) the sequential `postAnthropic` retry ladder could throw immediately on a combination of different error types (e.g. temperature error first, then fallback error, then success)._
11
+
12
+ ### Fixed
13
+ - **OAuth support for server-side fallback** (`src/ai/providers/anthropic.ts`) — removed the `credential.kind !== "api_key"` constraint. The Claude Code OAuth proxy has been verified to fully support both the `fallbacks` parameter and the `server-side-fallback-2026-06-01` beta header. `headersFor` now dynamically injects the fallback beta header into the OAuth request headers when applicable, extending the refusal-avoidance benefit to all subscription users.
14
+ - **Loop-based unified retry handling** (`src/ai/providers/anthropic.ts`) — refactored the sequential `if (isXError)` retry lines into a clean, unified `for` loop. The loop dynamically flips state flags (`includeTemperature`, `stripArtifacts`, `disableFallback`) and retries up to 4 times, allowing successful recovery from any combination of errors.
15
+
16
+ ### Added
17
+ - **Combination retry test** (`test/anthropic-fallback.test.ts`) — added a robust regression test verifying that `postAnthropic` recovers successfully when multiple orthogonal errors occur in sequence (temperature error first, then fallback error, then success) and yields the correct final payload.
18
+
19
+ ## [0.8.7] - 2026-07-08
20
+ _Fix-forward for a CI-caught release failure in v0.8.6 (this same session): `test/launch-prompt-routing.test.ts` shipped with a dropped closing brace (a git 3-way merge misaligned on a duplicate `});` line during this session's own working-tree isolation, then a subsequent `git stash push --keep-index` silently reset the working tree to the pre-fix staged content — the fix was applied once, verified once, then invisibly reverted before the commit that actually shipped). The v0.8.6 GitHub Actions release workflow correctly caught the syntax error at `bun test` and failed BEFORE the npm publish step ran; `npm view jeo-code version` confirmed 0.8.5 remained latest throughout — nothing broken ever reached the registry. Per this repo's established pattern for a same-day CI-caught regression (v0.8.0 → v0.8.1), shipping forward rather than retargeting the already-publicly-failed v0.8.6 tag._
21
+
22
+ ### Fixed
23
+ - **Restored the dropped closing brace** in `test/launch-prompt-routing.test.ts`'s "servability veto: does NOT fire for a Codex id..." test — it was missing `});` for both the `withOpenAiEnvCleared` callback and the outer `test()`, which swallowed the next test's `test(...)` opener into the same block and produced a hard parse error (`error: Unexpected end of file`) under `bun test`.
24
+
25
+ ### Verified
26
+ Full suite 2779/2779 pass, typecheck clean — run directly against the working tree immediately after the fix, with no stash operation between fix and verification this time (the actual root cause of v0.8.6 shipping broken).
27
+
28
+ ## [0.8.6] - 2026-07-08
29
+ _Explicit `/model` pin always winning over PromptRouter was correct but opaque: `/route status` showed "routing: on" even while a session pin blocked routing from ever evaluating a prompt, and there was no way back to routed mode short of restarting the session. Also closed a related escalation gap: `routing.enabled` with `roles.smol` unset made ambiguous-prompt LLM escalation silently skip every turn, even when a cheaper credentialed model existed to run the classifier._
30
+
31
+ ### Added
32
+ - **`/route status` pin visibility** (`src/commands/launch/route-slash.ts`) — a new `pinnedModel` field on `RouteSlashCtx` (threaded from `sessionModel` in `launch.ts`) makes an active session pin print `note: model pinned to '<model>' this session — routing will not evaluate any prompt until the pin is cleared (/model auto)` instead of a bare, misleading `routing: on`. Purely additive — the actual routing gate (`!sessionModel` in `runTurn`) is unchanged.
33
+ - **`/model auto` (alias `/model clear`)** (`src/commands/launch/model-slash.ts`, `src/commands/launch.ts`) — releases an explicit `--model`/`/model <id>` pin, resuming per-turn `PromptRouter` evaluation for the rest of the session (subject to `routing.enabled`). `ModelSlashResult.sessionModel` is now 3-state (`undefined`=unchanged, `string`=new pin, `null`=pin explicitly cleared); `launch.ts` converts `null` to `undefined` so `runTurn`'s routing gate reopens. The "explicit pin always wins" contract itself is untouched — this only adds a way to release it.
34
+ - **`routePrompt` escalation fallback when `roles.smol` is unconfigured** (`src/agent/prompt-router.ts`) — instead of unconditionally skipping LLM escalation for ambiguous prompts, `routePrompt` now falls back to `cheapestCredentialed(config)` (the same live `MODEL_CATALOG` lookup trivial-tier auto-select already uses) as the classifier model. Escalation is only skipped (heuristic-only tier, one-time warning) when that fallback also resolves to `defaultModel` or nothing qualifies at all.
35
+ - **`jeo doctor` routing note synced to the same fallback** (`src/commands/doctor.ts`) — the "roles.smol is unset" note now checks `cheapestCredentialed(config)` before firing, so it stops claiming escalation "will never fire" when a real fallback classifier exists.
36
+
37
+ ### Verified
38
+ Full suite 2778/2778 pass, typecheck clean. 10 new tests (`model-slash` ×4, `route-slash` ×2, `prompt-router` ×2, `launch-prompt-routing` ×2). Live tmux smoke test on the real interactive binary: `/route status` → pin note shown → `/model auto` → "Model pin (...) cleared" notice → `/route status` again → note gone, `routing: on (this session)` only.
39
+
9
40
  ## [0.8.5] - 2026-07-08
10
41
  _"딥리서치 다음과같이 페이블5 모델이 응답하게 되는경우를 피하기 위해" — a claude-fable-5 turn hit `Error: Refusal (reasoning_extraction): This request was blocked as it seems to violate Anthropic's Terms of Service.` jeo already had a full reactive recovery ladder (context reset → thinking-artifact strip → guidance strip → fail-fast on a category-shaped refusal), but every rung still resends to the SAME model — a deterministic classifier trip re-refuses identically no matter how much context gets stripped. Deep-researched Anthropic's current docs and found the actual fix: a documented, dedicated **server-side fallback** beta released alongside Fable 5 that retries a safety-classifier decline against a different model inside the SAME request, before the caller ever sees an error._
11
42
 
package/README.ja.md CHANGED
@@ -262,11 +262,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
262
262
  ## 変更履歴 (Changelog)
263
263
 
264
264
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
265
+ - **[0.8.8]** (2026-07-08) — "실동작검증을 통해서 유효성 평가하고 개선해줘" — live-verified the v0.8.5 fallback mechanism using active Claude Code OAuth credentials on this machine (confirmed 200 OK for both Fable 5 and Opus 4.8 via the proxy). Identified two critical improvement vectors during verification: (1) server-side fallback was previously scoped strictly to API-key credentials, leaving OAuth users (who also hit `reasoning_extraction` refusals) with today's reactive-only recovery; (2) the sequential `postAnthropic` retry ladder could throw immediately on a combination of different error types (e.g. temperature error first, then fallback error, then success).
266
+ - **[0.8.7]** (2026-07-08) — Fix-forward for a CI-caught release failure in v0.8.6 (this same session): `test/launch-prompt-routing.test.ts` shipped with a dropped closing brace (a git 3-way merge misaligned on a duplicate `});` line during this session's own working-tree isolation, then a subsequent `git stash push --keep-index` silently reset the working tree to the pre-fix staged content — the fix was applied once, verified once, then invisibly reverted before the commit that actually shipped). The v0.8.6 GitHub Actions release workflow correctly caught the syntax error at `bun test` and failed BEFORE the npm publish step ran; `npm view jeo-code version` confirmed 0.8.5 remained latest throughout — nothing broken ever reached the registry. Per this repo's established pattern for a same-day CI-caught regression (v0.8.0 → v0.8.1), shipping forward rather than retargeting the already-publicly-failed v0.8.6 tag.
267
+ - **[0.8.6]** (2026-07-08) — Explicit `/model` pin always winning over PromptRouter was correct but opaque: `/route status` showed "routing: on" even while a session pin blocked routing from ever evaluating a prompt, and there was no way back to routed mode short of restarting the session. Also closed a related escalation gap: `routing.enabled` with `roles.smol` unset made ambiguous-prompt LLM escalation silently skip every turn, even when a cheaper credentialed model existed to run the classifier.
265
268
  - **[0.8.5]** (2026-07-08) — "딥리서치 다음과같이 페이블5 모델이 응답하게 되는경우를 피하기 위해" — a claude-fable-5 turn hit `Error: Refusal (reasoning_extraction): This request was blocked as it seems to violate Anthropic's Terms of Service.` jeo already had a full reactive recovery ladder (context reset → thinking-artifact strip → guidance strip → fail-fast on a category-shaped refusal), but every rung still resends to the SAME model — a deterministic classifier trip re-refuses identically no matter how much context gets stripped. Deep-researched Anthropic's current docs and found the actual fix: a documented, dedicated **server-side fallback** beta released alongside Fable 5 that retries a safety-classifier decline against a different model inside the SAME request, before the caller ever sees an error.
266
269
  - **[0.8.4]** (2026-07-08) — Follow-up to the 0.8.3 `jeo --tmux` E2E verification, which surfaced two real bugs: (1) the fake `test-anthropic-key` fixture found sitting in the user's REAL `~/.jeo/config.json` turned out to be reproducible — jeo's own test suite could silently write to the real config with no guard, and a prior session's routing tests had done exactly that; (2) the E2E run's TUI showed `'antigravity/gemini-pro-agent' is not in the live antigravity catalog` for a model that WAS live — traced to the note firing on a FAILED provider discovery (timeout/expired OAuth) rather than a genuinely absent model.
267
- - **[0.8.3]** (2026-07-08) — "잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth.
268
- - **[0.8.2]** (2026-07-08) — "프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD.
269
- - **[0.8.1]** (2026-07-08) — Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it.
270
270
 
271
271
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
272
272
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -263,11 +263,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
263
263
  ## 변경 이력 (Changelog)
264
264
 
265
265
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
266
+ - **[0.8.8]** (2026-07-08) — "실동작검증을 통해서 유효성 평가하고 개선해줘" — live-verified the v0.8.5 fallback mechanism using active Claude Code OAuth credentials on this machine (confirmed 200 OK for both Fable 5 and Opus 4.8 via the proxy). Identified two critical improvement vectors during verification: (1) server-side fallback was previously scoped strictly to API-key credentials, leaving OAuth users (who also hit `reasoning_extraction` refusals) with today's reactive-only recovery; (2) the sequential `postAnthropic` retry ladder could throw immediately on a combination of different error types (e.g. temperature error first, then fallback error, then success).
267
+ - **[0.8.7]** (2026-07-08) — Fix-forward for a CI-caught release failure in v0.8.6 (this same session): `test/launch-prompt-routing.test.ts` shipped with a dropped closing brace (a git 3-way merge misaligned on a duplicate `});` line during this session's own working-tree isolation, then a subsequent `git stash push --keep-index` silently reset the working tree to the pre-fix staged content — the fix was applied once, verified once, then invisibly reverted before the commit that actually shipped). The v0.8.6 GitHub Actions release workflow correctly caught the syntax error at `bun test` and failed BEFORE the npm publish step ran; `npm view jeo-code version` confirmed 0.8.5 remained latest throughout — nothing broken ever reached the registry. Per this repo's established pattern for a same-day CI-caught regression (v0.8.0 → v0.8.1), shipping forward rather than retargeting the already-publicly-failed v0.8.6 tag.
268
+ - **[0.8.6]** (2026-07-08) — Explicit `/model` pin always winning over PromptRouter was correct but opaque: `/route status` showed "routing: on" even while a session pin blocked routing from ever evaluating a prompt, and there was no way back to routed mode short of restarting the session. Also closed a related escalation gap: `routing.enabled` with `roles.smol` unset made ambiguous-prompt LLM escalation silently skip every turn, even when a cheaper credentialed model existed to run the classifier.
266
269
  - **[0.8.5]** (2026-07-08) — "딥리서치 다음과같이 페이블5 모델이 응답하게 되는경우를 피하기 위해" — a claude-fable-5 turn hit `Error: Refusal (reasoning_extraction): This request was blocked as it seems to violate Anthropic's Terms of Service.` jeo already had a full reactive recovery ladder (context reset → thinking-artifact strip → guidance strip → fail-fast on a category-shaped refusal), but every rung still resends to the SAME model — a deterministic classifier trip re-refuses identically no matter how much context gets stripped. Deep-researched Anthropic's current docs and found the actual fix: a documented, dedicated **server-side fallback** beta released alongside Fable 5 that retries a safety-classifier decline against a different model inside the SAME request, before the caller ever sees an error.
267
270
  - **[0.8.4]** (2026-07-08) — Follow-up to the 0.8.3 `jeo --tmux` E2E verification, which surfaced two real bugs: (1) the fake `test-anthropic-key` fixture found sitting in the user's REAL `~/.jeo/config.json` turned out to be reproducible — jeo's own test suite could silently write to the real config with no guard, and a prior session's routing tests had done exactly that; (2) the E2E run's TUI showed `'antigravity/gemini-pro-agent' is not in the live antigravity catalog` for a model that WAS live — traced to the note firing on a FAILED provider discovery (timeout/expired OAuth) rather than a genuinely absent model.
268
- - **[0.8.3]** (2026-07-08) — "잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth.
269
- - **[0.8.2]** (2026-07-08) — "프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD.
270
- - **[0.8.1]** (2026-07-08) — Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it.
271
271
 
272
272
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
273
273
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -265,11 +265,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
265
265
  ## Changelog
266
266
 
267
267
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
268
+ - **[0.8.8]** (2026-07-08) — "실동작검증을 통해서 유효성 평가하고 개선해줘" — live-verified the v0.8.5 fallback mechanism using active Claude Code OAuth credentials on this machine (confirmed 200 OK for both Fable 5 and Opus 4.8 via the proxy). Identified two critical improvement vectors during verification: (1) server-side fallback was previously scoped strictly to API-key credentials, leaving OAuth users (who also hit `reasoning_extraction` refusals) with today's reactive-only recovery; (2) the sequential `postAnthropic` retry ladder could throw immediately on a combination of different error types (e.g. temperature error first, then fallback error, then success).
269
+ - **[0.8.7]** (2026-07-08) — Fix-forward for a CI-caught release failure in v0.8.6 (this same session): `test/launch-prompt-routing.test.ts` shipped with a dropped closing brace (a git 3-way merge misaligned on a duplicate `});` line during this session's own working-tree isolation, then a subsequent `git stash push --keep-index` silently reset the working tree to the pre-fix staged content — the fix was applied once, verified once, then invisibly reverted before the commit that actually shipped). The v0.8.6 GitHub Actions release workflow correctly caught the syntax error at `bun test` and failed BEFORE the npm publish step ran; `npm view jeo-code version` confirmed 0.8.5 remained latest throughout — nothing broken ever reached the registry. Per this repo's established pattern for a same-day CI-caught regression (v0.8.0 → v0.8.1), shipping forward rather than retargeting the already-publicly-failed v0.8.6 tag.
270
+ - **[0.8.6]** (2026-07-08) — Explicit `/model` pin always winning over PromptRouter was correct but opaque: `/route status` showed "routing: on" even while a session pin blocked routing from ever evaluating a prompt, and there was no way back to routed mode short of restarting the session. Also closed a related escalation gap: `routing.enabled` with `roles.smol` unset made ambiguous-prompt LLM escalation silently skip every turn, even when a cheaper credentialed model existed to run the classifier.
268
271
  - **[0.8.5]** (2026-07-08) — "딥리서치 다음과같이 페이블5 모델이 응답하게 되는경우를 피하기 위해" — a claude-fable-5 turn hit `Error: Refusal (reasoning_extraction): This request was blocked as it seems to violate Anthropic's Terms of Service.` jeo already had a full reactive recovery ladder (context reset → thinking-artifact strip → guidance strip → fail-fast on a category-shaped refusal), but every rung still resends to the SAME model — a deterministic classifier trip re-refuses identically no matter how much context gets stripped. Deep-researched Anthropic's current docs and found the actual fix: a documented, dedicated **server-side fallback** beta released alongside Fable 5 that retries a safety-classifier decline against a different model inside the SAME request, before the caller ever sees an error.
269
272
  - **[0.8.4]** (2026-07-08) — Follow-up to the 0.8.3 `jeo --tmux` E2E verification, which surfaced two real bugs: (1) the fake `test-anthropic-key` fixture found sitting in the user's REAL `~/.jeo/config.json` turned out to be reproducible — jeo's own test suite could silently write to the real config with no guard, and a prior session's routing tests had done exactly that; (2) the E2E run's TUI showed `'antigravity/gemini-pro-agent' is not in the live antigravity catalog` for a model that WAS live — traced to the note firing on a FAILED provider discovery (timeout/expired OAuth) rather than a genuinely absent model.
270
- - **[0.8.3]** (2026-07-08) — "잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth.
271
- - **[0.8.2]** (2026-07-08) — "프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD.
272
- - **[0.8.1]** (2026-07-08) — Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it.
273
273
 
274
274
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
275
275
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -262,11 +262,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
262
262
  ## 更新日志 (Changelog)
263
263
 
264
264
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
265
+ - **[0.8.8]** (2026-07-08) — "실동작검증을 통해서 유효성 평가하고 개선해줘" — live-verified the v0.8.5 fallback mechanism using active Claude Code OAuth credentials on this machine (confirmed 200 OK for both Fable 5 and Opus 4.8 via the proxy). Identified two critical improvement vectors during verification: (1) server-side fallback was previously scoped strictly to API-key credentials, leaving OAuth users (who also hit `reasoning_extraction` refusals) with today's reactive-only recovery; (2) the sequential `postAnthropic` retry ladder could throw immediately on a combination of different error types (e.g. temperature error first, then fallback error, then success).
266
+ - **[0.8.7]** (2026-07-08) — Fix-forward for a CI-caught release failure in v0.8.6 (this same session): `test/launch-prompt-routing.test.ts` shipped with a dropped closing brace (a git 3-way merge misaligned on a duplicate `});` line during this session's own working-tree isolation, then a subsequent `git stash push --keep-index` silently reset the working tree to the pre-fix staged content — the fix was applied once, verified once, then invisibly reverted before the commit that actually shipped). The v0.8.6 GitHub Actions release workflow correctly caught the syntax error at `bun test` and failed BEFORE the npm publish step ran; `npm view jeo-code version` confirmed 0.8.5 remained latest throughout — nothing broken ever reached the registry. Per this repo's established pattern for a same-day CI-caught regression (v0.8.0 → v0.8.1), shipping forward rather than retargeting the already-publicly-failed v0.8.6 tag.
267
+ - **[0.8.6]** (2026-07-08) — Explicit `/model` pin always winning over PromptRouter was correct but opaque: `/route status` showed "routing: on" even while a session pin blocked routing from ever evaluating a prompt, and there was no way back to routed mode short of restarting the session. Also closed a related escalation gap: `routing.enabled` with `roles.smol` unset made ambiguous-prompt LLM escalation silently skip every turn, even when a cheaper credentialed model existed to run the classifier.
265
268
  - **[0.8.5]** (2026-07-08) — "딥리서치 다음과같이 페이블5 모델이 응답하게 되는경우를 피하기 위해" — a claude-fable-5 turn hit `Error: Refusal (reasoning_extraction): This request was blocked as it seems to violate Anthropic's Terms of Service.` jeo already had a full reactive recovery ladder (context reset → thinking-artifact strip → guidance strip → fail-fast on a category-shaped refusal), but every rung still resends to the SAME model — a deterministic classifier trip re-refuses identically no matter how much context gets stripped. Deep-researched Anthropic's current docs and found the actual fix: a documented, dedicated **server-side fallback** beta released alongside Fable 5 that retries a safety-classifier decline against a different model inside the SAME request, before the caller ever sees an error.
266
269
  - **[0.8.4]** (2026-07-08) — Follow-up to the 0.8.3 `jeo --tmux` E2E verification, which surfaced two real bugs: (1) the fake `test-anthropic-key` fixture found sitting in the user's REAL `~/.jeo/config.json` turned out to be reproducible — jeo's own test suite could silently write to the real config with no guard, and a prior session's routing tests had done exactly that; (2) the E2E run's TUI showed `'antigravity/gemini-pro-agent' is not in the live antigravity catalog` for a model that WAS live — traced to the note firing on a FAILED provider discovery (timeout/expired OAuth) rather than a genuinely absent model.
267
- - **[0.8.3]** (2026-07-08) — "잘못된 모델 설정하는 경우가 있는데" — the static Antigravity catalog had drifted badly from the LIVE Cloud Code Assist agent backend, and several current wire ids actively LIE about their tier: `gemini-3-flash-agent` is really "Gemini 3.5 Flash (High)" (the flagship agent model, which the name-suffix heuristic filed under the SMALL flash tier), `gemini-pro-agent` is really "Gemini 3.1 Pro (High)" (the code-agent model), and `gemini-3.1-pro-high` is DEPRECATED by the backend in favor of `gemini-pro-agent`. Probed the real `fetchAvailableModels` response (displayNames + agentModelSorts + deprecatedModelIds) and made it the single source of truth.
268
- - **[0.8.2]** (2026-07-08) — "프롬프트라우팅이 api 입력을 요구하면서 동작안해" — prompt routing kept demanding an API key even when the user was already logged in via OAuth (GPT showed the same symptom). Root cause: every routing gate was PROVIDER-level ("any stored credential exists") while OAuth credentials are MODEL-level (ChatGPT OAuth serves only Codex ids, Kimi OAuth only the Kimi Code catalog, gemini OAuth nothing at all since the Cloud Code Assist masquerade was removed) — so routing happily picked models the stored login could never serve, and the turn then failed at call time asking for `<PROVIDER>_API_KEY`. Also restored a missing export that had broken `bun run typecheck` on HEAD.
269
- - **[0.8.1]** (2026-07-08) — Fix-forward for a functional regression this same session shipped in v0.8.0. This repo's working tree is shared with a concurrent interactive session; `git add`-ing `src/ai/provider-status.ts` for v0.8.0's routing-veto-gate fix also swept up that session's already-modified, uncommitted copy of the SAME file (containing an unrelated design change: gemini OAuth alone no longer serving `google/gemini-*` models) without its companion piece in `src/auth/flows/index.ts` — so v0.8.0 shipped with a test suite (`provider-status.test.ts`) asserting behavior whose supporting data flag never landed. CI caught this immediately post-release: 2 failing tests on the exact tagged commit. Investigated, confirmed the missing piece was real (and independently observable live via `jeo doctor` before this session had made ANY of its own edits — not a stray/speculative change), and completed it.
270
270
 
271
271
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
272
272
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.8.5",
3
+ "version": "0.8.8",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -515,16 +515,30 @@ export async function routePrompt(
515
515
 
516
516
  if (heuristic.confidence < threshold) {
517
517
  const smolModel = resolveRoleModel("smol", config);
518
- if (smolModel === config.defaultModel) {
519
- // roles.smol not configured -> "escalate to a cheap model" would actually call
520
- // defaultModel (expensive) — the exact paradox the design doc identifies. Skip
521
- // escalation, keep the heuristic result, and surface a one-time warning.
522
- warning = warnOnce(
523
- SMOL_UNCONFIGURED_WARNING_KEY,
524
- "[route] confidence below threshold but roles.smol is not configured — skipping LLM escalation, using heuristic tier (set roles.smol to enable escalation)",
525
- );
518
+ let classifierModel: string | undefined;
519
+ if (smolModel !== config.defaultModel) {
520
+ classifierModel = smolModel;
526
521
  } else {
527
- const escalated = await escalateToLlm(prompt, smolModel, opts.signal);
522
+ // roles.smol not configured -> "escalate to a cheap model" would otherwise call
523
+ // defaultModel (expensive) — the exact paradox the design doc identifies. Rather
524
+ // than giving up outright, fall back to the cheapest CREDENTIALED catalog model
525
+ // (same live `MODEL_CATALOG` lookup `resolveTierModel`'s trivial-tier auto-select
526
+ // already uses) so an ambiguous per-turn prompt still gets a real LLM classification
527
+ // pass whenever a genuinely cheaper option exists. Only skip escalation entirely
528
+ // (heuristic-only, one-time warning) when even that fallback resolves to
529
+ // `defaultModel` itself (nothing cheaper is credentialed) or nothing qualifies at all.
530
+ const fallback = cheapestCredentialed(config);
531
+ if (fallback && fallback !== config.defaultModel) {
532
+ classifierModel = fallback;
533
+ } else {
534
+ warning = warnOnce(
535
+ SMOL_UNCONFIGURED_WARNING_KEY,
536
+ "[route] confidence below threshold but roles.smol is not configured and no cheaper credentialed model was found — skipping LLM escalation, using heuristic tier (set roles.smol to enable escalation)",
537
+ );
538
+ }
539
+ }
540
+ if (classifierModel) {
541
+ const escalated = await escalateToLlm(prompt, classifierModel, opts.signal);
528
542
  if (escalated) {
529
543
  tier = escalated;
530
544
  source = "llm";
@@ -137,14 +137,14 @@ function parseAnthropicVersion(model: string): { kind: "opus" | "sonnet" | "haik
137
137
  * artifact strip, guidance strip) because every rung resent the SAME model. Scoped to
138
138
  * `claude-fable-5` (the model this beta's docs are written against — Mythos-5's
139
139
  * invite-only status makes its fallback support unconfirmed) on a DIRECT api.anthropic.com
140
- * API-key call only: the beta's request/response shape is undocumented for the
141
- * Claude-Code OAuth cloak or a custom baseUrl (Anthropic-compatible third-party hosts),
142
- * so those paths keep today's reactive (post-refusal) recovery unchanged.
140
+ * API-key or Claude-Code OAuth call: the beta's request/response shape is undocumented for
141
+ * a custom baseUrl (Anthropic-compatible third-party hosts), so those paths keep today's
142
+ * reactive (post-refusal) recovery unchanged.
143
143
  * `JEO_ANTHROPIC_FALLBACK=0` opts out entirely. */
144
144
  const FALLBACK_BETA = "server-side-fallback-2026-06-01";
145
145
  function anthropicFallbackModels(model: string, credential: Credential, baseUrl: string | undefined): string[] {
146
146
  if (jeoEnv("ANTHROPIC_FALLBACK") === "0") return [];
147
- if (credential.kind !== "api_key" || baseUrl) return [];
147
+ if (baseUrl) return [];
148
148
  const v = parseAnthropicVersion(model);
149
149
  if (!v || v.kind !== "fable") return [];
150
150
  return ["claude-opus-4-8"];
@@ -397,44 +397,48 @@ async function postAnthropic(
397
397
  credential: Credential,
398
398
  stream: boolean,
399
399
  ): Promise<Response> {
400
- const send = (includeTemperature: boolean, stripArtifacts = false, disableFallback = false) => {
401
- const { url, headers, body } = anthropicRequest(messages, options, credential, stream, includeTemperature, stripArtifacts, disableFallback);
402
- return fetch(url, { method: "POST", headers, body, signal: options.signal });
403
- };
400
+ let includeTemperature = true;
401
+ let stripArtifacts = false;
402
+ let disableFallback = false;
403
+
404
+ for (let attempt = 1; attempt <= 4; attempt++) {
405
+ const { url, headers, body } = anthropicRequest(
406
+ messages,
407
+ options,
408
+ credential,
409
+ stream,
410
+ includeTemperature,
411
+ stripArtifacts,
412
+ disableFallback,
413
+ );
414
+ const response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
415
+ if (response.ok) return response;
404
416
 
405
- let response = await send(true);
406
- if (response.ok) return response;
417
+ const detail = await response.text().catch(() => "");
418
+ if (isDeprecatedTemperatureError(response.status, detail) && includeTemperature) {
419
+ includeTemperature = false;
420
+ continue;
421
+ }
422
+ if (isReasoningArtifactError(response.status, detail) && !stripArtifacts) {
423
+ stripArtifacts = true;
424
+ continue;
425
+ }
426
+ if (isFallbackUnsupportedError(response.status, detail) && !disableFallback) {
427
+ disableFallback = true;
428
+ continue;
429
+ }
407
430
 
408
- const detail = await response.text().catch(() => "");
409
- if (isDeprecatedTemperatureError(response.status, detail)) {
410
- response = await send(false);
411
- if (response.ok) return response;
412
- throw await providerHttpError("Anthropic", response, stream ? "(stream)" : undefined);
413
- }
414
- // Fail-safe: a rejected replay artifact → retry once with artifacts stripped (plain history).
415
- if (isReasoningArtifactError(response.status, detail)) {
416
- response = await send(true, true);
417
- if (response.ok) return response;
418
- throw await providerHttpError("Anthropic", response, stream ? "(stream)" : undefined);
419
- }
420
- // Fail-safe: the account/endpoint doesn't support server-side fallback (or the
421
- // named fallback model rejected THIS request's shape) → retry once without it.
422
- if (isFallbackUnsupportedError(response.status, detail)) {
423
- response = await send(true, false, true);
424
- if (response.ok) return response;
425
- throw await providerHttpError("Anthropic", response, stream ? "(stream)" : undefined);
431
+ // Unrecoverable error, or all retries for the active error type spent
432
+ const enrichedDetail = response.status === 429 ? `${detail}${anthropicRateLimitEvidence(response.headers)}` : detail;
433
+ throw new ProviderHttpError(
434
+ "Anthropic",
435
+ response.status,
436
+ enrichedDetail,
437
+ stream ? "(stream)" : undefined,
438
+ parseRetryAfter(response.headers.get("retry-after")) ?? parseRetryFromBody(detail),
439
+ );
426
440
  }
427
-
428
- // 429s carry their rate-limit headers in the DETAIL so isUsageLimitError /
429
- // friendlyProviderError can classify on the full evidence (gjc parity).
430
- const enrichedDetail = response.status === 429 ? `${detail}${anthropicRateLimitEvidence(response.headers)}` : detail;
431
- throw new ProviderHttpError(
432
- "Anthropic",
433
- response.status,
434
- enrichedDetail,
435
- stream ? "(stream)" : undefined,
436
- parseRetryAfter(response.headers.get("retry-after")) ?? parseRetryFromBody(detail),
437
- );
441
+ throw new Error("Anthropic request failed: maximum retries reached without response");
438
442
  }
439
443
 
440
444
  /** Anthropic usage: with prompt caching the input splits into uncached + cache read +
@@ -665,12 +669,20 @@ function headersFor(credential: Credential, stream: boolean, model: string, base
665
669
  "anthropic-version": "2023-06-01",
666
670
  };
667
671
  }
668
- return {
672
+ const headers: Record<string, string> = {
669
673
  "content-type": "application/json",
670
674
  authorization: `Bearer ${credential.token}`,
671
675
  "anthropic-version": "2023-06-01",
672
676
  ...claudeCodeOAuthHeaders(stream, model),
673
677
  };
678
+ if (!disableFallback && anthropicFallbackModels(model, credential, baseUrl).length) {
679
+ const betas = headers["anthropic-beta"] ? headers["anthropic-beta"].split(",") : [];
680
+ if (!betas.includes(FALLBACK_BETA)) {
681
+ betas.push(FALLBACK_BETA);
682
+ headers["anthropic-beta"] = betas.join(",");
683
+ }
684
+ }
685
+ return headers;
674
686
  }
675
687
  if (credential.kind === "api_key") {
676
688
  const betas = ANTHROPIC_API_KEY_BETA.slice();
@@ -8,7 +8,7 @@ import { meter } from "../tui/components/meter";
8
8
  import { size } from "../tui/terminal";
9
9
  import chalk from "chalk";
10
10
  import { extractChatgptAccountId, CODEX_RESPONSES_URL } from "../ai/providers/openai-responses";
11
- import { resolveTierModel, tierModelPool } from "../agent/prompt-router";
11
+ import { resolveTierModel, tierModelPool, cheapestCredentialed } from "../agent/prompt-router";
12
12
 
13
13
  interface ProbeResult {
14
14
  status: "ok" | "fail" | "skipped";
@@ -173,20 +173,28 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
173
173
  const ollamaBase = config.ollamaBaseUrl ?? "http://localhost:11434";
174
174
 
175
175
  // Routing diagnostic (design doc §7 risk #2): routing.enabled without roles.smol
176
- // means routePrompt()'s LLM-escalation path silently never fires — resolveRoleModel
177
- // falls back to defaultModel, which is exactly the "cheap escalation call is actually
178
- // the expensive default model" paradox routePrompt() itself guards against at runtime
179
- // (see prompt-router.ts's warnOnce/SMOL_UNCONFIGURED_WARNING_KEY). Surface it here
180
- // proactively, at onboarding time, instead of only mid-session on the first qualifying
181
- // turn.
176
+ // used to mean routePrompt()'s LLM-escalation path silently never fired. It no
177
+ // longer unconditionally gives up — it now falls back to the cheapest CREDENTIALED
178
+ // catalog model as the ambiguous-prompt classifier (same live MODEL_CATALOG lookup
179
+ // resolveTierModel's trivial-tier auto-select already uses), and only skips
180
+ // escalation entirely when that fallback ALSO resolves to defaultModel (nothing
181
+ // cheaper is credentialed) or nothing qualifies at all. Mirror that exact condition
182
+ // here so this note stays accurate instead of unconditionally claiming escalation
183
+ // "will never fire" — see prompt-router.ts's warnOnce/SMOL_UNCONFIGURED_WARNING_KEY
184
+ // for the matching runtime-side guard. Surface it here proactively, at onboarding
185
+ // time, instead of only mid-session on the first qualifying turn.
182
186
  const routingEnabled = !!config.routing?.enabled;
183
187
  const smolConfigured = !!config.roles?.smol;
184
188
  const routingNotes: string[] = [];
185
189
  if (routingEnabled && !smolConfigured) {
186
- routingNotes.push(
187
- "routing is enabled but roles.smol is unset — LLM escalation for ambiguous prompts will never fire; heuristic-only tier resolution applies every turn. Set roles.smol to enable escalation.",
188
- );
190
+ const escalationFallback = cheapestCredentialed(config);
191
+ if (!escalationFallback || escalationFallback === config.defaultModel) {
192
+ routingNotes.push(
193
+ "routing is enabled but roles.smol is unset — LLM escalation for ambiguous prompts will never fire; heuristic-only tier resolution applies every turn. Set roles.smol to enable escalation.",
194
+ );
195
+ }
189
196
  }
197
+
190
198
  // Proactive credential-readiness check for every model routing could actually pick —
191
199
  // mirrors launch.ts's runTurn per-turn veto gate (v0.7.51) but surfaced at onboarding/
192
200
  // doctor time instead of only reactively on a session's first qualifying turn. Catches
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * `/model` slash-command handler extracted from launch.ts.
3
3
  *
4
- * Handles `/model [id|#N|save [id]|thinking <level>|subagent <role> [model|#N]]`
4
+ * Handles `/model [id|#N|auto|clear|save [id]|thinking <level>|subagent <role> [model|#N]]`
5
5
  * (default: interactive live-model picker on a TTY, else prints the current
6
6
  * model + a numbered pick list). This block shares mutable REPL state with
7
+
7
8
  * still-inline commands (`/agents`, `/roles`, `/provider` also read/write
8
9
  * `lastPickIndex`; `/thinking` reads/writes `sessionThinking`), so the caller
9
10
  * passes the current values in via an explicit context object and reads back
@@ -60,14 +61,18 @@ export interface ModelSlashCtx {
60
61
  }
61
62
 
62
63
  export interface ModelSlashResult {
63
- /** Present only when changed. */
64
- sessionModel?: string;
64
+ /** Present only when changed. `null` means the pin was explicitly CLEARED
65
+ * (`/model auto`/`/model clear`) — distinct from `undefined` ("unchanged"),
66
+ * since `sessionModel` itself must become `undefined` in the caller so
67
+ * `runTurn`'s `!sessionModel` routing gate re-opens for the rest of the session. */
68
+ sessionModel?: string | null;
65
69
  /** Present only when changed. */
66
70
  sessionThinking?: ThinkLevel;
67
71
  /** Present only when the block builds/refreshes a pick list. */
68
72
  lastPickIndex?: PickEntry[];
69
73
  }
70
74
 
75
+
71
76
  /**
72
77
  * Handle `/model [id|#N|save|thinking <level>|subagent <role> ...]`. Extracted
73
78
  * verbatim from launch.ts's inline REPL branch — the caller keeps the
@@ -91,6 +96,26 @@ export async function runModelSlash(input: string, ctx: ModelSlashCtx): Promise<
91
96
  });
92
97
 
93
98
  let arg = input.substring(6).trim();
99
+ // `/model auto` (or `/model clear`) releases an explicit pin (set by a prior
100
+ // `--model`/`/model <id>`), resuming per-turn PromptRouter evaluation for the
101
+ // rest of this session (subject to routing.enabled) — the release valve for
102
+ // the pin-always-wins contract, which otherwise gives no way back to routed
103
+ // mode short of restarting `jeo launch` with no `--model`. Distinct from
104
+ // `/model save`, which PERSISTS a pin; this clears the session-local one only
105
+ // (never touches ~/.jeo/config.json's defaultModel).
106
+ if (arg === "auto" || arg === "clear") {
107
+ if (!sessionModel) {
108
+ console.log("No model pin is active this session — routing already evaluates every turn (if routing.enabled).");
109
+ return result();
110
+ }
111
+ console.log(`Model pin ('${sessionModel}') cleared — routing resumes evaluating every turn (if routing.enabled); falls back to '${defaultModel}' whenever routing is off or produces no decision.`);
112
+ return {
113
+ sessionModel: null,
114
+ ...(sessionThinkingChanged ? { sessionThinking } : {}),
115
+ ...(lastPickIndexChanged ? { lastPickIndex } : {}),
116
+ };
117
+ }
118
+
94
119
  // `/model save [id]` → persist the (session or given) model as the config default.
95
120
  if (arg === "save" || arg.startsWith("save ")) {
96
121
  let toSave = arg.slice(4).trim();
@@ -23,8 +23,14 @@ export interface RouteSlashCtx {
23
23
  /** `turnConfig.routing?.enabled ?? false`, read fresh by the caller. */
24
24
  routingConfigEnabled: boolean;
25
25
  lastRouteDecision: RouteDecision | { note: string } | null;
26
+ /** The session's explicit model pin (`sessionModel`, from `--model`/`/model`),
27
+ * if any. Threaded through purely for `/route status`'s visibility note below —
28
+ * never changes routing's actual gating logic (`!sessionModel` in launch.ts's
29
+ * `runTurn` stays the single source of truth for whether routing engages). */
30
+ pinnedModel?: string;
26
31
  }
27
32
 
33
+
28
34
  export interface RouteSlashResult {
29
35
  /** Present only when changed by "on"/"off". */
30
36
  sessionRouteOverride?: boolean | undefined;
@@ -69,9 +75,17 @@ export function runRouteSlash(input: string, ctx: RouteSlashCtx): RouteSlashResu
69
75
  if (isRealDecision(ctx.lastRouteDecision)) {
70
76
  lines.push(`last decision: ${ctx.lastRouteDecision.tier} → ${ctx.lastRouteDecision.model} (${ctx.lastRouteDecision.source}: ${ctx.lastRouteDecision.signals.join(", ") || "none"}, confidence ${ctx.lastRouteDecision.confidence.toFixed(2)})`);
71
77
  }
78
+ // A model pin (`--model`/`/model`) unconditionally blocks routing regardless of
79
+ // this session's on/off toggle — surface that explicitly here, since "routing: on"
80
+ // read alone is misleading if it will never actually evaluate a prompt. Use
81
+ // /model auto to release the pin and let routing resume.
82
+ if (ctx.pinnedModel) {
83
+ lines.push(`note: model pinned to '${ctx.pinnedModel}' this session — routing will not evaluate any prompt until the pin is cleared (/model auto)`);
84
+ }
72
85
  return { lines };
73
86
  }
74
87
 
88
+
75
89
  if (sub === "on") {
76
90
  return { sessionRouteOverride: true, lines: ["routing: on (this session)"] };
77
91
  }
@@ -4166,7 +4166,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4166
4166
  isTTY: !!(process.stdin.isTTY && process.stdout.isTTY),
4167
4167
  getLiveModels, applyPickedModelWithTarget, persistSessionModel, pickLiveProviderModel,
4168
4168
  });
4169
- if (modelResult.sessionModel !== undefined) sessionModel = modelResult.sessionModel;
4169
+ // `null` (from `/model auto`/`/model clear`) means "release the pin", NOT
4170
+ // "unchanged" — `sessionModel` must become `undefined` so `runTurn`'s
4171
+ // `!sessionModel` routing gate re-opens for the rest of this session.
4172
+ if (modelResult.sessionModel !== undefined) sessionModel = modelResult.sessionModel === null ? undefined : modelResult.sessionModel;
4170
4173
  if (modelResult.sessionThinking !== undefined) sessionThinking = modelResult.sessionThinking;
4171
4174
  if (modelResult.lastPickIndex !== undefined) lastPickIndex = modelResult.lastPickIndex;
4172
4175
  continue;
@@ -4177,6 +4180,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4177
4180
  sessionRouteOverride,
4178
4181
  routingConfigEnabled: !!cfgNow.routing?.enabled,
4179
4182
  lastRouteDecision,
4183
+ pinnedModel: sessionModel,
4180
4184
  });
4181
4185
  if (routeResult.sessionRouteOverride !== undefined) sessionRouteOverride = routeResult.sessionRouteOverride;
4182
4186
  for (const line of routeResult.lines) console.log(line);