@stablekernel/opencode-cursor 0.7.1-next.0 → 0.7.1-next.2

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,6 +4,52 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.1-next.2] — 2026-08-04 (pre-release)
8
+
9
+ Pre-release of the auto-compaction suppression (#92). Not yet on `latest`; install with
10
+ `npm install @stablekernel/opencode-cursor@next` to test.
11
+
12
+ - **opencode's threshold-triggered auto-compaction is now suppressed for Cursor models by
13
+ default.** The Cursor agent runtime already self-compacts on its own context threshold
14
+ (`preCompact` hook with `trigger: "auto"`), so opencode-driven compaction was redundant —
15
+ and it caused two real failures. First, the compaction turn runs with zero tools declared
16
+ while the Cursor agent uses its own tools anyway, which opencode rejects (`Tool call not
17
+ allowed while generating summary`) — mitigated in 0.7.1-next.1 (#91), and now avoided
18
+ entirely for the automatic trigger. Second, compaction rewrites the transcript, which
19
+ classifies as a divergence and mints a **fresh Cursor agentId** — and every distinct
20
+ agentId permanently holds a guarded SQLite `store.db`/`-wal`/`-shm` triple that
21
+ `agent.close()` cannot release (it only flushes analytics and releases the executor lease).
22
+ That descriptor growth fed an uncatchable `EXC_GUARD` process kill.
23
+
24
+ Suppression uses a large `limit.input` — the value opencode uses as its compaction
25
+ threshold — leaving the real `limit.context` intact so the TUI context gauge and cost
26
+ reporting still work. Manual `/compact` is unaffected and still relies on #91's fix.
27
+
28
+ **Tradeoff:** this suppresses the proactive threshold trigger only, and opencode has no
29
+ reactive context-overflow recovery wired up for this provider, so its transcript is no
30
+ longer trimmed automatically. Ordinary turns send only the new message, but a cold replay
31
+ (new session, expired agent, changed MCP set) resends everything; if that overflows the
32
+ model the turn fails and `/compact` is the manual recovery. Opt back out with
33
+ `provider.cursor.options.autoCompaction: true`.
34
+
35
+ ## [0.7.1-next.1] — 2026-08-03 (pre-release)
36
+
37
+ Pre-release of the compaction fix (#91). Not yet on `latest`; install with
38
+ `npm install @stablekernel/opencode-cursor@next` to test.
39
+
40
+ - **Fixed: auto-compaction (and manual `/compact`) failed with `Tool call not
41
+ allowed while generating summary` whenever the Cursor agent used a tool while
42
+ summarizing.** opencode declares zero tools on a compaction/summary turn, but
43
+ the Cursor agent runs its own tools regardless; the provider forwarded that
44
+ activity as provider-executed `tool-call` parts, which opencode's summary
45
+ guard rejects. The provider now routes no-tools turns through the existing
46
+ `"reasoning"` tool-display path, so Cursor's tool activity surfaces as
47
+ reasoning text instead of crossing the tool-execution boundary. Manual
48
+ `/compact` was affected all along; **auto**-compaction became reachable only
49
+ in 0.7.1-next.0, because #89 published real per-model context windows —
50
+ pre-0.7.1 opencode saw `limit.context: 0` for every Cursor model, and a zero
51
+ context limit structurally disables the auto-compaction trigger.
52
+
7
53
  ## [0.7.1-next.0] — 2026-08-03 (pre-release)
8
54
 
9
55
  Pre-release of the skills bridge (#90) and per-model context limits + pricing
@@ -30,17 +76,17 @@ Pre-release of the skills bridge (#90) and per-model context limits + pricing
30
76
  through opencode's structured plugin logging rather than the terminal,
31
77
  matching 0.7.0's logging change. `config.skills.urls` (HTTP catalogs) and
32
78
  skills bundled inside opencode plugin packages are not yet supported.
33
- - **Per-model context limits and pricing generated from Cursor's docs.** The
34
- plugin now ships a generated `src/model-limits.ts` (regenerated on schedule via
35
- `scripts/sync-model-limits.mjs`, with a CI drift check that fails the job if
36
- the committed data falls behind Cursor's published tables). Each resolved
37
- model carries a context-window limit and per-token pricing, emitted on the
38
- config channel opencode reads so the TUI can show cost and token counts.
39
- High-output frontier models carry a separate output-token limit. A
40
- `models` generator CLI (`scripts/sync-model-limits-cli.mjs`) is import-pure
41
- and calls `main()` unconditionally; the drift check captures stdout and
42
- requires the run summary line, so "exited 0 having done nothing" fails
43
- instead of passing for free.
79
+ Skills bridge contributed by Wayne Simpson (@WayneSimpson).
80
+ - **Per-model context limits and pricing generated from Cursor's docs.** Each
81
+ resolved model now carries a context-window limit and per-token pricing,
82
+ emitted on the config channel opencode reads so the TUI can show cost and
83
+ token counts. High-output frontier models carry a separate output-token
84
+ limit. The data ships as a generated `src/model-limits.ts`, produced from
85
+ Cursor's published tables by `npm run sync:model-limits`; a weekly CI job
86
+ re-checks the committed data against those tables and fails if it has fallen
87
+ behind (it verifies only regenerating is a manual step).
88
+ - **Dependency bumps:** `@ai-sdk/provider` 3.0.13 3.0.14, `@types/node`
89
+ 26.0.0 26.1.2, `vitest` 4.1.9 → 4.1.10 (dev-dependencies group, #88).
44
90
 
45
91
  ## [0.7.0] — 2026-07-30
46
92
 
package/README.md CHANGED
@@ -187,6 +187,7 @@ See [SECURITY.md](./SECURITY.md) for the full threat model.
187
187
  | `toolDisplay` | `"blocks"` | How Cursor's internal tool activity is shown — see [Tool display](#tool-display) |
188
188
  | `systemPrompt` | `"rules"` | How opencode's system prompt reaches the agent — see [System prompt](#system-prompt) |
189
189
  | `transport` | — | Cursor agent transport (`"http1"` \| `"http2-direct"` \| `"sidecar"`) — see [Transport](#transport) |
190
+ | `autoCompaction` | `false` | Let opencode drive auto-compaction. Off by default because the Cursor agent self-compacts — see [Compaction](#compaction) |
190
191
 
191
192
  | Environment variable | Default | Meaning |
192
193
  | --- | --- | --- |
@@ -413,12 +414,52 @@ open a PR.
413
414
  - **`"reasoning"`** — compact inline lines (`[tool] write {"path":…}`). Works on any host; use
414
415
  this on older opencode versions.
415
416
 
417
+ Turns where the host declares no tools at all — compaction/summary and title generation — always
418
+ use `"reasoning"` regardless of this setting. opencode rejects tool parts on a summary turn, so
419
+ Cursor's tool activity is folded into reasoning text there instead.
420
+
416
421
  To force the fallback:
417
422
 
418
423
  ```json
419
424
  { "provider": { "cursor": { "options": { "toolDisplay": "reasoning" } } } }
420
425
  ```
421
426
 
427
+ ## Compaction
428
+
429
+ **opencode's threshold-triggered auto-compaction is suppressed for Cursor models by default.** The
430
+ Cursor agent runtime compacts its own conversation as it approaches its context threshold, so a
431
+ second, opencode-driven pass is redundant — and it is actively harmful here:
432
+
433
+ - The compaction turn asks the model to summarize with **no tools available**. The Cursor agent runs
434
+ its own tools regardless, which opencode rejects outright
435
+ (`Tool call not allowed while generating summary`).
436
+ - Compaction rewrites the transcript, so the next turn no longer matches what the Cursor agent saw.
437
+ The plugin correctly treats that as a divergence and creates a **fresh Cursor agent** — and every
438
+ distinct agent permanently holds its own SQLite store open for the life of the process, which has
439
+ been observed to crash opencode outright.
440
+
441
+ The suppression works by emitting a very large `limit.input`, which is what opencode uses as its
442
+ compaction threshold. The real `limit.context` is left untouched, so the TUI's context-window gauge
443
+ and cost reporting keep working.
444
+
445
+ > [!IMPORTANT]
446
+ > This suppresses the **proactive** threshold trigger only, and opencode has no reactive
447
+ > context-overflow recovery wired up for this provider. In exchange, opencode's transcript is no
448
+ > longer trimmed automatically, so it grows for the life of the session. Ordinary turns send only
449
+ > the new message to an already-running agent, but a *cold replay* — a new session, an expired
450
+ > agent, or a changed MCP server set — resends the whole transcript. If that ever overflows the
451
+ > model, the turn fails with a provider error and the fix is to run `/compact` manually.
452
+ >
453
+ > Set `autoCompaction: true` if you would rather have opencode keep bounding the transcript for you.
454
+
455
+ Manual `/compact` is unaffected and still works — it has no threshold gate.
456
+
457
+ To hand compaction back to opencode:
458
+
459
+ ```json
460
+ { "provider": { "cursor": { "options": { "autoCompaction": true } } } }
461
+ ```
462
+
422
463
  ## Transport
423
464
 
424
465
  opencode runs on [Bun](https://bun.sh), whose `node:http2` client is incompatible with the Cursor
@@ -55,6 +55,7 @@ var MODEL_CONTEXT_LIMITS = {
55
55
  "grok-4.5": 256e3
56
56
  };
57
57
  var DEFAULT_CONTEXT_LIMIT = 2e5;
58
+ var NO_AUTO_COMPACTION_INPUT_LIMIT = 1e9;
58
59
  function resolveContextLimit(modelId) {
59
60
  let best;
60
61
  let bestLen = 0;
@@ -313,7 +314,7 @@ async function discoverModels(options = {}) {
313
314
  function modelSupportsReasoning(item) {
314
315
  return (item.parameters ?? []).some((p) => /think|reason/i.test(p.id));
315
316
  }
316
- function toOpencodeModels(items) {
317
+ function toOpencodeModels(items, opts = {}) {
317
318
  const out = {};
318
319
  for (const item of items) {
319
320
  const params = defaultModelParams(item);
@@ -329,6 +330,10 @@ function toOpencodeModels(items) {
329
330
  options: Object.keys(params).length > 0 ? { params } : {},
330
331
  limit: {
331
332
  context: resolveContextLimit(item.id),
333
+ // Suppress opencode's auto-compaction unless the user opts in: the
334
+ // Cursor agent self-compacts, and opencode's compaction mints a
335
+ // fresh agentId per cycle, permanently leaking guarded SQLite fds.
336
+ ...opts.autoCompaction ? {} : { input: NO_AUTO_COMPACTION_INPUT_LIMIT },
332
337
  output: resolveOutputLimit(item.id)
333
338
  },
334
339
  cost: {
@@ -348,7 +353,7 @@ var NPM_PACKAGE = "@stablekernel/opencode-cursor";
348
353
  function providerNpm() {
349
354
  return process.env.OPENCODE_CURSOR_PROVIDER_NPM?.trim() || NPM_PACKAGE;
350
355
  }
351
- function buildModelV2Map(items) {
356
+ function buildModelV2Map(items, opts = {}) {
352
357
  const out = {};
353
358
  for (const item of items) {
354
359
  const params = defaultModelParams(item);
@@ -370,7 +375,11 @@ function buildModelV2Map(items) {
370
375
  const c = resolveCost(item.id);
371
376
  return { input: c.input, output: c.output, cache: { read: c.cacheRead, write: c.cacheWrite } };
372
377
  })(),
373
- limit: { context: resolveContextLimit(item.id), output: resolveOutputLimit(item.id) },
378
+ limit: {
379
+ context: resolveContextLimit(item.id),
380
+ ...opts.autoCompaction ? {} : { input: NO_AUTO_COMPACTION_INPUT_LIMIT },
381
+ output: resolveOutputLimit(item.id)
382
+ },
374
383
  status: "active",
375
384
  options: Object.keys(params).length > 0 ? { params } : {},
376
385
  headers: {},
@@ -785,7 +794,7 @@ function clearVersionCache() {
785
794
  }
786
795
  }
787
796
  function getLocalVersion() {
788
- if (true) return "0.7.1-next.0";
797
+ if (true) return "0.7.1-next.2";
789
798
  try {
790
799
  const require2 = createRequire(import.meta.url);
791
800
  const pkg = require2("../package.json");
@@ -1473,6 +1482,7 @@ var CursorPlugin = async (input) => {
1473
1482
  let resolvedCwd = directory ?? process.cwd();
1474
1483
  let forwardMcp = true;
1475
1484
  let userMcp = {};
1485
+ let autoCompaction = false;
1476
1486
  let forwardSkills = true;
1477
1487
  let skillFilterOptions;
1478
1488
  let lastSkillHash = "";
@@ -1505,6 +1515,7 @@ var CursorPlugin = async (input) => {
1505
1515
  config.provider ??= {};
1506
1516
  const existing = config.provider[PROVIDER_ID] ?? {};
1507
1517
  const existingOptions = existing.options ?? {};
1518
+ autoCompaction = existingOptions["autoCompaction"] === true;
1508
1519
  forwardMcp = existingOptions["forwardMcp"] !== false;
1509
1520
  userMcp = existingOptions["mcpServers"] ?? {};
1510
1521
  const mcpServers = forwardMcp ? { ...userMcp, ...translateMcpServers(config.mcp) } : userMcp;
@@ -1558,7 +1569,10 @@ var CursorPlugin = async (input) => {
1558
1569
  ...Object.keys(modelParamDefaults).length > 0 ? { modelParamDefaults } : {},
1559
1570
  ...currentSkillsCatalogue ? { skillsCatalogue: currentSkillsCatalogue } : {}
1560
1571
  },
1561
- models: { ...toOpencodeModels(models), ...existing.models ?? {} }
1572
+ models: {
1573
+ ...toOpencodeModels(models, { autoCompaction }),
1574
+ ...existing.models ?? {}
1575
+ }
1562
1576
  };
1563
1577
  },
1564
1578
  provider: {
@@ -1566,7 +1580,7 @@ var CursorPlugin = async (input) => {
1566
1580
  models: async (_provider, ctx) => {
1567
1581
  const apiKey = apiKeyFromAuth(ctx.auth);
1568
1582
  const { models } = await discoverModels({ apiKey });
1569
- return buildModelV2Map(models);
1583
+ return buildModelV2Map(models, { autoCompaction });
1570
1584
  }
1571
1585
  },
1572
1586
  // Bridge opencode's session id to the provider: it lands in