jeo-code 0.7.49 → 0.7.50
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 +16 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/prompt-router.ts +18 -0
- package/src/commands/doctor.ts +23 -0
- package/src/commands/launch.ts +13 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ 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.50] - 2026-07-06
|
|
10
|
+
_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._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`jeo doctor` routing diagnostic** (`src/commands/doctor.ts`) — when `routing.enabled` is true but `roles.smol` is unset, `routePrompt`'s LLM-escalation path silently never fires every turn (it degrades to heuristic-only tier resolution, the same paradox `routePrompt` itself already guards against at runtime via its one-time `warnOnce`). `jeo doctor` now surfaces this proactively at onboarding time instead of only mid-session: a `routing: { enabled, smolConfigured, note? }` block in `--json` output, and a yellow `[routing]` note (or a terse green confirmation when `roles.smol` IS configured) printed after the OAuth section, before the final verdict. Purely informational — never affects the `ready`/`strict` exit-code gate.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Prompt-cache-key collision on mid-session model switch** (`src/agent/prompt-router.ts`'s new `deriveCacheSessionKey`, wired into `src/commands/launch.ts`'s `runTurn`) — `sessionKey` (forwarded as `prompt_cache_key`/`session_id` to provider requests) was the bare `sessionId`, unchanged across turns. Provider-side prompt caches are keyed PER MODEL, so when `routePrompt` switches `activeModel` between turns in the same session (its entire reason for existing), every switch silently guaranteed a cache miss on the new model — undermining the exact cost/latency savings routing exists to deliver. The derived key is now `` `${sessionId}:${activeModel}` ``: stable (same key) across consecutive turns on the same model, distinct (different key) the instant the model changes, with zero effect on `--no-session` (still forwards `undefined` unchanged).
|
|
17
|
+
|
|
18
|
+
### Verified
|
|
19
|
+
- `bun run typecheck` clean; `bun test` 2644 pass / 0 fail across 280 files — 2 consecutive full-suite runs, both clean.
|
|
20
|
+
- `test/doctor.test.ts` (+5 tests, 7 total): routing off → no `routing` block; routing on + `roles.smol` unset → note present with escalation-skip wording (both `--json` and human-output paths); routing on + `roles.smol` set → no note, `smolConfigured: true`; human-output note never flips the `ready`/`strict` verdict.
|
|
21
|
+
- `test/prompt-router.test.ts` (+4 tests, 36 total): `deriveCacheSessionKey` pure-function unit tests — same session+model → same key, different model → different key, key format, `--no-session` (undefined sessionId) short-circuit.
|
|
22
|
+
- `test/launch-prompt-routing.test.ts` (+2 tests, 6 total): real end-to-end proof through `runLaunchCommand` with a genuine persisted session (not `--no-session`) — two turns routed to the SAME model produce the SAME `sessionKey` (cache reuse preserved), two turns routed to DIFFERENT models produce DIFFERENT `sessionKey`s sharing the same session-id prefix (no false cross-model cache hit, still same session lineage).
|
|
23
|
+
- Independent verification pass (separate subagent, read-only, no code touched) re-derived every §0–§8 claim in the design doc directly from git history (`ff7816a^` pre-PromptRouter vs. current HEAD) rather than trusting the doc's own text — confirmed the core architecture claims, design-principle-5 tool-isolation guarantee (exhaustive grep: `RouteDecision` has no tool/permission field, zero cross-references between `routing.` and any subagent-toolset code), and the bilingual heuristic test corpus; also independently ran `graphify` itself (not grep proxies) to reproduce 3 of 5 named hub-degree figures from the doc almost exactly, while flagging that 2 of the doc's supporting dependency-direction claims (`state.ts`, `model-catalog*.ts` importing `model-manager.ts`) do not hold under direct import inspection.
|
|
24
|
+
|
|
9
25
|
## [0.7.49] - 2026-07-06
|
|
10
26
|
_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._
|
|
11
27
|
|
package/README.ja.md
CHANGED
|
@@ -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.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.
|
|
262
263
|
- **[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.
|
|
263
264
|
- **[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.
|
|
264
265
|
- **[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.
|
|
265
266
|
- **[0.7.46]** (2026-07-06) — Registry-only correction: `npm publish` packs the working-tree filesystem, not the git commit — a concurrent, unrelated, uncommitted feature-in-progress from another session sharing this checkout (`src/agent/prompt-router.ts`, `src/commands/launch/route-slash.ts`, and edits to `config-schema.ts`/`state.ts`/`launch.ts`/`slash.ts`) was physically present on disk during the 0.7.45 `npm publish` and got bundled into that tarball even though it was never committed to git and is absent from the `0.7.45` git tag/branch. Unpublished `jeo-code@0.7.45` from the registry within minutes (npm then permanently blocks republishing that exact version number, hence the bump to 0.7.46) and republished from a verified-clean working tree (`git stash` of the foreign files, `npm pack --dry-run` confirmed their absence, then restored the stash afterward so the other session's in-progress work was never touched or lost). No functional change versus the intended 0.7.45 content — see that entry below.
|
|
266
|
-
- **[0.7.45]** (2026-07-06) — gjc parity: jeo's subagent `task {tasks:[...]}` fan-out batches now visibly run as PARALLEL processes the way gjc's own task tool does, instead of quietly forcing the mutating executor role to serialize. Two compounding bugs made a batch of independent subagent tasks look and behave sequential even though the read-only roles were already technically concurrent: (1) the executor role's fan-out was hard-coded to concurrency 1 regardless of batch size, and (2) the TUI's live status line tracked ONE shared string clobbered by whichever worker's event landed last — worse, ANY single worker reaching "done" cleared the whole `(sub)` marker even while its siblings were still actively running.
|
|
267
267
|
|
|
268
268
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
269
269
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -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.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.
|
|
263
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.
|
|
264
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.
|
|
265
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.
|
|
266
267
|
- **[0.7.46]** (2026-07-06) — Registry-only correction: `npm publish` packs the working-tree filesystem, not the git commit — a concurrent, unrelated, uncommitted feature-in-progress from another session sharing this checkout (`src/agent/prompt-router.ts`, `src/commands/launch/route-slash.ts`, and edits to `config-schema.ts`/`state.ts`/`launch.ts`/`slash.ts`) was physically present on disk during the 0.7.45 `npm publish` and got bundled into that tarball even though it was never committed to git and is absent from the `0.7.45` git tag/branch. Unpublished `jeo-code@0.7.45` from the registry within minutes (npm then permanently blocks republishing that exact version number, hence the bump to 0.7.46) and republished from a verified-clean working tree (`git stash` of the foreign files, `npm pack --dry-run` confirmed their absence, then restored the stash afterward so the other session's in-progress work was never touched or lost). No functional change versus the intended 0.7.45 content — see that entry below.
|
|
267
|
-
- **[0.7.45]** (2026-07-06) — gjc parity: jeo's subagent `task {tasks:[...]}` fan-out batches now visibly run as PARALLEL processes the way gjc's own task tool does, instead of quietly forcing the mutating executor role to serialize. Two compounding bugs made a batch of independent subagent tasks look and behave sequential even though the read-only roles were already technically concurrent: (1) the executor role's fan-out was hard-coded to concurrency 1 regardless of batch size, and (2) the TUI's live status line tracked ONE shared string clobbered by whichever worker's event landed last — worse, ANY single worker reaching "done" cleared the whole `(sub)` marker even while its siblings were still actively running.
|
|
268
268
|
|
|
269
269
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
270
270
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -262,11 +262,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
262
262
|
## Changelog
|
|
263
263
|
|
|
264
264
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
265
|
+
- **[0.7.50]** (2026-07-06) — Design-doc audit of v0.7.47's PromptRouter, dispatched as two parallel subagents (independent re-verification + gap implementation) against the original Korean-language design report. The core proposal was confirmed shipped correctly (heuristic classifier, escalation, `/model`-always-wins, tool-selection isolation — all independently re-derived from git history and grep evidence, not just re-read from the doc). Two gaps the design doc itself had flagged as open questions (§7 risks #2 and #4) were real and are fixed here; a third finding (an internal contradiction in the design doc's own §3.2 vs §3.4 text on image-attachment gating — the SHIPPED code was already correct, only the doc's prose disagreed with itself) required no code change.
|
|
265
266
|
- **[0.7.49]** (2026-07-06) — gjc Telegram-daemon parity, Tier 2 (follow-up to 0.7.37/0.7.38/0.7.48's subagent-visibility-only daemon): adds a dynamic, PER-SESSION forum topic that mirrors that session's OWN activity — not just its subagents — and lets a Telegram reply steer it directly. Opt-in via `notifications.telegram.perSessionTopics`; the flat/global `topicId` path from 0.7.48 is UNCHANGED when it's off, so existing setups see zero regression.
|
|
266
267
|
- **[0.7.48]** (2026-07-06) — gjc Telegram-daemon parity, phase 2 (follow-up to 0.7.37/0.7.38's baseline subagent-visibility daemon, which deliberately scoped OUT forum topics, inline keyboards, and image attachments — see that entry): the daemon now speaks gjc's richer notification surface instead of the plain-text-only baseline.
|
|
267
268
|
- **[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
269
|
- **[0.7.46]** (2026-07-06) — Registry-only correction: `npm publish` packs the working-tree filesystem, not the git commit — a concurrent, unrelated, uncommitted feature-in-progress from another session sharing this checkout (`src/agent/prompt-router.ts`, `src/commands/launch/route-slash.ts`, and edits to `config-schema.ts`/`state.ts`/`launch.ts`/`slash.ts`) was physically present on disk during the 0.7.45 `npm publish` and got bundled into that tarball even though it was never committed to git and is absent from the `0.7.45` git tag/branch. Unpublished `jeo-code@0.7.45` from the registry within minutes (npm then permanently blocks republishing that exact version number, hence the bump to 0.7.46) and republished from a verified-clean working tree (`git stash` of the foreign files, `npm pack --dry-run` confirmed their absence, then restored the stash afterward so the other session's in-progress work was never touched or lost). No functional change versus the intended 0.7.45 content — see that entry below.
|
|
269
|
-
- **[0.7.45]** (2026-07-06) — gjc parity: jeo's subagent `task {tasks:[...]}` fan-out batches now visibly run as PARALLEL processes the way gjc's own task tool does, instead of quietly forcing the mutating executor role to serialize. Two compounding bugs made a batch of independent subagent tasks look and behave sequential even though the read-only roles were already technically concurrent: (1) the executor role's fan-out was hard-coded to concurrency 1 regardless of batch size, and (2) the TUI's live status line tracked ONE shared string clobbered by whichever worker's event landed last — worse, ANY single worker reaching "done" cleared the whole `(sub)` marker even while its siblings were still actively running.
|
|
270
270
|
|
|
271
271
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
272
272
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -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.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.
|
|
262
263
|
- **[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.
|
|
263
264
|
- **[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.
|
|
264
265
|
- **[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.
|
|
265
266
|
- **[0.7.46]** (2026-07-06) — Registry-only correction: `npm publish` packs the working-tree filesystem, not the git commit — a concurrent, unrelated, uncommitted feature-in-progress from another session sharing this checkout (`src/agent/prompt-router.ts`, `src/commands/launch/route-slash.ts`, and edits to `config-schema.ts`/`state.ts`/`launch.ts`/`slash.ts`) was physically present on disk during the 0.7.45 `npm publish` and got bundled into that tarball even though it was never committed to git and is absent from the `0.7.45` git tag/branch. Unpublished `jeo-code@0.7.45` from the registry within minutes (npm then permanently blocks republishing that exact version number, hence the bump to 0.7.46) and republished from a verified-clean working tree (`git stash` of the foreign files, `npm pack --dry-run` confirmed their absence, then restored the stash afterward so the other session's in-progress work was never touched or lost). No functional change versus the intended 0.7.45 content — see that entry below.
|
|
266
|
-
- **[0.7.45]** (2026-07-06) — gjc parity: jeo's subagent `task {tasks:[...]}` fan-out batches now visibly run as PARALLEL processes the way gjc's own task tool does, instead of quietly forcing the mutating executor role to serialize. Two compounding bugs made a batch of independent subagent tasks look and behave sequential even though the read-only roles were already technically concurrent: (1) the executor role's fan-out was hard-coded to concurrency 1 regardless of batch size, and (2) the TUI's live status line tracked ONE shared string clobbered by whichever worker's event landed last — worse, ANY single worker reaching "done" cleared the whole `(sub)` marker even while its siblings were still actively running.
|
|
267
267
|
|
|
268
268
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
269
269
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -271,3 +271,21 @@ export async function routePrompt(
|
|
|
271
271
|
return null;
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Derive the provider-side prompt-cache correlation key for a turn (forwarded as
|
|
277
|
+
* `sessionKey` -> `prompt_cache_key`/`session_id` in loop.ts/model-manager.ts/
|
|
278
|
+
* openai-responses.ts). Provider-side prompt caches are keyed PER MODEL — reusing
|
|
279
|
+
* the bare `sessionId` across a mid-session model switch (routePrompt changing
|
|
280
|
+
* `activeModel` turn to turn) would send the same cache-correlation key to a
|
|
281
|
+
* DIFFERENT model/provider, guaranteeing a cache miss on every switch and silently
|
|
282
|
+
* undermining the cost/latency savings routing exists for (design doc §7 risk #4).
|
|
283
|
+
* Scoping the key to `${sessionId}:${model}` gives each model its own cache
|
|
284
|
+
* lineage within the session: same model across turns -> same key (cache reuse
|
|
285
|
+
* preserved), different model -> different key (no false cross-model cache hit).
|
|
286
|
+
* Deliberately excludes any turn counter/timestamp — the key must stay STABLE
|
|
287
|
+
* across consecutive turns that keep the same model.
|
|
288
|
+
*/
|
|
289
|
+
export function deriveCacheSessionKey(sessionId: string, model: string): string {
|
|
290
|
+
return `${sessionId}:${model}`;
|
|
291
|
+
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -170,6 +170,19 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
|
|
|
170
170
|
const defaultProvider = resolveProvider(resolvedModel);
|
|
171
171
|
const ollamaBase = config.ollamaBaseUrl ?? "http://localhost:11434";
|
|
172
172
|
|
|
173
|
+
// Routing diagnostic (design doc §7 risk #2): routing.enabled without roles.smol
|
|
174
|
+
// means routePrompt()'s LLM-escalation path silently never fires — resolveRoleModel
|
|
175
|
+
// falls back to defaultModel, which is exactly the "cheap escalation call is actually
|
|
176
|
+
// the expensive default model" paradox routePrompt() itself guards against at runtime
|
|
177
|
+
// (see prompt-router.ts's warnOnce/SMOL_UNCONFIGURED_WARNING_KEY). Surface it here
|
|
178
|
+
// proactively, at onboarding time, instead of only mid-session on the first qualifying
|
|
179
|
+
// turn.
|
|
180
|
+
const routingEnabled = !!config.routing?.enabled;
|
|
181
|
+
const smolConfigured = !!config.roles?.smol;
|
|
182
|
+
const routingNote = routingEnabled && !smolConfigured
|
|
183
|
+
? "routing is enabled but roles.smol is unset — LLM escalation for ambiguous prompts will never fire; heuristic-only tier resolution applies every turn. Set roles.smol to enable escalation."
|
|
184
|
+
: undefined;
|
|
185
|
+
|
|
173
186
|
// --- Gather (probes run concurrently → ~1× the slowest timeout, not N×) ---
|
|
174
187
|
const probes: { name: string; credKind: string; result: ProbeResult }[] = [];
|
|
175
188
|
const cloud = ["anthropic", "openai", "gemini"] as AuthProvider[];
|
|
@@ -235,6 +248,7 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
|
|
|
235
248
|
})),
|
|
236
249
|
oauth: oauthHealth,
|
|
237
250
|
ready,
|
|
251
|
+
...(routingEnabled ? { routing: { enabled: true, smolConfigured, ...(routingNote ? { note: routingNote } : {}) } } : {}),
|
|
238
252
|
};
|
|
239
253
|
console.log(JSON.stringify(report, null, 2));
|
|
240
254
|
if (strict && !ready) process.exit(1);
|
|
@@ -276,6 +290,15 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
|
|
|
276
290
|
for (const line of oauthLines) console.log(line);
|
|
277
291
|
console.log("");
|
|
278
292
|
}
|
|
293
|
+
// Routing diagnostic: informational only — never affects ready/strict exit logic.
|
|
294
|
+
if (routingEnabled) {
|
|
295
|
+
if (routingNote) {
|
|
296
|
+
console.log(`${chalk.yellow("[routing]")} ${routingNote}`);
|
|
297
|
+
} else {
|
|
298
|
+
console.log(`${chalk.green("[routing]")} enabled, roles.smol configured — LLM escalation available on ambiguous prompts.`);
|
|
299
|
+
}
|
|
300
|
+
console.log("");
|
|
301
|
+
}
|
|
279
302
|
|
|
280
303
|
// Final verdict
|
|
281
304
|
if (ready) {
|
package/src/commands/launch.ts
CHANGED
|
@@ -48,7 +48,7 @@ import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recen
|
|
|
48
48
|
import { describeModel, describeAllProviders, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, resolveRoleModel, CODEX_MODELS, qualifyModelId } from "../ai";
|
|
49
49
|
import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
|
|
50
50
|
import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../agent/goal-verifier";
|
|
51
|
-
import { routePrompt, type RouteDecision } from "../agent/prompt-router";
|
|
51
|
+
import { routePrompt, deriveCacheSessionKey, type RouteDecision } from "../agent/prompt-router";
|
|
52
52
|
|
|
53
53
|
import { listAliases } from "../ai/model-registry";
|
|
54
54
|
import { openaiCompatDef, SUBSCRIPTION_PROVIDER_NAMES } from "../ai/providers/openai-compatible-catalog";
|
|
@@ -891,6 +891,16 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
891
891
|
// Explicit /model always wins: `routed` is only ever computed when `!sessionModel`.
|
|
892
892
|
const activeModel = routed?.model ?? (sessionModel || defaultModel);
|
|
893
893
|
const activeThinking = routed?.thinking ?? sessionThinking;
|
|
894
|
+
// Provider-side prompt caches are keyed PER MODEL — reusing the bare `sessionId`
|
|
895
|
+
// across a mid-session model switch (routePrompt changing activeModel turn to
|
|
896
|
+
// turn) sends the same cache-correlation key to a different model/provider,
|
|
897
|
+
// guaranteeing a cache miss on every switch and silently undermining routing's
|
|
898
|
+
// own cost/latency goal (design doc §7 risk #4). Scope the key to the resolved
|
|
899
|
+
// model so each model keeps its own stable cache lineage within the session.
|
|
900
|
+
// `sessionId` is undefined only under `--no-session` — in that case there is no
|
|
901
|
+
// provider-side cache lineage to protect anyway, so forward `undefined` unchanged
|
|
902
|
+
// (matches the prior behavior of `sessionKey: sessionId` for that mode).
|
|
903
|
+
const turnSessionKey = sessionId ? deriveCacheSessionKey(sessionId, activeModel) : undefined;
|
|
894
904
|
const contextTokens = catalogMetadata(activeModel)?.contextTokens;
|
|
895
905
|
|
|
896
906
|
// Resolve provider + dirty count up front — both are cheap and feed the live
|
|
@@ -1201,7 +1211,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1201
1211
|
maxTokens: resolveMaxOutputTokens(activeModel, activeThinking),
|
|
1202
1212
|
reasoningEffort: activeThinking ? thinkingToReasoningEffort(activeThinking) : undefined,
|
|
1203
1213
|
signal: ac.signal,
|
|
1204
|
-
sessionKey:
|
|
1214
|
+
sessionKey: turnSessionKey,
|
|
1205
1215
|
steer: drainSteer,
|
|
1206
1216
|
events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, persistTurnTail), opik),
|
|
1207
1217
|
});
|
|
@@ -1221,7 +1231,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1221
1231
|
maxTokens: resolveMaxOutputTokens(activeModel, activeThinking),
|
|
1222
1232
|
reasoningEffort: activeThinking ? thinkingToReasoningEffort(activeThinking) : undefined,
|
|
1223
1233
|
signal: ac.signal,
|
|
1224
|
-
sessionKey:
|
|
1234
|
+
sessionKey: turnSessionKey,
|
|
1225
1235
|
steer: drainSteer,
|
|
1226
1236
|
events: wrapEvents(withToolDetailCapture(tui ? tui.events() : streamEvents), opik),
|
|
1227
1237
|
});
|