jeo-code 0.8.17 → 0.8.19
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 +35 -0
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +4 -4
- package/README.zh.md +3 -3
- package/package.json +1 -1
- package/src/agent/prompt-router.ts +40 -2
- package/src/agent/session.ts +24 -0
- package/src/commands/launch/route-slash.ts +41 -13
- package/src/commands/launch/session-slash.ts +13 -0
- package/src/commands/launch.ts +58 -3
- package/src/tui/app.ts +18 -0
- package/src/tui/components/slash.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.8.19] - 2026-07-09
|
|
10
|
+
_"프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Antigravity's Claude Sonnet/Opus are now reachable at the `high`/`complex` routing tiers** (`src/agent/prompt-router.ts`) — added `antigravityCompanyPoolPick`: when Antigravity is credentialed and a tier's pool spans 2+ companies, session-stably spreads across ONE candidate per company (the strongest Antigravity row for that company) instead of the plain single-winner comparator always resolving to Google's Gemini rows. Wired into `resolveTierModel`'s `high`/`complex` branches, after the explicit `routing.crossProviderPool` opt-in but before the plain single-winner fallback — purely additive, default-on specifically for Antigravity's multi-vendor case (every other provider represents exactly one vendor and is unaffected).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **Antigravity multi-company routing coverage** (`test/prompt-router.test.ts`) — 3 new tests: reachability across all 3 re-exported companies (Anthropic/Google/OpenAI) over 50 sessions, session-stability (same session always resolves to the same pick), and an explicit `roles.high` override still winning over the new spread. Updated one test whose assertion had locked in the old Gemini-only behavior — its actual invariant (never leak a bare public `gemini-*` id) still holds and is now asserted more precisely.
|
|
17
|
+
|
|
18
|
+
### Verified
|
|
19
|
+
- Live: `resolveTierModel("high"/"complex", …)` across 5 distinct real sessionIds confirms `antigravity/claude-sonnet-4-6` reachable at `high` and `antigravity/claude-opus-4-6-thinking` reachable at `complex`, spread across all 3 companies.
|
|
20
|
+
- `bun test test/prompt-router.test.ts test/prompt-router-tiers.test.ts test/model-manager.test.ts test/launch-prompt-routing.test.ts test/model-provider-mapping.test.ts` — 159 pass / 0 fail.
|
|
21
|
+
- Full `bun test` — 2893 pass / 0 fail across 291 files.
|
|
22
|
+
- `bun run typecheck` — no errors.
|
|
23
|
+
|
|
24
|
+
## [0.8.18] - 2026-07-09
|
|
25
|
+
_"라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct._
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **TUI footer no longer shows a stale model after a mid-turn equivalent-pool fallback** (`src/tui/app.ts`, `src/commands/launch.ts`) — the footer's `model`/`provider` fields were set once at `LaunchTui` construction and never updated when `launch.ts`'s post-call fallback loop (routed model hit a persistent server-side error, rate limit, etc.) switched `activeModel` mid-turn; the status bar kept showing the ORIGINAL pre-fallback model for the remainder of that turn even though the reply actually came from the fallback model. Added an `onModelSwitch(model, provider)` event, fired from the fallback loop right alongside the existing `[route] … switching to equivalent …` notice, that updates the live footer in place.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **`/route history [n]`** (`src/agent/route-history.ts`, `src/commands/launch/route-slash.ts`, registered in `src/tui/components/slash.ts`) — wires the `RouteHistory` class (shipped disconnected in the prior commit) into the live turn loop: a session-scoped bounded FIFO (never persisted) records every real routing decision — both the pre-call routed pick and any mid-turn equivalent-model fallback that actually served the turn — so history reflects reality, not a since-superseded pre-call guess. `/route history [n]` prints the last n (default 10, non-positive/non-numeric args fall back to the default) as `turn N: tier -> model (source, confidence X.XX)`.
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
- **`IMPROVEMENT_SUMMARY.md`, `REVIEW.md`** — stray AI-session working notes from the prior commit, never referenced by `README.md`, `CHANGELOG.md`, or `package.json`'s `files` allowlist.
|
|
35
|
+
|
|
36
|
+
### Verified
|
|
37
|
+
- Live PTY re-verification (`bun src/cli.ts`, isolated `JEO_CONFIG_DIR`, real Anthropic credentials): 5 prompts of varying complexity in one session routed to 5 distinct tier/model pairs (trivial→claude-haiku-4-5, complex→claude-fable-5, trivial→claude-haiku-4-5, high→claude-sonnet-5, trivial→claude-haiku-4-5); `/route why` and `/route status` matched the actual serving model on every turn.
|
|
38
|
+
- Live post-call-fallback reproduction: a real `Bun.serve` mock returning HTTP 500 on every call forced `launch.ts`'s equivalent-pool fallback to fire mid-turn; `/route why` immediately after correctly reported the POST-fallback model and a `warning: … switched to equivalent …` field, not the original pre-call pick.
|
|
39
|
+
- `bun test test/route-history.test.ts test/route-slash.test.ts test/launch-prompt-routing.test.ts` — 69 pass / 0 fail.
|
|
40
|
+
- `bun test test/tui-app.test.ts` — 51 pass / 0 fail (50 pre-existing + 1 new, covering the footer live-update fix).
|
|
41
|
+
- Full `bun test` — 2885 pass / 0 fail across 290 files.
|
|
42
|
+
- `bun run typecheck` — no errors.
|
|
43
|
+
|
|
9
44
|
## [0.8.17] - 2026-07-09
|
|
10
45
|
_"서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns._
|
|
11
46
|
|
package/README.ja.md
CHANGED
|
@@ -79,7 +79,7 @@ jeo --tmux # 独立した tmux セッションで実行
|
|
|
79
79
|
| `/provider login <name>` · `/logout` | 入力欄から OAuth ログイン/ログアウト |
|
|
80
80
|
| `/agents [role]` · `/subagent` | ロール別(executor/planner/architect/critic)モデル・thinking・ステップ設定 |
|
|
81
81
|
| `/thinking [level]` | デフォルト推論予算(low…xhigh)の表示/設定 |
|
|
82
|
-
| `/route [status\|on\|off\|why]` | セッション単位のプロンプトベース・モデルルーティングの切替 · 直近のルーティング判断の説明(設定済み資格情報 — OAuth または API キー — が実際に提供するモデルの中でのみ自動ルーティング) |
|
|
82
|
+
| `/route [status\|on\|off\|why\|history [n]]` | セッション単位のプロンプトベース・モデルルーティングの切替 · 直近のルーティング判断の説明 · `history [n]` はこのセッションの直近 n 件(デフォルト 10 件)のルーティング判断を表示(設定済み資格情報 — OAuth または API キー — が実際に提供するモデルの中でのみ自動ルーティング) |
|
|
83
83
|
| `/fast [on\|off\|status]` | 現在のモデルが low 推論をサポートする場合に fast thinking モードを切替 |
|
|
84
84
|
| `/skill` · `$<skill> [intent]` | ワークフロースキルの一覧/実行(`$team "task"` 形式) |
|
|
85
85
|
| `/view` · `/diff` · `/find` · `/search` | コード表示、git diff、ファイル/パターン検索 |
|
|
@@ -265,11 +265,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
265
265
|
## 変更履歴 (Changelog)
|
|
266
266
|
|
|
267
267
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
268
|
+
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
269
|
+
- **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
|
|
268
270
|
- **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
|
|
269
271
|
- **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
|
|
270
272
|
- **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
|
|
271
|
-
- **[0.8.14]** (2026-07-09) — "GPT 연결 실패가 여전히 무응답으로 끝나는 케이스를 잡아줘" — traced the remaining GPT/routing failure path to post-call continuity: the route target could be credentialed and servable, but still end a turn with a recoverable terminal result such as plain `OpenAI returned no content`, rate/quota pressure, model unavailable/not found, or an invalid agent-tool response. jeo already knew the prompt tier and equivalent credentialed fallbacks, but it only applied that fallback before the model call. After the call failed, the turn stopped instead of trying another model in the same tier.
|
|
272
|
-
- **[0.8.13]** (2026-07-09) — "테스트검증 jeo --tmux 로 진행해줘" — traced a routing recovery weakness that survived the earlier credential, servability, and local reachability vetoes: once a configured route target was rejected, jeo threw away the whole routing decision and dropped to the session/default model even when the same prompt tier had another credentialed, servable model available. That kept turns working, but it also silently undid routing's cost/latency goal and made `/route why` less useful because the recorded decision was a fallback note instead of the actual model used.
|
|
273
273
|
|
|
274
274
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
275
275
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -80,7 +80,7 @@ jeo --tmux # 독립 tmux 세션에서 실행
|
|
|
80
80
|
| `/provider login <name>` · `/logout` | 입력창에서 OAuth 로그인/로그아웃 |
|
|
81
81
|
| `/agents [role]` · `/subagent` | 역할별(executor/planner/architect/critic) 모델·thinking·스텝 구성 |
|
|
82
82
|
| `/thinking [level]` | 기본 추론 예산(low…xhigh) 조회/설정 |
|
|
83
|
-
| `/route [status\|on\|off\|why]` | 세션별 프롬프트 기반 모델 라우팅 켜기/끄기 · 마지막 라우팅 결정 설명(설정된 자격증명 — OAuth 또는 API 키 — 이 실제로 서비스하는 모델 안에서만 자동 라우팅) |
|
|
83
|
+
| `/route [status\|on\|off\|why\|history [n]]` | 세션별 프롬프트 기반 모델 라우팅 켜기/끄기 · 마지막 라우팅 결정 설명 · `history [n]`은 이번 세션의 최근 n개(기본 10개) 라우팅 결정 표시(설정된 자격증명 — OAuth 또는 API 키 — 이 실제로 서비스하는 모델 안에서만 자동 라우팅) |
|
|
84
84
|
| `/fast [on\|off\|status]` | 현재 모델이 low 추론을 지원하면 fast thinking 모드를 켜고 끔 |
|
|
85
85
|
| `/skill` · `$<skill> [intent]` | 워크플로 스킬 목록/실행(`$team "작업"` 스타일) |
|
|
86
86
|
| `/view` · `/diff` · `/find` · `/search` | 코드 보기, git diff, 파일/패턴 검색 |
|
|
@@ -266,11 +266,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
266
266
|
## 변경 이력 (Changelog)
|
|
267
267
|
|
|
268
268
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
269
|
+
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
270
|
+
- **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
|
|
269
271
|
- **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
|
|
270
272
|
- **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
|
|
271
273
|
- **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
|
|
272
|
-
- **[0.8.14]** (2026-07-09) — "GPT 연결 실패가 여전히 무응답으로 끝나는 케이스를 잡아줘" — traced the remaining GPT/routing failure path to post-call continuity: the route target could be credentialed and servable, but still end a turn with a recoverable terminal result such as plain `OpenAI returned no content`, rate/quota pressure, model unavailable/not found, or an invalid agent-tool response. jeo already knew the prompt tier and equivalent credentialed fallbacks, but it only applied that fallback before the model call. After the call failed, the turn stopped instead of trying another model in the same tier.
|
|
273
|
-
- **[0.8.13]** (2026-07-09) — "테스트검증 jeo --tmux 로 진행해줘" — traced a routing recovery weakness that survived the earlier credential, servability, and local reachability vetoes: once a configured route target was rejected, jeo threw away the whole routing decision and dropped to the session/default model even when the same prompt tier had another credentialed, servable model available. That kept turns working, but it also silently undid routing's cost/latency goal and made `/route why` less useful because the recorded decision was a fallback note instead of the actual model used.
|
|
274
274
|
|
|
275
275
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
276
276
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
|
|
|
40
40
|
|
|
41
41
|
## Highlights
|
|
42
42
|
|
|
43
|
-
- **Prompt routing (cost-aware, credential-aware)** — every turn can auto-route to a tier-appropriate model among only the providers your configured credentials actually serve (`/route [status|on|off|why]`), with a live equivalent-model fallback whenever a routed provider is rate-limited, unauthenticated, unreachable, or silently times out — see `/route why` for the last decision.
|
|
43
|
+
- **Prompt routing (cost-aware, credential-aware)** — every turn can auto-route to a tier-appropriate model among only the providers your configured credentials actually serve (`/route [status|on|off|why|history]`), with a live equivalent-model fallback whenever a routed provider is rate-limited, unauthenticated, unreachable, or silently times out — see `/route why` for the last decision, `/route history` for the recent ones.
|
|
44
44
|
- **Computer use (desktop automation)** — a fail-closed `computer` tool (screenshot/click/type/scroll/drag/batch) gated by both a config flag and an independent kill-switch/heartbeat supervisor; toggle it for the current session with `/computer [status|on|off]` without touching `~/.jeo/config.json`.
|
|
45
45
|
- **Multi-provider, one loop** — Anthropic / OpenAI (+Codex) / Gemini / Antigravity / Ollama / LM Studio, plus 20+ OpenAI- and Anthropic-compatible clouds (Groq, DeepSeek, Mistral, OpenRouter, xAI, Kimi, z.ai, …), all behind one uniform JSON tool loop. OAuth login happens from the input box (`/provider login`), every model pick persists as the new default, and prompt routing only auto-selects usable credentialed paths: Gemini OAuth goes through the provider-qualified `antigravity/*` agent set (Gemini 3.5 Flash tiers, Gemini 3.1 Pro, Claude Sonnet/Opus 4.6), never public `google/gemini-*` rows that require `GEMINI_API_KEY`; if a configured route points at an unready provider, jeo switches to an equivalent credentialed tier model before falling back to the default.
|
|
46
46
|
|
|
@@ -85,7 +85,7 @@ Inside the `jeo` REPL (Tab autocompletes; `/` opens the palette).
|
|
|
85
85
|
| `/provider login <name>` · `/logout` | OAuth login/logout from the input box |
|
|
86
86
|
| `/agents [role]` · `/subagent` | Per-role (executor/planner/architect/critic) model · thinking · step config |
|
|
87
87
|
| `/thinking [level]` | Show/set default reasoning budget (low…xhigh) |
|
|
88
|
-
| `/route [status\|on\|off\|why]` | Toggle prompt-based model routing for this session · explain the last routing decision (auto-routes each turn to a tier-appropriate model among the models your configured credentials — OAuth or API key — actually serve, and switches to an equivalent tier model when a configured route is unready) |
|
|
88
|
+
| `/route [status\|on\|off\|why\|history [n]]` | Toggle prompt-based model routing for this session · explain the last routing decision · `history [n]` lists the last n (default 10) routing decisions this session (auto-routes each turn to a tier-appropriate model among the models your configured credentials — OAuth or API key — actually serve, and switches to an equivalent tier model when a configured route is unready) |
|
|
89
89
|
| `/fast [on\|off\|status]` | Toggle fast thinking mode when the active model advertises low reasoning |
|
|
90
90
|
| `/skill` · `$<skill> [intent]` | List/run workflow skills (`$team "task"` style) |
|
|
91
91
|
| `/view` · `/diff` · `/find` · `/search` | Code view, git diff, file/pattern search |
|
|
@@ -273,11 +273,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
273
273
|
## Changelog
|
|
274
274
|
|
|
275
275
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
276
|
+
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
277
|
+
- **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
|
|
276
278
|
- **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
|
|
277
279
|
- **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
|
|
278
280
|
- **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
|
|
279
|
-
- **[0.8.14]** (2026-07-09) — "GPT 연결 실패가 여전히 무응답으로 끝나는 케이스를 잡아줘" — traced the remaining GPT/routing failure path to post-call continuity: the route target could be credentialed and servable, but still end a turn with a recoverable terminal result such as plain `OpenAI returned no content`, rate/quota pressure, model unavailable/not found, or an invalid agent-tool response. jeo already knew the prompt tier and equivalent credentialed fallbacks, but it only applied that fallback before the model call. After the call failed, the turn stopped instead of trying another model in the same tier.
|
|
280
|
-
- **[0.8.13]** (2026-07-09) — "테스트검증 jeo --tmux 로 진행해줘" — traced a routing recovery weakness that survived the earlier credential, servability, and local reachability vetoes: once a configured route target was rejected, jeo threw away the whole routing decision and dropped to the session/default model even when the same prompt tier had another credentialed, servable model available. That kept turns working, but it also silently undid routing's cost/latency goal and made `/route why` less useful because the recorded decision was a fallback note instead of the actual model used.
|
|
281
281
|
|
|
282
282
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
283
283
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -79,7 +79,7 @@ jeo --tmux # 在独立 tmux 会话中运行
|
|
|
79
79
|
| `/provider login <name>` · `/logout` | 在输入框内 OAuth 登录/登出 |
|
|
80
80
|
| `/agents [role]` · `/subagent` | 按角色(executor/planner/architect/critic)配置模型·thinking·步数 |
|
|
81
81
|
| `/thinking [level]` | 查看/设置默认推理预算(low…xhigh) |
|
|
82
|
-
| `/route [status\|on\|off\|why]` | 切换本会话的基于提示词的模型路由 · 解释最近一次路由决策(仅在已配置凭证 — OAuth 或 API 密钥 — 实际可用的模型内自动路由) |
|
|
82
|
+
| `/route [status\|on\|off\|why\|history [n]]` | 切换本会话的基于提示词的模型路由 · 解释最近一次路由决策 · `history [n]` 列出本会话最近 n 条(默认 10 条)路由决策(仅在已配置凭证 — OAuth 或 API 密钥 — 实际可用的模型内自动路由) |
|
|
83
83
|
| `/fast [on\|off\|status]` | 当前模型支持 low 推理时切换 fast thinking 模式 |
|
|
84
84
|
| `/skill` · `$<skill> [intent]` | 列出/运行工作流技能(`$team "任务"` 风格) |
|
|
85
85
|
| `/view` · `/diff` · `/find` · `/search` | 代码查看、git diff、文件/模式搜索 |
|
|
@@ -265,11 +265,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
265
265
|
## 更新日志 (Changelog)
|
|
266
266
|
|
|
267
267
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
268
|
+
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
269
|
+
- **[0.8.18]** (2026-07-09) — "라우팅 매 프롬프트마다 변경되는지, route why 의도대로 동작하는지 검증하고 배포할게있는지 확인해줘" — 2 parallel subagents live-verified per-prompt routing + `/route why` (including the previously-only-unit-tested post-call equivalent-model fallback path, now reproduced against a real mock 500-error server) and audited a disconnected `RouteHistory` class shipped as incomplete scaffolding in 0.8.16/0.8.17 (class + 11 tests existed, but nothing ever called it — no `/route history` subcommand, no wiring into the turn loop). Live testing also surfaced one real bug: the TUI footer's model/provider label never updated after a mid-turn fallback, staying on the pre-fallback model for the rest of that turn's render even though the backend decision (`lastRouteDecision`) was already correct.
|
|
268
270
|
- **[0.8.17]** (2026-07-09) — "서브에이전트 이용해. 병렬루 부가 나머지모두 개선하고 검증후 배포까지하자" — a prior 4-way parallel subagent cross-verification pass found the README `[!CAUTION]` routing-lock block (added this session for "프롬프트 라우팅이 한번 정해지면 안바뀌는데?") was directionally correct but had 2 real inaccuracies, plus a concrete test-coverage gap: every existing pin/override regression test drove the lock via the `--model` CLI startup flag, never via typing `/model <name>` as an interactive slash command mid-session, and none proved persistence across 3+ consecutive turns.
|
|
269
271
|
- **[0.8.16]** (2026-07-09) — "jeo code의 computer use 기능은 / 명령어로 설정할수있도록하고, 실동작검증까지진행해줘" — the desktop-automation `computer` tool was only togglable by hand-editing `computer.enabled` in `~/.jeo/config.json`; there was no in-session way to enable it for a single run without leaving a permanent config change behind.
|
|
270
272
|
- **[0.8.15]** (2026-07-09) — "api 인증 모델도 사용가능하도록 딥리서치하고 개선해줘" — traced the API-authenticated GPT path to a routing catalog gap: `GET /models` with an OpenAI API key or an OpenAI-compatible provider returned real account-scoped model ids, and `/model` could show/pin them, but prompt routing still built its auto-select pools from static `MODEL_CATALOG` rows only. A custom `openaiBaseUrl` made this worse by treating every public OpenAI row as servable, so routing could choose `gpt-*`/`o*` ids that the configured local/Azure/proxy endpoint never listed.
|
|
271
|
-
- **[0.8.14]** (2026-07-09) — "GPT 연결 실패가 여전히 무응답으로 끝나는 케이스를 잡아줘" — traced the remaining GPT/routing failure path to post-call continuity: the route target could be credentialed and servable, but still end a turn with a recoverable terminal result such as plain `OpenAI returned no content`, rate/quota pressure, model unavailable/not found, or an invalid agent-tool response. jeo already knew the prompt tier and equivalent credentialed fallbacks, but it only applied that fallback before the model call. After the call failed, the turn stopped instead of trying another model in the same tier.
|
|
272
|
-
- **[0.8.13]** (2026-07-09) — "테스트검증 jeo --tmux 로 진행해줘" — traced a routing recovery weakness that survived the earlier credential, servability, and local reachability vetoes: once a configured route target was rejected, jeo threw away the whole routing decision and dropped to the session/default model even when the same prompt tier had another credentialed, servable model available. That kept turns working, but it also silently undid routing's cost/latency goal and made `/route why` less useful because the recorded decision was a fallback note instead of the actual model used.
|
|
273
273
|
|
|
274
274
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
275
275
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -456,6 +456,44 @@ function crossProviderPoolPick(tier: PromptTier, config: RoutingConfig, sessionI
|
|
|
456
456
|
return pool.length > 0 ? selectFromPool(pool, sessionId) : null;
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
/** Antigravity re-exports THREE distinct model families (Anthropic's Claude
|
|
460
|
+
* Sonnet/Opus, Google's own Gemini, OpenAI's GPT-OSS) behind one credential —
|
|
461
|
+
* structurally different from every other provider, which represents exactly
|
|
462
|
+
* one vendor. The tier's plain single-winner pick (`strongestMidTierCredentialed`/
|
|
463
|
+
* `strongestCredentialed`) always resolves to Google's rows for `high`/`complex`:
|
|
464
|
+
* Anthropic's real 64,000-token output ceiling loses a same-thinking-tier tie to
|
|
465
|
+
* Google's 65,536 by a margin with no practical significance, and Gemini's
|
|
466
|
+
* 1M-token context further outranks Claude's real (but comparatively narrower)
|
|
467
|
+
* 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking`
|
|
468
|
+
* were NEVER reachable through auto-select even though both are already
|
|
469
|
+
* correctly `sizeClass`-tagged into the `high`/`complex` pools (`tierModelPool`).
|
|
470
|
+
* This applies the SAME session-stable spread `routing.crossProviderPool` uses
|
|
471
|
+
* globally (opt-in, default off), but scoped and DEFAULT-ON specifically for
|
|
472
|
+
* Antigravity's multi-company pool: one candidate per COMPANY (the strongest
|
|
473
|
+
* Antigravity row for that company, by `compareStrengthAscending`) so Google's
|
|
474
|
+
* several same-tier Gemini rows don't crowd out the single Anthropic/OpenAI
|
|
475
|
+
* candidate 3-to-1 — each vendor Antigravity re-exports gets an equal, genuinely
|
|
476
|
+
* reachable share. `null` when Antigravity isn't credentialed this turn, the
|
|
477
|
+
* tier's Antigravity-only pool is empty, or it spans only ONE company (nothing
|
|
478
|
+
* to spread — the plain single-winner path already picks correctly and this
|
|
479
|
+
* would just add indirection). */
|
|
480
|
+
function antigravityCompanyPoolPick(tier: PromptTier, config: RoutingConfig, sessionId: string | undefined): string | null {
|
|
481
|
+
if (!hasAntigravityOauth(config)) return null;
|
|
482
|
+
const pool = tierModelPool(tier, config)
|
|
483
|
+
.map(id => catalogMetadata(id))
|
|
484
|
+
.filter((m): m is CatalogModel => !!m && m.provider === "antigravity");
|
|
485
|
+
if (pool.length === 0) return null;
|
|
486
|
+
const byCompany = new Map<string, CatalogModel>();
|
|
487
|
+
for (const m of pool) {
|
|
488
|
+
const company = m.company ?? "antigravity";
|
|
489
|
+
const current = byCompany.get(company);
|
|
490
|
+
if (!current || compareStrengthAscending(current, m) < 0) byCompany.set(company, m);
|
|
491
|
+
}
|
|
492
|
+
if (byCompany.size < 2) return null; // one vendor in this tier -> nothing to spread across
|
|
493
|
+
const representatives = [...byCompany.values()].map(m => m.canonical).sort();
|
|
494
|
+
return selectFromPool(representatives, sessionId);
|
|
495
|
+
}
|
|
496
|
+
|
|
459
497
|
// --- Tier -> model/thinking resolution (inline off config.roles/config.routing.tiers; no static map). ---
|
|
460
498
|
|
|
461
499
|
/** Exported for `jeo doctor`'s routing-preview diagnostic (same resolution the real
|
|
@@ -466,8 +504,8 @@ export function resolveTierModel(tier: PromptTier, config: RoutingConfig, sessio
|
|
|
466
504
|
if (configured) return configured;
|
|
467
505
|
if (tier === "trivial") return config.roles?.smol || crossProviderPoolPick(tier, config, sessionId) || cheapestCredentialed(config) || config.defaultModel;
|
|
468
506
|
if (tier === "standard") return config.roles?.medium || config.roles?.high || crossProviderPoolPick(tier, config, sessionId) || config.defaultModel;
|
|
469
|
-
if (tier === "high") return config.roles?.high || config.roles?.medium || crossProviderPoolPick(tier, config, sessionId) || strongestMidTierCredentialed(config) || config.defaultModel;
|
|
470
|
-
if (tier === "complex") return config.roles?.xhigh || config.roles?.slow || crossProviderPoolPick(tier, config, sessionId) || strongestCredentialed(config) || config.defaultModel;
|
|
507
|
+
if (tier === "high") return config.roles?.high || config.roles?.medium || crossProviderPoolPick(tier, config, sessionId) || antigravityCompanyPoolPick(tier, config, sessionId) || strongestMidTierCredentialed(config) || config.defaultModel;
|
|
508
|
+
if (tier === "complex") return config.roles?.xhigh || config.roles?.slow || crossProviderPoolPick(tier, config, sessionId) || antigravityCompanyPoolPick(tier, config, sessionId) || strongestCredentialed(config) || config.defaultModel;
|
|
471
509
|
return config.defaultModel;
|
|
472
510
|
}
|
|
473
511
|
|
package/src/agent/session.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface SessionHeader {
|
|
|
13
13
|
/** Model id pinned to this session; restored on resume so each session can carry
|
|
14
14
|
* its own model independent of the global default (per-session model selection). */
|
|
15
15
|
model?: string;
|
|
16
|
+
/** Unsent input-box draft at last save, restored into the prompt on `/resume`
|
|
17
|
+
* (gajae-code parity: an in-progress prompt survives a quit + resume instead
|
|
18
|
+
* of being silently lost). Absent/empty when there was nothing to restore. */
|
|
19
|
+
draft?: string;
|
|
20
|
+
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
export interface SessionEntry {
|
|
@@ -416,6 +421,25 @@ export async function updateSessionModel(id: string, model: string, cwd = proces
|
|
|
416
421
|
return true;
|
|
417
422
|
}, cwd);
|
|
418
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Persist (or clear) the unsent input-box draft in a session's JSONL header so a
|
|
426
|
+
* later `/resume` restores it into the prompt (gajae-code parity). Empty text
|
|
427
|
+
* clears any stale draft rather than writing an empty string. No-op (no write)
|
|
428
|
+
* when the header's current draft already matches. Throws a clear Error if the
|
|
429
|
+
* session file does not exist.
|
|
430
|
+
*/
|
|
431
|
+
export async function updateSessionDraft(id: string, draft: string, cwd = process.cwd()): Promise<void> {
|
|
432
|
+
const trimmed = draft.trim();
|
|
433
|
+
const next = trimmed ? trimmed : undefined;
|
|
434
|
+
await rewriteSessionHeader(id, header => {
|
|
435
|
+
if ((header.draft ?? undefined) === next) return false;
|
|
436
|
+
if (next === undefined) delete header.draft;
|
|
437
|
+
else header.draft = next;
|
|
438
|
+
return true;
|
|
439
|
+
}, cwd);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
419
443
|
|
|
420
444
|
/**
|
|
421
445
|
* Delete a session file.
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `/route` slash-command handler extracted from launch.ts.
|
|
3
3
|
*
|
|
4
|
-
* Handles `/route [status|on|off|why]` (default/`status`: report the
|
|
5
|
-
* session routing state + the last decision, if any; `on`/`off`:
|
|
6
|
-
* SESSION-LOCAL override of `config.routing.enabled`; `why`: explain
|
|
7
|
-
* routing decision in detail
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Handles `/route [status|on|off|why|history]` (default/`status`: report the
|
|
5
|
+
* effective session routing state + the last decision, if any; `on`/`off`:
|
|
6
|
+
* toggle a SESSION-LOCAL override of `config.routing.enabled`; `why`: explain
|
|
7
|
+
* the last routing decision in detail; `history [n]`: print the last `n`
|
|
8
|
+
* (default 10) recorded decisions, one per line). This block shares mutable
|
|
9
|
+
* REPL state (`sessionRouteOverride`, `lastRouteDecision`, `routeHistory`)
|
|
10
|
+
* with `runTurn`'s own routing insertion, so the caller passes the current
|
|
11
|
+
* values in via an explicit context object and reads back a result object
|
|
12
|
+
* instead of this function closing over REPL state directly.
|
|
12
13
|
*
|
|
13
14
|
* Session-level toggle only — `sessionRouteOverride` is never persisted to
|
|
14
15
|
* `~/.jeo/config.json` (mirrors `/thinking` being session-only vs. `/model
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import type { RouteDecision } from "../../agent/prompt-router";
|
|
21
|
+
import type { RouteHistoryEntry } from "../../agent/route-history";
|
|
20
22
|
|
|
21
23
|
export interface RouteSlashCtx {
|
|
22
24
|
sessionRouteOverride: boolean | undefined;
|
|
@@ -28,9 +30,13 @@ export interface RouteSlashCtx {
|
|
|
28
30
|
* never changes routing's actual gating logic (`!sessionModel` in launch.ts's
|
|
29
31
|
* `runTurn` stays the single source of truth for whether routing engages). */
|
|
30
32
|
pinnedModel?: string;
|
|
33
|
+
/** Chronological (oldest-first) snapshot of this session's `RouteHistory`
|
|
34
|
+
* (`routeHistory.getAll()`), read fresh by the caller for `/route history`. */
|
|
35
|
+
routeHistory: RouteHistoryEntry[];
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
|
|
39
|
+
|
|
34
40
|
export interface RouteSlashResult {
|
|
35
41
|
/** Present only when changed by "on"/"off". */
|
|
36
42
|
sessionRouteOverride?: boolean | undefined;
|
|
@@ -38,7 +44,7 @@ export interface RouteSlashResult {
|
|
|
38
44
|
lines: string[];
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
const USAGE = "Usage: /route [status|on|off|why]";
|
|
47
|
+
const USAGE = "Usage: /route [status|on|off|why|history [n]]";
|
|
42
48
|
|
|
43
49
|
function isRealDecision(decision: RouteDecision | { note: string } | null): decision is RouteDecision {
|
|
44
50
|
return !!decision && "model" in decision;
|
|
@@ -61,13 +67,25 @@ function explainDecision(decision: RouteDecision | { note: string } | null): str
|
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
70
|
+
* Format the last `n` history entries (default 10) as one line each:
|
|
71
|
+
* `turn N: tier -> model (source, confidence X.XX)`, oldest of the selected
|
|
72
|
+
* window first — matches `getAll()`'s chronological ordering.
|
|
73
|
+
*/
|
|
74
|
+
function formatHistory(entries: RouteHistoryEntry[], n: number): string[] {
|
|
75
|
+
if (entries.length === 0) return ["No routing decisions recorded yet this session."];
|
|
76
|
+
const selected = n > 0 ? entries.slice(Math.max(0, entries.length - n)) : [];
|
|
77
|
+
if (selected.length === 0) return ["No routing decisions recorded yet this session."];
|
|
78
|
+
return selected.map((e) => `turn ${e.turnNumber}: ${e.tier} -> ${e.model} (${e.source}, confidence ${e.confidence.toFixed(2)})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Handle `/route [status|on|off|why|history]`. Extracted for the same reason
|
|
83
|
+
* as `/model`: shares REPL-local routing state with `runTurn` via an
|
|
84
|
+
* explicit ctx/result object rather than closing over it.
|
|
67
85
|
*/
|
|
68
86
|
export function runRouteSlash(input: string, ctx: RouteSlashCtx): RouteSlashResult {
|
|
69
87
|
const rest = input.slice("/route".length).trim();
|
|
70
|
-
const [sub] = rest.split(/\s+/).filter(Boolean);
|
|
88
|
+
const [sub, arg] = rest.split(/\s+/).filter(Boolean);
|
|
71
89
|
const effective = ctx.sessionRouteOverride ?? ctx.routingConfigEnabled;
|
|
72
90
|
|
|
73
91
|
if (!sub || sub === "status") {
|
|
@@ -102,5 +120,15 @@ export function runRouteSlash(input: string, ctx: RouteSlashCtx): RouteSlashResu
|
|
|
102
120
|
return { lines: explainDecision(ctx.lastRouteDecision) };
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
if (sub === "history") {
|
|
124
|
+
// Only a finite POSITIVE count selects a window; 0, negatives, and
|
|
125
|
+
// non-numeric args (`bogus`) all fall back to the default 10 rather than
|
|
126
|
+
// silently printing the empty-history message when decisions actually
|
|
127
|
+
// exist (that message must mean "nothing recorded", never "you asked for 0").
|
|
128
|
+
const parsed = arg !== undefined ? Number.parseInt(arg, 10) : 10;
|
|
129
|
+
const n = Number.isFinite(parsed) && parsed > 0 ? parsed : 10;
|
|
130
|
+
return { lines: formatHistory(ctx.routeHistory, n) };
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
return { lines: [`Unknown /route subcommand: ${sub}`, USAGE] };
|
|
106
134
|
}
|
|
@@ -47,6 +47,11 @@ export interface SessionSlashResult {
|
|
|
47
47
|
lastUserInput?: string;
|
|
48
48
|
/** Present only when changed (by `/session resume` re-seeding retry state). */
|
|
49
49
|
lastReply?: string;
|
|
50
|
+
/** Present only when changed (by `/session resume` restoring header.draft into
|
|
51
|
+
* the prompt box). */
|
|
52
|
+
draft?: string;
|
|
53
|
+
|
|
54
|
+
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/**
|
|
@@ -62,14 +67,18 @@ export async function runSessionSlash(input: string, ctx: SessionSlashCtx): Prom
|
|
|
62
67
|
let sessionModelChanged = false;
|
|
63
68
|
let lastUserInput: string | undefined;
|
|
64
69
|
let lastReply: string | undefined;
|
|
70
|
+
let draft: string | undefined;
|
|
71
|
+
|
|
65
72
|
|
|
66
73
|
const result = (): SessionSlashResult => ({
|
|
67
74
|
sessionId,
|
|
68
75
|
...(sessionModelChanged ? { sessionModel } : {}),
|
|
69
76
|
...(lastUserInput !== undefined ? { lastUserInput } : {}),
|
|
70
77
|
...(lastReply !== undefined ? { lastReply } : {}),
|
|
78
|
+
...(draft !== undefined ? { draft } : {}),
|
|
71
79
|
});
|
|
72
80
|
|
|
81
|
+
|
|
73
82
|
const tokens = input.substring(8).trim().split(/\s+/).filter(Boolean);
|
|
74
83
|
const sub = (tokens[0] ?? "").toLowerCase();
|
|
75
84
|
|
|
@@ -125,6 +134,10 @@ export async function runSessionSlash(input: string, ctx: SessionSlashCtx): Prom
|
|
|
125
134
|
sessionId = rid;
|
|
126
135
|
// Restore the model this session was last using (per-session model).
|
|
127
136
|
if (header.model) { sessionModel = header.model; sessionModelChanged = true; }
|
|
137
|
+
// Restore the unsent input-box draft left over at last save, if any.
|
|
138
|
+
if (header.draft) draft = header.draft;
|
|
139
|
+
|
|
140
|
+
|
|
128
141
|
// Seed /retry + reply marker from the last user/assistant turn.
|
|
129
142
|
lastUserInput = ""; lastReply = "";
|
|
130
143
|
for (let k = history.length - 1; k >= 1; k--) {
|
package/src/commands/launch.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { describeModel, describeAllProviders, describeProvider, resolveProvider,
|
|
|
52
52
|
import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
|
|
53
53
|
import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../agent/goal-verifier";
|
|
54
54
|
import { routePrompt, deriveCacheSessionKey, warnOnce, tierModelPool, selectFromPool, PROMPT_TIERS, withRoutingTierSetting, inferTierForModel, type PromptTier, type RouteDecision } from "../agent/prompt-router";
|
|
55
|
+
import { RouteHistory } from "../agent/route-history";
|
|
55
56
|
|
|
56
57
|
import { isRateLimitError, isUsageLimitError, isConnectionError } from "../util/retry";
|
|
57
58
|
|
|
@@ -109,6 +110,8 @@ import {
|
|
|
109
110
|
latestSessionId,
|
|
110
111
|
exportSession,
|
|
111
112
|
updateSessionModel,
|
|
113
|
+
updateSessionDraft,
|
|
114
|
+
|
|
112
115
|
appendCompaction,
|
|
113
116
|
resolveSessionRef,
|
|
114
117
|
} from "../agent/session";
|
|
@@ -659,6 +662,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
659
662
|
let sessionRouteOverride: boolean | undefined;
|
|
660
663
|
// Last routing decision (or the reason none applied) — /route why reads this.
|
|
661
664
|
let lastRouteDecision: RouteDecision | { note: string } | null = null;
|
|
665
|
+
// Bounded FIFO of this session's routing decisions — /route history reads this.
|
|
666
|
+
// Same lifecycle scope as `lastRouteDecision`: one instance per REPL session,
|
|
667
|
+
// never persisted to config.
|
|
668
|
+
const routeHistory = new RouteHistory();
|
|
662
669
|
// Computer-use session override: undefined = follow config.computer.enabled, true/false = /computer on|off wins this session.
|
|
663
670
|
let sessionComputerOverride: boolean | undefined;
|
|
664
671
|
|
|
@@ -747,6 +754,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
747
754
|
|
|
748
755
|
// pi-style session persistence: resume an existing session or create a new one.
|
|
749
756
|
let sessionId: string | undefined;
|
|
757
|
+
// Unsent input-box draft restored from a resumed session's header (gajae-code
|
|
758
|
+
// parity) — folded into the very first prompt once `queuedPromptInput` exists.
|
|
759
|
+
let restoredDraft = "";
|
|
760
|
+
|
|
750
761
|
let compactionSeq = 0;
|
|
751
762
|
if (!flags.noSession) {
|
|
752
763
|
if (flags.resumeInteractive) {
|
|
@@ -765,6 +776,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
765
776
|
for (const m of messages) history.push(m);
|
|
766
777
|
sessionId = id;
|
|
767
778
|
if (!initialSessionModel && header.model) sessionModel = header.model;
|
|
779
|
+
if (header.draft) restoredDraft = header.draft;
|
|
780
|
+
|
|
768
781
|
const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
|
|
769
782
|
console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
|
|
770
783
|
} catch (err) {
|
|
@@ -803,6 +816,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
803
816
|
// Restore the model this session was last using unless the CLI explicitly
|
|
804
817
|
// pinned one (flags.model/role/provider → initialSessionModel wins).
|
|
805
818
|
if (!initialSessionModel && header.model) sessionModel = header.model;
|
|
819
|
+
if (header.draft) restoredDraft = header.draft;
|
|
820
|
+
|
|
806
821
|
const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
|
|
807
822
|
console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
|
|
808
823
|
} catch (err) {
|
|
@@ -1025,6 +1040,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1025
1040
|
}
|
|
1026
1041
|
}
|
|
1027
1042
|
lastRouteDecision = routed ?? (routeVetoNote ? { note: routeVetoNote } : routingEnabled && (routeOverridesPin || !sessionModel) ? { note: "routing produced no decision (fail-open)" } : { note: "routing not active this turn" });
|
|
1043
|
+
// Only a REAL decision (not a `{note}` placeholder) is worth recording —
|
|
1044
|
+
// `/route history` exists to show what actually routed, not every turn's
|
|
1045
|
+
// gating outcome.
|
|
1046
|
+
if (routed) routeHistory.add(routed);
|
|
1028
1047
|
// Explicit /model always wins UNLESS an explicit `/route on` this session opted
|
|
1029
1048
|
// back into per-prompt routing (`routeOverridesPin`); otherwise `routed` is only
|
|
1030
1049
|
// ever computed when `!sessionModel`.
|
|
@@ -1465,9 +1484,19 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1465
1484
|
contextTokens = catalogMetadata(activeModel)?.contextTokens;
|
|
1466
1485
|
fallbackBaseDecision = { ...fallbackBaseDecision, model: activeModel };
|
|
1467
1486
|
lastRouteDecision = { ...fallbackBaseDecision, warning: `model '${previousModel}' ${routedFailureReason}; switched to equivalent '${activeModel}'` };
|
|
1487
|
+
// Post-call fallback: the pre-call `routed` pick (already recorded above,
|
|
1488
|
+
// if any) got superseded mid-turn — record the decision that ACTUALLY
|
|
1489
|
+
// served the turn so `/route history`/`why` reflect reality, not a
|
|
1490
|
+
// since-abandoned pre-call guess.
|
|
1491
|
+
routeHistory.add(lastRouteDecision);
|
|
1468
1492
|
const notice = `[route] model '${previousModel}' ${routedFailureReason}; switching to equivalent '${activeModel}' for this turn.`;
|
|
1469
|
-
if (tui)
|
|
1470
|
-
|
|
1493
|
+
if (tui) {
|
|
1494
|
+
const { provider: fallbackProvider } = await describeModel(activeModel, turnConfig);
|
|
1495
|
+
tui.events().onModelSwitch?.(activeModel, fallbackProvider);
|
|
1496
|
+
tui.events().onNotice?.(notice);
|
|
1497
|
+
} else {
|
|
1498
|
+
console.log(notice);
|
|
1499
|
+
}
|
|
1471
1500
|
result = await runLoopWithModel(activeModel, activeThinking, turnSessionKey);
|
|
1472
1501
|
}
|
|
1473
1502
|
|
|
@@ -2164,6 +2193,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2164
2193
|
// (the user explicitly invoked them — no second Enter).
|
|
2165
2194
|
const pendingMidTurnCommands: string[] = [];
|
|
2166
2195
|
const queuedPromptInput: PromptInputQueue = { pendingLines: pendingStdinLines, partial: "", pastedLines: [], inPaste: false };
|
|
2196
|
+
// Fold a restored `/resume` draft (session header `draft`) into the very first
|
|
2197
|
+
// prompt the same way a queued mid-turn prefill lands — visible in the input box,
|
|
2198
|
+
// Enter to run, Esc/Ctrl+U to discard (gajae-code parity: an in-progress prompt
|
|
2199
|
+
// survives a quit + resume instead of being silently lost).
|
|
2200
|
+
if (restoredDraft) queuedPromptInput.partial = restoredDraft;
|
|
2201
|
+
|
|
2167
2202
|
queueBusyInput = (chunk: string) => captureLivePromptInputChunk(queuedPromptInput, chunk);
|
|
2168
2203
|
queueBusyPasteActive = () => queuedPromptInput.inPaste;
|
|
2169
2204
|
queueBusySnapshot = () => ({
|
|
@@ -2175,6 +2210,19 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2175
2210
|
// default routes to the immediate-dispatch queue served at the top of the loop.
|
|
2176
2211
|
(jeoEnv("NO_MIDTURN_DISPATCH") === "1" ? pendingStdinLines : pendingMidTurnCommands).push(line);
|
|
2177
2212
|
};
|
|
2213
|
+
// Persist (or clear) the unsent input-box draft into the session header so a later
|
|
2214
|
+
// `/resume` restores it (gajae-code parity). Best-effort, mirrors persistSessionModel;
|
|
2215
|
+
// reads readline's own line buffer since that is the single source of truth for
|
|
2216
|
+
// "what's currently typed but not submitted" (same buffer the box-render/prefill
|
|
2217
|
+
// logic above reads from).
|
|
2218
|
+
const persistSessionDraft = async (): Promise<void> => {
|
|
2219
|
+
if (flags.noSession || !sessionId) return;
|
|
2220
|
+
const current = (rl as unknown as { line?: string }).line ?? queuedPromptInput.partial ?? "";
|
|
2221
|
+
try {
|
|
2222
|
+
await updateSessionDraft(sessionId, current, cwd);
|
|
2223
|
+
} catch { /* best-effort */ }
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2178
2226
|
// Bracketed-paste line routing at the PROMPT: readline strips the 2004 markers
|
|
2179
2227
|
// and replays pasted lines as synthetic keypresses, emitting paste-start /
|
|
2180
2228
|
// paste-end around them. Lines submitted INSIDE that window are intentional
|
|
@@ -3676,6 +3724,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3676
3724
|
if (pickResult.sessionModel !== undefined) sessionModel = pickResult.sessionModel;
|
|
3677
3725
|
if (pickResult.lastUserInput !== undefined) lastUserInput = pickResult.lastUserInput;
|
|
3678
3726
|
if (pickResult.lastReply !== undefined) lastReply = pickResult.lastReply;
|
|
3727
|
+
if (pickResult.draft) queuedPromptInput.partial = pickResult.draft;
|
|
3728
|
+
|
|
3679
3729
|
}
|
|
3680
3730
|
|
|
3681
3731
|
// Remote (Telegram) free-text reply / config command routing — wired here,
|
|
@@ -3877,7 +3927,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3877
3927
|
});
|
|
3878
3928
|
sessionId = result.sessionId;
|
|
3879
3929
|
if (result.sessionModel !== undefined) sessionModel = result.sessionModel;
|
|
3880
|
-
if (result.
|
|
3930
|
+
if (result.lastReply !== undefined) lastReply = result.lastReply;
|
|
3931
|
+
if (result.draft) queuedPromptInput.partial = result.draft;
|
|
3932
|
+
|
|
3881
3933
|
if (result.lastReply !== undefined) lastReply = result.lastReply;
|
|
3882
3934
|
continue;
|
|
3883
3935
|
}
|
|
@@ -4435,6 +4487,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4435
4487
|
routingConfigEnabled: !!cfgNow.routing?.enabled,
|
|
4436
4488
|
lastRouteDecision,
|
|
4437
4489
|
pinnedModel: sessionModel,
|
|
4490
|
+
routeHistory: routeHistory.getAll(),
|
|
4438
4491
|
});
|
|
4439
4492
|
if (routeResult.sessionRouteOverride !== undefined) sessionRouteOverride = routeResult.sessionRouteOverride;
|
|
4440
4493
|
for (const line of routeResult.lines) console.log(line);
|
|
@@ -4578,7 +4631,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4578
4631
|
}
|
|
4579
4632
|
}
|
|
4580
4633
|
disarmPreview(); // clear footer + restore full-screen scrolling before leaving the REPL
|
|
4634
|
+
await persistSessionDraft();
|
|
4581
4635
|
if (hardExitOnLoopEnd) {
|
|
4636
|
+
|
|
4582
4637
|
try {
|
|
4583
4638
|
disarmPreview();
|
|
4584
4639
|
out.write("\x1b[?25h\n");
|
package/src/tui/app.ts
CHANGED
|
@@ -92,6 +92,13 @@ export interface AgentEventsLike {
|
|
|
92
92
|
* item). The TUI ignores these; launch.ts uses them to persist the final reply's artifacts. */
|
|
93
93
|
onReasoningArtifactStream?(artifact: import("../ai/types").ReasoningArtifact): void;
|
|
94
94
|
onBudget?(limit: number, reason: string): void;
|
|
95
|
+
/** Fired when the active model changes mid-turn (post-call equivalent-pool fallback,
|
|
96
|
+
* launch.ts's `routeFallbackAttempt` loop) — updates the footer's model/provider
|
|
97
|
+
* label so the status bar reflects the model that actually serves the REST of the
|
|
98
|
+
* turn, instead of staying stuck on the pre-fallback model for the remainder of the
|
|
99
|
+
* render (the backend `lastRouteDecision`/`routeHistory` were already correct; only
|
|
100
|
+
* this footer label lagged). */
|
|
101
|
+
onModelSwitch?(model: string, provider?: string): void;
|
|
95
102
|
|
|
96
103
|
}
|
|
97
104
|
|
|
@@ -696,6 +703,17 @@ export class LaunchTui {
|
|
|
696
703
|
this.appendLedger(dim(`${mark} ${reason}`) + "\n", "notice");
|
|
697
704
|
this.draw();
|
|
698
705
|
},
|
|
706
|
+
onModelSwitch: (model: string, provider?: string) => {
|
|
707
|
+
// Mid-turn equivalent-pool fallback: the footer's model/provider label was
|
|
708
|
+
// frozen at construction time otherwise, so a post-call reroute (the model
|
|
709
|
+
// that ACTUALLY produces the rest of the reply) never showed in the status
|
|
710
|
+
// bar for the remainder of the turn — even though lastRouteDecision/
|
|
711
|
+
// routeHistory were already correct. Mutate the SAME footer object the
|
|
712
|
+
// render loop reads every frame; no separate re-render path needed.
|
|
713
|
+
this.footer.model = model;
|
|
714
|
+
this.footer.provider = provider;
|
|
715
|
+
this.draw();
|
|
716
|
+
},
|
|
699
717
|
onUsage: (u: { inputTokens: number; outputTokens: number }) => {
|
|
700
718
|
// Live cumulative token usage for the turn — shown in the final summary
|
|
701
719
|
// (and available to the footer meter).
|
|
@@ -43,7 +43,7 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
|
|
|
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
45
|
{ command: "/thinking", usage: "/thinking [level]", description: "Show or set thinking budget (low/medium/high/xhigh)", group: "models" },
|
|
46
|
-
{ command: "/route", usage: "/route [on|off|why]", description: "Show/toggle prompt-based model routing for this session,
|
|
46
|
+
{ command: "/route", usage: "/route [on|off|why|history [n]]", description: "Show/toggle prompt-based model routing for this session, explain the last routing decision, or list recent routing history", 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" },
|
|
49
49
|
{ command: "/view", usage: "/view <file> [a-b]", description: "Render a file with line numbers + light highlight", group: "code" },
|