From 6b889c36e03e7525105a6a6f22765c9c9c1ef916 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 22 Aug 2026 12:23:15 +0900 Subject: [PATCH] devlog: round-2 Cursor stabilization research and roadmap lock (docs-only) --- .../100_stabilization_round2_plan.md | 71 ++++++++++++++++++ .../110_stream_health_watchdog.md | 67 +++++++++++++++++ .../120_small_hardening_pair.md | 51 +++++++++++++ .../190_round2_roadmap_lock.md | 72 +++++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 devlog/_plan/260822_senpi_cursor_transfer/100_stabilization_round2_plan.md create mode 100644 devlog/_plan/260822_senpi_cursor_transfer/110_stream_health_watchdog.md create mode 100644 devlog/_plan/260822_senpi_cursor_transfer/120_small_hardening_pair.md create mode 100644 devlog/_plan/260822_senpi_cursor_transfer/190_round2_roadmap_lock.md diff --git a/devlog/_plan/260822_senpi_cursor_transfer/100_stabilization_round2_plan.md b/devlog/_plan/260822_senpi_cursor_transfer/100_stabilization_round2_plan.md new file mode 100644 index 0000000000..251529c72d --- /dev/null +++ b/devlog/_plan/260822_senpi_cursor_transfer/100_stabilization_round2_plan.md @@ -0,0 +1,71 @@ +# 100 — Stabilization round 2 (research + implementation loop) + +Continuation of the 090 verdict. T01/T03/T05 landed (#2320/#2321/#2322). This +round re-inventories what REMAINS transferable from senpi and +yelixir-dev/cursor-ai-proxy-bridge (and any other public Cursor bridge found +during the swarm), locks a new decade-doc roadmap (110+), then implements the +top candidates as separate cycles, each pushed to dev. + +## Inputs + +- origin/dev head at research time: `525568652` (weighted credential router, unwired). +- Selected remaining 090 verdict candidates: T02 (conversation rotation), T04 + (heartbeat stall), T06 (maxMode), T07 (OAuth poll fail-fast), T09 (cacheRead + clamp), T24 (EOF-without-turnEnded fold into T03). Unlanded ADAPT rows T08 + (per-exec heartbeat — conditional on long native-exec staying enabled; + default off, so deferred unless research contradicts) and T10 (dedicated + proto unit prerequisite) are dispositioned in 190, not silently dropped. +- New-in-dev artifacts needing follow-up regardless of senpi: #2334 + CursorCredentialRouter is dead code (only tests import it); cursorH2Pool has + no shutdown hook wiring. + +## Security / ToS boundary (binding, per AGENTS.md) + +- No pre-disclosure security material in this public devlog: if research + surfaces an unfixed weakness (in Cursor, senpi, or OpenCodex), the analysis + goes to `.tmp/` scratch and the devlog records only a neutral + "handled out-of-band" pointer once resolved. +- Excluded transfer classes regardless of source value: leaked/private + artifacts, credential extraction, auth bypass, Safe Storage / native-app + patching (090 T20 stays UNSAFE), live account mutation. +- ToS/product-policy questions (e.g. new endpoints whose use may be + policy-sensitive) are NEEDS_HUMAN, not merely "needs live probe". +- Reference clones live in gitignored scratch (`.tmp/chase/`), matching the + `devlog/_chase/` license rule: third-party source never enters this + repository's history. + +## Research lanes (Luna swarm, candidates only — main agent proves) + +1. senpi delta since a5eed44536f3 (commits/releases): new Cursor mechanisms. +2. senpi issues/PRs: open stability reports naming Cursor adapter defects. +3. yelixir-dev/cursor-ai-proxy-bridge full file inventory beyond + h2-session-pool.ts / credentials.ts. +4. Other public Cursor-protocol bridges/proxies (GitHub sweep). +5. Cursor upstream changes (client version strings, api2 endpoints, protocol + deprecations) that could break the adapter soon. +6. Local-clone deep read (main agent, .tmp/chase/senpi + + .tmp/chase/cursor-ai-proxy-bridge): git history, issues-referenced diffs, + and rationale not visible in file inventories. +7. OpenCodex's own Cursor issue/PR/test delta on GitHub since 090 lock, so + locally-reported regressions rank alongside external candidates. + +## Verification lane (sol-medium, read-only) + +Audit backlog items (a)-(f) from the goal objective against origin/dev head +with file/line evidence: wired-or-dead status of #2334, shutdown hook absence, +T04/T06/T07/T24 current state in live-transport.ts / oauth/cursor.ts / +live-models.ts. Every NEW candidate from lanes 1-7 gets the same falsification +pass against the current tree before it may enter a decade doc — no candidate +is roadmapped on snippet evidence alone. + +## Output contract + +- Decade docs 110, 120, ... — one per implementation cycle, diff-level + (target files, function names, test names, expected diff shape). +- 190_roadmap_lock.md — ranked order, rejected/deferred candidates with + reasons, NEEDS_HUMAN items (live-probe-only) explicitly marked. +- No production code in this cycle. +- Gate: implementation cycles may not start until 190 is locked (the D of + this docs-only cycle). "Pushed to dev" in the header describes those later + cycles, each separately gated by typecheck + full tests; the docs-only + cycle pushes documentation only. diff --git a/devlog/_plan/260822_senpi_cursor_transfer/110_stream_health_watchdog.md b/devlog/_plan/260822_senpi_cursor_transfer/110_stream_health_watchdog.md new file mode 100644 index 0000000000..ef82190220 --- /dev/null +++ b/devlog/_plan/260822_senpi_cursor_transfer/110_stream_health_watchdog.md @@ -0,0 +1,67 @@ +# 110 — Inbound stream-health watchdog (T04, senpi #1062 second half) + +## Why now + +OpenCodex issue #2210 reports Cursor/Grok turns dying with +`upstream_stall_timeout` after a silent stream — the 300s bridge default +(`src/stall-timeout.ts:8`) is the only guard after the first frame. senpi +PR #1062 pairs the turnEnded close (already landed as #2321) with an +inbound-frame watchdog we did NOT take: 30s of total inbound silence, or 90s +of heartbeat/checkpoint-only traffic, fails the turn instead of waiting for +the bridge. + +## Current state (verified 525568652) + +- `live-transport.ts:93` `CURSOR_FIRST_FRAME_TIMEOUT_MS = 30_000` — armed + once, cleared permanently by the FIRST raw chunk (`onData` calls + `clearFirstFrameTimer()` unconditionally, live-transport.ts:1133). +- The 5s HEARTBEAT_MS at :92 is OUTBOUND client traffic, not a detector. +- No transport-level watchdog exists after the first chunk; sol audit lane + confirmed GAP (c) with file/line refs. + +## Design (ADAPT, not copy) + +senpi resets `lastInboundFrameAt` on every decoded frame and +`lastMeaningfulFrameAt` only when the frame is not liveness-only +(heartbeat / conversationCheckpointUpdate), then arms one timer at +`min(lastInbound+30s, lastMeaningful+90s)` (cursor-agent.ts:589-673 in the +.tmp/chase clone). OpenCodex differences to respect: + +- Our decode path is `handleFrame` inside live-transport.ts, protobuf event + mapping in protobuf-events.ts; liveness classification must happen where + the AgentServerMessage case is visible, not on raw chunks — raw-chunk + resets would let TLS keepalive noise defeat the watchdog. +- Client-tool suspend (live-transport.ts:203-206) intentionally ends without + turnEnded: the watchdog must disarm when the transport is settling or a + client-tool suspend is in progress, mirroring the #2321 grace-timer guards + (expectedClose). +- Long native-exec turns emit synthetic progress; those count as inbound + frames already (they arrive as real server frames), so no special case — + 090's warning about "not fighting synthetic progress heartbeats" is + satisfied by the meaningful/liveness split. +- Timeout action: fail the turn through the SAME error path a transport + error takes today (failAndClear with a typed message naming the stall + class), so bridge mapping and tests stay uniform. + +## Diff shape + +- `src/adapters/cursor/live-transport.ts`: two constants + (`CURSOR_STREAM_SILENCE_FAIL_MS = 30_000`, + `CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000`), fields + `lastInboundFrameAt` / `lastMeaningfulFrameAt` / `streamHealthTimer`, + arm/reset/disarm helpers; reset hooks in the decoded-frame path; disarm in + finalize/cleanup paths alongside firstFrameTimer/turnEndedCloseTimer. +- Optional input knobs on CursorTransportFactoryInput mirroring + `firstFrameTimeoutMs` for tests. +- Tests: `tests/cursor-stream-health.test.ts` — (1) silent stream after + first frame fails at ~30s (fake timers); (2) heartbeat-only stream + survives 30s but fails at 90s; (3) meaningful frames keep resetting both; + (4) client-tool suspend path never trips the watchdog; (5) turnEnded + disarms it. + +## Risks + +- False positives on genuinely slow models: thresholds are senpi-live-tested + but our traffic mix differs; keep knobs overridable and document defaults. +- Interaction with #2307 clean-terminal settle: watchdog must check the + settler state before firing (same guard the grace timer uses). diff --git a/devlog/_plan/260822_senpi_cursor_transfer/120_small_hardening_pair.md b/devlog/_plan/260822_senpi_cursor_transfer/120_small_hardening_pair.md new file mode 100644 index 0000000000..883aac9aaf --- /dev/null +++ b/devlog/_plan/260822_senpi_cursor_transfer/120_small_hardening_pair.md @@ -0,0 +1,51 @@ +# 120 — Small hardening pair: OAuth poll fail-fast (T07) + H2 pool shutdown + +Two independent, low-risk fixes small enough to share one cycle; neither +depends on 110. + +## 120a — OAuth poll fail-fast on terminal statuses (T07) + +### Current state (verified 525568652) + +`src/oauth/cursor.ts:108-149` `pollCursorAuth`: 404 = pending, 200 = done, +EVERY other status throws into the generic catch and retries until +3 consecutive errors. A denied/expired login (400/401/403/410) costs three +extra round-trips and surfaces as "Too many consecutive errors" instead of +the real reason. senpi oauth/cursor.ts L165-178 (PR #905) fails immediately +on 400/401/403/410. + +### Diff shape + +- `src/oauth/cursor.ts`: inside the status dispatch, add + `if ([400, 401, 403, 410].includes(response.status)) throw new CursorAuthTerminalError(...)` + where the error carries the status and is NOT retried by the catch block + (rethrow when `err instanceof CursorAuthTerminalError`). +- Keep 5xx/network on the existing 3-strike retry path (OpenCodex keeps its + refresh retry / JWT accountId handling — 090 T07 note). +- Tests: extend `tests/cursor-oauth.test.ts` — 401 fails on FIRST attempt + with status in message; 500 still retries 3x; 404→200 still succeeds. + +## 120b — cursorH2Pool shutdown registration + +### Current state + +`cursorH2Pool.shutdown()` (`src/adapters/cursor/h2-pool.ts:41`) has no +caller. The core-owned seam exists: `src/lib/optional-shutdown-hooks.ts:32` +registry, invoked by `src/server/lifecycle.ts:454`. Lab registers teardown +at activation (orchestrator.ts:109). The seam's hook contract must be +checked: if it is sync-only, register `() => { void cursorH2Pool.shutdown(); }` +or extend the seam if it already awaits promises (verify before coding). + +### Diff shape + +- Registration at the point the pool first activates — lazily inside + `h2-pool.ts` on first `request()` (keeps core free of adapter imports, + matching the optional-subsystem doctrine) via + `registerOptionalShutdownHook("cursor-h2-pool", ...)`. +- Also correct the pool doc comment: it claims "GetUsableModels / Run + requests" reuse, but the Run path dials its own session + (live-transport.ts:928); comment must say discovery-only until Run-path + integration is a separate, deliberate cycle (deferred — see 190). +- Tests: `tests/cursor-h2-pool.test.ts` (or extend existing) — after + registration, invoking the registered hook closes sessions (pool.size 0) + and is idempotent. diff --git a/devlog/_plan/260822_senpi_cursor_transfer/190_round2_roadmap_lock.md b/devlog/_plan/260822_senpi_cursor_transfer/190_round2_roadmap_lock.md new file mode 100644 index 0000000000..207f50978d --- /dev/null +++ b/devlog/_plan/260822_senpi_cursor_transfer/190_round2_roadmap_lock.md @@ -0,0 +1,72 @@ +# 190 — Round-2 roadmap lock + +Locked from: 5 Luna research lanes (senpi delta, senpi issues, yelixir +inventory, other bridges, upstream), local clones under .tmp/chase/ (senpi +@041bb5e64, cursor-ai-proxy-bridge @main), sol read-only code audit of +origin/dev 525568652, and OpenCodex open Cursor issues (#1527, #2210, #2300, +#2305). + +## Implementation order (this loop) + +1. **110 — inbound stream-health watchdog (T04)**. Directly addresses open + issue #2210 (silent stream → upstream_stall_timeout at 300s). Verified + GAP: only a first-frame timer exists (live-transport.ts:93,1133). senpi + constants live-verified in clone (cursor-agent.ts:250-252, 589-673). +2. **120 — OAuth poll fail-fast (T07) + cursorH2Pool shutdown hook**. + Verified GAPs: cursor.ts:117-147 retries terminal 4xx thrice; + h2-pool.ts:41 shutdown() has no caller; hook seam is sync-only + (optional-shutdown-hooks.ts:23) so the registration wraps the async + shutdown in a void fire-and-forget. + +## Deferred / rejected this round (with reasons) + +- **#2334 CursorCredentialRouter wiring — NEEDS_HUMAN.** Natural seam is the + OAuth snapshot-selection boundary (oauth/index.ts:463 → + responses/core.ts:2615), but wiring weighted rotation there overrides the + user's explicit activeAccountId choice. That is a product decision + (multi-account rotation semantics), not a stabilization patch. Until + decided, the module stays test-covered but unwired; its doc comment + already says "complements" rather than "replaces". +- **H2 pool Run-path integration — deferred.** Run streams are long-lived + bidi; pooling them changes lifecycle/EOF semantics that #2307/#2321 just + stabilized. Discovery-only stays. 120b fixes the overclaiming comment. +- **T02 conversation rotation — deferred.** senpi #998 persists rotated ids + under its own agent dir; OpenCodex equivalent needs checkpoint-store + migration via existing rekey and evidence that Codex compact does not + already recover (090 residual unknown still unresolved; #1527 may be this + class — needs a live reproduction first). +- **T06 maxMode — deferred (live probe).** GAP confirmed (hardcoded false, + protobuf-request.ts:970; discovery drops ModelDetails.maxMode, + live-models.ts:116), but 090 requires a live probe to show user-visible + gain and billing semantics before flipping a wire flag. +- **T08 per-exec heartbeat — deferred.** Long native exec remains + default-off; senpi's 3s ExecClientHeartbeat only matters with it enabled. +- **T09 cacheRead clamp — deferred.** Needs live billed turnEnded int64 + evidence (090 residual unknown). +- **T10 protobuf regen — deferred.** Requires a dedicated proto unit per + 090; touching gen/ ad hoc is not stabilization. +- **senpi #1020 suffix-alias — NOOP for OpenCodex.** Our effort-map already + flattens suffix variants (090 T14 kept static tiers; request-builder + suffix flatten at :187-204 on the audited head). +- **senpi #1016 stop-with-pending-tools, #1002 exec run ownership — out of + adapter scope here.** Both live in senpi's agent loop; OpenCodex's + analogues are the bridge/Responses layer. Issue #2305 (tool-call-like + text to Pi on client-tool continuation) is the closest local symptom and + deserves its own unit with a reproduction, not a blind port. +- **yelixir retry.ts / auto-runtime failover — partially rejected.** The + transport-code retry table overlaps cursor-errors.ts mapping already + landed (T01). The API→CLI backend failover is a product architecture + OpenCodex does not have (no CLI backend); single useful residue is the + non-retryable Cursor errorType detail sniffing, folded as a candidate + into a future cursor-errors extension if live reports justify it. +- **cursor/sdk-bridge (official SDK) — tracked, not actioned.** A future + migration study unit; policy-sensitive surface questions are NEEDS_HUMAN + per the 100 boundary. +- **api2direct host migration reports — watch only.** Forum-level evidence, + no reproducible breakage against our pinned client version yet. + +## Gate + +This lock is the D of the docs-only cycle. Implementation cycles 110 → 120 +follow, one decade doc per PABCD cycle, each gated by focused tests + +typecheck + full suite before its dev push.