jeo-code 0.7.51 → 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 +41 -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 +62 -6
- package/src/agent/opik-tracer.ts +1 -1
- package/src/agent/output-util.ts +3 -58
- package/src/agent/state.ts +5 -5
- package/src/agent/step-budget.ts +2 -3
- package/src/agent/subagents.ts +1 -1
- package/src/agent/task-tool.ts +16 -4
- package/src/agent/ts-language-service.ts +0 -7
- package/src/ai/model-catalog.ts +4 -4
- package/src/ai/model-manager.ts +35 -40
- 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/stream.ts +8 -1
- package/src/commands/launch.ts +4 -4
- package/src/tui/app.ts +16 -0
- 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/tui/components/status.ts +9 -0
- package/src/util/provider-error.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,47 @@ 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
|
+
|
|
31
|
+
## [0.7.52] - 2026-07-06
|
|
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._
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **Persistent routed-tier marker in the status bar** (`src/tui/components/status.ts`'s `StatusBarData.routedTier`/`renderStatusBar`, threaded through `LaunchTuiOptions`/`LaunchTui` in `src/tui/app.ts` and `src/commands/launch.ts`'s `runTurn`) — when PromptRouter picks THIS turn's model, a `⚡tier` marker (`~tier` in ASCII mode) renders right after the model name in the always-visible model bar, e.g. `claude-haiku-4-5 ⚡trivial`. Unlike the existing `[route] …` console notice (which intentionally stays silent for routine tier resolutions and only fires for interesting/escalated cases), the status-bar marker is unconditional whenever routing actually chose the model this turn — omitted when routing is off, the model is pinned, or the v0.7.51 credential-readiness gate vetoed the routed decision back to the default.
|
|
36
|
+
- **Subagent thinking visibility** (`TaskSubEvent`'s new `"thinking"` kind in `src/agent/task-tool.ts`, wired through `runSubagentOnce`'s `onReasoningStream` callback and handled in `LaunchTui.onSubagentEvent`) — a delegated subagent's own native extended-thinking stream now surfaces as a live, per-slot preview in the TUI (mirrors the main turn's dimmed "Thinking" block, scoped to that subagent's row instead of a single shared region), tail-sliced and whitespace-collapsed the same way tool/step previews already are. Transient only: never persisted to the ledger/scrollback (matches how the main turn's live reasoning preview also never appears in the final record), and the non-TTY streaming log (`formatTaskSubEvent` in `src/commands/launch/stream.ts`) explicitly no-ops on `"thinking"` instead of falling through to a bogus `"ROLE done: <reasoning text>"` line.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- **`retry.failFastStatuses`/`retry.failFastPatterns` silently stripped from every saved config** (`src/agent/config-schema.ts`) — both fields existed on `Config.retry` (`state.ts`) and were actively read by `src/ai/model-manager.ts` to decide non-retryable errors, but were never declared on the NESTED `retry` `z.object`. Only the outer `ConfigSchema` calls `.passthrough()`; Zod's default strip-unknown-keys behavior silently dropped both fields on every config read/write that round-tripped through `parseConfig`, even though `config.json` faithfully stored them on disk — a user-configured fail-fast rule would quietly stop applying the moment anything else touched the config file. Fixed by declaring both fields on the nested object directly.
|
|
40
|
+
- **Ponytail-mode dead-code sweep** (six files, five independent findings, all confirmed zero-call-site via `grep` before deletion): `src/agent/output-util.ts` lost `TOOL_SPILL_THRESHOLD`, `truncateToolOutput`, `spillToolResult`, and `logPerformanceMetric` (the last of these means `.jeo/state/performance-metrics.json` is never actually written by anything in this codebase — `PerfMetric`'s read-side in `dev/self-analysis.ts` is kept as the interface contract, but the writer was dead weight, not a working feature); `src/agent/ts-language-service.ts` lost `resetLanguageServiceCache` (no caller); `src/agent/task-tool.ts` lost the `@deprecated` `TASK_TOOL_PROTOCOL_LINE` static snapshot (superseded by `taskToolProtocolLine(config)`); `src/agent/step-budget.ts` simplified a no-op `key.replace(/^JEO_/, "JEO_")` (replacing a prefix with itself) in both `envNum` and `envSet` to a plain lookup; `src/agent/opik-tracer.ts` removed a `try/finally` around `onStep` whose `finally` block was an empty comment with no actual cleanup; `src/ai/model-manager.ts`'s `composeAbort` dropped an unreachable manual-polyfill branch for `AbortSignal.any` (native since Bun 1.1.13; `src/cli.ts` hard-enforces `engines.bun: ">=1.3.14"` at process start), cutting it from 17 lines to 2 with identical behavior for every real input.
|
|
41
|
+
|
|
42
|
+
### Verified
|
|
43
|
+
- `bun run typecheck` clean; `bun test` 2660 pass / 0 fail across 280 files on first run; a second full run reproduced 1 pre-existing flake (`test/hooks.test.ts`'s "disabled hooks never run", isolated-run-clean 3/3 — a cross-file `process.env.JEO_CONFIG_DIR` race under full-suite concurrency, not a regression from any change in this release).
|
|
44
|
+
- `test/status-bar.test.ts` (+4 tests): `routedTier` renders the `⚡` marker right after the model and before thinking; omitted entirely when routing didn't engage; ASCII fallback uses `~`; all three tier values (`trivial`/`standard`/`complex`) render verbatim.
|
|
45
|
+
- `test/tui-app.test.ts` (+3): `LaunchTui` threads `routedTier` into the persistent inline model bar and omits it when unset; `onSubagentEvent` with kind `"thinking"` drives the live per-slot preview but is never captured in the final scrollback record.
|
|
46
|
+
- `test/task-tool.test.ts` (+2): a subagent whose mocked `callLlm` invokes `onReasoning` emits live `"thinking"` events tagged to its own slot/index; a subagent with no reasoning stream emits zero `"thinking"` events (no false positives).
|
|
47
|
+
- `test/config-schema.test.ts` (+2, regression-focused): `retry.failFastStatuses`/`failFastPatterns` now survive `parseConfig` round-trip; a non-integer entry in `failFastStatuses` is rejected. The bug was confirmed empirically (bypassing the fix, both fields stripped silently) before applying it.
|
|
48
|
+
- Every dead-code deletion re-verified zero call sites via `grep` across `src/` and `test/` immediately before removal; the env-signal-composition simplifications (`composeAbort`, `envNum`/`envSet`) are exercised indirectly through existing abort-signal tests (`test/engine.test.ts`, `test/retry.test.ts`, `test/transient-network-recovery.test.ts`) and env-override tests (`test/step-budget.test.ts`), all passing unchanged — proving each simplification is behavior-preserving, not just "looks safe."
|
|
49
|
+
|
|
9
50
|
## [0.7.51] - 2026-07-06
|
|
10
51
|
_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._
|
|
11
52
|
|
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.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.
|
|
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.
|
|
262
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.
|
|
263
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.
|
|
264
|
-
- **[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.
|
|
265
|
-
- **[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.
|
|
266
|
-
- **[0.7.47]** (2026-07-06) — PromptRouter (gjc-inspired, jeo-native design — NOT a port of katanemo/plano, whose always-on proxy-orchestrator architecture doesn't fit an interactive CLI's per-turn latency budget): jeo already had static, role-based model mapping (`resolveSubagentModel`/`resolveRoleModel`) but zero logic that varied the model by what THIS turn's prompt actually asks for. Adds an opt-in (default OFF), heuristic-first, fail-open per-turn router: a bilingual regex classifier scores a prompt into trivial/standard/complex, escalating to one cheap LLM call ONLY when the heuristic is genuinely ambiguous (confidence below a configurable threshold — most turns never escalate), and an explicit `/model` pin always wins over routing. No new plumbing: reuses `resolveRoleModel`, `callLlm`, `jsonMode`, `catalogMetadata`, `tryExtractJsonObject`, and the existing `onNotice` transparency pattern.
|
|
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.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.
|
|
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
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.
|
|
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
|
-
- **[0.7.47]** (2026-07-06) — PromptRouter (gjc-inspired, jeo-native design — NOT a port of katanemo/plano, whose always-on proxy-orchestrator architecture doesn't fit an interactive CLI's per-turn latency budget): jeo already had static, role-based model mapping (`resolveSubagentModel`/`resolveRoleModel`) but zero logic that varied the model by what THIS turn's prompt actually asks for. Adds an opt-in (default OFF), heuristic-first, fail-open per-turn router: a bilingual regex classifier scores a prompt into trivial/standard/complex, escalating to one cheap LLM call ONLY when the heuristic is genuinely ambiguous (confidence below a configurable threshold — most turns never escalate), and an explicit `/model` pin always wins over routing. No new plumbing: reuses `resolveRoleModel`, `callLlm`, `jsonMode`, `catalogMetadata`, `tryExtractJsonObject`, and the existing `onNotice` transparency pattern.
|
|
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.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.
|
|
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.
|
|
265
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.
|
|
266
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.
|
|
267
|
-
- **[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.
|
|
268
|
-
- **[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.
|
|
269
|
-
- **[0.7.47]** (2026-07-06) — PromptRouter (gjc-inspired, jeo-native design — NOT a port of katanemo/plano, whose always-on proxy-orchestrator architecture doesn't fit an interactive CLI's per-turn latency budget): jeo already had static, role-based model mapping (`resolveSubagentModel`/`resolveRoleModel`) but zero logic that varied the model by what THIS turn's prompt actually asks for. Adds an opt-in (default OFF), heuristic-first, fail-open per-turn router: a bilingual regex classifier scores a prompt into trivial/standard/complex, escalating to one cheap LLM call ONLY when the heuristic is genuinely ambiguous (confidence below a configurable threshold — most turns never escalate), and an explicit `/model` pin always wins over routing. No new plumbing: reuses `resolveRoleModel`, `callLlm`, `jsonMode`, `catalogMetadata`, `tryExtractJsonObject`, and the existing `onNotice` transparency pattern.
|
|
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.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.
|
|
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.
|
|
262
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.
|
|
263
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.
|
|
264
|
-
- **[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.
|
|
265
|
-
- **[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.
|
|
266
|
-
- **[0.7.47]** (2026-07-06) — PromptRouter (gjc-inspired, jeo-native design — NOT a port of katanemo/plano, whose always-on proxy-orchestrator architecture doesn't fit an interactive CLI's per-turn latency budget): jeo already had static, role-based model mapping (`resolveSubagentModel`/`resolveRoleModel`) but zero logic that varied the model by what THIS turn's prompt actually asks for. Adds an opt-in (default OFF), heuristic-first, fail-open per-turn router: a bilingual regex classifier scores a prompt into trivial/standard/complex, escalating to one cheap LLM call ONLY when the heuristic is genuinely ambiguous (confidence below a configurable threshold — most turns never escalate), and an explicit `/model` pin always wins over routing. No new plumbing: reuses `resolveRoleModel`, `callLlm`, `jsonMode`, `catalogMetadata`, `tryExtractJsonObject`, and the existing `onNotice` transparency pattern.
|
|
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(),
|
|
@@ -140,6 +140,18 @@ export const ConfigSchema = z
|
|
|
140
140
|
rateLimitRetries: z.number().int().min(0).optional(),
|
|
141
141
|
rateLimitMinDelayMs: z.number().int().min(0).optional(),
|
|
142
142
|
rateLimitMaxServerDelayMs: z.number().int().min(0).optional(),
|
|
143
|
+
/** HTTP statuses to treat as NON-retryable even when defaultRetryable would
|
|
144
|
+
* retry them (e.g. pin 503 to fail fast instead of riding the backoff ladder).
|
|
145
|
+
* Bug fix: this field (and failFastPatterns below) existed on Config.retry in
|
|
146
|
+
* state.ts and was actively read by model-manager.ts, but was never declared
|
|
147
|
+
* on THIS nested z.object — since only the OUTER ConfigSchema calls
|
|
148
|
+
* `.passthrough()`, Zod's default strip-unknown-keys behavior silently dropped
|
|
149
|
+
* both fields on every config read/write that round-tripped through this
|
|
150
|
+
* schema, even though the user's config.json faithfully stored them on disk. */
|
|
151
|
+
failFastStatuses: z.array(z.number().int()).optional(),
|
|
152
|
+
/** Case-insensitive substrings; an error whose message matches any of these
|
|
153
|
+
* fails fast (non-retryable) even when the chosen predicate would retry it. */
|
|
154
|
+
failFastPatterns: z.array(z.string()).optional(),
|
|
143
155
|
})
|
|
144
156
|
.optional(),
|
|
145
157
|
/**
|
|
@@ -155,7 +167,7 @@ export const ConfigSchema = z
|
|
|
155
167
|
provider: z.enum(["anthropic", "openai", "gemini", "antigravity", "ollama"]).optional(),
|
|
156
168
|
maxSteps: z.number().int().min(1).optional(),
|
|
157
169
|
/** Per-role reasoning budget; absent = inherit the global thinkingLevel. */
|
|
158
|
-
thinking: z.enum(["
|
|
170
|
+
thinking: z.enum(["low", "medium", "high", "xhigh"]).optional(),
|
|
159
171
|
// ─── Custom-role declaration (SYSTEM-driven registry) ───
|
|
160
172
|
// An entry under a NON-bundled id that sets any of these becomes a
|
|
161
173
|
// first-class subagent role at runtime — no code change required.
|
|
@@ -189,9 +201,9 @@ export const ConfigSchema = z
|
|
|
189
201
|
confidenceThreshold: z.number().min(0).max(1).optional(),
|
|
190
202
|
tiers: z
|
|
191
203
|
.object({
|
|
192
|
-
trivial: z.object({ model: z.string().optional(), thinking: z.enum(["
|
|
193
|
-
standard: z.object({ model: z.string().optional(), thinking: z.enum(["
|
|
194
|
-
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(),
|
|
195
207
|
})
|
|
196
208
|
.optional(),
|
|
197
209
|
})
|
|
@@ -250,8 +262,52 @@ function normalizeModelId(id: string | undefined, aliases: Record<string, string
|
|
|
250
262
|
return trimmed;
|
|
251
263
|
}
|
|
252
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
|
+
|
|
253
309
|
export function parseConfig(raw: unknown): { ok: true; config: ValidatedConfig } | { ok: false; message: string } {
|
|
254
|
-
const result = ConfigSchema.safeParse(raw);
|
|
310
|
+
const result = ConfigSchema.safeParse(migrateMinimalThinking(raw));
|
|
255
311
|
if (result.success) {
|
|
256
312
|
const config = result.data;
|
|
257
313
|
const aliases = config.modelAliases || {};
|
package/src/agent/opik-tracer.ts
CHANGED
|
@@ -337,7 +337,7 @@ export function wrapEvents(events: AgentLoopEvents | undefined, tracer: OpikTrac
|
|
|
337
337
|
onStep(step: number) {
|
|
338
338
|
currentStep = step;
|
|
339
339
|
stepStartedAt = Date.now();
|
|
340
|
-
|
|
340
|
+
base.onStep?.(step);
|
|
341
341
|
},
|
|
342
342
|
onAssistant(raw: string, invocation: ToolInvocation | null) {
|
|
343
343
|
base.onAssistant?.(raw, invocation);
|
package/src/agent/output-util.ts
CHANGED
|
@@ -1,64 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const TOOL_SPILL_THRESHOLD = 32_000;
|
|
5
|
-
|
|
6
|
-
export function truncateToolOutput(output: string, limit = 8000): string {
|
|
7
|
-
if (output.length <= limit) return output;
|
|
8
|
-
const half = Math.floor(limit / 2);
|
|
9
|
-
return output.slice(0, half) + "\n\n... (elided " + (output.length - limit) + " chars) ...\n\n" + output.slice(-half);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function spillToolResult(tool: string, output: string, cwd: string): Promise<string> {
|
|
13
|
-
const artifactsDir = path.join(cwd, ".jeo", "artifacts");
|
|
14
|
-
await fs.mkdir(artifactsDir, { recursive: true });
|
|
15
|
-
|
|
16
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
17
|
-
const filename = "result-" + tool + "-" + ts + ".txt";
|
|
18
|
-
const fullPath = path.join(artifactsDir, filename);
|
|
19
|
-
|
|
20
|
-
await fs.writeFile(fullPath, output, "utf-8");
|
|
21
|
-
return path.relative(cwd, fullPath);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** One tool execution's performance record (consumed by dev self-analysis). */
|
|
1
|
+
/** One tool execution's performance record (consumed by dev self-analysis's read
|
|
2
|
+
* path — `.jeo/state/performance-metrics.json`, currently never populated by
|
|
3
|
+
* any writer in this codebase; the shape is kept as the read-side contract). */
|
|
25
4
|
export interface PerfMetric {
|
|
26
5
|
tool: string;
|
|
27
6
|
duration: number;
|
|
28
7
|
success: boolean;
|
|
29
8
|
error?: string;
|
|
30
9
|
}
|
|
31
|
-
|
|
32
|
-
/** Append a tool performance metric to `.jeo/state/performance-metrics.json`
|
|
33
|
-
* (bounded to the most recent 200 records). Best-effort: failures are ignored
|
|
34
|
-
* so metrics can never break an agent turn. */
|
|
35
|
-
export async function logPerformanceMetric(cwd: string, metric: PerfMetric): Promise<void> {
|
|
36
|
-
try {
|
|
37
|
-
const dir = path.join(cwd, ".jeo", "state");
|
|
38
|
-
const file = path.join(dir, "performance-metrics.json");
|
|
39
|
-
await fs.mkdir(dir, { recursive: true });
|
|
40
|
-
let records: PerfMetric[] = [];
|
|
41
|
-
try {
|
|
42
|
-
const parsed = JSON.parse(await fs.readFile(file, "utf-8"));
|
|
43
|
-
if (Array.isArray(parsed)) records = parsed;
|
|
44
|
-
} catch { /* first write or corrupt file → start fresh */ }
|
|
45
|
-
records.push(metric);
|
|
46
|
-
if (records.length > 200) records = records.slice(-200);
|
|
47
|
-
await fs.writeFile(file, JSON.stringify(records), "utf-8");
|
|
48
|
-
} catch { /* best-effort */ }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Read spec-kit project context (`.specify/memory/constitution.md`) when the repo
|
|
52
|
-
* was initialized with GitHub spec-kit, so the agent honors the project
|
|
53
|
-
* constitution. Returns null when absent/unreadable; content is capped. */
|
|
54
|
-
export async function loadSpecKitContext(cwd: string): Promise<string | null> {
|
|
55
|
-
try {
|
|
56
|
-
const file = path.join(cwd, ".specify", "memory", "constitution.md");
|
|
57
|
-
const content = (await fs.readFile(file, "utf-8")).trim();
|
|
58
|
-
if (!content) return null;
|
|
59
|
-
const capped = content.length > 4000 ? content.slice(0, 4000) + "…" : content;
|
|
60
|
-
return "\n\n<spec-kit-constitution>\n" + capped + "\n</spec-kit-constitution>";
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
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/step-budget.ts
CHANGED
|
@@ -41,8 +41,7 @@ export interface ExtensionDecision {
|
|
|
41
41
|
type EnvLike = Record<string, string | undefined>;
|
|
42
42
|
|
|
43
43
|
function envNum(env: EnvLike, key: string, dflt: number, min: number, max: number): number {
|
|
44
|
-
|
|
45
|
-
const raw = env[key.replace(/^JEO_/, "JEO_")] ?? env[key];
|
|
44
|
+
const raw = env[key];
|
|
46
45
|
if (raw === undefined || raw === "") return dflt;
|
|
47
46
|
const n = Number(raw);
|
|
48
47
|
if (!Number.isFinite(n)) return dflt;
|
|
@@ -82,7 +81,7 @@ export function resolveStepBudgetConfig(
|
|
|
82
81
|
|
|
83
82
|
/** True when `key` carries a non-empty value in `env`. */
|
|
84
83
|
function envSet(env: EnvLike, key: string): boolean {
|
|
85
|
-
const raw = env[key
|
|
84
|
+
const raw = env[key];
|
|
86
85
|
return raw !== undefined && raw !== "";
|
|
87
86
|
}
|
|
88
87
|
|
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/agent/task-tool.ts
CHANGED
|
@@ -35,7 +35,11 @@ import { ensureSessionNotifyEndpoint, type SessionNotifyEndpoint } from "./notif
|
|
|
35
35
|
/** Lifecycle event emitted while a delegated subagent runs. */
|
|
36
36
|
export interface TaskSubEvent {
|
|
37
37
|
role: string;
|
|
38
|
-
|
|
38
|
+
/** `"thinking"` streams the subagent's live reasoning/thought text (native
|
|
39
|
+
* extended-thinking models) — a transient live-preview beat, not persisted to
|
|
40
|
+
* the ledger (mirrors the main turn's dimmed "Thinking" block, scoped per
|
|
41
|
+
* subagent slot instead of a single shared region). */
|
|
42
|
+
kind: "start" | "step" | "tool" | "done" | "error" | "thinking";
|
|
39
43
|
detail?: string;
|
|
40
44
|
success?: boolean;
|
|
41
45
|
/** Current nested subagent step, when known. */
|
|
@@ -142,9 +146,6 @@ export function taskToolProtocolLine(config?: Pick<Config, "subagents">): string
|
|
|
142
146
|
);
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
/** @deprecated static snapshot (bundled roles only) — prefer taskToolProtocolLine(config). */
|
|
146
|
-
export const TASK_TOOL_PROTOCOL_LINE = taskToolProtocolLine();
|
|
147
|
-
|
|
148
149
|
/**
|
|
149
150
|
* A concise, gjc-style label for a subagent's tool call — the actual TARGET (file / command /
|
|
150
151
|
* glob), not just the bare tool name — so the parent's live monitor shows "read src/x.ts" or
|
|
@@ -300,6 +301,17 @@ export async function runSubagentOnce(
|
|
|
300
301
|
tools: subagentToolset(role),
|
|
301
302
|
events: {
|
|
302
303
|
onStep: n => { currentStep = n; },
|
|
304
|
+
// Live reasoning preview (native extended-thinking models only — the JSON-protocol
|
|
305
|
+
// "reasoning" field the main turn also extracts from onModelStream is intentionally
|
|
306
|
+
// NOT wired here: a subagent's forming tool-call JSON is rarely useful mid-stream and
|
|
307
|
+
// doubling the stream sinks would double emit() calls for the same underlying delta).
|
|
308
|
+
// Tail-sliced + whitespace-collapsed to a compact one-line preview — the ledger never
|
|
309
|
+
// records this (see the `kind` doc comment); it only drives the live per-slot status.
|
|
310
|
+
onReasoningStream: textSoFar => {
|
|
311
|
+
const tail = textSoFar.length > 200 ? textSoFar.slice(textSoFar.length - 200) : textSoFar;
|
|
312
|
+
const preview = tail.replace(/\s+/g, " ").trim();
|
|
313
|
+
if (preview) emit({ role: role.id, kind: "thinking", detail: preview, step: currentStep, maxSteps, model });
|
|
314
|
+
},
|
|
303
315
|
onAssistant: (_raw, invocation) => {
|
|
304
316
|
if (invocation && invocation.tool && invocation.tool !== "done") {
|
|
305
317
|
lastTarget = toolTarget(invocation.tool, invocation.arguments);
|
|
@@ -108,13 +108,6 @@ export async function languageServiceFor(cwd: string, absPath: string): Promise<
|
|
|
108
108
|
return cached.service;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/** Drop a cached language service (tests / long-running sessions that need a
|
|
112
|
-
* clean project graph after large structural changes). */
|
|
113
|
-
export function resetLanguageServiceCache(cwd?: string): void {
|
|
114
|
-
if (cwd) serviceCache.delete(cwd);
|
|
115
|
-
else serviceCache.clear();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
111
|
export interface SymbolSelector { text: string; occurrence: number }
|
|
119
112
|
|
|
120
113
|
/** Parse a loose `"name"` or `"name#N"` selector (1-indexed occurrence, default 1). */
|
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 —
|
|
@@ -472,25 +476,12 @@ const DEFAULT_CALL_TIMEOUT_MS = 300_000;
|
|
|
472
476
|
* phase runs longer still; Ctrl-C remains the interactive escape for a truly dead one. */
|
|
473
477
|
const STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
474
478
|
|
|
475
|
-
/** Combine two abort signals into one
|
|
476
|
-
* is
|
|
477
|
-
*
|
|
479
|
+
/** Combine two abort signals into one — neither the caller's cancel nor the timeout
|
|
480
|
+
* is ever silently dropped. `AbortSignal.any` is native on every Bun this CLI runs on
|
|
481
|
+
* (shipped in Bun 1.1.13; `src/cli.ts` hard-enforces `engines.bun: ">=1.3.14"` at
|
|
482
|
+
* process start, so a manual polyfill fallback would be permanently unreachable). */
|
|
478
483
|
function composeAbort(a: AbortSignal | undefined, b: AbortSignal): AbortSignal {
|
|
479
|
-
|
|
480
|
-
if (typeof AbortSignal.any === "function") return AbortSignal.any([a, b]);
|
|
481
|
-
if (a.aborted || b.aborted) return AbortSignal.abort();
|
|
482
|
-
const ctrl = new AbortController();
|
|
483
|
-
// Memory hygiene: `a` is typically the TURN-long abort signal — a once-listener
|
|
484
|
-
// per model call would otherwise accumulate on it for the whole turn. Detach
|
|
485
|
-
// BOTH listeners as soon as either side fires.
|
|
486
|
-
const onAbort = () => {
|
|
487
|
-
a.removeEventListener("abort", onAbort);
|
|
488
|
-
b.removeEventListener("abort", onAbort);
|
|
489
|
-
ctrl.abort();
|
|
490
|
-
};
|
|
491
|
-
a.addEventListener("abort", onAbort, { once: true });
|
|
492
|
-
b.addEventListener("abort", onAbort, { once: true });
|
|
493
|
-
return ctrl.signal;
|
|
484
|
+
return a ? AbortSignal.any([a, b]) : b;
|
|
494
485
|
}
|
|
495
486
|
|
|
496
487
|
/** Compose the caller's signal (if any) with a fresh per-attempt timeout. */
|
|
@@ -577,13 +568,17 @@ async function nextMaybeIdle(iter: AsyncIterator<string>, watchdog?: Promise<nev
|
|
|
577
568
|
}
|
|
578
569
|
|
|
579
570
|
/** Overall stream wall-clock from the environment. Defaults to the same hard bound as
|
|
580
|
-
* callTimeoutMs() (DEFAULT_CALL_TIMEOUT_MS) so the streaming path has parity with
|
|
581
|
-
* non-streaming call()/fallback path — a stream that stays connected and keeps
|
|
582
|
-
* SOMETHING (SSE keepalives, reasoning deltas) re-arms the per-chunk idle
|
|
583
|
-
* forever and would otherwise never resolve or reject.
|
|
584
|
-
* to
|
|
585
|
-
*
|
|
586
|
-
*
|
|
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. */
|
|
587
582
|
export function streamMaxMs(env?: Record<string, string | undefined>): number | undefined {
|
|
588
583
|
const raw = jeoEnv("STREAM_MAX_MS", env);
|
|
589
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;
|
|
@@ -53,11 +53,18 @@ export function formatTaskSubEvent(e: TaskSubEvent): string {
|
|
|
53
53
|
if (e.kind === "step") return ` ${badge} ${chalk.cyan(`├─ ${roleLabel}`)} · ${detail || "working"}`;
|
|
54
54
|
if (e.kind === "tool") return ` ${badge} ${e.success === false ? chalk.red("├─") : chalk.green("├─")} ${roleLabel} ${e.success === false ? chalk.red("✗") : chalk.green("✓")} ${detail || "tool"}${summary}`;
|
|
55
55
|
if (e.kind === "error") return ` ${badge} ${chalk.red("├─")} ${roleLabel} ${chalk.red("✗")} ${detail || "error"}`;
|
|
56
|
+
// "thinking" is a live-only TUI preview (never persisted to the ledger — see
|
|
57
|
+
// TaskSubEvent.kind's doc comment) and has no place in an append-only, non-TTY
|
|
58
|
+
// log stream: a reasoning delta stream would otherwise fall through to the
|
|
59
|
+
// "done" branch below and print a bogus "ROLE done: <reasoning text>" line for
|
|
60
|
+
// every emitted preview. Empty string signals the caller to skip the line.
|
|
61
|
+
if (e.kind === "thinking") return "";
|
|
56
62
|
return `${badge} ${e.success === false ? chalk.red("└─") : chalk.green("└─")} ${roleLabel} done${tokTag}${e.success === false ? " (incomplete)" : ""}${detail ? `: ${detail}` : ""}`;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
export function logTaskSubEvent(e: TaskSubEvent, log: (line: string) => void = (s: string) => console.log(s)): void {
|
|
60
|
-
|
|
66
|
+
const line = formatTaskSubEvent(e);
|
|
67
|
+
if (line) log(line);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export function createStreamEvents(
|
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.
|
|
@@ -937,7 +937,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
937
937
|
// config file. (gjc parity P1.B5: dirty count per-turn, not per-render.)
|
|
938
938
|
const { provider: activeProvider } = await describeModel(activeModel, turnConfig);
|
|
939
939
|
const turnDirtyCount = branch ? gitDirtyCount(cwd) : undefined;
|
|
940
|
-
const tui = useTui ? new LaunchTui({ model: activeModel, provider: activeProvider, sessionId, maxSteps: initialStepLimit, cwd, branch, dirtyCount: turnDirtyCount, thinking: activeThinking }) : null;
|
|
940
|
+
const tui = useTui ? new LaunchTui({ model: activeModel, provider: activeProvider, sessionId, maxSteps: initialStepLimit, cwd, branch, dirtyCount: turnDirtyCount, thinking: activeThinking, routedTier: routed?.tier }) : null;
|
|
941
941
|
if (routeCredentialNotice) {
|
|
942
942
|
if (tui) tui.events().onNotice?.(routeCredentialNotice);
|
|
943
943
|
else console.log(routeCredentialNotice);
|
|
@@ -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
|
@@ -68,6 +68,12 @@ export interface LaunchTuiOptions {
|
|
|
68
68
|
dirtyCount?: number;
|
|
69
69
|
/** Thinking-level label ("high", …) for the gjc-style model status bar. */
|
|
70
70
|
thinking?: string;
|
|
71
|
+
/** PromptRouter's resolved tier when routing chose THIS turn's `model` (undefined
|
|
72
|
+
* when routing didn't engage — pinned model, routing off, or the credential
|
|
73
|
+
* gate fell back). Renders a persistent `⚡tier` marker in the status bar so the
|
|
74
|
+
* routed model stays visible without depending on the transient `[route]`
|
|
75
|
+
* console notice. See `StatusBarData.routedTier` in components/status.ts. */
|
|
76
|
+
routedTier?: "trivial" | "standard" | "complex";
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
export interface AgentEventsLike {
|
|
@@ -361,6 +367,9 @@ export class LaunchTui {
|
|
|
361
367
|
private readonly inline: boolean;
|
|
362
368
|
// Thinking-level label for the gjc-style model status bar.
|
|
363
369
|
private readonly thinkingLevel?: string;
|
|
370
|
+
// PromptRouter's resolved tier when routing chose this turn's model; undefined when
|
|
371
|
+
// routing didn't engage. Renders the persistent `⚡tier` marker in the status bar.
|
|
372
|
+
private readonly routedTier?: "trivial" | "standard" | "complex";
|
|
364
373
|
|
|
365
374
|
constructor(opts: LaunchTuiOptions) {
|
|
366
375
|
// Backpressure-aware wrapper around the underlying writer (real stdout, or the
|
|
@@ -383,6 +392,7 @@ export class LaunchTui {
|
|
|
383
392
|
this.renderer = new Renderer(this.write, undefined, { reserve: this.inline });
|
|
384
393
|
this.spinner = new Spinner(undefined, { unicode: this.unicode });
|
|
385
394
|
this.thinkingLevel = opts.thinking;
|
|
395
|
+
this.routedTier = opts.routedTier;
|
|
386
396
|
this.footer = {
|
|
387
397
|
model: opts.model,
|
|
388
398
|
provider: opts.provider,
|
|
@@ -1041,6 +1051,11 @@ export class LaunchTui {
|
|
|
1041
1051
|
touchSlot(`${roleLabel} ${bad} ${detail || "error"}`);
|
|
1042
1052
|
this.appendLedger(` ${badge} ${branch} ${roleLabel} ${bad} ${detail || "error"}\n`, "subagent");
|
|
1043
1053
|
break;
|
|
1054
|
+
case "thinking":
|
|
1055
|
+
// Live-only preview (mirrors the main turn's dimmed "Thinking" block) — never
|
|
1056
|
+
// persisted to the ledger; see TaskSubEvent.kind's doc comment for why.
|
|
1057
|
+
touchSlot(`${roleLabel} ${this.unicode ? "…" : "..."} ${detail || "thinking"}`);
|
|
1058
|
+
break;
|
|
1044
1059
|
case "done":
|
|
1045
1060
|
// Clear ONLY this slot — a fan-out batch where one worker finishes early
|
|
1046
1061
|
// must keep showing its still-running siblings, not drop the `(sub)`
|
|
@@ -1632,6 +1647,7 @@ export class LaunchTui {
|
|
|
1632
1647
|
return renderStatusBar({
|
|
1633
1648
|
model: `${this.footer.model}${this.footer.provider ? ` (${this.footer.provider})` : ""}`,
|
|
1634
1649
|
thinking: this.thinkingLevel,
|
|
1650
|
+
routedTier: this.routedTier,
|
|
1635
1651
|
branch: this.footer.branch,
|
|
1636
1652
|
dirtyCount: this.footer.dirtyCount,
|
|
1637
1653
|
cwd: this.footer.cwd,
|
|
@@ -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" },
|
|
@@ -17,6 +17,14 @@ export interface StatusBarData {
|
|
|
17
17
|
model: string;
|
|
18
18
|
/** Thinking level label ("high", …); omitted when unset. */
|
|
19
19
|
thinking?: string;
|
|
20
|
+
/** PromptRouter's resolved tier ("trivial"/"standard"/"complex") when THIS turn's
|
|
21
|
+
* model was chosen by routing rather than the session/default model directly —
|
|
22
|
+
* omitted when routing didn't engage this turn (pinned model, routing off, or
|
|
23
|
+
* the credential-readiness gate fell back). Renders as a persistent `⚡tier`
|
|
24
|
+
* marker so the routed model stays visible without depending on the transient
|
|
25
|
+
* `[route] …` console notice, which intentionally stays silent for routine
|
|
26
|
+
* unescalated `standard`-tier turns. */
|
|
27
|
+
routedTier?: "trivial" | "standard" | "complex";
|
|
20
28
|
branch?: string;
|
|
21
29
|
/** Uncommitted-change count for the `?N` dirty flag; omit/0 = clean. */
|
|
22
30
|
dirtyCount?: number;
|
|
@@ -81,6 +89,7 @@ export function renderStatusBar(d: StatusBarData): string {
|
|
|
81
89
|
// Left identity segment (plain text; painted as one bg block at the end).
|
|
82
90
|
const bits: string[] = [];
|
|
83
91
|
let modelBit = `${unicode ? "⬢" : "*"} ${d.model}`;
|
|
92
|
+
if (d.routedTier) modelBit += ` ${unicode ? "⚡" : "~"}${d.routedTier}`;
|
|
84
93
|
if (d.thinking) modelBit += ` · ${unicode ? "◔" : "@"} ${d.thinking}`;
|
|
85
94
|
bits.push(modelBit);
|
|
86
95
|
if (d.branch) {
|
|
@@ -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
|
}
|