@khalilgharbaoui/opencode-claude-code-plugin 0.37.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/claude-code-language-model.ts
2
+ import { join as join11 } from "path";
3
+
1
4
  // src/ids.ts
2
5
  import { randomInt } from "crypto";
3
6
  var ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -72,6 +75,14 @@ function parseInput(raw) {
72
75
  return {};
73
76
  }
74
77
  }
78
+ function hostedToolResult(part) {
79
+ const result = part.result;
80
+ if (part.providerExecuted !== true) return part.result;
81
+ if (!result || typeof result !== "object" || typeof result.output !== "string") {
82
+ return part.result;
83
+ }
84
+ return result.output;
85
+ }
75
86
  function createHostToolPartTranslator(dialect = "v1") {
76
87
  if (dialect !== "v2") return (part) => part;
77
88
  const dropped = /* @__PURE__ */ new Set();
@@ -118,7 +129,8 @@ function createHostToolPartTranslator(dialect = "v1") {
118
129
  case "tool-result": {
119
130
  const tool = classify2(String(part.toolCallId), part.toolName);
120
131
  if (tool === null) return null;
121
- return tool ? { ...part, toolName: tool.name } : part;
132
+ const result = hostedToolResult(part);
133
+ return tool ? { ...part, toolName: tool.name, result } : { ...part, result };
122
134
  }
123
135
  default:
124
136
  return part;
@@ -584,6 +596,20 @@ function formatResetsAt(resetsAt) {
584
596
  const date = new Date(ms);
585
597
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
586
598
  }
599
+ function formatLocalMinute(ms) {
600
+ const when = new Date(ms);
601
+ const pad = (value) => String(value).padStart(2, "0");
602
+ return `${when.getFullYear()}-${pad(when.getMonth() + 1)}-${pad(when.getDate())} ${pad(when.getHours())}:${pad(when.getMinutes())}`;
603
+ }
604
+ function formatResetsAtLocal(resetsAt) {
605
+ const ms = resetsAtToMs(resetsAt);
606
+ if (ms === void 0 || Number.isNaN(new Date(ms).getTime())) return void 0;
607
+ return formatLocalMinute(ms);
608
+ }
609
+ function describeRateLimitWindow(rateLimitType) {
610
+ if (!rateLimitType) return void 0;
611
+ return RATE_LIMIT_WINDOWS[rateLimitType] ?? rateLimitType;
612
+ }
587
613
  function rateLimitKey(info) {
588
614
  return [
589
615
  info.status ?? "?",
@@ -599,7 +625,7 @@ function isRateLimitRejected(info) {
599
625
  }
600
626
  function describeRateLimit(info) {
601
627
  if (!info.status && !info.overageStatus) return null;
602
- const window = info.rateLimitType ? RATE_LIMIT_WINDOWS[info.rateLimitType] ?? info.rateLimitType : void 0;
628
+ const window = describeRateLimitWindow(info.rateLimitType);
603
629
  const resets = formatResetsAt(info.resetsAt);
604
630
  const overageResets = formatResetsAt(info.overageResetsAt);
605
631
  const key = rateLimitKey(info);
@@ -614,9 +640,7 @@ function describeRateLimit(info) {
614
640
  if (resetAt) parts.push(`Resets at ${resetAt}.`);
615
641
  parts.push(RATE_LIMIT_ACTION);
616
642
  const message = parts.join(" ");
617
- return { key, level: "warn", message, transcript: `
618
- ${RATE_LIMIT_MARKER} ${message}
619
- ` };
643
+ return { key, level: "warn", message, transcript: null };
620
644
  }
621
645
  if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
622
646
  const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
@@ -1083,6 +1107,7 @@ var PLAN_MODE_APPROVAL_QUESTION = "Do you want to proceed with this plan?";
1083
1107
  var OPENCODE_QUESTION_RESULT_PREFIX = "User has answered your questions: ";
1084
1108
  var OPENCODE_QUESTION_RESULT_SUFFIX = ". You can now continue with the user's answers in mind.";
1085
1109
  var OPENCODE_UNANSWERED = "Unanswered";
1110
+ var DCP_MESSAGE_ID_SUFFIX = /(?:\s*<dcp-message-id>[^<>]*<\/dcp-message-id>)+\s*$/;
1086
1111
  var KEY_SEPARATOR = "\0";
1087
1112
  function isPlanModeQuestionActive(input) {
1088
1113
  if (input.compactionMode) return false;
@@ -1182,15 +1207,16 @@ function unwrapToolOutput(part) {
1182
1207
  }
1183
1208
  }
1184
1209
  function unwrapOpencodeQuestionResult(value, question) {
1185
- const sentence = value.trim();
1210
+ const untagged = value.replace(DCP_MESSAGE_ID_SUFFIX, "");
1211
+ const sentence = untagged.trim();
1186
1212
  if (!sentence.startsWith(OPENCODE_QUESTION_RESULT_PREFIX) || !sentence.endsWith(OPENCODE_QUESTION_RESULT_SUFFIX)) {
1187
- return value;
1213
+ return untagged;
1188
1214
  }
1189
1215
  const body = sentence.slice(
1190
1216
  OPENCODE_QUESTION_RESULT_PREFIX.length,
1191
1217
  sentence.length - OPENCODE_QUESTION_RESULT_SUFFIX.length
1192
1218
  );
1193
- if (!body.startsWith('"') || !body.endsWith('"')) return value;
1219
+ if (!body.startsWith('"') || !body.endsWith('"')) return untagged;
1194
1220
  let answer;
1195
1221
  const head = question === void 0 ? void 0 : `"${question}"="`;
1196
1222
  if (head && body.startsWith(head) && body.length > head.length) {
@@ -1199,7 +1225,7 @@ function unwrapOpencodeQuestionResult(value, question) {
1199
1225
  const split = body.lastIndexOf('"="');
1200
1226
  if (split > 0) answer = body.slice(split + 3, -1);
1201
1227
  }
1202
- if (answer === void 0) return value;
1228
+ if (answer === void 0) return untagged;
1203
1229
  return answer === OPENCODE_UNANSWERED ? "" : answer;
1204
1230
  }
1205
1231
  function collectAnswerStrings(value, question) {
@@ -1257,6 +1283,13 @@ var ACCOUNT_LIMIT_PATTERNS = [
1257
1283
  /you[’'`]?ve hit your individual spend limit/i
1258
1284
  ];
1259
1285
  var ACCOUNT_BLOCK_MARKER = "\u258C **claude account:**";
1286
+ var USAGE_LIMIT_MARKER = "\u258C **usage limit:**";
1287
+ function describeOtherAccounts(candidates) {
1288
+ const names = candidates.map((candidate) => normalizeAccountName(candidate)).filter(Boolean).map((candidate) => `"${candidate}"`);
1289
+ if (names.length === 0) return "";
1290
+ const joined = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
1291
+ return `the ${joined} account`;
1292
+ }
1260
1293
  var ACCOUNT_BLOCKS = {
1261
1294
  authentication_failed: { what: "is not logged in (its login expired or was revoked)", login: true },
1262
1295
  oauth_org_not_allowed: { what: "belongs to an organization that does not allow this login", login: false },
@@ -1280,17 +1313,51 @@ function formatAccountBlockNote(input) {
1280
1313
  const block = ACCOUNT_BLOCKS[input.kind];
1281
1314
  const account = normalizeAccountName(input.account || DEFAULT_ACCOUNT);
1282
1315
  const fix = block.login ? `Log in again with \`${loginCommandFor(input.configDir)}\`, then resend your message.` : "Check the account at claude.ai, then resend your message.";
1283
- const then = input.offeringSwitch ? " Or pick another account below." : "";
1316
+ const others = input.offeringSwitch ? "" : describeOtherAccounts(input.candidates ?? []);
1317
+ const then = input.offeringSwitch ? " Or pick another account below." : others ? ` Or pick a model from ${others} and resend.` : "";
1284
1318
  return `
1285
1319
  ${ACCOUNT_BLOCK_MARKER} the Claude account "${account}" ${block.what}. ${fix}${then}
1286
1320
  `;
1287
1321
  }
1322
+ function formatUsageLimitNote(input) {
1323
+ const account = normalizeAccountName(input.sourceAccount || DEFAULT_ACCOUNT);
1324
+ const window = describeRateLimitWindow(input.window);
1325
+ const resets = formatResetsAtLocal(input.resetsAt);
1326
+ const others = describeOtherAccounts(input.candidates);
1327
+ const what = [
1328
+ `the Claude account "${account}" is out of usage`,
1329
+ window ? ` in ${window}` : "",
1330
+ resets ? `, which resets at ${resets}` : "",
1331
+ "."
1332
+ ].join("");
1333
+ const advice = others ? `Pick a model from ${others} and resend your message, or wait for the window to reset.` : "Wait for the window to reset, or enable extra usage on the account.";
1334
+ return `
1335
+ ${USAGE_LIMIT_MARKER} ${what} ${advice}
1336
+ `;
1337
+ }
1288
1338
  function isAccountLimitError(input) {
1289
1339
  if (input.rateLimit && isRateLimitRejected(input.rateLimit)) return true;
1290
1340
  const text = input.resultText;
1291
1341
  if (typeof text !== "string" || text.length === 0) return false;
1292
1342
  return ACCOUNT_LIMIT_PATTERNS.some((pattern) => pattern.test(text));
1293
1343
  }
1344
+ var lastAccountLimits = /* @__PURE__ */ new Map();
1345
+ function rememberAccountLimit(account, facts) {
1346
+ const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
1347
+ if (!name) return;
1348
+ lastAccountLimits.set(name, { resetsAt: facts.resetsAt, window: facts.window });
1349
+ }
1350
+ function recallAccountLimit(account, now = Date.now()) {
1351
+ const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
1352
+ const facts = lastAccountLimits.get(name);
1353
+ if (!facts) return {};
1354
+ const resetsAtMs = resetsAtToMs(facts.resetsAt);
1355
+ const fresh = resetsAtMs !== void 0 && resetsAtMs > now;
1356
+ return {
1357
+ ...fresh ? { resetsAt: facts.resetsAt } : {},
1358
+ ...facts.window === void 0 ? {} : { window: facts.window }
1359
+ };
1360
+ }
1294
1361
  var accountOverrides = /* @__PURE__ */ new Map();
1295
1362
  function setAccountOverride(source, target, until, now = Date.now()) {
1296
1363
  const from = normalizeAccountName(source || DEFAULT_ACCOUNT);
@@ -1358,7 +1425,7 @@ function isAccountFailoverQuestionActive(input) {
1358
1425
  if (input.compactionMode) return false;
1359
1426
  if (input.interactive) return false;
1360
1427
  if (input.childSession) return false;
1361
- if (input.configured === "off") return false;
1428
+ if (input.configured !== "ask") return false;
1362
1429
  if (input.candidates.length === 0) return false;
1363
1430
  return input.opencodeHasQuestion;
1364
1431
  }
@@ -4204,6 +4271,11 @@ function findForkParent(opts) {
4204
4271
  }
4205
4272
  return best;
4206
4273
  }
4274
+ function continuesRecordedConversation(recorded, prompt) {
4275
+ const { history, forkable } = splitForkHistory(prompt);
4276
+ if (!forkable || recorded.length === 0 || recorded.length > history.length) return false;
4277
+ return isPrefix(recorded, history) && tailIsReplyOnly(history, recorded.length);
4278
+ }
4207
4279
  function isPrefix(recorded, history) {
4208
4280
  for (let i = 0; i < recorded.length; i++) {
4209
4281
  if (recorded[i].role !== history[i].role) return false;
@@ -4218,6 +4290,109 @@ function tailIsReplyOnly(history, from) {
4218
4290
  return true;
4219
4291
  }
4220
4292
 
4293
+ // src/session-resume-store.ts
4294
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
4295
+ import { homedir as homedir4 } from "os";
4296
+ import { join as join5 } from "path";
4297
+ var MAX_RESUME_RECORDS = 256;
4298
+ var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
4299
+ var records = null;
4300
+ var pathOverride = null;
4301
+ function resumeStorePath() {
4302
+ if (pathOverride) return pathOverride;
4303
+ const stateRoot = process.env.XDG_STATE_HOME || join5(homedir4(), ".local", "state");
4304
+ return join5(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
4305
+ }
4306
+ function isDigest(value) {
4307
+ const entry = value;
4308
+ return !!entry && (entry.role === "user" || entry.role === "assistant" || entry.role === "tool") && typeof entry.digest === "string";
4309
+ }
4310
+ function isRecord2(value) {
4311
+ const record = value;
4312
+ return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest);
4313
+ }
4314
+ function load() {
4315
+ if (records) return records;
4316
+ records = /* @__PURE__ */ new Map();
4317
+ try {
4318
+ const parsed = JSON.parse(readFileSync2(resumeStorePath(), "utf8"));
4319
+ const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
4320
+ if (entries && typeof entries === "object") {
4321
+ const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
4322
+ const valid = Object.entries(entries).filter((entry) => isRecord2(entry[1])).filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt);
4323
+ for (const [key, record] of valid) records.set(key, record);
4324
+ }
4325
+ } catch {
4326
+ }
4327
+ return records;
4328
+ }
4329
+ function save(update) {
4330
+ const file = resumeStorePath();
4331
+ const merged = /* @__PURE__ */ new Map();
4332
+ try {
4333
+ const onDisk = JSON.parse(readFileSync2(file, "utf8"))?.sessions;
4334
+ if (onDisk && typeof onDisk === "object") {
4335
+ for (const [key, record] of Object.entries(onDisk)) {
4336
+ if (isRecord2(record)) merged.set(key, record);
4337
+ }
4338
+ }
4339
+ } catch {
4340
+ }
4341
+ update(merged);
4342
+ const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
4343
+ const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
4344
+ records = new Map(kept);
4345
+ try {
4346
+ mkdirSync3(join5(file, ".."), { recursive: true, mode: 448 });
4347
+ const tmp = `${file}.${process.pid}.tmp`;
4348
+ writeFileSync3(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
4349
+ mode: 384
4350
+ });
4351
+ renameSync2(tmp, file);
4352
+ } catch (err) {
4353
+ log.debug("could not write the claude session resume store", {
4354
+ error: err instanceof Error ? err.message : String(err)
4355
+ });
4356
+ }
4357
+ }
4358
+ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath) {
4359
+ const chain = conversationDigests(prompt);
4360
+ if (chain.length === 0) return;
4361
+ const current = load().get(sessionKey2);
4362
+ if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
4363
+ return;
4364
+ }
4365
+ save((merged) => {
4366
+ merged.delete(sessionKey2);
4367
+ merged.set(sessionKey2, { claudeSessionId, cliPath, chain, updatedAt: Date.now() });
4368
+ });
4369
+ }
4370
+ function forgetResumePoint(sessionKey2) {
4371
+ if (!load().has(sessionKey2)) return;
4372
+ save((merged) => {
4373
+ merged.delete(sessionKey2);
4374
+ });
4375
+ }
4376
+ function findResumePoint(opts) {
4377
+ const record = load().get(opts.sessionKey);
4378
+ if (!record) return void 0;
4379
+ const refuse = (reason) => {
4380
+ log.info("not resuming the claude session from before the restart", {
4381
+ sessionKey: opts.sessionKey,
4382
+ reason
4383
+ });
4384
+ return void 0;
4385
+ };
4386
+ if (record.cliPath !== opts.cliPath) return refuse("another claude binary");
4387
+ if (!continuesRecordedConversation(record.chain, opts.prompt)) {
4388
+ return refuse("the conversation changed");
4389
+ }
4390
+ if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
4391
+ return refuse("the transcript is gone");
4392
+ }
4393
+ return { claudeSessionId: record.claudeSessionId, matched: record.chain.length };
4394
+ }
4395
+
4221
4396
  // src/permission-presets.ts
4222
4397
  var READ_ONLY_DISALLOWED_CLI_TOOLS = [
4223
4398
  "Bash",
@@ -4330,7 +4505,7 @@ import { randomUUID as randomUUID2 } from "crypto";
4330
4505
  var MAX_HISTORY_EXCHANGES = 20;
4331
4506
  var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
4332
4507
  var pendingProcesses = /* @__PURE__ */ new WeakSet();
4333
- function isRecord2(value) {
4508
+ function isRecord3(value) {
4334
4509
  return value !== null && typeof value === "object" && !Array.isArray(value);
4335
4510
  }
4336
4511
  var SYSTEM_REMINDER_BLOCK = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
@@ -4341,7 +4516,7 @@ function parseSideQuestionContent(content) {
4341
4516
  } else if (Array.isArray(content)) {
4342
4517
  const parts = [];
4343
4518
  for (const part of content) {
4344
- if (!isRecord2(part) || part.type !== "text" || typeof part.text !== "string") return null;
4519
+ if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
4345
4520
  parts.push(part.text);
4346
4521
  }
4347
4522
  text = parts.join("\n");
@@ -4360,7 +4535,7 @@ function assistantText(content) {
4360
4535
  if (!Array.isArray(content)) return "";
4361
4536
  const parts = [];
4362
4537
  for (const part of content) {
4363
- if (isRecord2(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
4538
+ if (isRecord3(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
4364
4539
  }
4365
4540
  return parts.join("\n").trim();
4366
4541
  }
@@ -4390,9 +4565,9 @@ function dispatchSideQuestionResponse(activeProcess, line) {
4390
4565
  } catch {
4391
4566
  return false;
4392
4567
  }
4393
- if (!isRecord2(message) || message.type !== "control_response") return false;
4568
+ if (!isRecord3(message) || message.type !== "control_response") return false;
4394
4569
  const response = message.response;
4395
- if (!isRecord2(response) || typeof response.request_id !== "string") return false;
4570
+ if (!isRecord3(response) || typeof response.request_id !== "string") return false;
4396
4571
  return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
4397
4572
  }
4398
4573
  async function requestSideQuestion(activeProcess, question, options) {
@@ -4477,7 +4652,7 @@ async function requestSideQuestion(activeProcess, question, options) {
4477
4652
  return;
4478
4653
  }
4479
4654
  const result = response.response;
4480
- if (response.subtype !== "success" || !isRecord2(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
4655
+ if (response.subtype !== "success" || !isRecord3(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
4481
4656
  fail(new Error("Claude Code returned an invalid /btw response."));
4482
4657
  return;
4483
4658
  }
@@ -4647,6 +4822,7 @@ function noteTurnLine(ap, line) {
4647
4822
  if (isTerminalResultLine(line)) settleTurn(ap);
4648
4823
  }
4649
4824
  function isTurnInFlight(ap) {
4825
+ if (ap.interactiveControl) return ap.interactiveControl.turnRunning();
4650
4826
  return ap.turnInFlight === true;
4651
4827
  }
4652
4828
  function awaitTurnIdle(ap, timeoutMs) {
@@ -4666,6 +4842,7 @@ function awaitTurnIdle(ap, timeoutMs) {
4666
4842
  });
4667
4843
  }
4668
4844
  function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
4845
+ if (ap.interactiveControl) return ap.interactiveControl.interrupt(timeoutMs);
4669
4846
  if (!ap.turnInFlight) return Promise.resolve(true);
4670
4847
  const stdin = ap.proc.stdin;
4671
4848
  if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
@@ -4706,6 +4883,15 @@ function setActiveProcess(key, ap) {
4706
4883
  cancelIdleProcessEviction(key);
4707
4884
  activeProcesses.set(key, ap);
4708
4885
  }
4886
+ function noteInteractiveProcessExit(sessionKey2, ap, code) {
4887
+ if (activeProcesses.get(sessionKey2) !== ap) return;
4888
+ cancelIdleProcessEviction(sessionKey2);
4889
+ activeProcesses.delete(sessionKey2);
4890
+ if (code !== 0 && code !== null) {
4891
+ claudeSessions.delete(sessionKey2);
4892
+ forgetResumePoint(sessionKey2);
4893
+ }
4894
+ }
4709
4895
  function scheduleIdleProcessEviction(key, timeoutMs) {
4710
4896
  cancelIdleProcessEviction(key);
4711
4897
  if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
@@ -4851,6 +5037,7 @@ function deleteClaudeSessionId(key) {
4851
5037
  if (claudeSessionId) clearLedger(claudeSessionId);
4852
5038
  claudeSessions.delete(key);
4853
5039
  forgetForkFingerprint(key);
5040
+ forgetResumePoint(key);
4854
5041
  }
4855
5042
  function effortSessionKey(baseKey, effort) {
4856
5043
  return effort ? `${baseKey}::effort=${effort}` : baseKey;
@@ -4963,6 +5150,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
4963
5150
  sessionKey: sessionKey2
4964
5151
  });
4965
5152
  claudeSessions.delete(sessionKey2);
5153
+ forgetResumePoint(sessionKey2);
4966
5154
  }
4967
5155
  });
4968
5156
  proc.stderr?.on("data", (data) => {
@@ -4977,6 +5165,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
4977
5165
  error: stderr.slice(0, 200)
4978
5166
  });
4979
5167
  claudeSessions.delete(sessionKey2);
5168
+ forgetResumePoint(sessionKey2);
4980
5169
  } else {
4981
5170
  log.debug("ignoring session ID error from stale claude process", {
4982
5171
  sessionKey: sessionKey2
@@ -5455,7 +5644,7 @@ async function handleBtwCommand(client, input, options = {}) {
5455
5644
  // src/diagnostic-bundle.ts
5456
5645
  import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
5457
5646
  import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
5458
- import { homedir as homedir4 } from "os";
5647
+ import { homedir as homedir5 } from "os";
5459
5648
 
5460
5649
  // src/log-messages.ts
5461
5650
  var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
@@ -5474,6 +5663,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5474
5663
  "btw: early aside failed; the message will ask again",
5475
5664
  "cannot interrupt this transport; waiting for the turn to end",
5476
5665
  "claude --version output unparseable",
5666
+ "claude account is out of usage; the turn ends with a note",
5477
5667
  "claude already registers a different skill under this name; not bridging ours",
5478
5668
  "claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
5479
5669
  "claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
@@ -5530,6 +5720,9 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5530
5720
  "ignoring unknown proxyTools entries",
5531
5721
  "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI",
5532
5722
  "interactive system prompt disabled; opencode agent prompts will not be appended",
5723
+ "interactive transport cancelled the usage-limit auto-continue",
5724
+ "interactive transport cannot continue",
5725
+ "interactive transport denied a permission prompt",
5533
5726
  "interactive transport dropped non-text content blocks",
5534
5727
  "interactive turn failed",
5535
5728
  "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
@@ -5561,6 +5754,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5561
5754
  "remote URL images are not supported by Claude CLI, skipping",
5562
5755
  "replaying stdout the child emitted between turns",
5563
5756
  "result fallback timer fired \u2014 closing stream without result event",
5757
+ "resuming the claude session from before the restart instead of replaying it",
5564
5758
  "shared Claude capability already exists; leaving untouched",
5565
5759
  "skipping MCP server with unknown type",
5566
5760
  "skipping local MCP server with no command",
@@ -5784,7 +5978,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
5784
5978
  return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
5785
5979
  }
5786
5980
  function createRedactionContext(options = {}) {
5787
- const home = options.home ?? homedir4();
5981
+ const home = options.home ?? homedir5();
5788
5982
  const homes = /* @__PURE__ */ new Set();
5789
5983
  if (home) homes.add(home);
5790
5984
  if (home) {
@@ -6328,11 +6522,6 @@ function compareBuilds(loaded, onDisk) {
6328
6522
  rebuiltAt: onDisk.mtimeMs
6329
6523
  };
6330
6524
  }
6331
- function formatLocalMinute(ms) {
6332
- const when = new Date(ms);
6333
- const pad = (value) => String(value).padStart(2, "0");
6334
- return `${when.getFullYear()}-${pad(when.getMonth() + 1)}-${pad(when.getDate())} ${pad(when.getHours())}:${pad(when.getMinutes())}`;
6335
- }
6336
6525
  function formatStaleBuildNote(stale) {
6337
6526
  const loadedAt = formatLocalMinute(stale.loadedAt);
6338
6527
  if (stale.kind === "version") {
@@ -6463,7 +6652,7 @@ function staleBuildWatch() {
6463
6652
  var DOCTOR_COMMAND = "claude-code-doctor";
6464
6653
  var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls. Add `usage` for plan windows, or `bundle` for a redacted log you can paste into an issue";
6465
6654
  var DOCTOR_MARKER = "\u258C **claude-code doctor**";
6466
- function isRecord3(value) {
6655
+ function isRecord4(value) {
6467
6656
  return value !== null && typeof value === "object" && !Array.isArray(value);
6468
6657
  }
6469
6658
  var SYSTEM_REMINDER_BLOCK2 = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
@@ -6474,7 +6663,7 @@ function parseDoctorCommandContent(content) {
6474
6663
  } else if (Array.isArray(content)) {
6475
6664
  const parts = [];
6476
6665
  for (const part of content) {
6477
- if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
6666
+ if (!isRecord4(part) || part.type !== "text" || typeof part.text !== "string") return null;
6478
6667
  parts.push(part.text);
6479
6668
  }
6480
6669
  text = parts.join("\n");
@@ -7215,8 +7404,8 @@ var registry = {};
7215
7404
  var defaultSubagentModel;
7216
7405
  var defaultSubagentCacheTtl;
7217
7406
  var providerFallbackModels = [];
7218
- function setAgentRegistry(records) {
7219
- registry = records;
7407
+ function setAgentRegistry(records2) {
7408
+ registry = records2;
7220
7409
  }
7221
7410
  function getAgentRegistry() {
7222
7411
  return registry;
@@ -7356,7 +7545,7 @@ function parseFallbackModelList(value) {
7356
7545
  return value.trim().replace(/^\[|\]$/g, "").split(/[,\s]+/).map((entry) => entry.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
7357
7546
  }
7358
7547
  async function readAgentMarkdownRecords(directories) {
7359
- const records = {};
7548
+ const records2 = {};
7360
7549
  for (const directory of directories) {
7361
7550
  let entries;
7362
7551
  try {
@@ -7367,10 +7556,10 @@ async function readAgentMarkdownRecords(directories) {
7367
7556
  for (const entry of entries) {
7368
7557
  if (!entry.endsWith(".md")) continue;
7369
7558
  const name = entry.slice(0, -3);
7370
- if (records[name]) continue;
7559
+ if (records2[name]) continue;
7371
7560
  try {
7372
7561
  const text = await readFile(path7.join(directory, entry), "utf8");
7373
- records[name] = parseAgentFrontmatter(text);
7562
+ records2[name] = parseAgentFrontmatter(text);
7374
7563
  } catch (err) {
7375
7564
  log.debug("failed to read agent markdown", {
7376
7565
  file: path7.join(directory, entry),
@@ -7379,7 +7568,7 @@ async function readAgentMarkdownRecords(directories) {
7379
7568
  }
7380
7569
  }
7381
7570
  }
7382
- return records;
7571
+ return records2;
7383
7572
  }
7384
7573
  function agentDirectories(home, projectDirectory) {
7385
7574
  const directories = [];
@@ -7558,6 +7747,7 @@ var PLUGIN_NOTE_MARKERS = [
7558
7747
  SILENT_TURN_MARKER,
7559
7748
  FAILOVER_MARKER,
7560
7749
  ACCOUNT_BLOCK_MARKER,
7750
+ USAGE_LIMIT_MARKER,
7561
7751
  MODEL_FALLBACK_MARKER,
7562
7752
  UNATTENDED_REPLAY_MARKER,
7563
7753
  STALE_BUILD_MARKER
@@ -8317,6 +8507,7 @@ async function resolveSkillPluginDirs(opts) {
8317
8507
 
8318
8508
  // src/claude-session-wrapper.ts
8319
8509
  import { EventEmitter as EventEmitter4 } from "events";
8510
+ import { existsSync as existsSync4 } from "fs";
8320
8511
  import { unlink as unlink2 } from "fs/promises";
8321
8512
 
8322
8513
  // src/claude-session-bun.ts
@@ -8350,10 +8541,99 @@ function encodeCwd(cwd) {
8350
8541
  }
8351
8542
  return real.replace(/[^a-zA-Z0-9]/g, "-");
8352
8543
  }
8544
+ var bunPtySpawner = (argv, opts) => {
8545
+ const [command, ...args] = argv;
8546
+ return Bun.spawn([resolveClaude(command ?? "claude"), ...args], {
8547
+ cwd: opts.cwd,
8548
+ env: opts.env,
8549
+ terminal: {
8550
+ cols: opts.cols,
8551
+ rows: opts.rows,
8552
+ data: (_term, data) => opts.onData(Buffer.from(data).toString("utf8"))
8553
+ }
8554
+ });
8555
+ };
8556
+ function words(text) {
8557
+ return text.split(/\s+/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
8558
+ }
8559
+ var SCREEN_PATTERNS = [
8560
+ {
8561
+ kind: "login",
8562
+ pattern: new RegExp(
8563
+ [
8564
+ words("Select login method"),
8565
+ words("Please run /login"),
8566
+ words("Invalid API key"),
8567
+ words("OAuth token has expired"),
8568
+ words("Not logged in")
8569
+ ].join("|"),
8570
+ "i"
8571
+ )
8572
+ },
8573
+ {
8574
+ kind: "onboarding",
8575
+ pattern: new RegExp(
8576
+ [words("Choose the text style that looks best"), words("Let's get started.")].join("|"),
8577
+ "i"
8578
+ )
8579
+ },
8580
+ {
8581
+ kind: "trust",
8582
+ pattern: new RegExp(
8583
+ [
8584
+ words("Do you trust the files in this folder?"),
8585
+ words("Is this a project you created or one you trust?"),
8586
+ words("Yes, I trust this folder")
8587
+ ].join("|"),
8588
+ "i"
8589
+ )
8590
+ },
8591
+ {
8592
+ // The question alone is not enough: Claude can write "Do you want to
8593
+ // proceed?" in a reply. The dialog always numbers its options.
8594
+ kind: "permission",
8595
+ pattern: new RegExp(
8596
+ `${words("Do you want to")}\\s*(?:proceed|make\\s*this\\s*edit|create|overwrite|allow|run)[^?]{0,160}\\?[\\s\\S]{0,600}?1\\.\\s*Yes`,
8597
+ "i"
8598
+ )
8599
+ },
8600
+ {
8601
+ kind: "auto-continue",
8602
+ pattern: new RegExp(
8603
+ `${words("continuing automatically at")}[^\xB7\\n]{1,40}\xB7\\s*${words("esc to cancel")}`,
8604
+ "i"
8605
+ )
8606
+ }
8607
+ ];
8608
+ function stripTerminal(raw) {
8609
+ return raw.replace(
8610
+ /\x1B\[(\d*)C/g,
8611
+ (_match, count) => " ".repeat(Math.min(Number(count) || 1, 200))
8612
+ ).replace(/\x1B\[[0-9;?]*[ABDEFGHJKfd]/g, "\n").replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "").replace(/[ \t]+/g, " ");
8613
+ }
8614
+ function highlightedChoice(text) {
8615
+ let last = null;
8616
+ for (const match of text.matchAll(/❯\s*(?:\d+\s*\.\s*)?(Yes|No)\b/gi)) {
8617
+ last = match[1].toLowerCase();
8618
+ }
8619
+ return last === "yes" || last === "no" ? last : null;
8620
+ }
8621
+ function classifyScreen(text) {
8622
+ for (const { kind, pattern } of SCREEN_PATTERNS) {
8623
+ const match = pattern.exec(text);
8624
+ if (match) {
8625
+ return { kind, detail: match[0].replace(/\s+/g, " ").trim().slice(0, 200) };
8626
+ }
8627
+ }
8628
+ return null;
8629
+ }
8630
+ function configDirEnv(configDir) {
8631
+ return configDir ? { CLAUDE_CONFIG_DIR: configDir } : {};
8632
+ }
8353
8633
  function interactiveSpawnEnv(opts) {
8354
8634
  return {
8355
8635
  ...process.env,
8356
- CLAUDE_CONFIG_DIR: opts.configDir,
8636
+ ...configDirEnv(opts.configDir),
8357
8637
  TERM: "xterm-256color",
8358
8638
  // Pin the binary so a mid-session autoupdate cannot invalidate the
8359
8639
  // detected version the flag gates read, and skip non-essential traffic.
@@ -8428,6 +8708,21 @@ var TurnUsageAccumulator = class {
8428
8708
  };
8429
8709
  var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
8430
8710
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
8711
+ var RAW_KEEP_CHARS = 64 * 1024;
8712
+ var SCREEN_WINDOW_CHARS = 16 * 1024;
8713
+ var INTERRUPT_MARKER = "[Request interrupted by user";
8714
+ function isInterruptRecord(rec) {
8715
+ if (!rec || rec.type !== "user" || !rec.message) return false;
8716
+ const content = rec.message.content;
8717
+ if (typeof content === "string") return content.startsWith(INTERRUPT_MARKER);
8718
+ if (!Array.isArray(content)) return false;
8719
+ return content.some(
8720
+ (block) => block?.type === "text" && typeof block.text === "string" && block.text.startsWith(INTERRUPT_MARKER)
8721
+ );
8722
+ }
8723
+ function isTurnDurationRecord(rec) {
8724
+ return rec?.type === "system" && rec.subtype === "turn_duration";
8725
+ }
8431
8726
  function resolveConfigDir(configDir) {
8432
8727
  const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
8433
8728
  if (!value) return path9.join(os4.homedir(), ".claude");
@@ -8437,11 +8732,21 @@ function resolveConfigDir(configDir) {
8437
8732
  }
8438
8733
  return path9.resolve(value);
8439
8734
  }
8735
+ function fatalScreenMessage(kind, configDir, cwd) {
8736
+ if (kind === "trust") {
8737
+ return `Claude Code's folder trust dialog for ${cwd} could not be answered. Run \`claude\` in that folder once by hand and trust it, then retry.`;
8738
+ }
8739
+ if (kind === "login") {
8740
+ return `Claude Code is not logged in for ${configDir}. Run \`claude\` there once by hand and log in (or \`claude auth login\`), then retry.`;
8741
+ }
8742
+ return `Claude Code has not finished its first-run setup for ${configDir}. Run \`claude\` there once by hand, then retry.`;
8743
+ }
8440
8744
  var ClaudeSession = class {
8441
8745
  sessionId;
8442
8746
  cwd;
8443
8747
  configDir;
8444
8748
  jsonlPath;
8749
+ /** The newest terminal output, ANSI included, for diagnostics. */
8445
8750
  raw = "";
8446
8751
  proc = null;
8447
8752
  cursor = 0;
@@ -8451,12 +8756,36 @@ var ClaudeSession = class {
8451
8756
  exitCode = null;
8452
8757
  aborted = false;
8453
8758
  signal;
8759
+ resumeSessionId;
8760
+ env;
8761
+ onScreen;
8762
+ onExit;
8763
+ spawnPty;
8764
+ /** The configured config dir, which alone is exported to the child. */
8765
+ explicitConfigDir;
8766
+ /** Output since the screen was last acted on, read by `checkScreen`. */
8767
+ screen = "";
8768
+ screenTimer = null;
8769
+ fatal = null;
8770
+ /** The folder trust dialog is up and not answered yet. */
8771
+ trustPending = false;
8772
+ trustMoves = 0;
8773
+ trustMovedAt = 0;
8774
+ turn = null;
8775
+ turnDenied = [];
8776
+ abandonTurn = false;
8454
8777
  o;
8455
8778
  constructor(opts = {}) {
8456
8779
  this.cwd = path9.resolve(opts.cwd ?? process.cwd());
8457
8780
  this.configDir = resolveConfigDir(opts.configDir);
8781
+ this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
8458
8782
  this.signal = opts.signal;
8459
- this.sessionId = randomUUID4();
8783
+ this.resumeSessionId = opts.resumeSessionId;
8784
+ this.env = opts.env;
8785
+ this.onScreen = opts.onScreen;
8786
+ this.onExit = opts.onExit;
8787
+ this.spawnPty = opts.spawnPty ?? bunPtySpawner;
8788
+ this.sessionId = opts.resumeSessionId ?? randomUUID4();
8460
8789
  this.jsonlPath = path9.join(
8461
8790
  this.configDir,
8462
8791
  "projects",
@@ -8486,9 +8815,47 @@ var ClaudeSession = class {
8486
8815
  submitMinMs: opts.submitMinMs ?? 200,
8487
8816
  submitConfirmMs: opts.submitConfirmMs ?? 1500,
8488
8817
  submitMaxRetries: opts.submitMaxRetries ?? 8,
8489
- debug: opts.debug ?? false
8818
+ debug: opts.debug ?? false,
8819
+ // The headless CLI's `tool_progress` heartbeat runs every 30 s, and the
8820
+ // wire-inactivity watchdog it keeps quiet fires at 60 s.
8821
+ heartbeatMs: opts.heartbeatMs ?? 3e4,
8822
+ interruptGraceMs: opts.interruptGraceMs ?? 5e3,
8823
+ // Shorter than `bootQuietMs`, so a trust dialog is answered before boot
8824
+ // could declare the TUI ready and paste a prompt into it.
8825
+ permissionQuietMs: opts.permissionQuietMs ?? 1e3,
8826
+ stopSettleMs: opts.stopSettleMs ?? 750
8490
8827
  };
8491
8828
  }
8829
+ /** The CLI arguments this session spawns with. */
8830
+ spawnArgs() {
8831
+ const args = this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
8832
+ if (this.o.model) args.push("--model", this.o.model);
8833
+ if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
8834
+ args.push("--setting-sources", this.o.settingSources);
8835
+ }
8836
+ if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
8837
+ return args;
8838
+ }
8839
+ /** The child environment: the caller's (or the default one) plus `TERM`,
8840
+ * and `CLAUDE_CONFIG_DIR` only for a configured one (`configDirEnv`). */
8841
+ spawnEnv() {
8842
+ if (!this.env) {
8843
+ return interactiveSpawnEnv({
8844
+ configDir: this.explicitConfigDir,
8845
+ ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
8846
+ effort: this.o.effort
8847
+ });
8848
+ }
8849
+ return { ...this.env, ...configDirEnv(this.explicitConfigDir), TERM: "xterm-256color" };
8850
+ }
8851
+ /** True while a turn is between its paste and its end. */
8852
+ get turnRunning() {
8853
+ return this.turn !== null;
8854
+ }
8855
+ /** True once the child is gone (exited, killed or never started). */
8856
+ get hasExited() {
8857
+ return this.exited;
8858
+ }
8492
8859
  async start() {
8493
8860
  if (this.signal?.aborted) throw new Error("aborted before start");
8494
8861
  this.signal?.addEventListener(
@@ -8499,61 +8866,147 @@ var ClaudeSession = class {
8499
8866
  },
8500
8867
  { once: true }
8501
8868
  );
8502
- const claude = resolveClaude(this.o.cliPath ?? "claude");
8503
- const args = ["--session-id", this.sessionId];
8504
- if (this.o.model) args.push("--model", this.o.model);
8505
- if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
8506
- args.push("--setting-sources", this.o.settingSources);
8507
- }
8508
- if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
8869
+ const claude = this.o.cliPath ?? "claude";
8870
+ const args = this.spawnArgs();
8509
8871
  if (this.o.debug)
8510
8872
  process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
8511
8873
  `);
8512
8874
  this.lastDataAt = Date.now();
8513
- this.proc = Bun.spawn([claude, ...args], {
8875
+ const proc = this.spawnPty([claude, ...args], {
8514
8876
  cwd: this.cwd,
8515
- env: interactiveSpawnEnv({
8516
- // The resolved field, not `this.o.configDir`: same value (the
8517
- // constructor copies it in) but typed as always present.
8518
- configDir: this.configDir,
8519
- ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
8520
- effort: this.o.effort
8521
- }),
8522
- terminal: {
8523
- cols: this.o.cols,
8524
- rows: this.o.rows,
8525
- data: (_term, d) => {
8526
- this.lastDataAt = Date.now();
8527
- const chunk = Buffer.from(d).toString("utf8");
8528
- this.raw += chunk;
8529
- if (this.o.debug) process.stdout.write(chunk);
8530
- }
8531
- }
8877
+ env: this.spawnEnv(),
8878
+ cols: this.o.cols,
8879
+ rows: this.o.rows,
8880
+ onData: (chunk) => this.onData(chunk)
8532
8881
  });
8533
- this.proc.exited.then((code) => {
8534
- this.exitCode = typeof code === "number" ? code : null;
8535
- this.exited = true;
8536
- this.proc = null;
8537
- }).catch(() => {
8882
+ this.proc = proc;
8883
+ const markExited = (code) => {
8884
+ if (this.exited) return;
8885
+ this.exitCode = code;
8538
8886
  this.exited = true;
8539
8887
  this.proc = null;
8540
- });
8888
+ this.clearScreenTimer();
8889
+ this.onExit?.(code);
8890
+ };
8891
+ proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
8541
8892
  await this.waitForBoot();
8542
8893
  this.cursor = this.lineCount();
8543
8894
  }
8895
+ onData(chunk) {
8896
+ this.lastDataAt = Date.now();
8897
+ this.raw = (this.raw + chunk).slice(-RAW_KEEP_CHARS);
8898
+ this.screen = (this.screen + chunk).slice(-SCREEN_WINDOW_CHARS);
8899
+ if (this.o.debug) process.stdout.write(chunk);
8900
+ this.clearScreenTimer();
8901
+ this.screenTimer = setTimeout(() => {
8902
+ this.screenTimer = null;
8903
+ this.checkScreen();
8904
+ }, this.o.permissionQuietMs);
8905
+ }
8906
+ clearScreenTimer() {
8907
+ if (this.screenTimer) {
8908
+ clearTimeout(this.screenTimer);
8909
+ this.screenTimer = null;
8910
+ }
8911
+ }
8912
+ /**
8913
+ * Answer the screen the TUI is blocked on, if it is one of the few this
8914
+ * session recognises. Returns what it did, or null. Each screen is acted on
8915
+ * once: the window is cleared afterwards, so the next check only reads what
8916
+ * the TUI drew since.
8917
+ */
8918
+ checkScreen() {
8919
+ if (!this.proc || this.exited) return null;
8920
+ const text = stripTerminal(this.screen);
8921
+ const state = this.trustPending ? { kind: "trust", detail: "folder trust dialog" } : classifyScreen(text);
8922
+ if (!state) return null;
8923
+ let action;
8924
+ switch (state.kind) {
8925
+ case "trust": {
8926
+ this.trustPending = true;
8927
+ const choice = highlightedChoice(text);
8928
+ if (choice === "yes") {
8929
+ this.write("\r");
8930
+ this.trustPending = false;
8931
+ action = "accepted";
8932
+ break;
8933
+ }
8934
+ if (choice === null) return null;
8935
+ if (this.trustMovedAt && this.lastDataAt <= this.trustMovedAt) return null;
8936
+ if (this.trustMoves >= 4) {
8937
+ action = "fatal";
8938
+ break;
8939
+ }
8940
+ this.trustMoves++;
8941
+ this.trustMovedAt = Date.now();
8942
+ this.write("\x1B[B");
8943
+ return null;
8944
+ }
8945
+ case "permission":
8946
+ if (!this.turn) return null;
8947
+ this.write("\x1B");
8948
+ this.turnDenied.push(state);
8949
+ action = "denied";
8950
+ break;
8951
+ case "auto-continue":
8952
+ this.write("\x1B");
8953
+ action = "cancelled";
8954
+ break;
8955
+ case "login":
8956
+ case "onboarding":
8957
+ action = "fatal";
8958
+ break;
8959
+ }
8960
+ this.screen = "";
8961
+ const event = { ...state, action };
8962
+ if (action === "fatal") {
8963
+ this.fatal = event;
8964
+ this.killProcess();
8965
+ }
8966
+ try {
8967
+ this.onScreen?.(event);
8968
+ } catch {
8969
+ }
8970
+ return event;
8971
+ }
8972
+ write(data) {
8973
+ try {
8974
+ this.proc?.terminal.write(data);
8975
+ } catch {
8976
+ }
8977
+ }
8978
+ fatalError() {
8979
+ if (!this.fatal) return null;
8980
+ return new Error(
8981
+ this.failureMessage(fatalScreenMessage(this.fatal.kind, this.configDir, this.cwd), true)
8982
+ );
8983
+ }
8544
8984
  /** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
8545
- * bootMinMs..bootMaxMs. */
8985
+ * bootMinMs..bootMaxMs. A screen still waiting to be answered when the TUI
8986
+ * goes quiet is answered first, and the wait goes on. */
8546
8987
  async waitForBoot() {
8547
8988
  const start = Date.now();
8548
8989
  while (Date.now() - start < this.o.bootMaxMs) {
8549
8990
  await delay(150);
8550
8991
  if (this.aborted) throw new Error("aborted during boot");
8992
+ const fatal = this.fatalError();
8993
+ if (fatal) throw fatal;
8551
8994
  if (this.exited) {
8552
8995
  throw new Error(this.failureMessage("claude exited during boot", true));
8553
8996
  }
8554
8997
  const elapsed = Date.now() - start;
8555
8998
  const sinceData = Date.now() - this.lastDataAt;
8556
- if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) return;
8999
+ if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) {
9000
+ const acted = this.checkScreen();
9001
+ if (!acted && !this.trustPending) return;
9002
+ const fatalNow = this.fatalError();
9003
+ if (fatalNow) throw fatalNow;
9004
+ }
9005
+ }
9006
+ if (this.trustPending) {
9007
+ this.fatal = { kind: "trust", detail: "folder trust dialog", action: "fatal" };
9008
+ this.killProcess();
9009
+ throw this.fatalError();
8557
9010
  }
8558
9011
  }
8559
9012
  /** Submit the freshly-injected prompt and confirm the turn was actually
@@ -8591,7 +9044,7 @@ var ClaudeSession = class {
8591
9044
  return lines.length > 0 ? lines.length - 1 : 0;
8592
9045
  }
8593
9046
  rawTail(max = 600) {
8594
- const clean = this.raw.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\s+/g, " ").trim();
9047
+ const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
8595
9048
  return clean.length > max ? clean.slice(-max) : clean;
8596
9049
  }
8597
9050
  failureMessage(reason, includeRaw = false) {
@@ -8605,61 +9058,133 @@ var ClaudeSession = class {
8605
9058
  return parts.join("; ");
8606
9059
  }
8607
9060
  /**
8608
- * Inject a turn into the live session and return the assistant reply once a
8609
- * terminal stop_reason is observed in the transcript.
9061
+ * Paste a turn into the live session and follow the transcript until the
9062
+ * turn ends, handing every new record to `onRecord` (raw line and parsed
9063
+ * record, the latter null when the line is not JSON). One turn at a time:
9064
+ * a second call while one runs is refused, because both would read the
9065
+ * same cursor.
8610
9066
  */
8611
- async ask(prompt, perTurnTimeoutMs) {
9067
+ async runTurnLoop(prompt, onRecord, timeout, onHeartbeat) {
8612
9068
  if (this.aborted) throw new Error("aborted");
9069
+ const fatal = this.fatalError();
9070
+ if (fatal) throw fatal;
8613
9071
  if (!this.proc || this.exited)
8614
9072
  throw new Error("session not started or already exited");
8615
- const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
8616
- const t0 = Date.now();
8617
- if (this.o.bracketedPaste) {
8618
- this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
8619
- } else {
8620
- this.proc.terminal.write(prompt);
8621
- }
8622
- await this.submitTurn();
8623
- const collected2 = [];
8624
- const usage = new TurnUsageAccumulator();
8625
- let stopReason = null;
8626
- const deadline = Date.now() + timeout;
8627
- while (Date.now() < deadline) {
8628
- await delay(this.o.pollMs);
8629
- if (this.aborted) throw new Error("aborted mid-turn");
8630
- const lines = this.readRawLines();
8631
- const lastComplete = lines.length - 1;
8632
- if (lastComplete <= this.cursor) {
8633
- if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
8634
- continue;
9073
+ if (this.turn) throw new Error("a turn is already running in this session");
9074
+ let settle;
9075
+ this.turn = new Promise((resolve6) => settle = resolve6);
9076
+ this.turnDenied = [];
9077
+ this.abandonTurn = false;
9078
+ this.cursor = Math.max(this.cursor, this.lineCount());
9079
+ this.screen = "";
9080
+ try {
9081
+ if (this.o.bracketedPaste) {
9082
+ this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
9083
+ } else {
9084
+ this.proc.terminal.write(prompt);
8635
9085
  }
8636
- for (let i = this.cursor; i < lastComplete; i++) {
8637
- const s = lines[i];
8638
- if (!s || !s.trim()) continue;
8639
- let rec;
8640
- try {
8641
- rec = JSON.parse(s);
8642
- } catch {
9086
+ await this.submitTurn();
9087
+ let stopReason = null;
9088
+ let stopSeenAt = 0;
9089
+ let end = null;
9090
+ let sawUserRecord = false;
9091
+ let lastBeat = Date.now();
9092
+ const deadline = Date.now() + timeout;
9093
+ while (Date.now() < deadline) {
9094
+ await delay(this.o.pollMs);
9095
+ if (this.aborted) throw new Error("aborted mid-turn");
9096
+ const fatalNow = this.fatalError();
9097
+ if (fatalNow) throw fatalNow;
9098
+ const lines = this.readRawLines();
9099
+ const lastComplete = lines.length - 1;
9100
+ if (lastComplete > this.cursor) {
9101
+ for (let i = this.cursor; i < lastComplete && !end; i++) {
9102
+ const s = lines[i];
9103
+ if (!s || !s.trim()) continue;
9104
+ let rec = null;
9105
+ try {
9106
+ rec = JSON.parse(s);
9107
+ } catch {
9108
+ }
9109
+ onRecord(s, rec);
9110
+ if (!rec) continue;
9111
+ if (rec.type === "assistant" && rec.message) {
9112
+ const reason = rec.message.stop_reason;
9113
+ if (typeof reason === "string") {
9114
+ stopReason = TERMINAL_STOP.has(reason) ? reason : null;
9115
+ }
9116
+ } else if (rec.type === "user") {
9117
+ if (sawUserRecord && isInterruptRecord(rec)) end = "interrupted";
9118
+ sawUserRecord = true;
9119
+ } else if ((sawUserRecord || stopReason) && isTurnDurationRecord(rec)) {
9120
+ end = stopReason ? "stop" : "ended";
9121
+ }
9122
+ this.cursor = i + 1;
9123
+ }
9124
+ if (!end) this.cursor = lastComplete;
9125
+ lastBeat = Date.now();
9126
+ stopSeenAt = Date.now();
9127
+ if (end) return { stopReason: end === "stop" ? stopReason : null, end, denied: this.turnDenied };
8643
9128
  continue;
8644
9129
  }
8645
- if (rec.type === "assistant" && rec.message) {
8646
- for (const b of rec.message.content ?? []) {
8647
- if (b?.type === "text" && typeof b.text === "string")
8648
- collected2.push(b.text);
8649
- }
8650
- usage.add(rec);
8651
- if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
8652
- stopReason = rec.message.stop_reason;
8653
- }
9130
+ if (stopReason && Date.now() - stopSeenAt >= this.o.stopSettleMs) {
9131
+ return { stopReason, end: "stop", denied: this.turnDenied };
9132
+ }
9133
+ if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
9134
+ if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
9135
+ const now = Date.now();
9136
+ if (onHeartbeat && now - lastBeat >= this.o.heartbeatMs) {
9137
+ lastBeat = now;
9138
+ if (now - this.lastDataAt < this.o.heartbeatMs) onHeartbeat();
8654
9139
  }
8655
9140
  }
8656
- this.cursor = lastComplete;
8657
- if (stopReason) break;
9141
+ return { stopReason: null, end: null, denied: this.turnDenied };
9142
+ } finally {
9143
+ this.turn = null;
9144
+ this.abandonTurn = false;
9145
+ settle();
8658
9146
  }
9147
+ }
9148
+ /**
9149
+ * Stop the running turn the way the TUI's own Esc does, and keep the
9150
+ * session alive for the next one. Resolves true once the turn ended, false
9151
+ * when it was abandoned after `interruptGraceMs` without the TUI saying so.
9152
+ */
9153
+ async interrupt(graceMs = this.o.interruptGraceMs) {
9154
+ const turn = this.turn;
9155
+ if (!turn) return true;
9156
+ this.write("\x1B");
9157
+ const ended = await Promise.race([turn.then(() => true), delay(graceMs).then(() => false)]);
9158
+ if (ended) return true;
9159
+ this.write("\x1B");
9160
+ this.abandonTurn = true;
9161
+ await Promise.race([turn, delay(this.o.pollMs * 4)]);
9162
+ return false;
9163
+ }
9164
+ /**
9165
+ * Inject a turn into the live session and return the assistant reply once a
9166
+ * terminal stop_reason is observed in the transcript.
9167
+ */
9168
+ async ask(prompt, perTurnTimeoutMs) {
9169
+ const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
9170
+ const t0 = Date.now();
9171
+ const collected2 = [];
9172
+ const usage = new TurnUsageAccumulator();
9173
+ const { stopReason } = await this.runTurnLoop(
9174
+ prompt,
9175
+ (_raw, rec) => {
9176
+ if (rec?.type !== "assistant" || !rec.message) return;
9177
+ for (const b of rec.message.content ?? []) {
9178
+ if (b?.type === "text" && typeof b.text === "string") collected2.push(b.text);
9179
+ }
9180
+ usage.add(rec);
9181
+ },
9182
+ timeout
9183
+ );
8659
9184
  if (!stopReason) {
8660
9185
  throw new Error(
8661
9186
  this.failureMessage(
8662
- `turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected2.length} text block(s))`
9187
+ `turn ended without a terminal assistant record after ${Date.now() - t0}ms (collected ${collected2.length} text block(s))`
8663
9188
  )
8664
9189
  );
8665
9190
  }
@@ -8681,9 +9206,9 @@ var ClaudeSession = class {
8681
9206
  }
8682
9207
  /**
8683
9208
  * Like ask(), but instead of collecting the reply text it re-emits each NEW
8684
- * raw JSONL transcript line via onLine (verbatim) until a terminal
8685
- * stop_reason. Used by the opencode plugin transport shim, which feeds
8686
- * these raw lines into the existing stream-json line handler unchanged.
9209
+ * raw JSONL transcript line via onLine (verbatim) until the turn ends. Used
9210
+ * by the opencode plugin transport shim, which feeds these raw lines into
9211
+ * the existing stream-json line handler unchanged.
8687
9212
  *
8688
9213
  * `usage` is the turn summed over DISTINCT API calls, which is exactly what
8689
9214
  * a headless `result` frame reports, so the shim can synthesize one and
@@ -8691,53 +9216,24 @@ var ClaudeSession = class {
8691
9216
  * `turnStats`) behaves as it does on the headless path. `lastCallUsage` is
8692
9217
  * the newest real call, returned because a caller with no stream parser
8693
9218
  * (`askOnce`) has no other way to get the context side.
9219
+ *
9220
+ * A turn that ends any way other than a terminal stop (an interrupt, a
9221
+ * denied permission dialog, `turn_duration`) resolves with that `end` and a
9222
+ * null `stopReason`; only a timeout throws.
8694
9223
  */
8695
- async tailTurn(prompt, onLine, perTurnTimeoutMs) {
8696
- if (this.aborted) throw new Error("aborted");
8697
- if (!this.proc || this.exited)
8698
- throw new Error("session not started or already exited");
9224
+ async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
8699
9225
  const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
8700
- if (this.o.bracketedPaste) {
8701
- this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
8702
- } else {
8703
- this.proc.terminal.write(prompt);
8704
- }
8705
- await this.submitTurn();
8706
9226
  const usage = new TurnUsageAccumulator();
8707
- let stopReason = null;
8708
- const deadline = Date.now() + timeout;
8709
- while (Date.now() < deadline) {
8710
- await delay(this.o.pollMs);
8711
- if (this.aborted) throw new Error("aborted mid-turn");
8712
- const lines = this.readRawLines();
8713
- const lastComplete = lines.length - 1;
8714
- if (lastComplete <= this.cursor) {
8715
- if (this.exited) {
8716
- throw new Error(this.failureMessage("claude exited mid-turn", true));
8717
- }
8718
- continue;
8719
- }
8720
- for (let i = this.cursor; i < lastComplete; i++) {
8721
- const s = lines[i];
8722
- if (!s || !s.trim()) continue;
8723
- onLine(s);
8724
- let rec;
8725
- try {
8726
- rec = JSON.parse(s);
8727
- } catch {
8728
- continue;
8729
- }
8730
- usage.add(rec);
8731
- if (rec.type === "assistant" && rec.message) {
8732
- if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
8733
- stopReason = rec.message.stop_reason;
8734
- }
8735
- }
8736
- }
8737
- this.cursor = lastComplete;
8738
- if (stopReason) break;
8739
- }
8740
- if (!stopReason) {
9227
+ const { stopReason, end, denied } = await this.runTurnLoop(
9228
+ prompt,
9229
+ (raw, rec) => {
9230
+ onLine(raw);
9231
+ if (rec) usage.add(rec);
9232
+ },
9233
+ timeout,
9234
+ onHeartbeat
9235
+ );
9236
+ if (!end) {
8741
9237
  throw new Error(
8742
9238
  this.failureMessage(
8743
9239
  `turn timed out after ${timeout}ms (no terminal assistant record)`
@@ -8746,26 +9242,32 @@ var ClaudeSession = class {
8746
9242
  }
8747
9243
  return {
8748
9244
  stopReason,
9245
+ end,
8749
9246
  usage: usage.turnTotal,
8750
9247
  lastCallUsage: usage.lastCall,
8751
- callCount: usage.callCount
9248
+ callCount: usage.callCount,
9249
+ denied
8752
9250
  };
8753
9251
  }
8754
- dispose() {
8755
- if (this.proc) {
8756
- try {
8757
- this.proc.terminal.write("");
8758
- } catch {
8759
- }
8760
- try {
8761
- this.proc.kill();
8762
- } catch {
8763
- }
8764
- try {
8765
- this.proc.terminal.close();
8766
- } catch {
8767
- }
9252
+ killProcess() {
9253
+ const proc = this.proc;
9254
+ if (!proc) return;
9255
+ try {
9256
+ proc.terminal.write("");
9257
+ } catch {
9258
+ }
9259
+ try {
9260
+ proc.kill();
9261
+ } catch {
8768
9262
  }
9263
+ try {
9264
+ proc.terminal.close();
9265
+ } catch {
9266
+ }
9267
+ }
9268
+ dispose() {
9269
+ this.clearScreenTimer();
9270
+ this.killProcess();
8769
9271
  this.proc = null;
8770
9272
  }
8771
9273
  };
@@ -8839,9 +9341,88 @@ function interactiveExtraArgs(opts) {
8839
9341
  }
8840
9342
  return extraArgs;
8841
9343
  }
9344
+ var OpenToolCalls = class {
9345
+ open = /* @__PURE__ */ new Map();
9346
+ add(rec, now = Date.now()) {
9347
+ const content = rec?.message?.content;
9348
+ if (!Array.isArray(content)) return;
9349
+ for (const block of content) {
9350
+ if (rec.type === "assistant" && block?.type === "tool_use" && typeof block.id === "string") {
9351
+ this.open.set(block.id, { name: String(block.name ?? "tool"), startedAt: now });
9352
+ } else if (rec.type === "user" && block?.type === "tool_result") {
9353
+ this.open.delete(block.tool_use_id);
9354
+ }
9355
+ }
9356
+ }
9357
+ newest() {
9358
+ let newest = null;
9359
+ for (const [id, call] of this.open) newest = { id, ...call };
9360
+ return newest;
9361
+ }
9362
+ clear() {
9363
+ this.open.clear();
9364
+ }
9365
+ };
9366
+ function heartbeatFrame(sessionId, openCall, now = Date.now()) {
9367
+ if (openCall) {
9368
+ return JSON.stringify({
9369
+ type: "tool_progress",
9370
+ tool_use_id: `${openCall.id}-heartbeat`,
9371
+ tool_name: openCall.name,
9372
+ parent_tool_use_id: openCall.id,
9373
+ heartbeat: true,
9374
+ elapsed_time_seconds: Math.round((now - openCall.startedAt) / 1e3),
9375
+ session_id: sessionId
9376
+ });
9377
+ }
9378
+ return JSON.stringify({
9379
+ type: "system",
9380
+ subtype: "status",
9381
+ status: "requesting",
9382
+ session_id: sessionId
9383
+ });
9384
+ }
9385
+ function interactiveResultFrame(opts) {
9386
+ const completed = opts.end === "stop" && !!opts.stopReason;
9387
+ let result;
9388
+ let terminalReason = "completed";
9389
+ if (!completed) {
9390
+ if (opts.end === "failed") {
9391
+ terminalReason = "error_during_execution";
9392
+ result = `Interactive transport failed: ${opts.error ?? "unknown error"}`;
9393
+ } else if (opts.end === "interrupted") {
9394
+ terminalReason = "aborted";
9395
+ result = "Interactive transport: the turn was interrupted.";
9396
+ } else {
9397
+ terminalReason = "error_during_execution";
9398
+ result = "Interactive transport: the turn ended without a terminal stop_reason. Output above may be incomplete.";
9399
+ }
9400
+ }
9401
+ return JSON.stringify({
9402
+ type: "result",
9403
+ subtype: completed ? "success" : "error_during_execution",
9404
+ is_error: !completed,
9405
+ stop_reason: completed ? opts.stopReason : null,
9406
+ terminal_reason: terminalReason,
9407
+ result,
9408
+ session_id: opts.sessionId,
9409
+ usage: opts.usage ?? {},
9410
+ // The TUI has no `can_use_tool` channel, so a permission dialog is the
9411
+ // only way it asks; each one the session denied is reported in the
9412
+ // headless field's place, names and ids only (h #g109).
9413
+ permission_denials: opts.denials ?? [],
9414
+ total_cost_usd: null,
9415
+ duration_ms: 0
9416
+ });
9417
+ }
8842
9418
  function spawnInteractiveProcess(opts) {
8843
9419
  const extraArgs = interactiveExtraArgs(opts);
9420
+ const openCalls = new OpenToolCalls();
9421
+ let denials = [];
9422
+ const lineEmitter = new EventEmitter4();
9423
+ const proc = new EventEmitter4();
8844
9424
  const session = new ClaudeSession({
9425
+ ...opts.tuning,
8845
9426
  cwd: opts.cwd,
8846
9427
  cliPath: opts.cliPath,
8847
9428
  configDir: opts.configDir,
@@ -8851,7 +9432,28 @@ function spawnInteractiveProcess(opts) {
8851
9432
  settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
8852
9433
  extraArgs,
8853
9434
  ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
8854
- effort: opts.effort ? cliEffortLevel(opts.effort) : void 0
9435
+ effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
9436
+ resumeSessionId: opts.resumeSessionId,
9437
+ env: opts.env,
9438
+ spawnPty: opts.spawnPty,
9439
+ onScreen: (event) => {
9440
+ if (event.action === "denied") {
9441
+ const call = openCalls.newest();
9442
+ denials.push(call ? { tool_name: call.name, tool_use_id: call.id } : { tool_name: "unknown" });
9443
+ }
9444
+ const data = { sessionId: session.sessionId, screen: event.kind, detail: event.detail };
9445
+ if (event.action === "accepted") log.info("interactive transport accepted a prompt", data);
9446
+ else if (event.action === "fatal") log.error("interactive transport cannot continue", data);
9447
+ else if (event.action === "denied") log.warn("interactive transport denied a permission prompt", data);
9448
+ else log.warn("interactive transport cancelled the usage-limit auto-continue", data);
9449
+ },
9450
+ onExit: (code) => {
9451
+ proc.exitCode = code;
9452
+ proc.signalCode = code === null ? "SIGTERM" : null;
9453
+ log.info("interactive claude exited", { sessionId: session.sessionId, code });
9454
+ proc.emit("exit", proc.exitCode, proc.signalCode);
9455
+ proc.emit("close", proc.exitCode, proc.signalCode);
9456
+ }
8855
9457
  });
8856
9458
  log.info("prepared interactive claude session", {
8857
9459
  cwd: opts.cwd,
@@ -8860,80 +9462,95 @@ function spawnInteractiveProcess(opts) {
8860
9462
  model: opts.model,
8861
9463
  effort: opts.effort,
8862
9464
  sessionId: session.sessionId,
9465
+ resumed: !!opts.resumeSessionId,
8863
9466
  jsonlPath: session.jsonlPath
8864
9467
  });
8865
- const lineEmitter = new EventEmitter4();
8866
- const errorHandlers = /* @__PURE__ */ new Set();
8867
9468
  let startPromise = null;
8868
9469
  const ensureStarted = () => {
8869
9470
  if (!startPromise) startPromise = session.start();
8870
9471
  return startPromise;
8871
9472
  };
8872
- const emitResult = (opts2) => {
8873
- lineEmitter.emit(
8874
- "line",
8875
- JSON.stringify({
8876
- type: "result",
8877
- subtype: opts2.subtype,
8878
- is_error: opts2.isError,
8879
- stop_reason: opts2.stopReason,
8880
- terminal_reason: opts2.terminalReason,
8881
- result: opts2.result,
8882
- session_id: session.sessionId,
8883
- usage: opts2.usage ?? {},
8884
- total_cost_usd: null,
8885
- duration_ms: 0
8886
- })
8887
- );
8888
- };
8889
- const runTurn = (userMsg) => {
8890
- void (async () => {
8891
- try {
8892
- await ensureStarted();
8893
- const { stopReason, usage, lastCallUsage, callCount } = await session.tailTurn(userMsg, (raw) => {
8894
- lineEmitter.emit("line", raw);
8895
- });
8896
- log.info("interactive turn usage", {
8897
- apiCalls: callCount,
8898
- turnOutputTokens: usage?.output_tokens,
8899
- turnInputTokens: usage?.input_tokens,
8900
- turnCacheReadTokens: usage?.cache_read_input_tokens,
8901
- lastCallInputTokens: lastCallUsage?.input_tokens,
8902
- lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
8903
- lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
8904
- });
8905
- const timedOut = !stopReason;
8906
- emitResult({
8907
- subtype: timedOut ? "error_during_execution" : "success",
8908
- isError: timedOut,
8909
- stopReason: timedOut ? null : stopReason,
8910
- terminalReason: timedOut ? "error_during_execution" : "completed",
8911
- result: timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
8912
- usage
8913
- });
8914
- } catch (err) {
8915
- const e = err instanceof Error ? err : new Error(String(err));
8916
- log.error("interactive turn failed", { error: e.message });
8917
- emitResult({
8918
- subtype: "error_during_execution",
8919
- isError: true,
8920
- stopReason: null,
8921
- terminalReason: "error_during_execution",
8922
- result: `Interactive transport failed: ${e.message}`
8923
- });
8924
- if (errorHandlers.size > 0) {
8925
- for (const h of errorHandlers) h(e);
8926
- } else {
8927
- lineEmitter.emit("close");
8928
- }
9473
+ let writes = 0;
9474
+ let cancelledThrough = 0;
9475
+ let queued = 0;
9476
+ let chain = Promise.resolve();
9477
+ const runTurn = async (userMsg, turnNumber) => {
9478
+ const current = () => turnNumber === writes;
9479
+ const emit2 = (line) => {
9480
+ if (current()) lineEmitter.emit("line", line);
9481
+ };
9482
+ try {
9483
+ await ensureStarted();
9484
+ if (turnNumber <= cancelledThrough) {
9485
+ log.info("interactive turn cancelled before it started", { sessionId: session.sessionId });
9486
+ return;
8929
9487
  }
8930
- })();
9488
+ openCalls.clear();
9489
+ denials = [];
9490
+ const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
9491
+ userMsg,
9492
+ (raw) => {
9493
+ try {
9494
+ openCalls.add(JSON.parse(raw));
9495
+ } catch {
9496
+ }
9497
+ emit2(raw);
9498
+ },
9499
+ void 0,
9500
+ () => emit2(heartbeatFrame(session.sessionId, openCalls.newest()))
9501
+ );
9502
+ log.info("interactive turn ended", {
9503
+ end,
9504
+ stopReason,
9505
+ superseded: !current(),
9506
+ apiCalls: callCount,
9507
+ turnOutputTokens: usage?.output_tokens,
9508
+ turnInputTokens: usage?.input_tokens,
9509
+ turnCacheReadTokens: usage?.cache_read_input_tokens,
9510
+ lastCallInputTokens: lastCallUsage?.input_tokens,
9511
+ lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
9512
+ lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens,
9513
+ deniedPrompts: denied.length,
9514
+ cancelled: turnNumber <= cancelledThrough
9515
+ });
9516
+ emit2(
9517
+ interactiveResultFrame({
9518
+ sessionId: session.sessionId,
9519
+ end,
9520
+ stopReason,
9521
+ usage,
9522
+ denials
9523
+ })
9524
+ );
9525
+ } catch (err) {
9526
+ const e = err instanceof Error ? err : new Error(String(err));
9527
+ log.error("interactive turn failed", { error: e.message, superseded: !current() });
9528
+ if (!current()) return;
9529
+ emit2(
9530
+ interactiveResultFrame({
9531
+ sessionId: existsSync4(session.jsonlPath) ? session.sessionId : void 0,
9532
+ end: "failed",
9533
+ stopReason: null,
9534
+ error: e.message
9535
+ })
9536
+ );
9537
+ if (proc.listenerCount("error") > 0) proc.emit("error", e);
9538
+ else lineEmitter.emit("close");
9539
+ }
9540
+ };
9541
+ const enqueueTurn = (userMsg) => {
9542
+ const turnNumber = ++writes;
9543
+ queued++;
9544
+ chain = chain.then(() => runTurn(userMsg, turnNumber)).finally(() => {
9545
+ queued--;
9546
+ });
8931
9547
  };
8932
- const proc = {
9548
+ Object.assign(proc, {
8933
9549
  stdin: {
9550
+ writable: true,
8934
9551
  write(chunk) {
8935
9552
  const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
8936
- runTurn(decodeUserEnvelope(raw));
9553
+ enqueueTurn(decodeUserEnvelope(raw));
8937
9554
  return true;
8938
9555
  },
8939
9556
  end() {
@@ -8943,47 +9560,56 @@ function spawnInteractiveProcess(opts) {
8943
9560
  stderr: null,
8944
9561
  pid: -1,
8945
9562
  killed: false,
8946
- on(event, fn) {
8947
- if (event === "error") errorHandlers.add(fn);
8948
- return proc;
8949
- },
8950
- once() {
8951
- return proc;
8952
- },
8953
- off(event, fn) {
8954
- if (event === "error") errorHandlers.delete(fn);
8955
- return proc;
8956
- },
9563
+ exitCode: null,
9564
+ signalCode: null,
8957
9565
  kill() {
9566
+ if (!proc.killed) {
9567
+ proc.killed = true;
9568
+ if (opts.systemPromptFile) {
9569
+ void unlink2(opts.systemPromptFile).catch(() => {
9570
+ });
9571
+ }
9572
+ }
9573
+ const started = startPromise !== null;
8958
9574
  try {
8959
9575
  session.dispose();
8960
9576
  } catch {
8961
9577
  }
8962
- if (opts.systemPromptFile) {
8963
- void unlink2(opts.systemPromptFile).catch(() => {
8964
- });
9578
+ if (!started && proc.exitCode === null && proc.signalCode === null) {
9579
+ proc.signalCode = "SIGTERM";
9580
+ proc.emit("exit", null, "SIGTERM");
9581
+ proc.emit("close", null, "SIGTERM");
8965
9582
  }
8966
- proc.killed = true;
8967
9583
  return true;
8968
9584
  }
8969
- };
9585
+ });
8970
9586
  return {
8971
9587
  proc,
8972
9588
  lineEmitter,
8973
9589
  proxyServer: null,
8974
9590
  mcpHash: void 0,
8975
- systemPromptFile: opts.systemPromptFile
9591
+ systemPromptFile: opts.systemPromptFile,
9592
+ cliPath: opts.cliPath,
9593
+ interactiveControl: {
9594
+ turnRunning: () => queued > 0,
9595
+ // Stops the running turn and every queued one: they were all written
9596
+ // by the turn being aborted, or by one before it.
9597
+ interrupt: (timeoutMs) => {
9598
+ cancelledThrough = writes;
9599
+ return session.interrupt(timeoutMs);
9600
+ }
9601
+ }
8976
9602
  };
8977
9603
  }
8978
9604
 
8979
9605
  // src/prompts.ts
8980
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
8981
- import { homedir as homedir7 } from "os";
9606
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9607
+ import { homedir as homedir8 } from "os";
8982
9608
  import { randomUUID as randomUUID5 } from "crypto";
8983
- import { dirname as dirname6, join as join9 } from "path";
9609
+ import { dirname as dirname6, join as join10 } from "path";
8984
9610
  function readPromptFileIfPresent(path11) {
8985
9611
  try {
8986
- const content = readFileSync6(path11, "utf8").trim();
9612
+ const content = readFileSync7(path11, "utf8").trim();
8987
9613
  return content || void 0;
8988
9614
  } catch {
8989
9615
  return void 0;
@@ -8992,7 +9618,7 @@ function readPromptFileIfPresent(path11) {
8992
9618
  function nearestWorkspaceAgentsPrompt(cwd) {
8993
9619
  let dir = cwd;
8994
9620
  while (true) {
8995
- const content = readPromptFileIfPresent(join9(dir, "AGENTS.md"));
9621
+ const content = readPromptFileIfPresent(join10(dir, "AGENTS.md"));
8996
9622
  if (content) return content;
8997
9623
  const parent = dirname6(dir);
8998
9624
  if (parent === dir) return void 0;
@@ -9101,8 +9727,8 @@ ${options.compressionSummary.trim()}`
9101
9727
  for (const s of extraSystemContent) {
9102
9728
  if (s.trim()) parts.push(s.trim());
9103
9729
  }
9104
- const configRoot = process.env.XDG_CONFIG_HOME ?? join9(homedir7(), ".config");
9105
- const globalAgents = readPromptFileIfPresent(join9(configRoot, "opencode", "AGENTS.md"));
9730
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join10(homedir8(), ".config");
9731
+ const globalAgents = readPromptFileIfPresent(join10(configRoot, "opencode", "AGENTS.md"));
9106
9732
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
9107
9733
  const forwarded = extraSystemContent.join("\n\n");
9108
9734
  const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
@@ -9113,9 +9739,9 @@ ${options.compressionSummary.trim()}`
9113
9739
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
9114
9740
  const content = parts.join("\n\n");
9115
9741
  if (!content) return void 0;
9116
- const path11 = join9(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
9742
+ const path11 = join10(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
9117
9743
  try {
9118
- writeFileSync4(path11, content, { encoding: "utf8", mode: 384 });
9744
+ writeFileSync5(path11, content, { encoding: "utf8", mode: 384 });
9119
9745
  return path11;
9120
9746
  } catch (err) {
9121
9747
  log.warn("failed to write system prompt file", { error: String(err) });
@@ -9833,8 +10459,8 @@ function synthesizeTitle(prompt) {
9833
10459
  "summary",
9834
10460
  "project"
9835
10461
  ]);
9836
- const words = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
9837
- const picked = (words.length > 0 ? words : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
10462
+ const words2 = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
10463
+ const picked = (words2.length > 0 ? words2 : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
9838
10464
  return picked || "New Session";
9839
10465
  }
9840
10466
 
@@ -9968,6 +10594,7 @@ function createTurnState(init) {
9968
10594
  lastCallUsage: void 0,
9969
10595
  resultFailure: void 0,
9970
10596
  accountLimitHit: null,
10597
+ usageLimitNote: false,
9971
10598
  accountBlock: null,
9972
10599
  modelRefusal: null,
9973
10600
  drainBuffer: [],
@@ -10297,6 +10924,11 @@ function clearStartWatchdog(state) {
10297
10924
  function onStartWatchdogFire(state) {
10298
10925
  state.startWatchdog = null;
10299
10926
  if (state.controllerClosed || state.hasReceivedContent || state.hasReceivedProgress) return;
10927
+ const interactive = state.activeProcess?.interactiveControl;
10928
+ if (interactive) {
10929
+ if (interactive.turnRunning()) armStartWatchdog(state);
10930
+ return;
10931
+ }
10300
10932
  if (state.respawnAttempted) {
10301
10933
  log.error(
10302
10934
  "claude process still silent after respawn; ending turn",
@@ -10748,6 +11380,7 @@ function createLineHandler(state, ctx) {
10748
11380
  resetsAt: info.resetsAt ?? info.overageResetsAt,
10749
11381
  window: info.rateLimitType
10750
11382
  };
11383
+ rememberAccountLimit(ctx.sourceAccount, state.accountLimitHit);
10751
11384
  }
10752
11385
  const note = reportRateLimitEvent(msg);
10753
11386
  if (note) {
@@ -11031,6 +11664,13 @@ ${plan}
11031
11664
  if (hasText) {
11032
11665
  state.hasReceivedContent = true;
11033
11666
  }
11667
+ const apiErrorReply = msg.is_api_error_message === true;
11668
+ const limitErrorReply = apiErrorReply && ctx.usageLimitNoteActive && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
11669
+ resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
11670
+ }));
11671
+ if (limitErrorReply) {
11672
+ state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
11673
+ }
11034
11674
  if (hasText && !hasToolUse) {
11035
11675
  startResultFallback(state);
11036
11676
  }
@@ -11039,6 +11679,7 @@ ${plan}
11039
11679
  }
11040
11680
  for (const block2 of msg.message.content) {
11041
11681
  if (block2.type === "text" && block2.text) {
11682
+ if (limitErrorReply) continue;
11042
11683
  resetLastVisibleTextBlock(state);
11043
11684
  const blockId = state.startTextBlock();
11044
11685
  state.controller.enqueue({
@@ -11251,7 +11892,15 @@ ${plan}
11251
11892
  if (deliverPendingCompletions(state)) {
11252
11893
  return;
11253
11894
  }
11254
- if (!state.currentTextId && msg.is_error && typeof msg.result === "string" && msg.result.trim().length > 0) {
11895
+ if (!state.accountLimitHit && msg.is_error && isAccountLimitError({
11896
+ resultText: typeof msg.result === "string" ? msg.result : null
11897
+ })) {
11898
+ state.accountLimitHit = recallAccountLimit(ctx.sourceAccount);
11899
+ }
11900
+ state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
11901
+ // and says what to run; two notes about one failure is one too many.
11902
+ !state.accountBlock && msg.is_error === true;
11903
+ if (!state.currentTextId && msg.is_error && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
11255
11904
  const errId = state.startTextBlock();
11256
11905
  state.controller.enqueue({
11257
11906
  type: "text-delta",
@@ -11259,11 +11908,6 @@ ${plan}
11259
11908
  delta: msg.result
11260
11909
  });
11261
11910
  }
11262
- if (!state.accountLimitHit && msg.is_error && isAccountLimitError({
11263
- resultText: typeof msg.result === "string" ? msg.result : null
11264
- })) {
11265
- state.accountLimitHit = {};
11266
- }
11267
11911
  if (ctx.modelFallbackArmed && !state.modelRefusal) {
11268
11912
  state.modelRefusal = modelRefusalFromResult(msg);
11269
11913
  }
@@ -11277,7 +11921,11 @@ ${plan}
11277
11921
  kind: state.accountBlock,
11278
11922
  account: ctx.sourceAccount,
11279
11923
  configDir: ctx.config.configDir,
11280
- offeringSwitch
11924
+ offeringSwitch,
11925
+ // Read only when the form is not taking the turn, which is the
11926
+ // default: moving the work by hand is then the operator's only
11927
+ // route and nothing else on screen says it exists (h #g194).
11928
+ candidates: ctx.failoverAccounts
11281
11929
  })
11282
11930
  });
11283
11931
  state.endTextBlock();
@@ -12101,6 +12749,23 @@ var ClaudeCodeLanguageModel = class {
12101
12749
  }
12102
12750
  }
12103
12751
  }
12752
+ if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
12753
+ const configDir = resolveConfigDir(this.config.configDir);
12754
+ const resumePoint = findResumePoint({
12755
+ sessionKey: sk,
12756
+ prompt: options.prompt,
12757
+ cliPath,
12758
+ transcriptPath: (id) => join11(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
12759
+ });
12760
+ if (resumePoint) {
12761
+ setClaudeSessionId(sk, resumePoint.claudeSessionId);
12762
+ includeHistoryContext = false;
12763
+ log.notice("resuming the claude session from before the restart instead of replaying it", {
12764
+ sessionKey: sk,
12765
+ matchedMessages: resumePoint.matched
12766
+ });
12767
+ }
12768
+ }
12104
12769
  if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
12105
12770
  recordForkFingerprint(sk, options.prompt, cliPath);
12106
12771
  }
@@ -12156,7 +12821,7 @@ var ClaudeCodeLanguageModel = class {
12156
12821
  this.config.failoverAccounts,
12157
12822
  sourceAccount
12158
12823
  );
12159
- const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover !== "off" && !compactionMode && !useInteractive && // A `doGenerate` caller takes the override and reports the plain
12824
+ const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && !useInteractive && // A `doGenerate` caller takes the override and reports the plain
12160
12825
  // rate-limit error, never the form. See `TurnMode`.
12161
12826
  mode !== "generate" && isAccountFailoverQuestionActive({
12162
12827
  configured: this.config.accountFailover,
@@ -12169,6 +12834,7 @@ var ClaudeCodeLanguageModel = class {
12169
12834
  // the operator is usually not even looking at.
12170
12835
  childSession: !!await fetchSessionParentId(affinity)
12171
12836
  });
12837
+ const usageLimitNoteActive = !compactionMode && mode !== "generate" && !failoverAskActive;
12172
12838
  let staleBuild = null;
12173
12839
  if (!compactionMode && mode !== "generate" && affinity !== "default") {
12174
12840
  staleBuild = staleBuildWatch().check();
@@ -12393,6 +13059,7 @@ var ClaudeCodeLanguageModel = class {
12393
13059
  ...self.skillBridgeSpawn(failover)
12394
13060
  });
12395
13061
  if (stoppedBeforeWork()) return;
13062
+ const resumeSessionId = getClaudeSessionId(sk);
12396
13063
  const ap = spawnInteractiveProcess({
12397
13064
  cwd,
12398
13065
  cliPath,
@@ -12404,8 +13071,20 @@ var ClaudeCodeLanguageModel = class {
12404
13071
  permissionsAllow: allow,
12405
13072
  systemPromptFile,
12406
13073
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
12407
- effort: reasoningEffort
13074
+ effort: reasoningEffort,
13075
+ resumeSessionId,
13076
+ // The headless spawn's env, so hygiene, effort and the
13077
+ // agent's prompt cache TTL reach both transports alike.
13078
+ env: claudeSpawnEnv({
13079
+ ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13080
+ effort: reasoningEffort,
13081
+ promptCacheTtl
13082
+ })
12408
13083
  });
13084
+ ap.proc.once(
13085
+ "exit",
13086
+ (code) => noteInteractiveProcessExit(sk, ap, code)
13087
+ );
12409
13088
  ap.mcpHash = mcp.bridgedHash;
12410
13089
  ap.mcpServers = mcp.allEnabledServerNames;
12411
13090
  setActiveProcess(sk, ap);
@@ -12414,6 +13093,7 @@ var ClaudeCodeLanguageModel = class {
12414
13093
  state.activeProcess = ap;
12415
13094
  log.info("spawned interactive claude session", {
12416
13095
  sk,
13096
+ resumed: !!resumeSessionId,
12417
13097
  cliPath,
12418
13098
  configDir: self.config.configDir,
12419
13099
  model: effectiveModelId
@@ -12653,6 +13333,9 @@ var ClaudeCodeLanguageModel = class {
12653
13333
  state.autoContinueState.startedAt = Date.now();
12654
13334
  const completeResult = (msg) => {
12655
13335
  if (state.controllerClosed) return;
13336
+ if (self.config.resumeAfterRestart !== false && !compactionMode && msg.is_error !== true && typeof msg.session_id === "string") {
13337
+ recordResumePoint(sk, msg.session_id, options.prompt, cliPath);
13338
+ }
12656
13339
  if (deliverPendingCompletions(state)) {
12657
13340
  if (state.drainBuffer.length > 0) drainNow(state);
12658
13341
  return;
@@ -12716,6 +13399,26 @@ var ClaudeCodeLanguageModel = class {
12716
13399
  return;
12717
13400
  }
12718
13401
  }
13402
+ if (state.usageLimitNote) {
13403
+ log.warn("claude account is out of usage; the turn ends with a note", {
13404
+ sessionKey: sk,
13405
+ account: sourceAccount,
13406
+ window: state.accountLimitHit?.window ?? null,
13407
+ resetsAt: state.accountLimitHit?.resetsAt ?? null,
13408
+ candidates: failoverAccounts
13409
+ });
13410
+ controller.enqueue({
13411
+ type: "text-delta",
13412
+ id: state.startTextBlock(),
13413
+ delta: formatUsageLimitNote({
13414
+ sourceAccount,
13415
+ candidates: failoverAccounts,
13416
+ resetsAt: state.accountLimitHit?.resetsAt,
13417
+ window: state.accountLimitHit?.window
13418
+ })
13419
+ });
13420
+ state.endTextBlock();
13421
+ }
12719
13422
  if (runAutoContinue(state, msg)) return;
12720
13423
  for (const [idx, reasoningId] of state.reasoningIds) {
12721
13424
  if (state.reasoningStarted.get(idx)) {
@@ -12789,6 +13492,8 @@ var ClaudeCodeLanguageModel = class {
12789
13492
  planModeQuestionActive,
12790
13493
  sourceAccount,
12791
13494
  failoverAskActive,
13495
+ usageLimitNoteActive,
13496
+ failoverAccounts,
12792
13497
  modelFallbackArmed,
12793
13498
  attempt,
12794
13499
  handleControlRequest: handleControlRequest2,
@@ -13087,15 +13792,15 @@ var ClaudeCodeLanguageModel = class {
13087
13792
 
13088
13793
  // src/cleanup-stale.ts
13089
13794
  import {
13090
- existsSync as existsSync3,
13091
- mkdirSync as mkdirSync4,
13092
- readFileSync as readFileSync7,
13795
+ existsSync as existsSync5,
13796
+ mkdirSync as mkdirSync5,
13797
+ readFileSync as readFileSync8,
13093
13798
  realpathSync as realpathSync4,
13094
13799
  rmSync as rmSync3,
13095
- writeFileSync as writeFileSync5
13800
+ writeFileSync as writeFileSync6
13096
13801
  } from "fs";
13097
- import { homedir as homedir8 } from "os";
13098
- import { dirname as dirname7, join as join10, resolve as resolve5 } from "path";
13802
+ import { homedir as homedir9 } from "os";
13803
+ import { dirname as dirname7, join as join12, resolve as resolve5 } from "path";
13099
13804
  import { fileURLToPath as fileURLToPath4 } from "url";
13100
13805
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
13101
13806
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -13103,20 +13808,20 @@ var alreadyRan = false;
13103
13808
  function candidateCacheRoots() {
13104
13809
  const xdg = process.env.XDG_CACHE_HOME;
13105
13810
  return [
13106
- xdg ? join10(xdg, "opencode") : null,
13107
- join10(homedir8(), ".cache", "opencode"),
13108
- join10(homedir8(), "Library", "Caches", "opencode")
13811
+ xdg ? join12(xdg, "opencode") : null,
13812
+ join12(homedir9(), ".cache", "opencode"),
13813
+ join12(homedir9(), "Library", "Caches", "opencode")
13109
13814
  ].filter((p) => Boolean(p));
13110
13815
  }
13111
13816
  function userOpencodeJsonPath() {
13112
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join10(homedir8(), ".config");
13113
- return join10(xdgConfig, "opencode", "opencode.json");
13817
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config");
13818
+ return join12(xdgConfig, "opencode", "opencode.json");
13114
13819
  }
13115
13820
  function userIntendsToUseUnscoped() {
13116
13821
  const cfg = userOpencodeJsonPath();
13117
- if (!existsSync3(cfg)) return false;
13822
+ if (!existsSync5(cfg)) return false;
13118
13823
  try {
13119
- const json = JSON.parse(readFileSync7(cfg, "utf8"));
13824
+ const json = JSON.parse(readFileSync8(cfg, "utf8"));
13120
13825
  const plugins = json.plugin;
13121
13826
  if (!Array.isArray(plugins)) return false;
13122
13827
  return plugins.some(
@@ -13137,19 +13842,19 @@ function ourLoadedDir() {
13137
13842
  function ourVersion(dir) {
13138
13843
  if (!dir) return null;
13139
13844
  try {
13140
- const pkg = JSON.parse(readFileSync7(join10(dir, "package.json"), "utf8"));
13845
+ const pkg = JSON.parse(readFileSync8(join12(dir, "package.json"), "utf8"));
13141
13846
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
13142
13847
  } catch {
13143
13848
  return null;
13144
13849
  }
13145
13850
  }
13146
13851
  function markerPath() {
13147
- const stateRoot = process.env.XDG_STATE_HOME ?? join10(homedir8(), ".local", "state");
13148
- return join10(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
13852
+ const stateRoot = process.env.XDG_STATE_HOME ?? join12(homedir9(), ".local", "state");
13853
+ return join12(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
13149
13854
  }
13150
13855
  function markerVersion() {
13151
13856
  try {
13152
- const marker = JSON.parse(readFileSync7(markerPath(), "utf8"));
13857
+ const marker = JSON.parse(readFileSync8(markerPath(), "utf8"));
13153
13858
  return typeof marker.version === "string" ? marker.version : null;
13154
13859
  } catch {
13155
13860
  return null;
@@ -13158,8 +13863,8 @@ function markerVersion() {
13158
13863
  function writeMarker(version) {
13159
13864
  const file = markerPath();
13160
13865
  try {
13161
- mkdirSync4(dirname7(file), { recursive: true });
13162
- writeFileSync5(
13866
+ mkdirSync5(dirname7(file), { recursive: true });
13867
+ writeFileSync6(
13163
13868
  file,
13164
13869
  JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
13165
13870
  "utf8"
@@ -13193,20 +13898,20 @@ function cleanupStaleUnscopedInstall() {
13193
13898
  if (version) writeMarker(version);
13194
13899
  }
13195
13900
  function cleanupOne(cacheRoot, ourDir) {
13196
- if (!existsSync3(cacheRoot)) return;
13197
- const stalePath = join10(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
13198
- if (!existsSync3(stalePath)) return;
13901
+ if (!existsSync5(cacheRoot)) return;
13902
+ const stalePath = join12(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
13903
+ if (!existsSync5(stalePath)) return;
13199
13904
  let realStalePath = stalePath;
13200
13905
  try {
13201
13906
  realStalePath = realpathSync4(stalePath);
13202
13907
  } catch {
13203
13908
  }
13204
13909
  if (ourDir && realStalePath === ourDir) return;
13205
- const pkgJsonPath = join10(stalePath, "package.json");
13206
- if (!existsSync3(pkgJsonPath)) return;
13910
+ const pkgJsonPath = join12(stalePath, "package.json");
13911
+ if (!existsSync5(pkgJsonPath)) return;
13207
13912
  let pkg = {};
13208
13913
  try {
13209
- pkg = JSON.parse(readFileSync7(pkgJsonPath, "utf8"));
13914
+ pkg = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
13210
13915
  } catch {
13211
13916
  return;
13212
13917
  }
@@ -13222,13 +13927,13 @@ function cleanupOne(cacheRoot, ourDir) {
13222
13927
  });
13223
13928
  return;
13224
13929
  }
13225
- const cachePkgJson = join10(cacheRoot, "package.json");
13226
- if (!existsSync3(cachePkgJson)) return;
13930
+ const cachePkgJson = join12(cacheRoot, "package.json");
13931
+ if (!existsSync5(cachePkgJson)) return;
13227
13932
  try {
13228
- const cfg = JSON.parse(readFileSync7(cachePkgJson, "utf8"));
13933
+ const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
13229
13934
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
13230
13935
  delete cfg.dependencies[STALE_PACKAGE_NAME];
13231
- writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
13936
+ writeFileSync6(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
13232
13937
  log.info("cleanup-stale: pruned dep from cache package.json");
13233
13938
  }
13234
13939
  } catch (err) {
@@ -13855,7 +14560,11 @@ function createClaudeCode(settings = {}) {
13855
14560
  configDir: settings.configDir,
13856
14561
  failoverAccounts: settings.failoverAccounts,
13857
14562
  baseCliPath: settings.baseCliPath ?? cliPath,
13858
- accountFailover: settings.accountFailover ?? "ask",
14563
+ // Passed through, with no default applied here: the form is opt-in and
14564
+ // `isAccountFailoverQuestionActive` asks for an explicit `"ask"`, so an
14565
+ // unset option must stay unset rather than being written to one mode or
14566
+ // the other in only one of the two entry points (h #g194).
14567
+ accountFailover: settings.accountFailover,
13859
14568
  providerID: settings.providerID,
13860
14569
  skipPermissions: preset?.skipPermissions ?? settings.skipPermissions ?? true,
13861
14570
  permissionMode: preset?.permissionMode ?? settings.permissionMode,
@@ -13885,6 +14594,7 @@ function createClaudeCode(settings = {}) {
13885
14594
  bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
13886
14595
  turnStats: settings.turnStats === true,
13887
14596
  forkSessions: settings.forkSessions === true,
14597
+ resumeAfterRestart: settings.resumeAfterRestart !== false,
13888
14598
  interactive: settings.interactive,
13889
14599
  interactiveBypass: settings.interactiveBypass,
13890
14600
  interactiveAllowTools: settings.interactiveAllowTools,
@@ -13970,12 +14680,12 @@ function configModelsForProvider(providerModels, providerID, modelSuffix) {
13970
14680
  }
13971
14681
  return models;
13972
14682
  }
13973
- function isRecord4(value) {
14683
+ function isRecord5(value) {
13974
14684
  return typeof value === "object" && value !== null && !Array.isArray(value);
13975
14685
  }
13976
14686
  function completeUserModel(id, entry, providerID) {
13977
- const record = isRecord4(entry) ? entry : {};
13978
- const runtimeShaped = isRecord4(record.capabilities) && isRecord4(record.capabilities.input) && isRecord4(record.capabilities.output) && isRecord4(record.cost) && isRecord4(record.cost.cache) && isRecord4(record.api);
14687
+ const record = isRecord5(entry) ? entry : {};
14688
+ const runtimeShaped = isRecord5(record.capabilities) && isRecord5(record.capabilities.input) && isRecord5(record.capabilities.output) && isRecord5(record.cost) && isRecord5(record.cost.cache) && isRecord5(record.api);
13979
14689
  if (runtimeShaped) {
13980
14690
  return toConfigModel({ ...record, providerID });
13981
14691
  }
@@ -13988,8 +14698,8 @@ function completeUserModel(id, entry, providerID) {
13988
14698
  const merged = { ...base, ...record, id };
13989
14699
  for (const key of ["cost", "limit", "variants"]) {
13990
14700
  merged[key] = {
13991
- ...isRecord4(base[key]) ? base[key] : {},
13992
- ...isRecord4(record[key]) ? record[key] : {}
14701
+ ...isRecord5(base[key]) ? base[key] : {},
14702
+ ...isRecord5(record[key]) ? record[key] : {}
13993
14703
  };
13994
14704
  }
13995
14705
  return merged;
@@ -14086,7 +14796,7 @@ async function buildAgentRegistry(config) {
14086
14796
  typeof configuredTtl === "string" ? configuredTtl : void 0
14087
14797
  );
14088
14798
  setProviderFallbackModels(parseFallbackModelList(options?.fallbackModels));
14089
- const records = await readAgentMarkdownRecords(
14799
+ const records2 = await readAgentMarkdownRecords(
14090
14800
  agentDirectories(
14091
14801
  process.env.HOME ?? process.env.USERPROFILE,
14092
14802
  getOpencodeProjectDirectory()
@@ -14101,18 +14811,18 @@ async function buildAgentRegistry(config) {
14101
14811
  const declaredChain = parseFallbackModelList(
14102
14812
  agent.fallbackModels ?? bag.fallbackModels
14103
14813
  );
14104
- records[name] = {
14105
- mode: pick("mode") ?? records[name]?.mode,
14106
- model: pick("model") ?? records[name]?.model,
14107
- forceModel: pick("forceModel") ?? records[name]?.forceModel,
14108
- reasoningEffort: pick("reasoningEffort") ?? records[name]?.reasoningEffort,
14109
- cacheTtl: pick("cacheTtl") ?? records[name]?.cacheTtl,
14110
- fallbackModels: declaredChain.length ? declaredChain : records[name]?.fallbackModels
14814
+ records2[name] = {
14815
+ mode: pick("mode") ?? records2[name]?.mode,
14816
+ model: pick("model") ?? records2[name]?.model,
14817
+ forceModel: pick("forceModel") ?? records2[name]?.forceModel,
14818
+ reasoningEffort: pick("reasoningEffort") ?? records2[name]?.reasoningEffort,
14819
+ cacheTtl: pick("cacheTtl") ?? records2[name]?.cacheTtl,
14820
+ fallbackModels: declaredChain.length ? declaredChain : records2[name]?.fallbackModels
14111
14821
  };
14112
14822
  }
14113
- setAgentRegistry(records);
14823
+ setAgentRegistry(records2);
14114
14824
  log.debug("agent registry built", {
14115
- agents: Object.keys(records).length,
14825
+ agents: Object.keys(records2).length,
14116
14826
  defaultSubagentModel: getDefaultSubagentModel()
14117
14827
  });
14118
14828
  }