jeo-code 0.7.52 → 0.7.54

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,28 @@ 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.7.54] - 2026-07-06
10
+ _Root-cause fix for a live-reported GPT-5.5 (Codex/ChatGPT OAuth backend) failure: "Error: stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted". Deep-dive traced this to a REGRESSION in v0.7.42 — a separate, correct fix for a genuinely-different bug (a connected-but-never-terminating stream) silently flipped `streamMaxMs()`'s default from opt-in-off to an ALWAYS-ON 300s overall wall-clock cap, which then killed any HIGH/XHIGH-reasoning-effort completion (GPT-5.5/o3-class) whose ACTIVELY-emitting generation legitimately ran past 5 minutes — OpenAI's own guidance states xhigh trades latency for depth by design. The non-streaming `call()` path (used by every subagent, compaction, and goal-verify call, since none wire `onModelStream`) carried the IDENTICAL bug with zero idle/activity tracking at all, exposed under a different, unhelpful raw `DOMException: The operation timed out.` message instead._
11
+
12
+ ### Fixed
13
+ - **`streamMaxMs()`/`callTimeoutMs()` shared default raised 300s → 30min** (`src/ai/model-manager.ts`'s `DEFAULT_CALL_TIMEOUT_MS`) — rather than reverting to opt-in-off (which would reopen a real gap for direct non-turn-wrapped callers like `jeo chat`, since `JEO_TURN_MAX_MS`'s turn-level stall-budget backstop only wraps `runAgentLoop` steps), the shared ceiling now matches `turnMaxMs()`'s own already-vetted 30min default AND the OBSERVED ~20-30min infra-side connection-duration cap on OpenAI's Codex/ChatGPT backend (a real boundary already handled elsewhere in this file as a retryable mid-stream socket close) — a real, evidenced ceiling, not an arbitrary pick. `JEO_STREAM_MAX_MS`/`JEO_CALL_TIMEOUT_MS` env overrides (including `0` to disable the overall stream deadline) are unchanged. The per-chunk idle watchdog (`JEO_STREAM_IDLE_MS`, still 300s) is untouched — this fix only raises the OVERALL ceiling that fires despite continuous activity, not the dead-stream detector.
14
+ - **`friendlyProviderError` now names the exact env var to raise** for both failure surfaces instead of a bare timeout string — a non-streaming `TimeoutError`/`"the operation timed out"` DOMException points at `JEO_CALL_TIMEOUT_MS`, and the stream overall-deadline message points at `JEO_STREAM_MAX_MS` (mentioning `0` disables it), both noting this is expected behavior for a deliberately long HIGH/XHIGH reasoning completion rather than an opaque failure.
15
+
16
+ ### Verified
17
+ - `bun run typecheck` clean; targeted suite (`round-b`, `provider-error`, `provider-error-taxonomy`, `cycle-and-turn-budget`, `retry`, `engine`, `transient-network-recovery`, `refusal-recovery`) 119/119 pass; full suite 2665/2665 pass across 280 files.
18
+ - `test/round-b.test.ts`: new regression test proves an ACTIVELY-emitting stream running past the OLD 300s threshold completes normally instead of being force-aborted (compressed-timescale reproduction of the exact reported failure mode); `streamMaxMs`/`callTimeoutMs` default-value tests updated to the new 30min figure; existing keepalive-forever-stream and healthy-stream tests re-confirmed unaffected.
19
+ - `test/provider-error.test.ts` (+2): a bare `TimeoutError` DOMException maps to `JEO_CALL_TIMEOUT_MS` guidance (raw DOMException text replaced); the stream overall-deadline message maps to `JEO_STREAM_MAX_MS` guidance including the disable-via-0 hint.
20
+ - `docs/deep-dive-stream-idle.md` gained a follow-up section documenting this regression's exact causal chain against the original (now partially superseded) investigation, so the "why not an overall cap" reasoning that got silently reversed in v0.7.42 stays discoverable.
21
+
22
+ ## [0.7.53] - 2026-07-06
23
+ _Verified PromptRouter end-to-end (63 pre-existing tests re-run, all passing; a live `jeo doctor --json`/human-output smoke test confirms real wiring, not just mocks) and closed the one genuine remaining gap: the v0.7.51 per-turn credential-readiness veto protected a running session reactively, but nothing caught a misconfigured `roles.smol`/`roles.slow`/`routing.tiers.*.model` proactively at setup time._
24
+
25
+ ### Added
26
+ - **`jeo doctor` proactive routing-credential diagnostic** (`src/commands/doctor.ts`) — for every tier PromptRouter could actually route to (`routing.tiers.trivial.model` or `roles.smol`, `routing.tiers.standard.model` when explicitly set, `routing.tiers.complex.model` or `roles.slow`), doctor now resolves the model's provider and checks credential readiness with the SAME `describeProvider` call `launch.ts`'s runtime gate uses — surfacing "routing.tiers.\<tier\> resolves to '\<model\>' (\<provider\>) which has no usable credential" at `jeo doctor` time instead of only reactively mid-session on the first qualifying turn. `standard` is only checked when explicitly configured, since its unconfigured fallback is `defaultModel`, already covered by the existing provider-connectivity probes.
27
+
28
+ ### Changed
29
+ - **`jeo doctor --json`'s `routing` block: singular `note` → `notes` array** — the prior shape could only ever represent the one "roles.smol unconfigured" case; the new `notes: string[]` (omitted when empty, same as before) represents zero, one, or several simultaneous routing diagnostics (e.g. `roles.smol` set to an uncredentialed provider AND `routing.tiers.complex` also uncredentialed) without collapsing them into one string. Purely additive to `jeo doctor`'s informational output — never affects the `ready`/`--strict` exit code, matching the existing routing-diagnostic contract.
30
+
9
31
  ## [0.7.52] - 2026-07-06
10
32
  _Two TUI visibility asks answered directly ("어느 모델이 라우팅됐는지 상시 표시" — persistently show which model routing picked, and "각 서브에이전트의 사고 과정도 보이게" — surface each subagent's own thinking) plus a Ponytail-mode minimal-code audit pass across six files that found one genuine correctness bug and five instances of dead/unreachable code._
11
33
 
package/README.ja.md CHANGED
@@ -78,8 +78,8 @@ jeo --tmux # 独立した tmux セッションで実行
78
78
  | `/model` · `/provider` | モデル/プロバイダ選択; `/model` でデフォルト/ロールバッジ、Ralph 風の入れ子ロール・thinking 選択、OpenAI Codex ロールプリセットを一つの流れで設定 |
79
79
  | `/provider login <name>` · `/logout` | 入力欄から OAuth ログイン/ログアウト |
80
80
  | `/agents [role]` · `/subagent` | ロール別(executor/planner/architect/critic)モデル・thinking・ステップ設定 |
81
- | `/thinking [level]` | デフォルト推論予算(minimal…xhigh)の表示/設定 |
82
- | `/fast [on|off|status]` | 現在のモデルが minimal/low 推論をサポートする場合に fast thinking モードを切替 |
81
+ | `/thinking [level]` | デフォルト推論予算(low…xhigh)の表示/設定 |
82
+ | `/fast [on|off|status]` | 現在のモデルが low 推論をサポートする場合に fast thinking モードを切替 |
83
83
  | `/skill` · `$<skill> [intent]` | ワークフロースキルの一覧/実行(`$team "task"` 形式) |
84
84
  | `/view` · `/diff` · `/find` · `/search` | コード表示、git diff、ファイル/パターン検索 |
85
85
  | `/new` · `/resume` · `/sessions` | セッション管理 |
@@ -259,11 +259,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
259
259
  ## 変更履歴 (Changelog)
260
260
 
261
261
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
262
+ - **[0.7.54]** (2026-07-06) — Root-cause fix for a live-reported GPT-5.5 (Codex/ChatGPT OAuth backend) failure: "Error: stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted". Deep-dive traced this to a REGRESSION in v0.7.42 — a separate, correct fix for a genuinely-different bug (a connected-but-never-terminating stream) silently flipped `streamMaxMs()`'s default from opt-in-off to an ALWAYS-ON 300s overall wall-clock cap, which then killed any HIGH/XHIGH-reasoning-effort completion (GPT-5.5/o3-class) whose ACTIVELY-emitting generation legitimately ran past 5 minutes — OpenAI's own guidance states xhigh trades latency for depth by design. The non-streaming `call()` path (used by every subagent, compaction, and goal-verify call, since none wire `onModelStream`) carried the IDENTICAL bug with zero idle/activity tracking at all, exposed under a different, unhelpful raw `DOMException: The operation timed out.` message instead.
263
+ - **[0.7.53]** (2026-07-06) — Verified PromptRouter end-to-end (63 pre-existing tests re-run, all passing; a live `jeo doctor --json`/human-output smoke test confirms real wiring, not just mocks) and closed the one genuine remaining gap: the v0.7.51 per-turn credential-readiness veto protected a running session reactively, but nothing caught a misconfigured `roles.smol`/`roles.slow`/`routing.tiers.*.model` proactively at setup time.
262
264
  - **[0.7.52]** (2026-07-06) — Two TUI visibility asks answered directly ("어느 모델이 라우팅됐는지 상시 표시" — persistently show which model routing picked, and "각 서브에이전트의 사고 과정도 보이게" — surface each subagent's own thinking) plus a Ponytail-mode minimal-code audit pass across six files that found one genuine correctness bug and five instances of dead/unreachable code.
263
265
  - **[0.7.51]** (2026-07-06) — Closes a real correctness gap the user directly identified by asking "does routing only ever pick from models the user can actually use?" — the honest answer at v0.7.50 was no: `routePrompt` resolves a tier's model purely from config (`routing.tiers`/`roles.smol`/`roles.slow`/`defaultModel`) and never checks whether that model's provider has a usable credential. A user could configure `roles.smol`/`roles.slow` to a provider they never logged into (or whose OAuth token expired since setup), and routing would silently turn a WORKING session into a FAILING turn — `resolveCall` throwing `"No credential for provider …"` — the instant it engaged, even though the turn would have succeeded unrouted. Routing is opt-in specifically so it can never make things worse than routing being off; this closes that hole.
264
266
  - **[0.7.50]** (2026-07-06) — Design-doc audit of v0.7.47's PromptRouter, dispatched as two parallel subagents (independent re-verification + gap implementation) against the original Korean-language design report. The core proposal was confirmed shipped correctly (heuristic classifier, escalation, `/model`-always-wins, tool-selection isolation — all independently re-derived from git history and grep evidence, not just re-read from the doc). Two gaps the design doc itself had flagged as open questions (§7 risks #2 and #4) were real and are fixed here; a third finding (an internal contradiction in the design doc's own §3.2 vs §3.4 text on image-attachment gating — the SHIPPED code was already correct, only the doc's prose disagreed with itself) required no code change.
265
- - **[0.7.49]** (2026-07-06) — gjc Telegram-daemon parity, Tier 2 (follow-up to 0.7.37/0.7.38/0.7.48's subagent-visibility-only daemon): adds a dynamic, PER-SESSION forum topic that mirrors that session's OWN activity — not just its subagents — and lets a Telegram reply steer it directly. Opt-in via `notifications.telegram.perSessionTopics`; the flat/global `topicId` path from 0.7.48 is UNCHANGED when it's off, so existing setups see zero regression.
266
- - **[0.7.48]** (2026-07-06) — gjc Telegram-daemon parity, phase 2 (follow-up to 0.7.37/0.7.38's baseline subagent-visibility daemon, which deliberately scoped OUT forum topics, inline keyboards, and image attachments — see that entry): the daemon now speaks gjc's richer notification surface instead of the plain-text-only baseline.
267
267
 
268
268
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
269
269
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -79,8 +79,8 @@ jeo --tmux # 독립 tmux 세션에서 실행
79
79
  | `/model` · `/provider` | 모델/프로바이더 선택; `/model`에서 기본/역할 배지, Ralph식 하위 리스트 역할·thinking 선택, OpenAI Codex 역할 프리셋을 한 흐름으로 설정 |
80
80
  | `/provider login <name>` · `/logout` | 입력창에서 OAuth 로그인/로그아웃 |
81
81
  | `/agents [role]` · `/subagent` | 역할별(executor/planner/architect/critic) 모델·thinking·스텝 구성 |
82
- | `/thinking [level]` | 기본 추론 예산(minimal…xhigh) 조회/설정 |
83
- | `/fast [on|off|status]` | 현재 모델이 minimal/low 추론을 지원하면 fast thinking 모드를 켜고 끔 |
82
+ | `/thinking [level]` | 기본 추론 예산(low…xhigh) 조회/설정 |
83
+ | `/fast [on|off|status]` | 현재 모델이 low 추론을 지원하면 fast thinking 모드를 켜고 끔 |
84
84
  | `/skill` · `$<skill> [intent]` | 워크플로 스킬 목록/실행(`$team "작업"` 스타일) |
85
85
  | `/view` · `/diff` · `/find` · `/search` | 코드 보기, git diff, 파일/패턴 검색 |
86
86
  | `/new` · `/resume` · `/sessions` | 세션 관리 |
@@ -260,11 +260,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
260
260
  ## 변경 이력 (Changelog)
261
261
 
262
262
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
263
+ - **[0.7.54]** (2026-07-06) — Root-cause fix for a live-reported GPT-5.5 (Codex/ChatGPT OAuth backend) failure: "Error: stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted". Deep-dive traced this to a REGRESSION in v0.7.42 — a separate, correct fix for a genuinely-different bug (a connected-but-never-terminating stream) silently flipped `streamMaxMs()`'s default from opt-in-off to an ALWAYS-ON 300s overall wall-clock cap, which then killed any HIGH/XHIGH-reasoning-effort completion (GPT-5.5/o3-class) whose ACTIVELY-emitting generation legitimately ran past 5 minutes — OpenAI's own guidance states xhigh trades latency for depth by design. The non-streaming `call()` path (used by every subagent, compaction, and goal-verify call, since none wire `onModelStream`) carried the IDENTICAL bug with zero idle/activity tracking at all, exposed under a different, unhelpful raw `DOMException: The operation timed out.` message instead.
264
+ - **[0.7.53]** (2026-07-06) — Verified PromptRouter end-to-end (63 pre-existing tests re-run, all passing; a live `jeo doctor --json`/human-output smoke test confirms real wiring, not just mocks) and closed the one genuine remaining gap: the v0.7.51 per-turn credential-readiness veto protected a running session reactively, but nothing caught a misconfigured `roles.smol`/`roles.slow`/`routing.tiers.*.model` proactively at setup time.
263
265
  - **[0.7.52]** (2026-07-06) — Two TUI visibility asks answered directly ("어느 모델이 라우팅됐는지 상시 표시" — persistently show which model routing picked, and "각 서브에이전트의 사고 과정도 보이게" — surface each subagent's own thinking) plus a Ponytail-mode minimal-code audit pass across six files that found one genuine correctness bug and five instances of dead/unreachable code.
264
266
  - **[0.7.51]** (2026-07-06) — Closes a real correctness gap the user directly identified by asking "does routing only ever pick from models the user can actually use?" — the honest answer at v0.7.50 was no: `routePrompt` resolves a tier's model purely from config (`routing.tiers`/`roles.smol`/`roles.slow`/`defaultModel`) and never checks whether that model's provider has a usable credential. A user could configure `roles.smol`/`roles.slow` to a provider they never logged into (or whose OAuth token expired since setup), and routing would silently turn a WORKING session into a FAILING turn — `resolveCall` throwing `"No credential for provider …"` — the instant it engaged, even though the turn would have succeeded unrouted. Routing is opt-in specifically so it can never make things worse than routing being off; this closes that hole.
265
267
  - **[0.7.50]** (2026-07-06) — Design-doc audit of v0.7.47's PromptRouter, dispatched as two parallel subagents (independent re-verification + gap implementation) against the original Korean-language design report. The core proposal was confirmed shipped correctly (heuristic classifier, escalation, `/model`-always-wins, tool-selection isolation — all independently re-derived from git history and grep evidence, not just re-read from the doc). Two gaps the design doc itself had flagged as open questions (§7 risks #2 and #4) were real and are fixed here; a third finding (an internal contradiction in the design doc's own §3.2 vs §3.4 text on image-attachment gating — the SHIPPED code was already correct, only the doc's prose disagreed with itself) required no code change.
266
- - **[0.7.49]** (2026-07-06) — gjc Telegram-daemon parity, Tier 2 (follow-up to 0.7.37/0.7.38/0.7.48's subagent-visibility-only daemon): adds a dynamic, PER-SESSION forum topic that mirrors that session's OWN activity — not just its subagents — and lets a Telegram reply steer it directly. Opt-in via `notifications.telegram.perSessionTopics`; the flat/global `topicId` path from 0.7.48 is UNCHANGED when it's off, so existing setups see zero regression.
267
- - **[0.7.48]** (2026-07-06) — gjc Telegram-daemon parity, phase 2 (follow-up to 0.7.37/0.7.38's baseline subagent-visibility daemon, which deliberately scoped OUT forum topics, inline keyboards, and image attachments — see that entry): the daemon now speaks gjc's richer notification surface instead of the plain-text-only baseline.
268
268
 
269
269
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
270
270
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -79,8 +79,8 @@ Inside the `jeo` REPL (Tab autocompletes; `/` opens the palette).
79
79
  | `/model` · `/provider` | Pick model/provider; `/model` shows default/role badges, Ralph-style nested Set-as-role thinking choices, and the OpenAI Codex role preset in one flow |
80
80
  | `/provider login <name>` · `/logout` | OAuth login/logout from the input box |
81
81
  | `/agents [role]` · `/subagent` | Per-role (executor/planner/architect/critic) model · thinking · step config |
82
- | `/thinking [level]` | Show/set default reasoning budget (minimal…xhigh) |
83
- | `/fast [on|off|status]` | Toggle fast thinking mode when the active model advertises minimal/low reasoning |
82
+ | `/thinking [level]` | Show/set default reasoning budget (low…xhigh) |
83
+ | `/fast [on|off|status]` | Toggle fast thinking mode when the active model advertises low reasoning |
84
84
  | `/skill` · `$<skill> [intent]` | List/run workflow skills (`$team "task"` style) |
85
85
  | `/view` · `/diff` · `/find` · `/search` | Code view, git diff, file/pattern search |
86
86
  | `/new` · `/resume` · `/sessions` | Session management |
@@ -214,8 +214,10 @@ JEO_TUI_THEME=cosmic # cosmic/matrix/solar/red-claw/blue-crab/mono/au
214
214
  JEO_TUI_ALT_SCREEN=1 # legacy alt-screen turn (default: inline scrollback)
215
215
  JEO_STEP_BASE=24 # dynamic step budget: rolling base
216
216
  JEO_STEP_HARD_CAP=600 # absolute termination guarantee
217
- JEO_STREAM_MAX_MS=300000 # opt-in overall stream deadline (default off; bounds slow-drip streams)
217
+ JEO_STREAM_MAX_MS=1800000 # overall stream deadline (default 30min; bounds slow-drip streams, not active ones); 0 disables
218
218
  JEO_STREAM_IDLE_MS=300000 # per-chunk idle cap (default 300s); raise for slow/local backends silent before first token
219
+ JEO_CALL_TIMEOUT_MS=1800000 # non-streaming call wall cap (default 30min; compaction/subagents/goal-verify)
220
+ JEO_TURN_MAX_MS=1800000 # turn stall budget: max time WITHOUT tool progress (default 30min); 0 disables
219
221
  JEO_TOOL_OUTPUT_MAX=4000 # model-visible tool output cap (full output spills to artifacts)
220
222
  ```
221
223
 
@@ -262,11 +264,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
262
264
  ## Changelog
263
265
 
264
266
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
267
+ - **[0.7.54]** (2026-07-06) — Root-cause fix for a live-reported GPT-5.5 (Codex/ChatGPT OAuth backend) failure: "Error: stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted". Deep-dive traced this to a REGRESSION in v0.7.42 — a separate, correct fix for a genuinely-different bug (a connected-but-never-terminating stream) silently flipped `streamMaxMs()`'s default from opt-in-off to an ALWAYS-ON 300s overall wall-clock cap, which then killed any HIGH/XHIGH-reasoning-effort completion (GPT-5.5/o3-class) whose ACTIVELY-emitting generation legitimately ran past 5 minutes — OpenAI's own guidance states xhigh trades latency for depth by design. The non-streaming `call()` path (used by every subagent, compaction, and goal-verify call, since none wire `onModelStream`) carried the IDENTICAL bug with zero idle/activity tracking at all, exposed under a different, unhelpful raw `DOMException: The operation timed out.` message instead.
268
+ - **[0.7.53]** (2026-07-06) — Verified PromptRouter end-to-end (63 pre-existing tests re-run, all passing; a live `jeo doctor --json`/human-output smoke test confirms real wiring, not just mocks) and closed the one genuine remaining gap: the v0.7.51 per-turn credential-readiness veto protected a running session reactively, but nothing caught a misconfigured `roles.smol`/`roles.slow`/`routing.tiers.*.model` proactively at setup time.
265
269
  - **[0.7.52]** (2026-07-06) — Two TUI visibility asks answered directly ("어느 모델이 라우팅됐는지 상시 표시" — persistently show which model routing picked, and "각 서브에이전트의 사고 과정도 보이게" — surface each subagent's own thinking) plus a Ponytail-mode minimal-code audit pass across six files that found one genuine correctness bug and five instances of dead/unreachable code.
266
270
  - **[0.7.51]** (2026-07-06) — Closes a real correctness gap the user directly identified by asking "does routing only ever pick from models the user can actually use?" — the honest answer at v0.7.50 was no: `routePrompt` resolves a tier's model purely from config (`routing.tiers`/`roles.smol`/`roles.slow`/`defaultModel`) and never checks whether that model's provider has a usable credential. A user could configure `roles.smol`/`roles.slow` to a provider they never logged into (or whose OAuth token expired since setup), and routing would silently turn a WORKING session into a FAILING turn — `resolveCall` throwing `"No credential for provider …"` — the instant it engaged, even though the turn would have succeeded unrouted. Routing is opt-in specifically so it can never make things worse than routing being off; this closes that hole.
267
271
  - **[0.7.50]** (2026-07-06) — Design-doc audit of v0.7.47's PromptRouter, dispatched as two parallel subagents (independent re-verification + gap implementation) against the original Korean-language design report. The core proposal was confirmed shipped correctly (heuristic classifier, escalation, `/model`-always-wins, tool-selection isolation — all independently re-derived from git history and grep evidence, not just re-read from the doc). Two gaps the design doc itself had flagged as open questions (§7 risks #2 and #4) were real and are fixed here; a third finding (an internal contradiction in the design doc's own §3.2 vs §3.4 text on image-attachment gating — the SHIPPED code was already correct, only the doc's prose disagreed with itself) required no code change.
268
- - **[0.7.49]** (2026-07-06) — gjc Telegram-daemon parity, Tier 2 (follow-up to 0.7.37/0.7.38/0.7.48's subagent-visibility-only daemon): adds a dynamic, PER-SESSION forum topic that mirrors that session's OWN activity — not just its subagents — and lets a Telegram reply steer it directly. Opt-in via `notifications.telegram.perSessionTopics`; the flat/global `topicId` path from 0.7.48 is UNCHANGED when it's off, so existing setups see zero regression.
269
- - **[0.7.48]** (2026-07-06) — gjc Telegram-daemon parity, phase 2 (follow-up to 0.7.37/0.7.38's baseline subagent-visibility daemon, which deliberately scoped OUT forum topics, inline keyboards, and image attachments — see that entry): the daemon now speaks gjc's richer notification surface instead of the plain-text-only baseline.
270
272
 
271
273
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
272
274
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -78,8 +78,8 @@ jeo --tmux # 在独立 tmux 会话中运行
78
78
  | `/model` · `/provider` | 选择模型/提供商;`/model` 在一个流程内显示默认/角色徽章、Ralph 风格嵌套角色·thinking 选择与 OpenAI Codex 角色预设 |
79
79
  | `/provider login <name>` · `/logout` | 在输入框内 OAuth 登录/登出 |
80
80
  | `/agents [role]` · `/subagent` | 按角色(executor/planner/architect/critic)配置模型·thinking·步数 |
81
- | `/thinking [level]` | 查看/设置默认推理预算(minimal…xhigh) |
82
- | `/fast [on|off|status]` | 当前模型支持 minimal/low 推理时切换 fast thinking 模式 |
81
+ | `/thinking [level]` | 查看/设置默认推理预算(low…xhigh) |
82
+ | `/fast [on|off|status]` | 当前模型支持 low 推理时切换 fast thinking 模式 |
83
83
  | `/skill` · `$<skill> [intent]` | 列出/运行工作流技能(`$team "任务"` 风格) |
84
84
  | `/view` · `/diff` · `/find` · `/search` | 代码查看、git diff、文件/模式搜索 |
85
85
  | `/new` · `/resume` · `/sessions` | 会话管理 |
@@ -259,11 +259,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
259
259
  ## 更新日志 (Changelog)
260
260
 
261
261
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
262
+ - **[0.7.54]** (2026-07-06) — Root-cause fix for a live-reported GPT-5.5 (Codex/ChatGPT OAuth backend) failure: "Error: stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted". Deep-dive traced this to a REGRESSION in v0.7.42 — a separate, correct fix for a genuinely-different bug (a connected-but-never-terminating stream) silently flipped `streamMaxMs()`'s default from opt-in-off to an ALWAYS-ON 300s overall wall-clock cap, which then killed any HIGH/XHIGH-reasoning-effort completion (GPT-5.5/o3-class) whose ACTIVELY-emitting generation legitimately ran past 5 minutes — OpenAI's own guidance states xhigh trades latency for depth by design. The non-streaming `call()` path (used by every subagent, compaction, and goal-verify call, since none wire `onModelStream`) carried the IDENTICAL bug with zero idle/activity tracking at all, exposed under a different, unhelpful raw `DOMException: The operation timed out.` message instead.
263
+ - **[0.7.53]** (2026-07-06) — Verified PromptRouter end-to-end (63 pre-existing tests re-run, all passing; a live `jeo doctor --json`/human-output smoke test confirms real wiring, not just mocks) and closed the one genuine remaining gap: the v0.7.51 per-turn credential-readiness veto protected a running session reactively, but nothing caught a misconfigured `roles.smol`/`roles.slow`/`routing.tiers.*.model` proactively at setup time.
262
264
  - **[0.7.52]** (2026-07-06) — Two TUI visibility asks answered directly ("어느 모델이 라우팅됐는지 상시 표시" — persistently show which model routing picked, and "각 서브에이전트의 사고 과정도 보이게" — surface each subagent's own thinking) plus a Ponytail-mode minimal-code audit pass across six files that found one genuine correctness bug and five instances of dead/unreachable code.
263
265
  - **[0.7.51]** (2026-07-06) — Closes a real correctness gap the user directly identified by asking "does routing only ever pick from models the user can actually use?" — the honest answer at v0.7.50 was no: `routePrompt` resolves a tier's model purely from config (`routing.tiers`/`roles.smol`/`roles.slow`/`defaultModel`) and never checks whether that model's provider has a usable credential. A user could configure `roles.smol`/`roles.slow` to a provider they never logged into (or whose OAuth token expired since setup), and routing would silently turn a WORKING session into a FAILING turn — `resolveCall` throwing `"No credential for provider …"` — the instant it engaged, even though the turn would have succeeded unrouted. Routing is opt-in specifically so it can never make things worse than routing being off; this closes that hole.
264
266
  - **[0.7.50]** (2026-07-06) — Design-doc audit of v0.7.47's PromptRouter, dispatched as two parallel subagents (independent re-verification + gap implementation) against the original Korean-language design report. The core proposal was confirmed shipped correctly (heuristic classifier, escalation, `/model`-always-wins, tool-selection isolation — all independently re-derived from git history and grep evidence, not just re-read from the doc). Two gaps the design doc itself had flagged as open questions (§7 risks #2 and #4) were real and are fixed here; a third finding (an internal contradiction in the design doc's own §3.2 vs §3.4 text on image-attachment gating — the SHIPPED code was already correct, only the doc's prose disagreed with itself) required no code change.
265
- - **[0.7.49]** (2026-07-06) — gjc Telegram-daemon parity, Tier 2 (follow-up to 0.7.37/0.7.38/0.7.48's subagent-visibility-only daemon): adds a dynamic, PER-SESSION forum topic that mirrors that session's OWN activity — not just its subagents — and lets a Telegram reply steer it directly. Opt-in via `notifications.telegram.perSessionTopics`; the flat/global `topicId` path from 0.7.48 is UNCHANGED when it's off, so existing setups see zero regression.
266
- - **[0.7.48]** (2026-07-06) — gjc Telegram-daemon parity, phase 2 (follow-up to 0.7.37/0.7.38's baseline subagent-visibility daemon, which deliberately scoped OUT forum topics, inline keyboards, and image attachments — see that entry): the daemon now speaks gjc's richer notification surface instead of the plain-text-only baseline.
267
267
 
268
268
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
269
269
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.52",
3
+ "version": "0.7.54",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -122,7 +122,7 @@ export const ConfigSchema = z
122
122
  * regardless of project/cwd. A leading `~` is expanded; env `JEO_WIKI_ROOT`
123
123
  * overrides. Consumed by `resolveWikiRoot` and injected into the prompt. */
124
124
  wikiRoot: z.string().optional(),
125
- thinkingLevel: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional(),
125
+ thinkingLevel: z.enum(["low", "medium", "high", "xhigh"]).optional(),
126
126
  modelAliases: z.record(z.string()).optional(),
127
127
  /** Most-recently-selected models, newest first (drives the default + pickers). */
128
128
  recentModels: z.array(z.string()).optional(),
@@ -167,7 +167,7 @@ export const ConfigSchema = z
167
167
  provider: z.enum(["anthropic", "openai", "gemini", "antigravity", "ollama"]).optional(),
168
168
  maxSteps: z.number().int().min(1).optional(),
169
169
  /** Per-role reasoning budget; absent = inherit the global thinkingLevel. */
170
- thinking: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional(),
170
+ thinking: z.enum(["low", "medium", "high", "xhigh"]).optional(),
171
171
  // ─── Custom-role declaration (SYSTEM-driven registry) ───
172
172
  // An entry under a NON-bundled id that sets any of these becomes a
173
173
  // first-class subagent role at runtime — no code change required.
@@ -201,9 +201,9 @@ export const ConfigSchema = z
201
201
  confidenceThreshold: z.number().min(0).max(1).optional(),
202
202
  tiers: z
203
203
  .object({
204
- trivial: z.object({ model: z.string().optional(), thinking: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional() }).optional(),
205
- standard: z.object({ model: z.string().optional(), thinking: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional() }).optional(),
206
- complex: z.object({ model: z.string().optional(), thinking: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional() }).optional(),
204
+ trivial: z.object({ model: z.string().optional(), thinking: z.enum(["low", "medium", "high", "xhigh"]).optional() }).optional(),
205
+ standard: z.object({ model: z.string().optional(), thinking: z.enum(["low", "medium", "high", "xhigh"]).optional() }).optional(),
206
+ complex: z.object({ model: z.string().optional(), thinking: z.enum(["low", "medium", "high", "xhigh"]).optional() }).optional(),
207
207
  })
208
208
  .optional(),
209
209
  })
@@ -262,8 +262,52 @@ function normalizeModelId(id: string | undefined, aliases: Record<string, string
262
262
  return trimmed;
263
263
  }
264
264
 
265
+ /** Migrate the retired "minimal" thinking level to "low" on every field that could carry
266
+ * it, BEFORE schema validation. Without this, a config.json persisted while "minimal"
267
+ * was still valid (thinkingLevel, any subagents.*.thinking, any routing.tiers.*.thinking)
268
+ * would fail the tightened enum and readGlobalConfig's schema-invalid path resets the
269
+ * ENTIRE config to defaults (modelAliases/subagents/routing/retry/hooks/notifications
270
+ * all silently lost, not just the one bad field) — see state.ts's salvageCredentials doc
271
+ * comment for why only oauth/providers survive that path. Mutates a shallow-cloned
272
+ * object only where needed; non-object/malformed input passes through untouched so the
273
+ * schema's own validation still reports the real error. */
274
+ function migrateMinimalThinking(raw: unknown): unknown {
275
+ if (!raw || typeof raw !== "object") return raw;
276
+ const cfg = raw as Record<string, unknown>;
277
+ let changed = false;
278
+ const out: Record<string, unknown> = { ...cfg };
279
+ if (out.thinkingLevel === "minimal") { out.thinkingLevel = "low"; changed = true; }
280
+ if (out.subagents && typeof out.subagents === "object") {
281
+ const subs = out.subagents as Record<string, unknown>;
282
+ const nextSubs: Record<string, unknown> = { ...subs };
283
+ for (const [roleId, role] of Object.entries(subs)) {
284
+ if (role && typeof role === "object" && (role as Record<string, unknown>).thinking === "minimal") {
285
+ nextSubs[roleId] = { ...(role as Record<string, unknown>), thinking: "low" };
286
+ changed = true;
287
+ }
288
+ }
289
+ if (changed) out.subagents = nextSubs;
290
+ }
291
+ if (out.routing && typeof out.routing === "object") {
292
+ const routing = out.routing as Record<string, unknown>;
293
+ if (routing.tiers && typeof routing.tiers === "object") {
294
+ const tiers = routing.tiers as Record<string, unknown>;
295
+ const nextTiers: Record<string, unknown> = { ...tiers };
296
+ let tiersChanged = false;
297
+ for (const [tierId, tier] of Object.entries(tiers)) {
298
+ if (tier && typeof tier === "object" && (tier as Record<string, unknown>).thinking === "minimal") {
299
+ nextTiers[tierId] = { ...(tier as Record<string, unknown>), thinking: "low" };
300
+ tiersChanged = true;
301
+ }
302
+ }
303
+ if (tiersChanged) { out.routing = { ...routing, tiers: nextTiers }; changed = true; }
304
+ }
305
+ }
306
+ return changed ? out : raw;
307
+ }
308
+
265
309
  export function parseConfig(raw: unknown): { ok: true; config: ValidatedConfig } | { ok: false; message: string } {
266
- const result = ConfigSchema.safeParse(raw);
310
+ const result = ConfigSchema.safeParse(migrateMinimalThinking(raw));
267
311
  if (result.success) {
268
312
  const config = result.data;
269
313
  const aliases = config.modelAliases || {};
@@ -76,7 +76,7 @@ export interface Config {
76
76
  };
77
77
  };
78
78
 
79
- thinkingLevel?: "minimal" | "low" | "medium" | "high" | "xhigh";
79
+ thinkingLevel?: "low" | "medium" | "high" | "xhigh";
80
80
  /** Friendly model aliases, e.g. { fast: "ollama/qwen2.5:0.5b" }. Override built-ins. */
81
81
  modelAliases?: { [alias: string]: string };
82
82
  /** Most-recently-selected models, newest first (MRU; head == defaultModel). */
@@ -119,7 +119,7 @@ export interface Config {
119
119
  [roleId: string]: {
120
120
  model?: string;
121
121
  maxSteps?: number;
122
- thinking?: "minimal" | "low" | "medium" | "high" | "xhigh";
122
+ thinking?: "low" | "medium" | "high" | "xhigh";
123
123
  title?: string;
124
124
  description?: string;
125
125
  prompt?: string;
@@ -137,9 +137,9 @@ export interface Config {
137
137
  enabled?: boolean;
138
138
  confidenceThreshold?: number;
139
139
  tiers?: {
140
- trivial?: { model?: string; thinking?: "minimal" | "low" | "medium" | "high" | "xhigh" };
141
- standard?: { model?: string; thinking?: "minimal" | "low" | "medium" | "high" | "xhigh" };
142
- complex?: { model?: string; thinking?: "minimal" | "low" | "medium" | "high" | "xhigh" };
140
+ trivial?: { model?: string; thinking?: "low" | "medium" | "high" | "xhigh" };
141
+ standard?: { model?: string; thinking?: "low" | "medium" | "high" | "xhigh" };
142
+ complex?: { model?: string; thinking?: "low" | "medium" | "high" | "xhigh" };
143
143
  };
144
144
  };
145
145
  gitAutoCommit?: boolean;
@@ -298,7 +298,7 @@ export function parseMaxSteps(input: string | undefined): number | undefined {
298
298
  * maxSteps). Pure — does not mutate `config`. Unknown roles are rejected by the
299
299
  * caller via `getSubagentRole`; this helper trusts the id it is given.
300
300
  */
301
- export type ThinkLevelValue = "minimal" | "low" | "medium" | "high" | "xhigh";
301
+ export type ThinkLevelValue = "low" | "medium" | "high" | "xhigh";
302
302
 
303
303
  /** Per-role reasoning level → explicit role override, else undefined (= INHERIT
304
304
  * the global thinkingLevel at call time — gjc's "(inherit)" semantics). */
@@ -9,9 +9,9 @@
9
9
  import type { ProviderName } from "./types";
10
10
  import { openaiCompatDef } from "./providers/openai-compatible-catalog";
11
11
 
12
- export type ThinkLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
12
+ export type ThinkLevel = "low" | "medium" | "high" | "xhigh";
13
13
 
14
- export const THINK_LEVELS: readonly ThinkLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
14
+ export const THINK_LEVELS: readonly ThinkLevel[] = ["low", "medium", "high", "xhigh"];
15
15
 
16
16
  export interface CatalogModel {
17
17
  /** jeo-facing canonical id (what a user types). */
@@ -31,8 +31,8 @@ export interface CatalogModel {
31
31
  company?: string;
32
32
  }
33
33
 
34
- const FULL: ThinkLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
35
- const STD: ThinkLevel[] = ["minimal", "low", "medium", "high"];
34
+ const FULL: ThinkLevel[] = ["low", "medium", "high", "xhigh"];
35
+ const STD: ThinkLevel[] = ["low", "medium", "high"];
36
36
 
37
37
  export const ANTIGRAVITY_MODELS = [
38
38
  "claude-opus-4-6-thinking",
@@ -92,8 +92,7 @@ export function providerModelFor(model: string): string {
92
92
  }
93
93
 
94
94
  /** Map the configured thinking level to a default max-token budget. */
95
- export function thinkingMaxTokens(level?: "minimal" | "low" | "medium" | "high" | "xhigh"): number {
96
- if (level === "minimal") return 4000;
95
+ export function thinkingMaxTokens(level?: "low" | "medium" | "high" | "xhigh"): number {
97
96
  if (level === "low") return 8000;
98
97
  if (level === "high") return 24000;
99
98
  if (level === "xhigh") return 31999;
@@ -117,7 +116,7 @@ const DEFAULT_MAX_OUTPUT_CAP = 64_000;
117
116
  * Uncatalogued models (local/live ids) keep the legacy thinking-table budget. */
118
117
  export function resolveMaxOutputTokens(
119
118
  model?: string,
120
- level?: "minimal" | "low" | "medium" | "high" | "xhigh",
119
+ level?: "low" | "medium" | "high" | "xhigh",
121
120
  ): number {
122
121
  const meta = model ? findCatalogEntry(expandAlias(model, ALIAS_DEFAULTS)) : undefined;
123
122
  if (!meta) return thinkingMaxTokens(level);
@@ -126,13 +125,13 @@ export function resolveMaxOutputTokens(
126
125
  return Math.min(findCatalogModel(meta.id)?.maxOutputTokens ?? cap, cap);
127
126
  }
128
127
 
129
- /** Map the thinking level to an OpenAI reasoning-effort tier. minimal/low/medium/high pass
128
+ /** Map the thinking level to an OpenAI reasoning-effort tier. low/medium/high pass
130
129
  * through unchanged and xhigh folds to high (the deepest tier the provider APIs accept), so
131
- * reasoning works at EVERY thinking level (gajae parity: minimal is a real effort). Only an
132
- * unset level returns undefined (reasoning off — the explicit /fast path). */
130
+ * reasoning works at EVERY thinking level (gajae parity). Only an unset level returns
131
+ * undefined (reasoning off — the explicit /fast path). */
133
132
  export function thinkingToReasoningEffort(
134
- level?: "minimal" | "low" | "medium" | "high" | "xhigh",
135
- ): "minimal" | "low" | "medium" | "high" | undefined {
133
+ level?: "low" | "medium" | "high" | "xhigh",
134
+ ): "low" | "medium" | "high" | undefined {
136
135
  if (!level) return undefined;
137
136
  return level === "xhigh" ? "high" : level;
138
137
  }
@@ -450,13 +449,18 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
450
449
  * streaming idle watchdog, this path collects an opaque buffered body (`response.json()`,
451
450
  * or an internally-streamed collect in codexResponsesCall / antigravity.call) and exposes
452
451
  * no per-chunk signal — so a wall clock is the only lever and a wire heartbeat cannot help.
453
- * Raised to 300s to match STREAM_IDLE_TIMEOUT_MS: non-interactive turns (callLlm WITHOUT
454
- * onToken — compaction, ralplan, deep-interview, memory distill, goal-verify, and subagent/
455
- * autopilot engine steps) route here, and a long reasoning completion legitimately exceeds
456
- * 120s; too-tight, it aborts an alive call, retries re-incur the same slow request, the
457
- * attempt budget exhausts, and the turn STOPS — the same false-failure the streaming
458
- * watchdog guards, on the path the wire heartbeat never reaches. */
459
- const DEFAULT_CALL_TIMEOUT_MS = 300_000;
452
+ * Raised 120s → 300s → 30min across two rounds of the SAME false-failure: non-interactive
453
+ * turns (callLlm WITHOUT onToken — compaction, ralplan, deep-interview, memory distill,
454
+ * goal-verify, and subagent/autopilot engine steps) route here, and a HIGH/XHIGH reasoning
455
+ * effort completion (GPT-5.5/o3-class) legitimately exceeds 300s — OpenAI's own guidance is
456
+ * that xhigh trades latency for depth deliberately. Too-tight, it aborts an alive call, the
457
+ * attempt budget exhausts, and the turn STOPS — exactly the false-failure the streaming
458
+ * watchdog guards, on the path the wire heartbeat never reaches. 30min matches both
459
+ * `turnMaxMs()`'s own vetted stall-budget default AND the OBSERVED ~20-30min infra-side
460
+ * connection-duration cap on OpenAI's Codex/ChatGPT backend (see socket-closed handling
461
+ * above) — genuinely dead connections still fail fast via TCP/TLS errors well before this;
462
+ * this wall clock exists only for the "server accepted the request and went silent" case. */
463
+ const DEFAULT_CALL_TIMEOUT_MS = 30 * 60_000;
460
464
 
461
465
  /** Per-chunk idle cap for streaming: a stream that emits NOTHING for this long is
462
466
  * aborted, but a healthy long generation (chunks keep arriving) runs unbounded —
@@ -564,13 +568,17 @@ async function nextMaybeIdle(iter: AsyncIterator<string>, watchdog?: Promise<nev
564
568
  }
565
569
 
566
570
  /** Overall stream wall-clock from the environment. Defaults to the same hard bound as
567
- * callTimeoutMs() (DEFAULT_CALL_TIMEOUT_MS) so the streaming path has parity with the
568
- * non-streaming call()/fallback path — a stream that stays connected and keeps emitting
569
- * SOMETHING (SSE keepalives, reasoning deltas) re-arms the per-chunk idle watchdog
570
- * forever and would otherwise never resolve or reject. JEO_STREAM_MAX_MS explicitly set
571
- * to a positive number overrides the default; explicitly set to 0 disables the overall
572
- * deadline entirely (mirrors JEO_TURN_MAX_MS's own 0-disables convention) for advanced
573
- * users with genuinely long-running legitimate reasoning models. */
571
+ * callTimeoutMs() (DEFAULT_CALL_TIMEOUT_MS, 30min) so the streaming path has parity with
572
+ * the non-streaming call()/fallback path — a stream that stays connected and keeps
573
+ * emitting SOMETHING (SSE keepalives, reasoning deltas) re-arms the per-chunk idle
574
+ * watchdog forever and would otherwise never resolve or reject. Bug fix: this previously
575
+ * defaulted to 300s (DEFAULT_CALL_TIMEOUT_MS's OLD value, before it too was raised) —
576
+ * that false-failed any HIGH/XHIGH-reasoning-effort model (GPT-5.5/o3-class) whose
577
+ * ACTIVELY-emitting completion legitimately ran past 5 minutes, killing a healthy
578
+ * generation with "stream exceeded the overall deadline" despite continuous activity.
579
+ * JEO_STREAM_MAX_MS explicitly set to a positive number overrides the default; explicitly
580
+ * set to 0 disables the overall deadline entirely (mirrors JEO_TURN_MAX_MS's own
581
+ * 0-disables convention) for advanced users with genuinely long-running reasoning models. */
574
582
  export function streamMaxMs(env?: Record<string, string | undefined>): number | undefined {
575
583
  const raw = jeoEnv("STREAM_MAX_MS", env);
576
584
  if (raw === undefined || raw === "") return DEFAULT_CALL_TIMEOUT_MS;
@@ -99,14 +99,13 @@ function anthropicSystemBlocks(
99
99
  }
100
100
 
101
101
  /** Anthropic extended-thinking budget by reasoning effort (kept under max_tokens). Tiers match
102
- * gjc's ANTHROPIC_THINKING table: minimal/low/medium/high ALL enable thinking with scaling
103
- * depth — reasoning works at every thinking level (gajae parity: Minimal is a real effort).
102
+ * gjc's ANTHROPIC_THINKING table: low/medium/high ALL enable thinking with scaling
103
+ * depth — reasoning works at every thinking level (gajae parity).
104
104
  * Only an UNSET effort stays non-thinking (the explicit /fast off path). The "xhigh" row is
105
105
  * correct-by-table even though upstream currently folds xhigh→high before it arrives. */
106
106
  function anthropicThinkingBudget(effort: CallOptions["reasoningEffort"] | "xhigh", maxTokens: number): number | undefined {
107
107
  let budget: number;
108
108
  switch (effort) {
109
- case "minimal": budget = 1024; break;
110
109
  case "low": budget = 4096; break;
111
110
  case "medium": budget = 8192; break;
112
111
  case "high": budget = 16384; break;
@@ -161,10 +160,9 @@ function anthropicThinkingMode(model: string): AnthropicThinkingMode {
161
160
  }
162
161
 
163
162
  /** Map jeo's reasoning effort to Anthropic's adaptive/output_config effort literal. jeo folds
164
- * xhigh→high upstream, so only minimal/low/medium/high arrive here. (gjc: mapEffortToAnthropicAdaptiveEffort) */
163
+ * xhigh→high upstream, so only low/medium/high arrive here. (gjc: mapEffortToAnthropicAdaptiveEffort) */
165
164
  function anthropicAdaptiveEffort(effort: NonNullable<CallOptions["reasoningEffort"]>): "low" | "medium" | "high" {
166
165
  switch (effort) {
167
- case "minimal":
168
166
  case "low": return "low";
169
167
  case "medium": return "medium";
170
168
  case "high": return "high";
@@ -14,11 +14,10 @@ const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googlea
14
14
  /** Anthropic-style thinking budget for Claude served via CCA. gemini's budget fn
15
15
  * returns undefined for claude ids, which left antigravity Claude with NO thinking
16
16
  * requested (the opus "no reasoning" gap). Mirrors anthropic's effort→budget tiers —
17
- * minimal/low/medium/high ALL think (gajae parity: reasoning at every level); only an
17
+ * low/medium/high ALL think (gajae parity: reasoning at every level); only an
18
18
  * UNSET effort stays non-thinking. */
19
19
  function antigravityClaudeThinkingBudget(effort: CallOptions["reasoningEffort"]): number | undefined {
20
20
  switch (effort) {
21
- case "minimal": return 2000;
22
21
  case "low": return 4000;
23
22
  case "medium": return 10000;
24
23
  case "high": return 24000;
@@ -26,11 +26,10 @@ export function geminiThinkingBudget(model: string, effort?: CallOptions["reason
26
26
  const effectiveEffort = effort ?? geminiNamedEffort(m);
27
27
  let budget: number;
28
28
  switch (effectiveEffort) {
29
- // minimal/low/medium/high ALL enable thinking with scaling depth — reasoning works at
30
- // every thinking level (gajae parity: Minimal is a real effort). Only an UNSET effort
29
+ // low/medium/high ALL enable thinking with scaling depth — reasoning works at
30
+ // every thinking level (gajae parity). Only an UNSET effort
31
31
  // (and no in-name depth marker) falls through to the floor.
32
32
  // Tier values mirror gjc's GOOGLE_THINKING table (stream.ts).
33
- case "minimal": budget = Math.max(floor, 1024); break;
34
33
  case "low": budget = 4096; break;
35
34
  case "medium": budget = 8192; break;
36
35
  case "high": budget = 16384; break;
@@ -63,10 +62,9 @@ export function geminiUsesThinkingLevel(model: string): boolean {
63
62
  }
64
63
 
65
64
  /** Effort → Google `thinkingLevel` enum value (gjc mapEffortToGoogleThinkingLevel):
66
- * minimal→MINIMAL, low→LOW, medium→MEDIUM, high/xhigh→HIGH. */
67
- export function geminiThinkingLevel(effort: NonNullable<CallOptions["reasoningEffort"]> | "xhigh"): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" {
65
+ * low→LOW, medium→MEDIUM, high/xhigh→HIGH. */
66
+ export function geminiThinkingLevel(effort: NonNullable<CallOptions["reasoningEffort"]> | "xhigh"): "LOW" | "MEDIUM" | "HIGH" {
68
67
  switch (effort) {
69
- case "minimal": return "MINIMAL";
70
68
  case "low": return "LOW";
71
69
  case "medium": return "MEDIUM";
72
70
  case "high":
@@ -80,7 +78,7 @@ export function geminiThinkingLevel(effort: NonNullable<CallOptions["reasoningEf
80
78
  * omitted so the model keeps its default (gjc parity: thinking not enabled → no config).
81
79
  * Other thinking-capable models → numeric thinkingBudget; pre-2.5 → undefined. */
82
80
  export type GeminiThinkingConfig =
83
- | { includeThoughts: true; thinkingLevel: "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" }
81
+ | { includeThoughts: true; thinkingLevel: "LOW" | "MEDIUM" | "HIGH" }
84
82
  | { includeThoughts: true; thinkingBudget: number };
85
83
 
86
84
  export function geminiThinkingConfig(model: string, effort?: CallOptions["reasoningEffort"], maxTokens?: number): GeminiThinkingConfig | undefined {
@@ -21,7 +21,7 @@ import pkg from "../../../package.json";
21
21
 
22
22
  export const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
23
23
 
24
- export const VALID_REASONING_EFFORTS = new Set(["minimal", "low", "medium", "high"]);
24
+ export const VALID_REASONING_EFFORTS = new Set(["low", "medium", "high"]);
25
25
 
26
26
  const CODEX_ORIGINATOR = "codex_cli_rs";
27
27
 
package/src/ai/types.ts CHANGED
@@ -107,7 +107,7 @@ export interface CallOptions {
107
107
  /** Abort in-flight provider requests (Ctrl-C / timeout / supersede). */
108
108
  signal?: AbortSignal;
109
109
  /** Reasoning effort for reasoning models (o-series / gpt-5), mapped from thinkingLevel. */
110
- reasoningEffort?: "minimal" | "low" | "medium" | "high";
110
+ reasoningEffort?: "low" | "medium" | "high";
111
111
  /** How an OpenAI-compatible backend enables/streams native reasoning (gjc parity):
112
112
  * "openai" → `reasoning_effort`; "openrouter" → `reasoning: {effort}`; "qwen" →
113
113
  * `enable_thinking: true`; "zai" → `thinking: {type:"enabled"}`. Set per provider by
package/src/cli/runner.ts CHANGED
@@ -331,7 +331,7 @@ export function renderHelp(ctx: DispatchContext): string {
331
331
  lines.push(" --model <id> Use a session model for launch/one-shot.");
332
332
  lines.push(" --provider <name> Start launch on a provider default (anthropic/openai/gemini/antigravity/ollama).");
333
333
  lines.push(" --smol|--slow|--plan Start launch with the configured model role tier.");
334
- lines.push(" --thinking <level> Set thinking budget (minimal/low/medium/high/xhigh).");
334
+ lines.push(" --thinking <level> Set thinking budget (low/medium/high/xhigh).");
335
335
  lines.push("");
336
336
  return lines.join("\n");
337
337
  }
@@ -46,8 +46,8 @@ export async function runChatCommand(args: string[] = []): Promise<void> {
46
46
  let any = false;
47
47
  let usage: { inputTokens?: number; outputTokens?: number; durationMs?: number } | undefined;
48
48
  try {
49
- const effort = (thinking === "xhigh" ? "high" : thinking) as "minimal" | "low" | "medium" | "high" | undefined;
50
- const maxTokens = explicitMaxTokens ?? (thinking ? thinkingMaxTokens(thinking as "minimal" | "low" | "medium" | "high" | "xhigh") : undefined);
49
+ const effort = (thinking === "xhigh" ? "high" : thinking) as "low" | "medium" | "high" | undefined;
50
+ const maxTokens = explicitMaxTokens ?? (thinking ? thinkingMaxTokens(thinking as "low" | "medium" | "high" | "xhigh") : undefined);
51
51
  for await (const chunk of manager.stream([{ role: "user", content: message }], { model, maxTokens, reasoningEffort: effort, onUsage: u => { usage = u; } })) {
52
52
  process.stdout.write(chunk);
53
53
  any = true;
@@ -1,6 +1,7 @@
1
1
  import { readGlobalConfig } from "../agent/state";
2
2
  import { resolveCredential, snapshotProvider, type AuthProvider, type Credential } from "../auth";
3
- import { resolveProvider } from "../ai";
3
+ import { resolveProvider, describeProvider } from "../ai";
4
+
4
5
  import { effectiveCredentialForProvider } from "../ai/model-manager";
5
6
  import { resolveModelId } from "../ai/model-registry";
6
7
  import { meter } from "../tui/components/meter";
@@ -179,9 +180,37 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
179
180
  // turn.
180
181
  const routingEnabled = !!config.routing?.enabled;
181
182
  const smolConfigured = !!config.roles?.smol;
182
- const routingNote = routingEnabled && !smolConfigured
183
- ? "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."
184
- : undefined;
183
+ const routingNotes: string[] = [];
184
+ if (routingEnabled && !smolConfigured) {
185
+ routingNotes.push(
186
+ "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.",
187
+ );
188
+ }
189
+ // Proactive credential-readiness check for every model routing could actually pick —
190
+ // mirrors launch.ts's runTurn per-turn veto gate (v0.7.51) but surfaced at onboarding/
191
+ // doctor time instead of only reactively on a session's first qualifying turn. Catches
192
+ // `roles.smol`/`roles.slow`/`routing.tiers.*.model` pointed at a provider the user never
193
+ // logged into (or whose credential was since removed). `standard` only checked when
194
+ // explicitly configured — its unconfigured fallback is `defaultModel`, already covered
195
+ // by the provider-connectivity probes above.
196
+ if (routingEnabled) {
197
+ const tierCandidates: { tier: string; model: string | undefined }[] = [
198
+ { tier: "trivial", model: config.routing?.tiers?.trivial?.model || config.roles?.smol },
199
+ { tier: "standard", model: config.routing?.tiers?.standard?.model },
200
+ { tier: "complex", model: config.routing?.tiers?.complex?.model || config.roles?.slow },
201
+ ];
202
+ for (const { tier, model } of tierCandidates) {
203
+ if (!model) continue;
204
+ const provider = resolveProvider(model);
205
+ const status = await describeProvider(provider, config);
206
+ if (!status.ready) {
207
+ routingNotes.push(
208
+ `routing.tiers.${tier} resolves to '${model}' (${provider}) which has no usable credential — run 'jeo auth login ${provider}' or reconfigure that tier/role for a provider you're logged into.`,
209
+ );
210
+ }
211
+ }
212
+ }
213
+
185
214
 
186
215
  // --- Gather (probes run concurrently → ~1× the slowest timeout, not N×) ---
187
216
  const probes: { name: string; credKind: string; result: ProbeResult }[] = [];
@@ -248,7 +277,8 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
248
277
  })),
249
278
  oauth: oauthHealth,
250
279
  ready,
251
- ...(routingEnabled ? { routing: { enabled: true, smolConfigured, ...(routingNote ? { note: routingNote } : {}) } } : {}),
280
+ ...(routingEnabled ? { routing: { enabled: true, smolConfigured, ...(routingNotes.length ? { notes: routingNotes } : {}) } } : {}),
281
+
252
282
  };
253
283
  console.log(JSON.stringify(report, null, 2));
254
284
  if (strict && !ready) process.exit(1);
@@ -292,11 +322,12 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
292
322
  }
293
323
  // Routing diagnostic: informational only — never affects ready/strict exit logic.
294
324
  if (routingEnabled) {
295
- if (routingNote) {
296
- console.log(`${chalk.yellow("[routing]")} ${routingNote}`);
325
+ if (routingNotes.length) {
326
+ for (const note of routingNotes) console.log(`${chalk.yellow("[routing]")} ${note}`);
297
327
  } else {
298
328
  console.log(`${chalk.green("[routing]")} enabled, roles.smol configured — LLM escalation available on ambiguous prompts.`);
299
329
  }
330
+
300
331
  console.log("");
301
332
  }
302
333
 
@@ -478,12 +478,12 @@ export async function runThinkingSlash(input: string, ctx: AgentsSlashCtx): Prom
478
478
  console.log(`Thinking level: ${sessionThinking ?? "medium"} (~${thinkingMaxTokens(sessionThinking)} max tokens/step)`);
479
479
  return result();
480
480
  }
481
- if (arg === "minimal" || arg === "low" || arg === "medium" || arg === "high" || arg === "xhigh") {
481
+ if (arg === "low" || arg === "medium" || arg === "high" || arg === "xhigh") {
482
482
  sessionThinking = arg;
483
483
  sessionThinkingChanged = true;
484
484
  console.log(`Thinking set to ${arg} (~${thinkingMaxTokens(arg)} max tokens/step)`);
485
485
  } else {
486
- console.log(`Invalid level '${arg}'. Use: minimal | low | medium | high | xhigh.`);
486
+ console.log(`Invalid level '${arg}'. Use: low | medium | high | xhigh.`);
487
487
  }
488
488
  return result();
489
489
  }
@@ -49,19 +49,18 @@ export function isProviderName(input: string | undefined): input is ProviderName
49
49
  }
50
50
 
51
51
  export function isThinkingLevel(input: string | undefined): input is ThinkLevel {
52
- return input === "minimal" || input === "low" || input === "medium" || input === "high" || input === "xhigh";
52
+ return input === "low" || input === "medium" || input === "high" || input === "xhigh";
53
53
  }
54
54
 
55
55
 
56
56
  export function fastThinkingLevelForModel(modelId: string): ThinkLevel | undefined {
57
57
  const supported = catalogMetadata(modelId)?.thinking ?? [];
58
- if (supported.includes("minimal")) return "minimal";
59
58
  if (supported.includes("low")) return "low";
60
59
  // Digit-count agnostic (gemini-10+ / 2.6+ stay reasoning) — mirrors the gates in
61
60
  // gemini.ts and inferCatalogMetadata. Last resort for prefixed ids (models/gemini-…)
62
61
  // the catalog lookup above misses; catalogued ids already returned via thinking caps.
63
62
  const g = modelId.toLowerCase().match(/gemini-(\d+)(?:\.(\d+))?/);
64
- if (g && (Number(g[1]) >= 3 || (Number(g[1]) === 2 && Number(g[2] ?? 0) >= 5))) return "minimal";
63
+ if (g && (Number(g[1]) >= 3 || (Number(g[1]) === 2 && Number(g[2] ?? 0) >= 5))) return "low";
65
64
  return undefined;
66
65
  }
67
66
 
@@ -114,7 +113,7 @@ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchF
114
113
  const { value, nextIndex } = takeValue(args, i, "--thinking=");
115
114
  const normalized = value?.toLowerCase();
116
115
  if (isThinkingLevel(normalized)) flags.thinking = normalized;
117
- else flags.errors.push("--thinking must be one of: minimal, low, medium, high, xhigh");
116
+ else flags.errors.push("--thinking must be one of: low, medium, high, xhigh");
118
117
  i = nextIndex;
119
118
  } else if (a === "--smol" || a === "--slow" || a === "--plan") {
120
119
  flags.modelRole = a.slice(2) as ModelRole;
@@ -124,7 +124,7 @@ export async function runModelSlash(input: string, ctx: ModelSlashCtx): Promise<
124
124
  if (modelThinking) {
125
125
  const level = (modelThinking[1] ?? "").toLowerCase();
126
126
  if (!isThinkingLevel(level)) {
127
- console.log("Usage: /model thinking <minimal|low|medium|high|xhigh>");
127
+ console.log("Usage: /model thinking <low|medium|high|xhigh>");
128
128
  return result();
129
129
  }
130
130
  sessionThinking = level;
@@ -648,7 +648,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
648
648
  };
649
649
  let sessionModel: string | undefined = initialSessionModel;
650
650
  // Session thinking-level override (`/thinking`); falls back to the config level.
651
- let sessionThinking: "minimal" | "low" | "medium" | "high" | "xhigh" | undefined = flags.thinking ?? cfg.thinkingLevel;
651
+ let sessionThinking: "low" | "medium" | "high" | "xhigh" | undefined = flags.thinking ?? cfg.thinkingLevel;
652
652
  // PromptRouter session override: undefined = follow config.routing.enabled, true/false = /route on|off wins this session.
653
653
  let sessionRouteOverride: boolean | undefined;
654
654
  // Last routing decision (or the reason none applied) — /route why reads this.
@@ -2832,7 +2832,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2832
2832
  ): Promise<ThinkLevel | "inherit" | undefined> => {
2833
2833
  const levels = buildThinkingLevelChoices(current, {
2834
2834
  inheritLabel,
2835
- tokenHint: lvl => (lvl === "minimal" ? undefined : `~${Math.round(thinkingMaxTokens(lvl as ThinkLevel) / 1000)}k tokens`),
2835
+ tokenHint: lvl => `~${Math.round(thinkingMaxTokens(lvl as ThinkLevel) / 1000)}k tokens`,
2836
2836
  });
2837
2837
  const picked = await pickFromOptions(title, levels);
2838
2838
  return picked === "inherit" || isThinkingLevel(picked) ? picked : undefined;
@@ -2849,7 +2849,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2849
2849
  return true;
2850
2850
  }
2851
2851
  if (!isThinkingLevel(level)) {
2852
- console.log(`Usage: /model subagent ${role.id} thinking <inherit|minimal|low|medium|high|xhigh>`);
2852
+ console.log(`Usage: /model subagent ${role.id} thinking <inherit|low|medium|high|xhigh>`);
2853
2853
  return true;
2854
2854
  }
2855
2855
  await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { thinking: level }) }));
@@ -6,7 +6,7 @@
6
6
  * - `/model gpt` → live (logged-in) model ids + aliases + catalog ids
7
7
  * - `/provider an` → provider names; second arg → that provider's live models
8
8
  * - `/agents exec` → subagent role ids; second arg → live model ids
9
- * - `/thinking h` → minimal/low/medium/high/xhigh
9
+ * - `/thinking h` → low/medium/high/xhigh
10
10
  *
11
11
  * Pure + synchronous: the dynamic data (live models from the OAuth-authenticated
12
12
  * accounts, alias snapshot) is passed in via `CompletionContext`, so the readline
@@ -59,7 +59,7 @@ const PREVIEW_LABEL: Record<string, string> = {
59
59
  path: "Paths",
60
60
  };
61
61
  const MAX_COMPLETIONS = 50;
62
- const THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
62
+ const THINKING_LEVELS = ["low", "medium", "high", "xhigh"];
63
63
 
64
64
  /** Static half of a completion context (no network/config needed). */
65
65
  export function staticCompletionContext(): Omit<CompletionContext, "liveModels" | "aliases" | "modelsForProvider"> {
@@ -107,11 +107,10 @@ export function renderLiveModelPicker(list: SelectList<PickEntry>, opts: RenderS
107
107
  return renderSelectList(list, { title: "Select a live model", rows: 12, showTabs: true, ...opts });
108
108
  }
109
109
 
110
- /** The five reasoning levels in display order, lightest → heaviest. */
111
- export const THINKING_LEVEL_ORDER = ["minimal", "low", "medium", "high", "xhigh"] as const;
110
+ /** The four reasoning levels in display order, lightest → heaviest. */
111
+ export const THINKING_LEVEL_ORDER = ["low", "medium", "high", "xhigh"] as const;
112
112
 
113
113
  const THINKING_LEVEL_DESCRIPTION: Record<string, string> = {
114
- minimal: "lightest reasoning",
115
114
  low: "light reasoning",
116
115
  medium: "moderate reasoning",
117
116
  high: "deep reasoning",
@@ -42,7 +42,7 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
42
42
  { command: "/login", usage: "/login [provider]", description: "OAuth login — opens a provider picker showing live login status (account · expiry) for each provider (alias of /provider login)", group: "models" },
43
43
  { command: "/logout", usage: "/logout <anthropic|openai|gemini|antigravity>", description: "Remove the stored OAuth token for a provider", group: "models" },
44
44
  { command: "/roles", usage: "/roles [tier model]", description: "Show or set model role tiers (smol/slow/plan)", group: "models" },
45
- { command: "/thinking", usage: "/thinking [level]", description: "Show or set thinking budget (minimal/low/medium/high/xhigh)", group: "models" },
45
+ { command: "/thinking", usage: "/thinking [level]", description: "Show or set thinking budget (low/medium/high/xhigh)", group: "models" },
46
46
  { command: "/route", usage: "/route [on|off|why]", description: "Show/toggle prompt-based model routing for this session, or explain the last routing decision", group: "models" },
47
47
  { command: "/agents", usage: "/agents [edit|role] [model|#N|thinking L|maxSteps N|reset]", description: "List subagent roles; use /agents edit for the interactive picker or pin role model/settings", group: "subagents" },
48
48
  { command: "/subagent", usage: "/subagent [role]", description: "Show the current subagent composition (per-role model · thinking · steps); alias of /agents", group: "subagents" },
@@ -57,6 +57,12 @@ export function friendlyProviderError(err: unknown): string {
57
57
  if (status === 401 || status === 403 || /\b40[13]\b/.test(msg)) {
58
58
  return `${provider} rejected the credential (HTTP ${status ?? "401/403"}). Run 'jeo auth status', re-login with /provider login <name>, and for Antigravity prefer '/provider login antigravity' (gemini login only works when the Cloud Code Assist backend authorizes that token).`;
59
59
  }
60
+ if ((err as { name?: string })?.name === "TimeoutError" || /the operation timed out/i.test(msg)) {
61
+ return `${provider} did not complete the request within the call timeout (default 30min). This is expected for a HIGH/XHIGH-reasoning-effort completion that legitimately runs long on a hard problem — raise JEO_CALL_TIMEOUT_MS (ms) if this recurs, or lower the thinking level with /model.`;
62
+ }
63
+ if (/exceeded the overall deadline \(JEO_STREAM_MAX_MS\)/i.test(msg)) {
64
+ return `${provider}'s stream hit the overall deadline (default 30min; JEO_STREAM_MAX_MS) despite staying active. Raise JEO_STREAM_MAX_MS (ms), set it to 0 to disable the overall cap, or lower the thinking level with /model.`;
65
+ }
60
66
  if (isContextOverflowError(err)) {
61
67
  return `${provider} rejected the request: the conversation no longer fits the model's context window. Run /compact, drop large attachments, or start a fresh session.`;
62
68
  }