@khalilgharbaoui/opencode-claude-code-plugin 0.57.1 → 0.58.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";
@@ -9635,6 +9635,625 @@ async function fetchPlanUsage(cliPath, options = {}) {
9635
9635
  }
9636
9636
  }
9637
9637
 
9638
+ // src/turn-history.ts
9639
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, renameSync as renameSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
9640
+ import { homedir as homedir11 } from "os";
9641
+ import { join as join11 } from "path";
9642
+
9643
+ // src/turn-progress-store.ts
9644
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
9645
+ import { homedir as homedir10 } from "os";
9646
+ import { join as join10 } from "path";
9647
+ var MAX_TURN_PROGRESS = 64;
9648
+ var TURN_PROGRESS_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
9649
+ var TAG = /^[0-9a-f]{8}$/;
9650
+ var pathOverride4 = null;
9651
+ function turnProgressStorePath() {
9652
+ if (pathOverride4) return pathOverride4;
9653
+ const stateRoot = process.env.XDG_STATE_HOME || join10(homedir10(), ".local", "state");
9654
+ return join10(stateRoot, "opencode-claude-code-plugin", "turn-progress.json");
9655
+ }
9656
+ function isProgress(value) {
9657
+ const record = value;
9658
+ if (!record || typeof record !== "object" || Array.isArray(record)) return false;
9659
+ const count = (field) => typeof field === "number" && Number.isFinite(field) && field >= 0;
9660
+ return count(record.turn) && count(record.out) && count(record.at);
9661
+ }
9662
+ function readEntries(file, now) {
9663
+ const out = /* @__PURE__ */ new Map();
9664
+ try {
9665
+ const parsed = JSON.parse(readFileSync6(file, "utf8"));
9666
+ const entries = parsed && typeof parsed === "object" ? parsed.turns : null;
9667
+ if (!entries || typeof entries !== "object") return out;
9668
+ const cutoff = now - TURN_PROGRESS_MAX_AGE_MS;
9669
+ 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);
9670
+ for (const [tag, record] of valid) out.set(tag, { turn: record.turn, out: Math.round(record.out), at: record.at });
9671
+ } catch {
9672
+ }
9673
+ return out;
9674
+ }
9675
+ function recordTurnProgress(tag, progress, now = Date.now()) {
9676
+ const record = { turn: progress.turn, out: progress.out, at: now };
9677
+ if (!TAG.test(tag) || !isProgress(record)) return false;
9678
+ const file = turnProgressStorePath();
9679
+ const merged = readEntries(file, now);
9680
+ merged.delete(tag);
9681
+ merged.set(tag, { ...record, out: Math.round(record.out) });
9682
+ const kept = [...merged.entries()].slice(-MAX_TURN_PROGRESS);
9683
+ try {
9684
+ mkdirSync6(join10(file, ".."), { recursive: true, mode: 448 });
9685
+ const tmp = `${file}.${process.pid}.tmp`;
9686
+ writeFileSync6(tmp, JSON.stringify({ version: 1, turns: Object.fromEntries(kept) }), { mode: 384 });
9687
+ renameSync5(tmp, file);
9688
+ } catch {
9689
+ return false;
9690
+ }
9691
+ return true;
9692
+ }
9693
+
9694
+ // src/turn-history.ts
9695
+ var MAX_TURN_SAMPLES = 48;
9696
+ var MAX_TURN_KEYS = 64;
9697
+ var TURN_SAMPLE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
9698
+ var SESSION_TAG = /^[0-9a-f]{8}$/;
9699
+ var pathOverride5 = null;
9700
+ function turnHistoryStorePath() {
9701
+ if (pathOverride5) return pathOverride5;
9702
+ const stateRoot = process.env.XDG_STATE_HOME || join11(homedir11(), ".local", "state");
9703
+ return join11(stateRoot, "opencode-claude-code-plugin", "turn-history.json");
9704
+ }
9705
+ function turnHistoryKey(agent, model) {
9706
+ return `${agent || "main"}/${model}`;
9707
+ }
9708
+ function sessionTag(sessionID) {
9709
+ if (!sessionID || sessionID === "default") return void 0;
9710
+ let hash = 2166136261;
9711
+ for (let index = 0; index < sessionID.length; index += 1) {
9712
+ hash ^= sessionID.charCodeAt(index);
9713
+ hash = Math.imul(hash, 16777619) >>> 0;
9714
+ }
9715
+ return hash.toString(16).padStart(8, "0");
9716
+ }
9717
+ var INJECTED_BLOCKS = [
9718
+ /<system-reminder>[\s\S]*?<\/system-reminder>/g,
9719
+ /<dcp-system-reminder>[\s\S]*?<\/dcp-system-reminder>/g,
9720
+ /<dcp-message-id>[\s\S]*?<\/dcp-message-id>/g
9721
+ ];
9722
+ function promptChars(texts) {
9723
+ if (texts.length === 0) return void 0;
9724
+ let total = 0;
9725
+ for (const text of texts) {
9726
+ let stripped = text;
9727
+ for (const block of INJECTED_BLOCKS) stripped = stripped.replace(block, "");
9728
+ total += stripped.trim().length;
9729
+ }
9730
+ return total;
9731
+ }
9732
+ function textsOf(content) {
9733
+ if (typeof content === "string") return [content];
9734
+ if (!Array.isArray(content)) return [];
9735
+ const texts = [];
9736
+ for (const part of content) {
9737
+ if (!part || typeof part !== "object" || part.type !== "text") continue;
9738
+ const text = part.text;
9739
+ if (typeof text === "string") texts.push(text);
9740
+ }
9741
+ return texts;
9742
+ }
9743
+ function newestPromptChars(prompt) {
9744
+ for (let index = prompt.length - 1; index >= 0; index -= 1) {
9745
+ if (prompt[index].role === "user") return promptChars(textsOf(prompt[index].content));
9746
+ }
9747
+ return void 0;
9748
+ }
9749
+ function toolResultParts(message) {
9750
+ if (!Array.isArray(message.content)) return [];
9751
+ return message.content.filter(
9752
+ (part) => !!part && typeof part === "object" && part.type === "tool-result"
9753
+ );
9754
+ }
9755
+ function startsTurn(prompt, pendingToolCallIds) {
9756
+ const last = prompt[prompt.length - 1];
9757
+ if (!last || last.role !== "user") return false;
9758
+ const before = prompt[prompt.length - 2];
9759
+ if (before === void 0 || before.role !== "tool") return true;
9760
+ if (!pendingToolCallIds) return false;
9761
+ return toolResultParts(before).every((part) => typeof part.toolCallId !== "string" || !pendingToolCallIds.has(part.toolCallId));
9762
+ }
9763
+ function turnToolCalls(prompt, from) {
9764
+ let start = 0;
9765
+ if (from !== void 0 && from >= 0 && from <= prompt.length) {
9766
+ start = from;
9767
+ } else {
9768
+ for (let index = prompt.length - 1; index >= 0; index -= 1) {
9769
+ if (prompt[index].role !== "user") continue;
9770
+ const before = prompt[index - 1];
9771
+ if (before === void 0 || before.role !== "tool") {
9772
+ start = index;
9773
+ break;
9774
+ }
9775
+ }
9776
+ }
9777
+ let calls = 0;
9778
+ for (let index = start; index < prompt.length; index += 1) {
9779
+ const message = prompt[index];
9780
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
9781
+ for (const part of message.content) {
9782
+ if (part && typeof part === "object" && part.type === "tool-call") calls += 1;
9783
+ }
9784
+ }
9785
+ return calls;
9786
+ }
9787
+ function lastToolResultFailed(prompt, from) {
9788
+ for (let index = prompt.length - 1; index >= Math.max(0, from); index -= 1) {
9789
+ const message = prompt[index];
9790
+ if (message.role === "user") continue;
9791
+ if (message.role !== "tool") return false;
9792
+ const parts = toolResultParts(message);
9793
+ const newest = parts[parts.length - 1];
9794
+ const output = newest?.output;
9795
+ const type = output && typeof output === "object" ? output.type : void 0;
9796
+ return type === "error-text" || type === "error-json" || type === "execution-denied";
9797
+ }
9798
+ return false;
9799
+ }
9800
+ var MAX_TURN_STARTS = 256;
9801
+ var turnStarts = /* @__PURE__ */ new Map();
9802
+ function markTurnStart(key, start) {
9803
+ turnStarts.delete(key);
9804
+ if (turnStarts.size >= MAX_TURN_STARTS) {
9805
+ const oldest = turnStarts.keys().next().value;
9806
+ if (oldest !== void 0) turnStarts.delete(oldest);
9807
+ }
9808
+ turnStarts.set(key, {
9809
+ at: start.at,
9810
+ ...start.chars !== void 0 ? { chars: start.chars } : {},
9811
+ ...start.promptLength !== void 0 ? { promptLength: start.promptLength } : {},
9812
+ ...start.session !== void 0 ? { session: start.session } : {}
9813
+ });
9814
+ }
9815
+ function noteTurnOutput(key, tokens, now = Date.now()) {
9816
+ const start = turnStarts.get(key);
9817
+ if (!start || !Number.isFinite(tokens) || tokens < 0) return;
9818
+ start.out = (start.out ?? 0) + tokens;
9819
+ if (start.session) recordTurnProgress(start.session, { turn: start.at, out: start.out }, now);
9820
+ }
9821
+ function takeTurnStart(key) {
9822
+ const start = turnStarts.get(key);
9823
+ turnStarts.delete(key);
9824
+ return start;
9825
+ }
9826
+ function isSample(value) {
9827
+ const sample = value;
9828
+ if (!sample || typeof sample !== "object" || Array.isArray(sample)) return false;
9829
+ if (typeof sample.ms !== "number" || !Number.isFinite(sample.ms) || sample.ms < 0) return false;
9830
+ if (typeof sample.at !== "number" || !Number.isFinite(sample.at)) return false;
9831
+ if (sample.chars !== void 0 && (typeof sample.chars !== "number" || !Number.isFinite(sample.chars) || sample.chars < 0)) {
9832
+ return false;
9833
+ }
9834
+ if (sample.effort !== void 0 && typeof sample.effort !== "string") return false;
9835
+ if (sample.calls !== void 0 && (typeof sample.calls !== "number" || !Number.isFinite(sample.calls) || sample.calls < 0)) {
9836
+ return false;
9837
+ }
9838
+ if (sample.out !== void 0 && (typeof sample.out !== "number" || !Number.isFinite(sample.out) || sample.out < 0)) {
9839
+ return false;
9840
+ }
9841
+ if (sample.failed !== void 0 && typeof sample.failed !== "boolean") return false;
9842
+ if (sample.session !== void 0 && (typeof sample.session !== "string" || !SESSION_TAG.test(sample.session))) {
9843
+ return false;
9844
+ }
9845
+ return true;
9846
+ }
9847
+ function cleanSample(sample) {
9848
+ return {
9849
+ ms: Math.round(sample.ms),
9850
+ ...sample.chars !== void 0 ? { chars: Math.round(sample.chars) } : {},
9851
+ ...sample.effort ? { effort: sample.effort } : {},
9852
+ ...sample.calls !== void 0 ? { calls: Math.round(sample.calls) } : {},
9853
+ ...sample.out !== void 0 ? { out: Math.round(sample.out) } : {},
9854
+ ...sample.failed ? { failed: true } : {},
9855
+ ...sample.session ? { session: sample.session } : {},
9856
+ at: sample.at
9857
+ };
9858
+ }
9859
+ function readEntries2(file, now) {
9860
+ const out = /* @__PURE__ */ new Map();
9861
+ try {
9862
+ const parsed = JSON.parse(readFileSync7(file, "utf8"));
9863
+ const entries = parsed && typeof parsed === "object" ? parsed.turns : null;
9864
+ if (!entries || typeof entries !== "object") return out;
9865
+ const cutoff = now - TURN_SAMPLE_MAX_AGE_MS;
9866
+ for (const [key, value] of Object.entries(entries)) {
9867
+ if (!key || !Array.isArray(value)) continue;
9868
+ const samples = value.filter(isSample).filter((sample) => sample.at >= cutoff).sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES).map(cleanSample);
9869
+ if (samples.length > 0) out.set(key, samples);
9870
+ }
9871
+ } catch {
9872
+ }
9873
+ return out;
9874
+ }
9875
+ function bounded2(history) {
9876
+ const newest = (samples) => samples[samples.length - 1]?.at ?? 0;
9877
+ return new Map([...history.entries()].sort((a, b) => newest(a[1]) - newest(b[1])).slice(-MAX_TURN_KEYS));
9878
+ }
9879
+ function recordTurnSample(key, sample) {
9880
+ if (!key || !isSample(sample)) return false;
9881
+ const file = turnHistoryStorePath();
9882
+ const merged = readEntries2(file, sample.at);
9883
+ const samples = [...merged.get(key) ?? [], cleanSample(sample)].sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES);
9884
+ merged.set(key, samples);
9885
+ const kept = bounded2(merged);
9886
+ try {
9887
+ mkdirSync7(join11(file, ".."), { recursive: true, mode: 448 });
9888
+ const tmp = `${file}.${process.pid}.tmp`;
9889
+ writeFileSync7(tmp, JSON.stringify({ version: 1, turns: Object.fromEntries(kept) }), { mode: 384 });
9890
+ renameSync6(tmp, file);
9891
+ } catch {
9892
+ return false;
9893
+ }
9894
+ return true;
9895
+ }
9896
+ function createTurnHistoryReader(file = turnHistoryStorePath) {
9897
+ let identity = "";
9898
+ let cached2 = /* @__PURE__ */ new Map();
9899
+ return {
9900
+ read(now = Date.now()) {
9901
+ const path15 = file();
9902
+ let next = "";
9903
+ try {
9904
+ const stats = statSync6(path15);
9905
+ next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
9906
+ } catch {
9907
+ next = "missing";
9908
+ }
9909
+ if (next !== identity) {
9910
+ identity = next;
9911
+ cached2 = next === "missing" ? /* @__PURE__ */ new Map() : readEntries2(path15, now);
9912
+ }
9913
+ return cached2;
9914
+ }
9915
+ };
9916
+ }
9917
+
9918
+ // src/turn-model.ts
9919
+ var MIN_TURN_SAMPLES = 3;
9920
+ var CHARS_BANDWIDTH = Math.log(4);
9921
+ var OTHER_EFFORT_WEIGHT = 0.5;
9922
+ var MIN_EFFORT_RATIO_SAMPLES = 2;
9923
+ var RECENCY_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1e3;
9924
+ var MIN_CONDITIONED_SAMPLES = 1;
9925
+ var SHRINKAGE_KAPPA = 2;
9926
+ var CONDITIONED_KAPPA = 1;
9927
+ var DEFAULT_LOG_SIGMA = 0.8;
9928
+ var MIN_LOG_SIGMA = 0.2;
9929
+ var MAX_LOG_SIGMA = 1.6;
9930
+ var CALIBRATION_PRIOR_RESIDUALS = 12;
9931
+ var MAX_ASSESSED_PER_KEY = 16;
9932
+ var MAX_CALIBRATION_RESIDUALS = 512;
9933
+ var GRID_LOW = -8;
9934
+ var GRID_HIGH = 8;
9935
+ var GRID_STEP = 0.04;
9936
+ var CHARS_FLOOR = 50;
9937
+ function normalCdf(z) {
9938
+ if (!Number.isFinite(z)) return z > 0 ? 1 : 0;
9939
+ const x = Math.abs(z);
9940
+ const t = 1 / (1 + 0.2316419 * x);
9941
+ const poly = t * (0.31938153 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
9942
+ const tail = Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI) * poly;
9943
+ return z >= 0 ? 1 - tail : tail;
9944
+ }
9945
+ function normalQuantile(p) {
9946
+ const q = Math.min(1 - 1e-12, Math.max(1e-12, p));
9947
+ const a = [-39.69683028665376, 220.9460984245205, -275.9285104469687, 138.357751867269, -30.66479806614716, 2.506628277459239];
9948
+ const b = [-54.47609879822406, 161.5858368580409, -155.6989798598866, 66.80131188771972, -13.28068155288572];
9949
+ const c = [-0.007784894002430293, -0.3223964580411365, -2.400758277161838, -2.549732539343734, 4.374664141464968, 2.938163982698783];
9950
+ const d = [0.007784695709041462, 0.3224671290700398, 2.445134137142996, 3.754408661907416];
9951
+ const low = 0.02425;
9952
+ if (q < low || q > 1 - low) {
9953
+ const r2 = Math.sqrt(-2 * Math.log(q < low ? q : 1 - q));
9954
+ 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);
9955
+ return q < low ? x : -x;
9956
+ }
9957
+ const r = q - 0.5;
9958
+ const s = r * r;
9959
+ 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);
9960
+ }
9961
+ function charsWeight(a, b) {
9962
+ if (a === void 0 || b === void 0) return 1;
9963
+ const distance = Math.log((a + CHARS_FLOOR) / (b + CHARS_FLOOR)) / CHARS_BANDWIDTH;
9964
+ return Math.exp(-0.5 * distance * distance);
9965
+ }
9966
+ function effortWeight(a, b) {
9967
+ return a === void 0 || b === void 0 || a === b ? 1 : OTHER_EFFORT_WEIGHT;
9968
+ }
9969
+ function recencyWeight(now, at) {
9970
+ const age = Math.max(0, now - at);
9971
+ return Math.pow(0.5, age / RECENCY_HALF_LIFE_MS);
9972
+ }
9973
+ function weightedMedian(values, weights) {
9974
+ const order = values.map((_, index) => index).sort((a, b) => values[a] - values[b]);
9975
+ const total = order.reduce((sum, index) => sum + weights[index], 0);
9976
+ let cumulative = 0;
9977
+ for (const index of order) {
9978
+ cumulative += weights[index];
9979
+ if (cumulative >= total / 2) return values[index];
9980
+ }
9981
+ return values[order[order.length - 1]];
9982
+ }
9983
+ function effortScales(samples, weights, effort) {
9984
+ const scales = /* @__PURE__ */ new Map();
9985
+ if (effort === void 0) return scales;
9986
+ const groups = /* @__PURE__ */ new Map();
9987
+ samples.forEach((sample, index) => {
9988
+ if (sample.effort === void 0) return;
9989
+ const group = groups.get(sample.effort) ?? [];
9990
+ group.push(index);
9991
+ groups.set(sample.effort, group);
9992
+ });
9993
+ const own = groups.get(effort);
9994
+ if (!own || own.length < MIN_EFFORT_RATIO_SAMPLES) return scales;
9995
+ const medianOf = (indices) => weightedMedian(
9996
+ indices.map((index) => samples[index].ms),
9997
+ indices.map((index) => weights[index])
9998
+ );
9999
+ const ownMedian = medianOf(own);
10000
+ for (const [other, indices] of groups) {
10001
+ if (other === effort || indices.length < MIN_EFFORT_RATIO_SAMPLES) continue;
10002
+ const otherMedian = medianOf(indices);
10003
+ if (ownMedian > 0 && otherMedian > 0) scales.set(other, ownMedian / otherMedian);
10004
+ }
10005
+ return scales;
10006
+ }
10007
+ function usableSamples(samples) {
10008
+ return samples.filter((sample) => Number.isFinite(sample.ms) && sample.ms >= 0 && !sample.failed);
10009
+ }
10010
+ function prepare(samples, input, now) {
10011
+ const usable = usableSamples(samples);
10012
+ const base = usable.map((sample) => charsWeight(input.chars, sample.chars) * recencyWeight(now, sample.at));
10013
+ const scales = effortScales(usable, base, input.effort);
10014
+ return usable.map((sample, index) => {
10015
+ const ms = sample.ms * (sample.effort !== void 0 ? scales.get(sample.effort) ?? 1 : 1);
10016
+ return {
10017
+ ms,
10018
+ y: Math.log(Math.max(1, ms)),
10019
+ weight: base[index] * effortWeight(input.effort, sample.effort),
10020
+ calls: sample.calls,
10021
+ out: sample.out,
10022
+ sameEffort: input.effort === void 0 || sample.effort === void 0 || input.effort === sample.effort,
10023
+ session: sample.session
10024
+ };
10025
+ });
10026
+ }
10027
+ function moments(set) {
10028
+ let sumW = 0;
10029
+ let sumW2 = 0;
10030
+ let sumWY = 0;
10031
+ for (const sample of set) {
10032
+ sumW += sample.weight;
10033
+ sumW2 += sample.weight * sample.weight;
10034
+ sumWY += sample.weight * sample.y;
10035
+ }
10036
+ if (!(sumW > 0)) return void 0;
10037
+ const mean = sumWY / sumW;
10038
+ let sumWD = 0;
10039
+ for (const sample of set) sumWD += sample.weight * (sample.y - mean) * (sample.y - mean);
10040
+ const n = sumW * sumW / sumW2;
10041
+ const variance = n > 1 ? sumWD / sumW * (n / (n - 1)) : 0;
10042
+ return { mean, variance, n };
10043
+ }
10044
+ function shrink(own, prior, kappa = SHRINKAGE_KAPPA, meanKappa = kappa) {
10045
+ if (!own) return prior;
10046
+ const priorMu = prior?.mu ?? own.mean;
10047
+ const priorVariance = prior ? prior.sigma * prior.sigma : DEFAULT_LOG_SIGMA * DEFAULT_LOG_SIGMA;
10048
+ const mu = (own.n * own.mean + meanKappa * priorMu) / (own.n + meanKappa);
10049
+ const variance = (own.n * own.variance + kappa * priorVariance) / (own.n + kappa);
10050
+ return { mu, sigma: Math.min(MAX_LOG_SIGMA, Math.max(MIN_LOG_SIGMA, Math.sqrt(Math.max(0, variance)))) };
10051
+ }
10052
+ function agentPool(history, key, effort, now) {
10053
+ const slash = key.indexOf("/");
10054
+ if (slash < 0) return { count: 0, n: 0 };
10055
+ const prefix = key.slice(0, slash + 1);
10056
+ const pool = [];
10057
+ for (const [other, samples] of history) {
10058
+ if (other === key || !other.startsWith(prefix)) continue;
10059
+ pool.push(...prepare(samples, { effort }, now));
10060
+ }
10061
+ const measured = moments(pool);
10062
+ return { fit: shrink(measured, void 0), count: pool.length, n: measured?.n ?? 0 };
10063
+ }
10064
+ function fitKey(own, pool, input) {
10065
+ if (own.length + pool.count < MIN_TURN_SAMPLES) return void 0;
10066
+ const poolPull = pool.fit ? SHRINKAGE_KAPPA * pool.n / (pool.n + SHRINKAGE_KAPPA) : SHRINKAGE_KAPPA;
10067
+ const key = shrink(moments(own), pool.fit, SHRINKAGE_KAPPA, poolPull);
10068
+ if (!key) return void 0;
10069
+ const mine = input.session === void 0 ? [] : own.filter((sample) => sample.session === input.session);
10070
+ const usual = mine.length > 0 ? shrink(moments(mine), key) ?? key : key;
10071
+ const calls = input.calls !== void 0 && input.calls > 0 ? input.calls : void 0;
10072
+ const out = input.out !== void 0 && input.out > 0 ? input.out : void 0;
10073
+ let fit = usual;
10074
+ if (calls !== void 0 || out !== void 0) {
10075
+ const kept = own.filter(
10076
+ (sample) => (calls === void 0 || sample.calls === void 0 || sample.calls >= calls) && (out === void 0 || sample.out === void 0 || !sample.sameEffort || sample.out >= out)
10077
+ );
10078
+ if (kept.length < own.length && kept.length >= MIN_CONDITIONED_SAMPLES) {
10079
+ fit = shrink(moments(kept), usual, CONDITIONED_KAPPA) ?? usual;
10080
+ }
10081
+ }
10082
+ const maxMs = own.length > 0 ? own.reduce((longest, sample) => Math.max(longest, sample.ms), 0) : Number.POSITIVE_INFINITY;
10083
+ return { fit, usual, maxMs };
10084
+ }
10085
+ var NORMAL_CALIBRATION = { count: 0, weight: 0, cdf: normalCdf, quantile: normalQuantile };
10086
+ function weightedQuantile(sorted, total, q) {
10087
+ let cumulative = 0;
10088
+ let previous;
10089
+ for (const entry of sorted) {
10090
+ const at = (cumulative + entry.weight / 2) / total;
10091
+ cumulative += entry.weight;
10092
+ if (at >= q) {
10093
+ if (!previous) return entry.value;
10094
+ return previous.value + (q - previous.at) / (at - previous.at) * (entry.value - previous.value);
10095
+ }
10096
+ previous = { value: entry.value, at };
10097
+ }
10098
+ return sorted[sorted.length - 1].value;
10099
+ }
10100
+ function calibrationFrom(residuals, weights) {
10101
+ 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);
10102
+ if (entries.length === 0) return NORMAL_CALIBRATION;
10103
+ let total = 0;
10104
+ let squares = 0;
10105
+ let sum = 0;
10106
+ for (const entry of entries) {
10107
+ total += entry.weight;
10108
+ squares += entry.weight * entry.weight;
10109
+ sum += entry.weight * entry.value;
10110
+ }
10111
+ const n = total * total / squares;
10112
+ const mean = sum / total;
10113
+ let deviation = 0;
10114
+ for (const entry of entries) deviation += entry.weight * (entry.value - mean) * (entry.value - mean);
10115
+ const sd = n > 1 ? Math.sqrt(deviation / total * (n / (n - 1))) : 0;
10116
+ const sorted = [...entries].sort((a, b) => a.value - b.value);
10117
+ const iqr = weightedQuantile(sorted, total, 0.75) - weightedQuantile(sorted, total, 0.25);
10118
+ const spreads = [sd, iqr / 1.349].filter((spread2) => spread2 > 0);
10119
+ const spread = spreads.length > 0 ? Math.min(...spreads) : 1;
10120
+ const width = Math.max(0.1, 0.9 * spread * Math.pow(n, -0.2));
10121
+ const weight = n / (n + CALIBRATION_PRIOR_RESIDUALS);
10122
+ const size = Math.round((GRID_HIGH - GRID_LOW) / GRID_STEP) + 1;
10123
+ const table = new Float64Array(size);
10124
+ for (let index = 0; index < size; index += 1) {
10125
+ const z = GRID_LOW + index * GRID_STEP;
10126
+ let kernel = 0;
10127
+ for (const entry of entries) kernel += entry.weight * normalCdf((z - entry.value) / width);
10128
+ table[index] = weight * (kernel / total) + (1 - weight) * normalCdf(z);
10129
+ }
10130
+ return {
10131
+ count: entries.length,
10132
+ weight,
10133
+ cdf(z) {
10134
+ if (Number.isNaN(z)) return 0.5;
10135
+ const position = (Math.min(GRID_HIGH, Math.max(GRID_LOW, z)) - GRID_LOW) / GRID_STEP;
10136
+ const index = Math.min(size - 2, Math.floor(position));
10137
+ return table[index] + (position - index) * (table[index + 1] - table[index]);
10138
+ },
10139
+ quantile(p) {
10140
+ if (!(p > table[0])) return GRID_LOW;
10141
+ if (!(p < table[size - 1])) return GRID_HIGH;
10142
+ let low = 0;
10143
+ let high = size - 1;
10144
+ while (high - low > 1) {
10145
+ const mid = low + high >> 1;
10146
+ if (table[mid] < p) low = mid;
10147
+ else high = mid;
10148
+ }
10149
+ const span = table[high] - table[low];
10150
+ return GRID_LOW + (low + (span > 0 ? (p - table[low]) / span : 1)) * GRID_STEP;
10151
+ }
10152
+ };
10153
+ }
10154
+ function quantileMs(fit, calibration, truncatedAtMs, q) {
10155
+ const passed = truncatedAtMs > 0 ? calibration.cdf((Math.log(truncatedAtMs) - fit.mu) / fit.sigma) : 0;
10156
+ return Math.exp(fit.mu + fit.sigma * calibration.quantile(passed + q * (1 - passed)));
10157
+ }
10158
+ var EMPTY_HISTORY = /* @__PURE__ */ new Map();
10159
+ function buildTurnModel(history = EMPTY_HISTORY, now = Date.now()) {
10160
+ const pools = /* @__PURE__ */ new Map();
10161
+ const poolFor = (key, effort) => {
10162
+ const id = `${key}\0${effort ?? ""}`;
10163
+ let pool = pools.get(id);
10164
+ if (!pool) {
10165
+ pool = agentPool(history, key, effort, now);
10166
+ pools.set(id, pool);
10167
+ }
10168
+ return pool;
10169
+ };
10170
+ const assessed = /* @__PURE__ */ new Map();
10171
+ const residuals = [];
10172
+ const residualWeights = [];
10173
+ for (const [key, samples] of history) {
10174
+ const usable = usableSamples(samples);
10175
+ if (usable.length < 2) continue;
10176
+ const rows = [];
10177
+ for (const held of usable.slice(-MAX_ASSESSED_PER_KEY)) {
10178
+ const input = { chars: held.chars, effort: held.effort, session: held.session };
10179
+ const own = prepare(
10180
+ usable.filter((sample) => sample !== held),
10181
+ input,
10182
+ now
10183
+ );
10184
+ const fitted = fitKey(own, poolFor(key, held.effort), input);
10185
+ if (!fitted) continue;
10186
+ rows.push({ ms: held.ms, fit: fitted.fit });
10187
+ residuals.push((Math.log(Math.max(1, held.ms)) - fitted.fit.mu) / fitted.fit.sigma);
10188
+ residualWeights.push(recencyWeight(now, held.at));
10189
+ }
10190
+ if (rows.length > 0) assessed.set(key, rows);
10191
+ }
10192
+ const calibration = calibrationFrom(residuals, residualWeights);
10193
+ const estimate = (key, input) => {
10194
+ const at = input.now ?? now;
10195
+ const own = prepare(history.get(key) ?? [], input, at);
10196
+ const fitted = fitKey(own, poolFor(key, input.effort), input);
10197
+ if (!fitted) return void 0;
10198
+ const elapsed = Math.max(0, Number.isFinite(input.elapsedMs) ? input.elapsedMs : 0);
10199
+ if (elapsed > fitted.maxMs) {
10200
+ return { totalMs: quantileMs(fitted.usual, calibration, 0, 0.5), fill: 1, over: true };
10201
+ }
10202
+ const totalMs = Math.max(elapsed, quantileMs(fitted.fit, calibration, elapsed, 0.5));
10203
+ const lowMs = Math.min(totalMs, Math.max(elapsed, quantileMs(fitted.fit, calibration, elapsed, 0.25)));
10204
+ const highMs = Math.max(totalMs, quantileMs(fitted.fit, calibration, elapsed, 0.75));
10205
+ return {
10206
+ totalMs,
10207
+ fill: totalMs > 0 ? elapsed / totalMs : 0,
10208
+ over: false,
10209
+ band: {
10210
+ lowMs,
10211
+ highMs,
10212
+ sure: highMs > 0 ? elapsed / highMs : 0,
10213
+ maybe: lowMs > 0 ? Math.min(1, elapsed / lowMs) : 0
10214
+ }
10215
+ };
10216
+ };
10217
+ const assess = () => {
10218
+ const keys = [];
10219
+ for (const [key, samples] of history) {
10220
+ const usable = usableSamples(samples);
10221
+ const failed = samples.filter((sample) => sample.failed).length;
10222
+ if (usable.length === 0 && failed === 0) continue;
10223
+ const durations = usable.map((sample) => sample.ms).sort((a, b) => a - b);
10224
+ const typicalMs = durations.length > 0 ? durations[Math.floor((durations.length - 1) / 2)] : 0;
10225
+ const rows = assessed.get(key) ?? [];
10226
+ const misses = [];
10227
+ let over = 0;
10228
+ let inBand = 0;
10229
+ for (const row of rows) {
10230
+ const expected = Math.exp(row.fit.mu + row.fit.sigma * calibration.quantile(0.5));
10231
+ const low = Math.exp(row.fit.mu + row.fit.sigma * calibration.quantile(0.25));
10232
+ const high = Math.exp(row.fit.mu + row.fit.sigma * calibration.quantile(0.75));
10233
+ misses.push(Math.abs(Math.log(Math.max(1, row.ms) / expected)));
10234
+ if (row.ms > expected) over += 1;
10235
+ if (row.ms >= low && row.ms <= high) inBand += 1;
10236
+ }
10237
+ misses.sort((a, b) => a - b);
10238
+ keys.push({
10239
+ key,
10240
+ turns: usable.length,
10241
+ failed,
10242
+ typicalMs,
10243
+ assessed: rows.length,
10244
+ ...rows.length > 0 ? {
10245
+ missFactor: Math.exp(misses[Math.floor((misses.length - 1) / 2)]),
10246
+ overShare: over / rows.length,
10247
+ inBandShare: inBand / rows.length
10248
+ } : {}
10249
+ });
10250
+ }
10251
+ keys.sort((a, b) => b.turns - a.turns || a.key.localeCompare(b.key));
10252
+ return { keys, residuals: residuals.length, calibrationWeight: calibration.weight };
10253
+ };
10254
+ return { estimate, calibration, assess };
10255
+ }
10256
+
9638
10257
  // src/stale-build.ts
9639
10258
  import * as fs7 from "fs";
9640
10259
  import * as path10 from "path";
@@ -10196,6 +10815,12 @@ function formatDoctorReport(report) {
10196
10815
  lines.push("");
10197
10816
  lines.push(...formatAccountUsage(report.accountUsage));
10198
10817
  }
10818
+ if (report.turnEstimate) {
10819
+ lines.push("");
10820
+ lines.push("**Progress estimate**");
10821
+ lines.push("");
10822
+ lines.push(...formatTurnEstimate(report.turnEstimate));
10823
+ }
10199
10824
  lines.push("");
10200
10825
  lines.push("**Background subagents**");
10201
10826
  lines.push("");
@@ -10312,6 +10937,37 @@ function formatAccountUsage(usage, now = Date.now()) {
10312
10937
  ...rows
10313
10938
  ];
10314
10939
  }
10940
+ function formatTurnEstimate(assessment) {
10941
+ if (assessment.keys.length === 0) {
10942
+ return [
10943
+ `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.`
10944
+ ];
10945
+ }
10946
+ const percent = (share) => share === void 0 ? "n/a" : `${Math.round(share * 100)}%`;
10947
+ const lines = [
10948
+ 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.`,
10949
+ "",
10950
+ "| agent/model | turns | failed | typical | assessed | off by | outran | in band |",
10951
+ "|---|---|---|---|---|---|---|---|"
10952
+ ];
10953
+ for (const key of assessment.keys) {
10954
+ lines.push(
10955
+ `| ${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)} |`
10956
+ );
10957
+ }
10958
+ lines.push("");
10959
+ lines.push(
10960
+ "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."
10961
+ );
10962
+ return lines;
10963
+ }
10964
+ function assessTurnEstimate() {
10965
+ try {
10966
+ return buildTurnModel(createTurnHistoryReader().read()).assess();
10967
+ } catch {
10968
+ return void 0;
10969
+ }
10970
+ }
10315
10971
  function formatBackgroundSubagents(state) {
10316
10972
  const lines = [];
10317
10973
  if (state.running) {
@@ -10497,6 +11153,7 @@ async function gatherDoctorReport(options) {
10497
11153
  hookFailures: snapshotHookFailures(),
10498
11154
  planUsage,
10499
11155
  accountUsage: snapshotAccountUsage(),
11156
+ turnEstimate: assessTurnEstimate(),
10500
11157
  backgroundSubagents: {
10501
11158
  gate: snapshotBackgroundSubagentGate(),
10502
11159
  ledgers: snapshotBackgroundTasks(),
@@ -11629,9 +12286,9 @@ async function resolveSkillPluginDirs(opts) {
11629
12286
  // src/claude-session-wrapper.ts
11630
12287
  import { EventEmitter as EventEmitter4 } from "events";
11631
12288
  import { randomUUID as randomUUID5 } from "crypto";
11632
- import { existsSync as existsSync4, writeFileSync as writeFileSync7 } from "fs";
12289
+ import { existsSync as existsSync4, writeFileSync as writeFileSync9 } from "fs";
11633
12290
  import { unlink as unlink2 } from "fs/promises";
11634
- import { join as join14 } from "path";
12291
+ import { join as join16 } from "path";
11635
12292
 
11636
12293
  // src/claude-session-bun.ts
11637
12294
  import * as os6 from "os";
@@ -12813,8 +13470,8 @@ ${text2}`
12813
13470
  }
12814
13471
  function stageImage(data, extension) {
12815
13472
  try {
12816
- const file = join14(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
12817
- writeFileSync7(file, data, { mode: 384, flag: "wx" });
13473
+ const file = join16(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
13474
+ writeFileSync9(file, data, { mode: 384, flag: "wx" });
12818
13475
  return file;
12819
13476
  } catch (err) {
12820
13477
  log.warn("interactive transport could not stage an image", {
@@ -13295,17 +13952,17 @@ function spawnInteractiveProcess(opts) {
13295
13952
  }
13296
13953
 
13297
13954
  // 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";
13955
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync11, renameSync as renameSync7, statSync as statSync10, writeFileSync as writeFileSync10 } from "fs";
13956
+ import { homedir as homedir14 } from "os";
13957
+ import { join as join17 } from "path";
13301
13958
  var MAX_SPAWN_RECORDS = 256;
13302
13959
  var SPAWN_RECORD_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
13303
- var pathOverride4 = null;
13960
+ var pathOverride6 = null;
13304
13961
  var lastWritten = /* @__PURE__ */ new Map();
13305
13962
  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");
13963
+ if (pathOverride6) return pathOverride6;
13964
+ const stateRoot = process.env.XDG_STATE_HOME || join17(homedir14(), ".local", "state");
13965
+ return join17(stateRoot, "opencode-claude-code-plugin", "session-spawns.json");
13309
13966
  }
13310
13967
  function isRecord5(value) {
13311
13968
  const record = value;
@@ -13315,10 +13972,10 @@ function isRecord5(value) {
13315
13972
  if (!ok(record.model) || !ok(record.effort) || !ok(record.account) || !ok(record.agent)) return false;
13316
13973
  return !!record.model;
13317
13974
  }
13318
- function readEntries(file) {
13975
+ function readEntries3(file) {
13319
13976
  const out = /* @__PURE__ */ new Map();
13320
13977
  try {
13321
- const parsed = JSON.parse(readFileSync9(file, "utf8"));
13978
+ const parsed = JSON.parse(readFileSync11(file, "utf8"));
13322
13979
  const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
13323
13980
  if (!entries || typeof entries !== "object") return out;
13324
13981
  const cutoff = Date.now() - SPAWN_RECORD_MAX_AGE_MS;
@@ -13351,18 +14008,18 @@ ${record.account ?? ""}
13351
14008
  ${record.agent ?? ""}`;
13352
14009
  if (lastWritten.get(sessionID) === fingerprint) return false;
13353
14010
  const file = spawnRecordStorePath();
13354
- const merged = readEntries(file);
14011
+ const merged = readEntries3(file);
13355
14012
  merged.delete(sessionID);
13356
14013
  merged.set(sessionID, record);
13357
14014
  const cutoff = now - SPAWN_RECORD_MAX_AGE_MS;
13358
14015
  const kept = [...merged.entries()].filter(([, entry]) => entry.at >= cutoff).sort((a, b) => a[1].at - b[1].at).slice(-MAX_SPAWN_RECORDS);
13359
14016
  try {
13360
- mkdirSync7(join15(file, ".."), { recursive: true, mode: 448 });
14017
+ mkdirSync9(join17(file, ".."), { recursive: true, mode: 448 });
13361
14018
  const tmp = `${file}.${process.pid}.tmp`;
13362
- writeFileSync8(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
14019
+ writeFileSync10(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
13363
14020
  mode: 384
13364
14021
  });
13365
- renameSync5(tmp, file);
14022
+ renameSync7(tmp, file);
13366
14023
  } catch {
13367
14024
  return false;
13368
14025
  }
@@ -13371,133 +14028,6 @@ ${record.agent ?? ""}`;
13371
14028
  return true;
13372
14029
  }
13373
14030
 
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
- var MAX_TURN_STARTS = 256;
13430
- var turnStarts = /* @__PURE__ */ new Map();
13431
- function markTurnStart(key, at, chars) {
13432
- turnStarts.delete(key);
13433
- if (turnStarts.size >= MAX_TURN_STARTS) {
13434
- const oldest = turnStarts.keys().next().value;
13435
- if (oldest !== void 0) turnStarts.delete(oldest);
13436
- }
13437
- turnStarts.set(key, { at, ...chars !== void 0 ? { chars } : {} });
13438
- }
13439
- function takeTurnStart(key) {
13440
- const start = turnStarts.get(key);
13441
- turnStarts.delete(key);
13442
- return start;
13443
- }
13444
- function isSample(value) {
13445
- const sample = value;
13446
- if (!sample || typeof sample !== "object" || Array.isArray(sample)) return false;
13447
- if (typeof sample.ms !== "number" || !Number.isFinite(sample.ms) || sample.ms < 0) return false;
13448
- if (typeof sample.at !== "number" || !Number.isFinite(sample.at)) return false;
13449
- if (sample.chars !== void 0 && (typeof sample.chars !== "number" || !Number.isFinite(sample.chars) || sample.chars < 0)) {
13450
- return false;
13451
- }
13452
- if (sample.effort !== void 0 && typeof sample.effort !== "string") return false;
13453
- return true;
13454
- }
13455
- function cleanSample(sample) {
13456
- return {
13457
- ms: Math.round(sample.ms),
13458
- ...sample.chars !== void 0 ? { chars: Math.round(sample.chars) } : {},
13459
- ...sample.effort ? { effort: sample.effort } : {},
13460
- at: sample.at
13461
- };
13462
- }
13463
- function readEntries2(file, now) {
13464
- const out = /* @__PURE__ */ new Map();
13465
- try {
13466
- const parsed = JSON.parse(readFileSync10(file, "utf8"));
13467
- const entries = parsed && typeof parsed === "object" ? parsed.turns : null;
13468
- if (!entries || typeof entries !== "object") return out;
13469
- const cutoff = now - TURN_SAMPLE_MAX_AGE_MS;
13470
- for (const [key, value] of Object.entries(entries)) {
13471
- if (!key || !Array.isArray(value)) continue;
13472
- const samples = value.filter(isSample).filter((sample) => sample.at >= cutoff).sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES).map(cleanSample);
13473
- if (samples.length > 0) out.set(key, samples);
13474
- }
13475
- } catch {
13476
- }
13477
- return out;
13478
- }
13479
- function bounded2(history) {
13480
- const newest = (samples) => samples[samples.length - 1]?.at ?? 0;
13481
- return new Map([...history.entries()].sort((a, b) => newest(a[1]) - newest(b[1])).slice(-MAX_TURN_KEYS));
13482
- }
13483
- function recordTurnSample(key, sample) {
13484
- if (!key || !isSample(sample)) return false;
13485
- const file = turnHistoryStorePath();
13486
- const merged = readEntries2(file, sample.at);
13487
- const samples = [...merged.get(key) ?? [], cleanSample(sample)].sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES);
13488
- merged.set(key, samples);
13489
- const kept = bounded2(merged);
13490
- try {
13491
- mkdirSync8(join16(file, ".."), { recursive: true, mode: 448 });
13492
- const tmp = `${file}.${process.pid}.tmp`;
13493
- writeFileSync9(tmp, JSON.stringify({ version: 1, turns: Object.fromEntries(kept) }), { mode: 384 });
13494
- renameSync6(tmp, file);
13495
- } catch {
13496
- return false;
13497
- }
13498
- return true;
13499
- }
13500
-
13501
14031
  // src/account-transcript.ts
13502
14032
  import { randomUUID as randomUUID6 } from "crypto";
13503
14033
  import { constants as fsConstants } from "fs";
@@ -13559,13 +14089,13 @@ function dedupe2(dirs) {
13559
14089
  }
13560
14090
 
13561
14091
  // src/prompts.ts
13562
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
13563
- import { homedir as homedir14 } from "os";
14092
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
14093
+ import { homedir as homedir15 } from "os";
13564
14094
  import { randomUUID as randomUUID7 } from "crypto";
13565
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join17 } from "path";
14095
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join18 } from "path";
13566
14096
  function readPromptFileIfPresent(path15) {
13567
14097
  try {
13568
- const content = readFileSync11(path15, "utf8").trim();
14098
+ const content = readFileSync12(path15, "utf8").trim();
13569
14099
  return content || void 0;
13570
14100
  } catch {
13571
14101
  return void 0;
@@ -13574,7 +14104,7 @@ function readPromptFileIfPresent(path15) {
13574
14104
  function nearestWorkspaceAgentsPrompt(cwd) {
13575
14105
  let dir = cwd;
13576
14106
  while (true) {
13577
- const content = readPromptFileIfPresent(join17(dir, "AGENTS.md"));
14107
+ const content = readPromptFileIfPresent(join18(dir, "AGENTS.md"));
13578
14108
  if (content) return content;
13579
14109
  const parent = dirname6(dir);
13580
14110
  if (parent === dir) return void 0;
@@ -13685,7 +14215,7 @@ var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLA
13685
14215
  function userAuthoredInstructions(forwarded, opts = {}) {
13686
14216
  const readFile3 = opts.readFile ?? ((path15) => {
13687
14217
  try {
13688
- return readFileSync11(path15, "utf8");
14218
+ return readFileSync12(path15, "utf8");
13689
14219
  } catch {
13690
14220
  return null;
13691
14221
  }
@@ -13726,8 +14256,8 @@ ${options.compressionSummary.trim()}`
13726
14256
  for (const s of extraSystemContent) {
13727
14257
  if (s.trim()) parts.push(s.trim());
13728
14258
  }
13729
- const configRoot = process.env.XDG_CONFIG_HOME ?? join17(homedir14(), ".config");
13730
- const globalAgents = readPromptFileIfPresent(join17(configRoot, "opencode", "AGENTS.md"));
14259
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join18(homedir15(), ".config");
14260
+ const globalAgents = readPromptFileIfPresent(join18(configRoot, "opencode", "AGENTS.md"));
13731
14261
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
13732
14262
  const forwarded = extraSystemContent.join("\n\n");
13733
14263
  const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
@@ -13743,9 +14273,9 @@ ${options.compressionSummary.trim()}`
13743
14273
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
13744
14274
  const content = parts.join("\n\n");
13745
14275
  if (!content) return void 0;
13746
- const path15 = join17(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
14276
+ const path15 = join18(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
13747
14277
  try {
13748
- writeFileSync10(path15, content, { encoding: "utf8", mode: 384 });
14278
+ writeFileSync11(path15, content, { encoding: "utf8", mode: 384 });
13749
14279
  return path15;
13750
14280
  } catch (err) {
13751
14281
  log.warn("failed to write system prompt file", { error: String(err) });
@@ -14675,6 +15205,7 @@ function createTurnState(init) {
14675
15205
  toolCallsById: /* @__PURE__ */ new Map(),
14676
15206
  toolCallEmittedIds: /* @__PURE__ */ new Set(),
14677
15207
  toolCallAnsweredIds: /* @__PURE__ */ new Set(),
15208
+ lastCliToolResultError: void 0,
14678
15209
  resultMeta: {},
14679
15210
  lastCallUsage: void 0,
14680
15211
  resultFailure: void 0,
@@ -15744,6 +16275,10 @@ ${plan}
15744
16275
  if (state.gotPartialEvents && msg.type === "message_delta" && typeof msg.delta?.stop_reason === "string") {
15745
16276
  state.lastStopReason = msg.delta.stop_reason;
15746
16277
  }
16278
+ if (msg.type === "message_delta") {
16279
+ const output = msg.usage?.output_tokens;
16280
+ if (typeof output === "number") noteTurnOutput(state.sessionKey, output);
16281
+ }
15747
16282
  if (msg.type === "assistant" && msg.message && typeof msg.message.stop_reason === "string") {
15748
16283
  state.lastStopReason = msg.message.stop_reason;
15749
16284
  }
@@ -16023,6 +16558,7 @@ ${plan}
16023
16558
  const toolCall = state.toolCallsById.get(block2.tool_use_id) ?? adoptOpenCliToolCall(state, ctx, block2.tool_use_id);
16024
16559
  if (toolCall) {
16025
16560
  const isError = block2.is_error === true;
16561
+ state.lastCliToolResultError = isError;
16026
16562
  state.controller.enqueue({
16027
16563
  type: "tool-result",
16028
16564
  toolCallId: block2.tool_use_id,
@@ -17150,8 +17686,14 @@ var ClaudeCodeLanguageModel = class {
17150
17686
  account: (this.config.failoverAccounts?.length ?? 0) > 1 ? failover.target ?? sourceAccount : void 0,
17151
17687
  agent: this.getOpencodeAgent(options)
17152
17688
  });
17153
- if (startsTurn(options.prompt)) {
17154
- markTurnStart(sk, turnStartedAt, newestPromptChars(options.prompt));
17689
+ const parked = new Set(getPendingProxyCalls(sk).map((call) => call.toolCallId));
17690
+ if (startsTurn(options.prompt, parked)) {
17691
+ markTurnStart(sk, {
17692
+ at: turnStartedAt,
17693
+ chars: newestPromptChars(options.prompt),
17694
+ promptLength: options.prompt.length,
17695
+ session: sessionTag(affinity)
17696
+ });
17155
17697
  }
17156
17698
  }
17157
17699
  const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
@@ -17442,7 +17984,7 @@ var ClaudeCodeLanguageModel = class {
17442
17984
  let replayReason = "no-session-for-key";
17443
17985
  if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
17444
17986
  const configDir = resolveConfigDir(this.config.configDir);
17445
- const transcriptPath2 = (id, dir) => join18(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
17987
+ const transcriptPath2 = (id, dir) => join19(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
17446
17988
  const resumePoint = findResumePoint({
17447
17989
  sessionKey: sk,
17448
17990
  prompt: options.prompt,
@@ -18513,6 +19055,21 @@ var ClaudeCodeLanguageModel = class {
18513
19055
  ms: endedAt - start.at,
18514
19056
  chars: start.chars,
18515
19057
  effort: reasoningEffort,
19058
+ // The turn's tool calls, counted as the TUI counts its tool
19059
+ // parts (h #g250): the earlier steps' from the prompt, this
19060
+ // step's own from what it emitted.
19061
+ calls: turnToolCalls(options.prompt, start.promptLength) + state.toolCallEmittedIds.size,
19062
+ // The output its API calls generated, summed as the live
19063
+ // channel wrote it, so a running turn is compared with the
19064
+ // record in the same unit (h #g251).
19065
+ out: start.out,
19066
+ // A turn whose newest tool result was an error ended on a
19067
+ // failure (h #g251): this step's own CLI-executed results
19068
+ // are newer than the prompt's, so they answer first.
19069
+ failed: state.lastCliToolResultError ?? lastToolResultFailed(options.prompt, start.promptLength ?? 0),
19070
+ // The conversation, as a hash of its id: the estimate's
19071
+ // session level (h #g251).
19072
+ session: sessionTag(affinity),
18516
19073
  at: endedAt
18517
19074
  });
18518
19075
  }
@@ -18967,7 +19524,7 @@ var ClaudeCodeLanguageModel = class {
18967
19524
  };
18968
19525
 
18969
19526
  // src/account-tier.ts
18970
- import { readFileSync as readFileSync12 } from "fs";
19527
+ import { readFileSync as readFileSync13 } from "fs";
18971
19528
  import path14 from "path";
18972
19529
  var ORGANIZATION_TYPE_LABELS = {
18973
19530
  claude_max: "Max",
@@ -19010,7 +19567,7 @@ function readTierFromConfigDir(configDir, allowLegacyFallback) {
19010
19567
  function readOauthAccount(file) {
19011
19568
  let raw;
19012
19569
  try {
19013
- raw = readFileSync12(file, "utf8");
19570
+ raw = readFileSync13(file, "utf8");
19014
19571
  } catch {
19015
19572
  return void 0;
19016
19573
  }
@@ -19047,14 +19604,14 @@ function registerSidebarAccounts(accounts) {
19047
19604
  // src/cleanup-stale.ts
19048
19605
  import {
19049
19606
  existsSync as existsSync5,
19050
- mkdirSync as mkdirSync9,
19051
- readFileSync as readFileSync13,
19607
+ mkdirSync as mkdirSync10,
19608
+ readFileSync as readFileSync14,
19052
19609
  realpathSync as realpathSync4,
19053
19610
  rmSync as rmSync3,
19054
- writeFileSync as writeFileSync11
19611
+ writeFileSync as writeFileSync12
19055
19612
  } from "fs";
19056
- import { homedir as homedir15 } from "os";
19057
- import { dirname as dirname7, join as join19, resolve as resolve6 } from "path";
19613
+ import { homedir as homedir16 } from "os";
19614
+ import { dirname as dirname7, join as join20, resolve as resolve6 } from "path";
19058
19615
  import { fileURLToPath as fileURLToPath4 } from "url";
19059
19616
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
19060
19617
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -19062,20 +19619,20 @@ var alreadyRan = false;
19062
19619
  function candidateCacheRoots() {
19063
19620
  const xdg = process.env.XDG_CACHE_HOME;
19064
19621
  return [
19065
- xdg ? join19(xdg, "opencode") : null,
19066
- join19(homedir15(), ".cache", "opencode"),
19067
- join19(homedir15(), "Library", "Caches", "opencode")
19622
+ xdg ? join20(xdg, "opencode") : null,
19623
+ join20(homedir16(), ".cache", "opencode"),
19624
+ join20(homedir16(), "Library", "Caches", "opencode")
19068
19625
  ].filter((p) => Boolean(p));
19069
19626
  }
19070
19627
  function userOpencodeJsonPath() {
19071
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join19(homedir15(), ".config");
19072
- return join19(xdgConfig, "opencode", "opencode.json");
19628
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join20(homedir16(), ".config");
19629
+ return join20(xdgConfig, "opencode", "opencode.json");
19073
19630
  }
19074
19631
  function userIntendsToUseUnscoped() {
19075
19632
  const cfg = userOpencodeJsonPath();
19076
19633
  if (!existsSync5(cfg)) return false;
19077
19634
  try {
19078
- const json = JSON.parse(readFileSync13(cfg, "utf8"));
19635
+ const json = JSON.parse(readFileSync14(cfg, "utf8"));
19079
19636
  const plugins = json.plugin;
19080
19637
  if (!Array.isArray(plugins)) return false;
19081
19638
  return plugins.some(
@@ -19096,19 +19653,19 @@ function ourLoadedDir() {
19096
19653
  function ourVersion(dir) {
19097
19654
  if (!dir) return null;
19098
19655
  try {
19099
- const pkg = JSON.parse(readFileSync13(join19(dir, "package.json"), "utf8"));
19656
+ const pkg = JSON.parse(readFileSync14(join20(dir, "package.json"), "utf8"));
19100
19657
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
19101
19658
  } catch {
19102
19659
  return null;
19103
19660
  }
19104
19661
  }
19105
19662
  function markerPath() {
19106
- const stateRoot = process.env.XDG_STATE_HOME ?? join19(homedir15(), ".local", "state");
19107
- return join19(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
19663
+ const stateRoot = process.env.XDG_STATE_HOME ?? join20(homedir16(), ".local", "state");
19664
+ return join20(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
19108
19665
  }
19109
19666
  function markerVersion() {
19110
19667
  try {
19111
- const marker = JSON.parse(readFileSync13(markerPath(), "utf8"));
19668
+ const marker = JSON.parse(readFileSync14(markerPath(), "utf8"));
19112
19669
  return typeof marker.version === "string" ? marker.version : null;
19113
19670
  } catch {
19114
19671
  return null;
@@ -19117,8 +19674,8 @@ function markerVersion() {
19117
19674
  function writeMarker(version) {
19118
19675
  const file = markerPath();
19119
19676
  try {
19120
- mkdirSync9(dirname7(file), { recursive: true, mode: 448 });
19121
- writeFileSync11(
19677
+ mkdirSync10(dirname7(file), { recursive: true, mode: 448 });
19678
+ writeFileSync12(
19122
19679
  file,
19123
19680
  JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
19124
19681
  "utf8"
@@ -19153,7 +19710,7 @@ function cleanupStaleUnscopedInstall() {
19153
19710
  }
19154
19711
  function cleanupOne(cacheRoot, ourDir) {
19155
19712
  if (!existsSync5(cacheRoot)) return;
19156
- const stalePath = join19(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
19713
+ const stalePath = join20(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
19157
19714
  if (!existsSync5(stalePath)) return;
19158
19715
  let realStalePath = stalePath;
19159
19716
  try {
@@ -19161,11 +19718,11 @@ function cleanupOne(cacheRoot, ourDir) {
19161
19718
  } catch {
19162
19719
  }
19163
19720
  if (ourDir && realStalePath === ourDir) return;
19164
- const pkgJsonPath = join19(stalePath, "package.json");
19721
+ const pkgJsonPath = join20(stalePath, "package.json");
19165
19722
  if (!existsSync5(pkgJsonPath)) return;
19166
19723
  let pkg = {};
19167
19724
  try {
19168
- pkg = JSON.parse(readFileSync13(pkgJsonPath, "utf8"));
19725
+ pkg = JSON.parse(readFileSync14(pkgJsonPath, "utf8"));
19169
19726
  } catch {
19170
19727
  return;
19171
19728
  }
@@ -19181,13 +19738,13 @@ function cleanupOne(cacheRoot, ourDir) {
19181
19738
  });
19182
19739
  return;
19183
19740
  }
19184
- const cachePkgJson = join19(cacheRoot, "package.json");
19741
+ const cachePkgJson = join20(cacheRoot, "package.json");
19185
19742
  if (!existsSync5(cachePkgJson)) return;
19186
19743
  try {
19187
- const cfg = JSON.parse(readFileSync13(cachePkgJson, "utf8"));
19744
+ const cfg = JSON.parse(readFileSync14(cachePkgJson, "utf8"));
19188
19745
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
19189
19746
  delete cfg.dependencies[STALE_PACKAGE_NAME];
19190
- writeFileSync11(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
19747
+ writeFileSync12(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
19191
19748
  log.info("cleanup-stale: pruned dep from cache package.json");
19192
19749
  }
19193
19750
  } catch (err) {