@stablekernel/opencode-cursor 0.6.2-next.0 → 0.7.0-next.0

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 CHANGED
@@ -4,11 +4,64 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ## [0.6.2-next.0] — 2026-07-27 (pre-release)
7
+ ## [0.7.0-next.0] — 2026-07-30 (pre-release)
8
8
 
9
- Pre-release of the version-check UX cleanup from #79. Not yet on `latest`; install with
9
+ Pre-release of structured logging (#85), the stream-watchdog tool-phase budget (#86), and the
10
+ session-pool title-generation race fix (#84). Not yet on `latest`; install with
10
11
  `npm install @stablekernel/opencode-cursor@next` to test.
11
12
 
13
+ - **Structured logging via `client.app.log()` instead of raw `console.*`.** The
14
+ plugin's own diagnostics (transport fallback warnings, per-turn debug traces
15
+ gated on `OPENCODE_CURSOR_DEBUG=1`) now route through opencode's plugin
16
+ logging API (`service: "opencode-cursor"`) rather than `console.warn`/
17
+ `console.error`. Falls back to `console.*` when no client is available
18
+ (e.g. running the provider standalone).
19
+ - **Cursor SDK's own "rules"/"skills" load diagnostics captured and forwarded.**
20
+ `@cursor/sdk`'s bundled local-exec runtime writes internal messages like
21
+ `LocalCursorRulesService load completed meta={durationMs, ruleCount}` and
22
+ `AgentSkillsCursorRulesService load completed meta={durationMs, ruleCount,
23
+ skillCount}` straight to `console.log`, with no public logger hook to
24
+ redirect it. These are now recognized (in-process transport via a narrowly
25
+ scoped `console.log` interceptor; sidecar transport via the child process's
26
+ own interceptor forwarding over the existing JSONL protocol) and re-emitted
27
+ as structured opencode logs instead of raw terminal noise. Every other
28
+ `console.log` call passes through unchanged.
29
+ - **Fixed: the stream watchdog killed healthy runs during long tool execution.** The watchdog
30
+ re-armed only on mapped event types, so a long shell command, build, or test suite that streamed
31
+ nothing for 60s was cancelled and the turn lost. It now uses two budgets — an idle budget
32
+ (`OPENCODE_CURSOR_STALL_MS`, default raised to `120000`) and a larger tool-phase budget
33
+ (`OPENCODE_CURSOR_TOOL_STALL_MS`, default `600000`) applied while a tool call is in flight — and
34
+ re-arms on **any** SDK update, including types the plugin doesn't model (progress/heartbeats). A
35
+ tool-phase stall is terminal and names the in-flight tool. `OPENCODE_CURSOR_STALL_MS=0` still
36
+ disables the whole watchdog; the tool-phase bound is independently disabled with
37
+ `OPENCODE_CURSOR_TOOL_STALL_MS=0`. Open tool calls are reconciled on `turn-ended` and on a forced
38
+ resend, so a dropped completion can't pin a turn to the 10-minute budget.
39
+ - **Fixed: a non-numeric `OPENCODE_CURSOR_STALL_MS` stalled every turn immediately.**
40
+ `Number("abc")` is `NaN`; `NaN <= 0` is `false`, so the guard passed and `setTimeout(fn, NaN)`
41
+ fired at once. Env parsing now falls back to the default for non-finite values (an empty string
42
+ still disables, preserving the historical escape hatch).
43
+ - **Fixed: an over-large stall budget overflowed to a ~1 ms deadline.** A `setTimeout` delay is
44
+ stored as a signed 32-bit int, so anything above `2147483647` is silently clamped to `1` — and the
45
+ tool-phase stall message tells operators to *raise* `OPENCODE_CURSOR_TOOL_STALL_MS`, making the
46
+ trap reachable by following the plugin's own advice. Setting it to e.g. `999999999999` stalled
47
+ every tool-bearing turn within milliseconds while reporting `no events for 999999999999ms`. Both
48
+ budgets are now capped at `2147483647`.
49
+ - **Fixed: opencode's title-generation call could poison a session's pool entry.** opencode forks a
50
+ title-generation call on the same `sessionID` as the session's real first turn, concurrently and
51
+ with an empty system prompt. `classifyTurn`'s side-call detection only fires once a prior pool
52
+ record exists, so on turn 1 both calls classified as "new" and both wrote to the pool — whichever
53
+ agent-creation round-trip resolved last silently overwrote the other, leaving the session
54
+ fingerprinted against the title prompt. Two fixes: the plugin's `chat.params` hook now marks
55
+ opencode's `title` agent call as `providerOptions.cursor.ephemeral = true` (the provider already
56
+ honored this flag but nothing set it), and `withSessionLock` (a per-`sessionID` async lock) now
57
+ wraps `agentRun`'s classify-then-acquire span so concurrent turns for one session serialize and
58
+ the second call always observes the first's completed pool write.
59
+ - **Dependency bumps:** `@cursor/sdk` 1.0.24 → 1.0.26, `@opencode-ai/plugin` (opencode-ai group).
60
+
61
+ ## [0.6.2] — 2026-07-28
62
+
63
+ Version-check UX cleanup from #79.
64
+
12
65
  - **Fixed: startup toast no longer suspends into the user's first prompt on slow networks.**
13
66
  The version-check toast previously ran `setTimeout(callback, 2000)` and then `await
14
67
  _versionCheckPromise` inside the callback, so a slow npm registry fetch could block the
package/README.md CHANGED
@@ -192,7 +192,8 @@ See [SECURITY.md](./SECURITY.md) for the full threat model.
192
192
  | `OPENCODE_CURSOR_MODEL_CACHE_TTL_MS` | `86400000` | Model-list cache lifetime (ms) |
193
193
  | `OPENCODE_CURSOR_DEBUG` | — | Set to `1` for trace logging on stderr |
194
194
  | `OPENCODE_CURSOR_TRANSPORT` | — | Force a transport: `http1` \| `http2-direct` \| `sidecar` — see [Transport](#transport) |
195
- | `OPENCODE_CURSOR_STALL_MS` | `60000` | Stream watchdog timeout (ms); `0` disables — see [Reliability](#reliability) |
195
+ | `OPENCODE_CURSOR_STALL_MS` | `120000` | Idle stream-watchdog timeout in ms (no tool call open). `0` disables the whole watchdog; an empty string also disables — see [Reliability](#reliability) |
196
+ | `OPENCODE_CURSOR_TOOL_STALL_MS` | `600000` | Stream-watchdog timeout in ms while a tool call is in flight (e.g. a long build or test suite). `0` disables the bound during tool execution only — see [Reliability](#reliability) |
196
197
  | `OPENCODE_CURSOR_SIDECAR` | — | Legacy: `1` maps to `sidecar`, `0` maps to `http2-direct` (superseded by `OPENCODE_CURSOR_TRANSPORT`) |
197
198
  | `OPENCODE_CURSOR_TOOL_INPUT_STREAM` | on | Set to `0` to disable live tool-input streaming (`tool-input-start`/`-delta`/`-end` parts) |
198
199
 
@@ -378,10 +379,20 @@ The provider classifies Cursor SDK errors into typed kinds (`agent-not-found`, `
378
379
 
379
380
  Sends carry an idempotency key so a retry is a server-side dedupe, not a duplicate turn.
380
381
 
381
- A **stream watchdog** guards against a wedged run that streams nothing: if no event arrives within
382
- `OPENCODE_CURSOR_STALL_MS` (default `60000`), a pre-first-event stall cancels and force-resends
383
- once; a stall after partial output is surfaced as a terminal error rather than re-emitting the
384
- already-yielded prefix. Set `OPENCODE_CURSOR_STALL_MS=0` to disable.
382
+ A **stream watchdog** guards against a wedged run that streams nothing. It uses two budgets:
383
+
384
+ - **Idle** (`OPENCODE_CURSOR_STALL_MS`, default `120000`): when no tool call is open. A
385
+ pre-first-event stall cancels and force-resends once; a stall after partial output is surfaced
386
+ as a terminal error rather than re-emitting the already-yielded prefix.
387
+ - **Tool-phase** (`OPENCODE_CURSOR_TOOL_STALL_MS`, default `600000`): while at least one Cursor
388
+ tool call is in flight. A long shell command, build, or test suite legitimately streams nothing
389
+ for minutes; the larger budget stops a healthy run from being killed mid-tool. A tool-phase
390
+ stall is terminal and names the in-flight tool. Set `0` to disable the bound during tool
391
+ execution only.
392
+
393
+ The watchdog re-arms on **any** SDK update — including types the plugin doesn't model — so
394
+ progress/heartbeat updates count as liveness. Set `OPENCODE_CURSOR_STALL_MS=0` to disable the whole
395
+ watchdog (an empty string also disables, for backward compatibility).
385
396
 
386
397
  ## Troubleshooting
387
398
 
@@ -16,6 +16,50 @@ function fingerprintApiKey(apiKey) {
16
16
  return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
17
17
  }
18
18
 
19
+ // src/provider/log-bridge.ts
20
+ var BRIDGE_KEY = /* @__PURE__ */ Symbol.for("@stablekernel/opencode-cursor:log-bridge");
21
+ function setLogBridge(bridge) {
22
+ globalThis[BRIDGE_KEY] = bridge;
23
+ }
24
+ function clearLogBridge() {
25
+ delete globalThis[BRIDGE_KEY];
26
+ }
27
+ function getLogBridge() {
28
+ return globalThis[BRIDGE_KEY];
29
+ }
30
+ var SERVICE = "opencode-cursor";
31
+ function pluginLog(level, message, extra) {
32
+ const bridge = getLogBridge();
33
+ if (bridge) {
34
+ void bridge.client.app.log({
35
+ body: {
36
+ service: SERVICE,
37
+ level,
38
+ message,
39
+ ...extra ? { extra } : {}
40
+ },
41
+ ...bridge.directory ? { query: { directory: bridge.directory } } : {}
42
+ }).catch(() => {
43
+ });
44
+ return;
45
+ }
46
+ const line = extra ? `[${SERVICE}] ${message} ${JSON.stringify(extra)}` : `[${SERVICE}] ${message}`;
47
+ switch (level) {
48
+ case "debug":
49
+ console.debug(line);
50
+ break;
51
+ case "info":
52
+ console.info(line);
53
+ break;
54
+ case "warn":
55
+ console.warn(line);
56
+ break;
57
+ case "error":
58
+ console.error(line);
59
+ break;
60
+ }
61
+ }
62
+
19
63
  // src/provider/agent-backend.ts
20
64
  import { execSync } from "child_process";
21
65
  import { existsSync } from "fs";
@@ -36,6 +80,48 @@ async function loadCursorSdk() {
36
80
  return cached;
37
81
  }
38
82
 
83
+ // src/provider/cursor-log-intercept.ts
84
+ var ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
85
+ function stripAnsi(input) {
86
+ return input.replace(ANSI_PATTERN, "");
87
+ }
88
+ var RULE_LOAD_PATTERN = /^\d{2}:\d{2}:\d{2}\.\d{3}\s+INFO\s+(LocalCursorRulesService|AgentSkillsCursorRulesService|CursorPluginsAgentSkillsService) load completed(?:\s+ctx=\S+)?\s+meta=\{([^}]*)\}\s*$/;
89
+ function parseCursorLogMeta(raw) {
90
+ const out = {};
91
+ for (const part of raw.split(",")) {
92
+ const [key, value] = part.split(":").map((s) => s.trim());
93
+ if (!key || value === void 0) continue;
94
+ const num = Number(value);
95
+ if (Number.isFinite(num)) out[key] = num;
96
+ }
97
+ return out;
98
+ }
99
+ function parseCursorRuleLoadLine(line) {
100
+ const match = RULE_LOAD_PATTERN.exec(stripAnsi(line));
101
+ if (!match) return void 0;
102
+ const [, service, meta] = match;
103
+ if (!service) return void 0;
104
+ return { service, meta: parseCursorLogMeta(meta ?? "") };
105
+ }
106
+ var installed = false;
107
+ var original;
108
+ function installCursorLogInterceptor() {
109
+ if (installed) return;
110
+ original = console.log.bind(console);
111
+ const passthrough = original;
112
+ console.log = (...args) => {
113
+ if (args.length === 1 && typeof args[0] === "string") {
114
+ const parsed = parseCursorRuleLoadLine(args[0]);
115
+ if (parsed) {
116
+ pluginLog("info", `${parsed.service} load completed`, parsed.meta);
117
+ return;
118
+ }
119
+ }
120
+ passthrough(...args);
121
+ };
122
+ installed = true;
123
+ }
124
+
39
125
  // src/provider/sidecar-client.ts
40
126
  import { spawn } from "child_process";
41
127
  import { createInterface } from "readline";
@@ -120,6 +206,18 @@ var SidecarClient = class {
120
206
  } catch {
121
207
  return;
122
208
  }
209
+ if (msg["ev"] === "log") {
210
+ const level = msg["level"];
211
+ const message = msg["message"];
212
+ if (typeof level === "string" && typeof message === "string") {
213
+ this.options.onLog?.(
214
+ level,
215
+ message,
216
+ msg["meta"]
217
+ );
218
+ }
219
+ return;
220
+ }
123
221
  const id = msg["id"];
124
222
  if (typeof id !== "number") return;
125
223
  const pending = this.pending.get(id);
@@ -280,6 +378,7 @@ async function ensureHttp1Configured() {
280
378
  http1Configured = true;
281
379
  }
282
380
  function inProcessBackend(useHttp1) {
381
+ installCursorLogInterceptor();
283
382
  return {
284
383
  kind: "in-process",
285
384
  createAgent: async (options) => {
@@ -310,7 +409,11 @@ function resolveSidecarScript() {
310
409
  return void 0;
311
410
  }
312
411
  function sidecarBackend(nodePath, scriptPath) {
313
- const client = new SidecarClient({ scriptPath, nodePath });
412
+ const client = new SidecarClient({
413
+ scriptPath,
414
+ nodePath,
415
+ onLog: (level, message, meta) => pluginLog(level, message, meta)
416
+ });
314
417
  return {
315
418
  kind: "sidecar",
316
419
  createAgent: (options) => client.createAgent(options),
@@ -324,15 +427,18 @@ function loadAgentBackend() {
324
427
  const transport = resolveTransport(env);
325
428
  const scriptPath = transport === "sidecar" ? resolveSidecarScript() : void 0;
326
429
  if (transport === "sidecar" && (!env.nodePath || !scriptPath)) {
327
- console.error(
328
- `[opencode-cursor] Node sidecar requested but unavailable (node: ${env.nodePath ?? "not found"}, script: ${scriptPath ?? "not found"}); falling back to in-process HTTP/1.1 transport.`
430
+ pluginLog(
431
+ "warn",
432
+ "Node sidecar requested but unavailable; falling back to in-process HTTP/1.1 transport.",
433
+ { node: env.nodePath ?? null, script: scriptPath ?? null }
329
434
  );
330
435
  cached2 = inProcessBackend(true);
331
436
  return cached2;
332
437
  }
333
438
  if (transport === "http2-direct" && env.isBun) {
334
- console.error(
335
- "[opencode-cursor] http2-direct under Bun: Cursor streams may fail (Bun node:http2 incompatibility, oven-sh/bun#31499). Set OPENCODE_CURSOR_TRANSPORT=http1 (recommended) or sidecar."
439
+ pluginLog(
440
+ "warn",
441
+ "http2-direct under Bun: Cursor streams may fail (Bun node:http2 incompatibility, oven-sh/bun#31499). Set OPENCODE_CURSOR_TRANSPORT=http1 (recommended) or sidecar."
336
442
  );
337
443
  }
338
444
  cached2 = transport === "sidecar" && env.nodePath && scriptPath ? sidecarBackend(env.nodePath, scriptPath) : inProcessBackend(transport === "http1");
@@ -436,15 +542,15 @@ function resolveSystemDelivery(options) {
436
542
  }
437
543
 
438
544
  // src/provider/subagent-bridge.ts
439
- var BRIDGE_KEY = /* @__PURE__ */ Symbol.for("@stablekernel/opencode-cursor:subagent-bridge");
545
+ var BRIDGE_KEY2 = /* @__PURE__ */ Symbol.for("@stablekernel/opencode-cursor:subagent-bridge");
440
546
  function setSubagentBridge(bridge) {
441
- globalThis[BRIDGE_KEY] = bridge;
547
+ globalThis[BRIDGE_KEY2] = bridge;
442
548
  }
443
549
  function clearSubagentBridge() {
444
- delete globalThis[BRIDGE_KEY];
550
+ delete globalThis[BRIDGE_KEY2];
445
551
  }
446
552
  function getSubagentBridge() {
447
- return globalThis[BRIDGE_KEY];
553
+ return globalThis[BRIDGE_KEY2];
448
554
  }
449
555
  function isRecord(v) {
450
556
  return typeof v === "object" && v !== null;
@@ -590,6 +696,15 @@ function toolDisplayName(toolCall) {
590
696
  }
591
697
  return toolCall.type ?? "tool";
592
698
  }
699
+ var MAX_TIMEOUT_MS = 2147483647;
700
+ function envMs(name, fallback) {
701
+ const raw = process.env[name];
702
+ if (raw === void 0) return fallback;
703
+ if (raw === "") return 0;
704
+ const n = Number(raw);
705
+ if (!Number.isFinite(n)) return fallback;
706
+ return Math.min(n, MAX_TIMEOUT_MS);
707
+ }
593
708
  async function* streamAgentTurn(agent, message, options) {
594
709
  const queue = [];
595
710
  let wake;
@@ -597,10 +712,12 @@ async function* streamAgentTurn(agent, message, options) {
597
712
  let failure;
598
713
  const debug = process.env.OPENCODE_CURSOR_DEBUG === "1";
599
714
  const counts = {};
600
- const stallMs = Number(process.env.OPENCODE_CURSOR_STALL_MS ?? 6e4);
715
+ const stallMs = envMs("OPENCODE_CURSOR_STALL_MS", 12e4);
716
+ const toolStallMs = envMs("OPENCODE_CURSOR_TOOL_STALL_MS", 6e5);
601
717
  let stallTimer;
602
718
  let forced = false;
603
719
  let anyEvent = false;
720
+ const openTools = /* @__PURE__ */ new Map();
604
721
  const push = (event) => {
605
722
  anyEvent = true;
606
723
  queue.push(event);
@@ -610,10 +727,15 @@ async function* streamAgentTurn(agent, message, options) {
610
727
  };
611
728
  const armWatchdog = () => {
612
729
  if (stallMs <= 0 || finished) return;
730
+ const budget = openTools.size > 0 ? toolStallMs : stallMs;
613
731
  if (stallTimer) clearTimeout(stallTimer);
732
+ if (budget <= 0) {
733
+ stallTimer = void 0;
734
+ return;
735
+ }
614
736
  stallTimer = setTimeout(() => {
615
737
  void onStall();
616
- }, stallMs);
738
+ }, budget);
617
739
  stallTimer.unref?.();
618
740
  };
619
741
  const onDelta = ({ update }) => {
@@ -642,6 +764,7 @@ async function* streamAgentTurn(agent, message, options) {
642
764
  });
643
765
  break;
644
766
  case "tool-call-started":
767
+ openTools.set(String(update.callId), toolDisplayName(update.toolCall));
645
768
  push({
646
769
  type: "tool-call",
647
770
  id: String(update.callId),
@@ -650,6 +773,7 @@ async function* streamAgentTurn(agent, message, options) {
650
773
  });
651
774
  break;
652
775
  case "tool-call-completed": {
776
+ openTools.delete(String(update.callId));
653
777
  const tool = update.toolCall ?? {};
654
778
  const result = tool.result;
655
779
  const mcpError = tool.type === "mcp" && result?.value?.isError === true;
@@ -663,12 +787,14 @@ async function* streamAgentTurn(agent, message, options) {
663
787
  break;
664
788
  }
665
789
  case "turn-ended":
790
+ openTools.clear();
666
791
  if (update.usage) {
667
792
  const summed = addUsage(options.usageBase, update.usage);
668
793
  if (summed) push({ type: "usage", usage: summed });
669
794
  }
670
795
  break;
671
796
  }
797
+ armWatchdog();
672
798
  };
673
799
  const runHolder = {};
674
800
  const onAbort = () => {
@@ -697,9 +823,11 @@ async function* streamAgentTurn(agent, message, options) {
697
823
  });
698
824
  const result = await run.wait();
699
825
  if (debug) {
700
- console.error(
701
- `[cursor:debug] updates=${JSON.stringify(counts)} status=${result.status} resultLen=${(result.result ?? "").length}`
702
- );
826
+ pluginLog("debug", "turn finished", {
827
+ updates: counts,
828
+ status: result.status,
829
+ resultLen: (result.result ?? "").length
830
+ });
703
831
  }
704
832
  if (gen !== runGen || finished) return;
705
833
  if (result.status === "error") {
@@ -711,7 +839,11 @@ async function* streamAgentTurn(agent, message, options) {
711
839
  }).catch((err) => {
712
840
  if (gen !== runGen) return;
713
841
  failure = err;
714
- if (debug) console.error(`[cursor:debug] send failed: ${err instanceof Error ? err.message : String(err)}`);
842
+ if (debug) {
843
+ pluginLog("debug", "send failed", {
844
+ error: err instanceof Error ? err.message : String(err)
845
+ });
846
+ }
715
847
  }).finally(() => {
716
848
  if (gen !== runGen) return;
717
849
  finished = true;
@@ -736,7 +868,13 @@ async function* streamAgentTurn(agent, message, options) {
736
868
  wake = void 0;
737
869
  };
738
870
  if (anyEvent) {
739
- await failTerminal(`Cursor run stalled (no events for ${stallMs}ms)`);
871
+ const budget = openTools.size > 0 ? toolStallMs : stallMs;
872
+ const inFlight = [...openTools.values()];
873
+ const toolHint = inFlight.length > 0 ? `; tool${inFlight.length > 1 ? "s" : ""} ${inFlight.map((n) => `"${n}"`).join(", ")} still in flight` : "";
874
+ const knob = openTools.size > 0 ? "OPENCODE_CURSOR_TOOL_STALL_MS" : "OPENCODE_CURSOR_STALL_MS";
875
+ await failTerminal(
876
+ `Cursor run stalled (no events for ${budget}ms${toolHint}). Raise ${knob} (or set 0 to disable) if this legitimately runs longer.`
877
+ );
740
878
  return;
741
879
  }
742
880
  if (forced) {
@@ -744,11 +882,12 @@ async function* streamAgentTurn(agent, message, options) {
744
882
  return;
745
883
  }
746
884
  forced = true;
747
- if (debug) console.error("[cursor:debug] stream stalled; cancelling and resending with local.force");
885
+ if (debug) pluginLog("debug", "stream stalled; cancelling and resending with local.force");
748
886
  try {
749
887
  await runHolder.run?.cancel();
750
888
  } catch {
751
889
  }
890
+ openTools.clear();
752
891
  armWatchdog();
753
892
  startRun(true);
754
893
  };
@@ -781,14 +920,14 @@ async function sendWithRecovery(agent, message, sendOptions, debug) {
781
920
  } catch (err) {
782
921
  const classified = classifyError(err);
783
922
  if (classified.kind === "agent-busy") {
784
- if (debug) console.error("[cursor:debug] agent busy; retrying send with local.force");
923
+ if (debug) pluginLog("debug", "agent busy; retrying send with local.force");
785
924
  return agent.send(message, { ...sendOptions, local: { force: true } });
786
925
  }
787
926
  if ((classified.kind === "rate-limit" || classified.kind === "network") && attempt < RETRY_BACKOFF_MS.length) {
788
927
  if (debug)
789
- console.error(
790
- `[cursor:debug] ${classified.kind}; retrying send in ${RETRY_BACKOFF_MS[attempt]}ms`
791
- );
928
+ pluginLog("debug", `${classified.kind}; retrying send`, {
929
+ delayMs: RETRY_BACKOFF_MS[attempt]
930
+ });
792
931
  await sleep(RETRY_BACKOFF_MS[attempt]);
793
932
  continue;
794
933
  }
@@ -924,6 +1063,19 @@ function dropSessionRecord(sessionID) {
924
1063
  hydrate();
925
1064
  if (pool.delete(sessionID)) saveSessionRecords(pool);
926
1065
  }
1066
+ var sessionLocks = /* @__PURE__ */ new Map();
1067
+ function withSessionLock(sessionID, fn) {
1068
+ if (!sessionID) return fn();
1069
+ const prior = sessionLocks.get(sessionID) ?? Promise.resolve();
1070
+ const run = prior.then(fn, fn);
1071
+ const guarded = run.catch(() => {
1072
+ });
1073
+ sessionLocks.set(sessionID, guarded);
1074
+ void guarded.finally(() => {
1075
+ if (sessionLocks.get(sessionID) === guarded) sessionLocks.delete(sessionID);
1076
+ });
1077
+ return run;
1078
+ }
927
1079
  async function acquireAgent(params) {
928
1080
  const backend = loadAgentBackend();
929
1081
  const createOptions = {
@@ -979,6 +1131,9 @@ export {
979
1131
  resolveCursorApiKey,
980
1132
  fingerprintApiKey,
981
1133
  loadCursorSdk,
1134
+ setLogBridge,
1135
+ clearLogBridge,
1136
+ pluginLog,
982
1137
  setPreferredTransport,
983
1138
  extractSystemText,
984
1139
  removeSystemRule,
@@ -994,6 +1149,7 @@ export {
994
1149
  resolveControls,
995
1150
  getSessionRecord,
996
1151
  dropSessionRecord,
1152
+ withSessionLock,
997
1153
  acquireAgent
998
1154
  };
999
- //# sourceMappingURL=chunk-52IYLL6B.js.map
1155
+ //# sourceMappingURL=chunk-COE6ZXMP.js.map