vibeostheog 0.22.23 → 0.22.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeostheog",
3
- "version": "0.22.23",
3
+ "version": "0.22.25",
4
4
  "description": "Cost-aware delegation enforcer for OpenCode. Tracks model usage, routes Task subagents to cheaper tiers, surfaces cumulative savings in chat. Includes research audit, reporting framework, project memory, progressive scratchpad decadence, and trinity CLI for brain/medium/cheap slot switching.",
5
5
  "scripts": {
6
6
  "release": "node scripts/release.mjs",
@@ -111,6 +111,12 @@ export function classify(m) {
111
111
  return "high";
112
112
  if (MID_TIER_RE.test(s))
113
113
  return "mid";
114
+ // Fallback: strip provider prefix and test bare name
115
+ const bare = s.includes("/") ? s.split("/").slice(1).join("/") : s;
116
+ if (HIGH_TIER_RE.test(bare))
117
+ return "high";
118
+ if (MID_TIER_RE.test(bare))
119
+ return "mid";
114
120
  return "budget";
115
121
  }
116
122
  // Map a model ID to a human-readable label with tier icon.
@@ -160,9 +166,9 @@ export function resolveExecutionIdentity(modelId, directory = "") {
160
166
  const normalized = normalizeModelId(resolved || raw);
161
167
  const quality = isModelFree(resolved || raw)
162
168
  ? "free"
163
- : HIGH_TIER_RE.test(normalized)
169
+ : classify(resolved || raw) === "high"
164
170
  ? "brain"
165
- : MID_TIER_RE.test(normalized)
171
+ : classify(resolved || raw) === "mid"
166
172
  ? "medium"
167
173
  : "cheap";
168
174
  return {
@@ -260,6 +266,8 @@ export function trendDisplay(sesTrend) {
260
266
  const CACHE_SAVED_PER_1M_INPUT_TOKENS = 0.10;
261
267
  // Approximate bytes per token for JSON/text content (varies 3-6, use 4 as safe estimate).
262
268
  const BYTES_PER_TOKEN = 4;
269
+ // Average tokens per turn for cost estimation heuristic.
270
+ const AVG_TOKENS_PER_TURN = 375;
263
271
  export function parseOpenRouterInputPer1M(modelRow) {
264
272
  const p = modelRow?.pricing || {};
265
273
  const inTok = Number(p.prompt ?? p.input ?? p.request);
@@ -300,7 +308,7 @@ export function cacheSavePer1MInputTokens(model) {
300
308
  }
301
309
  const turnCost = modelCostPerTurn(model);
302
310
  if (Number.isFinite(turnCost) && turnCost > 0) {
303
- return Math.round(turnCost * 375 * 100) / 100;
311
+ return Math.round(turnCost * AVG_TOKENS_PER_TURN * 100) / 100;
304
312
  }
305
313
  return CACHE_SAVED_PER_1M_INPUT_TOKENS;
306
314
  }
@@ -790,9 +798,6 @@ const DFLT_SEL = { enabled: true, active_slot: null, thinking_level: "off", flow
790
798
  export function readConfig(dir) {
791
799
  try {
792
800
  const configs = [];
793
- const workspaceModel = readWorkspaceSessionModel(dir);
794
- if (workspaceModel)
795
- return workspaceModel;
796
801
  const projectCfg = readOpenCodeConfigObject(dir);
797
802
  if (projectCfg && typeof projectCfg === "object")
798
803
  configs.push(projectCfg);
@@ -802,6 +807,9 @@ export function readConfig(dir) {
802
807
  if (homeCfg && typeof homeCfg === "object")
803
808
  configs.push(homeCfg);
804
809
  }
810
+ const workspaceModel = readWorkspaceSessionModel(dir);
811
+ if (workspaceModel)
812
+ return resolveConfiguredModelId(workspaceModel, configs) || workspaceModel;
805
813
  const selectedCfg = configs[0] || {};
806
814
  const selectedModel = selectedCfg?.agent?.build?.model || selectedCfg?.model || "";
807
815
  return resolveConfiguredModelId(selectedModel, configs);
@@ -902,7 +910,7 @@ function readOpenCodeConfigObject(dir) {
902
910
  }
903
911
  function collectConfiguredProviderModelsFromConfig(cfg) {
904
912
  const out = [];
905
- const providers = cfg?.provider || {};
913
+ const providers = (cfg && typeof cfg === "object") ? (cfg?.provider || {}) : {};
906
914
  for (const [providerName, providerCfg] of Object.entries(providers)) {
907
915
  const models = providerCfg?.models || {};
908
916
  for (const rawId of Object.keys(models)) {
@@ -929,7 +937,23 @@ function resolveConfiguredModelId(model, configs = []) {
929
937
  matches.add(id);
930
938
  }
931
939
  }
932
- return matches.size === 1 ? [...matches][0] : raw;
940
+ if (matches.size === 0) {
941
+ // No exact match — try suffix/prefix match against bare model names
942
+ for (const cfg of configs) {
943
+ for (const id of collectConfiguredProviderModelsFromConfig(cfg)) {
944
+ const bare = String(id || "").includes("/") ? String(id).split("/").pop() : id;
945
+ const nb = normalizeModelId(bare);
946
+ if (nb.includes(normalized) || normalized.includes(nb))
947
+ matches.add(id);
948
+ }
949
+ }
950
+ }
951
+ if (matches.size === 0)
952
+ return "";
953
+ if (matches.size === 1)
954
+ return [...matches][0];
955
+ const qualified = [...matches].find(m => m.includes("/"));
956
+ return qualified || raw;
933
957
  }
934
958
  export function resolveDisplayModelId(model, directory = "") {
935
959
  const raw = String(model || "").trim();
@@ -1029,11 +1053,11 @@ export function _refreshModel(directory) {
1029
1053
  }
1030
1054
  }
1031
1055
  // Reconcile with the directory's opencode.json config.
1032
- // The trinity slot is authoritative UNLESS the directory config specifies a different model.
1056
+ // The trinity slot is authoritative UNLESS the directory config specifies a resolveable model.
1033
1057
  // This prevents the bootstrap's default slot from overriding a project-local model choice.
1034
1058
  if (!_modelLocked) {
1035
1059
  const cfgModel = readConfig(directory) || readConfig(getOpenCodeHome()) || "";
1036
- if (cfgModel && cfgModel !== currentModel) {
1060
+ if (cfgModel && cfgModel.includes("/") && cfgModel !== currentModel) {
1037
1061
  const oldModel = currentModel;
1038
1062
  const oldTier = currentTier;
1039
1063
  setCurrentModel(cfgModel);
@@ -3,7 +3,7 @@ function getRuntimeState() {
3
3
  const g = globalThis;
4
4
  if (!g[RUNTIME_KEY]) {
5
5
  g[RUNTIME_KEY] = {
6
- apiConnected: false,
6
+ apiConnected: true,
7
7
  apiFallbackMode: false,
8
8
  apiFallbackSince: null,
9
9
  sessionId: "opencode-" + (process.pid || "x") + "-" + Date.now(),
package/src/lib/state.js CHANGED
@@ -47,6 +47,8 @@ const MAX_SCRATCHPAD_FILES = 1000;
47
47
  const MAX_SCRATCHPAD_BYTES = 10 * 1024 * 1024;
48
48
  const MAX_SESSION_SCRATCHPAD_FILES = 200;
49
49
  const MAX_SESSION_SCRATCHPAD_BYTES = 2 * 1024 * 1024;
50
+ const MAX_PTR_CANDIDATES = 50;
51
+ const SUMMARY_HEAD_TRUNCATE = 500;
50
52
  function getVibeOSHome() {
51
53
  return VIBEOS_CONTEXT.getStore()?.home || process.env.VIBEOS_HOME || join(process.env.HOME || "", ".claude");
52
54
  }
@@ -839,7 +841,7 @@ function scanRecentScratchpad(dir, titleCase, maxScan = 2000) {
839
841
  const ptrFiles = entries.filter(e => e.endsWith(".ptr"));
840
842
  const ptrCandidates = [];
841
843
  for (const pf of ptrFiles) {
842
- if (ptrCandidates.length >= 50)
844
+ if (ptrCandidates.length >= MAX_PTR_CANDIDATES)
843
845
  break;
844
846
  try {
845
847
  const st = statSync(join(dir, pf));
@@ -1011,7 +1013,7 @@ function _pruneScratchpadDir(targetDir, opts = {}) {
1011
1013
  if (!existsSync(summaryPath))
1012
1014
  try {
1013
1015
  const content = readFileSync(fullPath, "utf-8");
1014
- writeFileSync(summaryPath, content.slice(0, 500).replace(/\n+/g, " ").trim() + (content.length > 500 ? "…" : ""));
1016
+ writeFileSync(summaryPath, content.slice(0, SUMMARY_HEAD_TRUNCATE).replace(/\n+/g, " ").trim() + (content.length > SUMMARY_HEAD_TRUNCATE ? "…" : ""));
1015
1017
  }
1016
1018
  catch { }
1017
1019
  const head = _readHead(fullPath);
@@ -210,11 +210,20 @@ function _modelCost(id) {
210
210
  function _modelTier(id) {
211
211
  if (!id)
212
212
  return "budget";
213
+ // Test both the full provider-qualified ID and the bare name
213
214
  const high = HIGH_TIER_RE?.test?.(id);
214
215
  if (high)
215
216
  return "high";
216
217
  const mid = MID_TIER_RE?.test?.(id);
217
- return mid ? "mid" : "budget";
218
+ if (mid)
219
+ return "mid";
220
+ // Fallback: strip provider prefix and test bare name
221
+ const bare = String(id).includes("/") ? String(id).split("/").slice(1).join("/") : String(id);
222
+ if (HIGH_TIER_RE?.test?.(bare))
223
+ return "high";
224
+ if (MID_TIER_RE?.test?.(bare))
225
+ return "mid";
226
+ return "budget";
218
227
  }
219
228
  export async function discoverAvailableModels(providers, auth) {
220
229
  const all = collectConfiguredProviderModels(providers);
@@ -319,12 +328,12 @@ export function classifyAndRankModels(models) {
319
328
  }
320
329
  if (unique.length === 0)
321
330
  return null;
322
- const normalizeModelId = (id) => String(id || "").toLowerCase()
331
+ const normalizeModelIdLocal = (id) => String(id || "").toLowerCase()
323
332
  .replace(/\./g, "-")
324
333
  .replace(/^(openrouter|opencode|deepseek|anthropic|google)\//, "");
325
- const isDeprecatedDeepseekChat = (id) => normalizeModelId(id).includes("deepseek-chat");
334
+ const isDeprecatedDeepseekChat = (id) => normalizeModelIdLocal(id).includes("deepseek-chat");
326
335
  const hasReplacementDeepseek = unique.some((m) => {
327
- const raw = normalizeModelId(m.id);
336
+ const raw = normalizeModelIdLocal(m.id);
328
337
  return raw.startsWith("deepseek-") && !raw.includes("deepseek-chat");
329
338
  });
330
339
  const ranked = hasReplacementDeepseek
@@ -333,7 +342,7 @@ export function classifyAndRankModels(models) {
333
342
  if (ranked.length === 0)
334
343
  return null;
335
344
  const modelPreference = (id) => {
336
- const raw = normalizeModelId(id);
345
+ const raw = normalizeModelIdLocal(id);
337
346
  if (raw.includes("deepseek-v4-flash"))
338
347
  return 2;
339
348
  if (raw.includes("deepseek-chat"))
@@ -3,6 +3,16 @@ import { join } from "node:path";
3
3
  import { LABEL_MODES, buildDeterministicTrinity, resolveExecutionIdentity } from "./pricing.js";
4
4
  import { BRANDED_MODES, RUNTIME_MODES } from "./mode-router.js";
5
5
  import { invalidateApiToken } from "./api-client.js";
6
+ // ── Named constants (magic number extraction) ────────────────────────
7
+ const MIN_TOOL_BREAKDOWN_THRESHOLD = 0.005;
8
+ const STRESS_GAUGE_CRITICAL = 0.85;
9
+ const STRESS_GAUGE_HIGH = 0.7;
10
+ const STRESS_GAUGE_ELEVATED = 0.5;
11
+ const STRESS_GAUGE_CALM = 0.3;
12
+ const STRESS_GAUGE_MIN = 0.1;
13
+ const MOMENTUM_SIGNIFICANT_THRESHOLD = 0.3;
14
+ const DIAGNOSE_BUDGET_LINES = 50;
15
+ const CREDIT_MIN_OK = 40;
6
16
  export function createTrinityTool(deps) {
7
17
  return {
8
18
  description: "Control the vibeOS plugin and active model slot. " +
@@ -57,7 +67,7 @@ export function createTrinityTool(deps) {
57
67
  const sesRate = sv.sesRatePerHour || 0;
58
68
  const missedC7 = sv.missedC7 || 0;
59
69
  const toolBreakdown = sv.sesToolBreakdown || {};
60
- const topTools = Object.entries(toolBreakdown).filter(([, v]) => v > 0.005).sort((a, b) => b[1] - a[1]).slice(0, 5);
70
+ const topTools = Object.entries(toolBreakdown).filter(([, v]) => v > MIN_TOOL_BREAKDOWN_THRESHOLD).sort((a, b) => b[1] - a[1]).slice(0, 5);
61
71
  const brainModel = tiers?.brain?.oc || "(unset)";
62
72
  const mediumModel = tiers?.medium?.oc || "(unset)";
63
73
  cheapModel = tiers?.cheap?.oc || cheapModel;
@@ -66,8 +76,8 @@ export function createTrinityTool(deps) {
66
76
  const lockedModel = deps._lockedModel || null;
67
77
  const onboardingMode = sel.onboarding_mode || "strict";
68
78
  const stressScore = deps.latestUserIntent ? deps.scoreStress(deps.latestUserIntent) : 0;
69
- const stressBar = stressScore > 0.85 ? "█" : stressScore > 0.7 ? "▆" : stressScore > 0.5 ? "▅" : stressScore > 0.3 ? "▃" : stressScore > 0.1 ? "▂" : "▁";
70
- const stressLabel = stressScore > 0.7 ? "high" : stressScore > 0.4 ? "elevated" : stressScore > 0.1 ? "calm" : "none";
79
+ const stressBar = stressScore > STRESS_GAUGE_CRITICAL ? "█" : stressScore > STRESS_GAUGE_HIGH ? "▆" : stressScore > STRESS_GAUGE_ELEVATED ? "▅" : stressScore > STRESS_GAUGE_CALM ? "▃" : stressScore > STRESS_GAUGE_MIN ? "▂" : "▁";
80
+ const stressLabel = stressScore > STRESS_GAUGE_HIGH ? "high" : stressScore > 0.4 ? "elevated" : stressScore > STRESS_GAUGE_MIN ? "calm" : "none";
71
81
  const totalTurns = (sv.sesModelTurns?.brain || 0) + (sv.sesModelTurns?.worker || 0);
72
82
  const brainPct = totalTurns > 0 ? Math.round((sv.sesModelTurns.brain / totalTurns) * 100) : 0;
73
83
  const workerPct = 100 - brainPct;
@@ -80,7 +90,7 @@ export function createTrinityTool(deps) {
80
90
  try {
81
91
  const res = deps._latestBlackboxState || deps.getBlackboxResolution();
82
92
  if (res && res.n_interactions > 3) {
83
- const momentumIcon = res.momentum > 0.3 ? "up up" : res.momentum > 0 ? "up" : res.momentum < -0.3 ? "down down" : res.momentum < 0 ? "down" : "flat";
93
+ const momentumIcon = res.momentum > MOMENTUM_SIGNIFICANT_THRESHOLD ? "up up" : res.momentum > 0 ? "up" : res.momentum < -MOMENTUM_SIGNIFICANT_THRESHOLD ? "down down" : res.momentum < 0 ? "down" : "flat";
84
94
  const loopTag = res.is_looping ? " (loop)" : "";
85
95
  decisionLine = `${res.resolution} ${res.sub_regime} ${momentumIcon}${loopTag}`;
86
96
  }
@@ -863,7 +873,7 @@ export function createTrinityTool(deps) {
863
873
  results.push({ ok: false, okLabel: "\u274c", label: "model probe", detail: "no current model detected" });
864
874
  }
865
875
  const credit = deps.loadCredit();
866
- let budget = 50;
876
+ let budget = DIAGNOSE_BUDGET_LINES;
867
877
  let totalBal = 0;
868
878
  try {
869
879
  const j = deps.safeJsonParse(deps.readFileSync(deps.TIERS_FILE, "utf-8"));
@@ -886,7 +896,7 @@ export function createTrinityTool(deps) {
886
896
  : runway.turnsRemaining != null && runway.costPerTurn != null
887
897
  ? `${Number(runway.turnsRemaining).toLocaleString()} turns on ${cheapModel} @ $${deps.formatUsd(runway.costPerTurn)}/turn`
888
898
  : "n/a";
889
- const creditOk = credit >= 40;
899
+ const creditOk = credit >= CREDIT_MIN_OK;
890
900
  results.push({
891
901
  ok: creditOk, okLabel: creditOk ? "\u2705" : "\u274c",
892
902
  label: "credits",