jeo-code 0.7.52 → 0.7.55
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 +34 -0
- package/README.ja.md +5 -5
- package/README.ko.md +5 -5
- package/README.md +8 -6
- package/README.zh.md +5 -5
- package/package.json +1 -1
- package/src/agent/config-schema.ts +50 -6
- package/src/agent/state.ts +5 -5
- package/src/agent/subagents.ts +1 -1
- package/src/ai/model-catalog.ts +4 -4
- package/src/ai/model-manager.ts +30 -22
- package/src/ai/providers/anthropic.ts +3 -5
- package/src/ai/providers/antigravity.ts +1 -2
- package/src/ai/providers/gemini.ts +5 -7
- package/src/ai/providers/openai-responses.ts +1 -1
- package/src/ai/types.ts +1 -1
- package/src/cli/runner.ts +1 -1
- package/src/commands/chat.ts +2 -2
- package/src/commands/doctor.ts +38 -7
- package/src/commands/launch/agents-slash.ts +2 -2
- package/src/commands/launch/flags.ts +3 -4
- package/src/commands/launch/model-slash.ts +1 -1
- package/src/commands/launch.ts +3 -3
- package/src/tui/app.ts +32 -12
- package/src/tui/components/autocomplete.ts +2 -2
- package/src/tui/components/live-model-picker.ts +2 -3
- package/src/tui/components/slash.ts +1 -1
- package/src/util/provider-error.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ 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.55] - 2026-07-07
|
|
10
|
+
_Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`modelProviderLabel()` helper** (`src/tui/app.ts`) — DRY "model (provider)" formatting shared with the existing `renderModelBar`/status-bar convention, now threaded through all three Thinking-related surfaces: the live `currentActivity()` status line ("reasoning (…)"/"calling model (…)"), the live streaming Thinking block header, and the live placeholder Thinking block (signature-only reasoning models like opus-4-8 that stream no thought text).
|
|
14
|
+
- **`thinkingHeader()` optional 3rd param** (`modelLabel?: string`) for the persisted (scrollback-committed) Thinking header — fully backward-compatible; existing 2-arg call sites are unchanged and produce byte-identical output.
|
|
15
|
+
|
|
16
|
+
### Verified
|
|
17
|
+
- `bun run typecheck` clean; full suite 2675/2675 pass across 280 files (+3 new tests: `thinkingHeader` 3-arg unit test, a real `LaunchTui` persisted-header integration test, and a live-frame streaming-block test).
|
|
18
|
+
- End-to-end proof with a real `LaunchTui` instance (not just the pure function) across a cross-provider two-turn session: turn 1 (claude-haiku-4-5/anthropic) and turn 2 (gemini-2.5-pro/gemini) each show their own correct model+provider in both the live and persisted Thinking headers, confirming the label updates turn-to-turn rather than being fixed at session start.
|
|
19
|
+
- A pre-existing test asserting the OLD bare `"thinking · Ns"` format (`streaming-reasoning.test.ts`) was updated to the new model-labeled format — an intentional format change, not a loosened assertion.
|
|
20
|
+
|
|
21
|
+
## [0.7.54] - 2026-07-06
|
|
22
|
+
_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._
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **`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.
|
|
26
|
+
- **`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.
|
|
27
|
+
|
|
28
|
+
### Verified
|
|
29
|
+
- `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.
|
|
30
|
+
- `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.
|
|
31
|
+
- `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.
|
|
32
|
+
- `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.
|
|
33
|
+
|
|
34
|
+
## [0.7.53] - 2026-07-06
|
|
35
|
+
_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._
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **`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.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- **`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.
|
|
42
|
+
|
|
9
43
|
## [0.7.52] - 2026-07-06
|
|
10
44
|
_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
45
|
|
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]` | デフォルト推論予算(
|
|
82
|
-
| `/fast [on|off|status]` | 現在のモデルが
|
|
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.55]** (2026-07-07) — Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end.
|
|
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.
|
|
262
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.
|
|
263
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.
|
|
264
|
-
- **[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]` | 기본 추론 예산(
|
|
83
|
-
| `/fast [on|off|status]` | 현재 모델이
|
|
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.55]** (2026-07-07) — Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end.
|
|
264
|
+
- **[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.
|
|
265
|
+
- **[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
266
|
- **[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
267
|
- **[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
|
-
- **[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 (
|
|
83
|
-
| `/fast [on|off|status]` | Toggle fast thinking mode when the active model advertises
|
|
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=
|
|
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.55]** (2026-07-07) — Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end.
|
|
268
|
+
- **[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.
|
|
269
|
+
- **[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
270
|
- **[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
271
|
- **[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
|
-
- **[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]` | 查看/设置默认推理预算(
|
|
82
|
-
| `/fast [on|off|status]` | 当前模型支持
|
|
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.55]** (2026-07-07) — Cross-provider routing (PromptRouter, v0.7.47+) already named the routed tier in the status bar (v0.7.52), but nothing surfaced WHICH model/provider actually produced a turn's reasoning in the Thinking-block surfaces — a gap noticed while manually verifying the v0.7.52 status-bar work end-to-end.
|
|
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.
|
|
262
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.
|
|
263
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.
|
|
264
|
-
- **[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
|
@@ -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(["
|
|
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(["
|
|
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(["
|
|
205
|
-
standard: z.object({ model: z.string().optional(), thinking: z.enum(["
|
|
206
|
-
complex: z.object({ model: z.string().optional(), thinking: z.enum(["
|
|
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 || {};
|
package/src/agent/state.ts
CHANGED
|
@@ -76,7 +76,7 @@ export interface Config {
|
|
|
76
76
|
};
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
thinkingLevel?: "
|
|
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?: "
|
|
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?: "
|
|
141
|
-
standard?: { model?: string; thinking?: "
|
|
142
|
-
complex?: { model?: string; thinking?: "
|
|
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;
|
package/src/agent/subagents.ts
CHANGED
|
@@ -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 = "
|
|
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). */
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -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 = "
|
|
12
|
+
export type ThinkLevel = "low" | "medium" | "high" | "xhigh";
|
|
13
13
|
|
|
14
|
-
export const THINK_LEVELS: readonly ThinkLevel[] = ["
|
|
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[] = ["
|
|
35
|
-
const STD: ThinkLevel[] = ["
|
|
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",
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -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?: "
|
|
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?: "
|
|
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.
|
|
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
|
|
132
|
-
*
|
|
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?: "
|
|
135
|
-
): "
|
|
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
|
|
454
|
-
* onToken — compaction, ralplan, deep-interview, memory distill,
|
|
455
|
-
* autopilot engine steps) route here, and a
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
|
|
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
|
|
568
|
-
* non-streaming call()/fallback path — a stream that stays connected and keeps
|
|
569
|
-
* SOMETHING (SSE keepalives, reasoning deltas) re-arms the per-chunk idle
|
|
570
|
-
* forever and would otherwise never resolve or reject.
|
|
571
|
-
* to
|
|
572
|
-
*
|
|
573
|
-
*
|
|
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:
|
|
103
|
-
* depth — reasoning works at every thinking level (gajae parity
|
|
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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
30
|
-
// every thinking level (gajae parity
|
|
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
|
-
*
|
|
67
|
-
export function geminiThinkingLevel(effort: NonNullable<CallOptions["reasoningEffort"]> | "xhigh"): "
|
|
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: "
|
|
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(["
|
|
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?: "
|
|
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 (
|
|
334
|
+
lines.push(" --thinking <level> Set thinking budget (low/medium/high/xhigh).");
|
|
335
335
|
lines.push("");
|
|
336
336
|
return lines.join("\n");
|
|
337
337
|
}
|
package/src/commands/chat.ts
CHANGED
|
@@ -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 "
|
|
50
|
-
const maxTokens = explicitMaxTokens ?? (thinking ? thinkingMaxTokens(thinking as "
|
|
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;
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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
|
|
183
|
-
|
|
184
|
-
|
|
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, ...(
|
|
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 (
|
|
296
|
-
console.log(`${chalk.yellow("[routing]")} ${
|
|
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 === "
|
|
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:
|
|
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 === "
|
|
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 "
|
|
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:
|
|
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 <
|
|
127
|
+
console.log("Usage: /model thinking <low|medium|high|xhigh>");
|
|
128
128
|
return result();
|
|
129
129
|
}
|
|
130
130
|
sessionThinking = level;
|
package/src/commands/launch.ts
CHANGED
|
@@ -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: "
|
|
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 =>
|
|
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|
|
|
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 }) }));
|
package/src/tui/app.ts
CHANGED
|
@@ -158,11 +158,17 @@ export function clipReasoningLines(text: string, cap = THINKING_COMMIT_MAX_LINES
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/** gjc-style "thought for Ns" header for a committed/streaming Thinking block. Omits the
|
|
161
|
-
* duration when no step start is known (e.g. resumed/exported records).
|
|
162
|
-
|
|
161
|
+
* duration when no step start is known (e.g. resumed/exported records). The optional
|
|
162
|
+
* `modelLabel` (e.g. "gpt-4o-mini (openai)", built by `modelProviderLabel()`) names
|
|
163
|
+
* which routed model actually produced this committed thought — a cross-provider
|
|
164
|
+
* routed turn's scrollback record otherwise gives no way to tell, after the fact,
|
|
165
|
+
* which provider reasoned about which step. Omitted entirely when absent (e.g. the
|
|
166
|
+
* 2-arg legacy call, or a resumed/exported record with no model context). */
|
|
167
|
+
export function thinkingHeader(elapsedMs: number | undefined, unicode: boolean, modelLabel?: string): string {
|
|
163
168
|
const diamond = unicode ? "◇" : "*";
|
|
164
169
|
const secs = elapsedMs !== undefined && elapsedMs >= 0 ? `${(elapsedMs / 1000).toFixed(1)}s` : null;
|
|
165
|
-
|
|
170
|
+
const model = modelLabel ? ` · ${modelLabel}` : "";
|
|
171
|
+
return `${diamond} thinking${model}${secs ? ` · ${secs}` : ""}`;
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
/** Status animation palette while a tool/process runs (background verification): an
|
|
@@ -553,8 +559,10 @@ export class LaunchTui {
|
|
|
553
559
|
const style = (prose: string) => prose.split("\n").map(styleThought).join("\n");
|
|
554
560
|
const parts: string[] = [this.agentLabel()];
|
|
555
561
|
// gjc "thought for Ns" header: step-start → commit ≈ the model's think+gen time.
|
|
562
|
+
// Names the routed model/provider so a cross-provider routed turn's scrollback
|
|
563
|
+
// record stays legible after the fact, not just live in the model bar.
|
|
556
564
|
const elapsedMs = this.currentStepStartedAt ? Date.now() - this.currentStepStartedAt : undefined;
|
|
557
|
-
const header = thinkingHeader(elapsedMs, this.unicode);
|
|
565
|
+
const header = thinkingHeader(elapsedMs, this.unicode, this.modelProviderLabel());
|
|
558
566
|
parts.push(this.theme.color ? chalk.dim(header) : header);
|
|
559
567
|
if (willFlushThought) {
|
|
560
568
|
this.flushedThought = this.streamingThought;
|
|
@@ -943,6 +951,15 @@ export class LaunchTui {
|
|
|
943
951
|
* Replaces the per-tick cycling status text so the line shows genuine content (thinking
|
|
944
952
|
* about a real file/step) instead of churning decorative messages every 120ms.
|
|
945
953
|
*/
|
|
954
|
+
/** `model (provider)` — the same convention `renderModelBar` uses for the persistent
|
|
955
|
+
* status bar, reused here so the live/persisted "thinking" surfaces name which
|
|
956
|
+
* routed model+provider is actually reasoning right now (not just the bare model id,
|
|
957
|
+
* which alone doesn't distinguish e.g. a `gpt-4o-mini` served via openai vs. a
|
|
958
|
+
* same-named alias on an OpenAI-compatible provider). */
|
|
959
|
+
private modelProviderLabel(): string {
|
|
960
|
+
return `${this.footer.model}${this.footer.provider ? ` (${this.footer.provider})` : ""}`;
|
|
961
|
+
}
|
|
962
|
+
|
|
946
963
|
private currentActivity(): string {
|
|
947
964
|
const running = this.tools.currentTool();
|
|
948
965
|
// An in-flight tool's real target beats the workflow phase banner (gjc-style:
|
|
@@ -974,14 +991,14 @@ export class LaunchTui {
|
|
|
974
991
|
if (this.retryNotice) return `${this.retryNotice} (${elapsed}s)`;
|
|
975
992
|
// Reasoning is streaming → the live thought block already shows it; label the row.
|
|
976
993
|
if (this.streamingThought.trim() || this.streamingReasoning.trim()) {
|
|
977
|
-
return `reasoning (${this.
|
|
994
|
+
return `reasoning (${this.modelProviderLabel()}) (${elapsed}s)…`;
|
|
978
995
|
}
|
|
979
996
|
// No tokens after a few seconds: the model is almost certainly reasoning
|
|
980
997
|
// server-side (e.g. OpenAI hidden reasoning), NOT hung — say so instead of a
|
|
981
998
|
// frozen "calling model …" so a long silent wait still reads as progress.
|
|
982
999
|
const waited = this.currentStepStartedAt ? (Date.now() - this.currentStepStartedAt) / 1000 : 0;
|
|
983
|
-
if (waited >= 8) return `thinking (${this.
|
|
984
|
-
return `calling model (${this.
|
|
1000
|
+
if (waited >= 8) return `thinking (${this.modelProviderLabel()}) — reasoning, no token stream yet (${elapsed}s)…`;
|
|
1001
|
+
return `calling model (${this.modelProviderLabel()}) (${elapsed}s)…`;
|
|
985
1002
|
}
|
|
986
1003
|
if (running) {
|
|
987
1004
|
const last = this.forgeSummaries[this.forgeSummaries.length - 1];
|
|
@@ -1567,17 +1584,20 @@ export class LaunchTui {
|
|
|
1567
1584
|
// rectangle, so a short trace leaves no padded "hole" and a short terminal is spared.
|
|
1568
1585
|
const liveThink = this.streamingThought.trim() || this.streamingReasoning.trim();
|
|
1569
1586
|
if (isThinking && liveThink) {
|
|
1570
|
-
// gjc-parity: the Thinking block label carries
|
|
1571
|
-
//
|
|
1587
|
+
// gjc-parity: the Thinking block label carries the routed model/provider and a
|
|
1588
|
+
// running timer ("Thinking · gpt-4o-mini (openai) · Ns") — names which model is
|
|
1589
|
+
// actually reasoning right now, so a cross-provider routed turn is legible mid-turn,
|
|
1590
|
+
// not just after the fact in the status bar. Cache key stays the constant "Thinking"
|
|
1591
|
+
// so the per-frame wrap memo is unaffected by the model/provider text.
|
|
1572
1592
|
const liveMs = this.currentStepStartedAt ? Date.now() - this.currentStepStartedAt : undefined;
|
|
1573
|
-
const liveLabel = liveMs !== undefined ? `
|
|
1593
|
+
const liveLabel = `Thinking · ${this.modelProviderLabel()}${liveMs !== undefined ? ` · ${(liveMs / 1000).toFixed(1)}s` : ""}`;
|
|
1574
1594
|
tail.push(...this.renderLiveBlock(liveLabel, liveThink, cols, rows, 6, "Thinking"));
|
|
1575
1595
|
} else if (isThinking && this.thinkingActive) {
|
|
1576
1596
|
// Signature-only reasoning models (opus-4-7/4-8) open a thinking block but stream no
|
|
1577
1597
|
// thought text — show a live placeholder so the wait reads as active thinking, not a
|
|
1578
1598
|
// frozen screen. Replaced the instant any real thought/answer text streams (branch above).
|
|
1579
1599
|
const liveMs = this.currentStepStartedAt ? Date.now() - this.currentStepStartedAt : undefined;
|
|
1580
|
-
const liveLabel = liveMs !== undefined ? `
|
|
1600
|
+
const liveLabel = `Thinking · ${this.modelProviderLabel()}${liveMs !== undefined ? ` · ${(liveMs / 1000).toFixed(1)}s` : ""}`;
|
|
1581
1601
|
tail.push(...this.renderLiveBlock(liveLabel, "(thinking…)", cols, rows, 6, "Thinking"));
|
|
1582
1602
|
}
|
|
1583
1603
|
|
|
@@ -1645,7 +1665,7 @@ export class LaunchTui {
|
|
|
1645
1665
|
? Math.round((this.footer.contextUsedTokens / this.footer.contextMaxTokens) * 100)
|
|
1646
1666
|
: undefined;
|
|
1647
1667
|
return renderStatusBar({
|
|
1648
|
-
model:
|
|
1668
|
+
model: this.modelProviderLabel(),
|
|
1649
1669
|
thinking: this.thinkingLevel,
|
|
1650
1670
|
routedTier: this.routedTier,
|
|
1651
1671
|
branch: this.footer.branch,
|
|
@@ -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` →
|
|
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 = ["
|
|
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
|
|
111
|
-
export const THINKING_LEVEL_ORDER = ["
|
|
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 (
|
|
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
|
}
|