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
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { LanguageModelV3CallOptions } from "@ai-sdk/provider";
|
|
2
2
|
/** Mirrors MAX_TURN_STATE_SESSIONS; evicted sessions re-hydrate from disk. */
|
|
3
3
|
export declare const MAX_PROVENANCE_SESSIONS = 256;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/** Tool call ids emitted in the latest non-empty step. */
|
|
4
|
+
type EmittedStep = {
|
|
5
|
+
/** Tool call ids emitted in the step. */
|
|
7
6
|
toolCallIds: string[];
|
|
8
|
-
/** Whitespace-free text emitted in the
|
|
7
|
+
/** Whitespace-free text emitted in the step (first 4 KiB). */
|
|
9
8
|
text: string;
|
|
10
9
|
/**
|
|
11
10
|
* Whitespace-free reasoning and text of the same step in emission order
|
|
@@ -15,6 +14,17 @@ export type TurnProvenance = {
|
|
|
15
14
|
*/
|
|
16
15
|
textWithReasoning: string;
|
|
17
16
|
};
|
|
17
|
+
/** The latest non-empty step, as fields of the record itself. */
|
|
18
|
+
export type TurnProvenance = EmittedStep & {
|
|
19
|
+
conversationId: string;
|
|
20
|
+
/**
|
|
21
|
+
* The latest step has not finished: it is still streaming, or the host
|
|
22
|
+
* stopped it. The host may keep only what it read of it, or drop it.
|
|
23
|
+
*/
|
|
24
|
+
unfinished?: boolean;
|
|
25
|
+
/** The step before an unfinished one, which the host's history may still end on. */
|
|
26
|
+
previous?: EmittedStep;
|
|
27
|
+
};
|
|
18
28
|
export type ForeignHistoryReason = "foreign-assistant";
|
|
19
29
|
/**
|
|
20
30
|
* Mark the start of one host step (one doStream). The previous step stays
|
|
@@ -28,6 +38,10 @@ export declare function recordEmittedPart(sessionKey: string, conversationId: st
|
|
|
28
38
|
delta?: unknown;
|
|
29
39
|
toolCallId?: unknown;
|
|
30
40
|
}): void;
|
|
41
|
+
/** The host step's stream finished normally; an empty step leaves the previous one recorded. */
|
|
42
|
+
export declare function finishEmittedStep(sessionKey: string, conversationId: string): void;
|
|
43
|
+
/** The finished step goes on in the same host step (a progress-only continuation Run). */
|
|
44
|
+
export declare function resumeEmittedStep(sessionKey: string, conversationId: string): void;
|
|
31
45
|
/**
|
|
32
46
|
* Bind provenance to the conversation a Run was opened on. A reminted
|
|
33
47
|
* conversation starts with an empty record, so it never inherits the previous
|
|
@@ -50,3 +64,4 @@ export declare function detectForeignHistory(input: {
|
|
|
50
64
|
conversationId: string;
|
|
51
65
|
prompt: LanguageModelV3CallOptions["prompt"];
|
|
52
66
|
}): ForeignHistoryReason | undefined;
|
|
67
|
+
export {};
|
|
@@ -37,10 +37,18 @@ function entryFor(sessionKey, conversationId) {
|
|
|
37
37
|
function appendCapped(value, delta) {
|
|
38
38
|
return value.length < MAX_TEXT_CHARS ? (value + delta).slice(0, MAX_TEXT_CHARS) : value;
|
|
39
39
|
}
|
|
40
|
+
function hasContent(step) {
|
|
41
|
+
return step.toolCallIds.length > 0 || !!step.text || !!step.textWithReasoning;
|
|
42
|
+
}
|
|
43
|
+
function copyStep(step) {
|
|
44
|
+
return { toolCallIds: [...step.toolCallIds], text: step.text, textWithReasoning: step.textWithReasoning };
|
|
45
|
+
}
|
|
40
46
|
function contentEntry(sessionKey, conversationId) {
|
|
41
47
|
const entry = entryFor(sessionKey, conversationId);
|
|
42
48
|
if (entry.stepPending) {
|
|
49
|
+
entry.previous = hasContent(entry) ? copyStep(entry) : undefined;
|
|
43
50
|
entry.stepPending = false;
|
|
51
|
+
entry.unfinished = true;
|
|
44
52
|
entry.toolCallIds = [];
|
|
45
53
|
entry.text = "";
|
|
46
54
|
entry.textWithReasoning = "";
|
|
@@ -73,6 +81,21 @@ export function recordEmittedPart(sessionKey, conversationId, part) {
|
|
|
73
81
|
entry.toolCallIds.push(part.toolCallId);
|
|
74
82
|
}
|
|
75
83
|
}
|
|
84
|
+
/** The host step's stream finished normally; an empty step leaves the previous one recorded. */
|
|
85
|
+
export function finishEmittedStep(sessionKey, conversationId) {
|
|
86
|
+
const entry = provenanceBySession.get(sessionKey);
|
|
87
|
+
if (!entry || entry.conversationId !== conversationId || entry.stepPending)
|
|
88
|
+
return;
|
|
89
|
+
entry.unfinished = false;
|
|
90
|
+
entry.previous = undefined;
|
|
91
|
+
}
|
|
92
|
+
/** The finished step goes on in the same host step (a progress-only continuation Run). */
|
|
93
|
+
export function resumeEmittedStep(sessionKey, conversationId) {
|
|
94
|
+
const entry = provenanceBySession.get(sessionKey);
|
|
95
|
+
if (!entry || entry.conversationId !== conversationId || entry.stepPending)
|
|
96
|
+
return;
|
|
97
|
+
entry.unfinished = true;
|
|
98
|
+
}
|
|
76
99
|
/**
|
|
77
100
|
* Bind provenance to the conversation a Run was opened on. A reminted
|
|
78
101
|
* conversation starts with an empty record, so it never inherits the previous
|
|
@@ -87,13 +110,19 @@ export function getTurnProvenance(sessionKey) {
|
|
|
87
110
|
return undefined;
|
|
88
111
|
return {
|
|
89
112
|
conversationId: entry.conversationId,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
...copyStep(entry),
|
|
114
|
+
...(entry.unfinished ? { unfinished: true } : {}),
|
|
115
|
+
...(entry.unfinished && entry.previous ? { previous: copyStep(entry.previous) } : {}),
|
|
93
116
|
};
|
|
94
117
|
}
|
|
95
118
|
export function restoreTurnProvenance(sessionKey, value) {
|
|
96
|
-
touch(sessionKey, {
|
|
119
|
+
touch(sessionKey, {
|
|
120
|
+
conversationId: value.conversationId,
|
|
121
|
+
...copyStep(value),
|
|
122
|
+
...(value.unfinished ? { unfinished: true } : {}),
|
|
123
|
+
...(value.unfinished && value.previous ? { previous: copyStep(value.previous) } : {}),
|
|
124
|
+
stepPending: false,
|
|
125
|
+
});
|
|
97
126
|
}
|
|
98
127
|
export function resetTurnProvenanceForTests() {
|
|
99
128
|
provenanceBySession.clear();
|
|
@@ -101,20 +130,31 @@ export function resetTurnProvenanceForTests() {
|
|
|
101
130
|
export function serializeTurnProvenance(value) {
|
|
102
131
|
return JSON.stringify(value);
|
|
103
132
|
}
|
|
133
|
+
function parseStep(value) {
|
|
134
|
+
return {
|
|
135
|
+
toolCallIds: Array.isArray(value.toolCallIds)
|
|
136
|
+
? value.toolCallIds.filter((id) => typeof id === "string").slice(0, MAX_TOOL_CALL_IDS)
|
|
137
|
+
: [],
|
|
138
|
+
text: typeof value.text === "string" ? value.text.slice(0, MAX_TEXT_CHARS) : "",
|
|
139
|
+
textWithReasoning: typeof value.textWithReasoning === "string"
|
|
140
|
+
? value.textWithReasoning.slice(0, MAX_TEXT_CHARS)
|
|
141
|
+
: "",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
104
144
|
export function parseTurnProvenance(raw) {
|
|
105
145
|
try {
|
|
106
146
|
const value = JSON.parse(raw);
|
|
107
147
|
if (typeof value.conversationId !== "string" || !value.conversationId)
|
|
108
148
|
return undefined;
|
|
149
|
+
const unfinished = value.unfinished === true;
|
|
150
|
+
const previous = unfinished && value.previous && typeof value.previous === "object"
|
|
151
|
+
? parseStep(value.previous)
|
|
152
|
+
: undefined;
|
|
109
153
|
return {
|
|
110
154
|
conversationId: value.conversationId,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
text: typeof value.text === "string" ? value.text.slice(0, MAX_TEXT_CHARS) : "",
|
|
115
|
-
textWithReasoning: typeof value.textWithReasoning === "string"
|
|
116
|
-
? value.textWithReasoning.slice(0, MAX_TEXT_CHARS)
|
|
117
|
-
: "",
|
|
155
|
+
...parseStep(value),
|
|
156
|
+
...(unfinished ? { unfinished } : {}),
|
|
157
|
+
...(previous ? { previous } : {}),
|
|
118
158
|
};
|
|
119
159
|
}
|
|
120
160
|
catch {
|
|
@@ -158,17 +198,34 @@ export function detectForeignHistory(input) {
|
|
|
158
198
|
// A turn carrying only typed reasoning gives no evidence either way.
|
|
159
199
|
if (toolCallIds.length === 0 && !text)
|
|
160
200
|
return undefined;
|
|
161
|
-
|
|
162
|
-
return undefined;
|
|
201
|
+
const host = { toolCallIds, text: text.slice(0, MAX_TEXT_CHARS), textWithReasoning: textWithReasoning.slice(0, MAX_TEXT_CHARS) };
|
|
163
202
|
// Compare against the latest step only: a foreign "Done." must not match an
|
|
164
|
-
// older Cursor turn that happened to contain the same words.
|
|
165
|
-
|
|
166
|
-
// model switch OpenCode 1.x replays that reasoning as text parts in place,
|
|
167
|
-
// so the turn matches our reasoning and text in emission order instead.
|
|
168
|
-
if (entry.text && text.slice(0, MAX_TEXT_CHARS) === entry.text)
|
|
169
|
-
return undefined;
|
|
170
|
-
if (entry.textWithReasoning && textWithReasoning.slice(0, MAX_TEXT_CHARS) === entry.textWithReasoning) {
|
|
203
|
+
// older Cursor turn that happened to contain the same words.
|
|
204
|
+
if (sameStep(entry, host))
|
|
171
205
|
return undefined;
|
|
206
|
+
if (entry.unfinished) {
|
|
207
|
+
// A stopped step: the host drops it when it holds nothing, so its history
|
|
208
|
+
// still ends on the step before. Otherwise it keeps what it had read,
|
|
209
|
+
// which can miss the last parts this provider emitted.
|
|
210
|
+
if (entry.previous && sameStep(entry.previous, host))
|
|
211
|
+
return undefined;
|
|
212
|
+
// A tool call this provider did not emit makes the turn someone else's.
|
|
213
|
+
if (host.toolCallIds.length === 0) {
|
|
214
|
+
if (host.text && entry.text.startsWith(host.text))
|
|
215
|
+
return undefined;
|
|
216
|
+
if (host.textWithReasoning && entry.textWithReasoning.startsWith(host.textWithReasoning))
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
172
219
|
}
|
|
173
220
|
return "foreign-assistant";
|
|
174
221
|
}
|
|
222
|
+
function sameStep(step, host) {
|
|
223
|
+
if (host.toolCallIds.some((id) => step.toolCallIds.includes(id)))
|
|
224
|
+
return true;
|
|
225
|
+
// Same model: the host keeps our reasoning typed, so its text parts match our
|
|
226
|
+
// text. After a model switch OpenCode 1.x replays that reasoning as text
|
|
227
|
+
// parts in place, so the turn matches our reasoning and text in emission order.
|
|
228
|
+
if (step.text && host.text === step.text)
|
|
229
|
+
return true;
|
|
230
|
+
return !!step.textWithReasoning && host.textWithReasoning === step.textWithReasoning;
|
|
231
|
+
}
|
package/dist/replay-safety.d.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import type { CursorProviderError } from "./errors.js";
|
|
2
|
-
export type ReplayBarrierReason = "visible-text" | "visible-reasoning" | "display-tool-lifecycle" | "non-control-exec" | "stateful-interaction" | "unknown-or-malformed-frame";
|
|
2
|
+
export type ReplayBarrierReason = "visible-text" | "visible-reasoning" | "visible-tool-row" | "display-tool-lifecycle" | "non-control-exec" | "stateful-interaction" | "unknown-or-malformed-frame";
|
|
3
|
+
/** Plain Run-owned state retained across host tool-step pump calls. */
|
|
4
|
+
export type ReplaySafetyState = {
|
|
5
|
+
statefulBarrier?: ReplayBarrierReason;
|
|
6
|
+
/** Every exec needs its owner's host result; ids can restart on a resumed Run. */
|
|
7
|
+
execs?: Array<{
|
|
8
|
+
sessionId: string;
|
|
9
|
+
id: number;
|
|
10
|
+
}>;
|
|
11
|
+
/** History cannot represent opaque interactions, unknown frames or started display work. */
|
|
12
|
+
historyRebaseUnsafe?: boolean;
|
|
13
|
+
};
|
|
3
14
|
export declare class AttemptReplaySafety {
|
|
4
15
|
private readonly sessionId;
|
|
16
|
+
private readonly state;
|
|
5
17
|
private barrierReason;
|
|
6
|
-
|
|
18
|
+
private stateful;
|
|
19
|
+
constructor(sessionId: string, state?: ReplaySafetyState);
|
|
7
20
|
markBarrier(reason: ReplayBarrierReason): void;
|
|
21
|
+
recordExec(id: unknown): void;
|
|
8
22
|
applyTo(failure: CursorProviderError): CursorProviderError;
|
|
9
23
|
}
|
|
10
24
|
export type DecodedReplayFrame = {
|
|
@@ -13,10 +27,8 @@ export type DecodedReplayFrame = {
|
|
|
13
27
|
kv?: Record<string, unknown>;
|
|
14
28
|
execControl?: Record<string, unknown>;
|
|
15
29
|
interactionQuery?: Record<string, unknown>;
|
|
16
|
-
checkpointBytes?: Uint8Array;
|
|
17
30
|
};
|
|
18
31
|
export type ReplayFrameAnalysis = {
|
|
19
|
-
semanticProgress: boolean;
|
|
20
32
|
barrier?: ReplayBarrierReason;
|
|
21
33
|
};
|
|
22
34
|
/**
|
package/dist/replay-safety.js
CHANGED
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
import { trace } from "./debug.js";
|
|
2
2
|
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
3
|
+
/** Host output without execution authority: sending the turn again repeats no side effect. */
|
|
4
|
+
const OUTPUT_ONLY_BARRIERS = new Set([
|
|
5
|
+
"visible-text", "visible-reasoning", "visible-tool-row",
|
|
6
|
+
]);
|
|
3
7
|
export class AttemptReplaySafety {
|
|
4
8
|
sessionId;
|
|
9
|
+
state;
|
|
5
10
|
barrierReason;
|
|
6
|
-
|
|
11
|
+
stateful = false;
|
|
12
|
+
constructor(sessionId, state = {}) {
|
|
7
13
|
this.sessionId = sessionId;
|
|
14
|
+
this.state = state;
|
|
15
|
+
this.stateful = state.statefulBarrier !== undefined;
|
|
8
16
|
}
|
|
9
17
|
markBarrier(reason) {
|
|
18
|
+
if (!OUTPUT_ONLY_BARRIERS.has(reason)) {
|
|
19
|
+
this.stateful = true;
|
|
20
|
+
this.state.statefulBarrier ??= reason;
|
|
21
|
+
if (reason !== "non-control-exec")
|
|
22
|
+
this.state.historyRebaseUnsafe = true;
|
|
23
|
+
}
|
|
10
24
|
if (this.barrierReason)
|
|
11
25
|
return;
|
|
12
26
|
this.barrierReason = reason;
|
|
13
27
|
trace(`replay barrier: reason=${reason} sessionId=${this.sessionId}`);
|
|
14
28
|
}
|
|
29
|
+
recordExec(id) {
|
|
30
|
+
const execs = this.state.execs ??= [];
|
|
31
|
+
if (typeof id !== "number" || !Number.isSafeInteger(id)
|
|
32
|
+
|| execs.some(exec => exec.sessionId === this.sessionId && exec.id === id)) {
|
|
33
|
+
this.state.historyRebaseUnsafe = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
execs.push({ sessionId: this.sessionId, id });
|
|
37
|
+
}
|
|
15
38
|
applyTo(failure) {
|
|
16
|
-
failure.replaySafe = this.barrierReason === undefined && failure.replaySafe;
|
|
39
|
+
failure.replaySafe = this.barrierReason === undefined && !this.stateful && failure.replaySafe;
|
|
17
40
|
if (this.barrierReason) {
|
|
18
|
-
|
|
41
|
+
failure.replayBarrier = this.stateful ? "stateful" : "output-only";
|
|
42
|
+
trace(`replay suppressed: reason=${this.barrierReason} barrier=${failure.replayBarrier} ` +
|
|
43
|
+
`sessionId=${this.sessionId}`);
|
|
19
44
|
}
|
|
20
45
|
return failure;
|
|
21
46
|
}
|
|
@@ -25,9 +50,11 @@ export class AttemptReplaySafety {
|
|
|
25
50
|
// `InteractionUpdate` oneof members this client recognizes. Besides the
|
|
26
51
|
// declared ones: `thinking_completed` #5 (duration), `token_delta` #8 (count),
|
|
27
52
|
// `tool_call_delta` #15 (argument stream, like `partial_tool_call` #7),
|
|
28
|
-
// `
|
|
29
|
-
//
|
|
30
|
-
|
|
53
|
+
// `context_injection_state` #23 (status of a client injection, which Cursor
|
|
54
|
+
// CLI only shows for queued steers), `routed_model` #24 (display name) and
|
|
55
|
+
// `tool_requests_listed` #27 (count) carry no output and no state. Other
|
|
56
|
+
// members stay barriers.
|
|
57
|
+
const INTERACTION_UPDATE_FIELDS = new Set([1, 2, 3, 4, 5, 7, 8, 13, 14, 15, 16, 17, 23, 24, 27]);
|
|
31
58
|
/** `InteractionUpdate.message_started_at_ms` #25: a timestamp beside the oneof member. */
|
|
32
59
|
const INTERACTION_UPDATE_STARTED_AT = 25;
|
|
33
60
|
const INTERACTION_QUERY_FIELDS = new Set([2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14]);
|
|
@@ -112,21 +139,6 @@ function decodedMatchesWire(topLevel, decoded) {
|
|
|
112
139
|
|| (topLevel?.fn === 5 && !decoded.execControl)
|
|
113
140
|
|| (topLevel?.fn === 7 && !decoded.interactionQuery));
|
|
114
141
|
}
|
|
115
|
-
function hasSemanticProgress(decoded) {
|
|
116
|
-
const update = decoded.interactionUpdate;
|
|
117
|
-
const text = update?.text_delta?.text;
|
|
118
|
-
const thinking = update?.thinking_delta?.text;
|
|
119
|
-
return (typeof text === "string" && text.length > 0)
|
|
120
|
-
|| (typeof thinking === "string" && thinking.length > 0)
|
|
121
|
-
|| !!update?.turn_ended
|
|
122
|
-
|| !!update?.tool_call_started
|
|
123
|
-
|| !!update?.tool_call_completed
|
|
124
|
-
|| !!decoded.exec
|
|
125
|
-
|| !!decoded.kv
|
|
126
|
-
|| !!decoded.execControl
|
|
127
|
-
|| !!decoded.interactionQuery
|
|
128
|
-
|| !!decoded.checkpointBytes?.length;
|
|
129
|
-
}
|
|
130
142
|
/**
|
|
131
143
|
* Field numbers and wire types of a frame, nested three levels, without any
|
|
132
144
|
* payload bytes: `1:2{16:2{1:0}}`. Compare it with `agent.proto` when a frame
|
|
@@ -169,8 +181,5 @@ export function analyzeReplayFrame(payload, decoded) {
|
|
|
169
181
|
else if (topLevel.fn === 2 && !exec.exactRequestContext && !exec.exactMcpState) {
|
|
170
182
|
barrier = "non-control-exec";
|
|
171
183
|
}
|
|
172
|
-
return {
|
|
173
|
-
semanticProgress: hasSemanticProgress(decoded),
|
|
174
|
-
barrier,
|
|
175
|
-
};
|
|
184
|
+
return { barrier };
|
|
176
185
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CursorProviderError } from "./errors.js";
|
|
2
|
+
/** Plain data shared by the plugin and SDK copies, for one logical user turn. */
|
|
3
|
+
export type RetryBudget = {
|
|
4
|
+
server: number;
|
|
5
|
+
transport: number;
|
|
6
|
+
retries: number;
|
|
7
|
+
stallStartedAt?: number;
|
|
8
|
+
/** A plain-text host continuation is admitted only after we requested a host retry. */
|
|
9
|
+
hostRetryPending?: boolean;
|
|
10
|
+
hostRetryKey?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function newRetryBudget(): RetryBudget;
|
|
13
|
+
/** A host retry or tool continuation must not mint a fresh retry allowance. */
|
|
14
|
+
export declare function turnRetryBudget(sessionKey: string | undefined, turnKey: string, continuationKey?: string): RetryBudget;
|
|
15
|
+
export declare function forgetRetryBudget(sessionKey: string | undefined, budget?: RetryBudget): void;
|
|
16
|
+
export declare function endlessRetriesEnabled(): boolean;
|
|
17
|
+
/** CLI's incomplete-stream error is explicitly transport; an idle deadline is server. */
|
|
18
|
+
export declare function retryCategory(failure: CursorProviderError): "server" | "transport";
|
|
19
|
+
/** Reserve one retry before either reopening locally or handing it to OpenCode. */
|
|
20
|
+
export declare function reserveRetry(budget: RetryBudget, failure: CursorProviderError, maxAttempts?: number, now?: number): void;
|
|
21
|
+
export declare function resetRetryBudgetsForTests(): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { CursorRetryExhaustedError } from "./errors.js";
|
|
2
|
+
import { processShared } from "./process-shared.js";
|
|
3
|
+
const budgets = () => processShared("run-retry-budgets.v1", () => new Map());
|
|
4
|
+
const MAX_SESSIONS = 1_024;
|
|
5
|
+
const MAX_TURNS_PER_SESSION = 16;
|
|
6
|
+
export function newRetryBudget() {
|
|
7
|
+
return { server: 0, transport: 0, retries: 0 };
|
|
8
|
+
}
|
|
9
|
+
/** A host retry or tool continuation must not mint a fresh retry allowance. */
|
|
10
|
+
export function turnRetryBudget(sessionKey, turnKey, continuationKey) {
|
|
11
|
+
if (!sessionKey)
|
|
12
|
+
return newRetryBudget();
|
|
13
|
+
const sessions = budgets();
|
|
14
|
+
let turns = sessions.get(sessionKey);
|
|
15
|
+
if (!turns) {
|
|
16
|
+
turns = new Map();
|
|
17
|
+
}
|
|
18
|
+
sessions.delete(sessionKey);
|
|
19
|
+
sessions.set(sessionKey, turns);
|
|
20
|
+
const lookup = (key) => {
|
|
21
|
+
const direct = turns.get(key);
|
|
22
|
+
return direct ? [key, direct] : [...turns].find(([, budget]) => budget.hostRetryKey === key);
|
|
23
|
+
};
|
|
24
|
+
let entry = lookup(turnKey);
|
|
25
|
+
let budget = entry?.[1];
|
|
26
|
+
const currentKey = turnKey;
|
|
27
|
+
if (entry)
|
|
28
|
+
turnKey = entry[0];
|
|
29
|
+
if (!budget && continuationKey !== undefined) {
|
|
30
|
+
entry = lookup(continuationKey);
|
|
31
|
+
const previous = entry?.[1];
|
|
32
|
+
if (previous?.hostRetryPending) {
|
|
33
|
+
budget = previous;
|
|
34
|
+
budget.hostRetryKey = currentKey;
|
|
35
|
+
turnKey = entry[0];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!budget) {
|
|
39
|
+
budget = newRetryBudget();
|
|
40
|
+
}
|
|
41
|
+
budget.hostRetryPending = false;
|
|
42
|
+
turns.delete(turnKey);
|
|
43
|
+
turns.set(turnKey, budget);
|
|
44
|
+
// Abandoned host retries and ephemeral helpers never reach turn_ended.
|
|
45
|
+
// Bound their lookup records; live recovery loops retain their budget object.
|
|
46
|
+
while (turns.size > MAX_TURNS_PER_SESSION)
|
|
47
|
+
turns.delete(turns.keys().next().value);
|
|
48
|
+
while (sessions.size > MAX_SESSIONS)
|
|
49
|
+
sessions.delete(sessions.keys().next().value);
|
|
50
|
+
return budget;
|
|
51
|
+
}
|
|
52
|
+
export function forgetRetryBudget(sessionKey, budget) {
|
|
53
|
+
if (!sessionKey)
|
|
54
|
+
return;
|
|
55
|
+
const sessions = budgets();
|
|
56
|
+
const turns = sessions.get(sessionKey);
|
|
57
|
+
if (!turns)
|
|
58
|
+
return;
|
|
59
|
+
if (!budget)
|
|
60
|
+
sessions.delete(sessionKey);
|
|
61
|
+
else {
|
|
62
|
+
for (const [key, value] of turns)
|
|
63
|
+
if (value === budget)
|
|
64
|
+
turns.delete(key);
|
|
65
|
+
if (turns.size === 0)
|
|
66
|
+
sessions.delete(sessionKey);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function endlessRetriesEnabled() {
|
|
70
|
+
return process.env.CURSOR_ENDLESS_RETRIES === "1" || process.env.CURSOR_ENDLESS_RETRIES === "true";
|
|
71
|
+
}
|
|
72
|
+
/** CLI's incomplete-stream error is explicitly transport; an idle deadline is server. */
|
|
73
|
+
export function retryCategory(failure) {
|
|
74
|
+
return failure.origin === "transport"
|
|
75
|
+
&& failure.code !== "CURSOR_READ_IDLE_TIMEOUT" ? "transport" : "server";
|
|
76
|
+
}
|
|
77
|
+
/** Reserve one retry before either reopening locally or handing it to OpenCode. */
|
|
78
|
+
export function reserveRetry(budget, failure, maxAttempts, now = Date.now()) {
|
|
79
|
+
const category = retryCategory(failure);
|
|
80
|
+
const stalled = failure.code === "CURSOR_SEMANTIC_IDLE_TIMEOUT";
|
|
81
|
+
if (!stalled)
|
|
82
|
+
budget.stallStartedAt = undefined;
|
|
83
|
+
const stallExpired = stalled && budget.stallStartedAt !== undefined && now - budget.stallStartedAt >= 180_000;
|
|
84
|
+
if (stallExpired || (!endlessRetriesEnabled() && (budget[category] >= (category === "server" ? 3 : 10)
|
|
85
|
+
|| (maxAttempts !== undefined && budget.retries >= maxAttempts - 1))))
|
|
86
|
+
throw new CursorRetryExhaustedError(budget.retries + 1, failure);
|
|
87
|
+
if (stalled)
|
|
88
|
+
budget.stallStartedAt ??= now;
|
|
89
|
+
budget[category]++;
|
|
90
|
+
budget.retries++;
|
|
91
|
+
}
|
|
92
|
+
export function resetRetryBudgetsForTests() {
|
|
93
|
+
budgets().clear();
|
|
94
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const MAX_RUNTIME_WRITE_BYTES: number;
|
|
2
|
+
export declare const MAX_RUNTIME_READ_BYTES: number;
|
|
3
|
+
export declare const RUNTIME_FILE_TTL_MS: number;
|
|
4
|
+
export declare const MAX_RUNTIME_SPILL_BYTES: number;
|
|
5
|
+
export declare const MAX_RUNTIME_SPILL_FILES = 128;
|
|
6
|
+
export type RuntimeLayout = {
|
|
7
|
+
root: string;
|
|
8
|
+
agentTools: string;
|
|
9
|
+
terminals: string;
|
|
10
|
+
};
|
|
11
|
+
/** The one provider-owned layout, pinned by the RequestContext's project root. */
|
|
12
|
+
export declare function runtimeLayout(root: string, create?: boolean): RuntimeLayout;
|
|
13
|
+
export declare function runtimeRootFromContext(context: Record<string, unknown> | undefined): string | undefined;
|
|
14
|
+
export declare function maintainRuntimeFiles(layout: RuntimeLayout, now?: number): void;
|
|
15
|
+
/** Native internal filesystem requests never enter the host's workspace tools. */
|
|
16
|
+
export declare function runtimeExecMessages(message: Record<string, unknown>, context: Record<string, unknown> | undefined, allowTools: boolean): Promise<Uint8Array[] | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Age-based cleanup. A live supervisor rewrites its record every second, so a
|
|
19
|
+
* terminal record or job untouched for the TTL is finished, was never
|
|
20
|
+
* started, or was orphaned by a crash or reboot. Only deletions are budgeted:
|
|
21
|
+
* young entries early in directory order cannot stall later cleanup.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sweepRuntimeFiles(layout: RuntimeLayout, now?: number): void;
|