Skip to content

Commit 6e0eca1

Browse files
committed
fix: final-review follow-ups — legacy SIDECAR=0 http1 under Bun, variant collision loop, abort parity, docs
1 parent bef6e5a commit 6e0eca1

11 files changed

Lines changed: 72 additions & 6 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7-
## [0.5.0] — 2026-07-23
7+
## [0.5.0-next.0] — 2026-07-23
88

99
Native-experience overhaul: in-process HTTP/1.1 transport under Bun, typed-error
1010
reliability, and full streaming fidelity.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ See [SECURITY.md](./SECURITY.md) for the full threat model.
156156
| `OPENCODE_CURSOR_TRANSPORT` || Force a transport: `http1` \| `http2-direct` \| `sidecar` — see [Transport](#transport) |
157157
| `OPENCODE_CURSOR_STALL_MS` | `60000` | Stream watchdog timeout (ms); `0` disables — see [Reliability](#reliability) |
158158
| `OPENCODE_CURSOR_SIDECAR` || Legacy: `1` maps to `sidecar`, `0` maps to `http2-direct` (superseded by `OPENCODE_CURSOR_TRANSPORT`) |
159+
| `OPENCODE_CURSOR_TOOL_INPUT_STREAM` | on | Set to `0` to disable live tool-input streaming (`tool-input-start`/`-delta`/`-end` parts) |
159160

160161
### Session reuse (`session`)
161162

src/model-variants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ export function buildModelVariants(item: ModelListItem): Record<string, CursorVa
7272
const params: Record<string, string> = { ...defaults };
7373
for (const p of v.params ?? []) params[p.id] = p.value;
7474
const key = variantKey(v.displayName);
75-
out[out[key] === undefined ? key : `${key}-2`] = { params };
75+
let candidate = key;
76+
for (let n = 2; out[candidate] !== undefined; n++) candidate = `${key}-${n}`;
77+
out[candidate] = { params };
7678
}
7779
return out;
7880
}

src/provider/agent-backend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function resolveTransport(env: BackendEnvironment): TransportKind {
7575
if (legacy === "1" || legacy === "true") {
7676
return env.nodePath ? "sidecar" : env.isBun ? "http1" : "http2-direct";
7777
}
78-
if (legacy === "0" || legacy === "false") return "http2-direct";
78+
if (legacy === "0" || legacy === "false") return env.isBun ? "http1" : "http2-direct";
7979
return env.isBun ? DEFAULT_BUN_TRANSPORT : "http2-direct";
8080
}
8181

src/provider/agent-events.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ export async function* streamAgentTurn(
197197
)
198198
.then(async (run) => {
199199
runHolder.run = run;
200+
// The signal may have fired while send() was in flight (before runHolder
201+
// was populated, so onAbort had nothing to cancel); cancel now.
202+
if (options.abortSignal?.aborted) void Promise.resolve(run.cancel()).catch(() => {});
200203
const result = await run.wait();
201204
if (debug) {
202205
console.error(

src/provider/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export interface CursorProviderOptions {
9393
/**
9494
* Transport for Cursor agent traffic: "http1" (in-process, Bun-safe),
9595
* "http2-direct" (in-process, Node only), "sidecar" (Node child, rollback).
96-
* Beats OPENCODE_CURSOR_TRANSPORT. Process-global: first provider wins.
96+
* Beats OPENCODE_CURSOR_TRANSPORT. Process-global: last provider to set it wins.
9797
*/
9898
transport?: "http1" | "http2-direct" | "sidecar";
9999
}

src/provider/language-model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ export class CursorLanguageModel implements LanguageModelV3 {
369369
{
370370
mode,
371371
abortSignal: options.abortSignal,
372+
// Key intentionally diverges from the single-turn key: the multi-replay and single-turn branches are mutually exclusive.
372373
idempotencyKey: sendIdempotencyKey(
373374
sessionID,
374375
{

test/agent-events.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,47 @@ describe("sendAgentTurnSilently", () => {
252252
await promise;
253253
expect(cancelled).toBe(true);
254254
});
255+
256+
it("streamAgentTurn cancels the run when abort fires during the in-flight send", async () => {
257+
// Abort lands after send() is called but before runHolder.run is
258+
// populated (onAbort has nothing to cancel). The post-assignment guard
259+
// in startRun must still cancel the resolved run.
260+
let cancelled = false;
261+
const controller = new AbortController();
262+
let releaseSend: (() => void) | undefined;
263+
const agent = {
264+
agentId: "agent-inflight-abort",
265+
send: async (
266+
_message: SDKUserMessage,
267+
_sendOptions?: Record<string, unknown>,
268+
) => {
269+
// Hold send() in flight until the caller releases it (post-abort).
270+
await new Promise<void>((resolve) => {
271+
releaseSend = resolve;
272+
});
273+
const run: Partial<Run> = {
274+
wait: async () => ({ status: "cancelled" }) as never,
275+
cancel: async () => {
276+
cancelled = true;
277+
},
278+
};
279+
return run as Run;
280+
},
281+
} as unknown as AgentLike;
282+
283+
const promise = collect(
284+
streamAgentTurn(agent, MESSAGE, {
285+
mode: "agent",
286+
abortSignal: controller.signal,
287+
}),
288+
);
289+
// Abort while send() is still in flight, then let send() resolve.
290+
await new Promise((r) => setTimeout(r, 5));
291+
controller.abort();
292+
releaseSend?.();
293+
await promise;
294+
expect(cancelled).toBe(true);
295+
});
255296
});
256297

257298
describe("streamAgentTurn MCP error surfacing", () => {

test/model-variants.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,22 @@ describe("buildModelVariants", () => {
259259
expect(variants["turbo"]?.params).toEqual({ fast: "true", thinking: "high" });
260260
});
261261

262+
it("assigns distinct keys when three SDK variants slug to the same key", () => {
263+
// Three displayNames that slugify identically must yield three distinct
264+
// keys (key, key-2, key-3) via the collision counter — no variant lost.
265+
const item = {
266+
id: "m", displayName: "m",
267+
parameters: [{ id: "thinking", values: [{ value: "low" }, { value: "high" }] }],
268+
variants: [
269+
{ params: [{ id: "thinking", value: "low" }], displayName: "Deep Think" },
270+
{ params: [{ id: "thinking", value: "high" }], displayName: "Deep-Think" },
271+
{ params: [{ id: "thinking", value: "low" }], displayName: "Deep Think!" },
272+
],
273+
} as unknown as ModelListItem;
274+
const variants = buildModelVariants(item);
275+
expect(Object.keys(variants).sort()).toEqual(["deep-think", "deep-think-2", "deep-think-3"]);
276+
});
277+
262278
it("falls back to generated variants when SDK variants absent", () => {
263279
const item = {
264280
id: "m", displayName: "m",

test/stream-map.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ describe("cursorEventsToStream", () => {
486486
expect(types(parts)).toContain("text-end");
487487
const finish = parts.find((p) => p.type === "finish");
488488
expect(finish).toMatchObject({ finishReason: { unified: "error" } });
489+
expect(parts.filter((p) => p.type === "finish")).toHaveLength(1);
489490
});
490491

491492
it("uses empty usage when no usage event arrives", async () => {

0 commit comments

Comments
 (0)