@khalilgharbaoui/opencode-claude-code-plugin 0.57.2 → 0.59.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,5 +1,5 @@
1
1
  // src/claude-code-language-model.ts
2
- import { join as join18 } from "path";
2
+ import { join as join19 } from "path";
3
3
 
4
4
  // src/ids.ts
5
5
  import { randomInt } from "crypto";
@@ -272,6 +272,8 @@ var MAX_TOASTED_MESSAGES = 200;
272
272
  var toastBurstLimit = 3;
273
273
  var toastBurstWindowMs = 2e3;
274
274
  var activeSink = null;
275
+ var activeNotifier = null;
276
+ var bufferedNotices = [];
275
277
  var bufferedEntries = [];
276
278
  var droppedBeforeSink = 0;
277
279
  var toastedMessages = /* @__PURE__ */ new Set();
@@ -296,6 +298,13 @@ function setLogSink(sink) {
296
298
  });
297
299
  }
298
300
  }
301
+ function setToastNotifier(notifier) {
302
+ activeNotifier = notifier;
303
+ if (!notifier) return;
304
+ const pending2 = bufferedNotices;
305
+ bufferedNotices = [];
306
+ for (const entry of pending2) maybeToast(entry, notifier);
307
+ }
299
308
  function routeToSink(entry) {
300
309
  if (!activeSink) {
301
310
  if (bufferedEntries.length >= MAX_BUFFERED_SINK_ENTRIES) {
@@ -314,18 +323,34 @@ function deliverToSink(entry) {
314
323
  sink.log(entry);
315
324
  } catch {
316
325
  }
326
+ maybeToast(entry, (toast) => sink.toast(toast));
327
+ }
328
+ function maybeToast(entry, send) {
317
329
  if (entry.level !== "warn" && entry.level !== "error") return;
318
330
  if (toastedMessages.has(entry.message)) return;
319
331
  if (toastedMessages.size < MAX_TOASTED_MESSAGES) toastedMessages.add(entry.message);
320
332
  if (!allowToastNow()) return;
321
- sendToast(sink, entry);
333
+ sendToast(send, entry);
322
334
  }
323
- function sendToast(sink, entry) {
335
+ function sendToast(send, entry) {
324
336
  try {
325
- sink.toast(entry);
337
+ send(entry);
326
338
  } catch {
327
339
  }
328
340
  }
341
+ function toastTarget() {
342
+ const sink = activeSink;
343
+ if (sink) return (entry) => sink.toast(entry);
344
+ return activeNotifier;
345
+ }
346
+ function routeToNotifier(entry) {
347
+ const notifier = activeNotifier;
348
+ if (notifier) {
349
+ maybeToast(entry, notifier);
350
+ return;
351
+ }
352
+ if (bufferedNotices.length < MAX_BUFFERED_SINK_ENTRIES) bufferedNotices.push(entry);
353
+ }
329
354
  function allowToastNow() {
330
355
  const now = Date.now();
331
356
  if (now - burstWindowStart >= toastBurstWindowMs) {
@@ -347,10 +372,11 @@ function armBurstSummary() {
347
372
  const suppressed = burstSuppressed;
348
373
  burstSuppressed = 0;
349
374
  if (suppressed <= 0) return;
350
- const sink = activeSink;
351
- if (!sink) return;
352
- const message = `${suppressed} more claude-code warning(s) were not shown; the full text is in opencode's log`;
353
- sendToast(sink, { level: "warn", message, line: fmt("WARN", message) });
375
+ const target = toastTarget();
376
+ if (!target) return;
377
+ const where = activeSink ? "opencode's log" : "the plugin log file";
378
+ const message = `${suppressed} more claude-code warning(s) were not shown; the full text is in ${where}`;
379
+ sendToast(target, { level: "warn", message, line: fmt("WARN", message) });
354
380
  }, toastBurstWindowMs);
355
381
  burstSummaryTimer.unref?.();
356
382
  }
@@ -366,6 +392,7 @@ function emit(level, msg, data) {
366
392
  routeToSink({ level, message: msg, data, line });
367
393
  } else {
368
394
  console.error(line);
395
+ if (level === "warn" || level === "error") routeToNotifier({ level, message: msg, data, line });
369
396
  }
370
397
  }
371
398
  writeToFile(line);
@@ -6374,10 +6401,71 @@ function watchChildSession(sessionID) {
6374
6401
  });
6375
6402
  return () => answer;
6376
6403
  }
6404
+ var eventRunStates = /* @__PURE__ */ new Map();
6405
+ var MAX_EVENT_RUN_STATES = 512;
6406
+ var openEventFeeds = 0;
6407
+ function openSessionRunEventFeed() {
6408
+ openEventFeeds += 1;
6409
+ let closed = false;
6410
+ return () => {
6411
+ if (closed) return;
6412
+ closed = true;
6413
+ openEventFeeds = Math.max(0, openEventFeeds - 1);
6414
+ eventRunStates.clear();
6415
+ };
6416
+ }
6417
+ function noteSessionRunEvent(event) {
6418
+ if (openEventFeeds === 0 || !event || typeof event !== "object") return;
6419
+ const { type, data } = event;
6420
+ if (typeof type !== "string" || !data || typeof data !== "object") return;
6421
+ const sessionID = data.sessionID;
6422
+ if (typeof sessionID !== "string" || !sessionID || sessionID === "default") return;
6423
+ if (type === "session.deleted") {
6424
+ eventRunStates.delete(sessionID);
6425
+ return;
6426
+ }
6427
+ let next;
6428
+ switch (type) {
6429
+ case "session.execution.started":
6430
+ next = "busy";
6431
+ break;
6432
+ case "session.execution.succeeded":
6433
+ case "session.execution.failed":
6434
+ case "session.execution.interrupted":
6435
+ case "session.idle":
6436
+ next = "idle";
6437
+ break;
6438
+ case "session.status": {
6439
+ const status = data.status?.type;
6440
+ if (typeof status === "string") next = status === "idle" ? "idle" : "busy";
6441
+ break;
6442
+ }
6443
+ }
6444
+ if (!next) return;
6445
+ eventRunStates.delete(sessionID);
6446
+ eventRunStates.set(sessionID, next);
6447
+ while (eventRunStates.size > MAX_EVENT_RUN_STATES) {
6448
+ const oldest = eventRunStates.keys().next().value;
6449
+ if (oldest === void 0) break;
6450
+ eventRunStates.delete(oldest);
6451
+ }
6452
+ }
6453
+ function sessionRunStateFromEvents(sessionID) {
6454
+ if (openEventFeeds === 0) return "unknown";
6455
+ return eventRunStates.get(sessionID) ?? "unknown";
6456
+ }
6457
+ function busySessionsFromEvents() {
6458
+ if (openEventFeeds === 0) return void 0;
6459
+ const busy = {};
6460
+ for (const [sessionID, state] of eventRunStates) {
6461
+ if (state === "busy") busy[sessionID] = { type: "busy" };
6462
+ }
6463
+ return busy;
6464
+ }
6377
6465
  async function fetchSessionRunState(sessionID, directory) {
6378
6466
  if (!sessionID || sessionID === "default") return "unknown";
6379
6467
  const status = opencodeClient?.session?.status;
6380
- if (!status) return "unknown";
6468
+ if (!status) return sessionRunStateFromEvents(sessionID);
6381
6469
  try {
6382
6470
  const dir = directory ?? await fetchSessionDirectory(sessionID);
6383
6471
  const res = await status.call(
@@ -7734,6 +7822,27 @@ function deleteActiveProcessesForSession(sessionID) {
7734
7822
  forgetDispatchChoicesForSession(sessionID);
7735
7823
  return released;
7736
7824
  }
7825
+ var STOPPED_TURN_MESSAGE = "opencode stopped the turn while it was running its proxy tool calls";
7826
+ function releaseParkedProxyCallsForSession(sessionID, reason = STOPPED_TURN_MESSAGE) {
7827
+ if (!sessionID || sessionID === "default") return [];
7828
+ const released = [];
7829
+ for (const [key, ap] of [...activeProcesses]) {
7830
+ const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
7831
+ if (!owned) continue;
7832
+ if (ap.lineEmitter.listenerCount("line") > 0) continue;
7833
+ if (getPendingProxyCalls(key).length === 0) continue;
7834
+ log.info("opencode stopped the turn at a proxy tool boundary; releasing pending calls", {
7835
+ sessionKey: key,
7836
+ sessionID
7837
+ });
7838
+ void interruptTurn(ap).then((idle) => {
7839
+ log.info("interrupt sent for aborted turn", { sk: key, idle });
7840
+ });
7841
+ rejectAllPendingProxyCallsForSession(key, new Error(reason));
7842
+ released.push(key);
7843
+ }
7844
+ return released;
7845
+ }
7737
7846
  function killAllActiveProcesses() {
7738
7847
  const keys = [...activeProcesses.keys()];
7739
7848
  for (const key of keys) deleteActiveProcess(key);
@@ -8373,17 +8482,14 @@ async function fetchAsideHistory(client, sessionID, question) {
8373
8482
  return [];
8374
8483
  }
8375
8484
  }
8376
- async function handleBtwCommand(client, input, options = {}) {
8485
+ async function runBtwCommand(client, input, options = {}) {
8377
8486
  const question = input.arguments.trim();
8378
- if (!question) {
8379
- showToast(client, { title: "btw", message: SIDE_QUESTION_USAGE, variant: "warning", duration: 6e3 });
8380
- throw new BtwHandledError("/btw needs a question.");
8381
- }
8487
+ if (!question) return "bare";
8382
8488
  const active = await waitForAsideProcess(client, input.sessionID, options);
8383
8489
  const transport = active?.asideTransport;
8384
8490
  if (!active || !transport) {
8385
8491
  if (active) log.info("btw: process has no aside transport, leaving it to the turn", { sessionID: input.sessionID });
8386
- return;
8492
+ return "release";
8387
8493
  }
8388
8494
  let busy = false;
8389
8495
  let inlineDone = false;
@@ -8444,7 +8550,7 @@ async function handleBtwCommand(client, input, options = {}) {
8444
8550
  }
8445
8551
  );
8446
8552
  }
8447
- if (!busy) return;
8553
+ if (!busy) return "release";
8448
8554
  const started2 = Date.now();
8449
8555
  const outcome = await Promise.race([
8450
8556
  inlineDelivered,
@@ -8457,18 +8563,46 @@ async function handleBtwCommand(client, input, options = {}) {
8457
8563
  sessionID: input.sessionID,
8458
8564
  waitedMs: Date.now() - started2
8459
8565
  });
8460
- throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
8566
+ return "inline";
8461
8567
  }
8462
8568
  log.info("btw: turn over, releasing the /btw message", {
8463
8569
  sessionID: input.sessionID,
8464
8570
  idle: outcome === "idle",
8465
8571
  waitedMs: Date.now() - started2
8466
8572
  });
8573
+ return outcome === "timeout" ? "timeout" : "release";
8574
+ }
8575
+ async function handleBtwCommand(client, input, options = {}) {
8576
+ const outcome = await runBtwCommand(client, input, options);
8577
+ if (outcome === "bare") {
8578
+ showToast(client, { title: "btw", message: SIDE_QUESTION_USAGE, variant: "warning", duration: 6e3 });
8579
+ throw new BtwHandledError("/btw needs a question.");
8580
+ }
8581
+ if (outcome === "inline") throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
8467
8582
  if (outcome === "timeout") {
8468
8583
  showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
8469
8584
  throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
8470
8585
  }
8471
8586
  }
8587
+ async function askBtwThenRelease(client, input, release2, options = {}) {
8588
+ let outcome;
8589
+ try {
8590
+ outcome = await runBtwCommand(client, input, options);
8591
+ } catch (error) {
8592
+ log.warn("btw: the early aside failed; sending the /btw message as it is", {
8593
+ sessionID: input.sessionID,
8594
+ error: errorText(error)
8595
+ });
8596
+ outcome = "failed";
8597
+ }
8598
+ if (outcome === "inline") return outcome;
8599
+ try {
8600
+ await release2();
8601
+ } catch (error) {
8602
+ log.warn("btw: could not send the /btw message", { sessionID: input.sessionID, error: errorText(error) });
8603
+ }
8604
+ return outcome;
8605
+ }
8472
8606
 
8473
8607
  // src/budget-ledger.ts
8474
8608
  var BUDGET_MARKER = "\u258C **budget:**";
@@ -8638,7 +8772,9 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
8638
8772
  "automatic account switch skipped an account whose runtime could not be prepared",
8639
8773
  "background dispatch refused by maxBackgroundSubagents",
8640
8774
  "btw: a turn is running but no claude process appeared for it",
8775
+ "btw: could not send the /btw message",
8641
8776
  "btw: early aside failed; the message will ask again",
8777
+ "btw: the early aside failed; sending the /btw message as it is",
8642
8778
  "budget option refused: not a positive number of dollars",
8643
8779
  "budgetWarnAt refused: expected percentages between 0 and 100",
8644
8780
  "cannot interrupt this transport; waiting for the turn to end",
@@ -8727,7 +8863,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
8727
8863
  "no configured account could take this turn; it ends here",
8728
8864
  "no proxyTools entry was recognised; nothing will be proxied this turn",
8729
8865
  "no stdout after envelope write; respawning claude process to resume conversation",
8730
- "opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction",
8866
+ "opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction, and stopped turns keep their proxy calls until the next message",
8731
8867
  "opencode's stored OAuth token for this MCP server has expired, so the bridged server carries no credential; re-authenticate it in opencode",
8732
8868
  "pending proxy call arrived after stream close; rejecting",
8733
8869
  'permissionMode "plan" is enforced: claude cannot edit files or run commands, and --dangerously-skip-permissions is deliberately not passed so it stays that way. Headless Claude Code is not offered an ExitPlanMode tool, so nothing releases plan mode mid-session; approving a plan in chat does not unlock writes. Leaving plan mode means changing the config and restarting opencode.',
@@ -9635,6 +9771,625 @@ async function fetchPlanUsage(cliPath, options = {}) {
9635
9771
  }
9636
9772
  }
9637
9773
 
9774
+ // src/turn-history.ts
9775
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, renameSync as renameSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
9776
+ import { homedir as homedir11 } from "os";
9777
+ import { join as join11 } from "path";
9778
+
9779
+ // src/turn-progress-store.ts
9780
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
9781
+ import { homedir as homedir10 } from "os";
9782
+ import { join as join10 } from "path";
9783
+ var MAX_TURN_PROGRESS = 64;
9784
+ var TURN_PROGRESS_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
9785
+ var TAG = /^[0-9a-f]{8}$/;
9786
+ var pathOverride4 = null;
9787
+ function turnProgressStorePath() {
9788
+ if (pathOverride4) return pathOverride4;
9789
+ const stateRoot = process.env.XDG_STATE_HOME || join10(homedir10(), ".local", "state");
9790
+ return join10(stateRoot, "opencode-claude-code-plugin", "turn-progress.json");
9791
+ }
9792
+ function isProgress(value) {
9793
+ const record = value;
9794
+ if (!record || typeof record !== "object" || Array.isArray(record)) return false;
9795
+ const count = (field) => typeof field === "number" && Number.isFinite(field) && field >= 0;
9796
+ return count(record.turn) && count(record.out) && count(record.at);
9797
+ }
9798
+ function readEntries(file, now) {
9799
+ const out = /* @__PURE__ */ new Map();
9800
+ try {
9801
+ const parsed = JSON.parse(readFileSync6(file, "utf8"));
9802
+ const entries = parsed && typeof parsed === "object" ? parsed.turns : null;
9803
+ if (!entries || typeof entries !== "object") return out;
9804
+ const cutoff = now - TURN_PROGRESS_MAX_AGE_MS;
9805
+ const valid = Object.entries(entries).filter((entry) => TAG.test(entry[0]) && isProgress(entry[1])).filter(([, record]) => record.at >= cutoff).sort((a, b) => a[1].at - b[1].at).slice(-MAX_TURN_PROGRESS);
9806
+ for (const [tag, record] of valid) out.set(tag, { turn: record.turn, out: Math.round(record.out), at: record.at });
9807
+ } catch {
9808
+ }
9809
+ return out;
9810
+ }
9811
+ function recordTurnProgress(tag, progress, now = Date.now()) {
9812
+ const record = { turn: progress.turn, out: progress.out, at: now };
9813
+ if (!TAG.test(tag) || !isProgress(record)) return false;
9814
+ const file = turnProgressStorePath();
9815
+ const merged = readEntries(file, now);
9816
+ merged.delete(tag);
9817
+ merged.set(tag, { ...record, out: Math.round(record.out) });
9818
+ const kept = [...merged.entries()].slice(-MAX_TURN_PROGRESS);
9819
+ try {
9820
+ mkdirSync6(join10(file, ".."), { recursive: true, mode: 448 });
9821
+ const tmp = `${file}.${process.pid}.tmp`;
9822
+ writeFileSync6(tmp, JSON.stringify({ version: 1, turns: Object.fromEntries(kept) }), { mode: 384 });
9823
+ renameSync5(tmp, file);
9824
+ } catch {
9825
+ return false;
9826
+ }
9827
+ return true;
9828
+ }
9829
+
9830
+ // src/turn-history.ts
9831
+ var MAX_TURN_SAMPLES = 48;
9832
+ var MAX_TURN_KEYS = 64;
9833
+ var TURN_SAMPLE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
9834
+ var SESSION_TAG = /^[0-9a-f]{8}$/;
9835
+ var pathOverride5 = null;
9836
+ function turnHistoryStorePath() {
9837
+ if (pathOverride5) return pathOverride5;
9838
+ const stateRoot = process.env.XDG_STATE_HOME || join11(homedir11(), ".local", "state");
9839
+ return join11(stateRoot, "opencode-claude-code-plugin", "turn-history.json");
9840
+ }
9841
+ function turnHistoryKey(agent, model) {
9842
+ return `${agent || "main"}/${model}`;
9843
+ }
9844
+ function sessionTag(sessionID) {
9845
+ if (!sessionID || sessionID === "default") return void 0;
9846
+ let hash = 2166136261;
9847
+ for (let index = 0; index < sessionID.length; index += 1) {
9848
+ hash ^= sessionID.charCodeAt(index);
9849
+ hash = Math.imul(hash, 16777619) >>> 0;
9850
+ }
9851
+ return hash.toString(16).padStart(8, "0");
9852
+ }
9853
+ var INJECTED_BLOCKS = [
9854
+ /<system-reminder>[\s\S]*?<\/system-reminder>/g,
9855
+ /<dcp-system-reminder>[\s\S]*?<\/dcp-system-reminder>/g,
9856
+ /<dcp-message-id>[\s\S]*?<\/dcp-message-id>/g
9857
+ ];
9858
+ function promptChars(texts) {
9859
+ if (texts.length === 0) return void 0;
9860
+ let total = 0;
9861
+ for (const text of texts) {
9862
+ let stripped = text;
9863
+ for (const block of INJECTED_BLOCKS) stripped = stripped.replace(block, "");
9864
+ total += stripped.trim().length;
9865
+ }
9866
+ return total;
9867
+ }
9868
+ function textsOf(content) {
9869
+ if (typeof content === "string") return [content];
9870
+ if (!Array.isArray(content)) return [];
9871
+ const texts = [];
9872
+ for (const part of content) {
9873
+ if (!part || typeof part !== "object" || part.type !== "text") continue;
9874
+ const text = part.text;
9875
+ if (typeof text === "string") texts.push(text);
9876
+ }
9877
+ return texts;
9878
+ }
9879
+ function newestPromptChars(prompt) {
9880
+ for (let index = prompt.length - 1; index >= 0; index -= 1) {
9881
+ if (prompt[index].role === "user") return promptChars(textsOf(prompt[index].content));
9882
+ }
9883
+ return void 0;
9884
+ }
9885
+ function toolResultParts(message) {
9886
+ if (!Array.isArray(message.content)) return [];
9887
+ return message.content.filter(
9888
+ (part) => !!part && typeof part === "object" && part.type === "tool-result"
9889
+ );
9890
+ }
9891
+ function startsTurn(prompt, pendingToolCallIds) {
9892
+ const last = prompt[prompt.length - 1];
9893
+ if (!last || last.role !== "user") return false;
9894
+ const before = prompt[prompt.length - 2];
9895
+ if (before === void 0 || before.role !== "tool") return true;
9896
+ if (!pendingToolCallIds) return false;
9897
+ return toolResultParts(before).every((part) => typeof part.toolCallId !== "string" || !pendingToolCallIds.has(part.toolCallId));
9898
+ }
9899
+ function turnToolCalls(prompt, from) {
9900
+ let start = 0;
9901
+ if (from !== void 0 && from >= 0 && from <= prompt.length) {
9902
+ start = from;
9903
+ } else {
9904
+ for (let index = prompt.length - 1; index >= 0; index -= 1) {
9905
+ if (prompt[index].role !== "user") continue;
9906
+ const before = prompt[index - 1];
9907
+ if (before === void 0 || before.role !== "tool") {
9908
+ start = index;
9909
+ break;
9910
+ }
9911
+ }
9912
+ }
9913
+ let calls = 0;
9914
+ for (let index = start; index < prompt.length; index += 1) {
9915
+ const message = prompt[index];
9916
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
9917
+ for (const part of message.content) {
9918
+ if (part && typeof part === "object" && part.type === "tool-call") calls += 1;
9919
+ }
9920
+ }
9921
+ return calls;
9922
+ }
9923
+ function lastToolResultFailed(prompt, from) {
9924
+ for (let index = prompt.length - 1; index >= Math.max(0, from); index -= 1) {
9925
+ const message = prompt[index];
9926
+ if (message.role === "user") continue;
9927
+ if (message.role !== "tool") return false;
9928
+ const parts = toolResultParts(message);
9929
+ const newest = parts[parts.length - 1];
9930
+ const output = newest?.output;
9931
+ const type = output && typeof output === "object" ? output.type : void 0;
9932
+ return type === "error-text" || type === "error-json" || type === "execution-denied";
9933
+ }
9934
+ return false;
9935
+ }
9936
+ var MAX_TURN_STARTS = 256;
9937
+ var turnStarts = /* @__PURE__ */ new Map();
9938
+ function markTurnStart(key, start) {
9939
+ turnStarts.delete(key);
9940
+ if (turnStarts.size >= MAX_TURN_STARTS) {
9941
+ const oldest = turnStarts.keys().next().value;
9942
+ if (oldest !== void 0) turnStarts.delete(oldest);
9943
+ }
9944
+ turnStarts.set(key, {
9945
+ at: start.at,
9946
+ ...start.chars !== void 0 ? { chars: start.chars } : {},
9947
+ ...start.promptLength !== void 0 ? { promptLength: start.promptLength } : {},
9948
+ ...start.session !== void 0 ? { session: start.session } : {}
9949
+ });
9950
+ }
9951
+ function noteTurnOutput(key, tokens, now = Date.now()) {
9952
+ const start = turnStarts.get(key);
9953
+ if (!start || !Number.isFinite(tokens) || tokens < 0) return;
9954
+ start.out = (start.out ?? 0) + tokens;
9955
+ if (start.session) recordTurnProgress(start.session, { turn: start.at, out: start.out }, now);
9956
+ }
9957
+ function takeTurnStart(key) {
9958
+ const start = turnStarts.get(key);
9959
+ turnStarts.delete(key);
9960
+ return start;
9961
+ }
9962
+ function isSample(value) {
9963
+ const sample = value;
9964
+ if (!sample || typeof sample !== "object" || Array.isArray(sample)) return false;
9965
+ if (typeof sample.ms !== "number" || !Number.isFinite(sample.ms) || sample.ms < 0) return false;
9966
+ if (typeof sample.at !== "number" || !Number.isFinite(sample.at)) return false;
9967
+ if (sample.chars !== void 0 && (typeof sample.chars !== "number" || !Number.isFinite(sample.chars) || sample.chars < 0)) {
9968
+ return false;
9969
+ }
9970
+ if (sample.effort !== void 0 && typeof sample.effort !== "string") return false;
9971
+ if (sample.calls !== void 0 && (typeof sample.calls !== "number" || !Number.isFinite(sample.calls) || sample.calls < 0)) {
9972
+ return false;
9973
+ }
9974
+ if (sample.out !== void 0 && (typeof sample.out !== "number" || !Number.isFinite(sample.out) || sample.out < 0)) {
9975
+ return false;
9976
+ }
9977
+ if (sample.failed !== void 0 && typeof sample.failed !== "boolean") return false;
9978
+ if (sample.session !== void 0 && (typeof sample.session !== "string" || !SESSION_TAG.test(sample.session))) {
9979
+ return false;
9980
+ }
9981
+ return true;
9982
+ }
9983
+ function cleanSample(sample) {
9984
+ return {
9985
+ ms: Math.round(sample.ms),
9986
+ ...sample.chars !== void 0 ? { chars: Math.round(sample.chars) } : {},
9987
+ ...sample.effort ? { effort: sample.effort } : {},
9988
+ ...sample.calls !== void 0 ? { calls: Math.round(sample.calls) } : {},
9989
+ ...sample.out !== void 0 ? { out: Math.round(sample.out) } : {},
9990
+ ...sample.failed ? { failed: true } : {},
9991
+ ...sample.session ? { session: sample.session } : {},
9992
+ at: sample.at
9993
+ };
9994
+ }
9995
+ function readEntries2(file, now) {
9996
+ const out = /* @__PURE__ */ new Map();
9997
+ try {
9998
+ const parsed = JSON.parse(readFileSync7(file, "utf8"));
9999
+ const entries = parsed && typeof parsed === "object" ? parsed.turns : null;
10000
+ if (!entries || typeof entries !== "object") return out;
10001
+ const cutoff = now - TURN_SAMPLE_MAX_AGE_MS;
10002
+ for (const [key, value] of Object.entries(entries)) {
10003
+ if (!key || !Array.isArray(value)) continue;
10004
+ const samples = value.filter(isSample).filter((sample) => sample.at >= cutoff).sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES).map(cleanSample);
10005
+ if (samples.length > 0) out.set(key, samples);
10006
+ }
10007
+ } catch {
10008
+ }
10009
+ return out;
10010
+ }
10011
+ function bounded2(history) {
10012
+ const newest = (samples) => samples[samples.length - 1]?.at ?? 0;
10013
+ return new Map([...history.entries()].sort((a, b) => newest(a[1]) - newest(b[1])).slice(-MAX_TURN_KEYS));
10014
+ }
10015
+ function recordTurnSample(key, sample) {
10016
+ if (!key || !isSample(sample)) return false;
10017
+ const file = turnHistoryStorePath();
10018
+ const merged = readEntries2(file, sample.at);
10019
+ const samples = [...merged.get(key) ?? [], cleanSample(sample)].sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES);
10020
+ merged.set(key, samples);
10021
+ const kept = bounded2(merged);
10022
+ try {
10023
+ mkdirSync7(join11(file, ".."), { recursive: true, mode: 448 });
10024
+ const tmp = `${file}.${process.pid}.tmp`;
10025
+ writeFileSync7(tmp, JSON.stringify({ version: 1, turns: Object.fromEntries(kept) }), { mode: 384 });
10026
+ renameSync6(tmp, file);
10027
+ } catch {
10028
+ return false;
10029
+ }
10030
+ return true;
10031
+ }
10032
+ function createTurnHistoryReader(file = turnHistoryStorePath) {
10033
+ let identity = "";
10034
+ let cached2 = /* @__PURE__ */ new Map();
10035
+ return {
10036
+ read(now = Date.now()) {
10037
+ const path15 = file();
10038
+ let next = "";
10039
+ try {
10040
+ const stats = statSync6(path15);
10041
+ next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
10042
+ } catch {
10043
+ next = "missing";
10044
+ }
10045
+ if (next !== identity) {
10046
+ identity = next;
10047
+ cached2 = next === "missing" ? /* @__PURE__ */ new Map() : readEntries2(path15, now);
10048
+ }
10049
+ return cached2;
10050
+ }
10051
+ };
10052
+ }
10053
+
10054
+ // src/turn-model.ts
10055
+ var MIN_TURN_SAMPLES = 3;
10056
+ var CHARS_BANDWIDTH = Math.log(4);
10057
+ var OTHER_EFFORT_WEIGHT = 0.5;
10058
+ var MIN_EFFORT_RATIO_SAMPLES = 2;
10059
+ var RECENCY_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1e3;
10060
+ var MIN_CONDITIONED_SAMPLES = 1;
10061
+ var SHRINKAGE_KAPPA = 2;
10062
+ var CONDITIONED_KAPPA = 1;
10063
+ var DEFAULT_LOG_SIGMA = 0.8;
10064
+ var MIN_LOG_SIGMA = 0.2;
10065
+ var MAX_LOG_SIGMA = 1.6;
10066
+ var CALIBRATION_PRIOR_RESIDUALS = 12;
10067
+ var MAX_ASSESSED_PER_KEY = 16;
10068
+ var MAX_CALIBRATION_RESIDUALS = 512;
10069
+ var GRID_LOW = -8;
10070
+ var GRID_HIGH = 8;
10071
+ var GRID_STEP = 0.04;
10072
+ var CHARS_FLOOR = 50;
10073
+ function normalCdf(z) {
10074
+ if (!Number.isFinite(z)) return z > 0 ? 1 : 0;
10075
+ const x = Math.abs(z);
10076
+ const t = 1 / (1 + 0.2316419 * x);
10077
+ const poly = t * (0.31938153 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
10078
+ const tail = Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI) * poly;
10079
+ return z >= 0 ? 1 - tail : tail;
10080
+ }
10081
+ function normalQuantile(p) {
10082
+ const q = Math.min(1 - 1e-12, Math.max(1e-12, p));
10083
+ const a = [-39.69683028665376, 220.9460984245205, -275.9285104469687, 138.357751867269, -30.66479806614716, 2.506628277459239];
10084
+ const b = [-54.47609879822406, 161.5858368580409, -155.6989798598866, 66.80131188771972, -13.28068155288572];
10085
+ const c = [-0.007784894002430293, -0.3223964580411365, -2.400758277161838, -2.549732539343734, 4.374664141464968, 2.938163982698783];
10086
+ const d = [0.007784695709041462, 0.3224671290700398, 2.445134137142996, 3.754408661907416];
10087
+ const low = 0.02425;
10088
+ if (q < low || q > 1 - low) {
10089
+ const r2 = Math.sqrt(-2 * Math.log(q < low ? q : 1 - q));
10090
+ const x = (((((c[0] * r2 + c[1]) * r2 + c[2]) * r2 + c[3]) * r2 + c[4]) * r2 + c[5]) / ((((d[0] * r2 + d[1]) * r2 + d[2]) * r2 + d[3]) * r2 + 1);
10091
+ return q < low ? x : -x;
10092
+ }
10093
+ const r = q - 0.5;
10094
+ const s = r * r;
10095
+ return (((((a[0] * s + a[1]) * s + a[2]) * s + a[3]) * s + a[4]) * s + a[5]) * r / (((((b[0] * s + b[1]) * s + b[2]) * s + b[3]) * s + b[4]) * s + 1);
10096
+ }
10097
+ function charsWeight(a, b) {
10098
+ if (a === void 0 || b === void 0) return 1;
10099
+ const distance = Math.log((a + CHARS_FLOOR) / (b + CHARS_FLOOR)) / CHARS_BANDWIDTH;
10100
+ return Math.exp(-0.5 * distance * distance);
10101
+ }
10102
+ function effortWeight(a, b) {
10103
+ return a === void 0 || b === void 0 || a === b ? 1 : OTHER_EFFORT_WEIGHT;
10104
+ }
10105
+ function recencyWeight(now, at) {
10106
+ const age = Math.max(0, now - at);
10107
+ return Math.pow(0.5, age / RECENCY_HALF_LIFE_MS);
10108
+ }
10109
+ function weightedMedian(values, weights) {
10110
+ const order = values.map((_, index) => index).sort((a, b) => values[a] - values[b]);
10111
+ const total = order.reduce((sum, index) => sum + weights[index], 0);
10112
+ let cumulative = 0;
10113
+ for (const index of order) {
10114
+ cumulative += weights[index];
10115
+ if (cumulative >= total / 2) return values[index];
10116
+ }
10117
+ return values[order[order.length - 1]];
10118
+ }
10119
+ function effortScales(samples, weights, effort) {
10120
+ const scales = /* @__PURE__ */ new Map();
10121
+ if (effort === void 0) return scales;
10122
+ const groups = /* @__PURE__ */ new Map();
10123
+ samples.forEach((sample, index) => {
10124
+ if (sample.effort === void 0) return;
10125
+ const group = groups.get(sample.effort) ?? [];
10126
+ group.push(index);
10127
+ groups.set(sample.effort, group);
10128
+ });
10129
+ const own = groups.get(effort);
10130
+ if (!own || own.length < MIN_EFFORT_RATIO_SAMPLES) return scales;
10131
+ const medianOf = (indices) => weightedMedian(
10132
+ indices.map((index) => samples[index].ms),
10133
+ indices.map((index) => weights[index])
10134
+ );
10135
+ const ownMedian = medianOf(own);
10136
+ for (const [other, indices] of groups) {
10137
+ if (other === effort || indices.length < MIN_EFFORT_RATIO_SAMPLES) continue;
10138
+ const otherMedian = medianOf(indices);
10139
+ if (ownMedian > 0 && otherMedian > 0) scales.set(other, ownMedian / otherMedian);
10140
+ }
10141
+ return scales;
10142
+ }
10143
+ function usableSamples(samples) {
10144
+ return samples.filter((sample) => Number.isFinite(sample.ms) && sample.ms >= 0 && !sample.failed);
10145
+ }
10146
+ function prepare(samples, input, now) {
10147
+ const usable = usableSamples(samples);
10148
+ const base = usable.map((sample) => charsWeight(input.chars, sample.chars) * recencyWeight(now, sample.at));
10149
+ const scales = effortScales(usable, base, input.effort);
10150
+ return usable.map((sample, index) => {
10151
+ const ms = sample.ms * (sample.effort !== void 0 ? scales.get(sample.effort) ?? 1 : 1);
10152
+ return {
10153
+ ms,
10154
+ y: Math.log(Math.max(1, ms)),
10155
+ weight: base[index] * effortWeight(input.effort, sample.effort),
10156
+ calls: sample.calls,
10157
+ out: sample.out,
10158
+ sameEffort: input.effort === void 0 || sample.effort === void 0 || input.effort === sample.effort,
10159
+ session: sample.session
10160
+ };
10161
+ });
10162
+ }
10163
+ function moments(set) {
10164
+ let sumW = 0;
10165
+ let sumW2 = 0;
10166
+ let sumWY = 0;
10167
+ for (const sample of set) {
10168
+ sumW += sample.weight;
10169
+ sumW2 += sample.weight * sample.weight;
10170
+ sumWY += sample.weight * sample.y;
10171
+ }
10172
+ if (!(sumW > 0)) return void 0;
10173
+ const mean = sumWY / sumW;
10174
+ let sumWD = 0;
10175
+ for (const sample of set) sumWD += sample.weight * (sample.y - mean) * (sample.y - mean);
10176
+ const n = sumW * sumW / sumW2;
10177
+ const variance = n > 1 ? sumWD / sumW * (n / (n - 1)) : 0;
10178
+ return { mean, variance, n };
10179
+ }
10180
+ function shrink(own, prior, kappa = SHRINKAGE_KAPPA, meanKappa = kappa) {
10181
+ if (!own) return prior;
10182
+ const priorMu = prior?.mu ?? own.mean;
10183
+ const priorVariance = prior ? prior.sigma * prior.sigma : DEFAULT_LOG_SIGMA * DEFAULT_LOG_SIGMA;
10184
+ const mu = (own.n * own.mean + meanKappa * priorMu) / (own.n + meanKappa);
10185
+ const variance = (own.n * own.variance + kappa * priorVariance) / (own.n + kappa);
10186
+ return { mu, sigma: Math.min(MAX_LOG_SIGMA, Math.max(MIN_LOG_SIGMA, Math.sqrt(Math.max(0, variance)))) };
10187
+ }
10188
+ function agentPool(history, key, effort, now) {
10189
+ const slash = key.indexOf("/");
10190
+ if (slash < 0) return { count: 0, n: 0 };
10191
+ const prefix = key.slice(0, slash + 1);
10192
+ const pool = [];
10193
+ for (const [other, samples] of history) {
10194
+ if (other === key || !other.startsWith(prefix)) continue;
10195
+ pool.push(...prepare(samples, { effort }, now));
10196
+ }
10197
+ const measured = moments(pool);
10198
+ return { fit: shrink(measured, void 0), count: pool.length, n: measured?.n ?? 0 };
10199
+ }
10200
+ function fitKey(own, pool, input) {
10201
+ if (own.length + pool.count < MIN_TURN_SAMPLES) return void 0;
10202
+ const poolPull = pool.fit ? SHRINKAGE_KAPPA * pool.n / (pool.n + SHRINKAGE_KAPPA) : SHRINKAGE_KAPPA;
10203
+ const key = shrink(moments(own), pool.fit, SHRINKAGE_KAPPA, poolPull);
10204
+ if (!key) return void 0;
10205
+ const mine = input.session === void 0 ? [] : own.filter((sample) => sample.session === input.session);
10206
+ const usual = mine.length > 0 ? shrink(moments(mine), key) ?? key : key;
10207
+ const calls = input.calls !== void 0 && input.calls > 0 ? input.calls : void 0;
10208
+ const out = input.out !== void 0 && input.out > 0 ? input.out : void 0;
10209
+ let fit = usual;
10210
+ if (calls !== void 0 || out !== void 0) {
10211
+ const kept = own.filter(
10212
+ (sample) => (calls === void 0 || sample.calls === void 0 || sample.calls >= calls) && (out === void 0 || sample.out === void 0 || !sample.sameEffort || sample.out >= out)
10213
+ );
10214
+ if (kept.length < own.length && kept.length >= MIN_CONDITIONED_SAMPLES) {
10215
+ fit = shrink(moments(kept), usual, CONDITIONED_KAPPA) ?? usual;
10216
+ }
10217
+ }
10218
+ const maxMs = own.length > 0 ? own.reduce((longest, sample) => Math.max(longest, sample.ms), 0) : Number.POSITIVE_INFINITY;
10219
+ return { fit, usual, maxMs };
10220
+ }
10221
+ var NORMAL_CALIBRATION = { count: 0, weight: 0, cdf: normalCdf, quantile: normalQuantile };
10222
+ function weightedQuantile(sorted, total, q) {
10223
+ let cumulative = 0;
10224
+ let previous;
10225
+ for (const entry of sorted) {
10226
+ const at = (cumulative + entry.weight / 2) / total;
10227
+ cumulative += entry.weight;
10228
+ if (at >= q) {
10229
+ if (!previous) return entry.value;
10230
+ return previous.value + (q - previous.at) / (at - previous.at) * (entry.value - previous.value);
10231
+ }
10232
+ previous = { value: entry.value, at };
10233
+ }
10234
+ return sorted[sorted.length - 1].value;
10235
+ }
10236
+ function calibrationFrom(residuals, weights) {
10237
+ const entries = residuals.map((value, index) => ({ value, weight: weights?.[index] ?? 1 })).filter((entry) => Number.isFinite(entry.value) && Number.isFinite(entry.weight) && entry.weight > 0).slice(-MAX_CALIBRATION_RESIDUALS);
10238
+ if (entries.length === 0) return NORMAL_CALIBRATION;
10239
+ let total = 0;
10240
+ let squares = 0;
10241
+ let sum = 0;
10242
+ for (const entry of entries) {
10243
+ total += entry.weight;
10244
+ squares += entry.weight * entry.weight;
10245
+ sum += entry.weight * entry.value;
10246
+ }
10247
+ const n = total * total / squares;
10248
+ const mean = sum / total;
10249
+ let deviation = 0;
10250
+ for (const entry of entries) deviation += entry.weight * (entry.value - mean) * (entry.value - mean);
10251
+ const sd = n > 1 ? Math.sqrt(deviation / total * (n / (n - 1))) : 0;
10252
+ const sorted = [...entries].sort((a, b) => a.value - b.value);
10253
+ const iqr = weightedQuantile(sorted, total, 0.75) - weightedQuantile(sorted, total, 0.25);
10254
+ const spreads = [sd, iqr / 1.349].filter((spread2) => spread2 > 0);
10255
+ const spread = spreads.length > 0 ? Math.min(...spreads) : 1;
10256
+ const width = Math.max(0.1, 0.9 * spread * Math.pow(n, -0.2));
10257
+ const weight = n / (n + CALIBRATION_PRIOR_RESIDUALS);
10258
+ const size = Math.round((GRID_HIGH - GRID_LOW) / GRID_STEP) + 1;
10259
+ const table = new Float64Array(size);
10260
+ for (let index = 0; index < size; index += 1) {
10261
+ const z = GRID_LOW + index * GRID_STEP;
10262
+ let kernel = 0;
10263
+ for (const entry of entries) kernel += entry.weight * normalCdf((z - entry.value) / width);
10264
+ table[index] = weight * (kernel / total) + (1 - weight) * normalCdf(z);
10265
+ }
10266
+ return {
10267
+ count: entries.length,
10268
+ weight,
10269
+ cdf(z) {
10270
+ if (Number.isNaN(z)) return 0.5;
10271
+ const position = (Math.min(GRID_HIGH, Math.max(GRID_LOW, z)) - GRID_LOW) / GRID_STEP;
10272
+ const index = Math.min(size - 2, Math.floor(position));
10273
+ return table[index] + (position - index) * (table[index + 1] - table[index]);
10274
+ },
10275
+ quantile(p) {
10276
+ if (!(p > table[0])) return GRID_LOW;
10277
+ if (!(p < table[size - 1])) return GRID_HIGH;
10278
+ let low = 0;
10279
+ let high = size - 1;
10280
+ while (high - low > 1) {
10281
+ const mid = low + high >> 1;
10282
+ if (table[mid] < p) low = mid;
10283
+ else high = mid;
10284
+ }
10285
+ const span = table[high] - table[low];
10286
+ return GRID_LOW + (low + (span > 0 ? (p - table[low]) / span : 1)) * GRID_STEP;
10287
+ }
10288
+ };
10289
+ }
10290
+ function quantileMs(fit, calibration, truncatedAtMs, q) {
10291
+ const passed = truncatedAtMs > 0 ? calibration.cdf((Math.log(truncatedAtMs) - fit.mu) / fit.sigma) : 0;
10292
+ return Math.exp(fit.mu + fit.sigma * calibration.quantile(passed + q * (1 - passed)));
10293
+ }
10294
+ var EMPTY_HISTORY = /* @__PURE__ */ new Map();
10295
+ function buildTurnModel(history = EMPTY_HISTORY, now = Date.now()) {
10296
+ const pools = /* @__PURE__ */ new Map();
10297
+ const poolFor = (key, effort) => {
10298
+ const id = `${key}\0${effort ?? ""}`;
10299
+ let pool = pools.get(id);
10300
+ if (!pool) {
10301
+ pool = agentPool(history, key, effort, now);
10302
+ pools.set(id, pool);
10303
+ }
10304
+ return pool;
10305
+ };
10306
+ const assessed = /* @__PURE__ */ new Map();
10307
+ const residuals = [];
10308
+ const residualWeights = [];
10309
+ for (const [key, samples] of history) {
10310
+ const usable = usableSamples(samples);
10311
+ if (usable.length < 2) continue;
10312
+ const rows = [];
10313
+ for (const held of usable.slice(-MAX_ASSESSED_PER_KEY)) {
10314
+ const input = { chars: held.chars, effort: held.effort, session: held.session };
10315
+ const own = prepare(
10316
+ usable.filter((sample) => sample !== held),
10317
+ input,
10318
+ now
10319
+ );
10320
+ const fitted = fitKey(own, poolFor(key, held.effort), input);
10321
+ if (!fitted) continue;
10322
+ rows.push({ ms: held.ms, fit: fitted.fit });
10323
+ residuals.push((Math.log(Math.max(1, held.ms)) - fitted.fit.mu) / fitted.fit.sigma);
10324
+ residualWeights.push(recencyWeight(now, held.at));
10325
+ }
10326
+ if (rows.length > 0) assessed.set(key, rows);
10327
+ }
10328
+ const calibration = calibrationFrom(residuals, residualWeights);
10329
+ const estimate = (key, input) => {
10330
+ const at = input.now ?? now;
10331
+ const own = prepare(history.get(key) ?? [], input, at);
10332
+ const fitted = fitKey(own, poolFor(key, input.effort), input);
10333
+ if (!fitted) return void 0;
10334
+ const elapsed = Math.max(0, Number.isFinite(input.elapsedMs) ? input.elapsedMs : 0);
10335
+ if (elapsed > fitted.maxMs) {
10336
+ return { totalMs: quantileMs(fitted.usual, calibration, 0, 0.5), fill: 1, over: true };
10337
+ }
10338
+ const totalMs = Math.max(elapsed, quantileMs(fitted.fit, calibration, elapsed, 0.5));
10339
+ const lowMs = Math.min(totalMs, Math.max(elapsed, quantileMs(fitted.fit, calibration, elapsed, 0.25)));
10340
+ const highMs = Math.max(totalMs, quantileMs(fitted.fit, calibration, elapsed, 0.75));
10341
+ return {
10342
+ totalMs,
10343
+ fill: totalMs > 0 ? elapsed / totalMs : 0,
10344
+ over: false,
10345
+ band: {
10346
+ lowMs,
10347
+ highMs,
10348
+ sure: highMs > 0 ? elapsed / highMs : 0,
10349
+ maybe: lowMs > 0 ? Math.min(1, elapsed / lowMs) : 0
10350
+ }
10351
+ };
10352
+ };
10353
+ const assess = () => {
10354
+ const keys = [];
10355
+ for (const [key, samples] of history) {
10356
+ const usable = usableSamples(samples);
10357
+ const failed = samples.filter((sample) => sample.failed).length;
10358
+ if (usable.length === 0 && failed === 0) continue;
10359
+ const durations = usable.map((sample) => sample.ms).sort((a, b) => a - b);
10360
+ const typicalMs = durations.length > 0 ? durations[Math.floor((durations.length - 1) / 2)] : 0;
10361
+ const rows = assessed.get(key) ?? [];
10362
+ const misses = [];
10363
+ let over = 0;
10364
+ let inBand = 0;
10365
+ for (const row of rows) {
10366
+ const expected = Math.exp(row.fit.mu + row.fit.sigma * calibration.quantile(0.5));
10367
+ const low = Math.exp(row.fit.mu + row.fit.sigma * calibration.quantile(0.25));
10368
+ const high = Math.exp(row.fit.mu + row.fit.sigma * calibration.quantile(0.75));
10369
+ misses.push(Math.abs(Math.log(Math.max(1, row.ms) / expected)));
10370
+ if (row.ms > expected) over += 1;
10371
+ if (row.ms >= low && row.ms <= high) inBand += 1;
10372
+ }
10373
+ misses.sort((a, b) => a - b);
10374
+ keys.push({
10375
+ key,
10376
+ turns: usable.length,
10377
+ failed,
10378
+ typicalMs,
10379
+ assessed: rows.length,
10380
+ ...rows.length > 0 ? {
10381
+ missFactor: Math.exp(misses[Math.floor((misses.length - 1) / 2)]),
10382
+ overShare: over / rows.length,
10383
+ inBandShare: inBand / rows.length
10384
+ } : {}
10385
+ });
10386
+ }
10387
+ keys.sort((a, b) => b.turns - a.turns || a.key.localeCompare(b.key));
10388
+ return { keys, residuals: residuals.length, calibrationWeight: calibration.weight };
10389
+ };
10390
+ return { estimate, calibration, assess };
10391
+ }
10392
+
9638
10393
  // src/stale-build.ts
9639
10394
  import * as fs7 from "fs";
9640
10395
  import * as path10 from "path";
@@ -10196,6 +10951,12 @@ function formatDoctorReport(report) {
10196
10951
  lines.push("");
10197
10952
  lines.push(...formatAccountUsage(report.accountUsage));
10198
10953
  }
10954
+ if (report.turnEstimate) {
10955
+ lines.push("");
10956
+ lines.push("**Progress estimate**");
10957
+ lines.push("");
10958
+ lines.push(...formatTurnEstimate(report.turnEstimate));
10959
+ }
10199
10960
  lines.push("");
10200
10961
  lines.push("**Background subagents**");
10201
10962
  lines.push("");
@@ -10312,6 +11073,37 @@ function formatAccountUsage(usage, now = Date.now()) {
10312
11073
  ...rows
10313
11074
  ];
10314
11075
  }
11076
+ function formatTurnEstimate(assessment) {
11077
+ if (assessment.keys.length === 0) {
11078
+ return [
11079
+ `No turns recorded yet. The server half records one per served turn in \`turn-history.json\`; the sidebar draws a bar once an agent has ${MIN_TURN_SAMPLES} under one model, or that many across its other models.`
11080
+ ];
11081
+ }
11082
+ const percent = (share) => share === void 0 ? "n/a" : `${Math.round(share * 100)}%`;
11083
+ const lines = [
11084
+ assessment.residuals === 0 ? "No leave-one-out error to calibrate on yet, so the band is the normal curve's." : `The band is calibrated on ${assessment.residuals} leave-one-out error${assessment.residuals === 1 ? "" : "s"}, which weigh ${percent(assessment.calibrationWeight)} against the normal curve: the curve counts as ${CALIBRATION_PRIOR_RESIDUALS}, so the measured spread takes over as they accrue.`,
11085
+ "",
11086
+ "| agent/model | turns | failed | typical | assessed | off by | outran | in band |",
11087
+ "|---|---|---|---|---|---|---|---|"
11088
+ ];
11089
+ for (const key of assessment.keys) {
11090
+ lines.push(
11091
+ `| ${tableCell(key.key)} | ${key.turns} | ${key.failed} | ${key.turns > 0 ? formatAge(key.typicalMs) : "n/a"} | ${key.assessed} | ${key.missFactor === void 0 ? "n/a" : `\xD7${key.missFactor.toFixed(1)}`} | ${percent(key.overShare)} | ${percent(key.inBandShare)} |`
11092
+ );
11093
+ }
11094
+ lines.push("");
11095
+ lines.push(
11096
+ "Each turn is estimated at its start from the rest of its record. `off by` is the typical factor between that estimate and what the turn took, either way (\xD71.5 is 50% off); `outran` is the share that took longer than estimated, and half is unbiased; `in band` is the share that ended inside the band the bar draws, and half is calibrated. A turn that ended on a failed tool is counted under `failed` and never read."
11097
+ );
11098
+ return lines;
11099
+ }
11100
+ function assessTurnEstimate() {
11101
+ try {
11102
+ return buildTurnModel(createTurnHistoryReader().read()).assess();
11103
+ } catch {
11104
+ return void 0;
11105
+ }
11106
+ }
10315
11107
  function formatBackgroundSubagents(state) {
10316
11108
  const lines = [];
10317
11109
  if (state.running) {
@@ -10497,6 +11289,7 @@ async function gatherDoctorReport(options) {
10497
11289
  hookFailures: snapshotHookFailures(),
10498
11290
  planUsage,
10499
11291
  accountUsage: snapshotAccountUsage(),
11292
+ turnEstimate: assessTurnEstimate(),
10500
11293
  backgroundSubagents: {
10501
11294
  gate: snapshotBackgroundSubagentGate(),
10502
11295
  ledgers: snapshotBackgroundTasks(),
@@ -11629,9 +12422,9 @@ async function resolveSkillPluginDirs(opts) {
11629
12422
  // src/claude-session-wrapper.ts
11630
12423
  import { EventEmitter as EventEmitter4 } from "events";
11631
12424
  import { randomUUID as randomUUID5 } from "crypto";
11632
- import { existsSync as existsSync4, writeFileSync as writeFileSync7 } from "fs";
12425
+ import { existsSync as existsSync4, writeFileSync as writeFileSync9 } from "fs";
11633
12426
  import { unlink as unlink2 } from "fs/promises";
11634
- import { join as join14 } from "path";
12427
+ import { join as join16 } from "path";
11635
12428
 
11636
12429
  // src/claude-session-bun.ts
11637
12430
  import * as os6 from "os";
@@ -12813,8 +13606,8 @@ ${text2}`
12813
13606
  }
12814
13607
  function stageImage(data, extension) {
12815
13608
  try {
12816
- const file = join14(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
12817
- writeFileSync7(file, data, { mode: 384, flag: "wx" });
13609
+ const file = join16(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
13610
+ writeFileSync9(file, data, { mode: 384, flag: "wx" });
12818
13611
  return file;
12819
13612
  } catch (err) {
12820
13613
  log.warn("interactive transport could not stage an image", {
@@ -13295,17 +14088,17 @@ function spawnInteractiveProcess(opts) {
13295
14088
  }
13296
14089
 
13297
14090
  // src/spawn-record-store.ts
13298
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync5, statSync as statSync8, writeFileSync as writeFileSync8 } from "fs";
13299
- import { homedir as homedir12 } from "os";
13300
- import { join as join15 } from "path";
14091
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync11, renameSync as renameSync7, statSync as statSync10, writeFileSync as writeFileSync10 } from "fs";
14092
+ import { homedir as homedir14 } from "os";
14093
+ import { join as join17 } from "path";
13301
14094
  var MAX_SPAWN_RECORDS = 256;
13302
14095
  var SPAWN_RECORD_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
13303
- var pathOverride4 = null;
14096
+ var pathOverride6 = null;
13304
14097
  var lastWritten = /* @__PURE__ */ new Map();
13305
14098
  function spawnRecordStorePath() {
13306
- if (pathOverride4) return pathOverride4;
13307
- const stateRoot = process.env.XDG_STATE_HOME || join15(homedir12(), ".local", "state");
13308
- return join15(stateRoot, "opencode-claude-code-plugin", "session-spawns.json");
14099
+ if (pathOverride6) return pathOverride6;
14100
+ const stateRoot = process.env.XDG_STATE_HOME || join17(homedir14(), ".local", "state");
14101
+ return join17(stateRoot, "opencode-claude-code-plugin", "session-spawns.json");
13309
14102
  }
13310
14103
  function isRecord5(value) {
13311
14104
  const record = value;
@@ -13315,10 +14108,10 @@ function isRecord5(value) {
13315
14108
  if (!ok(record.model) || !ok(record.effort) || !ok(record.account) || !ok(record.agent)) return false;
13316
14109
  return !!record.model;
13317
14110
  }
13318
- function readEntries(file) {
14111
+ function readEntries3(file) {
13319
14112
  const out = /* @__PURE__ */ new Map();
13320
14113
  try {
13321
- const parsed = JSON.parse(readFileSync9(file, "utf8"));
14114
+ const parsed = JSON.parse(readFileSync11(file, "utf8"));
13322
14115
  const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
13323
14116
  if (!entries || typeof entries !== "object") return out;
13324
14117
  const cutoff = Date.now() - SPAWN_RECORD_MAX_AGE_MS;
@@ -13351,18 +14144,18 @@ ${record.account ?? ""}
13351
14144
  ${record.agent ?? ""}`;
13352
14145
  if (lastWritten.get(sessionID) === fingerprint) return false;
13353
14146
  const file = spawnRecordStorePath();
13354
- const merged = readEntries(file);
14147
+ const merged = readEntries3(file);
13355
14148
  merged.delete(sessionID);
13356
14149
  merged.set(sessionID, record);
13357
14150
  const cutoff = now - SPAWN_RECORD_MAX_AGE_MS;
13358
14151
  const kept = [...merged.entries()].filter(([, entry]) => entry.at >= cutoff).sort((a, b) => a[1].at - b[1].at).slice(-MAX_SPAWN_RECORDS);
13359
14152
  try {
13360
- mkdirSync7(join15(file, ".."), { recursive: true, mode: 448 });
14153
+ mkdirSync9(join17(file, ".."), { recursive: true, mode: 448 });
13361
14154
  const tmp = `${file}.${process.pid}.tmp`;
13362
- writeFileSync8(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
14155
+ writeFileSync10(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
13363
14156
  mode: 384
13364
14157
  });
13365
- renameSync5(tmp, file);
14158
+ renameSync7(tmp, file);
13366
14159
  } catch {
13367
14160
  return false;
13368
14161
  }
@@ -13371,157 +14164,6 @@ ${record.agent ?? ""}`;
13371
14164
  return true;
13372
14165
  }
13373
14166
 
13374
- // src/turn-history.ts
13375
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync as renameSync6, statSync as statSync9, writeFileSync as writeFileSync9 } from "fs";
13376
- import { homedir as homedir13 } from "os";
13377
- import { join as join16 } from "path";
13378
- var MAX_TURN_SAMPLES = 48;
13379
- var MAX_TURN_KEYS = 64;
13380
- var TURN_SAMPLE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
13381
- var CHARS_BANDWIDTH = Math.log(4);
13382
- var pathOverride5 = null;
13383
- function turnHistoryStorePath() {
13384
- if (pathOverride5) return pathOverride5;
13385
- const stateRoot = process.env.XDG_STATE_HOME || join16(homedir13(), ".local", "state");
13386
- return join16(stateRoot, "opencode-claude-code-plugin", "turn-history.json");
13387
- }
13388
- function turnHistoryKey(agent, model) {
13389
- return `${agent || "main"}/${model}`;
13390
- }
13391
- var INJECTED_BLOCKS = [
13392
- /<system-reminder>[\s\S]*?<\/system-reminder>/g,
13393
- /<dcp-system-reminder>[\s\S]*?<\/dcp-system-reminder>/g,
13394
- /<dcp-message-id>[\s\S]*?<\/dcp-message-id>/g
13395
- ];
13396
- function promptChars(texts) {
13397
- if (texts.length === 0) return void 0;
13398
- let total = 0;
13399
- for (const text of texts) {
13400
- let stripped = text;
13401
- for (const block of INJECTED_BLOCKS) stripped = stripped.replace(block, "");
13402
- total += stripped.trim().length;
13403
- }
13404
- return total;
13405
- }
13406
- function textsOf(content) {
13407
- if (typeof content === "string") return [content];
13408
- if (!Array.isArray(content)) return [];
13409
- const texts = [];
13410
- for (const part of content) {
13411
- if (!part || typeof part !== "object" || part.type !== "text") continue;
13412
- const text = part.text;
13413
- if (typeof text === "string") texts.push(text);
13414
- }
13415
- return texts;
13416
- }
13417
- function newestPromptChars(prompt) {
13418
- for (let index = prompt.length - 1; index >= 0; index -= 1) {
13419
- if (prompt[index].role === "user") return promptChars(textsOf(prompt[index].content));
13420
- }
13421
- return void 0;
13422
- }
13423
- function startsTurn(prompt) {
13424
- const last = prompt[prompt.length - 1];
13425
- if (!last || last.role !== "user") return false;
13426
- const before = prompt[prompt.length - 2];
13427
- return before === void 0 || before.role !== "tool";
13428
- }
13429
- function turnToolCalls(prompt) {
13430
- let start = 0;
13431
- for (let index = prompt.length - 1; index >= 0; index -= 1) {
13432
- if (prompt[index].role !== "user") continue;
13433
- const before = prompt[index - 1];
13434
- if (before === void 0 || before.role !== "tool") {
13435
- start = index;
13436
- break;
13437
- }
13438
- }
13439
- let calls = 0;
13440
- for (let index = start; index < prompt.length; index += 1) {
13441
- const message = prompt[index];
13442
- if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
13443
- for (const part of message.content) {
13444
- if (part && typeof part === "object" && part.type === "tool-call") calls += 1;
13445
- }
13446
- }
13447
- return calls;
13448
- }
13449
- var MAX_TURN_STARTS = 256;
13450
- var turnStarts = /* @__PURE__ */ new Map();
13451
- function markTurnStart(key, at, chars) {
13452
- turnStarts.delete(key);
13453
- if (turnStarts.size >= MAX_TURN_STARTS) {
13454
- const oldest = turnStarts.keys().next().value;
13455
- if (oldest !== void 0) turnStarts.delete(oldest);
13456
- }
13457
- turnStarts.set(key, { at, ...chars !== void 0 ? { chars } : {} });
13458
- }
13459
- function takeTurnStart(key) {
13460
- const start = turnStarts.get(key);
13461
- turnStarts.delete(key);
13462
- return start;
13463
- }
13464
- function isSample(value) {
13465
- const sample = value;
13466
- if (!sample || typeof sample !== "object" || Array.isArray(sample)) return false;
13467
- if (typeof sample.ms !== "number" || !Number.isFinite(sample.ms) || sample.ms < 0) return false;
13468
- if (typeof sample.at !== "number" || !Number.isFinite(sample.at)) return false;
13469
- if (sample.chars !== void 0 && (typeof sample.chars !== "number" || !Number.isFinite(sample.chars) || sample.chars < 0)) {
13470
- return false;
13471
- }
13472
- if (sample.effort !== void 0 && typeof sample.effort !== "string") return false;
13473
- if (sample.calls !== void 0 && (typeof sample.calls !== "number" || !Number.isFinite(sample.calls) || sample.calls < 0)) {
13474
- return false;
13475
- }
13476
- return true;
13477
- }
13478
- function cleanSample(sample) {
13479
- return {
13480
- ms: Math.round(sample.ms),
13481
- ...sample.chars !== void 0 ? { chars: Math.round(sample.chars) } : {},
13482
- ...sample.effort ? { effort: sample.effort } : {},
13483
- ...sample.calls !== void 0 ? { calls: Math.round(sample.calls) } : {},
13484
- at: sample.at
13485
- };
13486
- }
13487
- function readEntries2(file, now) {
13488
- const out = /* @__PURE__ */ new Map();
13489
- try {
13490
- const parsed = JSON.parse(readFileSync10(file, "utf8"));
13491
- const entries = parsed && typeof parsed === "object" ? parsed.turns : null;
13492
- if (!entries || typeof entries !== "object") return out;
13493
- const cutoff = now - TURN_SAMPLE_MAX_AGE_MS;
13494
- for (const [key, value] of Object.entries(entries)) {
13495
- if (!key || !Array.isArray(value)) continue;
13496
- const samples = value.filter(isSample).filter((sample) => sample.at >= cutoff).sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES).map(cleanSample);
13497
- if (samples.length > 0) out.set(key, samples);
13498
- }
13499
- } catch {
13500
- }
13501
- return out;
13502
- }
13503
- function bounded2(history) {
13504
- const newest = (samples) => samples[samples.length - 1]?.at ?? 0;
13505
- return new Map([...history.entries()].sort((a, b) => newest(a[1]) - newest(b[1])).slice(-MAX_TURN_KEYS));
13506
- }
13507
- function recordTurnSample(key, sample) {
13508
- if (!key || !isSample(sample)) return false;
13509
- const file = turnHistoryStorePath();
13510
- const merged = readEntries2(file, sample.at);
13511
- const samples = [...merged.get(key) ?? [], cleanSample(sample)].sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES);
13512
- merged.set(key, samples);
13513
- const kept = bounded2(merged);
13514
- try {
13515
- mkdirSync8(join16(file, ".."), { recursive: true, mode: 448 });
13516
- const tmp = `${file}.${process.pid}.tmp`;
13517
- writeFileSync9(tmp, JSON.stringify({ version: 1, turns: Object.fromEntries(kept) }), { mode: 384 });
13518
- renameSync6(tmp, file);
13519
- } catch {
13520
- return false;
13521
- }
13522
- return true;
13523
- }
13524
-
13525
14167
  // src/account-transcript.ts
13526
14168
  import { randomUUID as randomUUID6 } from "crypto";
13527
14169
  import { constants as fsConstants } from "fs";
@@ -13583,13 +14225,13 @@ function dedupe2(dirs) {
13583
14225
  }
13584
14226
 
13585
14227
  // src/prompts.ts
13586
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
13587
- import { homedir as homedir14 } from "os";
14228
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
14229
+ import { homedir as homedir15 } from "os";
13588
14230
  import { randomUUID as randomUUID7 } from "crypto";
13589
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join17 } from "path";
14231
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join18 } from "path";
13590
14232
  function readPromptFileIfPresent(path15) {
13591
14233
  try {
13592
- const content = readFileSync11(path15, "utf8").trim();
14234
+ const content = readFileSync12(path15, "utf8").trim();
13593
14235
  return content || void 0;
13594
14236
  } catch {
13595
14237
  return void 0;
@@ -13598,7 +14240,7 @@ function readPromptFileIfPresent(path15) {
13598
14240
  function nearestWorkspaceAgentsPrompt(cwd) {
13599
14241
  let dir = cwd;
13600
14242
  while (true) {
13601
- const content = readPromptFileIfPresent(join17(dir, "AGENTS.md"));
14243
+ const content = readPromptFileIfPresent(join18(dir, "AGENTS.md"));
13602
14244
  if (content) return content;
13603
14245
  const parent = dirname6(dir);
13604
14246
  if (parent === dir) return void 0;
@@ -13709,7 +14351,7 @@ var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLA
13709
14351
  function userAuthoredInstructions(forwarded, opts = {}) {
13710
14352
  const readFile3 = opts.readFile ?? ((path15) => {
13711
14353
  try {
13712
- return readFileSync11(path15, "utf8");
14354
+ return readFileSync12(path15, "utf8");
13713
14355
  } catch {
13714
14356
  return null;
13715
14357
  }
@@ -13750,8 +14392,8 @@ ${options.compressionSummary.trim()}`
13750
14392
  for (const s of extraSystemContent) {
13751
14393
  if (s.trim()) parts.push(s.trim());
13752
14394
  }
13753
- const configRoot = process.env.XDG_CONFIG_HOME ?? join17(homedir14(), ".config");
13754
- const globalAgents = readPromptFileIfPresent(join17(configRoot, "opencode", "AGENTS.md"));
14395
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join18(homedir15(), ".config");
14396
+ const globalAgents = readPromptFileIfPresent(join18(configRoot, "opencode", "AGENTS.md"));
13755
14397
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
13756
14398
  const forwarded = extraSystemContent.join("\n\n");
13757
14399
  const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
@@ -13767,9 +14409,9 @@ ${options.compressionSummary.trim()}`
13767
14409
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
13768
14410
  const content = parts.join("\n\n");
13769
14411
  if (!content) return void 0;
13770
- const path15 = join17(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
14412
+ const path15 = join18(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
13771
14413
  try {
13772
- writeFileSync10(path15, content, { encoding: "utf8", mode: 384 });
14414
+ writeFileSync11(path15, content, { encoding: "utf8", mode: 384 });
13773
14415
  return path15;
13774
14416
  } catch (err) {
13775
14417
  log.warn("failed to write system prompt file", { error: String(err) });
@@ -14699,6 +15341,7 @@ function createTurnState(init) {
14699
15341
  toolCallsById: /* @__PURE__ */ new Map(),
14700
15342
  toolCallEmittedIds: /* @__PURE__ */ new Set(),
14701
15343
  toolCallAnsweredIds: /* @__PURE__ */ new Set(),
15344
+ lastCliToolResultError: void 0,
14702
15345
  resultMeta: {},
14703
15346
  lastCallUsage: void 0,
14704
15347
  resultFailure: void 0,
@@ -15768,6 +16411,10 @@ ${plan}
15768
16411
  if (state.gotPartialEvents && msg.type === "message_delta" && typeof msg.delta?.stop_reason === "string") {
15769
16412
  state.lastStopReason = msg.delta.stop_reason;
15770
16413
  }
16414
+ if (msg.type === "message_delta") {
16415
+ const output = msg.usage?.output_tokens;
16416
+ if (typeof output === "number") noteTurnOutput(state.sessionKey, output);
16417
+ }
15771
16418
  if (msg.type === "assistant" && msg.message && typeof msg.message.stop_reason === "string") {
15772
16419
  state.lastStopReason = msg.message.stop_reason;
15773
16420
  }
@@ -16047,6 +16694,7 @@ ${plan}
16047
16694
  const toolCall = state.toolCallsById.get(block2.tool_use_id) ?? adoptOpenCliToolCall(state, ctx, block2.tool_use_id);
16048
16695
  if (toolCall) {
16049
16696
  const isError = block2.is_error === true;
16697
+ state.lastCliToolResultError = isError;
16050
16698
  state.controller.enqueue({
16051
16699
  type: "tool-result",
16052
16700
  toolCallId: block2.tool_use_id,
@@ -16552,12 +17200,6 @@ var ClaudeCodeLanguageModel = class {
16552
17200
  this.config.provider
16553
17201
  );
16554
17202
  }
16555
- controlRequestBehaviorForTool(toolName) {
16556
- return controlRequestBehaviorForTool(this.config, toolName);
16557
- }
16558
- writeControlResponse(proc, requestId, response) {
16559
- writeControlResponse(proc, requestId, response);
16560
- }
16561
17203
  /**
16562
17204
  * Handle Claude stream-json control requests (`can_use_tool`, etc.) and
16563
17205
  * respond via stdin with a matching `control_response`.
@@ -16606,9 +17248,6 @@ var ClaudeCodeLanguageModel = class {
16606
17248
  thinkingDisplay: process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0 ? "summarized" : void 0
16607
17249
  };
16608
17250
  }
16609
- latestUserText(prompt) {
16610
- return latestUserText(prompt);
16611
- }
16612
17251
  synthesizeTitle(prompt) {
16613
17252
  return synthesizeTitle(prompt);
16614
17253
  }
@@ -17174,8 +17813,14 @@ var ClaudeCodeLanguageModel = class {
17174
17813
  account: (this.config.failoverAccounts?.length ?? 0) > 1 ? failover.target ?? sourceAccount : void 0,
17175
17814
  agent: this.getOpencodeAgent(options)
17176
17815
  });
17177
- if (startsTurn(options.prompt)) {
17178
- markTurnStart(sk, turnStartedAt, newestPromptChars(options.prompt));
17816
+ const parked = new Set(getPendingProxyCalls(sk).map((call) => call.toolCallId));
17817
+ if (startsTurn(options.prompt, parked)) {
17818
+ markTurnStart(sk, {
17819
+ at: turnStartedAt,
17820
+ chars: newestPromptChars(options.prompt),
17821
+ promptLength: options.prompt.length,
17822
+ session: sessionTag(affinity)
17823
+ });
17179
17824
  }
17180
17825
  }
17181
17826
  const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
@@ -17466,7 +18111,7 @@ var ClaudeCodeLanguageModel = class {
17466
18111
  let replayReason = "no-session-for-key";
17467
18112
  if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
17468
18113
  const configDir = resolveConfigDir(this.config.configDir);
17469
- const transcriptPath2 = (id, dir) => join18(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
18114
+ const transcriptPath2 = (id, dir) => join19(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
17470
18115
  const resumePoint = findResumePoint({
17471
18116
  sessionKey: sk,
17472
18117
  prompt: options.prompt,
@@ -18540,7 +19185,18 @@ var ClaudeCodeLanguageModel = class {
18540
19185
  // The turn's tool calls, counted as the TUI counts its tool
18541
19186
  // parts (h #g250): the earlier steps' from the prompt, this
18542
19187
  // step's own from what it emitted.
18543
- calls: turnToolCalls(options.prompt) + state.toolCallEmittedIds.size,
19188
+ calls: turnToolCalls(options.prompt, start.promptLength) + state.toolCallEmittedIds.size,
19189
+ // The output its API calls generated, summed as the live
19190
+ // channel wrote it, so a running turn is compared with the
19191
+ // record in the same unit (h #g251).
19192
+ out: start.out,
19193
+ // A turn whose newest tool result was an error ended on a
19194
+ // failure (h #g251): this step's own CLI-executed results
19195
+ // are newer than the prompt's, so they answer first.
19196
+ failed: state.lastCliToolResultError ?? lastToolResultFailed(options.prompt, start.promptLength ?? 0),
19197
+ // The conversation, as a hash of its id: the estimate's
19198
+ // session level (h #g251).
19199
+ session: sessionTag(affinity),
18544
19200
  at: endedAt
18545
19201
  });
18546
19202
  }
@@ -18766,38 +19422,40 @@ var ClaudeCodeLanguageModel = class {
18766
19422
  dropped: unattended.dropped
18767
19423
  });
18768
19424
  let partialText = false;
18769
- {
19425
+ const narration = [];
19426
+ for (const line of unattended.lines) {
19427
+ try {
19428
+ const outer = JSON.parse(line);
19429
+ const msg = outer.type === "stream_event" && outer.event ? outer.event : outer;
19430
+ let text = "";
19431
+ if (msg.type === "content_block_delta" && msg.delta?.type === "text_delta") {
19432
+ text = msg.delta.text ?? "";
19433
+ partialText = true;
19434
+ } else if (msg.type === "assistant") {
19435
+ if (!partialText) text = (msg.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
19436
+ partialText = false;
19437
+ } else if (msg.type === "result") {
19438
+ state.unattendedTurnEnded = true;
19439
+ for (const entry of state.activeProcess.pendingProxyCompletions?.values() ?? []) {
19440
+ if (isPendingProxyCallChannelClosed(entry.call)) entry.recoveryRequired = true;
19441
+ }
19442
+ if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
19443
+ if (msg.is_error && msg.result) text = msg.result;
19444
+ }
19445
+ if (text) narration.push(text);
19446
+ } catch {
19447
+ }
19448
+ }
19449
+ if (narration.length > 0 || unattended.dropped > 0) {
18770
19450
  const replayId = state.startTextBlock();
18771
19451
  controller.enqueue({
18772
19452
  type: "text-delta",
18773
19453
  id: replayId,
18774
19454
  delta: formatUnattendedReplayNote(unattended.dropped)
18775
19455
  });
18776
- for (const line of unattended.lines) {
18777
- try {
18778
- const outer = JSON.parse(line);
18779
- const msg = outer.type === "stream_event" && outer.event ? outer.event : outer;
18780
- let text = "";
18781
- if (msg.type === "content_block_delta" && msg.delta?.type === "text_delta") {
18782
- text = msg.delta.text ?? "";
18783
- partialText = true;
18784
- } else if (msg.type === "assistant") {
18785
- if (!partialText) text = (msg.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
18786
- partialText = false;
18787
- } else if (msg.type === "result") {
18788
- state.unattendedTurnEnded = true;
18789
- for (const entry of state.activeProcess.pendingProxyCompletions?.values() ?? []) {
18790
- if (isPendingProxyCallChannelClosed(entry.call)) entry.recoveryRequired = true;
18791
- }
18792
- if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
18793
- if (msg.is_error && msg.result) text = msg.result;
18794
- }
18795
- if (text) controller.enqueue({ type: "text-delta", id: state.currentTextId ?? state.startTextBlock(), delta: text });
18796
- } catch {
18797
- }
18798
- }
19456
+ for (const text of narration) controller.enqueue({ type: "text-delta", id: replayId, delta: text });
19457
+ state.endTextBlock();
18799
19458
  }
18800
- state.endTextBlock();
18801
19459
  clearFallbackTimer(state);
18802
19460
  state.hasReceivedContent = false;
18803
19461
  }
@@ -18995,7 +19653,7 @@ var ClaudeCodeLanguageModel = class {
18995
19653
  };
18996
19654
 
18997
19655
  // src/account-tier.ts
18998
- import { readFileSync as readFileSync12 } from "fs";
19656
+ import { readFileSync as readFileSync13 } from "fs";
18999
19657
  import path14 from "path";
19000
19658
  var ORGANIZATION_TYPE_LABELS = {
19001
19659
  claude_max: "Max",
@@ -19038,7 +19696,7 @@ function readTierFromConfigDir(configDir, allowLegacyFallback) {
19038
19696
  function readOauthAccount(file) {
19039
19697
  let raw;
19040
19698
  try {
19041
- raw = readFileSync12(file, "utf8");
19699
+ raw = readFileSync13(file, "utf8");
19042
19700
  } catch {
19043
19701
  return void 0;
19044
19702
  }
@@ -19075,14 +19733,14 @@ function registerSidebarAccounts(accounts) {
19075
19733
  // src/cleanup-stale.ts
19076
19734
  import {
19077
19735
  existsSync as existsSync5,
19078
- mkdirSync as mkdirSync9,
19079
- readFileSync as readFileSync13,
19736
+ mkdirSync as mkdirSync10,
19737
+ readFileSync as readFileSync14,
19080
19738
  realpathSync as realpathSync4,
19081
19739
  rmSync as rmSync3,
19082
- writeFileSync as writeFileSync11
19740
+ writeFileSync as writeFileSync12
19083
19741
  } from "fs";
19084
- import { homedir as homedir15 } from "os";
19085
- import { dirname as dirname7, join as join19, resolve as resolve6 } from "path";
19742
+ import { homedir as homedir16 } from "os";
19743
+ import { dirname as dirname7, join as join20, resolve as resolve6 } from "path";
19086
19744
  import { fileURLToPath as fileURLToPath4 } from "url";
19087
19745
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
19088
19746
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -19090,20 +19748,20 @@ var alreadyRan = false;
19090
19748
  function candidateCacheRoots() {
19091
19749
  const xdg = process.env.XDG_CACHE_HOME;
19092
19750
  return [
19093
- xdg ? join19(xdg, "opencode") : null,
19094
- join19(homedir15(), ".cache", "opencode"),
19095
- join19(homedir15(), "Library", "Caches", "opencode")
19751
+ xdg ? join20(xdg, "opencode") : null,
19752
+ join20(homedir16(), ".cache", "opencode"),
19753
+ join20(homedir16(), "Library", "Caches", "opencode")
19096
19754
  ].filter((p) => Boolean(p));
19097
19755
  }
19098
19756
  function userOpencodeJsonPath() {
19099
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join19(homedir15(), ".config");
19100
- return join19(xdgConfig, "opencode", "opencode.json");
19757
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join20(homedir16(), ".config");
19758
+ return join20(xdgConfig, "opencode", "opencode.json");
19101
19759
  }
19102
19760
  function userIntendsToUseUnscoped() {
19103
19761
  const cfg = userOpencodeJsonPath();
19104
19762
  if (!existsSync5(cfg)) return false;
19105
19763
  try {
19106
- const json = JSON.parse(readFileSync13(cfg, "utf8"));
19764
+ const json = JSON.parse(readFileSync14(cfg, "utf8"));
19107
19765
  const plugins = json.plugin;
19108
19766
  if (!Array.isArray(plugins)) return false;
19109
19767
  return plugins.some(
@@ -19124,19 +19782,19 @@ function ourLoadedDir() {
19124
19782
  function ourVersion(dir) {
19125
19783
  if (!dir) return null;
19126
19784
  try {
19127
- const pkg = JSON.parse(readFileSync13(join19(dir, "package.json"), "utf8"));
19785
+ const pkg = JSON.parse(readFileSync14(join20(dir, "package.json"), "utf8"));
19128
19786
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
19129
19787
  } catch {
19130
19788
  return null;
19131
19789
  }
19132
19790
  }
19133
19791
  function markerPath() {
19134
- const stateRoot = process.env.XDG_STATE_HOME ?? join19(homedir15(), ".local", "state");
19135
- return join19(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
19792
+ const stateRoot = process.env.XDG_STATE_HOME ?? join20(homedir16(), ".local", "state");
19793
+ return join20(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
19136
19794
  }
19137
19795
  function markerVersion() {
19138
19796
  try {
19139
- const marker = JSON.parse(readFileSync13(markerPath(), "utf8"));
19797
+ const marker = JSON.parse(readFileSync14(markerPath(), "utf8"));
19140
19798
  return typeof marker.version === "string" ? marker.version : null;
19141
19799
  } catch {
19142
19800
  return null;
@@ -19145,8 +19803,8 @@ function markerVersion() {
19145
19803
  function writeMarker(version) {
19146
19804
  const file = markerPath();
19147
19805
  try {
19148
- mkdirSync9(dirname7(file), { recursive: true, mode: 448 });
19149
- writeFileSync11(
19806
+ mkdirSync10(dirname7(file), { recursive: true, mode: 448 });
19807
+ writeFileSync12(
19150
19808
  file,
19151
19809
  JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
19152
19810
  "utf8"
@@ -19181,7 +19839,7 @@ function cleanupStaleUnscopedInstall() {
19181
19839
  }
19182
19840
  function cleanupOne(cacheRoot, ourDir) {
19183
19841
  if (!existsSync5(cacheRoot)) return;
19184
- const stalePath = join19(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
19842
+ const stalePath = join20(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
19185
19843
  if (!existsSync5(stalePath)) return;
19186
19844
  let realStalePath = stalePath;
19187
19845
  try {
@@ -19189,11 +19847,11 @@ function cleanupOne(cacheRoot, ourDir) {
19189
19847
  } catch {
19190
19848
  }
19191
19849
  if (ourDir && realStalePath === ourDir) return;
19192
- const pkgJsonPath = join19(stalePath, "package.json");
19850
+ const pkgJsonPath = join20(stalePath, "package.json");
19193
19851
  if (!existsSync5(pkgJsonPath)) return;
19194
19852
  let pkg = {};
19195
19853
  try {
19196
- pkg = JSON.parse(readFileSync13(pkgJsonPath, "utf8"));
19854
+ pkg = JSON.parse(readFileSync14(pkgJsonPath, "utf8"));
19197
19855
  } catch {
19198
19856
  return;
19199
19857
  }
@@ -19209,13 +19867,13 @@ function cleanupOne(cacheRoot, ourDir) {
19209
19867
  });
19210
19868
  return;
19211
19869
  }
19212
- const cachePkgJson = join19(cacheRoot, "package.json");
19870
+ const cachePkgJson = join20(cacheRoot, "package.json");
19213
19871
  if (!existsSync5(cachePkgJson)) return;
19214
19872
  try {
19215
- const cfg = JSON.parse(readFileSync13(cachePkgJson, "utf8"));
19873
+ const cfg = JSON.parse(readFileSync14(cachePkgJson, "utf8"));
19216
19874
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
19217
19875
  delete cfg.dependencies[STALE_PACKAGE_NAME];
19218
- writeFileSync11(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
19876
+ writeFileSync12(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
19219
19877
  log.info("cleanup-stale: pruned dep from cache package.json");
19220
19878
  }
19221
19879
  } catch (err) {
@@ -19225,11 +19883,42 @@ function cleanupOne(cacheRoot, ourDir) {
19225
19883
  }
19226
19884
  }
19227
19885
 
19228
- // src/tui-log-sink.ts
19229
- var LOG_SERVICE_NAME = "opencode-claude-code";
19886
+ // src/plugin-notice.ts
19887
+ var NOTICE_RPC_ID = "opencode-claude-code-plugin";
19888
+ var NOTICE_EVENT = "notice";
19889
+ var NOTICE_EVENT_TYPE = `rpc.${NOTICE_RPC_ID}.${NOTICE_EVENT}`;
19890
+ var NOTICE_RPC_DEFINITION = {
19891
+ id: NOTICE_RPC_ID,
19892
+ methods: {},
19893
+ events: {
19894
+ [NOTICE_EVENT]: {
19895
+ schema: {
19896
+ type: "object",
19897
+ properties: {
19898
+ id: { type: "string" },
19899
+ level: { type: "string", enum: ["warn", "error"] },
19900
+ message: { type: "string" }
19901
+ },
19902
+ required: ["id", "level", "message"],
19903
+ additionalProperties: false
19904
+ }
19905
+ }
19906
+ }
19907
+ };
19230
19908
  var TOAST_TITLE = "claude-code";
19231
19909
  var TOAST_DURATION_MS = 1e4;
19232
19910
  var MAX_TOAST_CHARS = 240;
19911
+ function toastMessage(message) {
19912
+ const flat = message.replace(/\s+/g, " ").trim();
19913
+ if (flat.length <= MAX_TOAST_CHARS) return flat;
19914
+ return `${flat.slice(0, MAX_TOAST_CHARS - 1)}\u2026`;
19915
+ }
19916
+ function pluginNotice(id, level, message) {
19917
+ return { id, level, message: toastMessage(message) };
19918
+ }
19919
+
19920
+ // src/tui-log-sink.ts
19921
+ var LOG_SERVICE_NAME = "opencode-claude-code";
19233
19922
  var APP_LOG_LEVEL = {
19234
19923
  debug: "debug",
19235
19924
  info: "info",
@@ -19237,11 +19926,6 @@ var APP_LOG_LEVEL = {
19237
19926
  warn: "warn",
19238
19927
  error: "error"
19239
19928
  };
19240
- function toastMessage(message) {
19241
- const flat = message.replace(/\s+/g, " ").trim();
19242
- if (flat.length <= MAX_TOAST_CHARS) return flat;
19243
- return `${flat.slice(0, MAX_TOAST_CHARS - 1)}\u2026`;
19244
- }
19245
19929
  function appLogBody(entry) {
19246
19930
  return {
19247
19931
  service: LOG_SERVICE_NAME,
@@ -19291,6 +19975,9 @@ function registerOpencodeLogSink(client) {
19291
19975
  return true;
19292
19976
  }
19293
19977
 
19978
+ // src/v2.ts
19979
+ import { randomUUID as randomUUID8 } from "crypto";
19980
+
19294
19981
  // src/v2-client.ts
19295
19982
  function formatAgentTypeList(agents) {
19296
19983
  const lines = agents.flatMap((agent) => {
@@ -19580,24 +20267,89 @@ function deletedSessionIdV2(event) {
19580
20267
  const id = event.data?.sessionID;
19581
20268
  return typeof id === "string" && id.length > 0 ? id : void 0;
19582
20269
  }
19583
- async function releaseDeletedSessions(subscribe, signal) {
20270
+ function stoppedSessionIdV2(event) {
20271
+ if (!event || event.type !== "session.execution.interrupted") return void 0;
20272
+ if (event.data?.reason !== "user") return void 0;
20273
+ const id = event.data?.sessionID;
20274
+ return typeof id === "string" && id.length > 0 ? id : void 0;
20275
+ }
20276
+ async function followSessionEvents(subscribe, signal) {
20277
+ const closeFeed = openSessionRunEventFeed();
19584
20278
  try {
19585
20279
  for await (const event of subscribe({ signal })) {
19586
20280
  if (signal.aborted) break;
19587
- const sessionID = deletedSessionIdV2(event);
19588
- if (!sessionID) continue;
19589
- const released = deleteActiveProcessesForSession(sessionID);
19590
- if (released.length > 0) {
19591
- log.info("released claude state for deleted session", { sessionID, released });
20281
+ noteSessionRunEvent(event);
20282
+ const deleted = deletedSessionIdV2(event);
20283
+ if (deleted) {
20284
+ const released = deleteActiveProcessesForSession(deleted);
20285
+ if (released.length > 0) {
20286
+ log.info("released claude state for deleted session", { sessionID: deleted, released });
20287
+ }
20288
+ continue;
19592
20289
  }
20290
+ const stopped = stoppedSessionIdV2(event);
20291
+ if (stopped) releaseParkedProxyCallsForSession(stopped);
19593
20292
  }
19594
20293
  } catch (err) {
19595
20294
  if (!signal.aborted) {
19596
- log.warn("opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction", {
19597
- error: err instanceof Error ? err.message : String(err)
19598
- });
20295
+ log.warn(
20296
+ "opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction, and stopped turns keep their proxy calls until the next message",
20297
+ { error: err instanceof Error ? err.message : String(err) }
20298
+ );
19599
20299
  }
20300
+ } finally {
20301
+ closeFeed();
20302
+ }
20303
+ }
20304
+ var noticeEmitters = /* @__PURE__ */ new Set();
20305
+ var NOTICE_PROCESS_TAG = randomUUID8().slice(0, 8);
20306
+ var noticeCounter = 0;
20307
+ function emitNotice(entry) {
20308
+ if (entry.level !== "warn" && entry.level !== "error") return;
20309
+ noticeCounter += 1;
20310
+ const notice = pluginNotice(`${NOTICE_PROCESS_TAG}-${noticeCounter}`, entry.level, entry.message);
20311
+ for (const emit2 of noticeEmitters) emit2(notice);
20312
+ }
20313
+ async function registerNoticeChannel(ctx) {
20314
+ const rpc = ctx.rpc;
20315
+ if (typeof rpc?.register !== "function") return void 0;
20316
+ let registration;
20317
+ try {
20318
+ registration = await rpc.register(NOTICE_RPC_DEFINITION, {});
20319
+ } catch (err) {
20320
+ log.debug("opencode 2 refused the notice channel; warnings stay in the plugin log", {
20321
+ error: err instanceof Error ? err.message : String(err)
20322
+ });
20323
+ return void 0;
19600
20324
  }
20325
+ const emitter2 = (notice) => {
20326
+ try {
20327
+ void Promise.resolve(registration.events.emit(NOTICE_EVENT, { ...notice })).catch(() => void 0);
20328
+ } catch {
20329
+ }
20330
+ };
20331
+ noticeEmitters.add(emitter2);
20332
+ setToastNotifier(emitNotice);
20333
+ return {
20334
+ dispose: async () => {
20335
+ noticeEmitters.delete(emitter2);
20336
+ if (noticeEmitters.size === 0) setToastNotifier(null);
20337
+ await registration.dispose();
20338
+ }
20339
+ };
20340
+ }
20341
+ function v2BtwClient(shim) {
20342
+ const shimSession = shim.session;
20343
+ const messages = shimSession?.messages;
20344
+ const status = async () => ({ data: busySessionsFromEvents() ?? {} });
20345
+ return {
20346
+ session: {
20347
+ ...typeof messages === "function" ? { messages: (options) => messages.call(shimSession, options) } : {},
20348
+ get status() {
20349
+ return busySessionsFromEvents() === void 0 ? void 0 : status;
20350
+ }
20351
+ }
20352
+ };
19601
20353
  }
19602
20354
  function createV2Setup(deps) {
19603
20355
  return async (ctx) => {
@@ -19608,9 +20360,13 @@ function createV2Setup(deps) {
19608
20360
  return () => void 0;
19609
20361
  }
19610
20362
  ensureProcessExitCleanup();
19611
- setOpencodeClient(createV1ClientShim(ctx));
20363
+ const shim = createV1ClientShim(ctx);
20364
+ setOpencodeClient(shim);
19612
20365
  const directory = ctx.location?.directory;
19613
20366
  setOpencodeProjectDirectory(isUsableDirectory(directory) ? directory : void 0);
20367
+ const registrations = [];
20368
+ const noticeChannel = await registerNoticeChannel(ctx);
20369
+ if (noticeChannel) registrations.push(noticeChannel);
19614
20370
  if (deps.buildAgentRegistry) {
19615
20371
  try {
19616
20372
  const config = deps.loadConfig(directory ?? process.cwd());
@@ -19630,7 +20386,6 @@ function createV2Setup(deps) {
19630
20386
  }
19631
20387
  }
19632
20388
  const planned = /* @__PURE__ */ new Map();
19633
- const registrations = [];
19634
20389
  registrations.push(
19635
20390
  await ctx.provider.transform((editor) => {
19636
20391
  const seed = editor.get(BASE_PROVIDER_ID);
@@ -19686,6 +20441,7 @@ function createV2Setup(deps) {
19686
20441
  })
19687
20442
  );
19688
20443
  const prompt = ctx.session.prompt;
20444
+ const btwClient = v2BtwClient(shim);
19689
20445
  if (ctx.command?.transform && typeof prompt === "function") {
19690
20446
  const send = (input, command, delivery) => prompt.call(ctx.session, {
19691
20447
  ...input.prompt,
@@ -19703,7 +20459,15 @@ function createV2Setup(deps) {
19703
20459
  editor.add({
19704
20460
  name: "btw",
19705
20461
  description: BTW_COMMAND_DESCRIPTION,
19706
- execute: (input) => send(input, "btw", "queue")
20462
+ execute: (input) => {
20463
+ void askBtwThenRelease(
20464
+ btwClient,
20465
+ { command: "btw", sessionID: input.sessionID, arguments: input.prompt.text ?? "" },
20466
+ () => send(input, "btw", "queue"),
20467
+ deps.btwWaitOptions
20468
+ );
20469
+ return Promise.resolve();
20470
+ }
19707
20471
  });
19708
20472
  })
19709
20473
  );
@@ -19724,7 +20488,7 @@ function createV2Setup(deps) {
19724
20488
  }
19725
20489
  const events = new AbortController();
19726
20490
  if (typeof ctx.event?.subscribe === "function") {
19727
- void releaseDeletedSessions(ctx.event.subscribe.bind(ctx.event), events.signal);
20491
+ void followSessionEvents(ctx.event.subscribe.bind(ctx.event), events.signal);
19728
20492
  }
19729
20493
  log.info("claude-code plugin set up for opencode 2", {
19730
20494
  opencode: ctx.app?.version,