cursor-opencode-provider 0.2.2 → 0.2.4

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.
@@ -1,6 +1,7 @@
1
1
  import { encodeMessage } from "./messages.js";
2
2
  import { encodeJsonAsValue, decodeStructEntriesToJson, readAllFields } from "./struct.js";
3
3
  import { trace } from "../debug.js";
4
+ import { cursorExecVariantByRequestName } from "./exec-variants.js";
4
5
  // Exec variant field number whose reply is the server-initiated request_context
5
6
  // probe (ExecServerMessage #10 → ExecClientMessage #10). request/result share a
6
7
  // field number for every exec variant, so this is also the result field.
@@ -148,6 +149,8 @@ const cursorToolToOpencode = {
148
149
  ls_args: "read",
149
150
  delete_args: "bash",
150
151
  shell_stream_args: "bash",
152
+ background_shell_spawn_args: "bash",
153
+ subagent_args: "task",
151
154
  mcp_args: "mcp",
152
155
  };
153
156
  const opencodeToolToCursor = {
@@ -155,6 +158,7 @@ const opencodeToolToCursor = {
155
158
  write: "write_args",
156
159
  grep: "grep_args",
157
160
  bash: "shell_stream_args",
161
+ task: "subagent_args",
158
162
  mcp: "mcp_args",
159
163
  };
160
164
  /** Cursor-only fields that must not be forwarded as OpenCode tool input. */
@@ -203,26 +207,7 @@ export function mapExecServerToToolName(execField) {
203
207
  export function mapToolNameToExecField(toolName) {
204
208
  return opencodeToolToCursor[toolName];
205
209
  }
206
- // Cursor exec-request variant → the ExecClientMessage result field the client
207
- // must reply with. This is keyed off the REQUEST variant, not the opencode tool
208
- // name, because an MCP call must always answer with `mcp_result` even though the
209
- // resolved tool name (e.g. "read") looks like a built-in.
210
- const execVariantToResultField = {
211
- read_args: "read_result",
212
- write_args: "write_result",
213
- pi_read_args: "pi_read_result",
214
- pi_bash_args: "pi_bash_result",
215
- pi_edit_args: "pi_edit_result",
216
- pi_write_args: "pi_write_result",
217
- pi_grep_args: "pi_grep_result",
218
- pi_find_args: "pi_find_result",
219
- pi_ls_args: "pi_ls_result",
220
- grep_args: "grep_result",
221
- ls_args: "ls_result",
222
- delete_args: "delete_result",
223
- shell_stream_args: "shell_stream",
224
- mcp_args: "mcp_result",
225
- };
210
+ const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__";
226
211
  export function parseExecServerMessage(msg) {
227
212
  const id = msg.id;
228
213
  if (id === undefined)
@@ -233,12 +218,71 @@ export function parseExecServerMessage(msg) {
233
218
  "pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args",
234
219
  "pi_grep_args", "pi_find_args", "pi_ls_args",
235
220
  "grep_args", "ls_args",
236
- "delete_args", "shell_stream_args", "mcp_args",
221
+ "delete_args", "shell_stream_args", "background_shell_spawn_args", "mcp_args",
222
+ "subagent_args",
237
223
  ]);
238
224
  if (!execVariant)
239
225
  return undefined;
240
- const resultField = execVariantToResultField[execVariant];
226
+ // Use the complete canonical request/result table. In particular, Pi request
227
+ // fields #45..#51 pair with result fields #46..#52 rather than matching ids.
228
+ const resultField = cursorExecVariantByRequestName(execVariant)?.resultName;
229
+ if (!resultField)
230
+ return undefined;
241
231
  const execId = msg.exec_id ?? "";
232
+ if (execVariant === "background_shell_spawn_args") {
233
+ const raw = msg.background_shell_spawn_args ?? {};
234
+ const command = str(raw.command);
235
+ const workingDirectory = str(raw.working_directory) ?? "";
236
+ const args = {};
237
+ if (command)
238
+ args.command = buildBackgroundShellCommand(command);
239
+ if (workingDirectory)
240
+ args.workdir = workingDirectory;
241
+ return {
242
+ id,
243
+ execId,
244
+ toolName: "bash",
245
+ args,
246
+ resultField,
247
+ resultMetadata: { command: command ?? "", working_directory: workingDirectory },
248
+ localError: raw.enable_write_shell_stdin_tool === true
249
+ ? "Interactive background shells are not available through OpenCode's bash tool."
250
+ : command
251
+ ? undefined
252
+ : "Cursor background shell request is missing a command.",
253
+ };
254
+ }
255
+ if (execVariant === "subagent_args") {
256
+ const raw = msg.subagent_args ?? {};
257
+ const prompt = str(raw.prompt);
258
+ const cursorSubagentType = str(raw.subagent_type);
259
+ const subagentType = cursorSubagentType
260
+ ? mapCursorSubagentTypeToOpenCode(cursorSubagentType)
261
+ : undefined;
262
+ const args = {
263
+ description: describeSubagentTask(prompt, subagentType),
264
+ prompt: prompt ?? "",
265
+ subagent_type: subagentType ?? "",
266
+ };
267
+ const resumeAgentId = str(raw.resume_agent_id);
268
+ if (resumeAgentId)
269
+ args.task_id = resumeAgentId;
270
+ // protobufjs materializes an absent proto3 optional bool as false in this
271
+ // reflection schema. OpenCode's foreground default is already false, so
272
+ // only forward the meaningful opt-in value.
273
+ if (raw.run_in_background === true)
274
+ args.background = true;
275
+ return {
276
+ id,
277
+ execId,
278
+ toolName: "task",
279
+ args,
280
+ resultField,
281
+ localError: prompt && cursorSubagentType
282
+ ? undefined
283
+ : "Cursor subagent request is missing a required prompt or subagent type.",
284
+ };
285
+ }
242
286
  if (execVariant === "mcp_args") {
243
287
  // An MCP call to one of the tools we advertised. Resolve the real opencode
244
288
  // tool name (Cursor's model may have shortened "opencode-read" → "read") and
@@ -284,12 +328,43 @@ export function parseExecServerMessage(msg) {
284
328
  if (!toolName)
285
329
  return undefined;
286
330
  const mapped = mapCursorArgsToOpencode(toolName, msg[execVariant] ?? {}, execVariant);
331
+ const resultMetadata = execVariant === "shell_stream_args"
332
+ ? shellStreamResultMetadata(msg[execVariant] ?? {})
333
+ : undefined;
334
+ if (resultMetadata
335
+ && resultMetadata.timeout_behavior !== 2
336
+ && typeof resultMetadata.timeout_ms === "number") {
337
+ // Cursor's protobuf default (timeout=0) means 30 seconds for an ordinary
338
+ // foreground shell. OpenCode instead treats zero literally, so pass the
339
+ // effective native value rather than the raw protobuf default.
340
+ mapped.args.timeout = resultMetadata.timeout_ms;
341
+ }
287
342
  return {
288
343
  id,
289
344
  execId,
290
345
  toolName: mapped.toolName,
291
346
  args: mapped.args,
292
347
  resultField,
348
+ ...(resultMetadata ? { resultMetadata } : {}),
349
+ };
350
+ }
351
+ function shellStreamResultMetadata(raw) {
352
+ const timeout = num(raw.timeout) ?? 0;
353
+ const timeoutBehavior = num(raw.timeout_behavior) ?? 0;
354
+ const hardTimeout = num(raw.hard_timeout);
355
+ // Cursor CLI: a nonzero timeout is used verbatim. A zero foreground timeout
356
+ // defaults to 30s; zero with background/hard-timeout semantics means an
357
+ // immediate soft handoff governed by the separate hard deadline.
358
+ const effectiveTimeout = timeout !== 0
359
+ ? timeout
360
+ : (timeoutBehavior === 2 || (hardTimeout !== undefined && hardTimeout > 0) ? 0 : 30_000);
361
+ return {
362
+ shell_stream: true,
363
+ command: str(raw.command) ?? "",
364
+ working_directory: str(raw.working_directory) ?? "",
365
+ timeout_ms: effectiveTimeout,
366
+ timeout_behavior: timeoutBehavior,
367
+ ...(hardTimeout !== undefined && hardTimeout > 0 ? { hard_timeout_ms: hardTimeout } : {}),
293
368
  };
294
369
  }
295
370
  /**
@@ -425,9 +500,42 @@ function num(v) {
425
500
  return Number(v);
426
501
  return undefined;
427
502
  }
503
+ function describeSubagentTask(prompt, subagentType) {
504
+ const words = prompt?.replace(/\s+/g, " ").trim().split(" ").filter(Boolean).slice(0, 5);
505
+ if (words?.length)
506
+ return words.join(" ");
507
+ return `${subagentType || "Delegated"} task`;
508
+ }
509
+ function mapCursorSubagentTypeToOpenCode(subagentType) {
510
+ // Cursor's built-in general agent uses a camelCase protocol identifier, and
511
+ // its native Bugbot reviewer has no OpenCode-specific agent definition.
512
+ // OpenCode `general` is the general-purpose equivalent; `explore` preserves
513
+ // Bugbot's review-only/read-oriented semantics while retaining grep/read/bash.
514
+ // Preserve every other value so `explore` and configured custom agents keep
515
+ // their exact advertised names.
516
+ if (subagentType === "generalPurpose")
517
+ return "general";
518
+ if (subagentType === "bugbot")
519
+ return "explore";
520
+ return subagentType;
521
+ }
428
522
  function shellQuote(s) {
429
523
  return `'${s.replace(/'/g, `'\\''`)}'`;
430
524
  }
525
+ /**
526
+ * OpenCode's bash tool is foreground-only. Detach the requested command inside
527
+ * that one foreground call and print a private marker containing the spawned
528
+ * PID and log path. With stdin and all output redirected, the host shell can
529
+ * return immediately instead of retaining OpenCode's tool pipe.
530
+ */
531
+ function buildBackgroundShellCommand(command) {
532
+ return [
533
+ 'bg_log="$(mktemp "${TMPDIR:-/tmp}/cursor-opencode-bg.XXXXXX")" || exit 1',
534
+ `nohup sh -c ${shellQuote(command)} >"$bg_log" 2>&1 </dev/null &`,
535
+ "bg_pid=$!",
536
+ `printf '${BACKGROUND_SHELL_MARKER}%s:%s\\n' "$bg_pid" "$bg_log"`,
537
+ ].join("\n");
538
+ }
431
539
  /**
432
540
  * Map Cursor McpArgs back to the OpenCode tool id.
433
541
  * Prefers provider_identifier + bare tool_name (github + create_pull_request
@@ -505,9 +613,32 @@ export function buildExecClientMessages(input) {
505
613
  if (input.output) {
506
614
  frames.push(encodeShellStream(input.execId, undefined, { stdout: { data: input.output } }));
507
615
  }
508
- frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
509
- exit: { code: 0, aborted: false },
510
- }));
616
+ if (input.shellOutcome?.kind === "backgrounded") {
617
+ frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
618
+ backgrounded: {
619
+ shell_id: input.shellOutcome.shellId,
620
+ command: input.shellOutcome.command,
621
+ working_directory: input.shellOutcome.workingDirectory,
622
+ pid: input.shellOutcome.pid,
623
+ ms_to_wait: input.shellOutcome.msToWait,
624
+ reason: input.shellOutcome.reason,
625
+ },
626
+ }));
627
+ }
628
+ else if (input.shellOutcome?.kind === "timeout") {
629
+ frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
630
+ // Native CLI represents timeout structurally. ShellAbortReason.TIMEOUT=2.
631
+ exit: { code: 0, aborted: true, abort_reason: 2 },
632
+ }));
633
+ }
634
+ else {
635
+ const exitCode = input.shellOutcome?.kind === "exit"
636
+ ? Math.max(0, Math.min(0xffff_ffff, input.shellOutcome.code))
637
+ : 0;
638
+ frames.push(encodeShellStream(input.execId, input.executionTimeMs, {
639
+ exit: { code: exitCode, aborted: false },
640
+ }));
641
+ }
511
642
  }
512
643
  }
513
644
  else {
@@ -515,7 +646,7 @@ export function buildExecClientMessages(input) {
515
646
  id: input.execId,
516
647
  local_execution_time_ms: input.executionTimeMs ?? 0,
517
648
  };
518
- clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName);
649
+ clientMsg[resultField] = buildTypedExecResult(resultField, input.output, input.error, input.toolName, input.resultMetadata);
519
650
  frames.push(encodeMessage("AgentClientMessage", {
520
651
  exec_client_message: clientMsg,
521
652
  }));
@@ -602,7 +733,7 @@ export function unwrapReadOutput(output) {
602
733
  * OpenCode returns free-form text; we wrap it in the minimal success shape the
603
734
  * server accepts (verified against agent.v1 wire captures).
604
735
  */
605
- export function buildTypedExecResult(resultField, output, error, toolName) {
736
+ export function buildTypedExecResult(resultField, output, error, toolName, resultMetadata) {
606
737
  switch (resultField) {
607
738
  case "read_result":
608
739
  if (error)
@@ -673,6 +804,31 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
673
804
  if (error)
674
805
  return { error: { path: "", error } };
675
806
  return { success: { path: "", deleted_file: "" } };
807
+ case "background_shell_spawn_result": {
808
+ const command = str(resultMetadata?.command) ?? "";
809
+ const workingDirectory = str(resultMetadata?.working_directory) ?? "";
810
+ if (error)
811
+ return { error: { command, working_directory: workingDirectory, error } };
812
+ const match = new RegExp(`${BACKGROUND_SHELL_MARKER}(\\d+):([^\\r\\n]+)`).exec(output);
813
+ const pid = match ? Number(match[1]) : 0;
814
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff) {
815
+ return {
816
+ error: {
817
+ command,
818
+ working_directory: workingDirectory,
819
+ error: "OpenCode did not return a valid background shell process id.",
820
+ },
821
+ };
822
+ }
823
+ return {
824
+ success: {
825
+ shell_id: pid,
826
+ command,
827
+ working_directory: workingDirectory,
828
+ pid,
829
+ },
830
+ };
831
+ }
676
832
  case "ls_result": {
677
833
  if (error)
678
834
  return { error: { path: "", error } };
@@ -706,6 +862,27 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
706
862
  is_error: false,
707
863
  },
708
864
  };
865
+ case "subagent_result": {
866
+ const task = parseOpenCodeTaskOutput(output);
867
+ if (error || task.state === "error") {
868
+ return {
869
+ error: {
870
+ ...(task.agentId ? { agent_id: task.agentId } : {}),
871
+ error: error ?? task.message ?? output,
872
+ },
873
+ };
874
+ }
875
+ return {
876
+ success: {
877
+ agent_id: task.agentId ?? "",
878
+ ...(task.message !== undefined ? { final_message: task.message } : {}),
879
+ tool_call_count: 0,
880
+ // OpenCode marks an asynchronous launch as state="running". Cursor's
881
+ // canonical USER_REQUEST enum value is 2; foreground/default is 0.
882
+ background_reason: task.state === "running" ? 2 : 0,
883
+ },
884
+ };
885
+ }
709
886
  default:
710
887
  // Unknown variant: best-effort success wrapper so the server sees a oneof.
711
888
  if (error)
@@ -713,6 +890,25 @@ export function buildTypedExecResult(resultField, output, error, toolName) {
713
890
  return { success: { content: output } };
714
891
  }
715
892
  }
893
+ function parseOpenCodeTaskOutput(output) {
894
+ // Attribute order is not guaranteed; accept id/state in either order and
895
+ // ignore additional attributes OpenCode may emit on the <task> open tag.
896
+ const open = /<task\b([^>]*)>/i.exec(output);
897
+ if (!open)
898
+ return { message: output };
899
+ const attrs = open[1];
900
+ const agentId = /\bid="([^"]+)"/i.exec(attrs)?.[1];
901
+ const state = /\bstate="(running|completed|error)"/i.exec(attrs)?.[1];
902
+ if (!agentId || !state)
903
+ return { message: output };
904
+ const tag = state === "error" ? "task_error" : "task_result";
905
+ const body = new RegExp(`<${tag}>\\n?([\\s\\S]*?)\\n?</${tag}>`, "i").exec(output);
906
+ return {
907
+ agentId,
908
+ state,
909
+ message: body?.[1] ?? output,
910
+ };
911
+ }
716
912
  function extractPathTag(output) {
717
913
  const m = output.match(/<path>([^<]+)<\/path>/);
718
914
  return m?.[1];
@@ -810,3 +1006,92 @@ export function buildRequestContextResult(execId, requestContext) {
810
1006
  },
811
1007
  });
812
1008
  }
1009
+ /**
1010
+ * Answer Cursor's exec #36 MCP-state probe from the same descriptors advertised
1011
+ * in RequestContext. OpenCode remains the executor; this only confirms that the
1012
+ * provider's virtual MCP servers and their tools are available.
1013
+ */
1014
+ export function buildMcpStateResult(execId, args, requestContext) {
1015
+ const requested = new Set(Array.isArray(args.server_identifiers)
1016
+ ? args.server_identifiers.filter((id) => typeof id === "string" && id.length > 0)
1017
+ : []);
1018
+ const fsOptions = recordValue(requestContext.mcp_file_system_options);
1019
+ const nested = Array.isArray(fsOptions?.mcp_descriptors)
1020
+ ? fsOptions.mcp_descriptors.map(recordValue).filter((d) => !!d)
1021
+ : [];
1022
+ const descriptors = nested.length > 0 ? nested : descriptorsFromFlatTools(requestContext.tools);
1023
+ const flatTools = Array.isArray(requestContext.tools)
1024
+ ? requestContext.tools.map(recordValue).filter((tool) => !!tool)
1025
+ : [];
1026
+ const servers = descriptors
1027
+ .filter((descriptor) => {
1028
+ const id = stringValue(descriptor.server_identifier);
1029
+ return requested.size === 0 || (id !== undefined && requested.has(id));
1030
+ })
1031
+ .map((descriptor) => {
1032
+ const serverIdentifier = stringValue(descriptor.server_identifier) ?? stringValue(descriptor.server_name) ?? "";
1033
+ const tools = Array.isArray(descriptor.tools)
1034
+ ? descriptor.tools
1035
+ .map(recordValue)
1036
+ .filter((tool) => !!tool)
1037
+ .map((tool) => mcpStateToolDefinition(serverIdentifier, tool, flatTools))
1038
+ : [];
1039
+ return {
1040
+ server_name: stringValue(descriptor.server_name) ?? serverIdentifier,
1041
+ server_identifier: serverIdentifier,
1042
+ tools,
1043
+ };
1044
+ });
1045
+ return encodeMessage("AgentClientMessage", {
1046
+ exec_client_message: {
1047
+ id: execId,
1048
+ mcp_state_exec_result: { success: { servers } },
1049
+ },
1050
+ });
1051
+ }
1052
+ /**
1053
+ * Exec #36 uses McpToolDefinition, not the narrower McpToolDescriptor used by
1054
+ * RequestContext's filesystem/meta-tool catalogs. Rehydrate the full identity
1055
+ * from RequestContext.tools so Cursor's native get_mcp_tools can correlate the
1056
+ * discovered definition with the later provider_identifier/tool_name request.
1057
+ */
1058
+ function mcpStateToolDefinition(serverIdentifier, descriptor, flatTools) {
1059
+ const toolName = stringValue(descriptor.tool_name) ?? "";
1060
+ const advertised = flatTools.find((tool) => stringValue(tool.provider_identifier) === serverIdentifier
1061
+ && stringValue(tool.tool_name) === toolName);
1062
+ return {
1063
+ name: stringValue(advertised?.name) ?? `${serverIdentifier}-${toolName}`,
1064
+ description: stringValue(advertised?.description) ?? stringValue(descriptor.description) ?? "",
1065
+ input_schema: advertised?.input_schema ?? descriptor.input_schema,
1066
+ provider_identifier: serverIdentifier,
1067
+ tool_name: toolName,
1068
+ };
1069
+ }
1070
+ function recordValue(value) {
1071
+ return value && typeof value === "object" && !Array.isArray(value)
1072
+ ? value
1073
+ : undefined;
1074
+ }
1075
+ function descriptorsFromFlatTools(value) {
1076
+ if (!Array.isArray(value))
1077
+ return [];
1078
+ const byServer = new Map();
1079
+ for (const raw of value) {
1080
+ const tool = recordValue(raw);
1081
+ if (!tool)
1082
+ continue;
1083
+ const server = stringValue(tool.provider_identifier) ?? "opencode";
1084
+ const tools = byServer.get(server) ?? [];
1085
+ tools.push({
1086
+ tool_name: stringValue(tool.tool_name) ?? stringValue(tool.name) ?? "",
1087
+ description: stringValue(tool.description) ?? "",
1088
+ input_schema: tool.input_schema,
1089
+ });
1090
+ byServer.set(server, tools);
1091
+ }
1092
+ return [...byServer].map(([server, tools]) => ({
1093
+ server_name: server,
1094
+ server_identifier: server,
1095
+ tools,
1096
+ }));
1097
+ }
package/dist/session.d.ts CHANGED
@@ -1,8 +1,26 @@
1
1
  import type { BidiStream } from "./transport/connect.js";
2
+ import { type CursorProviderError } from "./errors.js";
3
+ import { type SessionActivitySource } from "./activity.js";
2
4
  export type Frame = {
3
5
  flags: number;
4
6
  payload: Uint8Array;
5
7
  };
8
+ export type CursorContinuationOptions = {
9
+ semanticIdleMs?: number;
10
+ /** @deprecated Use semanticIdleMs. Kept as a strict alias for compatibility. */
11
+ softHealthMs?: number;
12
+ /** Pending-tool inactivity window, renewed by OpenCode session progress. */
13
+ hardCapMs?: number;
14
+ heartbeatMs?: number;
15
+ };
16
+ export type CursorContinuationPolicy = {
17
+ semanticIdleMs: number;
18
+ hardCapMs: number;
19
+ heartbeatMs: number;
20
+ };
21
+ export declare const DEFAULT_CONTINUATION_POLICY: Readonly<CursorContinuationPolicy>;
22
+ export declare function resolveContinuationPolicy(options: CursorContinuationOptions | undefined): CursorContinuationPolicy;
23
+ export type PendingExecState = "pending" | "claimed" | "delivered";
6
24
  /**
7
25
  * A held-open Run stream. Cursor drives the agentic loop server-side and
8
26
  * expects tool results on the SAME bidi stream. opencode, by contrast, owns
@@ -14,11 +32,16 @@ export type Frame = {
14
32
  export type PendingExec = {
15
33
  /** ExecClientMessage result field to reply with (matches the request variant). */
16
34
  resultField: string;
35
+ state: PendingExecState;
36
+ registeredAt: number;
37
+ hardDeadlineAt: number;
17
38
  /**
18
39
  * Resolved opencode tool name (read/write/grep/…). Used on continuation so
19
40
  * mcp_result can unwrap read envelopes even if the prompt omits toolName.
20
41
  */
21
42
  toolName?: string;
43
+ /** Original request fields required by a typed result message. */
44
+ resultMetadata?: Record<string, unknown>;
22
45
  /**
23
46
  * True when this pending entry was synthesized from a Cursor display-only
24
47
  * tool_call_* frame (no ExecServerMessage). Continuation must not write an
@@ -26,6 +49,43 @@ export type PendingExec = {
26
49
  */
27
50
  bridged?: boolean;
28
51
  };
52
+ 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";
53
+ export type SessionCloseReason = ContinuationTerminalReason | "ordinary-cleanup" | "turn-ended" | "initial-write-failed";
54
+ export type ContinuationClaim = {
55
+ session: CursorSession;
56
+ execId: number;
57
+ pending: PendingExec;
58
+ };
59
+ export type ContinuationClassification = {
60
+ kind: "deliverable";
61
+ session: CursorSession;
62
+ pending: PendingExec;
63
+ } | {
64
+ kind: "duplicate";
65
+ reason: "in-flight" | "delivered";
66
+ } | {
67
+ kind: "terminal";
68
+ reason: ContinuationTerminalReason;
69
+ } | {
70
+ kind: "missing";
71
+ reason: "missing-process-local-state";
72
+ };
73
+ export type DeliveryOutcome = {
74
+ kind: "delivered";
75
+ framesWritten: number;
76
+ } | {
77
+ kind: "duplicate";
78
+ reason: "in-flight" | "delivered";
79
+ framesWritten: 0;
80
+ } | {
81
+ kind: "terminal";
82
+ reason: ContinuationTerminalReason;
83
+ framesWritten: number;
84
+ } | {
85
+ kind: "missing";
86
+ reason: "missing-process-local-state";
87
+ framesWritten: 0;
88
+ };
29
89
  export type CursorSession = {
30
90
  /**
31
91
  * Stable per-Run-stream id (distinct from Cursor's own conversation_id).
@@ -36,8 +96,10 @@ export type CursorSession = {
36
96
  /**
37
97
  * Cursor conversation_id for this Run — used to store/echo
38
98
  * conversation_checkpoint_update (CLI parity).
39
- */
99
+ */
40
100
  conversationId: string;
101
+ /** OpenCode session whose own or descendant activity renews tool leases. */
102
+ openCodeSessionId?: string;
41
103
  stream: BidiStream;
42
104
  frames: AsyncIterator<Frame>;
43
105
  pending: Map<number, PendingExec>;
@@ -77,26 +139,62 @@ export type CursorSession = {
77
139
  * Prevents a late cancel/abort from a prior ReadableStream from destroying
78
140
  * the Run connection after tool results were delivered (pending cleared) but
79
141
  * Cursor is still generating.
80
- */
142
+ */
81
143
  pumpActive: boolean;
144
+ /** The active pull owner; stale cancel callbacks cannot affect a newer pump. */
145
+ pumpOwner: symbol | null;
82
146
  heartbeat: ReturnType<typeof setInterval> | null;
83
- expiresAt: number;
147
+ heartbeatCancel: (() => void) | null;
148
+ hardDeadlineTimer: ReturnType<typeof setTimeout> | null;
149
+ semanticDeadlineCancel: (() => void) | null;
150
+ terminalUnsubscribe: (() => void) | null;
151
+ deferredTerminalReason: "remote-clean-close" | "remote-error" | null;
152
+ policy: CursorContinuationPolicy;
153
+ createdAt: number;
154
+ lastInboundAt: number;
155
+ lastHeartbeatWriteAt: number;
156
+ semanticDeadlineAt: number;
157
+ closeError: CursorProviderError | null;
158
+ closed: boolean;
159
+ };
160
+ type SessionManagerOptions = {
161
+ now?: () => number;
162
+ setTimer?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
163
+ clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
164
+ activitySource?: SessionActivitySource;
165
+ tombstoneTtlMs?: number;
166
+ tombstoneLimit?: number;
84
167
  };
85
168
  export declare class SessionManager {
86
169
  private byExecId;
87
- private readonly idleTimeoutMs;
88
- constructor(idleTimeoutMs?: number);
89
- touch(session: CursorSession): void;
170
+ private sessions;
171
+ private tombstones;
172
+ private readonly now;
173
+ private readonly setTimer;
174
+ private readonly clearTimer;
175
+ private readonly activitySource;
176
+ private readonly tombstoneTtlMs;
177
+ private readonly tombstoneLimit;
178
+ constructor(options?: SessionManagerOptions);
179
+ registerSession(session: CursorSession): void;
180
+ recordSemanticProgress(session: CursorSession, at?: number): void;
181
+ recordHeartbeatWrite(session: CursorSession): void;
90
182
  /** Register that `session` is awaiting a result for `execId`. */
91
- registerPending(execId: number, session: CursorSession, resultField: string, toolName?: string, bridged?: boolean): void;
183
+ registerPending(execId: number, session: CursorSession, resultField: string, toolName?: string, bridged?: boolean, resultMetadata?: Record<string, unknown>): void;
92
184
  /** The pending exec info for an id on a specific session, if still awaiting it. */
93
185
  pendingFor(sessionId: string, execId: number): PendingExec | undefined;
186
+ classify(sessionId: string, execId: number): ContinuationClassification;
187
+ claim(sessionId: string, execId: number): ContinuationClaim | ContinuationClassification;
188
+ deliverClaim(claim: ContinuationClaim, frames: readonly Uint8Array[]): DeliveryOutcome;
94
189
  /** Find the live session awaiting one of the given exec ids. */
95
190
  findByExecIds(sessionId: string, execIds: number[]): CursorSession | undefined;
96
191
  /** Mark an exec id as resolved (its result has been delivered). */
97
192
  resolve(sessionId: string, execId: number): void;
193
+ beginPump(session: CursorSession, owner: symbol): void;
194
+ isPumpOwner(session: CursorSession, owner: symbol): boolean;
195
+ endPump(session: CursorSession, owner: symbol): boolean;
98
196
  private key;
99
- close(session: CursorSession): void;
197
+ close(session: CursorSession, reason?: SessionCloseReason, error?: CursorProviderError): void;
100
198
  /**
101
199
  * Close only if nothing is awaiting a tool result AND no pull() is actively
102
200
  * pumping this session. OpenCode aborts each doStream after finishReason
@@ -107,5 +205,14 @@ export declare class SessionManager {
107
205
  */
108
206
  closeUnlessPending(session: CursorSession): boolean;
109
207
  dispose(): void;
208
+ sweepHardDeadlines(): void;
209
+ private onStreamTerminal;
210
+ private refreshHardDeadline;
211
+ private hardDeadlineExpired;
212
+ private scheduleHardDeadline;
213
+ private getTombstone;
214
+ private putTombstone;
215
+ private isTerminalReason;
110
216
  }
111
217
  export declare const sessionManager: SessionManager;
218
+ export {};