@khalilgharbaoui/opencode-claude-code-plugin 0.45.0 → 0.46.1

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/dist/index.js CHANGED
@@ -916,6 +916,10 @@ function formatResultFailureNote(message) {
916
916
  ${RESULT_ERROR_MARKER} ${message}
917
917
  `;
918
918
  }
919
+ function formatCompactionFailure(cause, detail) {
920
+ const said = detail?.trim();
921
+ return `Claude Code could not compact this conversation (${cause}), so no summary was produced.` + (said ? ` The CLI said: ${said}` : "");
922
+ }
919
923
  var STREAM_TIMEOUT_MARKER = "\u258C **stream timeout:**";
920
924
  function formatStreamTimeoutNote(silenceMs) {
921
925
  const seconds = Math.max(1, Math.round(silenceMs / 1e3));
@@ -1621,7 +1625,108 @@ function failoverUntil(resetsAt) {
1621
1625
  // src/cli-version.ts
1622
1626
  import { execFile } from "child_process";
1623
1627
  import { promisify } from "util";
1628
+
1629
+ // src/windows-spawn.ts
1630
+ import * as fs from "fs";
1631
+ import * as path2 from "path";
1632
+ var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
1633
+ function envValue(env, name) {
1634
+ const direct = env[name];
1635
+ if (direct !== void 0) return direct;
1636
+ const match = Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase());
1637
+ return match === void 0 ? void 0 : env[match];
1638
+ }
1639
+ function defaultIsFile(candidate) {
1640
+ try {
1641
+ return fs.statSync(candidate).isFile();
1642
+ } catch {
1643
+ return false;
1644
+ }
1645
+ }
1646
+ function executableExtensions(env = process.env) {
1647
+ const raw = envValue(env, "PATHEXT") ?? DEFAULT_PATHEXT;
1648
+ return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
1649
+ }
1650
+ function isBatchFile(file) {
1651
+ const ext = path2.win32.extname(file).toLowerCase();
1652
+ return ext === ".cmd" || ext === ".bat";
1653
+ }
1654
+ function resolveWindowsCommand(command, deps = {}) {
1655
+ const env = deps.env ?? process.env;
1656
+ const isFile = deps.isFile ?? defaultIsFile;
1657
+ const extensions = executableExtensions(env);
1658
+ const candidatesFor = (base) => {
1659
+ const ext = path2.win32.extname(base).toLowerCase();
1660
+ if (ext && extensions.includes(ext)) return [base];
1661
+ return [...extensions.map((candidate) => base + candidate), base];
1662
+ };
1663
+ if (command.includes("\\") || command.includes("/")) {
1664
+ for (const candidate of candidatesFor(command)) {
1665
+ if (isFile(candidate)) return candidate;
1666
+ }
1667
+ return null;
1668
+ }
1669
+ const pathValue = envValue(env, "PATH") ?? "";
1670
+ for (const entry of pathValue.split(path2.win32.delimiter)) {
1671
+ const directory = entry.trim().replace(/^"(.*)"$/, "$1");
1672
+ if (!directory) continue;
1673
+ for (const candidate of candidatesFor(path2.win32.join(directory, command))) {
1674
+ if (isFile(candidate)) return candidate;
1675
+ }
1676
+ }
1677
+ return null;
1678
+ }
1679
+ function quoteBatchArgument(arg) {
1680
+ if (/[\r\n]/.test(arg)) {
1681
+ throw new Error("cmd.exe cannot carry a newline in an argument");
1682
+ }
1683
+ let out = '"';
1684
+ let backslashes = 0;
1685
+ for (const char of arg) {
1686
+ if (char === "\\") {
1687
+ backslashes += 1;
1688
+ continue;
1689
+ }
1690
+ if (char === '"') {
1691
+ out += "\\".repeat(backslashes * 2) + '""';
1692
+ backslashes = 0;
1693
+ continue;
1694
+ }
1695
+ out += "\\".repeat(backslashes) + char;
1696
+ backslashes = 0;
1697
+ }
1698
+ return `${out}${"\\".repeat(backslashes * 2)}"`;
1699
+ }
1700
+ function planCmdInvocation(file, args, env = process.env) {
1701
+ const comspec = envValue(env, "ComSpec") ?? "cmd.exe";
1702
+ const line = [`"${path2.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
1703
+ return {
1704
+ file: comspec,
1705
+ args: ["/d", "/s", "/c", `"${line.join(" ")}"`],
1706
+ windowsVerbatimArguments: true
1707
+ };
1708
+ }
1709
+ function planClaudeSpawn(command, args, deps = {}) {
1710
+ const platform2 = deps.platform ?? process.platform;
1711
+ if (platform2 !== "win32") return { file: command, args: [...args] };
1712
+ const env = deps.env ?? process.env;
1713
+ const resolved = resolveWindowsCommand(command, deps) ?? command;
1714
+ if (isBatchFile(resolved)) return planCmdInvocation(resolved, args, env);
1715
+ return { file: resolved, args: [...args] };
1716
+ }
1717
+
1718
+ // src/cli-version.ts
1624
1719
  var execFileAsync = promisify(execFile);
1720
+ function probeExec(cliPath, args, options) {
1721
+ const plan = planClaudeSpawn(cliPath, args);
1722
+ return execFileAsync(plan.file, plan.args, {
1723
+ ...options,
1724
+ // utf8 is already `execFile`'s default; naming it is what keeps the
1725
+ // promisified overload returning strings rather than Buffers.
1726
+ encoding: "utf8",
1727
+ windowsVerbatimArguments: plan.windowsVerbatimArguments
1728
+ });
1729
+ }
1625
1730
  var cache = /* @__PURE__ */ new Map();
1626
1731
  var PROBE_TIMEOUT_MS = 5e3;
1627
1732
  var probeTimeoutMs = PROBE_TIMEOUT_MS;
@@ -1668,7 +1773,7 @@ function detectCliVersion(cliPath) {
1668
1773
  let deadlineKill = false;
1669
1774
  const promise = (async () => {
1670
1775
  try {
1671
- const { stdout } = await execFileAsync(cliPath, ["--version"], {
1776
+ const { stdout } = await probeExec(cliPath, ["--version"], {
1672
1777
  timeout: probeTimeoutMs
1673
1778
  });
1674
1779
  const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout.trim());
@@ -1765,7 +1870,7 @@ function detectHeadlessSupport(cliPath) {
1765
1870
  let deadlineKill = false;
1766
1871
  const promise = (async () => {
1767
1872
  try {
1768
- const execution = execFileAsync(cliPath, [
1873
+ const execution = probeExec(cliPath, [
1769
1874
  "--print",
1770
1875
  "--input-format",
1771
1876
  "stream-json",
@@ -1801,7 +1906,7 @@ function detectCliSupportsFlag(cliPath, flag) {
1801
1906
  let deadlineKill = false;
1802
1907
  const promise = (async () => {
1803
1908
  try {
1804
- const execution = execFileAsync(cliPath, ["--help"], {
1909
+ const execution = probeExec(cliPath, ["--help"], {
1805
1910
  timeout: probeTimeoutMs,
1806
1911
  killSignal: "SIGKILL",
1807
1912
  maxBuffer: 4 * 1024 * 1024
@@ -1834,17 +1939,17 @@ import { unlink } from "fs/promises";
1834
1939
 
1835
1940
  // src/proxy-mcp.ts
1836
1941
  import { createServer } from "http";
1837
- import * as fs2 from "fs";
1838
- import * as path3 from "path";
1942
+ import * as fs3 from "fs";
1943
+ import * as path4 from "path";
1839
1944
  import * as crypto from "crypto";
1840
1945
  import { EventEmitter } from "events";
1841
1946
 
1842
1947
  // src/tmp.ts
1843
- import * as fs from "fs";
1948
+ import * as fs2 from "fs";
1844
1949
  import * as os from "os";
1845
- import * as path2 from "path";
1950
+ import * as path3 from "path";
1846
1951
  var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
1847
- var PID_TMP_DIR = path2.join(
1952
+ var PID_TMP_DIR = path3.join(
1848
1953
  os.tmpdir(),
1849
1954
  `${PLUGIN_TMP_PREFIX}${process.pid}`
1850
1955
  );
@@ -1857,7 +1962,7 @@ function currentUid() {
1857
1962
  function untrustworthyReason(dir) {
1858
1963
  let stat;
1859
1964
  try {
1860
- stat = fs.lstatSync(dir);
1965
+ stat = fs2.lstatSync(dir);
1861
1966
  } catch {
1862
1967
  return null;
1863
1968
  }
@@ -1870,16 +1975,16 @@ function untrustworthyReason(dir) {
1870
1975
  return null;
1871
1976
  }
1872
1977
  function ensureDir(dir) {
1873
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
1978
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
1874
1979
  try {
1875
- fs.chmodSync(dir, 448);
1980
+ fs2.chmodSync(dir, 448);
1876
1981
  } catch {
1877
1982
  }
1878
1983
  }
1879
1984
  function chooseDir() {
1880
1985
  const reason = untrustworthyReason(PID_TMP_DIR);
1881
1986
  if (!reason) return PID_TMP_DIR;
1882
- const fallback = fs.mkdtempSync(path2.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
1987
+ const fallback = fs2.mkdtempSync(path3.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
1883
1988
  log.warn("refusing the pid-named scratch directory", {
1884
1989
  path: PID_TMP_DIR,
1885
1990
  reason,
@@ -1893,7 +1998,7 @@ function registerExitCleanup() {
1893
1998
  process.on("exit", () => {
1894
1999
  for (const dir of createdDirs) {
1895
2000
  try {
1896
- fs.rmSync(dir, { recursive: true, force: true });
2001
+ fs2.rmSync(dir, { recursive: true, force: true });
1897
2002
  } catch {
1898
2003
  }
1899
2004
  }
@@ -1914,23 +2019,23 @@ function sweepStalePluginTmpDirs(keep) {
1914
2019
  const root = os.tmpdir();
1915
2020
  let entries;
1916
2021
  try {
1917
- entries = fs.readdirSync(root);
2022
+ entries = fs2.readdirSync(root);
1918
2023
  } catch {
1919
2024
  return removed;
1920
2025
  }
1921
2026
  const uid = currentUid();
1922
- const keepPath = keep ? path2.resolve(keep) : null;
2027
+ const keepPath = keep ? path3.resolve(keep) : null;
1923
2028
  for (const entry of entries) {
1924
2029
  const match = PID_DIR_NAME.exec(entry);
1925
2030
  if (!match) continue;
1926
2031
  const pid = Number(match[1]);
1927
2032
  if (!Number.isSafeInteger(pid) || pid <= 0) continue;
1928
2033
  if (pid === process.pid) continue;
1929
- const dir = path2.join(root, entry);
1930
- if (keepPath && path2.resolve(dir) === keepPath) continue;
2034
+ const dir = path3.join(root, entry);
2035
+ if (keepPath && path3.resolve(dir) === keepPath) continue;
1931
2036
  let stat;
1932
2037
  try {
1933
- stat = fs.lstatSync(dir);
2038
+ stat = fs2.lstatSync(dir);
1934
2039
  } catch {
1935
2040
  continue;
1936
2041
  }
@@ -1938,7 +2043,7 @@ function sweepStalePluginTmpDirs(keep) {
1938
2043
  if (uid !== void 0 && stat.uid !== uid) continue;
1939
2044
  if (isProcessAlive(pid)) continue;
1940
2045
  try {
1941
- fs.rmSync(dir, { recursive: true, force: true });
2046
+ fs2.rmSync(dir, { recursive: true, force: true });
1942
2047
  removed.push(dir);
1943
2048
  } catch (err) {
1944
2049
  log.debug("could not remove a stale scratch directory", {
@@ -2769,11 +2874,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2769
2874
  2
2770
2875
  );
2771
2876
  const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
2772
- const outPath = path3.join(
2877
+ const outPath = path4.join(
2773
2878
  pluginTmpDir(),
2774
2879
  `proxy-${hash}.json`
2775
2880
  );
2776
- fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
2881
+ fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
2777
2882
  configFilePath = outPath;
2778
2883
  return outPath;
2779
2884
  },
@@ -2790,7 +2895,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2790
2895
  });
2791
2896
  if (configFilePath) {
2792
2897
  try {
2793
- fs2.unlinkSync(configFilePath);
2898
+ fs3.unlinkSync(configFilePath);
2794
2899
  } catch {
2795
2900
  }
2796
2901
  configFilePath = null;
@@ -3359,8 +3464,8 @@ function clearCompression(sessionKey2) {
3359
3464
  }
3360
3465
 
3361
3466
  // src/mcp-bridge.ts
3362
- import * as fs3 from "fs";
3363
- import * as path4 from "path";
3467
+ import * as fs4 from "fs";
3468
+ import * as path5 from "path";
3364
3469
  import * as os2 from "os";
3365
3470
  import * as crypto2 from "crypto";
3366
3471
  import {
@@ -3371,21 +3476,21 @@ var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
3371
3476
  var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
3372
3477
  function fileExists(p) {
3373
3478
  try {
3374
- return fs3.statSync(p).isFile();
3479
+ return fs4.statSync(p).isFile();
3375
3480
  } catch {
3376
3481
  return false;
3377
3482
  }
3378
3483
  }
3379
3484
  function dirExists(p) {
3380
3485
  try {
3381
- return fs3.statSync(p).isDirectory();
3486
+ return fs4.statSync(p).isDirectory();
3382
3487
  } catch {
3383
3488
  return false;
3384
3489
  }
3385
3490
  }
3386
3491
  function readAndParse(file) {
3387
3492
  try {
3388
- const raw = fs3.readFileSync(file, "utf8");
3493
+ const raw = fs4.readFileSync(file, "utf8");
3389
3494
  const errors = [];
3390
3495
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
3391
3496
  if (errors.length > 0) {
@@ -3421,14 +3526,14 @@ function deepMerge(target, source) {
3421
3526
  }
3422
3527
  function walkUp(opts) {
3423
3528
  const out = [];
3424
- let current = path4.resolve(opts.start);
3529
+ let current = path5.resolve(opts.start);
3425
3530
  while (true) {
3426
3531
  for (const target of opts.targets) {
3427
- const candidate = path4.join(current, target);
3532
+ const candidate = path5.join(current, target);
3428
3533
  if (opts.predicate(candidate)) out.push(candidate);
3429
3534
  }
3430
- if (opts.stop && current === path4.resolve(opts.stop)) break;
3431
- const parent = path4.dirname(current);
3535
+ if (opts.stop && current === path5.resolve(opts.stop)) break;
3536
+ const parent = path5.dirname(current);
3432
3537
  if (parent === current) break;
3433
3538
  current = parent;
3434
3539
  }
@@ -3436,28 +3541,28 @@ function walkUp(opts) {
3436
3541
  }
3437
3542
  function detectWorktree(cwd) {
3438
3543
  const override = process.env.OPENCODE_WORKTREE;
3439
- if (override) return path4.resolve(override);
3440
- let current = path4.resolve(cwd);
3544
+ if (override) return path5.resolve(override);
3545
+ let current = path5.resolve(cwd);
3441
3546
  while (true) {
3442
- const gitPath = path4.join(current, ".git");
3547
+ const gitPath = path5.join(current, ".git");
3443
3548
  try {
3444
- if (fs3.existsSync(gitPath)) return current;
3549
+ if (fs4.existsSync(gitPath)) return current;
3445
3550
  } catch {
3446
3551
  }
3447
- const parent = path4.dirname(current);
3552
+ const parent = path5.dirname(current);
3448
3553
  if (parent === current) return void 0;
3449
3554
  current = parent;
3450
3555
  }
3451
3556
  }
3452
3557
  function globalConfigDir() {
3453
- const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
3454
- return path4.join(xdg, "opencode");
3558
+ const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os2.homedir(), ".config");
3559
+ return path5.join(xdg, "opencode");
3455
3560
  }
3456
3561
  function loadGlobalConfig(hostApi = "v1") {
3457
3562
  const dir = globalConfigDir();
3458
3563
  let merged = {};
3459
3564
  for (const name of FILE_NAMES.slice().reverse()) {
3460
- const file = path4.join(dir, name);
3565
+ const file = path5.join(dir, name);
3461
3566
  if (!fileExists(file)) continue;
3462
3567
  const parsed = readAndParse(file);
3463
3568
  if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
@@ -3467,7 +3572,7 @@ function loadGlobalConfig(hostApi = "v1") {
3467
3572
  function loadProjectFilesInDir(dir, hostApi = "v1") {
3468
3573
  let merged = {};
3469
3574
  for (const name of PROJECT_FILE_NAMES) {
3470
- const file = path4.join(dir, name);
3575
+ const file = path5.join(dir, name);
3471
3576
  if (!fileExists(file)) continue;
3472
3577
  const parsed = readAndParse(file);
3473
3578
  if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
@@ -3478,7 +3583,7 @@ function dotOpencodeDirs(cwd, worktree) {
3478
3583
  const dirs = [];
3479
3584
  const seen = /* @__PURE__ */ new Set();
3480
3585
  const push = (p) => {
3481
- const abs = path4.resolve(p);
3586
+ const abs = path5.resolve(p);
3482
3587
  if (!seen.has(abs) && dirExists(abs)) {
3483
3588
  seen.add(abs);
3484
3589
  dirs.push(abs);
@@ -3494,7 +3599,7 @@ function dotOpencodeDirs(cwd, worktree) {
3494
3599
  }
3495
3600
  const home = os2.homedir();
3496
3601
  if (home) {
3497
- const homeDot = path4.join(home, ".opencode");
3602
+ const homeDot = path5.join(home, ".opencode");
3498
3603
  if (dirExists(homeDot)) push(homeDot);
3499
3604
  }
3500
3605
  const envDir = process.env.OPENCODE_CONFIG_DIR;
@@ -3641,7 +3746,7 @@ function opencodeConfigLayers(cwd, hostApi = "v1") {
3641
3746
  const projectDirs = [];
3642
3747
  const seenProjectDirs = /* @__PURE__ */ new Set();
3643
3748
  for (const f of projectFiles) {
3644
- const d = path4.dirname(f);
3749
+ const d = path5.dirname(f);
3645
3750
  if (!seenProjectDirs.has(d)) {
3646
3751
  seenProjectDirs.add(d);
3647
3752
  projectDirs.push(d);
@@ -3708,13 +3813,13 @@ function finishBridge(input) {
3708
3813
  }
3709
3814
  const body = JSON.stringify({ mcpServers: servers }, null, 2);
3710
3815
  const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
3711
- const outPath = path4.join(
3816
+ const outPath = path5.join(
3712
3817
  pluginTmpDir(),
3713
3818
  `mcp-${bodyDigest}.json`
3714
3819
  );
3715
3820
  try {
3716
3821
  if (!fileExists(outPath)) {
3717
- fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3822
+ fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3718
3823
  }
3719
3824
  } catch (e) {
3720
3825
  log.warn("failed to write bridged MCP config", {
@@ -4250,7 +4355,8 @@ function messageDigest(msg) {
4250
4355
  }
4251
4356
  return hash.digest("hex").slice(0, 32);
4252
4357
  }
4253
- function conversationDigests(prompt) {
4358
+ var ASSISTANT_TURN_DIGEST = "assistant-turn";
4359
+ function conversationDigests(prompt, opts) {
4254
4360
  const out = [];
4255
4361
  for (const msg of prompt) {
4256
4362
  if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") {
@@ -4258,7 +4364,7 @@ function conversationDigests(prompt) {
4258
4364
  }
4259
4365
  out.push({
4260
4366
  role: msg.role,
4261
- digest: messageDigest(msg)
4367
+ digest: opts?.assistantContent === false && msg.role === "assistant" ? ASSISTANT_TURN_DIGEST : messageDigest(msg)
4262
4368
  });
4263
4369
  }
4264
4370
  return out;
@@ -4285,7 +4391,24 @@ function forkSiblingSignature(sessionKey2) {
4285
4391
  blanked[3] = "*";
4286
4392
  return blanked.join("::");
4287
4393
  }
4288
- function splitForkHistory(prompt) {
4394
+ function modelSiblingSignature(sessionKey2) {
4395
+ const parts = sessionKey2.split("::");
4396
+ if (parts.length < 5) return null;
4397
+ if (parts[2] === "compaction") return null;
4398
+ const withoutEffort = parts[parts.length - 1].startsWith("effort=") ? parts.slice(0, -1) : parts;
4399
+ if (withoutEffort.length < 5) return null;
4400
+ const blanked = [...withoutEffort];
4401
+ blanked[1] = "*";
4402
+ return blanked.join("::");
4403
+ }
4404
+ function moveForkFingerprint(fromKey, toKey) {
4405
+ const record = fingerprints.get(fromKey);
4406
+ if (!record) return;
4407
+ fingerprints.delete(fromKey);
4408
+ fingerprints.delete(toKey);
4409
+ fingerprints.set(toKey, { ...record, seq: ++fingerprintSeq });
4410
+ }
4411
+ function splitForkHistory(prompt, opts) {
4289
4412
  const messages = prompt;
4290
4413
  let lastAssistant = -1;
4291
4414
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -4304,7 +4427,7 @@ function splitForkHistory(prompt) {
4304
4427
  }
4305
4428
  }
4306
4429
  return {
4307
- history: conversationDigests(messages.slice(0, lastAssistant + 1)),
4430
+ history: conversationDigests(messages.slice(0, lastAssistant + 1), opts),
4308
4431
  forkable: true
4309
4432
  };
4310
4433
  }
@@ -4343,8 +4466,8 @@ function findForkParent(opts) {
4343
4466
  }
4344
4467
  return best;
4345
4468
  }
4346
- function continuesRecordedConversation(recorded, prompt) {
4347
- const { history, forkable } = splitForkHistory(prompt);
4469
+ function continuesRecordedConversation(recorded, prompt, opts) {
4470
+ const { history, forkable } = splitForkHistory(prompt, opts);
4348
4471
  if (!forkable || recorded.length === 0 || recorded.length > history.length) return false;
4349
4472
  return isPrefix(recorded, history) && tailIsReplyOnly(history, recorded.length);
4350
4473
  }
@@ -4381,7 +4504,7 @@ function isDigest(value) {
4381
4504
  }
4382
4505
  function isRecord2(value) {
4383
4506
  const record = value;
4384
- return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest);
4507
+ return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest));
4385
4508
  }
4386
4509
  function load() {
4387
4510
  if (records) return records;
@@ -4430,13 +4553,14 @@ function save(update) {
4430
4553
  function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath) {
4431
4554
  const chain = conversationDigests(prompt);
4432
4555
  if (chain.length === 0) return;
4556
+ const shape2 = conversationDigests(prompt, { assistantContent: false });
4433
4557
  const current = load().get(sessionKey2);
4434
- if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
4558
+ if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
4435
4559
  return;
4436
4560
  }
4437
4561
  save((merged) => {
4438
4562
  merged.delete(sessionKey2);
4439
- merged.set(sessionKey2, { claudeSessionId, cliPath, chain, updatedAt: Date.now() });
4563
+ merged.set(sessionKey2, { claudeSessionId, cliPath, chain, shape: shape2, updatedAt: Date.now() });
4440
4564
  });
4441
4565
  }
4442
4566
  function forgetResumePoint(sessionKey2) {
@@ -4445,25 +4569,84 @@ function forgetResumePoint(sessionKey2) {
4445
4569
  merged.delete(sessionKey2);
4446
4570
  });
4447
4571
  }
4572
+ function moveResumePoint(fromKey, toKey) {
4573
+ const record = load().get(fromKey);
4574
+ if (!record) return;
4575
+ save((merged) => {
4576
+ merged.delete(fromKey);
4577
+ if (merged.has(toKey)) return;
4578
+ merged.set(toKey, { ...record, updatedAt: Date.now() });
4579
+ });
4580
+ }
4448
4581
  function findResumePoint(opts) {
4449
4582
  const record = load().get(opts.sessionKey);
4450
- if (!record) return void 0;
4583
+ if (!record) {
4584
+ opts.onRefused?.("no-session-recorded");
4585
+ return void 0;
4586
+ }
4451
4587
  const refuse = (reason) => {
4452
4588
  log.info("not resuming the claude session from before the restart", {
4453
4589
  sessionKey: opts.sessionKey,
4454
4590
  reason
4455
4591
  });
4592
+ opts.onRefused?.(reason);
4456
4593
  return void 0;
4457
4594
  };
4458
- if (record.cliPath !== opts.cliPath) return refuse("another claude binary");
4595
+ if (record.cliPath !== opts.cliPath) return refuse("another-claude-binary");
4459
4596
  if (!continuesRecordedConversation(record.chain, opts.prompt)) {
4460
- return refuse("the conversation changed");
4597
+ return refuse("conversation-changed");
4461
4598
  }
4462
4599
  if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
4463
- return refuse("the transcript is gone");
4600
+ return refuse("transcript-gone");
4464
4601
  }
4465
4602
  return { claudeSessionId: record.claudeSessionId, matched: record.chain.length };
4466
4603
  }
4604
+ function findSiblingResumePoint(opts) {
4605
+ const signature = modelSiblingSignature(opts.sessionKey);
4606
+ if (!signature) {
4607
+ opts.onRefused?.("no-sibling-possible");
4608
+ return void 0;
4609
+ }
4610
+ let best;
4611
+ let bestUpdatedAt = -1;
4612
+ let refusal;
4613
+ for (const [key, record] of load()) {
4614
+ if (key === opts.sessionKey) continue;
4615
+ if (modelSiblingSignature(key) !== signature) continue;
4616
+ if (record.cliPath !== opts.cliPath) {
4617
+ refusal = "sibling-another-claude-binary";
4618
+ continue;
4619
+ }
4620
+ if (!record.shape || !continuesRecordedConversation(record.shape, opts.prompt, {
4621
+ assistantContent: false
4622
+ })) {
4623
+ refusal = "sibling-conversation-changed";
4624
+ continue;
4625
+ }
4626
+ if (opts.isBusy(key)) {
4627
+ refusal = "sibling-still-writing";
4628
+ continue;
4629
+ }
4630
+ if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
4631
+ refusal = "sibling-transcript-gone";
4632
+ continue;
4633
+ }
4634
+ if (record.updatedAt >= bestUpdatedAt) {
4635
+ bestUpdatedAt = record.updatedAt;
4636
+ best = {
4637
+ claudeSessionId: record.claudeSessionId,
4638
+ siblingKey: key,
4639
+ matched: record.chain.length
4640
+ };
4641
+ }
4642
+ }
4643
+ if (!best) {
4644
+ opts.onRefused?.(
4645
+ refusal ?? "no-sibling-recorded"
4646
+ );
4647
+ }
4648
+ return best;
4649
+ }
4467
4650
 
4468
4651
  // src/permission-presets.ts
4469
4652
  var READ_ONLY_DISALLOWED_CLI_TOOLS = [
@@ -5164,6 +5347,23 @@ function deleteClaudeSessionId(key) {
5164
5347
  forgetForkFingerprint(key);
5165
5348
  forgetResumePoint(key);
5166
5349
  }
5350
+ function transferClaudeSession(fromKey, toKey) {
5351
+ if (fromKey === toKey) return void 0;
5352
+ const claudeSessionId = claudeSessions.get(fromKey);
5353
+ if (!claudeSessionId) return void 0;
5354
+ if (claudeSessions.has(toKey)) return void 0;
5355
+ claudeSessions.delete(fromKey);
5356
+ setClaudeSessionId(toKey, claudeSessionId);
5357
+ moveResumePoint(fromKey, toKey);
5358
+ moveForkFingerprint(fromKey, toKey);
5359
+ clearExitPlanModeQuestions(fromKey);
5360
+ clearAccountFailoverQuestions(fromKey);
5361
+ log.notice("carrying the claude conversation over to the new session key", {
5362
+ fromKey,
5363
+ toKey
5364
+ });
5365
+ return claudeSessionId;
5366
+ }
5167
5367
  function effortSessionKey(baseKey, effort) {
5168
5368
  return effort ? `${baseKey}::effort=${effort}` : baseKey;
5169
5369
  }
@@ -5186,8 +5386,14 @@ function invalidateOtherEffortSessions(baseKey, effort) {
5186
5386
  );
5187
5387
  }
5188
5388
  }
5389
+ const currentKey = effortSessionKey(baseKey, effort);
5189
5390
  for (const key of staleKeys) {
5190
5391
  deleteActiveProcess(key);
5392
+ if (transferClaudeSession(key, currentKey)) {
5393
+ clearCompression(key);
5394
+ clearBackgroundTasks(key);
5395
+ continue;
5396
+ }
5191
5397
  deleteClaudeSessionId(key);
5192
5398
  clearCompression(key);
5193
5399
  clearBackgroundTasks(key);
@@ -5203,11 +5409,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
5203
5409
  effort,
5204
5410
  promptCacheTtl
5205
5411
  });
5206
- const proc = spawn(cliPath, cliArgs, {
5412
+ const plan = planClaudeSpawn(cliPath, cliArgs);
5413
+ const proc = spawn(plan.file, plan.args, {
5207
5414
  cwd,
5208
5415
  stdio: ["pipe", "pipe", "pipe"],
5209
5416
  env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl }),
5210
- shell: process.platform === "win32"
5417
+ windowsVerbatimArguments: plan.windowsVerbatimArguments
5211
5418
  });
5212
5419
  const lineEmitter = new EventEmitter3();
5213
5420
  const ap = {
@@ -5270,12 +5477,11 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
5270
5477
  }
5271
5478
  }
5272
5479
  if (ownsSessionKey && code !== 0 && code !== null) {
5273
- log.info("process exited with error, clearing session", {
5480
+ log.info("process exited with error; keeping the claude session to resume", {
5274
5481
  code,
5275
- sessionKey: sessionKey2
5482
+ sessionKey: sessionKey2,
5483
+ resumable: claudeSessions.has(sessionKey2)
5276
5484
  });
5277
- claudeSessions.delete(sessionKey2);
5278
- forgetResumePoint(sessionKey2);
5279
5485
  }
5280
5486
  });
5281
5487
  proc.stderr?.on("data", (data) => {
@@ -5788,6 +5994,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5788
5994
  "btw: a turn is running but no claude process appeared for it",
5789
5995
  "btw: early aside failed; the message will ask again",
5790
5996
  "cannot interrupt this transport; waiting for the turn to end",
5997
+ "carrying the claude conversation over to the new session key",
5791
5998
  "claude --version output unparseable",
5792
5999
  "claude account is out of usage; the turn ends with a note",
5793
6000
  "claude already registers a different skill under this name; not bridging ours",
@@ -5795,6 +6002,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5795
6002
  "claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
5796
6003
  "claude code compacted its own context",
5797
6004
  "claude code reset its conversation",
6005
+ "claude could not compact the conversation; no summary was stored",
5798
6006
  "claude finished the turn without a reply",
5799
6007
  "claude kept missing the probe deadline; keeping the conservative answer for this process",
5800
6008
  "claude process belongs to another account; starting fresh",
@@ -5810,6 +6018,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5810
6018
  "cleanup-stale: could not record the sweep marker",
5811
6019
  "cleanup-stale: error processing cache root",
5812
6020
  "cleanup-stale: rmSync failed",
6021
+ "continuing this conversation's claude session under the new model or effort instead of replaying it",
5813
6022
  "delivering proxy results after interrupted continuation",
5814
6023
  "draining proxy calls queued between turns",
5815
6024
  "empty user content; sending sentinel to satisfy CLI",
@@ -5881,6 +6090,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5881
6090
  "rejected pending proxy call",
5882
6091
  "remote URL images are not supported by Claude CLI, skipping",
5883
6092
  "replaying stdout the child emitted between turns",
6093
+ "replaying the conversation as text: no claude session was available to continue",
5884
6094
  "result fallback timer fired \u2014 closing stream without result event",
5885
6095
  "resuming the claude session from before the restart instead of replaying it",
5886
6096
  "shared Claude capability already exists; leaving untouched",
@@ -5936,6 +6146,7 @@ var BUNDLE_DATA_ALLOWLIST = {
5936
6146
  inFlight: "flag",
5937
6147
  anthropicApiKeyInEnv: "flag",
5938
6148
  includeHistoryContext: "flag",
6149
+ resumable: "flag",
5939
6150
  interactive: "flag",
5940
6151
  interactiveTransport: "flag",
5941
6152
  isError: "flag",
@@ -5957,6 +6168,8 @@ var BUNDLE_DATA_ALLOWLIST = {
5957
6168
  dropped: "count",
5958
6169
  exitCode: "count",
5959
6170
  historyLength: "count",
6171
+ matchedMessages: "count",
6172
+ messages: "count",
5960
6173
  inputTokens: "count",
5961
6174
  lastTextLength: "count",
5962
6175
  lines: "count",
@@ -6058,6 +6271,12 @@ var BUNDLE_DATA_ALLOWLIST = {
6058
6271
  sessionId: "id",
6059
6272
  sessionID: "id",
6060
6273
  sessionKey: "id",
6274
+ // The two ends of a conversation carried from one session key to another
6275
+ // (h #g215), and the fork lookup's parent. All three are session keys.
6276
+ fromKey: "id",
6277
+ toKey: "id",
6278
+ siblingKey: "id",
6279
+ parentKey: "id",
6061
6280
  toolCallId: "id",
6062
6281
  toolCallIds: "id",
6063
6282
  toolUseId: "id",
@@ -6294,8 +6513,8 @@ function redactLogLine(raw, context) {
6294
6513
  if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
6295
6514
  return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
6296
6515
  }
6297
- function readLogTail(path11, maxBytes = BUNDLE_TAIL_BYTES) {
6298
- const fd = openSync(path11, "r");
6516
+ function readLogTail(path12, maxBytes = BUNDLE_TAIL_BYTES) {
6517
+ const fd = openSync(path12, "r");
6299
6518
  try {
6300
6519
  const size = fstatSync(fd).size;
6301
6520
  const length = Math.min(size, maxBytes);
@@ -6445,13 +6664,20 @@ function parsePlanUsage(stdout) {
6445
6664
  return { status: "failed", error: "no result object in the CLI's reply" };
6446
6665
  }
6447
6666
  function runCli(cliPath, args, timeoutMs, env) {
6667
+ const plan = planClaudeSpawn(cliPath, args);
6448
6668
  return new Promise((resolve6, reject) => {
6449
6669
  execFile2(
6450
- cliPath,
6451
- args,
6670
+ plan.file,
6671
+ plan.args,
6452
6672
  // `killSignal` so a CLI wedged on a hook is actually gone, and a generous
6453
6673
  // buffer because the reply is prose of unbounded length.
6454
- { timeout: timeoutMs, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024, env },
6674
+ {
6675
+ timeout: timeoutMs,
6676
+ killSignal: "SIGKILL",
6677
+ maxBuffer: 4 * 1024 * 1024,
6678
+ env,
6679
+ windowsVerbatimArguments: plan.windowsVerbatimArguments
6680
+ },
6455
6681
  (error, stdout) => {
6456
6682
  if (stdout && stdout.trim()) return resolve6(stdout);
6457
6683
  if (error) return reject(error);
@@ -6490,14 +6716,14 @@ async function fetchPlanUsage(cliPath, options = {}) {
6490
6716
  }
6491
6717
 
6492
6718
  // src/stale-build.ts
6493
- import * as fs5 from "fs";
6494
- import * as path6 from "path";
6719
+ import * as fs6 from "fs";
6720
+ import * as path7 from "path";
6495
6721
  import { fileURLToPath as fileURLToPath2 } from "url";
6496
6722
 
6497
6723
  // src/startup-diagnostics.ts
6498
6724
  import { execFile as execFile3 } from "child_process";
6499
- import * as fs4 from "fs";
6500
- import * as path5 from "path";
6725
+ import * as fs5 from "fs";
6726
+ import * as path6 from "path";
6501
6727
  import { promisify as promisify2 } from "util";
6502
6728
  import { fileURLToPath } from "url";
6503
6729
 
@@ -6532,8 +6758,8 @@ var cachedPluginVersion;
6532
6758
  function pluginVersion() {
6533
6759
  if (cachedPluginVersion) return cachedPluginVersion;
6534
6760
  try {
6535
- const here = path5.dirname(fileURLToPath(import.meta.url));
6536
- const raw = fs4.readFileSync(path5.join(here, "..", "package.json"), "utf8");
6761
+ const here = path6.dirname(fileURLToPath(import.meta.url));
6762
+ const raw = fs5.readFileSync(path6.join(here, "..", "package.json"), "utf8");
6537
6763
  const version = JSON.parse(raw).version;
6538
6764
  cachedPluginVersion = typeof version === "string" ? version : "unknown";
6539
6765
  } catch {
@@ -6557,7 +6783,7 @@ var opencodeVersionProbe;
6557
6783
  function detectOpencodeVersion(execPath = process.execPath) {
6558
6784
  if (opencodeVersionProbe) return opencodeVersionProbe;
6559
6785
  opencodeVersionProbe = (async () => {
6560
- if (!path5.basename(execPath).toLowerCase().includes("opencode")) {
6786
+ if (!path6.basename(execPath).toLowerCase().includes("opencode")) {
6561
6787
  log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
6562
6788
  return void 0;
6563
6789
  }
@@ -6786,10 +7012,10 @@ function defaultEntryPath() {
6786
7012
  }
6787
7013
  function readOnDiskBuild(entryPath) {
6788
7014
  try {
6789
- const raw = fs5.readFileSync(path6.join(path6.dirname(entryPath), "..", "package.json"), "utf8");
7015
+ const raw = fs6.readFileSync(path7.join(path7.dirname(entryPath), "..", "package.json"), "utf8");
6790
7016
  const version = JSON.parse(raw).version;
6791
7017
  if (typeof version !== "string" || version.length === 0) return void 0;
6792
- const stat = fs5.statSync(entryPath);
7018
+ const stat = fs6.statSync(entryPath);
6793
7019
  return { version, mtimeMs: stat.mtimeMs, size: stat.size };
6794
7020
  } catch {
6795
7021
  return void 0;
@@ -6800,7 +7026,7 @@ function recordLoadedBuild() {
6800
7026
  let mtimeMs;
6801
7027
  let size;
6802
7028
  try {
6803
- const stat = fs5.statSync(entryPath);
7029
+ const stat = fs6.statSync(entryPath);
6804
7030
  mtimeMs = stat.mtimeMs;
6805
7031
  size = stat.size;
6806
7032
  } catch {
@@ -7237,7 +7463,7 @@ Could not build the report: ${message}`;
7237
7463
 
7238
7464
  // src/agent-models.ts
7239
7465
  import { readFile, readdir } from "fs/promises";
7240
- import path7 from "path";
7466
+ import path8 from "path";
7241
7467
 
7242
7468
  // src/models.ts
7243
7469
  var PROVIDER_ID = "claude-code";
@@ -7761,13 +7987,13 @@ async function readAgentMarkdownRecords(directories) {
7761
7987
  const name = entry.slice(0, -3);
7762
7988
  if (records2[name]) continue;
7763
7989
  try {
7764
- const text = await readFile(path7.join(directory, entry), "utf8");
7990
+ const text = await readFile(path8.join(directory, entry), "utf8");
7765
7991
  records2[name] = parseAgentFrontmatter(text);
7766
7992
  const prompt = agentMarkdownBody(text);
7767
7993
  if (prompt) records2[name].prompt = prompt;
7768
7994
  } catch (err) {
7769
7995
  log.debug("failed to read agent markdown", {
7770
- file: path7.join(directory, entry),
7996
+ file: path8.join(directory, entry),
7771
7997
  error: String(err)
7772
7998
  });
7773
7999
  }
@@ -7779,12 +8005,12 @@ function agentDirectories(home, projectDirectory) {
7779
8005
  const directories = [];
7780
8006
  if (projectDirectory) {
7781
8007
  for (const name of AGENT_DIR_NAMES) {
7782
- directories.push(path7.join(projectDirectory, ".opencode", name));
8008
+ directories.push(path8.join(projectDirectory, ".opencode", name));
7783
8009
  }
7784
8010
  }
7785
8011
  if (home) {
7786
8012
  for (const name of AGENT_DIR_NAMES) {
7787
- directories.push(path7.join(home, ".config", "opencode", name));
8013
+ directories.push(path8.join(home, ".config", "opencode", name));
7788
8014
  }
7789
8015
  }
7790
8016
  return directories;
@@ -8276,7 +8502,7 @@ The complete prior conversation appears above. The synthesis instructions follow
8276
8502
  content.push({
8277
8503
  type: "text",
8278
8504
  text: `<conversation_history>
8279
- The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
8505
+ The following is a summary of our conversation so far, replayed as text because no Claude session was available to continue it:
8280
8506
 
8281
8507
  ${historyContext}
8282
8508
 
@@ -8384,14 +8610,14 @@ function getTrailingUserMessages(prompt, opts = {}) {
8384
8610
 
8385
8611
  // src/skill-bridge.ts
8386
8612
  import * as crypto3 from "crypto";
8387
- import * as fs6 from "fs";
8613
+ import * as fs7 from "fs";
8388
8614
  import * as os3 from "os";
8389
- import * as path8 from "path";
8615
+ import * as path9 from "path";
8390
8616
  import { fileURLToPath as fileURLToPath3 } from "url";
8391
8617
  function bundledSkillsDir() {
8392
8618
  try {
8393
8619
  const here = fileURLToPath3(import.meta.url);
8394
- const dir = path8.resolve(path8.dirname(here), "..", "skills");
8620
+ const dir = path9.resolve(path9.dirname(here), "..", "skills");
8395
8621
  return dirExists2(dir) ? dir : null;
8396
8622
  } catch {
8397
8623
  return null;
@@ -8399,16 +8625,16 @@ function bundledSkillsDir() {
8399
8625
  }
8400
8626
  function dirExists2(p) {
8401
8627
  try {
8402
- return fs6.statSync(p).isDirectory();
8628
+ return fs7.statSync(p).isDirectory();
8403
8629
  } catch {
8404
8630
  return false;
8405
8631
  }
8406
8632
  }
8407
8633
  function realPath(p) {
8408
8634
  try {
8409
- return fs6.realpathSync(p);
8635
+ return fs7.realpathSync(p);
8410
8636
  } catch {
8411
- return path8.resolve(p);
8637
+ return path9.resolve(p);
8412
8638
  }
8413
8639
  }
8414
8640
  function envEnabled(name) {
@@ -8429,10 +8655,10 @@ function declaredSkillName(source) {
8429
8655
  return SAFE_SKILL_NAME.test(name) ? name : null;
8430
8656
  }
8431
8657
  function readSkill(root, entry) {
8432
- const dir = path8.join(root, entry);
8658
+ const dir = path9.join(root, entry);
8433
8659
  let source;
8434
8660
  try {
8435
- source = fs6.readFileSync(path8.join(dir, "SKILL.md"), "utf8");
8661
+ source = fs7.readFileSync(path9.join(dir, "SKILL.md"), "utf8");
8436
8662
  } catch {
8437
8663
  return null;
8438
8664
  }
@@ -8450,47 +8676,47 @@ function skillRoots(cwd) {
8450
8676
  const roots = [];
8451
8677
  const seen = /* @__PURE__ */ new Set();
8452
8678
  const push = (p) => {
8453
- const abs = path8.resolve(p);
8679
+ const abs = path9.resolve(p);
8454
8680
  if (seen.has(abs)) return;
8455
8681
  seen.add(abs);
8456
8682
  if (dirExists2(abs)) roots.push(abs);
8457
8683
  };
8458
8684
  const home = os3.homedir();
8459
- const homeDir = home ? path8.resolve(home) : null;
8460
- let current = path8.resolve(cwd);
8685
+ const homeDir = home ? path9.resolve(home) : null;
8686
+ let current = path9.resolve(cwd);
8461
8687
  while (true) {
8462
- push(path8.join(current, ".opencode", "skills"));
8688
+ push(path9.join(current, ".opencode", "skills"));
8463
8689
  if (current !== homeDir) {
8464
- push(path8.join(current, ".claude", "skills"));
8465
- push(path8.join(current, ".agents", "skills"));
8690
+ push(path9.join(current, ".claude", "skills"));
8691
+ push(path9.join(current, ".agents", "skills"));
8466
8692
  }
8467
- const parent = path8.dirname(current);
8693
+ const parent = path9.dirname(current);
8468
8694
  if (parent === current) break;
8469
8695
  current = parent;
8470
8696
  }
8471
- if (homeDir) push(path8.join(homeDir, ".opencode", "skills"));
8697
+ if (homeDir) push(path9.join(homeDir, ".opencode", "skills"));
8472
8698
  const envDir = process.env.OPENCODE_CONFIG_DIR;
8473
8699
  if (envDir) {
8474
- push(path8.join(envDir, "skills"));
8475
- push(path8.join(envDir, "skill"));
8700
+ push(path9.join(envDir, "skills"));
8701
+ push(path9.join(envDir, "skill"));
8476
8702
  }
8477
- const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path8.join(homeDir, ".config") : null);
8703
+ const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path9.join(homeDir, ".config") : null);
8478
8704
  if (xdg) {
8479
- push(path8.join(xdg, "opencode", "skills"));
8480
- push(path8.join(xdg, "opencode", "skill"));
8705
+ push(path9.join(xdg, "opencode", "skills"));
8706
+ push(path9.join(xdg, "opencode", "skill"));
8481
8707
  }
8482
8708
  if (homeDir && !envEnabled("OPENCODE_DISABLE_EXTERNAL_SKILLS")) {
8483
8709
  if (!envEnabled("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")) {
8484
- push(path8.join(homeDir, ".claude", "skills"));
8710
+ push(path9.join(homeDir, ".claude", "skills"));
8485
8711
  }
8486
- push(path8.join(homeDir, ".agents", "skills"));
8712
+ push(path9.join(homeDir, ".agents", "skills"));
8487
8713
  }
8488
8714
  return roots;
8489
8715
  }
8490
8716
  function collectSkills(root, claimed, found) {
8491
8717
  let entries;
8492
8718
  try {
8493
- entries = fs6.readdirSync(root, { withFileTypes: true });
8719
+ entries = fs7.readdirSync(root, { withFileTypes: true });
8494
8720
  } catch {
8495
8721
  return;
8496
8722
  }
@@ -8530,23 +8756,23 @@ function discoverBundledSkills() {
8530
8756
  }
8531
8757
  function claudeConfigDir(configDir) {
8532
8758
  const explicit = configDir ?? process.env.CLAUDE_CONFIG_DIR;
8533
- if (explicit) return path8.resolve(expandHome(explicit));
8759
+ if (explicit) return path9.resolve(expandHome(explicit));
8534
8760
  const home = os3.homedir();
8535
- return home ? path8.resolve(home, ".claude") : null;
8761
+ return home ? path9.resolve(home, ".claude") : null;
8536
8762
  }
8537
8763
  function installedPluginSkillRoots(configDir, cwd) {
8538
8764
  if (!configDir) return [];
8539
8765
  let parsed;
8540
8766
  try {
8541
8767
  parsed = JSON.parse(
8542
- fs6.readFileSync(path8.join(configDir, "plugins", "installed_plugins.json"), "utf8")
8768
+ fs7.readFileSync(path9.join(configDir, "plugins", "installed_plugins.json"), "utf8")
8543
8769
  );
8544
8770
  } catch {
8545
8771
  return [];
8546
8772
  }
8547
8773
  const plugins = parsed?.plugins;
8548
8774
  if (!plugins || typeof plugins !== "object") return [];
8549
- const workspace = path8.resolve(cwd);
8775
+ const workspace = path9.resolve(cwd);
8550
8776
  const roots = [];
8551
8777
  for (const installs of Object.values(plugins)) {
8552
8778
  if (!Array.isArray(installs)) continue;
@@ -8554,28 +8780,28 @@ function installedPluginSkillRoots(configDir, cwd) {
8554
8780
  const entry = install;
8555
8781
  if (typeof entry?.installPath !== "string" || !entry.installPath) continue;
8556
8782
  if (entry.scope !== "user" && !coversWorkspace(entry.projectPath, workspace)) continue;
8557
- roots.push(path8.join(entry.installPath, "skills"));
8783
+ roots.push(path9.join(entry.installPath, "skills"));
8558
8784
  }
8559
8785
  }
8560
8786
  return roots;
8561
8787
  }
8562
8788
  function coversWorkspace(projectPath, workspace) {
8563
8789
  if (typeof projectPath !== "string" || !projectPath) return false;
8564
- const root = path8.resolve(projectPath);
8790
+ const root = path9.resolve(projectPath);
8565
8791
  if (root === workspace) return true;
8566
- const rel = path8.relative(root, workspace);
8567
- return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
8792
+ const rel = path9.relative(root, workspace);
8793
+ return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
8568
8794
  }
8569
8795
  function discoverNativeClaudeSkills(opts) {
8570
8796
  const found = [];
8571
8797
  const scanned = /* @__PURE__ */ new Set();
8572
8798
  const scan = (root, scope) => {
8573
- const abs = path8.resolve(root);
8799
+ const abs = path9.resolve(root);
8574
8800
  if (scanned.has(abs)) return;
8575
8801
  scanned.add(abs);
8576
8802
  let entries;
8577
8803
  try {
8578
- entries = fs6.readdirSync(abs, { withFileTypes: true });
8804
+ entries = fs7.readdirSync(abs, { withFileTypes: true });
8579
8805
  } catch {
8580
8806
  return;
8581
8807
  }
@@ -8587,13 +8813,13 @@ function discoverNativeClaudeSkills(opts) {
8587
8813
  }
8588
8814
  };
8589
8815
  const configDir = claudeConfigDir(opts.configDir);
8590
- if (configDir) scan(path8.join(configDir, "skills"), "user");
8816
+ if (configDir) scan(path9.join(configDir, "skills"), "user");
8591
8817
  const home = os3.homedir();
8592
- const homeDir = home ? path8.resolve(home) : null;
8593
- let current = path8.resolve(opts.cwd);
8818
+ const homeDir = home ? path9.resolve(home) : null;
8819
+ let current = path9.resolve(opts.cwd);
8594
8820
  while (true) {
8595
- if (current !== homeDir) scan(path8.join(current, ".claude", "skills"), "project");
8596
- const parent = path8.dirname(current);
8821
+ if (current !== homeDir) scan(path9.join(current, ".claude", "skills"), "project");
8822
+ const parent = path9.dirname(current);
8597
8823
  if (parent === current) break;
8598
8824
  current = parent;
8599
8825
  }
@@ -8627,10 +8853,10 @@ function dropNativelyLoadedSkills(skills, native) {
8627
8853
  }
8628
8854
  function readBundledSkillInfos() {
8629
8855
  return discoverBundledSkills().flatMap((skill) => {
8630
- const file = path8.join(skill.dir, "SKILL.md");
8856
+ const file = path9.join(skill.dir, "SKILL.md");
8631
8857
  let text;
8632
8858
  try {
8633
- text = fs6.readFileSync(file, "utf8");
8859
+ text = fs7.readFileSync(file, "utf8");
8634
8860
  } catch {
8635
8861
  return [];
8636
8862
  }
@@ -8654,15 +8880,15 @@ function readBundledSkillInfos() {
8654
8880
  }
8655
8881
  function linkSkill(source, target) {
8656
8882
  try {
8657
- fs6.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
8883
+ fs7.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
8658
8884
  return;
8659
8885
  } catch {
8660
- fs6.cpSync(source, target, { recursive: true, dereference: true });
8886
+ fs7.cpSync(source, target, { recursive: true, dereference: true });
8661
8887
  }
8662
8888
  }
8663
8889
  function fileExists2(p) {
8664
8890
  try {
8665
- return fs6.statSync(p).isFile();
8891
+ return fs7.statSync(p).isFile();
8666
8892
  } catch {
8667
8893
  return false;
8668
8894
  }
@@ -8671,14 +8897,14 @@ function buildSkillPluginDir(skills) {
8671
8897
  if (skills.length === 0) return null;
8672
8898
  const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
8673
8899
  const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
8674
- const root = path8.join(pluginTmpDir(), `skills-${hash}`);
8675
- const manifest = path8.join(root, ".claude-plugin", "plugin.json");
8900
+ const root = path9.join(pluginTmpDir(), `skills-${hash}`);
8901
+ const manifest = path9.join(root, ".claude-plugin", "plugin.json");
8676
8902
  if (fileExists2(manifest)) return root;
8677
8903
  try {
8678
- fs6.rmSync(root, { recursive: true, force: true });
8679
- fs6.mkdirSync(path8.join(root, ".claude-plugin"), { recursive: true });
8680
- fs6.mkdirSync(path8.join(root, "skills"), { recursive: true });
8681
- fs6.writeFileSync(
8904
+ fs7.rmSync(root, { recursive: true, force: true });
8905
+ fs7.mkdirSync(path9.join(root, ".claude-plugin"), { recursive: true });
8906
+ fs7.mkdirSync(path9.join(root, "skills"), { recursive: true });
8907
+ fs7.writeFileSync(
8682
8908
  manifest,
8683
8909
  JSON.stringify(
8684
8910
  {
@@ -8691,7 +8917,7 @@ function buildSkillPluginDir(skills) {
8691
8917
  { encoding: "utf8", mode: 384 }
8692
8918
  );
8693
8919
  for (const skill of skills) {
8694
- linkSkill(skill.dir, path8.join(root, "skills", skill.name));
8920
+ linkSkill(skill.dir, path9.join(root, "skills", skill.name));
8695
8921
  }
8696
8922
  } catch (err) {
8697
8923
  log.warn("failed to stage opencode skill plugin dir", {
@@ -8707,7 +8933,7 @@ function registerBundledSkillPath(config) {
8707
8933
  if (!dir) return false;
8708
8934
  config.skills ??= {};
8709
8935
  const paths = config.skills.paths ??= [];
8710
- if (paths.some((p) => path8.resolve(p) === dir)) return false;
8936
+ if (paths.some((p) => path9.resolve(p) === dir)) return false;
8711
8937
  paths.push(dir);
8712
8938
  return true;
8713
8939
  }
@@ -8761,12 +8987,13 @@ import { join as join10 } from "path";
8761
8987
 
8762
8988
  // src/claude-session-bun.ts
8763
8989
  import * as os4 from "os";
8764
- import * as fs7 from "fs";
8765
- import * as path9 from "path";
8990
+ import * as fs8 from "fs";
8991
+ import * as path10 from "path";
8766
8992
  import { execFileSync } from "child_process";
8767
8993
  import { randomUUID as randomUUID4 } from "crypto";
8994
+ import { StringDecoder } from "string_decoder";
8768
8995
  function resolveClaude(cmd = "claude") {
8769
- if (path9.isAbsolute(cmd) && fs7.existsSync(cmd)) return cmd;
8996
+ if (path10.isAbsolute(cmd) && fs8.existsSync(cmd)) return cmd;
8770
8997
  const viaBun = Bun.which(cmd);
8771
8998
  if (viaBun) return viaBun;
8772
8999
  const isWin = os4.platform() === "win32";
@@ -8775,24 +9002,30 @@ function resolveClaude(cmd = "claude") {
8775
9002
  encoding: "utf8",
8776
9003
  stdio: ["ignore", "pipe", "ignore"]
8777
9004
  });
8778
- const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs7.existsSync(p));
9005
+ const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs8.existsSync(p));
8779
9006
  if (first) return first;
8780
9007
  } catch {
8781
9008
  }
8782
9009
  throw new Error(`Could not resolve command on PATH: ${cmd}`);
8783
9010
  }
8784
9011
  function encodeCwd(cwd) {
8785
- const resolved = path9.resolve(cwd);
9012
+ const resolved = path10.resolve(cwd);
8786
9013
  let real = resolved;
8787
9014
  try {
8788
- real = fs7.realpathSync.native(resolved);
9015
+ real = fs8.realpathSync.native(resolved);
8789
9016
  } catch {
8790
9017
  }
8791
9018
  return real.replace(/[^a-zA-Z0-9]/g, "-");
8792
9019
  }
8793
9020
  var bunPtySpawner = (argv, opts) => {
8794
9021
  const [command, ...args] = argv;
8795
- return Bun.spawn([resolveClaude(command ?? "claude"), ...args], {
9022
+ const resolved = resolveClaude(command ?? "claude");
9023
+ if (os4.platform() === "win32" && isBatchFile(resolved)) {
9024
+ throw new Error(
9025
+ `The interactive transport cannot run a Windows batch shim (${resolved}). Point \`cliPath\` at claude.exe, or use the default headless transport.`
9026
+ );
9027
+ }
9028
+ return Bun.spawn([resolved, ...args], {
8796
9029
  cwd: opts.cwd,
8797
9030
  env: opts.env,
8798
9031
  terminal: {
@@ -9007,6 +9240,123 @@ function isInterruptRecord(rec) {
9007
9240
  function isTurnDurationRecord(rec) {
9008
9241
  return rec?.type === "system" && rec.subtype === "turn_duration";
9009
9242
  }
9243
+ var TRANSCRIPT_CHUNK_BYTES = 1 << 20;
9244
+ var TranscriptCursor = class {
9245
+ /** The path is asked for on every poll rather than kept, because it is a
9246
+ * field of the session and a test may repoint it; a path that changes is
9247
+ * a different file, which the `dev:ino` check already starts over on. */
9248
+ constructor(resolvePath) {
9249
+ this.resolvePath = resolvePath;
9250
+ }
9251
+ resolvePath;
9252
+ /** Bytes of the current file already read. */
9253
+ offset = 0;
9254
+ /** `dev:ino` of the file those bytes came from, null before the first
9255
+ * successful stat. A different one is a new file, not growth. */
9256
+ fileId = null;
9257
+ /** Holds back an incomplete trailing UTF-8 sequence between two reads. */
9258
+ decoder = new StringDecoder("utf8");
9259
+ /** What was read after the last newline: a record the CLI is part way
9260
+ * through writing, completed by a later read. */
9261
+ partial = "";
9262
+ /** Complete lines seen since byte 0 of the current file. */
9263
+ seen = 0;
9264
+ /** Complete lines handed on: the line-index cursor. */
9265
+ taken = 0;
9266
+ /** Complete lines read and not handed on yet, oldest first. */
9267
+ queue = [];
9268
+ stats = { polls: 0, reads: 0, bytes: 0 };
9269
+ /** Complete lines in the file now. */
9270
+ lineCount() {
9271
+ this.poll(true);
9272
+ return this.seen;
9273
+ }
9274
+ /** The complete lines not handed on yet, oldest first. They stay queued
9275
+ * until `take` says how many of them were used. */
9276
+ pending() {
9277
+ this.poll(true);
9278
+ return this.queue;
9279
+ }
9280
+ /** Whether the file holds a complete line this cursor has not handed on. */
9281
+ hasUnread() {
9282
+ return this.pending().length > 0;
9283
+ }
9284
+ /** Mark the first `count` pending lines as handed on. */
9285
+ take(count) {
9286
+ const used = Math.min(count, this.queue.length);
9287
+ if (used <= 0) return;
9288
+ this.queue.splice(0, used);
9289
+ this.taken += used;
9290
+ }
9291
+ /** Hand on nothing of what is in the file now: the records written before a
9292
+ * turn started belong to the turn before it. */
9293
+ skipToEnd() {
9294
+ this.poll(false);
9295
+ this.taken = Math.max(this.taken, this.seen);
9296
+ this.queue.length = 0;
9297
+ }
9298
+ /** Read whatever was appended since the last look. */
9299
+ poll(collect) {
9300
+ this.stats.polls++;
9301
+ const filePath = this.resolvePath();
9302
+ let stat;
9303
+ try {
9304
+ stat = fs8.statSync(filePath);
9305
+ } catch {
9306
+ return;
9307
+ }
9308
+ const fileId = `${stat.dev}:${stat.ino}`;
9309
+ if (this.fileId !== null && (fileId !== this.fileId || stat.size < this.offset)) {
9310
+ this.offset = 0;
9311
+ this.seen = 0;
9312
+ this.partial = "";
9313
+ this.decoder = new StringDecoder("utf8");
9314
+ this.queue.length = 0;
9315
+ }
9316
+ this.fileId = fileId;
9317
+ if (stat.size <= this.offset) return;
9318
+ let fd = null;
9319
+ try {
9320
+ fd = fs8.openSync(filePath, "r");
9321
+ const buffer = Buffer.allocUnsafe(
9322
+ Math.min(TRANSCRIPT_CHUNK_BYTES, stat.size - this.offset)
9323
+ );
9324
+ while (this.offset < stat.size) {
9325
+ const want = Math.min(buffer.length, stat.size - this.offset);
9326
+ const read = fs8.readSync(fd, buffer, 0, want, this.offset);
9327
+ this.stats.reads++;
9328
+ if (read <= 0) break;
9329
+ this.offset += read;
9330
+ this.stats.bytes += read;
9331
+ this.ingest(this.decoder.write(buffer.subarray(0, read)), collect);
9332
+ }
9333
+ } catch {
9334
+ } finally {
9335
+ if (fd !== null) {
9336
+ try {
9337
+ fs8.closeSync(fd);
9338
+ } catch {
9339
+ }
9340
+ }
9341
+ }
9342
+ }
9343
+ /** Split decoded text into complete lines, keeping the unterminated tail. */
9344
+ ingest(text, collect) {
9345
+ if (!text) return;
9346
+ this.partial += text;
9347
+ let start = 0;
9348
+ for (; ; ) {
9349
+ const newline = this.partial.indexOf("\n", start);
9350
+ if (newline === -1) break;
9351
+ if (collect && this.seen >= this.taken) {
9352
+ this.queue.push(this.partial.slice(start, newline));
9353
+ }
9354
+ this.seen++;
9355
+ start = newline + 1;
9356
+ }
9357
+ if (start > 0) this.partial = this.partial.slice(start);
9358
+ }
9359
+ };
9010
9360
  var normalizedPrompt = (text) => text.replace(/\s+/g, " ").trim();
9011
9361
  function isOwnPromptRecord(rec, normalized) {
9012
9362
  const content = rec?.type === "user" ? rec.message?.content : void 0;
@@ -9014,12 +9364,12 @@ function isOwnPromptRecord(rec, normalized) {
9014
9364
  }
9015
9365
  function resolveConfigDir(configDir) {
9016
9366
  const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
9017
- if (!value) return path9.join(os4.homedir(), ".claude");
9367
+ if (!value) return path10.join(os4.homedir(), ".claude");
9018
9368
  if (value === "~") return os4.homedir();
9019
9369
  if (value.startsWith("~/") || value.startsWith("~\\")) {
9020
- return path9.join(os4.homedir(), value.slice(2));
9370
+ return path10.join(os4.homedir(), value.slice(2));
9021
9371
  }
9022
- return path9.resolve(value);
9372
+ return path10.resolve(value);
9023
9373
  }
9024
9374
  function fatalScreenMessage(kind, configDir, cwd) {
9025
9375
  if (kind === "trust") {
@@ -9038,8 +9388,8 @@ var ClaudeSession = class {
9038
9388
  /** The newest terminal output, ANSI included, for diagnostics. */
9039
9389
  raw = "";
9040
9390
  proc = null;
9041
- cursor = 0;
9042
- // index into transcript split('\n')
9391
+ /** The transcript, read forwards only; its `taken` is the old line cursor. */
9392
+ transcript;
9043
9393
  lastDataAt = 0;
9044
9394
  exited = false;
9045
9395
  exitCode = null;
@@ -9083,7 +9433,7 @@ var ClaudeSession = class {
9083
9433
  abandonTurn = false;
9084
9434
  o;
9085
9435
  constructor(opts = {}) {
9086
- this.cwd = path9.resolve(opts.cwd ?? process.cwd());
9436
+ this.cwd = path10.resolve(opts.cwd ?? process.cwd());
9087
9437
  this.configDir = resolveConfigDir(opts.configDir);
9088
9438
  this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
9089
9439
  this.signal = opts.signal;
@@ -9095,12 +9445,13 @@ var ClaudeSession = class {
9095
9445
  this.onExit = opts.onExit;
9096
9446
  this.spawnPty = opts.spawnPty ?? bunPtySpawner;
9097
9447
  this.sessionId = opts.resumeSessionId ?? randomUUID4();
9098
- this.jsonlPath = path9.join(
9448
+ this.jsonlPath = path10.join(
9099
9449
  this.configDir,
9100
9450
  "projects",
9101
9451
  encodeCwd(this.cwd),
9102
9452
  `${this.sessionId}.jsonl`
9103
9453
  );
9454
+ this.transcript = new TranscriptCursor(() => this.jsonlPath);
9104
9455
  this.o = {
9105
9456
  cwd: this.cwd,
9106
9457
  cliPath: opts.cliPath,
@@ -9205,7 +9556,12 @@ var ClaudeSession = class {
9205
9556
  };
9206
9557
  proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
9207
9558
  await this.waitForBoot();
9208
- this.cursor = this.lineCount();
9559
+ this.transcript.skipToEnd();
9560
+ }
9561
+ /** How much this session has read off the transcript. Read-only, and the
9562
+ * one thing `scripts/bench-transcript-poll.ts` measures. */
9563
+ get transcriptStats() {
9564
+ return this.transcript.stats;
9209
9565
  }
9210
9566
  onData(chunk) {
9211
9567
  this.lastDataAt = Date.now();
@@ -9352,22 +9708,10 @@ var ClaudeSession = class {
9352
9708
  while (Date.now() < until) {
9353
9709
  await delay(80);
9354
9710
  if (this.aborted || this.exited) return;
9355
- if (this.lineCount() > this.cursor) return;
9711
+ if (this.transcript.hasUnread()) return;
9356
9712
  }
9357
9713
  }
9358
9714
  }
9359
- readRawLines() {
9360
- try {
9361
- return fs7.readFileSync(this.jsonlPath, "utf8").split("\n");
9362
- } catch {
9363
- return [];
9364
- }
9365
- }
9366
- /** Count of complete lines (split('\n') minus the trailing/partial element). */
9367
- lineCount() {
9368
- const lines = this.readRawLines();
9369
- return lines.length > 0 ? lines.length - 1 : 0;
9370
- }
9371
9715
  rawTail(max = 600) {
9372
9716
  const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
9373
9717
  return clean.length > max ? clean.slice(-max) : clean;
@@ -9400,7 +9744,7 @@ var ClaudeSession = class {
9400
9744
  this.turn = new Promise((resolve6) => settle = resolve6);
9401
9745
  this.turnDenied = [];
9402
9746
  this.abandonTurn = false;
9403
- this.cursor = Math.max(this.cursor, this.lineCount());
9747
+ this.transcript.skipToEnd();
9404
9748
  this.screen = "";
9405
9749
  try {
9406
9750
  if (this.o.bracketedPaste) {
@@ -9465,30 +9809,26 @@ var ClaudeSession = class {
9465
9809
  /**
9466
9810
  * Read every complete transcript record past the cursor into the running
9467
9811
  * turn. Returns whether anything new was read. The one place a record is
9468
- * interpreted, shared by the poll loop and `flushTranscript`.
9812
+ * interpreted, shared by the poll loop and `flushTranscript`, and the only
9813
+ * one that consumes what `TranscriptCursor` has queued.
9469
9814
  */
9470
9815
  scanTranscript() {
9471
9816
  const scan = this.scan;
9472
9817
  if (!scan || scan.end) return false;
9473
- const lines = this.readRawLines();
9474
- const lastComplete = lines.length - 1;
9475
- if (lastComplete <= this.cursor) return false;
9476
- for (let i = this.cursor; i < lastComplete && !scan.end; i++) {
9818
+ const lines = this.transcript.pending().slice();
9819
+ if (lines.length === 0) return false;
9820
+ let used = 0;
9821
+ for (let i = 0; i < lines.length && !scan.end; i++) {
9477
9822
  const s = lines[i];
9478
- if (!s || !s.trim()) {
9479
- this.cursor = i + 1;
9480
- continue;
9481
- }
9823
+ used = i + 1;
9824
+ if (!s || !s.trim()) continue;
9482
9825
  let rec = null;
9483
9826
  try {
9484
9827
  rec = JSON.parse(s);
9485
9828
  } catch {
9486
9829
  }
9487
9830
  if (scan.awaitingOwnPrompt !== null) {
9488
- if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt)) {
9489
- this.cursor = i + 1;
9490
- continue;
9491
- }
9831
+ if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt)) continue;
9492
9832
  scan.awaitingOwnPrompt = null;
9493
9833
  }
9494
9834
  if (rec) this.notePlanApproval(rec);
@@ -9508,9 +9848,8 @@ var ClaudeSession = class {
9508
9848
  scan.end = scan.stopReason ? "stop" : "ended";
9509
9849
  }
9510
9850
  }
9511
- this.cursor = i + 1;
9512
9851
  }
9513
- if (!scan.end) this.cursor = lastComplete;
9852
+ this.transcript.take(used);
9514
9853
  scan.lastBeat = Date.now();
9515
9854
  scan.stopSeenAt = Date.now();
9516
9855
  return true;
@@ -10299,13 +10638,13 @@ function spawnInteractiveProcess(opts) {
10299
10638
  }
10300
10639
 
10301
10640
  // src/prompts.ts
10302
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
10641
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
10303
10642
  import { homedir as homedir8 } from "os";
10304
10643
  import { randomUUID as randomUUID6 } from "crypto";
10305
10644
  import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
10306
- function readPromptFileIfPresent(path11) {
10645
+ function readPromptFileIfPresent(path12) {
10307
10646
  try {
10308
- const content = readFileSync7(path11, "utf8").trim();
10647
+ const content = readFileSync6(path12, "utf8").trim();
10309
10648
  return content || void 0;
10310
10649
  } catch {
10311
10650
  return void 0;
@@ -10410,9 +10749,9 @@ function extractSystemMessages(prompt) {
10410
10749
  }
10411
10750
  var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
10412
10751
  function userAuthoredInstructions(forwarded, opts = {}) {
10413
- const readFile2 = opts.readFile ?? ((path11) => {
10752
+ const readFile2 = opts.readFile ?? ((path12) => {
10414
10753
  try {
10415
- return readFileSync7(path11, "utf8");
10754
+ return readFileSync6(path12, "utf8");
10416
10755
  } catch {
10417
10756
  return null;
10418
10757
  }
@@ -10424,16 +10763,16 @@ function userAuthoredInstructions(forwarded, opts = {}) {
10424
10763
  }
10425
10764
  const seen = /* @__PURE__ */ new Set();
10426
10765
  for (const match of forwarded.matchAll(/^Instructions from: (.+)$/gm)) {
10427
- const path11 = match[1].trim();
10428
- if (seen.has(path11) || !isAbsolute3(path11)) continue;
10429
- if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path11))) continue;
10430
- seen.add(path11);
10431
- const file = readFile2(path11);
10766
+ const path12 = match[1].trim();
10767
+ if (seen.has(path12) || !isAbsolute3(path12)) continue;
10768
+ if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path12))) continue;
10769
+ seen.add(path12);
10770
+ const file = readFile2(path12);
10432
10771
  if (!file?.trim()) continue;
10433
10772
  const start = match.index + match[0].length + 1;
10434
10773
  const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
10435
10774
  if (content === null) continue;
10436
- out.push({ text: `Instructions from: ${path11}
10775
+ out.push({ text: `Instructions from: ${path12}
10437
10776
  ${content}`, content });
10438
10777
  }
10439
10778
  return out;
@@ -10470,10 +10809,10 @@ ${options.compressionSummary.trim()}`
10470
10809
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
10471
10810
  const content = parts.join("\n\n");
10472
10811
  if (!content) return void 0;
10473
- const path11 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
10812
+ const path12 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
10474
10813
  try {
10475
- writeFileSync6(path11, content, { encoding: "utf8", mode: 384 });
10476
- return path11;
10814
+ writeFileSync6(path12, content, { encoding: "utf8", mode: 384 });
10815
+ return path12;
10477
10816
  } catch (err) {
10478
10817
  log.warn("failed to write system prompt file", { error: String(err) });
10479
10818
  return void 0;
@@ -11326,6 +11665,7 @@ function createTurnState(init) {
11326
11665
  accountLimitHit: null,
11327
11666
  apiErrorTextShown: null,
11328
11667
  usageLimitNote: false,
11668
+ compactionFailureText: null,
11329
11669
  accountBlock: null,
11330
11670
  modelRefusal: null,
11331
11671
  drainBuffer: [],
@@ -12420,6 +12760,7 @@ ${plan}
12420
12760
  resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
12421
12761
  }));
12422
12762
  const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
12763
+ const compactionApiError = ctx.compactionMode && (apiErrorReply || typeof msg.error === "string");
12423
12764
  if (limitFromReply) {
12424
12765
  state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
12425
12766
  const said = limitFactsFromText(
@@ -12441,6 +12782,10 @@ ${plan}
12441
12782
  for (const block2 of msg.message.content) {
12442
12783
  if (block2.type === "text" && block2.text) {
12443
12784
  if (limitErrorReply) continue;
12785
+ if (compactionApiError) {
12786
+ state.compactionFailureText ??= block2.text;
12787
+ continue;
12788
+ }
12444
12789
  if (apiErrorReply) state.apiErrorTextShown = block2.text;
12445
12790
  resetLastVisibleTextBlock(state);
12446
12791
  const blockId = state.startTextBlock();
@@ -12666,7 +13011,14 @@ ${plan}
12666
13011
  state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
12667
13012
  // and says what to run; two notes about one failure is one too many.
12668
13013
  !state.accountBlock && msg.is_error === true;
12669
- if (!state.currentTextId && msg.is_error && // Already on screen from the reply frame, word for word.
13014
+ const compactionFailure = ctx.compactionMode && (msg.is_error === true || typeof msg.subtype === "string" && msg.subtype !== "success");
13015
+ if (compactionFailure) {
13016
+ if (typeof msg.result === "string" && msg.result.trim()) {
13017
+ state.compactionFailureText ??= msg.result.trim();
13018
+ }
13019
+ state.resultFailure ??= state.accountBlock ?? (state.accountLimitHit ? "usage_limit" : void 0) ?? (typeof msg.subtype === "string" && msg.subtype !== "success" ? msg.subtype : "error");
13020
+ }
13021
+ if (!state.currentTextId && msg.is_error && !compactionFailure && // Already on screen from the reply frame, word for word.
12670
13022
  !(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
12671
13023
  const errId = state.startTextBlock();
12672
13024
  state.controller.enqueue({
@@ -12681,21 +13033,23 @@ ${plan}
12681
13033
  if (state.accountBlock && msg.is_error) {
12682
13034
  state.resultFailure ??= state.accountBlock;
12683
13035
  const offeringSwitch = ctx.failoverAskActive;
12684
- state.controller.enqueue({
12685
- type: "text-delta",
12686
- id: state.startTextBlock(),
12687
- delta: formatAccountBlockNote({
12688
- kind: state.accountBlock,
12689
- account: ctx.sourceAccount,
12690
- configDir: ctx.config.configDir,
12691
- offeringSwitch,
12692
- // Read only when the form is not taking the turn, which is the
12693
- // default: moving the work by hand is then the operator's only
12694
- // route and nothing else on screen says it exists (h #g194).
12695
- candidates: ctx.failoverAccounts
12696
- })
12697
- });
12698
- state.endTextBlock();
13036
+ if (!compactionFailure) {
13037
+ state.controller.enqueue({
13038
+ type: "text-delta",
13039
+ id: state.startTextBlock(),
13040
+ delta: formatAccountBlockNote({
13041
+ kind: state.accountBlock,
13042
+ account: ctx.sourceAccount,
13043
+ configDir: ctx.config.configDir,
13044
+ offeringSwitch,
13045
+ // Read only when the form is not taking the turn, which is the
13046
+ // default: moving the work by hand is then the operator's only
13047
+ // route and nothing else on screen says it exists (h #g194).
13048
+ candidates: ctx.failoverAccounts
13049
+ })
13050
+ });
13051
+ state.endTextBlock();
13052
+ }
12699
13053
  log.warn(`Claude account "${ctx.sourceAccount}" cannot serve requests`, {
12700
13054
  sessionKey: state.sessionKey,
12701
13055
  kind: state.accountBlock,
@@ -12705,11 +13059,14 @@ ${plan}
12705
13059
  const failure = describeResultFailure(msg);
12706
13060
  if (failure) {
12707
13061
  state.resultFailure = msg.subtype;
12708
- state.controller.enqueue({
12709
- type: "text-delta",
12710
- id: state.startTextBlock(),
12711
- delta: formatResultFailureNote(failure)
12712
- });
13062
+ if (!compactionFailure) {
13063
+ state.controller.enqueue({
13064
+ type: "text-delta",
13065
+ id: state.startTextBlock(),
13066
+ delta: formatResultFailureNote(failure)
13067
+ });
13068
+ }
13069
+ if (compactionFailure) state.compactionFailureText ??= failure;
12713
13070
  log.warn(failure, { sessionKey: state.sessionKey, subtype: msg.subtype });
12714
13071
  }
12715
13072
  const turnStats = extractTurnStats(msg);
@@ -13519,13 +13876,18 @@ var ClaudeCodeLanguageModel = class {
13519
13876
  }
13520
13877
  }
13521
13878
  }
13879
+ let replayReason = "no-session-for-key";
13522
13880
  if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
13523
13881
  const configDir = resolveConfigDir(this.config.configDir);
13882
+ const transcriptPath = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
13524
13883
  const resumePoint = findResumePoint({
13525
13884
  sessionKey: sk,
13526
13885
  prompt: options.prompt,
13527
13886
  cliPath,
13528
- transcriptPath: (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
13887
+ transcriptPath,
13888
+ onRefused: (reason) => {
13889
+ replayReason = reason;
13890
+ }
13529
13891
  });
13530
13892
  if (resumePoint) {
13531
13893
  setClaudeSessionId(sk, resumePoint.claudeSessionId);
@@ -13534,8 +13896,47 @@ var ClaudeCodeLanguageModel = class {
13534
13896
  sessionKey: sk,
13535
13897
  matchedMessages: resumePoint.matched
13536
13898
  });
13899
+ } else if (this.config.resumeAcrossModelChanges !== false) {
13900
+ const sibling = findSiblingResumePoint({
13901
+ sessionKey: sk,
13902
+ prompt: options.prompt,
13903
+ cliPath,
13904
+ transcriptPath,
13905
+ // `claudeSessionIsWriting`, not `getActiveProcess`: this walks every
13906
+ // sibling key, and that one refreshes LRU order and cancels idle
13907
+ // timers on everything it touches (the same reason the fork lookup
13908
+ // above uses it).
13909
+ isBusy: claudeSessionIsWriting,
13910
+ onRefused: (reason) => {
13911
+ replayReason = reason;
13912
+ }
13913
+ });
13914
+ if (sibling) {
13915
+ deleteActiveProcess(sibling.siblingKey);
13916
+ const carried = transferClaudeSession(sibling.siblingKey, sk);
13917
+ if (carried) {
13918
+ includeHistoryContext = false;
13919
+ log.notice(
13920
+ "continuing this conversation's claude session under the new model or effort instead of replaying it",
13921
+ {
13922
+ sessionKey: sk,
13923
+ siblingKey: sibling.siblingKey,
13924
+ matchedMessages: sibling.matched
13925
+ }
13926
+ );
13927
+ } else {
13928
+ replayReason = "sibling-carry-over-failed";
13929
+ }
13930
+ }
13537
13931
  }
13538
13932
  }
13933
+ if (includeHistoryContext && !compactionMode) {
13934
+ log.notice("replaying the conversation as text: no claude session was available to continue", {
13935
+ sessionKey: sk,
13936
+ reason: replayReason,
13937
+ messages: options.prompt.length
13938
+ });
13939
+ }
13539
13940
  if (this.config.forkSessions === true && !compactionMode) {
13540
13941
  recordForkFingerprint(sk, options.prompt, cliPath);
13541
13942
  }
@@ -14288,6 +14689,7 @@ var ClaudeCodeLanguageModel = class {
14288
14689
  })
14289
14690
  });
14290
14691
  state.endTextBlock();
14692
+ state.resultFailure ??= "usage_limit";
14291
14693
  }
14292
14694
  if (runAutoContinue(state, msg)) return;
14293
14695
  for (const [idx, reasoningId] of state.reasoningIds) {
@@ -14321,6 +14723,18 @@ var ClaudeCodeLanguageModel = class {
14321
14723
  });
14322
14724
  state.endTextBlock();
14323
14725
  }
14726
+ if (compactionMode && state.resultFailure) {
14727
+ log.warn("claude could not compact the conversation; no summary was stored", {
14728
+ sessionKey: sk,
14729
+ cause: state.resultFailure
14730
+ });
14731
+ controller.enqueue({
14732
+ type: "error",
14733
+ error: new Error(
14734
+ formatCompactionFailure(state.resultFailure, state.compactionFailureText)
14735
+ )
14736
+ });
14737
+ }
14324
14738
  const usage = toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage));
14325
14739
  controller.enqueue({
14326
14740
  type: "finish",
@@ -14716,7 +15130,7 @@ var ClaudeCodeLanguageModel = class {
14716
15130
  import {
14717
15131
  existsSync as existsSync5,
14718
15132
  mkdirSync as mkdirSync5,
14719
- readFileSync as readFileSync8,
15133
+ readFileSync as readFileSync7,
14720
15134
  realpathSync as realpathSync4,
14721
15135
  rmSync as rmSync3,
14722
15136
  writeFileSync as writeFileSync7
@@ -14743,7 +15157,7 @@ function userIntendsToUseUnscoped() {
14743
15157
  const cfg = userOpencodeJsonPath();
14744
15158
  if (!existsSync5(cfg)) return false;
14745
15159
  try {
14746
- const json = JSON.parse(readFileSync8(cfg, "utf8"));
15160
+ const json = JSON.parse(readFileSync7(cfg, "utf8"));
14747
15161
  const plugins = json.plugin;
14748
15162
  if (!Array.isArray(plugins)) return false;
14749
15163
  return plugins.some(
@@ -14764,7 +15178,7 @@ function ourLoadedDir() {
14764
15178
  function ourVersion(dir) {
14765
15179
  if (!dir) return null;
14766
15180
  try {
14767
- const pkg = JSON.parse(readFileSync8(join13(dir, "package.json"), "utf8"));
15181
+ const pkg = JSON.parse(readFileSync7(join13(dir, "package.json"), "utf8"));
14768
15182
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
14769
15183
  } catch {
14770
15184
  return null;
@@ -14776,7 +15190,7 @@ function markerPath() {
14776
15190
  }
14777
15191
  function markerVersion() {
14778
15192
  try {
14779
- const marker = JSON.parse(readFileSync8(markerPath(), "utf8"));
15193
+ const marker = JSON.parse(readFileSync7(markerPath(), "utf8"));
14780
15194
  return typeof marker.version === "string" ? marker.version : null;
14781
15195
  } catch {
14782
15196
  return null;
@@ -14833,7 +15247,7 @@ function cleanupOne(cacheRoot, ourDir) {
14833
15247
  if (!existsSync5(pkgJsonPath)) return;
14834
15248
  let pkg = {};
14835
15249
  try {
14836
- pkg = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
15250
+ pkg = JSON.parse(readFileSync7(pkgJsonPath, "utf8"));
14837
15251
  } catch {
14838
15252
  return;
14839
15253
  }
@@ -14852,7 +15266,7 @@ function cleanupOne(cacheRoot, ourDir) {
14852
15266
  const cachePkgJson = join13(cacheRoot, "package.json");
14853
15267
  if (!existsSync5(cachePkgJson)) return;
14854
15268
  try {
14855
- const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
15269
+ const cfg = JSON.parse(readFileSync7(cachePkgJson, "utf8"));
14856
15270
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
14857
15271
  delete cfg.dependencies[STALE_PACKAGE_NAME];
14858
15272
  writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
@@ -14932,7 +15346,7 @@ function registerOpencodeLogSink(client) {
14932
15346
  }
14933
15347
 
14934
15348
  // src/v2-client.ts
14935
- import path10 from "path";
15349
+ import path11 from "path";
14936
15350
  function formatAgentTypeList(agents) {
14937
15351
  const lines = agents.flatMap((agent) => {
14938
15352
  if (agent.hidden === true || agent.mode === "primary") return [];
@@ -14961,7 +15375,7 @@ function toV1Session(info) {
14961
15375
  if (!info || typeof info !== "object") return void 0;
14962
15376
  const session = info;
14963
15377
  const root = session.location?.directory;
14964
- const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path10.resolve(root, session.subpath) : root : void 0;
15378
+ const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path11.resolve(root, session.subpath) : root : void 0;
14965
15379
  return { ...info, directory, parentID: session.parentID };
14966
15380
  }
14967
15381
  function toV1Messages(messages) {
@@ -15519,6 +15933,7 @@ function createClaudeCode(settings = {}) {
15519
15933
  turnStats: settings.turnStats === true,
15520
15934
  forkSessions: settings.forkSessions === true,
15521
15935
  resumeAfterRestart: settings.resumeAfterRestart !== false,
15936
+ resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,
15522
15937
  transport: settings.transport,
15523
15938
  interactive: settings.interactive,
15524
15939
  interactiveBypass: settings.interactiveBypass,