opencode-anthropic-fix 0.0.35 → 0.0.37

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.
Files changed (3) hide show
  1. package/index.mjs +446 -9
  2. package/lib/config.mjs +13 -0
  3. package/package.json +1 -1
package/index.mjs CHANGED
@@ -148,6 +148,16 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
148
148
  const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1000;
149
149
  const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1000;
150
150
 
151
+ // Willow Mode: detect inactivity and suggest context reset.
152
+ // Named after the willow tree — when idle, the session "droops" and a gentle
153
+ // nudge suggests starting fresh rather than accumulating stale context.
154
+ const WILLOW_ENABLED = config.willow_mode?.enabled ?? true;
155
+ const WILLOW_IDLE_THRESHOLD_MS = (config.willow_mode?.idle_threshold_minutes ?? 30) * 60 * 1000;
156
+ const WILLOW_COOLDOWN_MS = (config.willow_mode?.cooldown_minutes ?? 60) * 60 * 1000;
157
+ const WILLOW_MIN_TURNS = config.willow_mode?.min_turns_before_suggest ?? 3;
158
+ let willowLastRequestTime = Date.now();
159
+ let willowLastSuggestionTime = 0;
160
+
151
161
  /**
152
162
  * Whether OPENCODE_ANTHROPIC_INITIAL_ACCOUNT env var pinned this session to a
153
163
  * specific account. When true, syncActiveIndexFromDisk is skipped and strategy
@@ -882,7 +892,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
882
892
  "Toggleable presets:",
883
893
  " /anthropic betas add structured-outputs-2025-12-15",
884
894
  " /anthropic betas add context-management-2025-06-27",
885
- " /anthropic betas add tool-examples-2025-10-29",
895
+ " /anthropic betas add task-budgets-2026-03-13",
886
896
  " /anthropic betas add web-search-2025-03-05",
887
897
  " /anthropic betas add compact-2026-01-12",
888
898
  " /anthropic betas add mcp-servers-2025-12-04",
@@ -1294,6 +1304,363 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
1294
1304
  }
1295
1305
  }
1296
1306
 
1307
+ // /anthropic review [pr <number>|branch <name>|status] — Claude Code Review (Bughunter) results
1308
+ if (primary === "review") {
1309
+ const action = (args[1] || "").toLowerCase();
1310
+
1311
+ /**
1312
+ * Execute a shell command and return { stdout, stderr, code }.
1313
+ * @param {string} cmd
1314
+ * @param {string[]} cmdArgs
1315
+ * @returns {Promise<{ stdout: string, stderr: string, code: number }>}
1316
+ */
1317
+ async function execShell(cmd, cmdArgs) {
1318
+ const { execFile } = await import("node:child_process");
1319
+ return new Promise((resolve) => {
1320
+ execFile(cmd, cmdArgs, { timeout: 30_000, maxBuffer: 2 * 1024 * 1024 }, (err, stdout, stderr) => {
1321
+ resolve({
1322
+ stdout: (stdout || "").trim(),
1323
+ stderr: (stderr || "").trim(),
1324
+ code: err ? err.code || 1 : 0,
1325
+ });
1326
+ });
1327
+ });
1328
+ }
1329
+
1330
+ /**
1331
+ * Parse bughunter severity from check run output text.
1332
+ * @param {string} text
1333
+ * @returns {{ normal: number, nit: number, pre_existing: number } | null}
1334
+ */
1335
+ function parseBughunterSeverity(text) {
1336
+ const m = text.match(/bughunter-severity:\s*(\{[^}]+\})/);
1337
+ if (!m) return null;
1338
+ try {
1339
+ return JSON.parse(m[1]);
1340
+ } catch {
1341
+ return null;
1342
+ }
1343
+ }
1344
+
1345
+ /**
1346
+ * Format a severity object into a human-readable string.
1347
+ * @param {{ normal: number, nit: number, pre_existing: number }} sev
1348
+ */
1349
+ function formatSeverity(sev) {
1350
+ const parts = [];
1351
+ if (sev.normal > 0) parts.push(`🔴 Important: ${sev.normal}`);
1352
+ if (sev.nit > 0) parts.push(`🟡 Nit: ${sev.nit}`);
1353
+ if (sev.pre_existing > 0) parts.push(`🟣 Pre-existing: ${sev.pre_existing}`);
1354
+ if (parts.length === 0) parts.push("No issues found");
1355
+ return parts.join(" | ");
1356
+ }
1357
+
1358
+ // Check gh CLI availability
1359
+ const ghCheck = await execShell("gh", ["--version"]);
1360
+ if (ghCheck.code !== 0) {
1361
+ await sendCommandMessage(
1362
+ input.sessionID,
1363
+ "▣ Anthropic Review (error)\n\nGitHub CLI (gh) not found. Install it from https://cli.github.com/",
1364
+ );
1365
+ return;
1366
+ }
1367
+
1368
+ // Detect current repo
1369
+ const repoResult = await execShell("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]);
1370
+ if (repoResult.code !== 0 || !repoResult.stdout) {
1371
+ await sendCommandMessage(
1372
+ input.sessionID,
1373
+ "▣ Anthropic Review (error)\n\nCould not detect GitHub repository. Ensure you are in a git repo with a GitHub remote.",
1374
+ );
1375
+ return;
1376
+ }
1377
+ const repo = repoResult.stdout.trim();
1378
+
1379
+ try {
1380
+ // /anthropic review status — check if code review is set up for this repo
1381
+ if (action === "status") {
1382
+ // Check for recent check runs named "Claude Code Review"
1383
+ const checkResult = await execShell("gh", [
1384
+ "api",
1385
+ `repos/${repo}/commits/HEAD/check-runs`,
1386
+ "--jq",
1387
+ '.check_runs[] | select(.name | test("claude|bughunter"; "i")) | .name + " — " + .status + " (" + .conclusion + ")"',
1388
+ ]);
1389
+ const lines = ["▣ Anthropic Review — Status", "", `Repository: ${repo}`, ""];
1390
+ if (checkResult.stdout) {
1391
+ lines.push("Recent Claude check runs:", checkResult.stdout);
1392
+ } else {
1393
+ lines.push(
1394
+ "No Claude Code Review check runs found on HEAD.",
1395
+ "",
1396
+ "Code Review must be enabled by an admin at claude.ai/admin-settings.",
1397
+ "It requires a Teams or Enterprise subscription.",
1398
+ );
1399
+ }
1400
+ await sendCommandMessage(input.sessionID, lines.join("\n"));
1401
+ return;
1402
+ }
1403
+
1404
+ // /anthropic review pr [<number>] — get review results for a PR
1405
+ if (!action || action === "pr") {
1406
+ const prNumber = args[2] ? parseInt(args[2], 10) : null;
1407
+
1408
+ // If no PR number, find the current branch's PR
1409
+ let prRef;
1410
+ if (prNumber) {
1411
+ prRef = String(prNumber);
1412
+ } else {
1413
+ const branchResult = await execShell("git", ["branch", "--show-current"]);
1414
+ const currentBranch = branchResult.stdout.trim();
1415
+ if (!currentBranch) {
1416
+ await sendCommandMessage(
1417
+ input.sessionID,
1418
+ "▣ Anthropic Review (error)\n\nDetached HEAD — specify a PR number: /anthropic review pr <number>",
1419
+ );
1420
+ return;
1421
+ }
1422
+ // Find PR for current branch
1423
+ const prLookup = await execShell("gh", [
1424
+ "pr",
1425
+ "list",
1426
+ "--head",
1427
+ currentBranch,
1428
+ "--json",
1429
+ "number,title,state",
1430
+ "--limit",
1431
+ "1",
1432
+ ]);
1433
+ if (prLookup.code !== 0 || !prLookup.stdout || prLookup.stdout === "[]") {
1434
+ await sendCommandMessage(
1435
+ input.sessionID,
1436
+ `▣ Anthropic Review (error)\n\nNo PR found for branch "${currentBranch}".\nUse: /anthropic review pr <number>`,
1437
+ );
1438
+ return;
1439
+ }
1440
+ const prs = JSON.parse(prLookup.stdout);
1441
+ if (!prs.length) {
1442
+ await sendCommandMessage(
1443
+ input.sessionID,
1444
+ `▣ Anthropic Review (error)\n\nNo PR found for branch "${currentBranch}".`,
1445
+ );
1446
+ return;
1447
+ }
1448
+ prRef = String(prs[0].number);
1449
+ }
1450
+
1451
+ // Get check runs for the PR's head SHA
1452
+ const prData = await execShell("gh", ["pr", "view", prRef, "--json", "number,title,headRefOid,state,url"]);
1453
+ if (prData.code !== 0) {
1454
+ await sendCommandMessage(
1455
+ input.sessionID,
1456
+ `▣ Anthropic Review (error)\n\nCould not fetch PR #${prRef}: ${prData.stderr}`,
1457
+ );
1458
+ return;
1459
+ }
1460
+ const pr = JSON.parse(prData.stdout);
1461
+ const sha = pr.headRefOid;
1462
+
1463
+ // Fetch check runs for this SHA
1464
+ const checksResult = await execShell("gh", [
1465
+ "api",
1466
+ `repos/${repo}/commits/${sha}/check-runs`,
1467
+ "--jq",
1468
+ '.check_runs[] | select(.name | test("claude|bughunter"; "i"))',
1469
+ ]);
1470
+
1471
+ const lines = [
1472
+ "▣ Anthropic Review",
1473
+ "",
1474
+ `PR #${pr.number}: ${pr.title}`,
1475
+ `State: ${pr.state} | Commit: ${sha.slice(0, 8)}`,
1476
+ `URL: ${pr.url}`,
1477
+ "",
1478
+ ];
1479
+
1480
+ if (!checksResult.stdout) {
1481
+ lines.push(
1482
+ "No Claude Code Review check runs found for this PR.",
1483
+ "",
1484
+ "Possible reasons:",
1485
+ " • Code Review not enabled for this repository",
1486
+ " • Review still in progress (avg ~20 min)",
1487
+ " • PR is a draft (drafts are not auto-reviewed)",
1488
+ );
1489
+ await sendCommandMessage(input.sessionID, lines.join("\n"));
1490
+ return;
1491
+ }
1492
+
1493
+ // Parse all check runs (could be multiple)
1494
+ const checkRunsRaw = `[${checksResult.stdout.split("\n}\n").join("},\n")}]`
1495
+ .replace(/,\s*]$/, "]")
1496
+ .replace(/}\s*{/g, "},{");
1497
+ let checkRuns;
1498
+ try {
1499
+ checkRuns = JSON.parse(checkRunsRaw);
1500
+ if (!Array.isArray(checkRuns)) checkRuns = [checkRuns];
1501
+ } catch {
1502
+ // Single object
1503
+ try {
1504
+ checkRuns = [JSON.parse(checksResult.stdout)];
1505
+ } catch {
1506
+ lines.push(
1507
+ "Found check run(s) but could not parse output.",
1508
+ "",
1509
+ "Raw:",
1510
+ checksResult.stdout.slice(0, 500),
1511
+ );
1512
+ await sendCommandMessage(input.sessionID, lines.join("\n"));
1513
+ return;
1514
+ }
1515
+ }
1516
+
1517
+ for (const run of checkRuns) {
1518
+ lines.push(`Check: ${run.name}`);
1519
+ lines.push(` Status: ${run.status} | Conclusion: ${run.conclusion || "pending"}`);
1520
+ if (run.html_url) lines.push(` Details: ${run.html_url}`);
1521
+
1522
+ // Parse bughunter severity
1523
+ const outputText = run.output?.text || "";
1524
+ const severity = parseBughunterSeverity(outputText);
1525
+ if (severity) {
1526
+ lines.push(` Findings: ${formatSeverity(severity)}`);
1527
+ const total = severity.normal + severity.nit + severity.pre_existing;
1528
+ lines.push(` Total: ${total} issue${total !== 1 ? "s" : ""}`);
1529
+ } else if (run.status === "completed") {
1530
+ lines.push(" Findings: No bughunter-severity data in output");
1531
+ } else {
1532
+ lines.push(" Review is still in progress...");
1533
+ }
1534
+ lines.push("");
1535
+ }
1536
+
1537
+ await sendCommandMessage(input.sessionID, lines.join("\n"));
1538
+ return;
1539
+ }
1540
+
1541
+ // /anthropic review branch [<name>] — find PR for branch and show review
1542
+ if (action === "branch") {
1543
+ const branchName = args[2] || (await execShell("git", ["branch", "--show-current"])).stdout.trim();
1544
+ if (!branchName) {
1545
+ await sendCommandMessage(
1546
+ input.sessionID,
1547
+ "▣ Anthropic Review (error)\n\nNo branch specified and HEAD is detached.",
1548
+ );
1549
+ return;
1550
+ }
1551
+
1552
+ const prLookup = await execShell("gh", [
1553
+ "pr",
1554
+ "list",
1555
+ "--head",
1556
+ branchName,
1557
+ "--json",
1558
+ "number,title,state,headRefOid,url",
1559
+ "--limit",
1560
+ "5",
1561
+ ]);
1562
+ if (prLookup.code !== 0 || !prLookup.stdout || prLookup.stdout === "[]") {
1563
+ await sendCommandMessage(
1564
+ input.sessionID,
1565
+ `▣ Anthropic Review (error)\n\nNo PRs found for branch "${branchName}".`,
1566
+ );
1567
+ return;
1568
+ }
1569
+ const prs = JSON.parse(prLookup.stdout);
1570
+ if (!prs.length) {
1571
+ await sendCommandMessage(
1572
+ input.sessionID,
1573
+ `▣ Anthropic Review (error)\n\nNo PRs found for branch "${branchName}".`,
1574
+ );
1575
+ return;
1576
+ }
1577
+
1578
+ const lines = ["▣ Anthropic Review — Branch", "", `Branch: ${branchName}`, ""];
1579
+
1580
+ for (const pr of prs) {
1581
+ lines.push(`PR #${pr.number}: ${pr.title} (${pr.state})`);
1582
+
1583
+ // Fetch check runs
1584
+ const checksResult = await execShell("gh", [
1585
+ "api",
1586
+ `repos/${repo}/commits/${pr.headRefOid}/check-runs`,
1587
+ "--jq",
1588
+ '.check_runs[] | select(.name | test("claude|bughunter"; "i"))',
1589
+ ]);
1590
+
1591
+ if (!checksResult.stdout) {
1592
+ lines.push(" No Claude Code Review check runs found.", "");
1593
+ continue;
1594
+ }
1595
+
1596
+ // Try to parse individual run
1597
+ let checkRuns;
1598
+ try {
1599
+ const raw = `[${checksResult.stdout.split("\n}\n").join("},\n")}]`
1600
+ .replace(/,\s*]$/, "]")
1601
+ .replace(/}\s*{/g, "},{");
1602
+ checkRuns = JSON.parse(raw);
1603
+ if (!Array.isArray(checkRuns)) checkRuns = [checkRuns];
1604
+ } catch {
1605
+ try {
1606
+ checkRuns = [JSON.parse(checksResult.stdout)];
1607
+ } catch {
1608
+ lines.push(" Could not parse check run output.", "");
1609
+ continue;
1610
+ }
1611
+ }
1612
+
1613
+ for (const run of checkRuns) {
1614
+ lines.push(` Check: ${run.name} — ${run.status} (${run.conclusion || "pending"})`);
1615
+ const outputText = run.output?.text || "";
1616
+ const severity = parseBughunterSeverity(outputText);
1617
+ if (severity) {
1618
+ lines.push(` ${formatSeverity(severity)}`);
1619
+ }
1620
+ }
1621
+ lines.push("");
1622
+ }
1623
+
1624
+ await sendCommandMessage(input.sessionID, lines.join("\n"));
1625
+ return;
1626
+ }
1627
+
1628
+ // /anthropic review help
1629
+ const helpLines = [
1630
+ "▣ Anthropic Review (Claude Code Review / Bughunter)",
1631
+ "",
1632
+ "Fetch and display code review results from Claude's automated PR reviewer.",
1633
+ "",
1634
+ "Usage:",
1635
+ " /anthropic review Review for current branch's PR",
1636
+ " /anthropic review pr <number> Review for a specific PR",
1637
+ " /anthropic review branch [<name>] Review for PRs on a branch",
1638
+ " /anthropic review status Check if review is configured",
1639
+ "",
1640
+ "Severity levels:",
1641
+ " 🔴 Important — bugs that should be fixed before merge",
1642
+ " 🟡 Nit — minor issues, worth fixing but not blocking",
1643
+ " 🟣 Pre-existing — bugs in codebase not introduced by this PR",
1644
+ "",
1645
+ "Requirements:",
1646
+ " • GitHub CLI (gh) must be installed and authenticated",
1647
+ " • Code Review must be enabled at claude.ai/admin-settings",
1648
+ " • Requires Teams or Enterprise subscription",
1649
+ "",
1650
+ "Machine-readable severity from check runs:",
1651
+ ' gh api repos/OWNER/REPO/check-runs/ID --jq \'.output.text | split("bughunter-severity: ")[1] | split(" -->")[0] | fromjson\'',
1652
+ ];
1653
+ await sendCommandMessage(input.sessionID, helpLines.join("\n"));
1654
+ return;
1655
+ } catch (err) {
1656
+ await sendCommandMessage(
1657
+ input.sessionID,
1658
+ `▣ Anthropic Review (error)\n\n${err instanceof Error ? err.message : String(err)}`,
1659
+ );
1660
+ return;
1661
+ }
1662
+ }
1663
+
1297
1664
  // Interactive CLI command is not compatible with slash flow.
1298
1665
  if (primary === "manage" || primary === "mg") {
1299
1666
  await sendCommandMessage(
@@ -1653,12 +2020,16 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
1653
2020
  if (!input.command) input.command = {};
1654
2021
  input.command["anthropic"] = {
1655
2022
  template: "/anthropic",
1656
- description: "Manage Anthropic auth, config, and betas (usage, login, config, set, betas, switch)",
2023
+ description: "Manage Anthropic auth, config, betas, review (usage, login, config, set, betas, review, switch)",
1657
2024
  };
1658
2025
  },
1659
2026
  "command.execute.before": async (input, output) => {
1660
2027
  if (input.command !== "anthropic") return;
1661
2028
 
2029
+ // Signal that this command is fully handled by the plugin —
2030
+ // do NOT forward it to the agent for further processing.
2031
+ output.noReply = true;
2032
+
1662
2033
  try {
1663
2034
  await handleAnthropicSlashCommand(input);
1664
2035
  } catch (err) {
@@ -1788,6 +2159,26 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
1788
2159
  await accountManager.syncActiveIndexFromDisk();
1789
2160
  }
1790
2161
 
2162
+ // Willow Mode: if the session has been idle for longer than the
2163
+ // configured threshold and has enough turns, show a gentle toast
2164
+ // suggesting the user consider starting a fresh context.
2165
+ if (WILLOW_ENABLED && showUsageToast) {
2166
+ const now = Date.now();
2167
+ const idleMs = now - willowLastRequestTime;
2168
+ const cooldownOk = now - willowLastSuggestionTime >= WILLOW_COOLDOWN_MS;
2169
+ if (idleMs >= WILLOW_IDLE_THRESHOLD_MS && cooldownOk && sessionMetrics.turns >= WILLOW_MIN_TURNS) {
2170
+ const idleMin = Math.round(idleMs / 60_000);
2171
+ willowLastSuggestionTime = now;
2172
+ toast(
2173
+ `🌿 Idle for ${idleMin}m with ${sessionMetrics.turns} turns of context. Consider /clear for a fresh start.`,
2174
+ "info",
2175
+ { debounceKey: "willow-idle" },
2176
+ ).catch(() => {});
2177
+ debugLog("willow mode: idle return detected", { idleMin, turns: sessionMetrics.turns });
2178
+ }
2179
+ willowLastRequestTime = now;
2180
+ }
2181
+
1791
2182
  // Try each account at most once. If the error is account-specific,
1792
2183
  // switch to the next account. If it's service-wide, return immediately.
1793
2184
  // QA fix M9: use enabled account count, not total (disabled accounts can't serve requests)
@@ -2060,9 +2451,41 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2060
2451
  ...requestInit,
2061
2452
  body,
2062
2453
  headers: requestHeaders,
2454
+ // Disable keepalive when a previous ECONNRESET/EPIPE was detected
2455
+ // to force a fresh TCP connection and avoid stale socket reuse.
2456
+ ...(requestInit._disableKeepalive ? { keepalive: false, agent: false } : {}),
2063
2457
  });
2064
2458
  } catch (err) {
2065
2459
  const fetchError = err instanceof Error ? err : new Error(String(err));
2460
+ const errMsg = fetchError.message || "";
2461
+ const errCode = /** @type {any} */ (fetchError).code || "";
2462
+
2463
+ // ECONNRESET/EPIPE recovery: these indicate a stale TCP connection
2464
+ // (server closed it while we were writing/reading). Disable keepalive
2465
+ // on the next attempt to force a fresh connection.
2466
+ const isConnectionReset =
2467
+ errCode === "ECONNRESET" ||
2468
+ errCode === "EPIPE" ||
2469
+ errCode === "ECONNABORTED" ||
2470
+ errMsg.includes("ECONNRESET") ||
2471
+ errMsg.includes("EPIPE") ||
2472
+ errMsg.includes("socket hang up") ||
2473
+ errMsg.includes("network socket disconnected");
2474
+
2475
+ if (isConnectionReset) {
2476
+ requestInit._disableKeepalive = true;
2477
+ debugLog("connection reset detected, disabling keepalive for retry", {
2478
+ code: errCode,
2479
+ message: errMsg,
2480
+ });
2481
+ // Don't mark the account as failed — this is a transport issue, not auth.
2482
+ // Retry the same account with keepalive disabled.
2483
+ if (accountManager && account) {
2484
+ lastError = fetchError;
2485
+ attempt--; // Don't consume an account slot
2486
+ continue;
2487
+ }
2488
+ }
2066
2489
 
2067
2490
  if (accountManager && account) {
2068
2491
  accountManager.markFailure(account);
@@ -3133,17 +3556,21 @@ process.once("beforeExit", () => {
3133
3556
  // Request building helpers (extracted from original fetch interceptor)
3134
3557
  // ---------------------------------------------------------------------------
3135
3558
 
3136
- const FALLBACK_CLAUDE_CLI_VERSION = "2.1.84";
3559
+ const FALLBACK_CLAUDE_CLI_VERSION = "2.1.88";
3137
3560
  const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
3138
- const CLAUDE_CODE_BUILD_TIME = "2026-03-25T23:48:41Z";
3561
+ const CLAUDE_CODE_BUILD_TIME = "2026-03-30T21:59:52Z";
3139
3562
 
3140
- // The @anthropic-ai/sdk version bundled with Claude Code v2.1.84.
3563
+ // The @anthropic-ai/sdk version bundled with Claude Code v2.1.88.
3141
3564
  // This is distinct from the CLI version and goes in X-Stainless-Package-Version.
3142
- // Verified by extracting VERSION="0.208.0" from the bundled cli.js of v2.1.84.
3565
+ // Verified by extracting VERSION="0.208.0" from the bundled cli.js of all versions .83-.88.
3143
3566
  const ANTHROPIC_SDK_VERSION = "0.208.0";
3144
3567
 
3145
3568
  // Map of CLI version → bundled SDK version (update when CLI version changes)
3146
3569
  const CLI_TO_SDK_VERSION = new Map([
3570
+ ["2.1.88", "0.208.0"],
3571
+ ["2.1.87", "0.208.0"],
3572
+ ["2.1.86", "0.208.0"],
3573
+ ["2.1.85", "0.208.0"],
3147
3574
  ["2.1.84", "0.208.0"],
3148
3575
  ["2.1.83", "0.208.0"],
3149
3576
  ["2.1.81", "0.208.0"],
@@ -3240,6 +3667,8 @@ const BETA_SHORTCUTS = new Map([
3240
3667
  ["fast", "fast-mode-2026-02-01"],
3241
3668
  ["fast-mode", "fast-mode-2026-02-01"],
3242
3669
  ["opus-fast", "fast-mode-2026-02-01"],
3670
+ ["task-budgets", "task-budgets-2026-03-13"],
3671
+ ["budgets", "task-budgets-2026-03-13"],
3243
3672
  ]);
3244
3673
  const STAINLESS_HELPER_KEYS = [
3245
3674
  "x_stainless_helper",
@@ -3962,9 +4391,6 @@ function buildAnthropicBetaHeader(
3962
4391
  betas.push("structured-outputs-2025-12-15");
3963
4392
  }
3964
4393
 
3965
- // Tool examples — always-on in Claude Code
3966
- betas.push("tool-examples-2025-10-29");
3967
-
3968
4394
  // Web search — for models that support it
3969
4395
  if (supportsWebSearch(model)) {
3970
4396
  betas.push("web-search-2025-03-05");
@@ -4367,6 +4793,17 @@ function transformRequestBody(body, signature, runtime, betaHeader) {
4367
4793
  return msg;
4368
4794
  });
4369
4795
  }
4796
+ // Task budgets: when the task-budgets beta is active, preserve or inject output_config.
4797
+ // The beta unlocks output_config.max_output_tokens for per-task budget control.
4798
+ // Model-router compatibility: the beta header + output_config body are forwarded as-is.
4799
+ if (betaHeader && betaHeader.includes("task-budgets-2026-03-13")) {
4800
+ if (!parsed.output_config) {
4801
+ // Default: set a reasonable per-task output budget for long-running agentic tasks.
4802
+ // Claude Code tasks typically need generous output budgets.
4803
+ parsed.output_config = { max_output_tokens: 16384 };
4804
+ }
4805
+ }
4806
+
4370
4807
  // Fast mode: inject speed parameter for supported models
4371
4808
  const fastModeEnabled = signature.fastMode && !isFalsyEnv(process.env.OPENCODE_ANTHROPIC_DISABLE_FAST_MODE);
4372
4809
  if (fastModeEnabled && parsed.model && (isOpus46Model(parsed.model) || isSonnet46Model(parsed.model))) {
package/lib/config.mjs CHANGED
@@ -154,6 +154,18 @@ export const DEFAULT_CONFIG = {
154
154
  escalation_threshold: 150_000,
155
155
  deescalation_threshold: 100_000,
156
156
  },
157
+ /** Willow Mode: detect inactivity and suggest context reset. */
158
+ willow_mode: {
159
+ /** Enable idle detection and context-reset suggestions. */
160
+ enabled: true,
161
+ /** Minutes of inactivity before suggesting a fresh context. */
162
+ idle_threshold_minutes: 30,
163
+ /** Minimum minutes between willow suggestions (to avoid nagging). */
164
+ cooldown_minutes: 60,
165
+ /** Minimum turns in the current session before suggesting reset
166
+ * (no point suggesting reset at the start of a session). */
167
+ min_turns_before_suggest: 3,
168
+ },
157
169
  };
158
170
 
159
171
  export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
@@ -181,6 +193,7 @@ function createDefaultConfig() {
181
193
  headers: { ...DEFAULT_CONFIG.headers, overrides: {}, disable: [] },
182
194
  cc_credential_reuse: { ...DEFAULT_CONFIG.cc_credential_reuse },
183
195
  adaptive_context: { ...DEFAULT_CONFIG.adaptive_context },
196
+ willow_mode: { ...DEFAULT_CONFIG.willow_mode },
184
197
  };
185
198
  }
186
199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "main": "./index.mjs",
6
6
  "files": [