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
18 changes: 18 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ the upstream HTTP-version helper. Server, provider, and WebSocket data types rem
It must not import routing, combos, OAuth, adapters, sidecars, response parsing, logging, or relay
modules merely because those imports existed in the pre-split `responses.ts` monolith.

### Semantic progress ownership

The Responses proxy does not treat transcript growth as repository progress. It can observe request
boundaries, response items, tool names and payloads, adapter events, retained bytes, and elapsed
silence. It cannot observe the client's workspace or prove whether a successful tool result changed
repository state. Consequently, the active-turn and session-lane gates are concurrency admission
limits, the translator budget is a live retained-byte limit, the response-state caps are cache
retention limits, and the stall watchdog is a silence limit. None is a cumulative continuation or
semantic no-progress budget.

[Decision Log]
- 목적과 의도: Keep long but progressing client-driven tool continuations valid while locating repository-semantic loop detection at the layer that owns the workspace and continuation policy.
- 기존 구현 및 제약 조건: Issue #2600 recorded 18 persisted Cursor continuations whose transcript and tool counters grew while the worktree did not. Every proxy-local liveness and capacity bound was therefore satisfied, but the proxy had no workspace delta to compare.
- 검토한 주요 대안: Stop after a fixed continuation count; classify read-like tool names as no progress; compare assistant prose; emit a new proxy-only terminal code after a time budget; or leave semantic progress to the client while preserving transport cancellation for objective proxy failures.
- 선택한 방식: Do not add a proxy semantic cutoff without a client-supplied progress contract. Keep objective transport, byte, concurrency, and silence bounds typed and cancellable; require the workspace-owning client to bound repeated continuations using repository state plus its own side-effect ledger.
- 다른 대안 대신 이 방식을 선택한 이유: Calls and prose are not a repository oracle, and tool names do not prove side effects. A proxy cutoff would either miss the reported loop because items kept changing or terminate legitimate slow work. Retrying after the cutoff could also replay side-effecting work.
- 장점, 단점 및 영향: OpenCodex does not manufacture a root cause or silently terminate healthy long turns. The combined route still needs a client-side semantic boundary; if a future client sends an explicit privacy-safe progress marker, the proxy may enforce that contract without inferring workspace state.

[Decision Log]
- 목적과 의도: Keep transport helpers reusable without making every consumer evaluate the full routed Responses and sidecar graph at module load.
- 기존 구현 및 제약 조건: The original `responses.ts` split copied the monolith import header into `fetch-helpers.ts`; seven helper exports therefore retained 39 distinct runtime import specifiers and reached 326 modules even though the implementations used only three runtime dependencies.
Expand Down
12 changes: 11 additions & 1 deletion tests/active-registry-admission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { abortAndReleaseAllTurns, activeRegistryMetrics, trackStreamLifetime, tryAdmitTurn, unregisterTurn } from "../src/server/lifecycle";
import { MAX_ACTIVE_TURNS, abortAndReleaseAllTurns, activeRegistryMetrics, trackStreamLifetime, tryAdmitTurn, unregisterTurn } from "../src/server/lifecycle";
import {
MAX_TRACKED_CODEX_WEBSOCKETS,
getTrackedCodexWebSocketCountForAccount,
Expand Down Expand Up @@ -176,6 +176,16 @@ describe("active registry admission", () => {
expect(activeRegistryMetrics().activeTurns.active).toBe(before);
});

test("the 256-turn gate bounds concurrency, not sequential continuation count", () => {
const before = activeRegistryMetrics().activeTurns.active;
for (let index = 0; index <= MAX_ACTIVE_TURNS; index += 1) {
const lease = tryAdmitTurn("one-logical-session");
expect(lease).not.toBeNull();
lease!.release();
}
expect(activeRegistryMetrics().activeTurns.active).toBe(before);
});

test("forced shutdown abort releases every lease and later finalizers cause no miss or underflow", () => {
const before = activeRegistryMetrics().activeTurns;
const controllers = [new AbortController(), new AbortController()];
Expand Down
28 changes: 28 additions & 0 deletions tests/responses-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,34 @@ describe("Responses previous_response_id state", () => {
clearResponseStateMemoryForTests();
});

test("a slow progressing Cursor chain remains replayable beyond the reported 18 continuations", () => {
let request: Record<string, unknown> = {
model: "cursor/grok-4.6",
input: [{ role: "user", content: "bounded task" }],
store: false,
};

for (let turn = 0; turn < 32; turn += 1) {
const responseId = `resp_progress_${turn}`;
const callId = `call_progress_${turn}`;
rememberResponseState(
request,
fixedResponse(responseId, [{ type: "function_call", call_id: callId, name: "inspect", arguments: "{}" }]),
{ cursor: { conversationId: "cursor_progress_chain" } },
{ force: true },
);
request = expandPreviousResponseInput({
model: "cursor/grok-4.6",
previous_response_id: responseId,
input: [{ type: "function_call_output", call_id: callId, output: `new evidence ${turn}` }],
store: false,
}) as Record<string, unknown>;
}

expect(request.input).toBeArrayOfSize(65);
expect(previousResponseConversationId("resp_progress_31")).toBe("cursor_progress_chain");
});

afterEach(() => {
setSpillIoForTest(null);
setIcaclsRunnerForTests(null);
Expand Down
Loading