cursor-opencode-provider 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/README.md +204 -37
- package/SECURITY.md +20 -8
- package/dist/activity.d.ts +73 -1
- package/dist/activity.js +231 -5
- package/dist/agent-url.js +37 -9
- package/dist/auth-renewal.d.ts +1 -1
- package/dist/auth-renewal.js +58 -26
- package/dist/billing.d.ts +105 -0
- package/dist/billing.js +212 -0
- package/dist/classic-tool-results.d.ts +1 -0
- package/dist/classic-tool-results.js +42 -0
- package/dist/context/build.d.ts +1 -0
- package/dist/context/build.js +32 -5
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/paths.d.ts +1 -1
- package/dist/context/paths.js +7 -4
- package/dist/debug.js +13 -2
- package/dist/early-tool-rows.d.ts +60 -0
- package/dist/early-tool-rows.js +96 -0
- package/dist/errors.d.ts +49 -7
- package/dist/errors.js +163 -8
- package/dist/host-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.js +4 -3
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +114 -23
- package/dist/language-model.js +2290 -469
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +50 -29
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- package/dist/opencode2/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- package/dist/plugin-opencode2.js +185 -25
- package/dist/plugin.js +28 -5
- package/dist/pricing-data.d.ts +26 -2
- package/dist/pricing-data.js +26 -2
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +7 -4
- package/dist/process-shared.d.ts +14 -0
- package/dist/process-shared.js +18 -0
- package/dist/protocol/apply-patch.d.ts +25 -13
- package/dist/protocol/apply-patch.js +79 -30
- package/dist/protocol/blob-store.d.ts +5 -1
- package/dist/protocol/blob-store.js +10 -6
- package/dist/protocol/client-version.js +6 -4
- package/dist/protocol/conversation-cache-header.d.ts +23 -0
- package/dist/protocol/conversation-cache-header.js +149 -0
- package/dist/protocol/conversation-persistence.d.ts +27 -1
- package/dist/protocol/conversation-persistence.js +628 -126
- package/dist/protocol/conversation-state.d.ts +8 -1
- package/dist/protocol/conversation-state.js +28 -7
- package/dist/protocol/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/messages.js +91 -8
- package/dist/protocol/request.d.ts +35 -19
- package/dist/protocol/request.js +53 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- package/dist/protocol/tool-call-bridge.d.ts +2 -0
- package/dist/protocol/tool-call-bridge.js +10 -0
- package/dist/protocol/tools.d.ts +74 -8
- package/dist/protocol/tools.js +431 -141
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +16 -4
- package/dist/replay-safety.js +34 -25
- package/dist/retry-budget.d.ts +21 -0
- package/dist/retry-budget.js +94 -0
- package/dist/runtime-files.d.ts +23 -0
- package/dist/runtime-files.js +581 -0
- package/dist/session.d.ts +94 -9
- package/dist/session.js +153 -34
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/shell-timeout.d.ts +54 -10
- package/dist/shell-timeout.js +362 -92
- package/dist/stream-write.d.ts +20 -0
- package/dist/stream-write.js +87 -0
- package/dist/terminal-runner.d.ts +15 -0
- package/dist/terminal-runner.js +528 -0
- package/dist/transport/connect.d.ts +8 -1
- package/dist/transport/connect.js +103 -23
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +63 -29
- package/package.json +3 -2
- package/dist/protocol/thinking.d.ts +0 -15
- package/dist/protocol/thinking.js +0 -17
package/dist/session.d.ts
CHANGED
|
@@ -3,12 +3,18 @@ import { type CursorProviderError } from "./errors.js";
|
|
|
3
3
|
import { type SessionActivitySource } from "./activity.js";
|
|
4
4
|
import type { HostSubagentCatalog, HostToolDialect, OpencodeToolDef, ToolAliasRegistry } from "./protocol/tools.js";
|
|
5
5
|
import type { CursorConversationTokenDetails } from "./protocol/token-details.js";
|
|
6
|
+
import type { HostNoteBatch } from "./protocol/host-notes.js";
|
|
7
|
+
import type { RunHistoryImages } from "./protocol/sent-history-images.js";
|
|
6
8
|
import type { ParallelStepState } from "./parallel-step.js";
|
|
9
|
+
import type { OpenCodeModelCost } from "./pricing.js";
|
|
10
|
+
import type { RunCostEstimate } from "./billing.js";
|
|
11
|
+
import type { ReplaySafetyState } from "./replay-safety.js";
|
|
7
12
|
export type Frame = {
|
|
8
13
|
flags: number;
|
|
9
14
|
payload: Uint8Array;
|
|
10
15
|
};
|
|
11
16
|
export type CursorContinuationOptions = {
|
|
17
|
+
/** A Run that owes the host nothing fails after this long without any inbound frame. */
|
|
12
18
|
semanticIdleMs?: number;
|
|
13
19
|
/** @deprecated Use semanticIdleMs. Kept as a strict alias for compatibility. */
|
|
14
20
|
softHealthMs?: number;
|
|
@@ -51,8 +57,14 @@ export type PendingExec = {
|
|
|
51
57
|
* exec result back to Cursor — just clear pending and keep pumping.
|
|
52
58
|
*/
|
|
53
59
|
bridged?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Cursor withdrew the exec (`ExecServerControlMessage.abort`). As in Cursor
|
|
62
|
+
* CLI, nothing is written back for it, but the host's late result still
|
|
63
|
+
* finds this Run so the turn goes on.
|
|
64
|
+
*/
|
|
65
|
+
aborted?: boolean;
|
|
54
66
|
};
|
|
55
|
-
export type ContinuationTerminalReason = "hard-cap-expired" | "remote-clean-close" | "remote-error" | "result-write-failed" | "ambiguous-partial-write" | "heartbeat-write-failed" | "reply-write-failed" | "process-disposed" | "superseded-by-new-run" | "open-session-cap-exceeded";
|
|
67
|
+
export type ContinuationTerminalReason = "host-interrupted" | "hard-cap-expired" | "remote-clean-close" | "remote-error" | "result-write-failed" | "ambiguous-partial-write" | "heartbeat-write-failed" | "reply-write-failed" | "process-disposed" | "superseded-by-new-run" | "open-session-cap-exceeded";
|
|
56
68
|
export type SessionCloseReason = ContinuationTerminalReason | "ordinary-cleanup" | "turn-ended" | "initial-write-failed";
|
|
57
69
|
export type ContinuationClaim = {
|
|
58
70
|
session: CursorSession;
|
|
@@ -79,7 +91,7 @@ export type DeliveryOutcome = {
|
|
|
79
91
|
} | {
|
|
80
92
|
kind: "duplicate";
|
|
81
93
|
reason: "in-flight" | "delivered";
|
|
82
|
-
framesWritten:
|
|
94
|
+
framesWritten: number;
|
|
83
95
|
} | {
|
|
84
96
|
kind: "terminal";
|
|
85
97
|
reason: ContinuationTerminalReason;
|
|
@@ -87,7 +99,7 @@ export type DeliveryOutcome = {
|
|
|
87
99
|
} | {
|
|
88
100
|
kind: "missing";
|
|
89
101
|
reason: "missing-process-local-state";
|
|
90
|
-
framesWritten:
|
|
102
|
+
framesWritten: number;
|
|
91
103
|
};
|
|
92
104
|
export type CursorSession = {
|
|
93
105
|
/**
|
|
@@ -98,6 +110,8 @@ export type CursorSession = {
|
|
|
98
110
|
sessionId: string;
|
|
99
111
|
/** AgentRunRequest.run_id, for context injections bound to this Run. */
|
|
100
112
|
runId?: string;
|
|
113
|
+
/** Host notes consumed by this Run's initial user action, retained for a rebase. */
|
|
114
|
+
userTurnHostNotes?: HostNoteBatch[];
|
|
101
115
|
/**
|
|
102
116
|
* Cursor conversation_id for this Run — used to store/echo
|
|
103
117
|
* conversation_checkpoint_update (CLI parity).
|
|
@@ -107,10 +121,32 @@ export type CursorSession = {
|
|
|
107
121
|
cacheDir?: string;
|
|
108
122
|
/** Latest eligible checkpoint emitted by this Run attempt. */
|
|
109
123
|
resumeCheckpoint?: Uint8Array;
|
|
124
|
+
/**
|
|
125
|
+
* This Run sent a user message or tool result after its last checkpoint:
|
|
126
|
+
* recovery must not resume earlier state that lacks the delivered input.
|
|
127
|
+
*/
|
|
128
|
+
checkpointRevoked?: boolean;
|
|
129
|
+
/** Only a delivered host result, rather than an unrepresented follow-up, revoked it. */
|
|
130
|
+
checkpointRevokedByHostResult?: boolean;
|
|
131
|
+
/** Ignore queued checkpoints received before the latest message/result delivery. */
|
|
132
|
+
resumeCheckpointAfterFrame?: number;
|
|
133
|
+
/** Stateful replay barriers survive host tool-step boundaries on this Run. */
|
|
134
|
+
replaySafety?: ReplaySafetyState;
|
|
135
|
+
/** History images this Run's checkpoints hold or may hold; absent for a Run outside an OpenCode session. */
|
|
136
|
+
historyImages?: RunHistoryImages;
|
|
110
137
|
/** Last checkpoint-derived context snapshot, independent of retry eligibility. */
|
|
111
138
|
tokenDetails?: CursorConversationTokenDetails;
|
|
112
139
|
/** True only after this Run receives a checkpoint containing token details. */
|
|
113
140
|
tokenDetailsFresh?: boolean;
|
|
141
|
+
/** Cost reporting for this Run's steps (`src/billing.ts`). */
|
|
142
|
+
billing: RunCostEstimate & {
|
|
143
|
+
/** Ledger key: the OpenCode session, or this Run for an ephemeral one. */
|
|
144
|
+
key: string;
|
|
145
|
+
/** Catalog cost entry; absent for a model Cursor publishes no rate for. */
|
|
146
|
+
cost?: OpenCodeModelCost;
|
|
147
|
+
/** Context the previous billed step already sent: read from cache on the next one. */
|
|
148
|
+
prefixTokens: number;
|
|
149
|
+
};
|
|
114
150
|
/**
|
|
115
151
|
* Run-lifetime cache evidence. Cursor exposes only aggregate cache counters at
|
|
116
152
|
* TurnEnded, so retain the inputs and protocol activity needed to explain
|
|
@@ -124,6 +160,8 @@ export type CursorSession = {
|
|
|
124
160
|
modelId?: string;
|
|
125
161
|
priorTokenDetails?: CursorConversationTokenDetails;
|
|
126
162
|
startedWithCheckpoint: boolean;
|
|
163
|
+
/** Why a Run started without a checkpoint: a reset reason, `ephemeral`, or `no-checkpoint`. */
|
|
164
|
+
coldReason?: string;
|
|
127
165
|
requestContextReused: boolean;
|
|
128
166
|
requestContextHash: string;
|
|
129
167
|
systemPromptHash?: string;
|
|
@@ -139,6 +177,8 @@ export type CursorSession = {
|
|
|
139
177
|
};
|
|
140
178
|
/** OpenCode session whose own or descendant activity renews tool leases. */
|
|
141
179
|
openCodeSessionId?: string;
|
|
180
|
+
/** OpenCode session whose Stop cancels this Run, also for a helper isolated from it. */
|
|
181
|
+
stoppedWithSessionId?: string;
|
|
142
182
|
/** Host primary agent whose prompt/permissions this Run was seeded with. */
|
|
143
183
|
hostAgent?: string;
|
|
144
184
|
/** Stable host-system + provider-guidance identity for restart validation. */
|
|
@@ -166,6 +206,12 @@ export type CursorSession = {
|
|
|
166
206
|
* after the host settles emitted tools. Timed-out reads use pendingFrameRead.
|
|
167
207
|
*/
|
|
168
208
|
pushbackFrame?: Frame;
|
|
209
|
+
/**
|
|
210
|
+
* Frames the held-Run reader took while the host ran tools and the next pump
|
|
211
|
+
* must see, in arrival order. They follow `pushbackFrame` and precede any
|
|
212
|
+
* unread frame.
|
|
213
|
+
*/
|
|
214
|
+
heldFrames?: Frame[];
|
|
169
215
|
/**
|
|
170
216
|
* In-progress parallel tool-call step (field 27 hold). Cleared when the step
|
|
171
217
|
* finishes; added to priorParallelStepCallIds for late-completion filtering.
|
|
@@ -255,13 +301,18 @@ export type CursorSession = {
|
|
|
255
301
|
deferredTerminalReason: "remote-clean-close" | "remote-error" | null;
|
|
256
302
|
policy: CursorContinuationPolicy;
|
|
257
303
|
createdAt: number;
|
|
304
|
+
/** When the host's model call that opened this Run started; it may open the Run after a stop. */
|
|
305
|
+
requestedAt?: number;
|
|
258
306
|
lastInboundAt: number;
|
|
259
307
|
lastHeartbeatWriteAt: number;
|
|
260
308
|
semanticDeadlineAt: number;
|
|
261
309
|
closeError: CursorProviderError | null;
|
|
262
310
|
closed: boolean;
|
|
263
|
-
|
|
311
|
+
/** The host stopped the turn (OpenCode 2 interrupt); the Run is being cancelled and must not be continued. */
|
|
312
|
+
hostInterrupted?: string;
|
|
313
|
+
reopenWithUserMessage?: (text: string, signal?: AbortSignal) => Promise<void>;
|
|
264
314
|
};
|
|
315
|
+
export declare function stoppedWith(session: CursorSession): string | undefined;
|
|
265
316
|
type SessionManagerOptions = {
|
|
266
317
|
now?: () => number;
|
|
267
318
|
setTimer?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
|
@@ -272,10 +323,14 @@ type SessionManagerOptions = {
|
|
|
272
323
|
maxOpenSessions?: number;
|
|
273
324
|
};
|
|
274
325
|
export declare class SessionManager {
|
|
326
|
+
/** A claimed result has one writer even while that writer awaits drain. */
|
|
327
|
+
private readonly delivering;
|
|
275
328
|
private byExecId;
|
|
276
329
|
private sessions;
|
|
277
330
|
private byOpenCodeSessionId;
|
|
278
331
|
private tombstones;
|
|
332
|
+
/** Early-row tool call id → the exec that bound it. Bounded like the tombstones. */
|
|
333
|
+
private toolCallRoutes;
|
|
279
334
|
private readonly now;
|
|
280
335
|
private readonly setTimer;
|
|
281
336
|
private readonly clearTimer;
|
|
@@ -288,12 +343,24 @@ export declare class SessionManager {
|
|
|
288
343
|
/** Live open Run for this OpenCode session id, if any. */
|
|
289
344
|
findOpenByOpenCodeSessionId(openCodeSessionId: string | undefined): CursorSession | undefined;
|
|
290
345
|
isActivelyPumping(session: CursorSession): boolean;
|
|
346
|
+
/** Every open Run a Stop of this OpenCode session cancels, including one a newer Run did not supersede. */
|
|
347
|
+
openSessionsStoppedWith(openCodeSessionId: string | undefined): CursorSession[];
|
|
348
|
+
/**
|
|
349
|
+
* Forget every result the host still owed this Run, without writing any: the
|
|
350
|
+
* host stopped the turn, and the Run is about to be cancelled.
|
|
351
|
+
*
|
|
352
|
+
* @returns number of pendings dropped
|
|
353
|
+
*/
|
|
354
|
+
abandonPending(session: CursorSession): number;
|
|
355
|
+
/** Cursor withdrew this exec; returns false when it is not one the host still owes. */
|
|
356
|
+
markExecAborted(session: CursorSession, execId: number): boolean;
|
|
291
357
|
/**
|
|
292
|
-
* Clear
|
|
293
|
-
* Used when the host starts a fresh user turn
|
|
294
|
-
*
|
|
358
|
+
* Clear pendings that do not require a Cursor exec write: display-only bridged
|
|
359
|
+
* ones and execs Cursor aborted. Used when the host starts a fresh user turn
|
|
360
|
+
* instead of returning the result (e.g. human `continue` while a todowrite
|
|
361
|
+
* mirror is outstanding).
|
|
295
362
|
*
|
|
296
|
-
* @returns number of
|
|
363
|
+
* @returns number of pendings settled
|
|
297
364
|
*/
|
|
298
365
|
settleBridgedPending(session: CursorSession): number;
|
|
299
366
|
private isPumping;
|
|
@@ -313,7 +380,9 @@ export declare class SessionManager {
|
|
|
313
380
|
pendingFor(sessionId: string, execId: number): PendingExec | undefined;
|
|
314
381
|
classify(sessionId: string, execId: number): ContinuationClassification;
|
|
315
382
|
claim(sessionId: string, execId: number): ContinuationClaim | ContinuationClassification;
|
|
316
|
-
deliverClaim(claim: ContinuationClaim, frames: readonly Uint8Array[]
|
|
383
|
+
deliverClaim(claim: ContinuationClaim, frames: readonly Uint8Array[],
|
|
384
|
+
/** Called at each actual serialized write, before write() and its drain. */
|
|
385
|
+
beforeWrite?: () => void): Promise<DeliveryOutcome>;
|
|
317
386
|
/** Find the live session awaiting one of the given exec ids. */
|
|
318
387
|
findByExecIds(sessionId: string, execIds: number[]): CursorSession | undefined;
|
|
319
388
|
/** Mark an exec id as resolved (its result has been delivered). */
|
|
@@ -342,6 +411,22 @@ export declare class SessionManager {
|
|
|
342
411
|
dispose(): void;
|
|
343
412
|
sweepHardDeadlines(): void;
|
|
344
413
|
private onStreamTerminal;
|
|
414
|
+
/**
|
|
415
|
+
* The host tool-call id a continuation will send for this exec, when it is
|
|
416
|
+
* not `cursor_<session>_<execId>` (an early row bound to the real exec).
|
|
417
|
+
* The route outlives its Run, like a tombstone, so a result that arrives
|
|
418
|
+
* after the Run closed still names its exec and rebuilds from host history.
|
|
419
|
+
*/
|
|
420
|
+
bindToolCall(toolCallId: string, sessionId: string, execId: number): void;
|
|
421
|
+
toolCallExec(toolCallId: string): {
|
|
422
|
+
sessionId: string;
|
|
423
|
+
execId: number;
|
|
424
|
+
} | undefined;
|
|
425
|
+
/**
|
|
426
|
+
* Leases only grow. Session activity renews from its own time; a running
|
|
427
|
+
* host tool renews from now, but never past that tool's own ceiling. The
|
|
428
|
+
* ceiling limits the tool's extension, not the lease activity already gave.
|
|
429
|
+
*/
|
|
345
430
|
private refreshHardDeadline;
|
|
346
431
|
private hardDeadlineExpired;
|
|
347
432
|
private scheduleHardDeadline;
|
package/dist/session.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { trace } from "./debug.js";
|
|
2
|
+
import { releaseUnstartedTerminal } from "./terminal-runner.js";
|
|
2
3
|
import { CursorProtocolError } from "./errors.js";
|
|
3
|
-
import {
|
|
4
|
+
import { StreamWriteDrainError, writeStreamMessage } from "./stream-write.js";
|
|
5
|
+
import { RUNNING_TOOL_CAP_MS, sessionActivity } from "./activity.js";
|
|
4
6
|
export const DEFAULT_CONTINUATION_POLICY = {
|
|
5
|
-
|
|
7
|
+
// Cursor CLI aborts an active attempt after 30 s without an inbound frame.
|
|
8
|
+
semanticIdleMs: 30_000,
|
|
6
9
|
hardCapMs: 600_000,
|
|
7
10
|
heartbeatMs: 5_000,
|
|
8
11
|
};
|
|
@@ -33,13 +36,15 @@ export function resolveContinuationPolicy(options) {
|
|
|
33
36
|
throw new CursorProtocolError(`Unknown Cursor continuation option: ${key}`);
|
|
34
37
|
}
|
|
35
38
|
}
|
|
39
|
+
// Read the deprecated alias once, without the deprecated typed accessor.
|
|
40
|
+
const legacySemanticIdleMs = options?.softHealthMs;
|
|
36
41
|
if (options?.semanticIdleMs !== undefined &&
|
|
37
|
-
|
|
38
|
-
options.semanticIdleMs !==
|
|
42
|
+
legacySemanticIdleMs !== undefined &&
|
|
43
|
+
options.semanticIdleMs !== legacySemanticIdleMs) {
|
|
39
44
|
throw new CursorProtocolError("Cursor continuation semanticIdleMs and deprecated softHealthMs must match when both are set");
|
|
40
45
|
}
|
|
41
46
|
const heartbeatMs = positiveInteger("heartbeatMs", options?.heartbeatMs, DEFAULT_CONTINUATION_POLICY.heartbeatMs);
|
|
42
|
-
const semanticIdleMs = positiveInteger("semanticIdleMs", options?.semanticIdleMs ??
|
|
47
|
+
const semanticIdleMs = positiveInteger("semanticIdleMs", options?.semanticIdleMs ?? legacySemanticIdleMs, DEFAULT_CONTINUATION_POLICY.semanticIdleMs);
|
|
43
48
|
const hardCapMs = positiveInteger("hardCapMs", options?.hardCapMs, DEFAULT_CONTINUATION_POLICY.hardCapMs);
|
|
44
49
|
if (heartbeatMs >= semanticIdleMs) {
|
|
45
50
|
throw new CursorProtocolError("Cursor continuation heartbeatMs must be less than semanticIdleMs");
|
|
@@ -49,7 +54,12 @@ export function resolveContinuationPolicy(options) {
|
|
|
49
54
|
}
|
|
50
55
|
return { heartbeatMs, semanticIdleMs, hardCapMs };
|
|
51
56
|
}
|
|
57
|
+
export function stoppedWith(session) {
|
|
58
|
+
return session.stoppedWithSessionId ?? session.openCodeSessionId;
|
|
59
|
+
}
|
|
52
60
|
export class SessionManager {
|
|
61
|
+
/** A claimed result has one writer even while that writer awaits drain. */
|
|
62
|
+
delivering = new WeakSet();
|
|
53
63
|
// Composite key `${sessionId}:${execId}` → owning session. Composite keying
|
|
54
64
|
// means two Run streams that both register an execId of 1 (Cursor resets
|
|
55
65
|
// counters per stream) coexist instead of overwriting each other.
|
|
@@ -60,6 +70,8 @@ export class SessionManager {
|
|
|
60
70
|
// of continuing the held-open one — the prior entry is stale and superseded.
|
|
61
71
|
byOpenCodeSessionId = new Map();
|
|
62
72
|
tombstones = new Map();
|
|
73
|
+
/** Early-row tool call id → the exec that bound it. Bounded like the tombstones. */
|
|
74
|
+
toolCallRoutes = new Map();
|
|
63
75
|
now;
|
|
64
76
|
setTimer;
|
|
65
77
|
clearTimer;
|
|
@@ -146,19 +158,54 @@ export class SessionManager {
|
|
|
146
158
|
isActivelyPumping(session) {
|
|
147
159
|
return this.isPumping(session);
|
|
148
160
|
}
|
|
161
|
+
/** Every open Run a Stop of this OpenCode session cancels, including one a newer Run did not supersede. */
|
|
162
|
+
openSessionsStoppedWith(openCodeSessionId) {
|
|
163
|
+
return [...this.sessions].filter((session) => !session.closed && (openCodeSessionId === undefined || stoppedWith(session) === openCodeSessionId));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Forget every result the host still owed this Run, without writing any: the
|
|
167
|
+
* host stopped the turn, and the Run is about to be cancelled.
|
|
168
|
+
*
|
|
169
|
+
* @returns number of pendings dropped
|
|
170
|
+
*/
|
|
171
|
+
abandonPending(session) {
|
|
172
|
+
if (session.closed)
|
|
173
|
+
return 0;
|
|
174
|
+
const dropped = session.pending.size;
|
|
175
|
+
for (const [execId, pending] of session.pending) {
|
|
176
|
+
const directory = pending.resultMetadata?.managed_job_directory;
|
|
177
|
+
if (typeof directory === "string")
|
|
178
|
+
releaseUnstartedTerminal(directory);
|
|
179
|
+
const key = this.key(session.sessionId, execId);
|
|
180
|
+
this.byExecId.delete(key);
|
|
181
|
+
this.putTombstone(key, "host-interrupted");
|
|
182
|
+
}
|
|
183
|
+
session.pending.clear();
|
|
184
|
+
this.scheduleHardDeadline(session);
|
|
185
|
+
return dropped;
|
|
186
|
+
}
|
|
187
|
+
/** Cursor withdrew this exec; returns false when it is not one the host still owes. */
|
|
188
|
+
markExecAborted(session, execId) {
|
|
189
|
+
const pending = session.pending.get(execId);
|
|
190
|
+
if (session.closed || !pending || pending.bridged || pending.aborted || pending.state !== "pending")
|
|
191
|
+
return false;
|
|
192
|
+
pending.aborted = true;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
149
195
|
/**
|
|
150
|
-
* Clear
|
|
151
|
-
* Used when the host starts a fresh user turn
|
|
152
|
-
*
|
|
196
|
+
* Clear pendings that do not require a Cursor exec write: display-only bridged
|
|
197
|
+
* ones and execs Cursor aborted. Used when the host starts a fresh user turn
|
|
198
|
+
* instead of returning the result (e.g. human `continue` while a todowrite
|
|
199
|
+
* mirror is outstanding).
|
|
153
200
|
*
|
|
154
|
-
* @returns number of
|
|
201
|
+
* @returns number of pendings settled
|
|
155
202
|
*/
|
|
156
203
|
settleBridgedPending(session) {
|
|
157
204
|
if (session.closed)
|
|
158
205
|
return 0;
|
|
159
206
|
let settled = 0;
|
|
160
207
|
for (const [execId, pending] of [...session.pending.entries()]) {
|
|
161
|
-
if (!pending.bridged)
|
|
208
|
+
if (!pending.bridged && !pending.aborted)
|
|
162
209
|
continue;
|
|
163
210
|
if (pending.state === "claimed")
|
|
164
211
|
continue;
|
|
@@ -192,8 +239,9 @@ export class SessionManager {
|
|
|
192
239
|
if (candidate === justRegistered || this.isPumping(candidate))
|
|
193
240
|
continue;
|
|
194
241
|
// Held-open Runs with pending tools must not be cap-evicted — that is
|
|
195
|
-
// mid-exec supersede by another name and throws away the prefix.
|
|
196
|
-
|
|
242
|
+
// mid-exec supersede by another name and throws away the prefix. An
|
|
243
|
+
// exec Cursor aborted no longer waits on the host, so it does not hold.
|
|
244
|
+
if ([...candidate.pending.values()].some((pending) => !pending.aborted))
|
|
197
245
|
continue;
|
|
198
246
|
if (!oldest || candidate.createdAt < oldest.createdAt)
|
|
199
247
|
oldest = candidate;
|
|
@@ -291,20 +339,25 @@ export class SessionManager {
|
|
|
291
339
|
pending: classification.pending,
|
|
292
340
|
};
|
|
293
341
|
}
|
|
294
|
-
deliverClaim(claim, frames
|
|
342
|
+
async deliverClaim(claim, frames,
|
|
343
|
+
/** Called at each actual serialized write, before write() and its drain. */
|
|
344
|
+
beforeWrite) {
|
|
295
345
|
const { session, execId, pending } = claim;
|
|
296
346
|
const key = this.key(session.sessionId, execId);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
347
|
+
const unavailable = (framesWritten) => {
|
|
348
|
+
if (!session.closed &&
|
|
349
|
+
this.byExecId.get(key) === session &&
|
|
350
|
+
session.pending.get(execId) === pending)
|
|
351
|
+
return undefined;
|
|
300
352
|
const current = this.classify(session.sessionId, execId);
|
|
301
|
-
if (current.kind === "duplicate")
|
|
302
|
-
return { ...current, framesWritten
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
353
|
+
if (current.kind === "duplicate" || current.kind === "terminal")
|
|
354
|
+
return { ...current, framesWritten };
|
|
355
|
+
return { kind: "missing", reason: "missing-process-local-state", framesWritten };
|
|
356
|
+
};
|
|
357
|
+
const invalid = unavailable(0);
|
|
358
|
+
if (invalid)
|
|
359
|
+
return invalid;
|
|
360
|
+
if (pending.state !== "claimed" || this.delivering.has(pending)) {
|
|
308
361
|
return {
|
|
309
362
|
kind: "duplicate",
|
|
310
363
|
reason: pending.state === "delivered" ? "delivered" : "in-flight",
|
|
@@ -315,17 +368,37 @@ export class SessionManager {
|
|
|
315
368
|
this.close(session, "hard-cap-expired");
|
|
316
369
|
return { kind: "terminal", reason: "hard-cap-expired", framesWritten: 0 };
|
|
317
370
|
}
|
|
371
|
+
this.delivering.add(pending);
|
|
318
372
|
let framesWritten = 0;
|
|
319
373
|
try {
|
|
320
|
-
if (!pending.bridged && frames.length === 0) {
|
|
374
|
+
if (!pending.bridged && !pending.aborted && frames.length === 0) {
|
|
321
375
|
throw new CursorProtocolError("No result frames were produced");
|
|
322
376
|
}
|
|
323
377
|
for (const frame of frames) {
|
|
324
|
-
|
|
378
|
+
try {
|
|
379
|
+
await writeStreamMessage(session.stream, frame, "continuation result", beforeWrite ? () => {
|
|
380
|
+
beforeWrite();
|
|
381
|
+
return true;
|
|
382
|
+
} : undefined);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
// write() returned false before the drain failed: the bytes are buffered.
|
|
386
|
+
if (error instanceof StreamWriteDrainError)
|
|
387
|
+
framesWritten++;
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
325
390
|
framesWritten++;
|
|
391
|
+
// The Run can close or replace this pending exec while drain is awaited.
|
|
392
|
+
// Never send its remaining frames or apply approval/state to that owner.
|
|
393
|
+
const invalid = unavailable(framesWritten);
|
|
394
|
+
if (invalid)
|
|
395
|
+
return invalid;
|
|
326
396
|
}
|
|
327
397
|
}
|
|
328
398
|
catch {
|
|
399
|
+
const invalid = unavailable(framesWritten);
|
|
400
|
+
if (invalid)
|
|
401
|
+
return invalid;
|
|
329
402
|
const reason = framesWritten === 0 ? "result-write-failed" : "ambiguous-partial-write";
|
|
330
403
|
this.close(session, reason);
|
|
331
404
|
return { kind: "terminal", reason, framesWritten };
|
|
@@ -377,7 +450,8 @@ export class SessionManager {
|
|
|
377
450
|
session.pumpActive = false;
|
|
378
451
|
const deferred = session.deferredTerminalReason;
|
|
379
452
|
session.deferredTerminalReason = null;
|
|
380
|
-
|
|
453
|
+
// A Run the host stopped is closed by its cancel, after it has read what Cursor sent last.
|
|
454
|
+
if (deferred && !session.hostInterrupted)
|
|
381
455
|
this.close(session, deferred);
|
|
382
456
|
return true;
|
|
383
457
|
}
|
|
@@ -395,11 +469,15 @@ export class SessionManager {
|
|
|
395
469
|
session.deferredTerminalReason = null;
|
|
396
470
|
const old = session.stream;
|
|
397
471
|
session.stream = next;
|
|
472
|
+
// Preserve session totals but give the replacement Run its own estimates.
|
|
473
|
+
session.billing.estimatedUsd = 0;
|
|
474
|
+
delete session.billing.ledgerEpoch;
|
|
398
475
|
if (runId !== undefined)
|
|
399
476
|
session.runId = runId;
|
|
400
477
|
session.frames = next.frames()[Symbol.asyncIterator]();
|
|
401
478
|
session.pendingFrameRead = undefined;
|
|
402
479
|
session.pushbackFrame = undefined;
|
|
480
|
+
session.heldFrames = undefined;
|
|
403
481
|
session.parallelStep = undefined;
|
|
404
482
|
session.priorParallelStepCallIds = undefined;
|
|
405
483
|
this.subscribeTerminal(session);
|
|
@@ -443,7 +521,10 @@ export class SessionManager {
|
|
|
443
521
|
session.terminalUnsubscribe?.();
|
|
444
522
|
session.terminalUnsubscribe = null;
|
|
445
523
|
session.deferredTerminalReason = null;
|
|
446
|
-
for (const id of session.pending
|
|
524
|
+
for (const [id, pending] of session.pending) {
|
|
525
|
+
const directory = pending.resultMetadata?.managed_job_directory;
|
|
526
|
+
if (typeof directory === "string")
|
|
527
|
+
releaseUnstartedTerminal(directory);
|
|
447
528
|
const key = this.key(session.sessionId, id);
|
|
448
529
|
this.byExecId.delete(key);
|
|
449
530
|
if (this.isTerminalReason(reason))
|
|
@@ -452,6 +533,7 @@ export class SessionManager {
|
|
|
452
533
|
session.pending.clear();
|
|
453
534
|
session.pendingFrameRead = undefined;
|
|
454
535
|
session.pushbackFrame = undefined;
|
|
536
|
+
session.heldFrames = undefined;
|
|
455
537
|
session.parallelStep = undefined;
|
|
456
538
|
session.priorParallelStepCallIds = undefined;
|
|
457
539
|
session.pumpOwner = null;
|
|
@@ -492,6 +574,7 @@ export class SessionManager {
|
|
|
492
574
|
for (const session of [...this.sessions])
|
|
493
575
|
this.close(session, "process-disposed");
|
|
494
576
|
this.byExecId.clear();
|
|
577
|
+
this.toolCallRoutes.clear();
|
|
495
578
|
}
|
|
496
579
|
sweepHardDeadlines() {
|
|
497
580
|
for (const session of [...this.sessions]) {
|
|
@@ -507,22 +590,55 @@ export class SessionManager {
|
|
|
507
590
|
const reason = event.kind === "remote-error" ? "remote-error" : "remote-clean-close";
|
|
508
591
|
if (event.kind === "remote-error")
|
|
509
592
|
session.closeError = event.error;
|
|
510
|
-
if (session.pumpOwner !== null) {
|
|
593
|
+
if (session.pumpOwner !== null || session.hostInterrupted) {
|
|
511
594
|
session.deferredTerminalReason = reason;
|
|
512
595
|
return;
|
|
513
596
|
}
|
|
514
597
|
this.close(session, reason, event.kind === "remote-error" ? event.error : undefined);
|
|
515
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* The host tool-call id a continuation will send for this exec, when it is
|
|
601
|
+
* not `cursor_<session>_<execId>` (an early row bound to the real exec).
|
|
602
|
+
* The route outlives its Run, like a tombstone, so a result that arrives
|
|
603
|
+
* after the Run closed still names its exec and rebuilds from host history.
|
|
604
|
+
*/
|
|
605
|
+
bindToolCall(toolCallId, sessionId, execId) {
|
|
606
|
+
this.toolCallRoutes.delete(toolCallId);
|
|
607
|
+
this.toolCallRoutes.set(toolCallId, { sessionId, execId });
|
|
608
|
+
while (this.toolCallRoutes.size > this.tombstoneLimit) {
|
|
609
|
+
const oldest = this.toolCallRoutes.keys().next().value;
|
|
610
|
+
if (oldest === undefined)
|
|
611
|
+
break;
|
|
612
|
+
this.toolCallRoutes.delete(oldest);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
toolCallExec(toolCallId) {
|
|
616
|
+
return this.toolCallRoutes.get(toolCallId);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Leases only grow. Session activity renews from its own time; a running
|
|
620
|
+
* host tool renews from now, but never past that tool's own ceiling. The
|
|
621
|
+
* ceiling limits the tool's extension, not the lease activity already gave.
|
|
622
|
+
*/
|
|
516
623
|
refreshHardDeadline(session, pending) {
|
|
517
624
|
if (!session.openCodeSessionId)
|
|
518
625
|
return pending.hardDeadlineAt;
|
|
626
|
+
const now = this.now();
|
|
627
|
+
const toolCapAt = this.activitySource.runningToolDeadline?.(session.openCodeSessionId, now, RUNNING_TOOL_CAP_MS);
|
|
519
628
|
const activityAt = this.activitySource.lastActivityAt(session.openCodeSessionId);
|
|
520
|
-
if (activityAt
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
629
|
+
if (activityAt !== undefined && activityAt > pending.registeredAt) {
|
|
630
|
+
const renewedDeadline = activityAt + session.policy.hardCapMs;
|
|
631
|
+
if (renewedDeadline > pending.hardDeadlineAt) {
|
|
632
|
+
pending.hardDeadlineAt = renewedDeadline;
|
|
633
|
+
trace("continuation lease renewed from OpenCode session activity");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (toolCapAt !== undefined) {
|
|
637
|
+
const renewedDeadline = Math.min(now + session.policy.hardCapMs, toolCapAt);
|
|
638
|
+
if (renewedDeadline > pending.hardDeadlineAt) {
|
|
639
|
+
pending.hardDeadlineAt = renewedDeadline;
|
|
640
|
+
trace("continuation lease renewed while a host tool is running");
|
|
641
|
+
}
|
|
526
642
|
}
|
|
527
643
|
return pending.hardDeadlineAt;
|
|
528
644
|
}
|
|
@@ -586,6 +702,9 @@ export async function readSessionFrame(session, timeoutMs) {
|
|
|
586
702
|
session.pushbackFrame = undefined;
|
|
587
703
|
return { done: false, value };
|
|
588
704
|
}
|
|
705
|
+
const held = session.heldFrames?.shift();
|
|
706
|
+
if (held)
|
|
707
|
+
return { done: false, value: held };
|
|
589
708
|
const pending = session.pendingFrameRead ??= session.frames.next();
|
|
590
709
|
// A timed-out read can reject while no pump is consuming it. Preserve the
|
|
591
710
|
// rejection for the next reader without an unhandled detached promise.
|
package/dist/shared.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableMo
|
|
|
21
21
|
export declare const SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig";
|
|
22
22
|
export declare const MODEL_CACHE_FILE = "cursor-models.json";
|
|
23
23
|
/** Bumped when the on-disk model cache shape/semantics change (forces refetch). */
|
|
24
|
-
export declare const MODEL_CACHE_SCHEMA_VERSION =
|
|
24
|
+
export declare const MODEL_CACHE_SCHEMA_VERSION = 6;
|
|
25
25
|
export declare const MODEL_CACHE_TTL_MS = 86400000;
|
|
26
26
|
export declare const CONVERSATION_CACHE_DIR = "cursor-conversations";
|
|
27
27
|
export declare const CONVERSATION_CACHE_SCHEMA_VERSION = 4;
|
package/dist/shared.js
CHANGED
|
@@ -21,7 +21,7 @@ export const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
|
|
|
21
21
|
export const SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig";
|
|
22
22
|
export const MODEL_CACHE_FILE = "cursor-models.json";
|
|
23
23
|
/** Bumped when the on-disk model cache shape/semantics change (forces refetch). */
|
|
24
|
-
export const MODEL_CACHE_SCHEMA_VERSION =
|
|
24
|
+
export const MODEL_CACHE_SCHEMA_VERSION = 6;
|
|
25
25
|
export const MODEL_CACHE_TTL_MS = 86_400_000;
|
|
26
26
|
export const CONVERSATION_CACHE_DIR = "cursor-conversations";
|
|
27
27
|
export const CONVERSATION_CACHE_SCHEMA_VERSION = 4;
|