@stablekernel/opencode-cursor 0.6.2 → 0.7.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 +53 -0
- package/README.md +16 -5
- package/dist/{chunk-52IYLL6B.js → chunk-COE6ZXMP.js} +178 -22
- package/dist/chunk-COE6ZXMP.js.map +1 -0
- package/dist/plugin/index.js +12 -3
- package/dist/plugin/index.js.map +1 -1
- package/dist/provider/index.js +102 -88
- package/dist/provider/index.js.map +1 -1
- package/dist/sidecar/agent-host.d.ts +43 -0
- package/dist/sidecar/agent-host.js +29 -0
- package/dist/sidecar/agent-host.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-52IYLL6B.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,59 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.0] — 2026-07-30
|
|
8
|
+
|
|
9
|
+
Structured logging (#85), the stream-watchdog tool-phase budget (#86), and the
|
|
10
|
+
session-pool title-generation race fix (#84).
|
|
11
|
+
|
|
12
|
+
- **Structured logging via `client.app.log()` instead of raw `console.*`.** The
|
|
13
|
+
plugin's own diagnostics (transport fallback warnings, per-turn debug traces
|
|
14
|
+
gated on `OPENCODE_CURSOR_DEBUG=1`) now route through opencode's plugin
|
|
15
|
+
logging API (`service: "opencode-cursor"`) rather than `console.warn`/
|
|
16
|
+
`console.error`. Falls back to `console.*` when no client is available
|
|
17
|
+
(e.g. running the provider standalone).
|
|
18
|
+
- **Cursor SDK's own "rules"/"skills" load diagnostics captured and forwarded.**
|
|
19
|
+
`@cursor/sdk`'s bundled local-exec runtime writes internal messages like
|
|
20
|
+
`LocalCursorRulesService load completed meta={durationMs, ruleCount}` and
|
|
21
|
+
`AgentSkillsCursorRulesService load completed meta={durationMs, ruleCount,
|
|
22
|
+
skillCount}` straight to `console.log`, with no public logger hook to
|
|
23
|
+
redirect it. These are now recognized (in-process transport via a narrowly
|
|
24
|
+
scoped `console.log` interceptor; sidecar transport via the child process's
|
|
25
|
+
own interceptor forwarding over the existing JSONL protocol) and re-emitted
|
|
26
|
+
as structured opencode logs instead of raw terminal noise. Every other
|
|
27
|
+
`console.log` call passes through unchanged.
|
|
28
|
+
- **Fixed: the stream watchdog killed healthy runs during long tool execution.** The watchdog
|
|
29
|
+
re-armed only on mapped event types, so a long shell command, build, or test suite that streamed
|
|
30
|
+
nothing for 60s was cancelled and the turn lost. It now uses two budgets — an idle budget
|
|
31
|
+
(`OPENCODE_CURSOR_STALL_MS`, default raised to `120000`) and a larger tool-phase budget
|
|
32
|
+
(`OPENCODE_CURSOR_TOOL_STALL_MS`, default `600000`) applied while a tool call is in flight — and
|
|
33
|
+
re-arms on **any** SDK update, including types the plugin doesn't model (progress/heartbeats). A
|
|
34
|
+
tool-phase stall is terminal and names the in-flight tool. `OPENCODE_CURSOR_STALL_MS=0` still
|
|
35
|
+
disables the whole watchdog; the tool-phase bound is independently disabled with
|
|
36
|
+
`OPENCODE_CURSOR_TOOL_STALL_MS=0`. Open tool calls are reconciled on `turn-ended` and on a forced
|
|
37
|
+
resend, so a dropped completion can't pin a turn to the 10-minute budget.
|
|
38
|
+
- **Fixed: a non-numeric `OPENCODE_CURSOR_STALL_MS` stalled every turn immediately.**
|
|
39
|
+
`Number("abc")` is `NaN`; `NaN <= 0` is `false`, so the guard passed and `setTimeout(fn, NaN)`
|
|
40
|
+
fired at once. Env parsing now falls back to the default for non-finite values (an empty string
|
|
41
|
+
still disables, preserving the historical escape hatch).
|
|
42
|
+
- **Fixed: an over-large stall budget overflowed to a ~1 ms deadline.** A `setTimeout` delay is
|
|
43
|
+
stored as a signed 32-bit int, so anything above `2147483647` is silently clamped to `1` — and the
|
|
44
|
+
tool-phase stall message tells operators to *raise* `OPENCODE_CURSOR_TOOL_STALL_MS`, making the
|
|
45
|
+
trap reachable by following the plugin's own advice. Setting it to e.g. `999999999999` stalled
|
|
46
|
+
every tool-bearing turn within milliseconds while reporting `no events for 999999999999ms`. Both
|
|
47
|
+
budgets are now capped at `2147483647`.
|
|
48
|
+
- **Fixed: opencode's title-generation call could poison a session's pool entry.** opencode forks a
|
|
49
|
+
title-generation call on the same `sessionID` as the session's real first turn, concurrently and
|
|
50
|
+
with an empty system prompt. `classifyTurn`'s side-call detection only fires once a prior pool
|
|
51
|
+
record exists, so on turn 1 both calls classified as "new" and both wrote to the pool — whichever
|
|
52
|
+
agent-creation round-trip resolved last silently overwrote the other, leaving the session
|
|
53
|
+
fingerprinted against the title prompt. Two fixes: the plugin's `chat.params` hook now marks
|
|
54
|
+
opencode's `title` agent call as `providerOptions.cursor.ephemeral = true` (the provider already
|
|
55
|
+
honored this flag but nothing set it), and `withSessionLock` (a per-`sessionID` async lock) now
|
|
56
|
+
wraps `agentRun`'s classify-then-acquire span so concurrent turns for one session serialize and
|
|
57
|
+
the second call always observes the first's completed pool write.
|
|
58
|
+
- **Dependency bumps:** `@cursor/sdk` 1.0.24 → 1.0.26, `@opencode-ai/plugin` (opencode-ai group).
|
|
59
|
+
|
|
7
60
|
## [0.6.2] — 2026-07-28
|
|
8
61
|
|
|
9
62
|
Version-check UX cleanup from #79.
|
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` | `
|
|
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
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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({
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
335
|
-
"
|
|
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
|
|
545
|
+
var BRIDGE_KEY2 = /* @__PURE__ */ Symbol.for("@stablekernel/opencode-cursor:subagent-bridge");
|
|
440
546
|
function setSubagentBridge(bridge) {
|
|
441
|
-
globalThis[
|
|
547
|
+
globalThis[BRIDGE_KEY2] = bridge;
|
|
442
548
|
}
|
|
443
549
|
function clearSubagentBridge() {
|
|
444
|
-
delete globalThis[
|
|
550
|
+
delete globalThis[BRIDGE_KEY2];
|
|
445
551
|
}
|
|
446
552
|
function getSubagentBridge() {
|
|
447
|
-
return globalThis[
|
|
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 =
|
|
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
|
-
},
|
|
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
|
-
|
|
701
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
790
|
-
|
|
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-
|
|
1155
|
+
//# sourceMappingURL=chunk-COE6ZXMP.js.map
|