Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Loading