From c7891d5c9d028605da4cad9d01bb6fe77e106665 Mon Sep 17 00:00:00 2001 From: tachyon6 Date: Wed, 11 Nov 2026 21:01:14 +0900 Subject: [PATCH] feat(jev): decide effort on every pinned turn with sessionPin.effort per-turn --- docs-site/src/content/docs/guides/combos.md | 26 ++++++- .../docs/reference/configuration/routing.md | 2 +- gui/src/combo-workspace-data.ts | 1 + .../combo-workspace-jev-decision.tsx | 6 +- .../references/01_surface_agents-routing.md | 3 +- src/cli/capabilities-agents-routing.ts | 4 +- src/cli/combo.ts | 21 +++++- src/combos/jev-session-pin-config.ts | 31 ++++++--- src/server/responses/core-combo.ts | 31 +++++++-- src/usage/jev-stats.ts | 19 +++-- structure/providers/jev-decision.md | 9 +++ tests/cli/cli-combo-session-pin.test.ts | 17 +++++ tests/routing/jev-session-pin.test.ts | 25 ++++++- tests/server/server-jev-session-pin.test.ts | 69 +++++++++++++++++++ 14 files changed, 230 insertions(+), 34 deletions(-) diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index e1ccfd9a4f3..61004386774 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -320,14 +320,36 @@ JEV decides again, and the new decision replaces the pin, when: - any send of a pinned session fails (that request still fails over as usual); - the configuration has changed since the pin was written. +#### Deciding effort on every pinned turn + +A pin keeps the effort of the first decision by default. A session that starts with a planning +request and continues with small edits then pays for that effort on every turn. Set +`effort: "per-turn"` to keep the pinned target, and its prompt cache, while JEV chooses the effort +again on every turn: + +```bash +ocx combo set jev-auto --session-pin 20m --session-pin-effort per-turn --json +``` + +```json +{ "strategy": "jev", "sessionPin": { "ttlMs": 1200000, "effort": "per-turn" } } +``` + +Each reused turn sends one decision request that offers only the pinned target and its efforts. +If JEV answers with an effort for that target, the turn uses it and the pin keeps the new effort. +If the call fails, times out or answers anything else, the turn uses the pinned effort as it would +without this setting. A target with a single effort skips the call. `--session-pin-effort pinned` +returns to the default, and changing the idle window with `--session-pin` keeps the mode. + A cancelled request keeps the pin. A fail-open decision (timeout, missing key, invalid answer) never creates one. Pins live in the proxy's memory, at most 512 sessions, and do not survive a restart. -Usage records mark a reused turn with `jevDecision.pinned: true` and `pinReason: "session"`. A +Usage records mark a reused turn with `jevDecision.pinned: true` and `pinReason: "session"`, or +`pinReason: "session_effort"` when a per-turn effort decision set its effort. A decision that replaced a released pin records why in `pinReason` (`expired`, `config_changed`, `target_unavailable`, `effort_unavailable` or `quota_nearly_exhausted`). `ocx combo stats` -counts pinned requests apart from decisions; they still count toward successful requests, model +counts pinned requests apart from decisions, and also counts a `session_effort` turn as a decision; they still count toward successful requests, model fallbacks and per-model tokens. A pin makes provider cache reuse possible; whether the provider actually serves cached tokens still diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index ac5e26bf3ad..18d2ffbc711 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -179,7 +179,7 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, | `decisionTimeoutMs?` | `number` | `4000` | `strategy: "jev"` only. Decision deadline before failing open, 1000–120000 ms. | | `decisionQuotaSignals?` | `boolean` | `false` | JEV only. Explicit `true` sends loaded advisory quota evidence to the selected backend; never changes hard eligibility or account selection. | | `decisionQuotaTiers?` | `{ moderate?: number; limited?: number; nearlyExhausted?: number }` | `{ limited: 70, nearlyExhausted: 90 }` | JEV only. Partial overrides merge with defaults; finite 0–100 values must be strictly ascending. `moderate` is absent unless explicit. Does not enable signals by itself. | -| `sessionPin?` | `{ ttlMs: number }` | off | JEV only. Keeps an explicit client session on the target JEV chose for it while the session stays active; `ttlMs` is the idle window, 60000–86400000. `null` clears. See [Session pin for JEV](/guides/combos/#session-pin-for-jev). | +| `sessionPin?` | `{ ttlMs: number, effort?: "pinned" \| "per-turn" }` | off | JEV only. Keeps an explicit client session on the target JEV chose for it while the session stays active; `ttlMs` is the idle window, 60000–86400000. `effort: "per-turn"` keeps the target but lets JEV choose its effort on every turn. `null` clears. See [Session pin for JEV](/guides/combos/#session-pin-for-jev). | Quota settings, privacy and unknown-pool semantics are described in [Advisory quota signals for JEV](/guides/combos/#advisory-quota-signals-for-jev). diff --git a/gui/src/combo-workspace-data.ts b/gui/src/combo-workspace-data.ts index 84bbc12198c..5654f2bd81d 100644 --- a/gui/src/combo-workspace-data.ts +++ b/gui/src/combo-workspace-data.ts @@ -498,6 +498,7 @@ export function draftEquals(a: ComboItem, b: ComboItem): boolean { || (a.decisionTimeoutMs ?? null) !== (b.decisionTimeoutMs ?? null) || (a.sessionPin === undefined) !== (b.sessionPin === undefined) || (a.sessionPin?.ttlMs ?? null) !== (b.sessionPin?.ttlMs ?? null) + || (a.sessionPin?.effort ?? null) !== (b.sessionPin?.effort ?? null) )) ) return false; if (a.targets.length !== b.targets.length) return false; diff --git a/gui/src/components/combo-workspace-jev-decision.tsx b/gui/src/components/combo-workspace-jev-decision.tsx index 869f4e9caef..0b6493db140 100644 --- a/gui/src/components/combo-workspace-jev-decision.tsx +++ b/gui/src/components/combo-workspace-jev-decision.tsx @@ -308,12 +308,14 @@ export function ComboJevDecisionSection({ aria-invalid={pinInvalid || undefined} aria-describedby={pinInvalid ? `${idPrefix}-session-pin-hint ${idPrefix}-session-pin-error` : `${idPrefix}-session-pin-hint`} onChange={(e) => { + // Editing the idle window keeps a per-turn effort mode set through the CLI or API. + const kept = sessionPin.effort ? { effort: sessionPin.effort } : {}; if (e.target.value === "") { - onChange({ sessionPin: { ttlMs: 0 } }); + onChange({ sessionPin: { ttlMs: 0, ...kept } }); return; } const ttlMs = Number(e.target.value); - onChange({ sessionPin: { ttlMs: Number.isFinite(ttlMs) ? ttlMs : 0 } }); + onChange({ sessionPin: { ttlMs: Number.isFinite(ttlMs) ? ttlMs : 0, ...kept } }); }} /> {pinInvalid &&