-
Notifications
You must be signed in to change notification settings - Fork 864
fix(cursor): classify bare 0-token resource_exhausted as context overflow #2320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # 260822 — senpi Cursor transfer investigation | ||
|
|
||
| Docs-only research unit. No production patches in this cycle. | ||
| Session `01a02665-e4c1-75a3-9660-c71284a1bba2`. Goalplan `investigate-whether-opencodex-can-adopt-any-curs`. | ||
|
|
||
| ## Loop spec | ||
|
|
||
| - Loop archetype: satisfy-spec research (inventory + classify). Not an optimization loop. | ||
| - Trigger: user asked whether OpenCodex can take Cursor-runtime mechanisms from senpi, with unlimited explorer dispatch, no model-name overrides. | ||
| - Goal: evidence-bearing transfer verdict in this unit. Every comparison row cites OpenCodex `path:line` and senpi GitHub blob/commit. | ||
| - Non-goals: production `src/` edits; copying senpi protobuf wholesale; starring repos; live Cursor account mutation; spawning `cursor-agent` CLI; extracting secrets. | ||
| - Verifier: files exist under this unit; `git status` shows no production `src/` diffs from this loop; 090 table rows have both-codebase citations. | ||
| - Stop: 090 locked and wp0 criteria captured. Implementation is a later appended work-phase, not this cycle. | ||
| - Memory artifact: this directory. | ||
| - Terminal: DONE (research lock) / NOOP (no residual gaps) / NEEDS_HUMAN (ToS) / UNSAFE (native-app patching). | ||
| - Escalation: live Cursor probes, ToS/product-policy, or proto-regen risk. | ||
|
|
||
| ## Sources | ||
|
|
||
| - OpenCodex tree: local checkout (explorers also cited `dev` `a228ed74` / GitHub `lidge-jun/opencodex`). | ||
| - senpi: `code-yeongyu/senpi` `main` SHA `a5eed44536f3024c5740dc3dfff4ffe0bb08b717` (2026-08-21), files also fetched as current default-branch blobs. | ||
| - Explorer lanes (inherit parent model; no model field): Helmholtz (protocol), Planck (auth/catalog), Hypatia (exec), Leibniz (stream/usage), Pasteur (senpi protocol), Archimedes (senpi auth/catalog), Plato (exec-bridge + CLI), Ohm (overflow/RE). | ||
|
|
||
| ## Docs | ||
|
|
||
| - 000 (this file) — unit map + later-implementation slice order. | ||
| - 001 — OpenCodex Cursor inventory. | ||
| - 002 — senpi Cursor inventory. | ||
| - 003 — protocol / transport compare. | ||
| - 004 — auth / catalog / effort / max-mode. | ||
| - 005 — exec / interactionQuery / tool pairing. | ||
| - 006 — stream completion / usage / overflow / rotation. | ||
| - 007 — CLI fallback lane. | ||
| - 090 — transfer verdict (ADOPT / ADAPT / REJECT / ALREADY-HAVE / NEEDS_HUMAN). | ||
|
|
||
| ## Work-phase map (dependency order, not effort) | ||
|
|
||
| 1. **wp0 (this cycle, docs-only):** inventories + 090 lock. Independent of later code. | ||
| 2. **wp1 (010, later):** Cursor error mapping + 0-token `resource_exhausted` surface. Owner: `src/adapters/cursor/cursor-errors.ts`, `src/lib/errors.ts`, `src/adapters/cursor/transport-retry.ts`. | ||
| 3. **wp2 (020, later):** `turnEnded` as application-complete + adapter stream-health. Owner: `src/adapters/cursor/live-transport.ts`, `src/adapters/cursor/protobuf-events.ts`. | ||
| 4. **wp3 (030, later):** unknown-exec typed reply (`ExecClientThrow` + stream-close) and optional newer exec oneofs as refusals. Owner: `src/adapters/cursor/native-exec.ts`. Do not regenerate protobuf in the same cycle as error mapping. | ||
| 5. **wp4 (040, later, optional):** live `GetUsableModels.maxMode` + richer catalog decode. Owner: `src/adapters/cursor/live-models.ts`, `src/adapters/cursor/protobuf-request.ts`, `src/adapters/cursor/discovery.ts`. | ||
|
|
||
| Do not implement two slices in one B. Do not start wp1 until this research cycle D-locks 090. | ||
|
|
||
| ## IN / OUT | ||
|
|
||
| IN: this `devlog/_plan/260822_senpi_cursor_transfer/` directory. | ||
| OUT: `src/`, `tests/`, `gui/`, `docs-site/`; senpi vendored proto copy; CLI spawn of `cursor-agent`. | ||
|
|
||
| ## Already-have headline | ||
|
|
||
| OpenCodex is not missing a Cursor provider. It already speaks `agent.v1.AgentService/Run` over Connect, answers `interactionQuery`, owns HTTP/1 `RunSSE` fallback, conversation-keyed `usedTokens` accounting, native-exec policy, and Responses-tool suspend. senpi's newer work is mostly overflow classification, turn-end close, exec-frame completeness, and a CLI fallback lane that OpenCodex deliberately does not have. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # 001 — OpenCodex Cursor inventory | ||
|
|
||
| Research only. Local tree + explorer Helmholtz / Planck / Hypatia / Leibniz. | ||
|
|
||
| ## Layout | ||
|
|
||
| `src/adapters/cursor/` owns the live protobuf adapter. Supporting files: | ||
|
|
||
| - Transport: `live-transport.ts` (1443 lines), `transport.ts`, `transport-retry.ts`, `http1-bidi.ts`, `framing.ts` | ||
| - Request: `request-builder.ts`, `protobuf-request.ts`, `tool-definitions.ts` | ||
| - Events / usage: `protobuf-events.ts`, `checkpoint-store.ts`, `thread-continuity.ts`, `kv-store.ts` | ||
| - Exec: `native-exec.ts` + `native-exec-*.ts`, `exec-policy.ts`, `mcp-manager.ts`, `mcp-config.ts` | ||
| - Catalog: `discovery.ts`, `live-models.ts`, `effort-map.ts` | ||
| - Errors: `cursor-errors.ts` | ||
| - Generated proto: `gen/agent_pb.ts` | ||
| - OAuth: `src/oauth/cursor.ts` (not under adapters) | ||
| - Adapter entry: `src/adapters/cursor.ts` | ||
| - Tests: `tests/cursor-*.test.ts` (39 files) | ||
|
|
||
| ## Protocol | ||
|
|
||
| OpenCodex posts `POST /agent.v1.AgentService/Run` as Connect proto, 5s `clientHeartbeat`, client version `cli-2026.07.08-0c04a8a`: | ||
|
|
||
| ```90:92:src/adapters/cursor/live-transport.ts | ||
| const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run"; | ||
| const CURSOR_CLIENT_VERSION = "cli-2026.07.08-0c04a8a"; | ||
| const HEARTBEAT_MS = 5_000; | ||
| ``` | ||
|
|
||
| HTTP/1 fallback exists: `RunSSE` + `BidiAppend` in `http1-bidi.ts:10-11`. First-frame timeout is 30s (`live-transport.ts:93`). After that, liveness is the Responses bridge stall watchdog (default 300s, `src/stall-timeout.ts:8`), kept alive by synthetic `heartbeat` events on swallowed progress frames (`live-transport.ts:1304-1309`). | ||
|
|
||
| `turnEnded` maps to `finalizeTurnEvents` (`protobuf-events.ts:1327-1328`). Transport still waits for Connect EOF. If EOF arrives after assistant text without `turnEnded`, it synthesizes `done` (`live-transport.ts:1147-1150`). Client-tool Responses path **intentionally** ends turn 1 without waiting for `turnEnded` (`live-transport.ts:203-206`). | ||
|
|
||
| Unknown `interactionQuery` replies empty with matching id so the server unblocks (`live-transport.ts:376-382`, issue #116). Web/exa queries are approved; askQuestion/switchMode rejected (`live-transport.ts:287-366`). | ||
|
|
||
| ## Auth / catalog | ||
|
|
||
| Same Cursor PKCE poll as senpi: `loginDeepControl`, `auth/poll`, `exchange_user_api_key` (`src/oauth/cursor.ts:13-15`). After login, catalog uses stored tokens via `getValidAccessToken`. `GetUsableModels` is empty-body unary (`live-models.ts:12-14, 28`). Decode keeps **ids only** (`live-models.ts:115-131`). Static seed in `discovery.ts` is filtered by live ids; `stripCursorWirePrefix` at the comparison boundary (`discovery.ts:67-84`, issue #117). Effort is a static suffix table (`effort-map.ts`). `RequestedModel.maxMode` is hardcoded `false` (`protobuf-request.ts:963-966`). | ||
|
|
||
| ## Exec | ||
|
|
||
| Known proto cases end at `writeShellStdinArgs` (`gen/agent_pb.ts:6886+`). Dispatcher: `native-exec.ts:550-609`. Default `nativeLocalExec` is **off**; only `"on"` authorizes local fs/shell/fetch (`exec-policy.ts:17-44`). Unknown exec returns `[]` to keep the stream alive (`native-exec.ts:605-609`). Responses `mcpArgs` are **not** executed locally (`live-transport.ts:226-246, 1236-1246`). Native exec emits `local_side_effect` before running so `invalid_argument` remint cannot replay (`live-transport.ts:1248-1252`). | ||
|
|
||
| ## Usage / overflow | ||
|
|
||
| Checkpoint `usedTokens` is absolute context, not an output delta (`protobuf-events.ts:1233-1238`). Conversation-keyed cache: 200 entries / 60 minutes (`protobuf-events.ts:21-22`). Generated `TurnEndedUpdate` is empty (`gen/agent_pb.ts:3083-3085`), so billed cacheRead is not ingested. Generic `resource_exhausted` classifies as 429 unless an explicit size phrase wins (`cursor-errors.ts:131-163`). Transport retry never retries RE (`transport-retry.ts:25`). Conversation remint exists only for external-model `invalid_argument` (`src/adapters/cursor.ts:231-247`). Compaction uses an isolated conversation and does not store its checkpoints (`request-builder.ts:397, 443-444`; `src/server/responses/core.ts:2247-2249`). | ||
|
|
||
| ## OpenCodex-only keepers | ||
|
|
||
| HTTP/1 RunSSE; interactionQuery matrix; fail-closed nativeLocalExec; Responses-tool suspend; JWT-sub multiauth; classified discovery errors; bounded blob KV / checkpoint store; `createTerminalSettler`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # 002 — senpi Cursor inventory | ||
|
|
||
| Research only. senpi `main` SHA `a5eed44536f3024c5740dc3dfff4ffe0bb08b717`. Explorers Pasteur / Archimedes / Plato / Ohm. | ||
|
|
||
| ## Layout | ||
|
|
||
| Cursor is a first-class builtin provider, not an OpenCodex-style proxy adapter. | ||
|
|
||
| - Provider: [packages/ai/src/providers/cursor.ts](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/providers/cursor.ts) — OAuth, empty static catalog, `fetchModels` = live `GetUsableModels` | ||
| - Run client: [packages/ai/src/api/cursor-agent.ts](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/api/cursor-agent.ts) (~4439 lines, Node http2) | ||
| - Lazy load: `cursor-agent.lazy.ts`; Bun static register: `cursor-agent-provider.ts` | ||
| - Catalog grouping: `packages/ai/src/cursor/catalog-grouping.ts`, `model-capabilities.ts`, `selection-descriptor.ts`, `store-migration.ts` | ||
| - OAuth: [packages/ai/src/auth/oauth/cursor.ts](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/auth/oauth/cursor.ts) | ||
| - Rotation: `packages/ai/src/api/cursor-conversation-rotation.ts` | ||
| - Overflow: `packages/ai/src/utils/overflow.ts` | ||
| - Host exec-bridge: `packages/coding-agent/src/core/cursor-exec-bridge.ts` | ||
| - CLI fallback: `packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/` | ||
| - PRs: [#905](https://github.com/code-yeongyu/senpi/pull/905) OAuth, [#910](https://github.com/code-yeongyu/senpi/pull/910) protocol, [#921](https://github.com/code-yeongyu/senpi/pull/921) CLI, [#948](https://github.com/code-yeongyu/senpi/pull/948) reasoning levels, [#1013](https://github.com/code-yeongyu/senpi/pull/1013) ANTML skip, [#1015](https://github.com/code-yeongyu/senpi/pull/1015) compact-before-rotate, [#1062](https://github.com/code-yeongyu/senpi/pull/1062) turnEnded completion | ||
|
|
||
| ## Protocol | ||
|
|
||
| Same `AgentService/Run` Connect path, 5s client heartbeat, client version `cli-2026.07.23-e383d2b`. HTTP/2 only; ALPN-stripping proxy is fatal (no h1 fallback). `turnEnded` is the application completion signal: drain exec ≤5s, then close the client HTTP/2 stream ([cursor-agent.ts L249-254, L698-704](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/api/cursor-agent.ts#L249-L254)). HTTP close without `turnEnded` is an error. Stream-health: 30s no inbound frames, 90s heartbeat/checkpoint-only. | ||
|
|
||
| Unknown exec is `ExecClientThrow` + `streamClose` so the server is never left blocked. Per-exec 3s heartbeat while a handler runs (`exec-lifecycle.ts`). | ||
|
|
||
| `handleServerMessage` has **no `interactionQuery` case** (open [#1026](https://github.com/code-yeongyu/senpi/issues/1026)). | ||
|
|
||
| ## Auth / catalog | ||
|
|
||
| Same PKCE poll. Fail-fast on poll 400/401/403/410; 429 does not burn the transient budget. Catalog is fully dynamic: `models: []`, live GetUsableModels, then `normalizeCursorCatalog` grouping with `thinkingLevelMap` / `cursorReasoning` / `cursorMaxMode`. Live `maxMode` is copied onto `RequestedModel` ([reasoning-params.ts](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/api/cursor-agent/reasoning-params.ts#L8-L20)). | ||
|
|
||
| ## Exec | ||
|
|
||
| Host-injected `CursorExecHandlers` map frames onto senpi tools (`read`/`bash`/`edit`/`write`/`grep`/`find`/`ls` + MCP). Exec-synthesized tool calls are stamped `kCursorExecResolved` so the agent loop does not re-run them. Pi exec family (proto 45–51) is dispatched. Computer-use / canvas / subagents / conversation-search are typed refusals (PR 910). CLI lane is a **separate** spawn of official `cursor-agent -p --output-format stream-json`; tools are display-only; `--force` needs `noApprovalAcknowledgedAt`; kill switch is verbatim `enabled: false`. | ||
|
|
||
| ## Overflow | ||
|
|
||
| 0-token `resource_exhausted` is payload overflow for compact-before-rotate (`overflow.ts` `isCursorPayloadResourceExhausted`). First 0-token RE is **surfaced** so session compaction can run; later ones rotate the wire id up to 3 times (`cursor-conversation-rotation.ts`). Billed `turnEnded` cacheRead that dwarfs checkpoint `usedTokens` (>3×) is ignored ([cursor-agent.ts L3544](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/api/cursor-agent.ts#L3544)). ANTML text-tool recovery is skipped for `api === "cursor-agent"` (PR #1013). Compact while a Cursor Run is live is skipped (#984). Open: [#1043](https://github.com/code-yeongyu/senpi/issues/1043) compact-reload restores full toolResult bodies. | ||
|
|
||
| ## Deliberately not ported (senpi) | ||
|
|
||
| Computer use, subagents, Cursor-managed background shells (typed refuse; OpenCodex actually implements bg shell when native exec is on), canvas, smart-mode classifier, conversation search, Kimi-K3 thinking replay, proxy tunneling (PR 910). | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # 003 — Protocol / transport compare | ||
|
|
||
| Helmholtz + Pasteur. senpi SHA `a5eed44536f3024c5740dc3dfff4ffe0bb08b717`. | ||
|
|
||
| ## Same | ||
|
|
||
| Both speak `agent.v1.AgentService/Run` over HTTP/2 Connect (`application/connect+proto`, `connect-protocol-version: 1`, Bearer, `x-ghost-mode: true`, `x-cursor-client-type: cli`). Both write a 5s `clientHeartbeat`. Both rebuild `rootPromptMessagesJson` as the model prompt and treat `turns[]` as display metadata. Both implement blob KV `getBlobArgs`/`setBlobArgs`. | ||
|
|
||
| OpenCodex: | ||
|
|
||
| ```90:92:src/adapters/cursor/live-transport.ts | ||
| const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run"; | ||
| const CURSOR_CLIENT_VERSION = "cli-2026.07.08-0c04a8a"; | ||
| const HEARTBEAT_MS = 5_000; | ||
| ``` | ||
|
|
||
| senpi: [cursor-agent.ts L522-547, L746-747](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/api/cursor-agent.ts#L522-L547). | ||
|
|
||
| ## Different | ||
|
|
||
| | Topic | OpenCodex | senpi | | ||
| |---|---|---| | ||
| | Client version | `cli-2026.07.08-0c04a8a` | `cli-2026.07.23-e383d2b` | | ||
| | HTTP/1 | `RunSSE` + `BidiAppend` (`http1-bidi.ts:10-11`) | HTTP/2-only; ALPN strip is fatal | | ||
| | Session header | sends `x-session-id` | does not | | ||
| | Completion | `turnEnded` finalizes mapper; transport waits for EOF; may synthesize `done` | `turnEnded` closes client HTTP/2 after ≤5s exec drain | | ||
| | Mid-turn health | 30s first-frame only; then 300s bridge stall | 30s silence / 90s heartbeat-only inside the adapter | | ||
| | Abort owner | `failAndClear` + `createTerminalSettler` | `settleH2` | | ||
| | Exec heartbeat | none (types exist) | 3s per-exec heartbeat | | ||
| | Blob store | TTL / 4096 / 64MiB | unbounded per-conversation Map | | ||
|
|
||
| ## Transfer suspicion | ||
|
|
||
| High: close HTTP/2 on `turnEnded` (frozen turns until bridge 300s). Medium: heartbeat-only stall fail. Low: bump client version without a live probe. Do not copy senpi's unbounded blob Map. Keep OpenCodex HTTP/1 fallback. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # 004 — Auth / catalog / effort / max-mode | ||
|
|
||
| Planck + Archimedes. | ||
|
|
||
| ## Auth — ALREADY-HAVE | ||
|
|
||
| Same three URLs and PKCE params (`challenge`, `uuid`, `mode=login`, `redirectTarget=cli`). | ||
|
|
||
| OpenCodex `src/oauth/cursor.ts:13-15, 78-85`. senpi [oauth/cursor.ts L17-19, L123-130](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/auth/oauth/cursor.ts#L17-L19). | ||
|
|
||
| Delta worth a small adapt: senpi fail-fasts poll 400/401/403/410 and does not spend the transient budget on 429. OpenCodex retries any non-ok as consecutive errors up to 3 (`src/oauth/cursor.ts:121-148`). OpenCodex-only keepers: JWT `sub`/`email` multiauth, 15s refresh timeout, 429/5xx refresh retry. | ||
|
|
||
| Login catalog refresh: senpi auto `fetchModels` after `/login cursor`. OpenCodex clears model cache and tells the operator to `ocx sync` (`src/oauth/index.ts:1234`, `src/oauth/login-cli.ts:95`). | ||
|
|
||
| ## Catalog — different-shape | ||
|
|
||
| OpenCodex: static seed + live id filter + `stripCursorWirePrefix` (`discovery.ts:67-84`). Decode keeps ids only (`live-models.ts:4-6`). Empty 0-byte GetUsableModels body is a Bun HTTP/2 requirement (`live-models.ts:12-14`). | ||
|
|
||
| senpi: no static baseline; live GetUsableModels is the catalog; grouping produces `thinkingLevelMap` / `cursorReasoning` / `legacyAliases` ([providers/cursor.ts L10-17](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/providers/cursor.ts#L10-L17), [catalog-grouping.ts L19-31](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/cursor/catalog-grouping.ts#L19-L31)). Decode keeps `maxMode`, display name, `thinkingDetails` ([cursor-agent.ts L4354-4369](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/api/cursor-agent.ts#L4354-L4369)). | ||
|
|
||
| ## Effort / max-mode | ||
|
|
||
| OpenCodex flattens Codex effort onto a static suffix table (`effort-map.ts:96-108`). Grok Fast is parameterized (`request-builder.ts:182-204`). `RequestedModel.maxMode` is always `false` (`protobuf-request.ts:963-966`; the 934-937 window is debug logging, not maxMode). | ||
|
|
||
| senpi copies live `cursorMaxMode` onto the wire ([reasoning-params.ts L8-20](https://github.com/code-yeongyu/senpi/blob/a5eed44536f3024c5740dc3dfff4ffe0bb08b717/packages/ai/src/api/cursor-agent/reasoning-params.ts#L8-L20)). Family-specific parameters (Claude thinking/context/effort, GPT extra-high, etc.) come from a captured AvailableModels capability table, not from GetUsableModels fields. | ||
|
|
||
| ## Transfer suspicion | ||
|
|
||
| Medium-high: honor live `maxMode` instead of hardcoding false (proto field already exists at `gen/agent_pb.ts:2617`). Medium: fail-fast OAuth poll. Low/product: replace static seed with fully dynamic catalog (OpenCodex still needs logged-out fallback and `auto-{cost,balance,intelligence}` router ids). Do not copy senpi's 204-id alias JSON wholesale. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Distinguish the catalog and request Line 29 cites 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Align the verifier with the verdict exceptions.
Lines 10-12 require every row in
090_transfer_verdict.mdto cite both codebases. Rows T20 and T21 usen/a,whole adapter, orwhole providerinstead. Either define an exception forUNSAFEandNEEDS_HUMANrows or add concrete citations.🤖 Prompt for AI Agents