@serkanalgur/opencode-nexus 2.5.0 → 2.6.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
@@ -8451,7 +8451,7 @@ function describeFile(file) {
8451
8451
  }
8452
8452
  function formatConfigLoadLog(info) {
8453
8453
  const models = Object.entries(info.models).map(([role, model]) => `${role}=${model}`).join(" ");
8454
- const override = info.sessionOverride ? " (+session override: storage; disk edits to models are IGNORED while a preset is set — clear the preset in the TUI to hand control back to disk)" : "";
8454
+ const override = info.sessionOverride ? ' (+session override: storage; disk edits to models are IGNORED while a preset is set — call the preset tool with mode "clear", or reset in the TUI, to hand control back to disk)' : "";
8455
8455
  return `[nexus] config loaded (#${info.loadCount} trigger=${info.trigger} at=${info.loadedAt}) ` + `project=${info.project.path} [${describeFile(info.project)}] ` + `global=${info.global.path} [${describeFile(info.global)}] ` + `models:${override} ${models}`;
8456
8456
  }
8457
8457
  var DEFAULT_CONFIG = {
@@ -8668,7 +8668,9 @@ class NexusConfigManager {
8668
8668
  return result;
8669
8669
  }
8670
8670
  resetToDefaults() {
8671
+ const hadOverride = this.storageConfig !== null;
8671
8672
  this.storageConfig = null;
8673
+ return hadOverride;
8672
8674
  }
8673
8675
  applyPreset(name) {
8674
8676
  const preset = PRESETS[name];
@@ -9797,10 +9799,20 @@ class PerformanceTracker {
9797
9799
  this.maxEntries = maxEntries;
9798
9800
  }
9799
9801
  record(entry) {
9800
- this.entries.push({ ...entry, timestamp: new Date });
9802
+ const id = `perf-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
9803
+ this.entries.push({ ...entry, id, timestamp: new Date });
9801
9804
  if (this.entries.length > this.maxEntries) {
9802
9805
  this.entries = this.entries.slice(-this.maxEntries);
9803
9806
  }
9807
+ return id;
9808
+ }
9809
+ adjust(id, { cost }) {
9810
+ const entry = this.entries.find((e) => e.id === id);
9811
+ if (!entry)
9812
+ return false;
9813
+ if (cost !== undefined)
9814
+ entry.cost += cost;
9815
+ return true;
9804
9816
  }
9805
9817
  getScores() {
9806
9818
  const groups = new Map;
@@ -9818,11 +9830,28 @@ class PerformanceTracker {
9818
9830
  const successRate = total > 0 ? successful / total : 0;
9819
9831
  const avgDuration = entries.reduce((s, e) => s + e.duration, 0) / total;
9820
9832
  const avgCost = entries.reduce((s, e) => s + e.cost, 0) / total;
9821
- const costEfficiency = avgCost > 0 ? successRate / avgCost : 0;
9833
+ const measured = entries.filter((e) => e.costProvenance.usage === "measured");
9834
+ const measuredTasks = measured.length;
9835
+ const estimatedTasks = total - measuredTasks;
9836
+ const measuredSuccessRate = measuredTasks > 0 ? measured.filter((e) => e.success).length / measuredTasks : 0;
9837
+ const measuredAvgCost = measuredTasks > 0 ? measured.reduce((s, e) => s + e.cost, 0) / measuredTasks : 0;
9838
+ const costEfficiency = measuredAvgCost > 0 ? measuredSuccessRate / measuredAvgCost : 0;
9822
9839
  const speedScore = Math.max(0, 1 - avgDuration / 30000);
9823
9840
  const costScore = Math.min(1, costEfficiency * 100);
9824
9841
  const overallScore = successRate * 40 + speedScore * 30 + costScore * 30;
9825
- scores.push({ model, role, totalTasks: total, successRate, avgDuration, avgCost, costEfficiency, overallScore });
9842
+ scores.push({
9843
+ model,
9844
+ role,
9845
+ totalTasks: total,
9846
+ successRate,
9847
+ avgDuration,
9848
+ avgCost,
9849
+ costEfficiency,
9850
+ overallScore,
9851
+ measuredTasks,
9852
+ estimatedTasks,
9853
+ costBasis: measuredTasks > 0 ? "measured" : "none"
9854
+ });
9826
9855
  }
9827
9856
  return scores.sort((a, b) => b.overallScore - a.overallScore);
9828
9857
  }
@@ -9858,6 +9887,16 @@ class ExecutionHistory {
9858
9887
  }
9859
9888
  return full;
9860
9889
  }
9890
+ adjust(id, { cost, tokensUsed }) {
9891
+ const record = this.records.find((r) => r.id === id);
9892
+ if (!record)
9893
+ return false;
9894
+ if (cost !== undefined)
9895
+ record.cost += cost;
9896
+ if (tokensUsed !== undefined)
9897
+ record.tokensUsed += tokensUsed;
9898
+ return true;
9899
+ }
9861
9900
  getAll() {
9862
9901
  return [...this.records];
9863
9902
  }
@@ -9879,7 +9918,17 @@ class ExecutionHistory {
9879
9918
  for (const r of this.records) {
9880
9919
  byRole[r.role] = (byRole[r.role] || 0) + 1;
9881
9920
  }
9882
- return { total, successRate: total > 0 ? successful / total : 0, totalCost, avgDuration, byRole };
9921
+ const costSplit = this.records.reduce((split, r) => {
9922
+ if (r.costProvenance.usage === "measured") {
9923
+ split.measuredCost += r.cost;
9924
+ split.measuredEntries++;
9925
+ } else {
9926
+ split.estimatedCost += r.cost;
9927
+ split.estimatedEntries++;
9928
+ }
9929
+ return split;
9930
+ }, { measuredCost: 0, estimatedCost: 0, measuredEntries: 0, estimatedEntries: 0 });
9931
+ return { total, successRate: total > 0 ? successful / total : 0, totalCost, costSplit, avgDuration, byRole };
9883
9932
  }
9884
9933
  clear() {
9885
9934
  this.records = [];
@@ -9930,16 +9979,19 @@ function per1kPricing(input, output) {
9930
9979
  cacheWrite: input * CACHE_WRITE_MULTIPLE
9931
9980
  };
9932
9981
  }
9982
+ function untiered(rates) {
9983
+ return { tiers: [{ rates }] };
9984
+ }
9933
9985
  var FALLBACK_PRICING_PER_1K = {
9934
- "claude-sonnet-4-6": per1kPricing(0.015, 0.075),
9935
- "claude-opus-4-7": per1kPricing(0.075, 0.375),
9936
- "claude-haiku-4-5": per1kPricing(0.001, 0.005),
9937
- "gpt-5-mini": per1kPricing(0.00015, 0.0006),
9938
- "gpt-5": per1kPricing(0.0025, 0.01),
9939
- "gemini-2.5-flash": per1kPricing(0.000075, 0.0003),
9940
- "minimax-m2.5-free": per1kPricing(0, 0)
9986
+ "claude-sonnet-4-6": untiered(per1kPricing(0.015, 0.075)),
9987
+ "claude-opus-4-7": untiered(per1kPricing(0.075, 0.375)),
9988
+ "claude-haiku-4-5": untiered(per1kPricing(0.001, 0.005)),
9989
+ "gpt-5-mini": untiered(per1kPricing(0.00015, 0.0006)),
9990
+ "gpt-5": untiered(per1kPricing(0.0025, 0.01)),
9991
+ "gemini-2.5-flash": untiered(per1kPricing(0.000075, 0.0003)),
9992
+ "minimax-m2.5-free": untiered(per1kPricing(0, 0))
9941
9993
  };
9942
- var UNKNOWN_PRICING_PER_1K = per1kPricing(0.01, 0.05);
9994
+ var UNKNOWN_PRICING_PER_1K = untiered(per1kPricing(0.01, 0.05));
9943
9995
  var per1k = (rate, tokens) => rate * tokens / 1000;
9944
9996
  function bareModelId(ref) {
9945
9997
  return ref.slice(ref.indexOf("/") + 1);
@@ -9947,6 +9999,35 @@ function bareModelId(ref) {
9947
9999
  function totalTokens(usage) {
9948
10000
  return usage.input + usage.output + usage.reasoning + usage.cache.read + usage.cache.write;
9949
10001
  }
10002
+ function promptSizeOf(usage) {
10003
+ return usage.input + usage.cache.read + usage.cache.write;
10004
+ }
10005
+ var ZERO_RATES = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
10006
+ function selectTier(tiers, promptSize) {
10007
+ let best;
10008
+ let bestThreshold = Number.NEGATIVE_INFINITY;
10009
+ for (const tier of tiers) {
10010
+ const { threshold } = tier;
10011
+ if (threshold === undefined)
10012
+ continue;
10013
+ if (!(promptSize > threshold))
10014
+ continue;
10015
+ if (threshold > bestThreshold) {
10016
+ best = tier;
10017
+ bestThreshold = threshold;
10018
+ }
10019
+ }
10020
+ if (best)
10021
+ return best;
10022
+ const base = tiers.find((t) => t.threshold === undefined);
10023
+ return base ?? { rates: ZERO_RATES };
10024
+ }
10025
+ function priceUsage(usage, pricing) {
10026
+ return priceTokens(usage, selectTier(pricing.tiers, promptSizeOf(usage)).rates);
10027
+ }
10028
+ function priceUsageAtSettledTier(increment, pricing, settledUsage) {
10029
+ return priceTokens(increment, selectTier(pricing.tiers, promptSizeOf(settledUsage)).rates);
10030
+ }
9950
10031
  function priceTokens(usage, pricing) {
9951
10032
  const inputCost = per1k(pricing.input, usage.input);
9952
10033
  const outputCost = per1k(pricing.output, usage.output + usage.reasoning);
@@ -9959,7 +10040,11 @@ class CostForecaster {
9959
10040
  constructor(pricing) {
9960
10041
  this.resolvePricing = pricing;
9961
10042
  }
9962
- priceFor(model, provider) {
10043
+ priceFor(model, provider, promptSize) {
10044
+ const { pricing, source } = this.tiersFor(model, provider);
10045
+ return { pricing: selectTier(pricing.tiers, promptSize ?? 0).rates, source };
10046
+ }
10047
+ tiersFor(model, provider) {
9963
10048
  const real = this.resolvePricing?.(model, provider);
9964
10049
  if (real)
9965
10050
  return { pricing: real, source: "model-costs" };
@@ -9970,16 +10055,16 @@ class CostForecaster {
9970
10055
  return { pricing: UNKNOWN_PRICING_PER_1K, source: "unknown-model" };
9971
10056
  }
9972
10057
  measureCost(usage, model, provider) {
9973
- const { pricing, source } = this.priceFor(model, provider);
10058
+ const { pricing, source } = this.tiersFor(model, provider);
9974
10059
  return {
9975
- cost: priceTokens(usage, pricing).total,
10060
+ cost: priceUsage(usage, pricing).total,
9976
10061
  tokens: totalTokens(usage),
9977
10062
  pricingSource: source,
9978
10063
  ...source === "model-costs" ? { confidence: 1 } : {}
9979
10064
  };
9980
10065
  }
9981
10066
  costOf(usage, model, provider) {
9982
- return priceTokens(usage, this.priceFor(model, provider).pricing).total;
10067
+ return priceUsage(usage, this.tiersFor(model, provider).pricing).total;
9983
10068
  }
9984
10069
  estimateTokensFor(complexity, fileCount) {
9985
10070
  const baseInput = 700;
@@ -9998,8 +10083,8 @@ class CostForecaster {
9998
10083
  }
9999
10084
  forecastTask(task, role, modelId, complexity) {
10000
10085
  const { input, output } = this.estimateTokens(task, complexity);
10001
- const { pricing, source } = this.priceFor(modelId);
10002
- const breakdown = priceTokens({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, pricing);
10086
+ const { pricing, source } = this.tiersFor(modelId);
10087
+ const breakdown = priceUsage({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, pricing);
10003
10088
  return {
10004
10089
  taskId: task.id,
10005
10090
  taskName: task.name,
@@ -10208,6 +10293,13 @@ ${lines.join(`
10208
10293
  }
10209
10294
 
10210
10295
  // src/orchestrator.ts
10296
+ class TaskTimeoutError extends Error {
10297
+ constructor(message = "Task timed out") {
10298
+ super(message);
10299
+ this.name = "TaskTimeoutError";
10300
+ }
10301
+ }
10302
+ var DELTA_READ_BACKOFF_MS = [1000, 2000, 4000];
10211
10303
  function assistantMessageText(message) {
10212
10304
  if (message.type !== "assistant")
10213
10305
  return "";
@@ -10229,6 +10321,9 @@ function sumBy(records, select) {
10229
10321
  total += select(record);
10230
10322
  return total;
10231
10323
  }
10324
+ function baseInputRate(cost) {
10325
+ return selectTier(cost.tiers, 0).rates.input;
10326
+ }
10232
10327
  var DEFAULT_ESCALATION = {
10233
10328
  maxRetries: 3,
10234
10329
  retryDelay: 1000,
@@ -10277,6 +10372,11 @@ class NexusOrchestrator {
10277
10372
  tokensByModel = new Map;
10278
10373
  costProvenance = new Map;
10279
10374
  cleanupInterval = null;
10375
+ deltaLedgers = new Map;
10376
+ uncollected = new Map;
10377
+ deltaTimers = new Set;
10378
+ shuttingDown = false;
10379
+ deltaReadBackoffMs = DELTA_READ_BACKOFF_MS;
10280
10380
  get healthMonitor() {
10281
10381
  return this._healthMonitor;
10282
10382
  }
@@ -10337,23 +10437,40 @@ class NexusOrchestrator {
10337
10437
  for (const model of data) {
10338
10438
  if (!model?.cost || !Array.isArray(model.cost) || model.cost.length === 0)
10339
10439
  continue;
10340
- const baseCost = model.cost[0];
10341
10440
  this.modelCosts.set(`${model.providerID}/${model.id}`, {
10342
- input: per1k(baseCost.input),
10343
- output: per1k(baseCost.output),
10344
- cacheRead: per1k(baseCost.cache?.read),
10345
- cacheWrite: per1k(baseCost.cache?.write)
10441
+ tiers: this.normaliseTiers(model.cost, per1k)
10346
10442
  });
10347
10443
  }
10348
10444
  } catch {}
10349
10445
  }
10446
+ normaliseTiers(cost, per1k) {
10447
+ const rates = (row) => ({
10448
+ input: per1k(row.input),
10449
+ output: per1k(row.output),
10450
+ cacheRead: per1k(row.cache?.read),
10451
+ cacheWrite: per1k(row.cache?.write)
10452
+ });
10453
+ const kept = cost.filter((row) => row?.tier === undefined || row.tier?.type === "context").map((row) => ({
10454
+ ...row.tier?.type === "context" && typeof row.tier.size === "number" ? { threshold: row.tier.size } : {},
10455
+ rates: rates(row)
10456
+ }));
10457
+ if (kept.length === 0)
10458
+ return [{ rates: rates(cost[0]) }];
10459
+ const base = kept.find((t) => t.threshold === undefined);
10460
+ const contextual = kept.filter((t) => t.threshold !== undefined).sort((a, b) => a.threshold - b.threshold);
10461
+ return base ? [{ rates: { ...base.rates } }, ...contextual] : [{ rates: { ...kept[0].rates } }, ...contextual];
10462
+ }
10350
10463
  setModelCosts(costs) {
10351
10464
  for (const [model, cost] of Object.entries(costs)) {
10352
10465
  this.modelCosts.set(model, {
10353
- input: cost.input,
10354
- output: cost.output,
10355
- cacheRead: cost.cacheRead || 0,
10356
- cacheWrite: cost.cacheWrite || 0
10466
+ tiers: [{
10467
+ rates: {
10468
+ input: cost.input,
10469
+ output: cost.output,
10470
+ cacheRead: cost.cacheRead || 0,
10471
+ cacheWrite: cost.cacheWrite || 0
10472
+ }
10473
+ }]
10357
10474
  });
10358
10475
  }
10359
10476
  }
@@ -10471,10 +10588,14 @@ class NexusOrchestrator {
10471
10588
  enabled: true,
10472
10589
  patternStorage: "memory",
10473
10590
  minConfidence: 0.7
10591
+ },
10592
+ cost: {
10593
+ timeoutDeltaGraceMs: 60000
10474
10594
  }
10475
10595
  };
10596
+ const { cost: defaultCost, ...restDefaults } = defaults;
10476
10597
  return {
10477
- ...defaults,
10598
+ ...restDefaults,
10478
10599
  ...partial,
10479
10600
  budget: { ...defaults.budget, ...partial?.budget },
10480
10601
  agents: { ...defaults.agents, ...partial?.agents },
@@ -10483,7 +10604,8 @@ class NexusOrchestrator {
10483
10604
  memory: { ...defaults.memory, ...partial?.memory },
10484
10605
  dashboard: { ...defaults.dashboard, ...partial?.dashboard },
10485
10606
  security: { ...defaults.security, ...partial?.security },
10486
- learning: { ...defaults.learning, ...partial?.learning }
10607
+ learning: { ...defaults.learning, ...partial?.learning },
10608
+ ...partial?.cost ? { cost: { ...defaultCost, ...partial.cost } } : {}
10487
10609
  };
10488
10610
  }
10489
10611
  notifyStateChange() {
@@ -10537,6 +10659,7 @@ class NexusOrchestrator {
10537
10659
  success: true,
10538
10660
  tasks: results,
10539
10661
  totalCost: this.totalSpent,
10662
+ ...this.spendSplit(),
10540
10663
  totalDuration,
10541
10664
  agentsUsed: this.agents.size
10542
10665
  };
@@ -10546,6 +10669,7 @@ class NexusOrchestrator {
10546
10669
  success: false,
10547
10670
  tasks: [],
10548
10671
  totalCost: this.totalSpent,
10672
+ ...this.spendSplit(),
10549
10673
  totalDuration: Date.now() - startTime,
10550
10674
  agentsUsed: this.agents.size
10551
10675
  };
@@ -10695,6 +10819,9 @@ class NexusOrchestrator {
10695
10819
  }
10696
10820
  const startTime = Date.now();
10697
10821
  const timeout = node.task.timeout || this.config.defaultTimeout;
10822
+ let timeoutTimer = null;
10823
+ let waitGuard = null;
10824
+ let waitAbort = null;
10698
10825
  try {
10699
10826
  const rolePrompt = this.buildRolePrompt(node.task.requiredRole);
10700
10827
  let taskPrompt = `${rolePrompt}
@@ -10726,8 +10853,12 @@ Please continue from where the previous agent left off.`;
10726
10853
  sessionID: agent.sessionID,
10727
10854
  text: taskPrompt
10728
10855
  });
10729
- const waitPromise = this.ctx.session.wait({ sessionID: agent.sessionID });
10730
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Task timed out")), timeout));
10856
+ waitAbort = new AbortController;
10857
+ const waitPromise = this.ctx.session.wait({ sessionID: agent.sessionID }, { signal: waitAbort.signal });
10858
+ waitGuard = waitPromise.then(() => ({ outcome: "idle" }), () => ({ outcome: "poll-failed" }));
10859
+ const timeoutPromise = new Promise((_, reject) => {
10860
+ timeoutTimer = setTimeout(() => reject(new TaskTimeoutError), timeout);
10861
+ });
10731
10862
  await Promise.race([waitPromise, timeoutPromise]);
10732
10863
  const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
10733
10864
  const output = lastAssistantText(messages) || "Task completed (no output captured)";
@@ -10753,6 +10884,7 @@ Please continue from where the previous agent left off.`;
10753
10884
  success: result.success,
10754
10885
  duration: result.duration,
10755
10886
  cost: result.cost,
10887
+ costProvenance: result.costProvenance,
10756
10888
  tokensUsed: result.tokensUsed
10757
10889
  });
10758
10890
  this.executionHistory.record({
@@ -10762,6 +10894,7 @@ Please continue from where the previous agent left off.`;
10762
10894
  model: agent.model.model,
10763
10895
  status: "success",
10764
10896
  cost: result.cost,
10897
+ costProvenance: result.costProvenance,
10765
10898
  duration: result.duration,
10766
10899
  tokensUsed: result.tokensUsed,
10767
10900
  startedAt: new Date(startTime),
@@ -10815,27 +10948,41 @@ Please continue from where the previous agent left off.`;
10815
10948
  duration,
10816
10949
  sessionID: agent.sessionID
10817
10950
  });
10818
- this.performanceTracker.record({
10951
+ const performanceId = this.performanceTracker.record({
10819
10952
  model: agent.model.model,
10820
10953
  role: node.task.requiredRole,
10821
10954
  success: result.success,
10822
10955
  duration: result.duration,
10823
10956
  cost: result.cost,
10957
+ costProvenance: result.costProvenance,
10824
10958
  tokensUsed: result.tokensUsed
10825
10959
  });
10826
- this.executionHistory.record({
10960
+ const historyId = this.executionHistory.record({
10827
10961
  taskId: node.id,
10828
10962
  taskName: node.task.name,
10829
10963
  role: node.task.requiredRole,
10830
10964
  model: agent.model.model,
10831
10965
  status: "failed",
10832
10966
  cost: result.cost,
10967
+ costProvenance: result.costProvenance,
10833
10968
  duration: result.duration,
10834
10969
  tokensUsed: result.tokensUsed,
10835
10970
  startedAt: new Date(startTime),
10836
10971
  completedAt: new Date,
10837
10972
  error: errorMessage
10838
- });
10973
+ }).id;
10974
+ if (error instanceof TaskTimeoutError && cost.usage && waitGuard && agent.sessionID) {
10975
+ this.armTimeoutDelta(agent, node, {
10976
+ usage: cost.usage,
10977
+ idleAt: cost.idleAt,
10978
+ result,
10979
+ waitGuard,
10980
+ abort: waitAbort,
10981
+ historyId,
10982
+ performanceId,
10983
+ timeout
10984
+ });
10985
+ }
10839
10986
  if (this.notifications?.isEnabled()) {
10840
10987
  this.notifications.notify({ title: "Nexus: Task Failed", body: `${node.task.name} failed: ${errorMessage}`, sound: true });
10841
10988
  }
@@ -10843,6 +10990,10 @@ Please continue from where the previous agent left off.`;
10843
10990
  await this.handleFailure(agent, node, new Error(errorMessage));
10844
10991
  }
10845
10992
  } finally {
10993
+ if (timeoutTimer) {
10994
+ clearTimeout(timeoutTimer);
10995
+ timeoutTimer = null;
10996
+ }
10846
10997
  if (agent.status !== "terminated" && agent.status !== "failed") {
10847
10998
  agent.status = "idle";
10848
10999
  }
@@ -10850,6 +11001,200 @@ Please continue from where the previous agent left off.`;
10850
11001
  this.notifyStateChange();
10851
11002
  }
10852
11003
  }
11004
+ armTimeoutDelta(agent, node, args) {
11005
+ const sessionID = agent.sessionID;
11006
+ if (!sessionID)
11007
+ return;
11008
+ const model = `${agent.model.provider}/${agent.model.model}`;
11009
+ if (this.shuttingDown)
11010
+ return;
11011
+ if (this.deltaLedgers.has(sessionID))
11012
+ return;
11013
+ const ledger = {
11014
+ sessionID,
11015
+ taskId: node.id,
11016
+ agentId: agent.id,
11017
+ agent,
11018
+ node,
11019
+ result: args.result,
11020
+ model,
11021
+ provider: agent.model.provider,
11022
+ charged: args.usage,
11023
+ idleAtTimeout: args.idleAt,
11024
+ pending: false,
11025
+ historyId: args.historyId,
11026
+ performanceId: args.performanceId,
11027
+ abort: args.abort ?? new AbortController
11028
+ };
11029
+ this.deltaLedgers.set(sessionID, ledger);
11030
+ const graceMs = this.config.cost?.timeoutDeltaGraceMs ?? Math.min(180000, Math.max(30000, args.timeout / 2));
11031
+ const timer = setTimeout(() => {
11032
+ this.deltaTimers.delete(timer);
11033
+ this.settleTimeoutDelta(ledger, "deadline").catch(() => {});
11034
+ }, graceMs);
11035
+ timer.unref?.();
11036
+ this.deltaTimers.add(timer);
11037
+ (async () => {
11038
+ try {
11039
+ const { outcome } = await args.waitGuard;
11040
+ if (outcome === "idle")
11041
+ await this.settleTimeoutDelta(ledger, "session-idle");
11042
+ else
11043
+ await this.settleTimeoutDelta(ledger, "poll-failed");
11044
+ } catch {}
11045
+ })();
11046
+ }
11047
+ async settleTimeoutDelta(ledger, trigger) {
11048
+ if (ledger.pending)
11049
+ return;
11050
+ if (this.deltaLedgers.get(ledger.sessionID) !== ledger)
11051
+ return;
11052
+ ledger.pending = true;
11053
+ try {
11054
+ let reason = trigger === "shutdown" ? "shutdown" : trigger === "session-idle" ? "session-idle" : "abandoned";
11055
+ let now;
11056
+ if (trigger === "deadline" || trigger === "poll-failed") {
11057
+ const probe = await this.readSessionTokens(ledger.sessionID);
11058
+ if (!probe.read || probe.idleAt <= ledger.idleAtTimeout) {
11059
+ this.abandonTimeoutDelta(ledger, probe.read ? probe.usage : undefined, reason);
11060
+ return;
11061
+ }
11062
+ now = probe.usage;
11063
+ reason = "session-idle";
11064
+ } else {
11065
+ const read = trigger === "shutdown" ? await this.readSessionTokens(ledger.sessionID) : await this.readSessionTokensWithRetry(ledger.sessionID);
11066
+ if (!read.read) {
11067
+ this.abandonTimeoutDelta(ledger, undefined, reason);
11068
+ return;
11069
+ }
11070
+ now = read.usage;
11071
+ }
11072
+ const delta = {
11073
+ input: Math.max(0, now.input - ledger.charged.input),
11074
+ output: Math.max(0, now.output - ledger.charged.output),
11075
+ reasoning: Math.max(0, now.reasoning - ledger.charged.reasoning),
11076
+ cache: {
11077
+ read: Math.max(0, now.cache.read - ledger.charged.cache.read),
11078
+ write: Math.max(0, now.cache.write - ledger.charged.cache.write)
11079
+ }
11080
+ };
11081
+ const deltaTokens = totalTokens(delta);
11082
+ if (deltaTokens === 0) {
11083
+ this.emit("cost:delta", {
11084
+ taskId: ledger.taskId,
11085
+ nodeId: ledger.taskId,
11086
+ agentId: ledger.agentId,
11087
+ sessionID: ledger.sessionID,
11088
+ model: ledger.model,
11089
+ deltaCost: 0,
11090
+ deltaTokens: 0,
11091
+ sessionTotalCost: priceUsage(ledger.charged, this.forecaster.tiersFor(ledger.model, ledger.provider).pricing).total,
11092
+ reason,
11093
+ settledTier: this.settledTierOf(ledger, ledger.charged)
11094
+ });
11095
+ return;
11096
+ }
11097
+ const { pricing, source } = this.forecaster.tiersFor(ledger.model, ledger.provider);
11098
+ const deltaCost = priceUsageAtSettledTier(delta, pricing, now).total;
11099
+ const provenance = { usage: "measured", pricing: source };
11100
+ ledger.charged = now;
11101
+ let historyAdjusted = false;
11102
+ let performanceAdjusted = false;
11103
+ let nodeAdjusted = false;
11104
+ let agentAdjusted = false;
11105
+ let error;
11106
+ try {
11107
+ this.trackCost(ledger.agentId, ledger.model, deltaCost, deltaTokens, provenance);
11108
+ historyAdjusted = this.executionHistory.adjust(ledger.historyId, {
11109
+ cost: deltaCost,
11110
+ tokensUsed: deltaTokens
11111
+ });
11112
+ performanceAdjusted = this.performanceTracker.adjust(ledger.performanceId, { cost: deltaCost });
11113
+ ledger.agent.metrics.totalCost += deltaCost;
11114
+ ledger.agent.metrics.totalTokens += deltaTokens;
11115
+ agentAdjusted = true;
11116
+ if (ledger.node.result === ledger.result) {
11117
+ ledger.result.cost += deltaCost;
11118
+ ledger.result.tokensUsed += deltaTokens;
11119
+ nodeAdjusted = true;
11120
+ }
11121
+ this.uncollected.delete(ledger.sessionID);
11122
+ } catch (thrown) {
11123
+ error = thrown instanceof Error ? thrown.message : String(thrown);
11124
+ }
11125
+ this.emit("cost:delta", {
11126
+ taskId: ledger.taskId,
11127
+ nodeId: ledger.taskId,
11128
+ agentId: ledger.agentId,
11129
+ sessionID: ledger.sessionID,
11130
+ model: ledger.model,
11131
+ deltaCost,
11132
+ deltaTokens,
11133
+ sessionTotalCost: priceUsage(now, pricing).total,
11134
+ reason,
11135
+ settledTier: this.settledTierOf(ledger, now),
11136
+ recordsAdjusted: { history: historyAdjusted, performance: performanceAdjusted, node: nodeAdjusted, agent: agentAdjusted },
11137
+ ...error === undefined ? {} : { error }
11138
+ });
11139
+ } finally {
11140
+ this.deltaLedgers.delete(ledger.sessionID);
11141
+ ledger.pending = false;
11142
+ ledger.abort.abort();
11143
+ }
11144
+ }
11145
+ abandonTimeoutDelta(ledger, probeUsage, reason) {
11146
+ const eventReason = reason === "shutdown" ? "shutdown" : "abandoned";
11147
+ const known = probeUsage ?? ledger.charged;
11148
+ const { pricing } = this.forecaster.tiersFor(ledger.model, ledger.provider);
11149
+ const observedIncrement = {
11150
+ input: Math.max(0, known.input - ledger.charged.input),
11151
+ output: Math.max(0, known.output - ledger.charged.output),
11152
+ reasoning: Math.max(0, known.reasoning - ledger.charged.reasoning),
11153
+ cache: {
11154
+ read: Math.max(0, known.cache.read - ledger.charged.cache.read),
11155
+ write: Math.max(0, known.cache.write - ledger.charged.cache.write)
11156
+ }
11157
+ };
11158
+ const observedUncollected = totalTokens(observedIncrement) > 0 ? priceUsageAtSettledTier(observedIncrement, pricing, known).total : 0;
11159
+ this.uncollected.set(ledger.sessionID, {
11160
+ sessionID: ledger.sessionID,
11161
+ taskId: ledger.taskId,
11162
+ agentId: ledger.agentId,
11163
+ model: ledger.model,
11164
+ lastKnownTokens: totalTokens(known),
11165
+ observedUncollected
11166
+ });
11167
+ this.emit("cost:delta", {
11168
+ taskId: ledger.taskId,
11169
+ nodeId: ledger.taskId,
11170
+ agentId: ledger.agentId,
11171
+ sessionID: ledger.sessionID,
11172
+ model: ledger.model,
11173
+ deltaCost: 0,
11174
+ deltaTokens: 0,
11175
+ sessionTotalCost: priceUsage(known, pricing).total,
11176
+ reason: eventReason,
11177
+ settledTier: this.settledTierOf(ledger, known),
11178
+ uncollected: { lastKnownTokens: totalTokens(known), observedUncollected }
11179
+ });
11180
+ }
11181
+ settledTierOf(ledger, usage) {
11182
+ const { pricing, source } = this.forecaster.tiersFor(ledger.model, ledger.provider);
11183
+ const promptSize = promptSizeOf(usage);
11184
+ return {
11185
+ pricing: source,
11186
+ promptSizeAtSettlement: promptSize,
11187
+ threshold: selectTier(pricing.tiers, promptSize).threshold ?? null
11188
+ };
11189
+ }
11190
+ async flushTimeoutDeltas() {
11191
+ for (const timer of this.deltaTimers)
11192
+ clearTimeout(timer);
11193
+ this.deltaTimers.clear();
11194
+ for (const ledger of [...this.deltaLedgers.values()]) {
11195
+ await this.settleTimeoutDelta(ledger, "shutdown").catch(() => {});
11196
+ }
11197
+ }
10853
11198
  buildRolePrompt(role) {
10854
11199
  if (this.customRoles.has(role)) {
10855
11200
  return this.customRoles.getPrompt(role) || `You are a ${role}. Complete the assigned task professionally.`;
@@ -10915,8 +11260,17 @@ Please continue from where the previous agent left off.`;
10915
11260
  await this.spawnAndExecute(node, { transferContext: context });
10916
11261
  return;
10917
11262
  }
10918
- if (policy.fallbackModels.length > 0) {
10919
- const fallbackModel = policy.fallbackModels.shift();
11263
+ const failedRef = `${agent.model.provider}/${agent.model.model}`;
11264
+ const hasUsableFallback = policy.fallbackModels.some((m) => m !== failedRef);
11265
+ if (policy.fallbackModels.length > 0 && hasUsableFallback) {
11266
+ let fallbackModel;
11267
+ while (policy.fallbackModels.length > 0) {
11268
+ const candidate = policy.fallbackModels.shift();
11269
+ if (candidate !== failedRef) {
11270
+ fallbackModel = candidate;
11271
+ break;
11272
+ }
11273
+ }
10920
11274
  if (fallbackModel) {
10921
11275
  await this.terminateAgent(agent.id);
10922
11276
  node.status = "pending";
@@ -11160,27 +11514,11 @@ Please continue from where the previous agent left off.`;
11160
11514
  for (const [key, cost] of this.modelCosts) {
11161
11515
  if (bareModelId(key) !== bare)
11162
11516
  continue;
11163
- if (!best || cost.input < best.input)
11517
+ if (!best || baseInputRate(cost) < baseInputRate(best))
11164
11518
  best = cost;
11165
11519
  }
11166
11520
  return best;
11167
11521
  }
11168
- estimateModelCost(model, provider) {
11169
- const realCost = this.getModelCost(model, provider);
11170
- if (realCost) {
11171
- return (realCost.input + realCost.output) / 2;
11172
- }
11173
- const costs = {
11174
- "claude-sonnet-4-6": 0.15,
11175
- "claude-opus-4-7": 15,
11176
- "claude-haiku-4-5": 0.8,
11177
- "gpt-5-mini": 0.05,
11178
- "gpt-5": 2.5,
11179
- "gemini-2.5-flash": 0.075,
11180
- "minimax-m2.5-free": 0
11181
- };
11182
- return costs[model] || 0.1;
11183
- }
11184
11522
  estimateModelQuality(model) {
11185
11523
  const quality = {
11186
11524
  "claude-opus-4-7": 0.95,
@@ -11205,13 +11543,17 @@ Please continue from where the previous agent left off.`;
11205
11543
  };
11206
11544
  return speeds[model] || 0.5;
11207
11545
  }
11208
- scoreModel(modelId, role, complexity) {
11546
+ scoreModel(modelId, role, complexity, estimates) {
11209
11547
  const [provider, ...parts] = modelId.split("/");
11210
11548
  const model = parts.join("/");
11211
- const cost = this.estimateModelCost(model, provider);
11549
+ const estimate = estimates?.get(modelId) ?? this.forecaster.estimateCost(complexity, model, provider);
11550
+ let maxEstimate = 0;
11551
+ for (const value of estimates?.values() ?? []) {
11552
+ if (value > maxEstimate)
11553
+ maxEstimate = value;
11554
+ }
11555
+ const costScore = maxEstimate === 0 ? 1 : 1 - estimate / maxEstimate;
11212
11556
  const quality = this.estimateModelQuality(model);
11213
- const maxCost = 15;
11214
- const costScore = 1 - cost / maxCost;
11215
11557
  const speedScore = this.estimateModelSpeed(model);
11216
11558
  const qualityWeight = complexity.overall > 70 ? 0.6 : complexity.overall > 40 ? 0.4 : 0.2;
11217
11559
  const costWeight = 1 - qualityWeight;
@@ -11223,7 +11565,7 @@ Please continue from where the previous agent left off.`;
11223
11565
  qualityScore: quality,
11224
11566
  speedScore,
11225
11567
  overallScore,
11226
- reasoning: `Score: ${overallScore.toFixed(2)} (quality: ${quality.toFixed(2)}, cost: ${costScore.toFixed(2)}, speed: ${speedScore.toFixed(2)})`
11568
+ reasoning: `Score: ${overallScore.toFixed(2)} (quality: ${quality.toFixed(2)}, cost: ${costScore.toFixed(2)} [~$${estimate.toFixed(4)}/task], speed: ${speedScore.toFixed(2)})`
11227
11569
  };
11228
11570
  }
11229
11571
  selectBestModel(role, complexity) {
@@ -11237,19 +11579,24 @@ Please continue from where the previous agent left off.`;
11237
11579
  "opencode/minimax-m2.5-free"
11238
11580
  ];
11239
11581
  const unique = [...new Set(candidates)];
11240
- const scored = unique.map((m) => this.scoreModel(m, role, complexity));
11582
+ const estimates = new Map;
11583
+ for (const ref of unique) {
11584
+ const [provider, ...parts] = ref.split("/");
11585
+ estimates.set(ref, this.forecaster.estimateCost(complexity, parts.join("/"), provider));
11586
+ }
11587
+ const scored = unique.map((ref) => ({ ref, score: this.scoreModel(ref, role, complexity, estimates) }));
11241
11588
  const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
11242
- const affordable = scored.filter((s) => {
11243
- const estimate = this.forecaster.estimateCost(complexity, s.model, s.provider);
11589
+ const affordable = scored.filter(({ ref }) => {
11590
+ const estimate = estimates.get(ref) ?? 0;
11244
11591
  return estimate <= budgetRemaining || estimate === 0;
11245
11592
  });
11246
- const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.overallScore - a.overallScore)[0];
11593
+ const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.score.overallScore - a.score.overallScore)[0];
11247
11594
  return {
11248
- provider: best.provider,
11249
- model: best.model,
11250
- estimatedCost: this.forecaster.estimateCost(complexity, best.model, best.provider),
11251
- estimatedQuality: best.qualityScore,
11252
- reasoning: best.reasoning
11595
+ provider: best.score.provider,
11596
+ model: best.score.model,
11597
+ estimatedCost: estimates.get(best.ref) ?? 0,
11598
+ estimatedQuality: best.score.qualityScore,
11599
+ reasoning: best.score.reasoning
11253
11600
  };
11254
11601
  }
11255
11602
  trackCost(agentId, model, cost, tokens, provenance) {
@@ -11285,11 +11632,19 @@ Please continue from where the previous agent left off.`;
11285
11632
  }
11286
11633
  this.costProvenance.set(model, prior);
11287
11634
  }
11635
+ spendSplit() {
11636
+ return {
11637
+ measuredSpend: sumBy(this.costProvenance, (p) => p.measuredSpend),
11638
+ estimatedSpend: sumBy(this.costProvenance, (p) => p.estimatedSpend)
11639
+ };
11640
+ }
11288
11641
  safeAccountTaskCost(agent, task) {
11289
11642
  return this.accountTaskCost(agent, task).catch(() => ({
11290
11643
  cost: 0,
11291
11644
  tokensUsed: 0,
11292
- provenance: { usage: "estimated", pricing: "unknown-model" }
11645
+ provenance: { usage: "estimated", pricing: "unknown-model" },
11646
+ usage: null,
11647
+ idleAt: 0
11293
11648
  }));
11294
11649
  }
11295
11650
  async accountTaskCost(agent, task) {
@@ -11300,14 +11655,18 @@ Please continue from where the previous agent left off.`;
11300
11655
  return {
11301
11656
  cost: measured.cost,
11302
11657
  tokensUsed: measured.tokens,
11303
- provenance: { usage: "measured", pricing: measured.pricingSource }
11658
+ provenance: { usage: "measured", pricing: measured.pricingSource },
11659
+ usage: read.usage,
11660
+ idleAt: read.idleAt
11304
11661
  };
11305
11662
  }
11306
11663
  const predicted = this.forecaster.forecastTask(task, task.requiredRole, model, task.complexity);
11307
11664
  return {
11308
11665
  cost: predicted.estimatedCost,
11309
11666
  tokensUsed: predicted.estimatedInputTokens + predicted.estimatedOutputTokens,
11310
- provenance: { usage: "estimated", pricing: predicted.pricingSource }
11667
+ provenance: { usage: "estimated", pricing: predicted.pricingSource },
11668
+ usage: null,
11669
+ idleAt: 0
11311
11670
  };
11312
11671
  }
11313
11672
  async readSessionTokens(sessionID) {
@@ -11319,6 +11678,7 @@ Please continue from where the previous agent left off.`;
11319
11678
  return { read: false };
11320
11679
  return {
11321
11680
  read: true,
11681
+ idleAt: typeof session?.time?.idle === "number" && Number.isFinite(session.time.idle) ? session.time.idle : 0,
11322
11682
  usage: {
11323
11683
  input: finite(tokens.input),
11324
11684
  output: finite(tokens.output),
@@ -11330,6 +11690,16 @@ Please continue from where the previous agent left off.`;
11330
11690
  return { read: false };
11331
11691
  }
11332
11692
  }
11693
+ async readSessionTokensWithRetry(sessionID) {
11694
+ for (let attempt = 0;attempt < this.deltaReadBackoffMs.length; attempt++) {
11695
+ if (attempt > 0)
11696
+ await this.sleep(this.deltaReadBackoffMs[attempt] ?? 0);
11697
+ const read = await this.readSessionTokens(sessionID);
11698
+ if (read.read)
11699
+ return read;
11700
+ }
11701
+ return { read: false };
11702
+ }
11333
11703
  checkBudget() {
11334
11704
  const remaining = this.budget.maxTotalCost - this.totalSpent;
11335
11705
  const remainingPercent = remaining / this.budget.maxTotalCost;
@@ -11396,8 +11766,9 @@ Please continue from where the previous agent left off.`;
11396
11766
  global: loadInfo?.global ?? null,
11397
11767
  models: this.configManager.getResolvedModels()
11398
11768
  };
11769
+ const spend = this.spendSplit();
11399
11770
  if (detailed)
11400
- return JSON.stringify({ ...state, budgetExceeded: this.budgetExceeded, config }, null, 2);
11771
+ return JSON.stringify({ ...state, ...spend, budgetExceeded: this.budgetExceeded, config }, null, 2);
11401
11772
  return JSON.stringify({
11402
11773
  running: state.running,
11403
11774
  paused: state.paused,
@@ -11405,6 +11776,7 @@ Please continue from where the previous agent left off.`;
11405
11776
  agents: state.agents.length,
11406
11777
  tasks: state.tasks.length,
11407
11778
  totalCost: state.totalSpent,
11779
+ ...spend,
11408
11780
  budgetRemaining: state.budgetRemaining,
11409
11781
  config
11410
11782
  }, null, 2);
@@ -11419,14 +11791,25 @@ Please continue from where the previous agent left off.`;
11419
11791
  return JSON.stringify({
11420
11792
  totalSpent: state.totalSpent,
11421
11793
  budgetRemaining: state.budgetRemaining,
11794
+ ...this.spendSplit(),
11422
11795
  byAgent: Object.fromEntries(this.costByAgent),
11423
11796
  byModel: Object.fromEntries(this.costByModel),
11424
11797
  tokensByModel: Object.fromEntries(this.tokensByModel),
11425
11798
  provenance: Object.fromEntries(this.costProvenance),
11426
11799
  measuredEntries: sumBy(this.costProvenance, (p) => p.measuredEntries),
11427
- estimatedEntries: sumBy(this.costProvenance, (p) => p.estimatedEntries)
11800
+ estimatedEntries: sumBy(this.costProvenance, (p) => p.estimatedEntries),
11801
+ uncollected: this.uncollectedSummary()
11428
11802
  }, null, 2);
11429
11803
  }
11804
+ uncollectedSummary() {
11805
+ const all = [...this.uncollected.values()];
11806
+ return {
11807
+ sessions: all.length,
11808
+ lastKnownTokens: all.reduce((sum, u) => sum + u.lastKnownTokens, 0),
11809
+ observedUncollected: all.reduce((sum, u) => sum + u.observedUncollected, 0),
11810
+ taskIds: all.map((u) => u.taskId)
11811
+ };
11812
+ }
11430
11813
  pause() {
11431
11814
  this.paused = true;
11432
11815
  this.emit("orchestrator:paused", {});
@@ -11444,6 +11827,7 @@ Please continue from where the previous agent left off.`;
11444
11827
  this.budgetExceeded = false;
11445
11828
  }
11446
11829
  async shutdown() {
11830
+ this.shuttingDown = true;
11447
11831
  await this.moduleRegistry.teardownAll();
11448
11832
  this._healthMonitor?.stop();
11449
11833
  this.stopDashboard();
@@ -11456,6 +11840,7 @@ Please continue from where the previous agent left off.`;
11456
11840
  clearTimeout(this.stateChangeTimer);
11457
11841
  this.stateChangeTimer = null;
11458
11842
  }
11843
+ await this.flushTimeoutDeltas();
11459
11844
  this.agents.forEach((agent) => {
11460
11845
  agent.status = "terminated";
11461
11846
  });
@@ -11821,8 +12206,18 @@ class AstGrep {
11821
12206
 
11822
12207
  // src/index.ts
11823
12208
  import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3, statSync } from "node:fs";
12209
+ import { createHash } from "node:crypto";
11824
12210
  import { join as join6, resolve as resolve3, basename, dirname as dirname4 } from "node:path";
11825
12211
  import { homedir as homedir2 } from "node:os";
12212
+ function renderCost(cost, provenance) {
12213
+ return provenance.usage === "measured" ? `$${cost.toFixed(4)} measured` : `$${cost.toFixed(4)} estimated`;
12214
+ }
12215
+ function renderCostBasis(score) {
12216
+ if (score.costBasis === "none") {
12217
+ return "cost unscored: 0 measured tasks, 0% of score from cost";
12218
+ }
12219
+ return `avg=$${score.avgCost.toFixed(4)} (${score.measuredTasks}/${score.totalTasks} measured)`;
12220
+ }
11826
12221
  var CONFIG_RELOAD_DEBOUNCE_MS = 150;
11827
12222
  var CONFIG_POLL_INTERVAL_MS = 2000;
11828
12223
  function isNexusConfigFile(file, eventDirectory, projectDirectory, projectPath, globalPath) {
@@ -11837,6 +12232,8 @@ var watchedContexts = new WeakSet;
11837
12232
  var MAX_RELOAD_WAIT_MS = 1000;
11838
12233
  var MIN_INTERVAL_MS = 25;
11839
12234
  var clampInterval = (ms, fallback) => Number.isFinite(ms) && ms >= MIN_INTERVAL_MS ? ms : fallback;
12235
+ var ABSENT = "absent";
12236
+ var UNREADABLE = "unreadable";
11840
12237
  function watchConfigFiles(ctx, orchestrator, debounceMs = CONFIG_RELOAD_DEBOUNCE_MS, pollMs = CONFIG_POLL_INTERVAL_MS) {
11841
12238
  if (watchedContexts.has(ctx))
11842
12239
  return () => {};
@@ -11856,15 +12253,24 @@ function watchConfigFiles(ctx, orchestrator, debounceMs = CONFIG_RELOAD_DEBOUNCE
11856
12253
  clearTimeout(debounceTimer);
11857
12254
  debounceTimer = null;
11858
12255
  };
12256
+ const digestOf = (filePath) => {
12257
+ try {
12258
+ return createHash("sha1").update(readFileSync3(filePath)).digest("hex");
12259
+ } catch {
12260
+ return UNREADABLE;
12261
+ }
12262
+ };
11859
12263
  const stampOf = (filePath) => {
12264
+ let stat;
11860
12265
  try {
11861
12266
  const stats = statSync(filePath);
11862
12267
  if (!stats.isFile())
11863
- return "absent";
11864
- return `${stats.mtimeMs}:${stats.size}`;
12268
+ return ABSENT;
12269
+ stat = `${stats.mtimeMs}:${stats.size}`;
11865
12270
  } catch {
11866
- return "absent";
12271
+ return ABSENT;
11867
12272
  }
12273
+ return `${stat}|${digestOf(filePath)}`;
11868
12274
  };
11869
12275
  const lastSeen = new Map(watchedPaths.map((p) => [p, stampOf(p)]));
11870
12276
  const runReload = (trigger) => {
@@ -12113,6 +12519,19 @@ Before marking a task complete:
12113
12519
  4. Follows project conventions
12114
12520
  5. Has appropriate test coverage
12115
12521
  `;
12522
+ function clearPresetOverride(orchestrator) {
12523
+ const configManager = orchestrator.configManager;
12524
+ const hadOverride = configManager.resetToDefaults();
12525
+ const models = configManager.getResolvedModels();
12526
+ const resolved = Object.entries(models).map(([role, model]) => `${role}=${model}`).join(" ");
12527
+ return {
12528
+ content: hadOverride ? `Cleared the session preset override. nexus.jsonc is in control again — no config file was modified.
12529
+ ` + `Resolved models now: ${resolved}
12530
+ ` + `sessionOverride: false` : `No session preset override was set, so nothing was cleared — nexus.jsonc was already in control. No config file was modified.
12531
+ ` + `Resolved models now: ${resolved}
12532
+ ` + `sessionOverride: false`
12533
+ };
12534
+ }
12116
12535
  var src_default = define({
12117
12536
  id: "nexus",
12118
12537
  async setup(ctx) {
@@ -12594,23 +13013,35 @@ You are a technical writer who creates documentation that developers actually wa
12594
13013
  });
12595
13014
  editor.add({
12596
13015
  name: "preset",
12597
- description: "Apply a preset configuration",
13016
+ description: "Apply a session preset (model/budget selection), or clear it with mode 'clear' to hand control back to nexus.jsonc. A preset shadows the config file's models, so call mode 'clear' if the user edited nexus.jsonc but no model changed.",
12598
13017
  input: {
12599
13018
  type: "object",
12600
13019
  properties: {
12601
- name: { type: "string", description: "Preset name (minimal, balanced, enterprise, cost-optimized)" }
13020
+ mode: {
13021
+ type: "string",
13022
+ enum: ["apply", "clear"],
13023
+ description: "'apply' (default) applies the named preset for this session. 'clear' drops the session preset override so nexus.jsonc is in control again — use it whenever edits to nexus.jsonc appear to have no effect."
13024
+ },
13025
+ name: {
13026
+ type: "string",
13027
+ description: "Preset name (minimal, balanced, enterprise, cost-optimized). Required for mode 'apply'; ignored by 'clear'."
13028
+ }
12602
13029
  },
12603
- required: ["name"],
12604
13030
  additionalProperties: false
12605
13031
  },
12606
13032
  options: { codemode: true },
12607
13033
  execute: async (input) => {
12608
- const { name } = input;
13034
+ const { mode, name } = input;
13035
+ if (mode === "clear")
13036
+ return clearPresetOverride(orchestrator);
13037
+ if (!name) {
13038
+ return { content: `Error: mode 'apply' needs a \`name\` (${orchestrator.configManager.listPresets().join(", ")}). To hand control back to nexus.jsonc instead, call preset with mode: 'clear'.` };
13039
+ }
12609
13040
  try {
12610
13041
  orchestrator.configManager.applyPreset(name);
12611
13042
  return {
12612
13043
  content: `Applied preset: ${PRESETS[name]?.name || name}
12613
- ` + `⚠️ This preset now overrides the \`models\` section of nexus.jsonc. ` + `Edits to models in the config file will NOT take effect until the preset is cleared. ` + `Clear it from the TUI (config manager) to hand control back to disk. ` + `Budget and self-healing values from the file still apply.`
13044
+ ` + `⚠️ This preset now overrides the \`models\` section of nexus.jsonc. ` + `Edits to models in the config file will NOT take effect until the preset is cleared. ` + `Clear it with this tool (mode: 'clear'), or from the TUI (config manager), to hand control back to disk. ` + `Budget and self-healing values from the file still apply.`
12614
13045
  };
12615
13046
  } catch (error) {
12616
13047
  return { content: `Error: ${error.message}` };
@@ -12665,6 +13096,8 @@ You are a technical writer who creates documentation that developers actually wa
12665
13096
  execute: async (input) => {
12666
13097
  const { model, setInput, setOutput } = input;
12667
13098
  const per1k = (v) => `$${v}/1K tokens`;
13099
+ const renderTiers = (cost) => cost.tiers.map((t) => t.threshold === undefined ? `base: in=${per1k(t.rates.input)}, out=${per1k(t.rates.output)}, cache_read=${per1k(t.rates.cacheRead)}, cache_write=${per1k(t.rates.cacheWrite)}` : `over ${t.threshold} prompt tokens: in=${per1k(t.rates.input)}, out=${per1k(t.rates.output)}, cache_read=${per1k(t.rates.cacheRead)}, cache_write=${per1k(t.rates.cacheWrite)}`).join(`
13100
+ `);
12668
13101
  if (model && setInput !== undefined && setOutput !== undefined) {
12669
13102
  orchestrator.setModelCosts({ [model]: { input: setInput, output: setOutput } });
12670
13103
  return { content: `Set ${model}: input=${per1k(setInput)}, output=${per1k(setOutput)}` };
@@ -12672,20 +13105,23 @@ You are a technical writer who creates documentation that developers actually wa
12672
13105
  if (model) {
12673
13106
  const cost = orchestrator.getModelCost(model);
12674
13107
  if (cost) {
12675
- return { content: `${model}: input=${per1k(cost.input)}, output=${per1k(cost.output)}, cache_read=${per1k(cost.cacheRead)}, cache_write=${per1k(cost.cacheWrite)}` };
13108
+ return { content: `${model} (real pricing, from OpenCode):
13109
+ ${renderTiers(cost)}` };
12676
13110
  }
12677
- const estimate = orchestrator["estimateModelCost"](model);
12678
- return { content: `${model}: no real pricing data (estimated $${estimate}/1K tokens)` };
13111
+ const { pricing, source } = orchestrator.forecaster.priceFor(model);
13112
+ const label = source === "model-costs" ? "real pricing" : source === "fallback-table" ? "ESTIMATE (fallback table, not provider pricing)" : "ESTIMATE (unknown model — no table knows this rate)";
13113
+ return { content: `${model}: no real pricing data.
13114
+ ${label}: in=${per1k(pricing.input)}, out=${per1k(pricing.output)}, cache_read=${per1k(pricing.cacheRead)}, cache_write=${per1k(pricing.cacheWrite)}` };
12679
13115
  }
12680
13116
  if (orchestrator.modelCosts.size > 0) {
12681
13117
  const lines = ["\uD83D\uDCCA Model Pricing (from OpenCode, per 1K tokens):"];
12682
13118
  for (const [id, cost] of orchestrator.modelCosts) {
12683
- lines.push(` ${id}: ${per1k(cost.input)} in, ${per1k(cost.output)} out`);
13119
+ lines.push(` ${id}: ${renderTiers(cost)}`);
12684
13120
  }
12685
13121
  return { content: lines.join(`
12686
13122
  `) };
12687
13123
  }
12688
- return { content: "No real pricing data loaded. Using hardcoded estimates." };
13124
+ return { content: "No real pricing data loaded. Using labelled fallback estimates." };
12689
13125
  }
12690
13126
  });
12691
13127
  editor.add({
@@ -12915,7 +13351,7 @@ You are a technical writer who creates documentation that developers actually wa
12915
13351
  const scores = orchestrator.performanceTracker.getScores();
12916
13352
  if (scores.length === 0)
12917
13353
  return { content: "No performance data yet. Scores build up as tasks are executed." };
12918
- const lines = scores.map((s) => `${s.role}/${s.model}: score=${s.overallScore.toFixed(1)} success=${(s.successRate * 100).toFixed(0)}% avg=$${s.avgCost.toFixed(4)} (${s.totalTasks} tasks)`);
13354
+ const lines = scores.map((s) => `${s.role}/${s.model}: score=${s.overallScore.toFixed(1)} success=${(s.successRate * 100).toFixed(0)}% ${renderCostBasis(s)} (${s.totalTasks} tasks)`);
12919
13355
  return { content: lines.join(`
12920
13356
  `) };
12921
13357
  }
@@ -12937,7 +13373,7 @@ You are a technical writer who creates documentation that developers actually wa
12937
13373
  const best = orchestrator.performanceTracker.getBestModel(role);
12938
13374
  if (!best)
12939
13375
  return { content: `No performance data for role '${role}' yet.` };
12940
- return { content: `Best for ${role}: ${best.model} (score: ${best.overallScore.toFixed(1)}, success: ${(best.successRate * 100).toFixed(0)}%, avg cost: $${best.avgCost.toFixed(4)})` };
13376
+ return { content: `Best for ${role}: ${best.model} (score: ${best.overallScore.toFixed(1)}, success: ${(best.successRate * 100).toFixed(0)}%, ${renderCostBasis(best)})` };
12941
13377
  }
12942
13378
  });
12943
13379
  editor.add({
@@ -12976,7 +13412,7 @@ You are a technical writer who creates documentation that developers actually wa
12976
13412
  const records = count ? orchestrator.executionHistory.getRecent(count) : orchestrator.executionHistory.getAll();
12977
13413
  if (records.length === 0)
12978
13414
  return { content: "No execution history yet." };
12979
- const lines = records.map((r) => `${r.status === "success" ? "✅" : "❌"} ${r.taskName} (${r.role}) — $${r.cost.toFixed(4)} — ${r.duration}ms`);
13415
+ const lines = records.map((r) => `${r.status === "success" ? "✅" : "❌"} ${r.taskName} (${r.role}) — ${renderCost(r.cost, r.costProvenance)} — ${r.duration}ms`);
12980
13416
  return { content: lines.join(`
12981
13417
  `) };
12982
13418
  }
@@ -12992,7 +13428,7 @@ You are a technical writer who creates documentation that developers actually wa
12992
13428
  options: { codemode: true },
12993
13429
  execute: async () => {
12994
13430
  const stats = orchestrator.executionHistory.getStats();
12995
- return { content: `Total: ${stats.total} | Success: ${(stats.successRate * 100).toFixed(1)}% | Cost: $${stats.totalCost.toFixed(4)} | Avg: ${stats.avgDuration.toFixed(0)}ms
13431
+ return { content: `Total: ${stats.total} | Success: ${(stats.successRate * 100).toFixed(1)}% | Cost: $${stats.totalCost.toFixed(4)} (measured $${stats.costSplit.measuredCost.toFixed(4)} over ${stats.costSplit.measuredEntries} tasks, estimated $${stats.costSplit.estimatedCost.toFixed(4)} over ${stats.costSplit.estimatedEntries} tasks) | Avg: ${stats.avgDuration.toFixed(0)}ms
12996
13432
  By role: ${JSON.stringify(stats.byRole)}` };
12997
13433
  }
12998
13434
  });
package/dist/tui.js CHANGED
@@ -94,7 +94,7 @@ function describeFile(file) {
94
94
  }
95
95
  function formatConfigLoadLog(info) {
96
96
  const models = Object.entries(info.models).map(([role, model]) => `${role}=${model}`).join(" ");
97
- const override = info.sessionOverride ? " (+session override: storage; disk edits to models are IGNORED while a preset is set \u2014 clear the preset in the TUI to hand control back to disk)" : "";
97
+ const override = info.sessionOverride ? ' (+session override: storage; disk edits to models are IGNORED while a preset is set \u2014 call the preset tool with mode "clear", or reset in the TUI, to hand control back to disk)' : "";
98
98
  return `[nexus] config loaded (#${info.loadCount} trigger=${info.trigger} at=${info.loadedAt}) ` + `project=${info.project.path} [${describeFile(info.project)}] ` + `global=${info.global.path} [${describeFile(info.global)}] ` + `models:${override} ${models}`;
99
99
  }
100
100
  var DEFAULT_CONFIG = {
@@ -311,7 +311,9 @@ class NexusConfigManager {
311
311
  return result;
312
312
  }
313
313
  resetToDefaults() {
314
+ const hadOverride = this.storageConfig !== null;
314
315
  this.storageConfig = null;
316
+ return hadOverride;
315
317
  }
316
318
  applyPreset(name) {
317
319
  const preset = PRESETS[name];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Adaptive Multi-Agent Orchestration with Cost Intelligence for OpenCode V2",
5
5
  "keywords": [
6
6
  "opencode",