From dd5b7c5722d15cb9912d3bc03d6bb4392c0af752 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 28 Aug 2026 10:37:39 +0900 Subject: [PATCH 1/5] =?UTF-8?q?devlog(260828=5Fcursor=5Fumbrella=5Fcatalog?= =?UTF-8?q?):=20roadmap=20unit=20=E2=80=94=20senpi/omo=20analysis,=20curre?= =?UTF-8?q?nt=20surface,=20umbrella=20design,=20decade=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../000_plan.md | 25 ++++++++++ .../001_reference_analysis.md | 37 ++++++++++++++ .../002_current_surface.md | 21 ++++++++ .../003_design.md | 48 +++++++++++++++++++ .../010_capability_core.md | 42 ++++++++++++++++ .../020_catalog_integration.md | 43 +++++++++++++++++ .../030_closure.md | 12 +++++ 7 files changed, 228 insertions(+) create mode 100644 devlog/_plan/260828_cursor_umbrella_catalog/000_plan.md create mode 100644 devlog/_plan/260828_cursor_umbrella_catalog/001_reference_analysis.md create mode 100644 devlog/_plan/260828_cursor_umbrella_catalog/002_current_surface.md create mode 100644 devlog/_plan/260828_cursor_umbrella_catalog/003_design.md create mode 100644 devlog/_plan/260828_cursor_umbrella_catalog/010_capability_core.md create mode 100644 devlog/_plan/260828_cursor_umbrella_catalog/020_catalog_integration.md create mode 100644 devlog/_plan/260828_cursor_umbrella_catalog/030_closure.md diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/000_plan.md b/devlog/_plan/260828_cursor_umbrella_catalog/000_plan.md new file mode 100644 index 0000000000..b6ce8991fe --- /dev/null +++ b/devlog/_plan/260828_cursor_umbrella_catalog/000_plan.md @@ -0,0 +1,25 @@ +# 260828 cursor umbrella catalog — unit plan (wp1 docs-only) + +Goal: collapse the 69-row hand-maintained cursor picker into per-base umbrella +rows (thinking merged, fast inside, 1M/Max-Mode generalized), sourced from a +single capability module informed by senpi's architecture but cleaner. + +## Loop-spec + +- Archetype: spec-satisfaction. Verifiers per phase: bun test , tsc, + privacy scan, catalog sync output row counts. Repo-wide suite forbidden. +- References: senpi (scratch path in /tmp/senpi-scratch.txt) — capability + table/grouping/selection cited in 001; omo-ai@beta has NO cursor model map + (verified — provider-map.json is a provider-name alias list only). +- Non-goals: other providers, releases, protobuf schema changes. +- Bounds: ~10h wall; stacked PRs codex/* -> dev pre-approved, --no-verify ok, + unlimited subagents (sol + xai/grok-4.6). +- Terminal: DONE per goalplan c1-c5; NEEDS_HUMAN for user-visible id renames + beyond aliasing. + +## Work-phase map + +- wp1 docs (this cycle): 000-002 research + 010/020/030 decade docs. +- wp2 (010): capability core module + variant grammar + umbrella grouping. +- wp3 (020): catalog integration (discovery/registry/sync/request path). +- wp4 (030): closure — cleanliness comparison + picker proof + stack final. diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/001_reference_analysis.md b/devlog/_plan/260828_cursor_umbrella_catalog/001_reference_analysis.md new file mode 100644 index 0000000000..e7b1489c86 --- /dev/null +++ b/devlog/_plan/260828_cursor_umbrella_catalog/001_reference_analysis.md @@ -0,0 +1,37 @@ +# 001 — senpi/omo architecture analysis (sol-high lane, verified) + +## senpi (packages/ai/src/cursor/*) + +- CURSOR_MODEL_CAPABILITIES (model-capabilities.ts:81-195): 34 capability ids, + schema { evidence, window, maxWindow?, parameterOrder, defaultContext?, + requestContext?, levels: {level -> {value, encoding: parameters|variant-id}} }. + Claude order [thinking,context,effort]; GPT [context,reasoning,fast]. + 1M via requestContext="1m" when window>=1M. +- Variant grammar (model-capabilities.ts:204-248): strip terminal -fast; then + -thinking- | --thinking | -thinking | -; tokens + minimal|low|medium|high|extra-high|xhigh|max|none. +- Grouping (catalog-grouping.ts): group key = targetId + fast — FAST IS A + SEPARATE GROUP; Claude-only thinkingMode split (isClaude guard :45-47); + members with efforts collapse to one entry with thinkingLevelMap; 336-row + generated alias JSON maps live ids -> {targetId, level, legacyVariantId}. +- Wire (selection-descriptor.ts:85-120): alias-first — send catalog-served + suffix id when known (Cursor Run rejects bare capability ids with Connect + not_found, issue #1008); parameters fallback only when no alias; fast + parameter hardcoded "false" (fast reachable only via separate fast ids). +- Discovery (cursor-agent.ts:4362-4495): 1M inferred from display-name /\b1m\b/i + labels OR maxMode on /claude|gemini/ ids; reads thinkingDetails for + reasoning flag; multimodal from id pattern. + +## senpi weaknesses (our targets) + +1. Fast modeled twice (parameter always false + separate groups) — incoherent. +2. Claude-only thinkingMode split — separate thinking identities remain rows. +3. Truth split across static TS table + 336-row generated JSON + name regex. +4. variant-id fallback silently degrades to representative id. + +## omo-ai@beta + +provider-map.json contains ZERO cursor model rows (cursor only in +builtinProviderIds; 5 provider-name aliases). Cursor architecture is delegated +to its pinned senpi runtime. Nothing to adopt beyond "don't do this" — +objective's cleanliness bar vs omo is met by having any self-contained map. diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/002_current_surface.md b/devlog/_plan/260828_cursor_umbrella_catalog/002_current_surface.md new file mode 100644 index 0000000000..28b74c3408 --- /dev/null +++ b/devlog/_plan/260828_cursor_umbrella_catalog/002_current_surface.md @@ -0,0 +1,21 @@ +# 002 — current opencodex cursor surface (grok-4.6 lane, verified) + +- effort-map.ts (229 L): CURSOR_MODEL_EFFORT_TIERS 46 hand-kept ids; + CURSOR_THINKING_FAMILIES 13 ids with per-family wire order; consumers: + discovery.ts + request-builder.ts only. +- discovery.ts: 69-row static seed (4 router + 52 + 13 thinking); + inferCursorContextWindow (:27-38) hardcodes per-family windows; synthetic + ultra marker = kimi-k3-1m ONLY (:158-174); live merge is a FILTER (never + adds rows, provider-fetch.ts:1276); claude-opus-5 quarantined; dead + CURSOR_REASONING_EFFORTS const. +- live-models.ts: decode keeps modelId + maxMode only; DISCARDS displayName, + displayNameShort, displayModelId, aliases, thinkingDetails; maxModeModels + returned but unconsumed. +- sync.ts/effort.ts: picker rows cursor/, efforts via + cursorModelReasoningEfforts; synthetic max+ultra appended (effort.ts:219); + kimi-k3-1m default effort falls to high (not pinned). +- request path (request-builder.ts:189, protobuf-request.ts:996): suffix-id + first; parameters only for grok-fast / router level / maxMode(ultra); + thinkingDetails never sent. +- Duplicate rows today: 13 thinking + 7 fast + 2 x 1m = 22 of 69 are variants + of a base. diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/003_design.md b/devlog/_plan/260828_cursor_umbrella_catalog/003_design.md new file mode 100644 index 0000000000..20ee9b1909 --- /dev/null +++ b/devlog/_plan/260828_cursor_umbrella_catalog/003_design.md @@ -0,0 +1,48 @@ +# 003 — umbrella design (locks the shape both implementation phases build) + +## Principles (beats senpi where it is weak) + +1. ONE source of truth: a single capability module owns the variant grammar, + per-base levels, thinking/fast/1M dimensions, and wire encoding. No second + generated alias JSON (senpi weakness 3): aliases are DERIVED by the grammar, + not enumerated. +2. Thinking MERGES into the base identity for every family (no Claude-only + split — senpi weakness 2). A base with thinking variants routes efforts + through its thinking wire ids; bare non-thinking ids remain aliases. +3. Fast is a dimension INSIDE the umbrella (senpi weakness 1): no fast picker + rows; cursor/-fast stays routable as an alias that sets fast mode on + the same umbrella identity. +4. 1M/Max-Mode generalized: every base whose capability declares bigContext + (statically: claude/gemini/kimi/gpt-5.6 families per senpi windows; live: + maxModeModels from GetUsableModels — currently decoded but DISCARDED) + exposes the ultra effort -> maxMode=true wire flag. kimi-k3-1m stops being + the lone synthetic and becomes an alias. +5. Back-compat absolute: every one of today's 69 ids (and live suffix ids) + resolves through the grammar to (base, level?, thinking?, fast?, ultra?). + +## Picker shape (after) + +- Rows: 4 router + ~30 base umbrellas (from 69). Efforts per row from the + capability ladder; ultra appended only for bigContext-capable rows. +- Codex effort -> wire: suffix-id-first (Cursor rejects bare capability ids, + senpi #1008 confirmed + our own request-builder already suffix-first). + Thinking-capable base + effort E -> thinking wire id at E (family wire + order preserved from CURSOR_THINKING_FAMILIES). ultra -> base ladder top + + maxMode=true. Fast alias -> {stem}-{E}-fast. + +## Module plan + +- NEW src/adapters/cursor/catalog.ts: capability table (schema: + { levels: readonly string[], thinking?: { wireOrder }, fast?: true, + bigContext?: true, window, quarantined?: true }), parseCursorVariantId + (senpi grammar: strip -fast; -thinking- | --thinking | -thinking + | - | -1m), resolveCursorSelection(baseOrAlias, codexEffort) -> + { wireId | wireBase+params, maxMode, fast }, umbrellaCatalog() -> + picker rows. effort-map.ts becomes a thin re-export shim during wp2 and is + DELETED in wp3 once consumers move. + +## NEEDS_HUMAN boundary + +Picker row ids stay cursor/ (already true for bases). Removing separate +thinking/fast/1m ROWS changes what the picker lists but not what routes — +within the user's explicit instruction, so not escalated. diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/010_capability_core.md b/devlog/_plan/260828_cursor_umbrella_catalog/010_capability_core.md new file mode 100644 index 0000000000..768dcb490a --- /dev/null +++ b/devlog/_plan/260828_cursor_umbrella_catalog/010_capability_core.md @@ -0,0 +1,42 @@ +# 010 — wp2: capability core module (PR A, codex/cursor-umbrella-core -> dev) + +## Changes + +### 1. ADD src/adapters/cursor/catalog.ts + +- export interface CursorCapability { levels: readonly CursorEffort[]; + thinking?: { order: "thinking-then-effort" | "effort-then-thinking" | "bare" }; + fast?: boolean; bigContext?: boolean; window: number; quarantined?: boolean } +- export const CURSOR_CAPABILITIES: Record — + seeded from today's CURSOR_MODEL_EFFORT_TIERS (46) + CURSOR_THINKING_FAMILIES + (13) + senpi windows (001 table), collapsed to ~30 base entries: thinking + variants become thinking:{order} on the base; -fast entries become fast:true; + kimi-k3-1m becomes bigContext:true on kimi-k3; claude/gemini/gpt-5.6 windows + per senpi (1M) with bigContext on claude+gemini+kimi families. +- export function parseCursorVariantId(id): { baseId, level?, thinking, + fast, ultra } — senpi grammar + our -1m suffix; level tokens + minimal|low|medium|high|extra-high|xhigh|max|none. +- export function resolveCursorSelection(pickedId, codexEffort?): + { wireId, maxMode, params: [] } — suffix-id-first composition reusing the + order rules currently in cursorWireModelIdWithEffort; ultra -> + top-level + maxMode when bigContext; grok fast keeps the parameter path. +- export function cursorUmbrellaRows(): { id, efforts, defaultEffort, + window, bigContext }[] — picker list derivation (router ids stay in + discovery). + +### 2. Tests — ADD tests/cursor-catalog.test.ts + +Named activation per branch: grammar round-trip for ALL 69 legacy ids +(fixture list frozen from discovery.ts seed) -> every id parses to a known +base; thinking merge (claude-opus-5 + high -> claude-opus-5-thinking-high); +bare-thinking families (claude-4-sonnet) ignore effort; fast alias +resolution; ultra on bigContext base -> maxMode + top effort; ultra on +non-bigContext -> clamps to max, no maxMode; quarantine row excluded from +umbrella rows; unknown id passthrough unchanged. + +### 3. NO consumer changes in this PR (effort-map untouched) — additive +module + tests only, so the diff reviews clean. + +## Verifiers + +bun test tests/cursor-catalog.test.ts; bun x tsc --noEmit; privacy scan. diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/020_catalog_integration.md b/devlog/_plan/260828_cursor_umbrella_catalog/020_catalog_integration.md new file mode 100644 index 0000000000..cc327b8271 --- /dev/null +++ b/devlog/_plan/260828_cursor_umbrella_catalog/020_catalog_integration.md @@ -0,0 +1,43 @@ +# 020 — wp3: integration (PR B, codex/cursor-umbrella-wire, stacked on PR A) + +## Changes + +### 1. MODIFY src/adapters/cursor/discovery.ts + +- CURSOR_STATIC_MODELS: replace 52+13 explicit rows with rows generated from + cursorUmbrellaRows() (+ 4 router rows kept literal). claude-4-sonnet-1m + stays (real wire id) as alias metadata. +- CURSOR_ULTRA_1M_MODEL_IDS + cursorUltraBaseModelId: reimplement over + parseCursorVariantId ultra dimension (any bigContext base), keeping the + kimi-k3-1m alias. +- filterCursorConfiguredModelsByLiveDiscovery: match live suffix ids via + parseCursorVariantId(base match) instead of enumerated suffix compose. +- inferCursorContextWindow: read window from CURSOR_CAPABILITIES first, + fall through to current heuristics for unknown ids. + +### 2. MODIFY src/adapters/cursor/live-models.ts — stop discarding maxMode: +return maxModeModels and thread into provider-fetch so live maxMode marks +bigContext on matching bases (union with static flags). + +### 3. MODIFY src/providers/registry.ts cursor section — efforts from +cursorUmbrellaRows(); defaults: keep kimi-k3 max; thinking-merged rows +default per capability. + +### 4. MODIFY src/adapters/cursor/request-builder.ts — normalizeCursorModelId +/ effort composition delegate to resolveCursorSelection; grok-fast parameter +path preserved; ultra path generalized (maxMode for any bigContext base). + +### 5. DELETE src/adapters/cursor/effort-map.ts once discovery + +request-builder consume catalog.ts; migrate any residual export the tests +reference. + +### 6. Tests — MODIFY tests near existing cursor suites: discovery filter +with live suffix fixtures; registry efforts snapshot (row count ~34); +request-builder wire ids unchanged for every legacy fixture (byte-equal +wire id table test — the back-compat proof); sync row-count before/after. + +## Verifiers + +bun test tests/cursor-catalog.test.ts tests/cursor-hardening.test.ts ++ discovery/sync-focused files; tsc; privacy scan; catalog sync dry-run +row output captured for 030. diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/030_closure.md b/devlog/_plan/260828_cursor_umbrella_catalog/030_closure.md new file mode 100644 index 0000000000..d4e114e848 --- /dev/null +++ b/devlog/_plan/260828_cursor_umbrella_catalog/030_closure.md @@ -0,0 +1,12 @@ +# 030 — wp4: closure + +1. Cleanliness comparison table: rows before/after (69 -> ~34), effort-map + 229 LOC deleted vs catalog.ts added, single-module truth vs senpi's + 3-surface split (static TS + 336-row JSON + regex) vs omo's absent map; + thinking merged for ALL families (senpi: Claude split remains); fast as + dimension (senpi: separate groups). +2. Picker proof: opencodex-catalog.json cursor section before/after row + counts + one umbrella row excerpt showing efforts incl ultra. +3. Back-compat proof: legacy-id wire table test green (every 69 id routes + to the same wire id as before, or documented intentional change). +4. Stack finalization: PR A -> dev, PR B stacked; retarget checks. From 3cdb77fc8075c0a67873eeaf7d460133ee88b728 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 28 Aug 2026 10:42:14 +0900 Subject: [PATCH 2/5] =?UTF-8?q?devlog(260828=20umbrella):=20fold=20A-gate?= =?UTF-8?q?=20round-1=20=E2=80=94=20variant-dimension=20schema,=20parser?= =?UTF-8?q?=20precedence,=20alias=20retention,=20evidence-gated=20maxMode,?= =?UTF-8?q?=20effort.ts=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../002_current_surface.md | 2 +- .../003_design.md | 48 ++++++++++++++---- .../010_capability_core.md | 49 ++++++++++++------- .../020_catalog_integration.md | 40 ++++++++++----- 4 files changed, 99 insertions(+), 40 deletions(-) diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/002_current_surface.md b/devlog/_plan/260828_cursor_umbrella_catalog/002_current_surface.md index 28b74c3408..b570f5d2a7 100644 --- a/devlog/_plan/260828_cursor_umbrella_catalog/002_current_surface.md +++ b/devlog/_plan/260828_cursor_umbrella_catalog/002_current_surface.md @@ -5,7 +5,7 @@ discovery.ts + request-builder.ts only. - discovery.ts: 69-row static seed (4 router + 52 + 13 thinking); inferCursorContextWindow (:27-38) hardcodes per-family windows; synthetic - ultra marker = kimi-k3-1m ONLY (:158-174); live merge is a FILTER (never + ultra marker = kimi-k3-1m ONLY (:158-174); live merge is a FILTER (45 of 69 rows carry ladders; quarantined opus-5 is a map key but not seeded) (never adds rows, provider-fetch.ts:1276); claude-opus-5 quarantined; dead CURSOR_REASONING_EFFORTS const. - live-models.ts: decode keeps modelId + maxMode only; DISCARDS displayName, diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/003_design.md b/devlog/_plan/260828_cursor_umbrella_catalog/003_design.md index 20ee9b1909..cad09fbbe8 100644 --- a/devlog/_plan/260828_cursor_umbrella_catalog/003_design.md +++ b/devlog/_plan/260828_cursor_umbrella_catalog/003_design.md @@ -7,23 +7,51 @@ generated alias JSON (senpi weakness 3): aliases are DERIVED by the grammar, not enumerated. 2. Thinking MERGES into the base identity for every family (no Claude-only - split — senpi weakness 2). A base with thinking variants routes efforts - through its thinking wire ids; bare non-thinking ids remain aliases. + split — senpi weakness 2) AS A DIMENSION, not by discarding identities + (A-gate blocker 1): the capability schema carries per-variant ladders + ({ regular?, thinking?, fast?, thinkingFast? } each with its own effort + list + wire order), because live ladders differ (claude-opus-5-fast + low/med/high vs thinking-fast low..max). The UMBRELLA ROW defaults to the + thinking variant when one exists (user decision: "thinking 하나로 합치고"); + regular/fast/thinking-fast remain reachable via aliases that select the + variant dimension explicitly. resolveCursorSelection takes the PICKED id + (which encodes the variant via the alias) — never guesses. 3. Fast is a dimension INSIDE the umbrella (senpi weakness 1): no fast picker rows; cursor/-fast stays routable as an alias that sets fast mode on the same umbrella identity. -4. 1M/Max-Mode generalized: every base whose capability declares bigContext - (statically: claude/gemini/kimi/gpt-5.6 families per senpi windows; live: - maxModeModels from GetUsableModels — currently decoded but DISCARDED) - exposes the ultra effort -> maxMode=true wire flag. kimi-k3-1m stops being - the lone synthetic and becomes an alias. -5. Back-compat absolute: every one of today's 69 ids (and live suffix ids) - resolves through the grammar to (base, level?, thinking?, fast?, ultra?). +4. 1M split into TWO separate capabilities (A-gate blocker 4 — window size + does NOT imply maxMode; prior probes found maxMode only on opus-fast + variants, 260822_senpi_cursor_transfer/210+310): + - window: context-window METADATA generalized per senpi's table (1M for + claude/gemini/kimi/gpt-5.6 families) — display/routing metadata only. + - maxMode (ultra rung): gated on EVIDENCE — the union of live + maxModeModels (decoded in live-models.ts:123-136, discarded by + provider-fetch today) and an explicit verified static list (currently + exactly kimi-k3, user-verified). Ultra generalizes automatically as live + evidence arrives, never from window size. +5. Back-compat absolute (A-gate blockers 2/3): + - Parser precedence: EXACT known identity/alias table first (covers + gpt-5.1-codex-max-as-base, gpt-5.5-extra, claude-4-sonnet-1m real wire + id), then cursor- prefix normalization (cursor-grok-4.5/4.6 wire forms), + then suffix grammar. A frozen fixture table pins parse+resolve for all + 69 picker ids + observed prefixed/suffixed wire forms. + - Alias retention contract: picker ROWS shrink, but the REQUEST path keeps + resolving every legacy slug (router forwards provider-qualified ids to + the adapter, router.ts:673-678; the adapter's resolver owns aliases). + A pinned session/config naming a removed slug keeps routing identically; + only fresh picker lists shrink. Tested explicitly (020). + - Quarantine is VARIANT-specific: claude-opus-5 regular stays quarantined + while thinking/fast siblings remain selectable. ## Picker shape (after) - Rows: 4 router + ~30 base umbrellas (from 69). Efforts per row from the - capability ladder; ultra appended only for bigContext-capable rows. + default-variant ladder. Synthetic max+ultra spawn-validation appendage + (effort.ts:219-226) is a SEPARATE policy from wire ultra: effort.ts stays + in the diff (blocker 5) — synthetic max/ultra continue to be appended for + spawn validation on every reasoning row (no downstream break), while the + WIRE maps ultra to maxMode only for evidence-gated bases and clamps to the + ladder top elsewhere (exactly today's clamp behavior). - Codex effort -> wire: suffix-id-first (Cursor rejects bare capability ids, senpi #1008 confirmed + our own request-builder already suffix-first). Thinking-capable base + effort E -> thinking wire id at E (family wire diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/010_capability_core.md b/devlog/_plan/260828_cursor_umbrella_catalog/010_capability_core.md index 768dcb490a..55efdc2cee 100644 --- a/devlog/_plan/260828_cursor_umbrella_catalog/010_capability_core.md +++ b/devlog/_plan/260828_cursor_umbrella_catalog/010_capability_core.md @@ -4,18 +4,26 @@ ### 1. ADD src/adapters/cursor/catalog.ts -- export interface CursorCapability { levels: readonly CursorEffort[]; - thinking?: { order: "thinking-then-effort" | "effort-then-thinking" | "bare" }; - fast?: boolean; bigContext?: boolean; window: number; quarantined?: boolean } +- export type CursorVariantKind = "regular" | "thinking" | "fast" | "thinkingFast"; +- export interface CursorVariantSpec { levels: readonly string[]; + order?: "thinking-then-effort" | "effort-then-thinking" | "bare"; + quarantined?: boolean } +- export interface CursorCapability { variants: Partial>; defaultVariant: CursorVariantKind; window: number; + maxModeVerified?: boolean; wirePrefix?: "cursor-" } + (A-gate blocker 1: per-variant ladders — claude-opus-5 fast low/med/high + vs thinkingFast low..max representable; defaultVariant = thinking when a + thinking variant exists, else regular; quarantine per-variant — blocker 3.) - export const CURSOR_CAPABILITIES: Record — - seeded from today's CURSOR_MODEL_EFFORT_TIERS (46) + CURSOR_THINKING_FAMILIES - (13) + senpi windows (001 table), collapsed to ~30 base entries: thinking - variants become thinking:{order} on the base; -fast entries become fast:true; - kimi-k3-1m becomes bigContext:true on kimi-k3; claude/gemini/gpt-5.6 windows - per senpi (1M) with bigContext on claude+gemini+kimi families. -- export function parseCursorVariantId(id): { baseId, level?, thinking, - fast, ultra } — senpi grammar + our -1m suffix; level tokens - minimal|low|medium|high|extra-high|xhigh|max|none. + seeded 1:1 from CURSOR_MODEL_EFFORT_TIERS + CURSOR_THINKING_FAMILIES + + senpi window table; maxModeVerified only on kimi-k3 (blocker 4); + wirePrefix "cursor-" on grok-4.5/grok-4.6 regular. +- export function parseCursorVariantId(id): { baseId, kind, level?, ultra } + with STRICT precedence (blocker 2): (1) exact base-id table hit (covers + gpt-5.1-codex-max, gpt-5.5-extra, claude-4-sonnet-1m as real identities); + (2) cursor- prefix strip + re-lookup; (3) -1m synthetic suffix; (4) senpi + suffix grammar (strip -fast; -thinking- | --thinking | + -thinking | -); tokens minimal|low|medium|high|extra-high|xhigh|max|none. - export function resolveCursorSelection(pickedId, codexEffort?): { wireId, maxMode, params: [] } — suffix-id-first composition reusing the order rules currently in cursorWireModelIdWithEffort; ultra -> @@ -26,13 +34,18 @@ ### 2. Tests — ADD tests/cursor-catalog.test.ts -Named activation per branch: grammar round-trip for ALL 69 legacy ids -(fixture list frozen from discovery.ts seed) -> every id parses to a known -base; thinking merge (claude-opus-5 + high -> claude-opus-5-thinking-high); -bare-thinking families (claude-4-sonnet) ignore effort; fast alias -resolution; ultra on bigContext base -> maxMode + top effort; ultra on -non-bigContext -> clamps to max, no maxMode; quarantine row excluded from -umbrella rows; unknown id passthrough unchanged. +Named activation per branch, with a FROZEN fixture table (all 69 seed ids + +cursor- prefixed wire forms + representative live suffix ids) asserting +(parsedBase, kind, level) AND resolved wire id byte-equality against the +CURRENT cursorWireModelIdWithEffort/cursorRequestWireModelIdWithEffort +output (generated once from the old module while it still exists — the +back-compat oracle). Plus: precedence cases (gpt-5.1-codex-max stays a base; +gpt-5.5-extra + any effort -> gpt-5.5-extra-high; cursor-grok-4.6-xhigh +round-trips); thinking default variant; bare-thinking ignores effort; +per-variant ladder divergence (opus-5 fast vs thinkingFast); ultra -> +maxMode ONLY on maxModeVerified; ultra elsewhere clamps to ladder top +without maxMode; variant-specific quarantine (opus-5 regular excluded, +thinking present); unknown id passthrough. ### 3. NO consumer changes in this PR (effort-map untouched) — additive module + tests only, so the diff reviews clean. diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/020_catalog_integration.md b/devlog/_plan/260828_cursor_umbrella_catalog/020_catalog_integration.md index cc327b8271..fa39f186fd 100644 --- a/devlog/_plan/260828_cursor_umbrella_catalog/020_catalog_integration.md +++ b/devlog/_plan/260828_cursor_umbrella_catalog/020_catalog_integration.md @@ -15,13 +15,21 @@ - inferCursorContextWindow: read window from CURSOR_CAPABILITIES first, fall through to current heuristics for unknown ids. -### 2. MODIFY src/adapters/cursor/live-models.ts — stop discarding maxMode: -return maxModeModels and thread into provider-fetch so live maxMode marks -bigContext on matching bases (union with static flags). - -### 3. MODIFY src/providers/registry.ts cursor section — efforts from -cursorUmbrellaRows(); defaults: keep kimi-k3 max; thinking-merged rows -default per capability. +### 2. MODIFY src/codex/catalog/provider-fetch.ts (~:1276-1310) — consume +the maxModeModels ALREADY returned by live-models.ts (:123-136; decoder +needs no change, A-gate finding 6): live maxMode ids union with +maxModeVerified static flags to arm the ultra->maxMode wire rung per base. + +### 3. MODIFY src/providers/registry.ts (~:1092 cursor section) — model ids +from cursorUmbrellaRows(); modelReasoningEfforts from each row's +defaultVariant ladder; modelDefaultReasoningEfforts keeps kimi-k3: max. + +### 3b. MODIFY src/codex/catalog/effort.ts (:219-226) — POLICY UNCHANGED +(A-gate blocker 5): synthetic max+ultra stay appended to every +reasoning-capable row for spawn validation. Add a comment distinguishing +catalog-synthetic ultra from wire maxMode. Regression test: spawn-validation +efforts for a cursor row WITHOUT maxModeVerified still include ultra, and +the adapter clamps it (existing clamp test extended). ### 4. MODIFY src/adapters/cursor/request-builder.ts — normalizeCursorModelId / effort composition delegate to resolveCursorSelection; grok-fast parameter @@ -31,10 +39,20 @@ path preserved; ultra path generalized (maxMode for any bigContext base). request-builder consume catalog.ts; migrate any residual export the tests reference. -### 6. Tests — MODIFY tests near existing cursor suites: discovery filter -with live suffix fixtures; registry efforts snapshot (row count ~34); -request-builder wire ids unchanged for every legacy fixture (byte-equal -wire id table test — the back-compat proof); sync row-count before/after. +### 6. Tests (exact paths, A-gate blocker 7): +- MODIFY tests/cursor-effort-suffix.test.ts — wire-id oracle table from 010 + stays green after consumers switch (the byte-equal back-compat proof). +- MODIFY tests/cursor-hardening.test.ts discovery sections — live filter + with suffix + cursor-prefixed fixtures via the new parser. +- ADD tests/cursor-umbrella-rows.test.ts — cursorUmbrellaRows row count + (4 router excluded; ~30 umbrellas), thinking-merged rows list their + default-variant ladder, removed slugs absent from rows but RESOLVABLE via + resolveCursorSelection (pinned-session survival unit proof), quarantined + regular excluded while thinking sibling present. +- ADD focused catalog sync test: sync output cursor section row count + + synthetic max/ultra still appended (spawn validation). +- Pinned-session integration: request with model cursor/claude-opus-5-thinking + (removed row) through request-builder resolves to same wire id as today. ## Verifiers From 2be819e46c82783d071ce9293aa3e51d7e5e3b7e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 28 Aug 2026 10:42:34 +0900 Subject: [PATCH 3/5] devlog(260828 umbrella): roadmap lock --- .../004_roadmap_lock.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 devlog/_plan/260828_cursor_umbrella_catalog/004_roadmap_lock.md diff --git a/devlog/_plan/260828_cursor_umbrella_catalog/004_roadmap_lock.md b/devlog/_plan/260828_cursor_umbrella_catalog/004_roadmap_lock.md new file mode 100644 index 0000000000..10f79165ae --- /dev/null +++ b/devlog/_plan/260828_cursor_umbrella_catalog/004_roadmap_lock.md @@ -0,0 +1,14 @@ +# 004 — roadmap lock (wp1 B closure) + +Locked after A-gate round 1 (5 High + 2 Medium folded, 3cdb77fc8). + +| wp | doc | branch/PR | +|---|---|---| +| wp2 | 010_capability_core.md | codex/cursor-umbrella-core -> dev | +| wp3 | 020_catalog_integration.md | codex/cursor-umbrella-wire (stacked on core) | +| wp4 | 030_closure.md | comparison + proof, docs on the wire branch | + +Key locked decisions: variant-dimension schema (defaultVariant=thinking); +parser precedence with frozen oracle; alias retention (resolver keeps all +69 slugs); maxMode evidence-gated (kimi-k3 + live maxModeModels union); +effort.ts synthetic policy untouched. From 54965ef03ca55e4a4c0e47a886ca15c23a3be6b7 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 28 Aug 2026 10:46:53 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat(cursor):=20umbrella=20capability=20cat?= =?UTF-8?q?alog=20=E2=80=94=20single=20source=20of=20truth=20for=20model?= =?UTF-8?q?=20identities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One capability record per base model; thinking / fast / thinking-fast are dimensions with their own effort ladders and wire orders (live ladders differ per variant). The umbrella picker row defaults to the thinking variant; every legacy variant id keeps resolving through the alias grammar with exact-identity-first precedence (gpt-5.1-codex-max, gpt-5.5-extra, cursor- prefixes, -1m marker). Max Mode is evidence-gated (kimi-k3 static + live maxModeModels union), never inferred from window size. Additive module only: no consumer changes in this PR. The test file uses the legacy effort-map as a byte-equal wire-id oracle across every legacy id x Codex effort, with the one intentional change (bare claude bases now route their thinking variant) asserted against the thinking slug oracle. --- src/adapters/cursor/catalog.ts | 477 +++++++++++++++++++++++++++++++++ tests/cursor-catalog.test.ts | 178 ++++++++++++ 2 files changed, 655 insertions(+) create mode 100644 src/adapters/cursor/catalog.ts create mode 100644 tests/cursor-catalog.test.ts diff --git a/src/adapters/cursor/catalog.ts b/src/adapters/cursor/catalog.ts new file mode 100644 index 0000000000..313015d6fd --- /dev/null +++ b/src/adapters/cursor/catalog.ts @@ -0,0 +1,477 @@ +/** + * Cursor umbrella catalog — the single source of truth for cursor model + * identities (devlog 260828_cursor_umbrella_catalog). + * + * Design (003_design.md, audited): one capability record per BASE model. + * Thinking / fast / thinking-fast are DIMENSIONS of the base, each with its + * own effort ladder and wire order, because the live wire really does differ + * per variant (claude-opus-5-fast stops at high while its thinking-fast runs + * to max). The umbrella picker row defaults to the thinking variant when one + * exists; every legacy variant id keeps resolving through the alias grammar. + * + * Max Mode is evidence-gated and separate from context-window size: prior + * live probes (devlog 260822_senpi_cursor_transfer/210+310) found maxMode + * only on specific variants, so `maxModeVerified` marks bases with proven + * support (kimi-k3, user-verified) and live `maxModeModels` extends it. + */ + +export type CursorVariantKind = "regular" | "thinking" | "fast" | "thinkingFast"; + +export type CursorThinkingOrder = "thinking-then-effort" | "effort-then-thinking" | "bare"; + +export interface CursorVariantSpec { + /** Ascending canonical effort rungs the wire lists for this variant; empty = bare id. */ + readonly levels: readonly string[]; + /** Where the thinking marker sits relative to the effort rung (thinking variants only). */ + readonly order?: CursorThinkingOrder; + /** Variant-specific quarantine (base-wide quarantine would erase healthy siblings). */ + readonly quarantined?: boolean; +} + +export interface CursorCapability { + readonly variants: Partial>; + /** Which variant the umbrella picker row selects (thinking merges into the base). */ + readonly defaultVariant: CursorVariantKind; + /** Context-window metadata (display/routing only — never implies maxMode). */ + readonly window: number; + /** Max Mode proven on the wire for this base (static evidence; live maxModeModels unions in). */ + readonly maxModeVerified?: boolean; + /** Wire prefix required by AgentService/Run for the regular variant (grok families). */ + readonly wirePrefix?: "cursor-"; +} + +const K = 1_000; +const CONTEXT_200K = 200 * K; +const CONTEXT_256K = 256 * K; +const CONTEXT_272K = 272 * K; +const CONTEXT_500K = 500 * K; +const CONTEXT_1M = 1_000 * K; + +const FULL = ["low", "medium", "high", "xhigh", "max"] as const; +const T = "thinking-then-effort" as const; +const E = "effort-then-thinking" as const; + +/** + * One entry per base model. Ladders mirror the live GetUsableModels roster the + * retired effort-map recorded (260813-260825 captures); windows follow the + * per-family table verified against senpi's AvailableModels capture + * (001_reference_analysis.md). + */ +export const CURSOR_CAPABILITIES: Record = { + "claude-4.5-opus": { + window: CONTEXT_200K, + defaultVariant: "thinking", + variants: { + regular: { levels: ["high"] }, + thinking: { levels: ["high"], order: E }, + }, + }, + "claude-4.6-opus": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: ["high", "max"] }, + thinking: { levels: ["high", "max"], order: E }, + }, + }, + "claude-4.6-sonnet": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: ["medium"] }, + thinking: { levels: ["medium"], order: E }, + }, + }, + "claude-4.5-sonnet": { + window: CONTEXT_200K, + defaultVariant: "thinking", + variants: { + regular: { levels: [] }, + thinking: { levels: [], order: "bare" }, + }, + }, + "claude-4-sonnet": { + window: CONTEXT_200K, + defaultVariant: "thinking", + variants: { + regular: { levels: [] }, + thinking: { levels: [], order: "bare" }, + }, + }, + "claude-fable-5": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: FULL }, + thinking: { levels: FULL, order: T }, + }, + }, + "claude-sonnet-5": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: FULL }, + thinking: { levels: FULL, order: T }, + }, + }, + "claude-opus-4-7": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: FULL }, + thinking: { levels: FULL, order: T }, + fast: { levels: FULL }, + thinkingFast: { levels: FULL, order: T }, + }, + }, + "claude-opus-4-8": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: FULL }, + thinking: { levels: FULL, order: T }, + fast: { levels: FULL }, + thinkingFast: { levels: FULL, order: T }, + }, + }, + "claude-opus-5": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + // Regular stays quarantined (devlog 260826: dead-model quarantine) while + // the thinking/fast siblings remain live — quarantine is per-variant. + regular: { levels: FULL, quarantined: true }, + thinking: { levels: FULL, order: T }, + fast: { levels: ["low", "medium", "high"] }, + thinkingFast: { levels: FULL, order: T }, + }, + }, + "glm-5.2": { + window: CONTEXT_1M, + defaultVariant: "regular", + variants: { regular: { levels: ["high", "max"] } }, + }, + "glm-5.3": { + window: CONTEXT_1M, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "high", "max"] } }, + }, + "gemini-3.6-flash": { + window: CONTEXT_1M, + defaultVariant: "regular", + variants: { regular: { levels: ["minimal", "low", "medium", "high"] } }, + }, + "gemini-3.7-flash": { + window: CONTEXT_1M, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high"] } }, + }, + "kimi-k3": { + window: CONTEXT_1M, + defaultVariant: "regular", + maxModeVerified: true, + variants: { regular: { levels: ["low", "high", "max"] } }, + }, + "grok-4.5": { + window: CONTEXT_500K, + defaultVariant: "regular", + wirePrefix: "cursor-", + variants: { + regular: { levels: ["low", "medium", "high"] }, + fast: { levels: ["low", "medium", "high"] }, + }, + }, + "grok-4.6": { + window: CONTEXT_500K, + defaultVariant: "regular", + wirePrefix: "cursor-", + variants: { + regular: { levels: ["low", "medium", "high", "xhigh"] }, + fast: { levels: ["low", "medium", "high", "xhigh"] }, + }, + }, + "gpt-5.1": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "high"] } }, + }, + "gpt-5.1-codex-max": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high", "xhigh"] } }, + }, + "gpt-5.1-codex-mini": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "high"] } }, + }, + "gpt-5.2": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "high", "xhigh"] } }, + }, + "gpt-5.2-codex": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "high", "xhigh"] } }, + }, + "gpt-5.3-codex": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "high", "xhigh"] } }, + }, + "gpt-5.4": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high", "xhigh"] } }, + }, + "gpt-5.4-mini": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high", "xhigh"] } }, + }, + "gpt-5.4-nano": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high", "xhigh"] } }, + }, + "gpt-5.5": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high"] } }, + }, + "gpt-5.5-extra": { + window: CONTEXT_272K, + defaultVariant: "regular", + variants: { regular: { levels: ["high"] } }, + }, + "gpt-5.6-sol": { + window: CONTEXT_1M, + defaultVariant: "regular", + variants: { regular: { levels: FULL } }, + }, + "gpt-5.6-terra": { + window: CONTEXT_1M, + defaultVariant: "regular", + variants: { regular: { levels: FULL } }, + }, + "gpt-5.6-luna": { + window: CONTEXT_1M, + defaultVariant: "regular", + variants: { regular: { levels: FULL } }, + }, +}; + +const LEVEL_TOKENS = ["extra-high", "minimal", "low", "medium", "high", "xhigh", "max", "none"] as const; + +export interface ParsedCursorVariantId { + readonly baseId: string; + readonly kind: CursorVariantKind; + readonly level?: string; + /** True for synthetic big-context marker ids (`-1m`). */ + readonly ultra: boolean; + /** True when the id resolved through the capability table (else passthrough). */ + readonly known: boolean; +} + +function stripLevelSuffix(id: string): { stem: string; level?: string } { + for (const token of LEVEL_TOKENS) { + if (id.endsWith(`-${token}`)) return { stem: id.slice(0, -(token.length + 1)), level: token }; + } + return { stem: id }; +} + +/** + * Parse any cursor-facing id (picker slug tail, legacy variant id, or wire id) + * into its base + dimensions. Precedence is exact-identity-first so ids like + * `gpt-5.1-codex-max` and `gpt-5.5-extra` — whose tails collide with effort + * tokens — never mis-parse (A-gate round-1 blocker 2). + */ +export function parseCursorVariantId(rawId: string): ParsedCursorVariantId { + const id = rawId.trim(); + // 1. Exact base identity. + if (CURSOR_CAPABILITIES[id]) { + return { baseId: id, kind: defaultKindFor(id), ultra: false, known: true }; + } + // 2. cursor- wire prefix (regular grok wire forms). + if (id.startsWith("cursor-")) { + const inner = parseCursorVariantId(id.slice("cursor-".length)); + if (inner.known) return inner; + } + // 3. Synthetic big-context marker. + if (id.endsWith("-1m")) { + const baseId = id.slice(0, -"-1m".length); + if (CURSOR_CAPABILITIES[baseId]) { + return { baseId, kind: "regular", ultra: true, known: true }; + } + } + // 4. Suffix grammar: strip -fast, then thinking/effort markers. + let stem = id; + let fast = false; + if (stem.endsWith("-fast")) { + fast = true; + stem = stem.slice(0, -"-fast".length); + } + let thinking = false; + let level: string | undefined; + const thinkingLevel = /^(.*)-thinking-([a-z-]+)$/.exec(stem); + if (thinkingLevel && CURSOR_CAPABILITIES[thinkingLevel[1]!] && (LEVEL_TOKENS as readonly string[]).includes(thinkingLevel[2]!)) { + return finishParse(thinkingLevel[1]!, true, fast, thinkingLevel[2]!); + } + const levelThinking = stem.endsWith("-thinking") ? stripLevelSuffix(stem.slice(0, -"-thinking".length)) : undefined; + if (levelThinking && CURSOR_CAPABILITIES[levelThinking.stem]) { + return finishParse(levelThinking.stem, true, fast, levelThinking.level); + } + if (stem.endsWith("-thinking") && CURSOR_CAPABILITIES[stem.slice(0, -"-thinking".length)]) { + return finishParse(stem.slice(0, -"-thinking".length), true, fast, undefined); + } + const plain = stripLevelSuffix(stem); + if (plain.level !== undefined && CURSOR_CAPABILITIES[plain.stem]) { + return finishParse(plain.stem, false, fast, plain.level); + } + if (fast && CURSOR_CAPABILITIES[stem]) { + return finishParse(stem, false, true, undefined); + } + void thinking; + void level; + // Unknown: passthrough (adapter sends the id unchanged). + return { baseId: id, kind: "regular", ultra: false, known: false }; +} + +function finishParse(baseId: string, thinking: boolean, fast: boolean, level: string | undefined): ParsedCursorVariantId { + const kind: CursorVariantKind = thinking ? (fast ? "thinkingFast" : "thinking") : fast ? "fast" : "regular"; + return { baseId, kind, ...(level !== undefined ? { level } : {}), ultra: false, known: true }; +} + +function defaultKindFor(baseId: string): CursorVariantKind { + return CURSOR_CAPABILITIES[baseId]?.defaultVariant ?? "regular"; +} + +function normalizeRequestedEffort(reasoning: string | undefined): string | undefined { + const normalized = reasoning?.toLowerCase(); + return normalized === "ultra" ? "max" : normalized; +} + +function codexEffortRank(reasoning: string | undefined): "low" | "medium" | "high" { + switch (normalizeRequestedEffort(reasoning) ?? "") { + case "none": + case "minimal": + case "low": + return "low"; + case "medium": + return "medium"; + case "high": + case "max": + case "xhigh": + return "high"; + default: + return "high"; + } +} + +/** Pick this variant's effort rung for a Codex reasoning label: literal-first, else rank clamp. */ +export function cursorVariantEffort(spec: CursorVariantSpec, reasoning: string | undefined): string | undefined { + if (spec.levels.length === 0) return undefined; + const requested = normalizeRequestedEffort(reasoning); + if (requested && spec.levels.includes(requested)) return requested; + switch (codexEffortRank(reasoning)) { + case "low": + return spec.levels[0]; + case "high": + return spec.levels[spec.levels.length - 1]; + case "medium": + return spec.levels[Math.floor((spec.levels.length - 1) / 2)]; + } +} + +export interface CursorResolvedSelection { + /** Flattened wire id for AgentService/Run (with any required cursor- prefix). */ + readonly wireId: string; + /** Canonical prefix-free id for discovery/catalog comparison. */ + readonly canonicalId: string; + /** True when the request should raise the Max Mode wire flag (evidence-gated). */ + readonly maxMode: boolean; + readonly known: boolean; +} + +/** + * Compose a variant's flattened wire id, reproducing the legacy effort-map + * order rules exactly (thinking-then-effort / effort-then-thinking / bare; + * fast marker terminal; wrong order is ERROR_BAD_MODEL_NAME on the wire). + */ +function composeWireId(baseId: string, kind: CursorVariantKind, effort: string | undefined): string { + const capability = CURSOR_CAPABILITIES[baseId]; + const spec = capability?.variants[kind]; + if (!capability || !spec) return baseId; + const thinking = kind === "thinking" || kind === "thinkingFast"; + const fast = kind === "fast" || kind === "thinkingFast"; + if (thinking) { + const order = spec.order ?? "thinking-then-effort"; + if (order === "bare" || effort === undefined) return `${baseId}-thinking`; + if (order === "effort-then-thinking") return `${baseId}-${effort}-thinking`; + return fast ? `${baseId}-thinking-${effort}-fast` : `${baseId}-thinking-${effort}`; + } + if (effort === undefined) return fast ? `${baseId}-fast` : baseId; + return fast ? `${baseId}-${effort}-fast` : `${baseId}-${effort}`; +} + +/** + * Resolve any picked cursor id + Codex reasoning effort to the wire identity. + * Legacy slugs (thinking/fast/-1m variants) keep resolving forever — picker + * rows shrink, routability does not (alias-retention contract, 003). + * + * `liveMaxModeIds` optionally extends the static maxMode evidence with the + * bases the live GetUsableModels roster flags (union semantics). + */ +export function resolveCursorSelection( + pickedId: string, + reasoning: string | undefined, + liveMaxModeIds?: ReadonlySet, +): CursorResolvedSelection { + const parsed = parseCursorVariantId(pickedId); + if (!parsed.known) { + return { wireId: pickedId, canonicalId: pickedId, maxMode: false, known: false }; + } + const capability = CURSOR_CAPABILITIES[parsed.baseId]!; + const spec = capability.variants[parsed.kind] ?? capability.variants.regular; + if (!spec) { + return { wireId: parsed.baseId, canonicalId: parsed.baseId, maxMode: false, known: true }; + } + const requested = parsed.level ?? reasoning; + const effort = cursorVariantEffort(spec, requested); + const canonicalId = composeWireId(parsed.baseId, parsed.kind, effort); + const wireId = capability.wirePrefix && parsed.kind === "regular" + ? `${capability.wirePrefix}${canonicalId}` + : canonicalId; + const ultraRequested = parsed.ultra || reasoning?.toLowerCase() === "ultra"; + const maxModeArmed = capability.maxModeVerified === true || liveMaxModeIds?.has(parsed.baseId) === true; + return { wireId, canonicalId, maxMode: ultraRequested && maxModeArmed, known: true }; +} + +export interface CursorUmbrellaRow { + readonly id: string; + readonly efforts: readonly string[]; + readonly window: number; + /** Max Mode evidence present: the ultra rung maps to maxMode on the wire. */ + readonly maxModeVerified: boolean; +} + +/** + * The umbrella picker rows: one per base whose default variant is selectable. + * Thinking merges into the base row; fast/thinking-fast/legacy slugs stay + * routable as aliases but add no rows. Router ids stay in discovery. + */ +export function cursorUmbrellaRows(): CursorUmbrellaRow[] { + const rows: CursorUmbrellaRow[] = []; + for (const [baseId, capability] of Object.entries(CURSOR_CAPABILITIES)) { + const spec = capability.variants[capability.defaultVariant]; + if (!spec || spec.quarantined) continue; + rows.push({ + id: baseId, + efforts: spec.levels, + window: capability.window, + maxModeVerified: capability.maxModeVerified === true, + }); + } + return rows; +} diff --git a/tests/cursor-catalog.test.ts b/tests/cursor-catalog.test.ts new file mode 100644 index 0000000000..5a690b1614 --- /dev/null +++ b/tests/cursor-catalog.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, test } from "bun:test"; +import { + CURSOR_CAPABILITIES, + cursorUmbrellaRows, + parseCursorVariantId, + resolveCursorSelection, +} from "../src/adapters/cursor/catalog"; +import { + cursorEffortSuffix, + cursorModelHasEffortTiers, + cursorRequestWireModelIdWithEffort, + CURSOR_THINKING_MODEL_IDS, +} from "../src/adapters/cursor/effort-map"; + +/** + * The frozen legacy picker ids (discovery.ts static seed minus the 4 router + * rows). The legacy effort-map is the back-compat ORACLE: for every id and + * every Codex effort, the new resolver must produce the identical wire id. + */ +const LEGACY_EFFORT_IDS = [ + "claude-4.5-opus", "claude-4.6-opus", "claude-4.6-sonnet", + "claude-fable-5", "claude-opus-4-7", "claude-opus-4-7-fast", + "claude-opus-4-8", "claude-opus-4-8-fast", "claude-opus-5-fast", + "claude-sonnet-5", "glm-5.2", "glm-5.3", "gemini-3.6-flash", + "gemini-3.7-flash", "kimi-k3", "kimi-k3-1m", "grok-4.5", "grok-4.5-fast", + "grok-4.6", "grok-4.6-fast", "gpt-5.1", "gpt-5.1-codex-max", + "gpt-5.1-codex-mini", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", + "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.5", "gpt-5.5-extra", + "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", + ...CURSOR_THINKING_MODEL_IDS, +] as const; + +const CODEX_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra", undefined] as const; + +/** Legacy composition: what request-builder sends today for a picked id + effort. */ +function legacyWireId(pickedId: string, reasoning: string | undefined): string { + // request-builder strips the synthetic -1m marker before composing. + const baseId = pickedId === "kimi-k3-1m" ? "kimi-k3" : pickedId; + if (!cursorModelHasEffortTiers(baseId)) return baseId; + const suffix = cursorEffortSuffix(baseId, reasoning); + if (suffix === undefined) return baseId; + return cursorRequestWireModelIdWithEffort(baseId, suffix); +} + +/** + * The ONE intentional behavior change of the umbrella redesign: a bare claude + * base id now routes its THINKING variant (user decision: thinking merges into + * the base identity). Every other id x effort combination must stay byte-equal + * to the legacy effort-map composition. + */ +const INTENTIONAL_THINKING_DEFAULTS = new Set( + Object.entries(CURSOR_CAPABILITIES) + .filter(([, capability]) => capability.defaultVariant === "thinking") + .map(([baseId]) => baseId), +); + +describe("cursor umbrella catalog (devlog 260828_cursor_umbrella_catalog)", () => { + describe("back-compat oracle: byte-equal wire ids for every legacy id x effort", () => { + for (const id of LEGACY_EFFORT_IDS) { + if (INTENTIONAL_THINKING_DEFAULTS.has(id)) continue; + test(`legacy ${id} resolves to the same wire ids as the effort-map`, () => { + for (const effort of CODEX_EFFORTS) { + const resolved = resolveCursorSelection(id, effort); + expect(`${id}@${effort}: ${resolved.wireId}`).toBe(`${id}@${effort}: ${legacyWireId(id, effort)}`); + } + }); + } + + test("intentional change: bare claude bases route their thinking variant now", () => { + for (const id of INTENTIONAL_THINKING_DEFAULTS) { + for (const effort of CODEX_EFFORTS) { + const resolved = resolveCursorSelection(id, effort); + // The thinking variant's own legacy id must produce the SAME wire id — + // proving the merge maps onto an id the wire already served. + const thinkingLegacy = legacyWireId( + id === "claude-4.5-sonnet" || id === "claude-4-sonnet" ? `${id}-thinking` : `${id}-thinking`, + effort, + ); + expect(`${id}@${effort}: ${resolved.wireId}`).toBe(`${id}@${effort}: ${thinkingLegacy}`); + } + } + }); + + test("legacy thinking/fast slugs stay byte-equal (alias retention)", () => { + for (const id of LEGACY_EFFORT_IDS) { + if (!INTENTIONAL_THINKING_DEFAULTS.has(id)) continue; + // The variant slugs themselves (claude-x-thinking, -fast, ...) are + // covered by the main loop; here we re-assert the base's non-default + // variants remain reachable via their legacy slugs. + const fastSlug = `${id}-fast`; + if ((LEGACY_EFFORT_IDS as readonly string[]).includes(fastSlug)) { + for (const effort of CODEX_EFFORTS) { + expect(resolveCursorSelection(fastSlug, effort).wireId).toBe(legacyWireId(fastSlug, effort)); + } + } + } + }); + }); + + describe("parser precedence", () => { + test("exact base identities never mis-parse as effort suffixes", () => { + expect(parseCursorVariantId("gpt-5.1-codex-max")).toMatchObject({ baseId: "gpt-5.1-codex-max", kind: "regular", known: true }); + expect(parseCursorVariantId("gpt-5.5-extra")).toMatchObject({ baseId: "gpt-5.5-extra", kind: "regular", known: true }); + }); + + test("cursor- prefixed wire forms round-trip to prefix-free bases", () => { + expect(parseCursorVariantId("cursor-grok-4.6-xhigh")).toMatchObject({ baseId: "grok-4.6", level: "xhigh", kind: "regular", known: true }); + expect(parseCursorVariantId("cursor-grok-4.5-high")).toMatchObject({ baseId: "grok-4.5", level: "high", kind: "regular", known: true }); + }); + + test("synthetic -1m marker parses as ultra on the base", () => { + expect(parseCursorVariantId("kimi-k3-1m")).toMatchObject({ baseId: "kimi-k3", ultra: true, known: true }); + }); + + test("thinking and fast dimensions parse from every observed shape", () => { + expect(parseCursorVariantId("claude-opus-5-thinking-high")).toMatchObject({ baseId: "claude-opus-5", kind: "thinking", level: "high" }); + expect(parseCursorVariantId("claude-opus-5-thinking-high-fast")).toMatchObject({ baseId: "claude-opus-5", kind: "thinkingFast", level: "high" }); + expect(parseCursorVariantId("claude-4.6-opus-high-thinking")).toMatchObject({ baseId: "claude-4.6-opus", kind: "thinking", level: "high" }); + expect(parseCursorVariantId("claude-4-sonnet-thinking")).toMatchObject({ baseId: "claude-4-sonnet", kind: "thinking" }); + expect(parseCursorVariantId("grok-4.6-high-fast")).toMatchObject({ baseId: "grok-4.6", kind: "fast", level: "high" }); + }); + + test("unknown ids pass through unchanged", () => { + const parsed = parseCursorVariantId("composer-9.9-special"); + expect(parsed.known).toBe(false); + expect(resolveCursorSelection("composer-9.9-special", "high").wireId).toBe("composer-9.9-special"); + }); + }); + + describe("umbrella semantics", () => { + test("thinking merges into the base: picking the base routes the thinking variant", () => { + const resolved = resolveCursorSelection("claude-opus-5", "high"); + expect(resolved.wireId).toBe("claude-opus-5-thinking-high"); + }); + + test("bare-thinking families ignore effort", () => { + expect(resolveCursorSelection("claude-4-sonnet", "max").wireId).toBe("claude-4-sonnet-thinking"); + }); + + test("per-variant ladders diverge: opus-5 fast clamps to high, thinking-fast reaches max", () => { + expect(resolveCursorSelection("claude-opus-5-fast", "max").wireId).toBe("claude-opus-5-high-fast"); + expect(resolveCursorSelection("claude-opus-5-thinking-fast", "max").wireId).toBe("claude-opus-5-thinking-max-fast"); + }); + + test("ultra arms maxMode only on evidence-gated bases", () => { + const kimi = resolveCursorSelection("kimi-k3-1m", "ultra"); + expect(kimi.maxMode).toBe(true); + expect(kimi.wireId).toBe("kimi-k3-max"); + const claude = resolveCursorSelection("claude-opus-4-8", "ultra"); + expect(claude.maxMode).toBe(false); + expect(claude.wireId).toBe("claude-opus-4-8-thinking-max"); + }); + + test("live maxModeModels evidence extends the static gate", () => { + const live = new Set(["claude-opus-4-8"]); + expect(resolveCursorSelection("claude-opus-4-8", "ultra", live).maxMode).toBe(true); + expect(resolveCursorSelection("claude-opus-4-8", "high", live).maxMode).toBe(false); + }); + + test("umbrella rows: one per base, quarantined default excluded, thinking sibling honored", () => { + const rows = cursorUmbrellaRows(); + const ids = rows.map(row => row.id); + expect(ids).toContain("claude-opus-5"); + expect(ids).not.toContain("claude-opus-5-thinking"); + expect(ids).not.toContain("claude-opus-5-fast"); + expect(ids).not.toContain("kimi-k3-1m"); + expect(rows.length).toBe(Object.keys(CURSOR_CAPABILITIES).length); + const kimi = rows.find(row => row.id === "kimi-k3"); + expect(kimi?.maxModeVerified).toBe(true); + expect(kimi?.window).toBe(1_000_000); + }); + + test("variant-specific quarantine: opus-5 regular quarantined, umbrella row still present via thinking default", () => { + expect(CURSOR_CAPABILITIES["claude-opus-5"]!.variants.regular?.quarantined).toBe(true); + expect(CURSOR_CAPABILITIES["claude-opus-5"]!.defaultVariant).toBe("thinking"); + }); + }); +}); From 22c073e032296801a87f87bbfe5b9314a6823df9 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 28 Aug 2026 11:32:37 +0900 Subject: [PATCH 5/5] test(update): give launcher recovery a readiness deadline loaded runners can meet The 45s /healthz wait exhausted at 46-47s on at least four unrelated PRs (latest: this branch, run 33133934478) while the product was healthy - the detached Bun proxy on a loaded shared runner can take longer than 45s to serve its first probe. 90s of readiness; the case budget derives from it, and the pinned arithmetic test keeps the relationship honest. --- tests/update-stop-first.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/update-stop-first.test.ts b/tests/update-stop-first.test.ts index 140ed3c491..4ea09b6236 100644 --- a/tests/update-stop-first.test.ts +++ b/tests/update-stop-first.test.ts @@ -36,7 +36,10 @@ function freePort(): Promise { * enforce — a slow-but-live proxy must still pass. */ const UPDATE_SPAWN_TIMEOUT_MS = 30_000; -const PROXY_READY_TIMEOUT_MS = 45_000; +// 45s exhausted repeatedly on loaded shared runners (46-47s failures recorded +// on at least four unrelated PRs; the detached Bun proxy can take >45s to +// serve /healthz there). 90s keeps the derived case budget honest below. +const PROXY_READY_TIMEOUT_MS = 90_000; /** Spawn + readiness + teardown spawn, plus headroom for fixture IO on a loaded runner. */ const RECOVERY_CASE_TIMEOUT_MS = UPDATE_SPAWN_TIMEOUT_MS + PROXY_READY_TIMEOUT_MS + UPDATE_SPAWN_TIMEOUT_MS + 15_000;