vibeostheog 0.22.0 → 0.22.7
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/CHANGELOG.md +19 -0
- package/README.md +15 -20
- package/bin/setup.js +33 -36
- package/package.json +3 -3
- package/src/lib/api-client.js +86 -0
- package/src/lib/cost-anomaly.js +75 -0
- package/src/lib/hooks/chat-transform.js +24 -18
- package/src/lib/hooks/tool-execute.js +20 -0
- package/src/lib/mode-router.js +100 -0
- package/src/lib/state.js +6 -6
- package/src/lib/turn-classify.js +24 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## 0.22.6
|
|
2
|
+
- feat: wire CostAnomalyDetector into tool-execute hook
|
|
3
|
+
- feat: replace TokenAnomalyDetector with CostAnomalyDetector
|
|
4
|
+
- feat: replace TokenAnomalyDetector with CostAnomalyDetector
|
|
5
|
+
- fix: bin/setup.js now delegates to deploy.mjs for proper plugin install
|
|
6
|
+
- fix: restore anomaly detector class in TS source, add mega regression tests
|
|
7
|
+
- fix: read path prefers global scratchpad over stale session-local copies
|
|
8
|
+
- fix: cross-session cache corruptions and hallucinations
|
|
9
|
+
- fix: VibeUltraX mode, anomaly token guard, regression suite
|
|
10
|
+
- chore: bump to 0.22.5
|
|
11
|
+
- chore: bump to 0.22.3
|
|
12
|
+
- chore: add regression tests for anomaly throttle permanent break fix
|
|
13
|
+
- chore: bump to 0.22.2
|
|
14
|
+
Merge pull request #103 from DrunkkToys/fix/anomaly-class-ts-source
|
|
15
|
+
Merge pull request #101 from DrunkkToys/fix/anomaly-throttle-permanent-break
|
|
16
|
+
Merge pull request #100 from DrunkkToys/fix/cross-session-corruptions
|
|
17
|
+
reorder policy comparison table by quality descending; deduplicate VibeUltraX formatting fix
|
|
18
|
+
|
|
19
|
+
|
|
1
20
|
## 0.20.16
|
|
2
21
|
- fix: skip cache savings for free models + add modelCostPerTurn fallback + regression tests
|
|
3
22
|
- fix: wire incrementTurnCounter into onToolExecuteAfter so session compaction fires at turn 7+
|
package/README.md
CHANGED
|
@@ -35,22 +35,25 @@ Benchmarked on the DeepSeek v4 family. Prices based on 700 input + 300 output to
|
|
|
35
35
|
|
|
36
36
|
## Optimization Modes
|
|
37
37
|
|
|
38
|
-
###
|
|
38
|
+
### Policy Comparison — Sorted by Quality Descending
|
|
39
39
|
|
|
40
|
-
|
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
40
|
+
| Policy | Quality vs Brain | Cost vs Brain | Savings | Method |
|
|
41
|
+
|--------|-----------------|--------------|---------|--------|
|
|
42
|
+
| VibeUltraX | 107% | 0.58x | 42% | local -> Flash -> Pro cascade |
|
|
43
|
+
| VibeQMaX | ~100% | 0.50x | 50% | same model, framework optimizations |
|
|
44
|
+
| Raw Brain | 100% | 1.00x | - | baseline |
|
|
45
|
+
| VibeMaX | ~75% | 0.18x | 82% | trained cascade (conservative escalate) |
|
|
46
|
+
| Budget | ~40% | 0.00x | 100% | direct routing |
|
|
47
|
+
|
|
48
|
+
**VibeUltraX** — Cascade pipeline: MagicCoder:7b (local Ollama) proposes, v4 Flash reviews, v4 Pro refines. Benchmarked at **107% of Brain quality** at 58% cost (local inference is free, only Flash/Pro API calls cost).
|
|
48
49
|
|
|
49
50
|
**VibeQMaX (Quality Max)** — Routes strategic turns through v4 Pro with full thinking, strict enforcement, strict flow checks, and quality TDD. Write/edit turns delegated to cheaper tiers per enforcement rules. Effective blended cost ~$0.00029/turn (50% of Raw Brain).
|
|
50
51
|
|
|
52
|
+
**Raw Brain** — v4 Pro (no framework). Baseline for all comparisons. 100% quality at 1.00x cost.
|
|
53
|
+
|
|
51
54
|
**VibeMaX (ML-Optimized, Default)** — Intelligent cost-quality sweet spot. Routes through v4 Flash (medium) and uses a random forest classifier (29 trees, gini-split, trained on telemetry) to decide each turn. Classifies on 11 derived features: message length, code block density, urgency, complexity, repetition, question ratio, and more. Benchmarked at ~75% of Brain quality at 18% of cost.
|
|
52
55
|
|
|
53
|
-
**
|
|
56
|
+
**Budget** — DeepSeek Chat. Direct routing. ~40% quality at 0.00x cost. 100% savings.
|
|
54
57
|
|
|
55
58
|
### Cost vs Quality Visual
|
|
56
59
|
|
|
@@ -73,7 +76,7 @@ Quality
|
|
|
73
76
|
|------|-------|----------|-------------|------|-----|
|
|
74
77
|
| Raw Brain | v4 Pro | full | - | - | - |
|
|
75
78
|
| VibeQMaX | v4 Pro | full | strict | strict | quality |
|
|
76
|
-
| VibeUltraX | cascade (local->Flash->Pro) | auto | auto | auto | auto |
|
|
79
|
+
| VibeUltraX | cascade (local->Flash->Pro) | auto | auto | auto | auto |
|
|
77
80
|
| VibeMaX | v4 Flash (auto-escalate) | auto | auto | auto | auto |
|
|
78
81
|
| Speed | v4 Flash | off | relaxed | audit | lazy |
|
|
79
82
|
| Budget | DeepSeek Chat | off | relaxed | audit | lazy |
|
|
@@ -82,14 +85,6 @@ Quality
|
|
|
82
85
|
|
|
83
86
|
All tests run with DeepSeek v4 family. Quality scores measured against Raw Brain (v4 Pro, full thinking, no vibeOS overhead). VibeMaX quality benchmark derived from real session telemetry with bootstrap confidence intervals (36 bootstrap samples). Pareto frontier computed from 70 holdout scenarios across 170 training samples via hyperparameter sweep. VibeUltra is the first mode that beats Raw Brain on both accuracy and cost — Pareto-dominant.
|
|
84
87
|
|
|
85
|
-
| Policy | Quality vs Brain | Cost vs Brain | Savings | Method |
|
|
86
|
-
|--------|-----------------|--------------|---------|--------|
|
|
87
|
-
| VibeUltraX | **107%** | 0.58x | 42% | local -> Flash -> Pro cascade |
|
|
88
|
-
| VibeMaX | ~75% | 0.18x | 82% | trained cascade (conservative escalate) |
|
|
89
|
-
| VibeQMaX | ~100% | 0.50x | 50% | same model, framework optimizations |
|
|
90
|
-
| Raw Brain | 100% | 1.00x | - | baseline |
|
|
91
|
-
| Budget | ~40% | 0.00x | 100% | direct routing |
|
|
92
|
-
|
|
93
88
|
Benchmarked on 1000 simulated questions across 20 runs, using model accuracies from MMLU-Pro / GPQA Diamond with real error correlation data.
|
|
94
89
|
|
|
95
90
|
## Features
|
|
@@ -156,7 +151,7 @@ npx vibeostheog setup --project # per-project
|
|
|
156
151
|
npx vibeostheog setup # global ~/.config/opencode/
|
|
157
152
|
```
|
|
158
153
|
|
|
159
|
-
|
|
154
|
+
One-command setup: deploys plugin files and registers in opencode.json. Restart OpenCode Desktop.
|
|
160
155
|
|
|
161
156
|
Local dev checkout:
|
|
162
157
|
|
package/bin/setup.js
CHANGED
|
@@ -1,48 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
4
5
|
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { homedir } from "node:os";
|
|
6
8
|
|
|
7
|
-
const
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const root = resolve(__dirname, "..");
|
|
8
11
|
const args = process.argv.slice(2);
|
|
9
|
-
const command = args[0] ?? "setup";
|
|
10
12
|
const isProject = args.includes("--project");
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
// Deploy plugin files to ~/.config/opencode/plugins/ and register globally
|
|
15
|
+
const deployScript = resolve(root, "scripts", "deploy.mjs");
|
|
16
|
+
if (!existsSync(deployScript)) {
|
|
17
|
+
console.error("Fatal: scripts/deploy.mjs not found at", deployScript);
|
|
14
18
|
process.exit(1);
|
|
15
19
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let config = {};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
config = {};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
execSync(`node "${deployScript}"`, { stdio: "inherit", cwd: root });
|
|
21
|
+
|
|
22
|
+
// For per-project setup, also register in project-level opencode.json
|
|
23
|
+
if (isProject) {
|
|
24
|
+
const configPath = resolve(process.cwd(), "opencode.json");
|
|
25
|
+
let config = {};
|
|
26
|
+
if (existsSync(configPath)) {
|
|
27
|
+
try {
|
|
28
|
+
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
29
|
+
} catch {
|
|
30
|
+
config = {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) config = {};
|
|
34
|
+
if (!config.$schema) config.$schema = "https://opencode.ai/config.json";
|
|
35
|
+
if (!Array.isArray(config.plugin)) config.plugin = [];
|
|
36
|
+
const pluginRef = resolve(homedir(), ".config", "opencode", "plugins", "vibeOS.js");
|
|
37
|
+
if (!config.plugin.includes(pluginRef)) {
|
|
38
|
+
config.plugin.push(pluginRef);
|
|
39
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
40
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
41
|
+
}
|
|
42
|
+
console.log(`vibeOS registered in ${configPath}`);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
config.plugin = [];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!config.plugin.includes(pkgName)) {
|
|
41
|
-
config.plugin.push(pkgName);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
45
|
-
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
46
|
-
|
|
47
|
-
console.log(`${pkgName} registered in ${configPath}`);
|
|
48
|
-
console.log("Restart OpenCode to activate the plugin.");
|
|
45
|
+
console.log("Done. Restart OpenCode to activate the plugin.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeostheog",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.7",
|
|
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",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"checkpoint:validate": "node scripts/checkpoint-validate.mjs",
|
|
15
15
|
"test:scripts": "node --test scripts/tests/checkpoint-validate.test.mjs tests/release-pack.test.mjs",
|
|
16
16
|
"ts:audit": "node scripts/ts-audit.mjs",
|
|
17
|
-
"test": "VIBEOS_MCP_PORT=0 node --test --test-timeout=240000 tests/deep_integration.test.mjs tests/production_regressions.test.mjs tests/release_hardening_tigerteam.test.mjs tests/test_api_migration.neutral.test.mjs tests/test_const_assignment_regression.test.mjs tests/test_delegation_enforcer.test.mjs tests/test_diagnose_cmd.test.mjs tests/test_install_and_recovery.test.mjs tests/test_internals_stress_patterns_offtopic.test.mjs tests/test_saveos_e2e_cleanup.test.mjs tests/test_tdd_enforcer.test.mjs src/tests/*.test.js src/utils/tests/*.test.mjs \"src/vibeOS-lib/tests/auto-select-mode.test.mjs\" \"src/vibeOS-lib/tests/blackbox-regression.test.mjs\" \"src/vibeOS-lib/tests/blackbox-smoke.test.mjs\" \"src/vibeOS-lib/tests/budget-first-mode.test.mjs\" \"src/vibeOS-lib/tests/flow-enforcer.test.mjs\" \"src/vibeOS-lib/tests/flow-secrets.test.mjs\" \"src/vibeOS-lib/tests/session-metrics.test.mjs\" \"src/vibeOS-lib/tests/test_stress.test.mjs\"",
|
|
18
|
-
"test:ci": "VIBEOS_MCP_PORT=0 node --test --test-timeout=30000 tests/production_regressions.test.mjs tests/release_hardening_tigerteam.test.mjs tests/test_const_assignment_regression.test.mjs tests/test_diagnose_cmd.test.mjs tests/test_install_and_recovery.test.mjs tests/test_saveos_e2e_cleanup.test.mjs tests/test_tdd_enforcer.test.mjs src/tests/*.test.js src/utils/tests/*.test.mjs \"src/vibeOS-lib/tests/auto-select-mode.test.mjs\" \"src/vibeOS-lib/tests/blackbox-regression.test.mjs\" \"src/vibeOS-lib/tests/blackbox-smoke.test.mjs\" \"src/vibeOS-lib/tests/budget-first-mode.test.mjs\" \"src/vibeOS-lib/tests/flow-enforcer.test.mjs\" \"src/vibeOS-lib/tests/flow-secrets.test.mjs\" \"src/vibeOS-lib/tests/session-metrics.test.mjs\" \"src/vibeOS-lib/tests/test_stress.test.mjs\"",
|
|
17
|
+
"test": "VIBEOS_MCP_PORT=0 node --test --test-timeout=240000 tests/deep_integration.test.mjs tests/production_regressions.test.mjs tests/release_hardening_tigerteam.test.mjs tests/test_api_migration.neutral.test.mjs tests/test_const_assignment_regression.test.mjs tests/test_delegation_enforcer.test.mjs tests/test_diagnose_cmd.test.mjs tests/test_install_and_recovery.test.mjs tests/test_internals_stress_patterns_offtopic.test.mjs tests/test_saveos_e2e_cleanup.test.mjs tests/test_tdd_enforcer.test.mjs tests/test_10fixes_regression.test.mjs tests/test_cross_session_regression.test.mjs tests/test_mega_all_fixes.test.mjs tests/test_smart_cache_regression.test.mjs src/tests/*.test.js src/utils/tests/*.test.mjs \"src/vibeOS-lib/tests/auto-select-mode.test.mjs\" \"src/vibeOS-lib/tests/blackbox-regression.test.mjs\" \"src/vibeOS-lib/tests/blackbox-smoke.test.mjs\" \"src/vibeOS-lib/tests/budget-first-mode.test.mjs\" \"src/vibeOS-lib/tests/flow-enforcer.test.mjs\" \"src/vibeOS-lib/tests/flow-secrets.test.mjs\" \"src/vibeOS-lib/tests/session-metrics.test.mjs\" \"src/vibeOS-lib/tests/test_stress.test.mjs\"",
|
|
18
|
+
"test:ci": "VIBEOS_MCP_PORT=0 node --test --test-timeout=30000 tests/production_regressions.test.mjs tests/release_hardening_tigerteam.test.mjs tests/test_const_assignment_regression.test.mjs tests/test_diagnose_cmd.test.mjs tests/test_install_and_recovery.test.mjs tests/test_saveos_e2e_cleanup.test.mjs tests/test_tdd_enforcer.test.mjs tests/test_10fixes_regression.test.mjs tests/test_cross_session_regression.test.mjs tests/test_mega_all_fixes.test.mjs tests/test_smart_cache_regression.test.mjs src/tests/*.test.js src/utils/tests/*.test.mjs \"src/vibeOS-lib/tests/auto-select-mode.test.mjs\" \"src/vibeOS-lib/tests/blackbox-regression.test.mjs\" \"src/vibeOS-lib/tests/blackbox-smoke.test.mjs\" \"src/vibeOS-lib/tests/budget-first-mode.test.mjs\" \"src/vibeOS-lib/tests/flow-enforcer.test.mjs\" \"src/vibeOS-lib/tests/flow-secrets.test.mjs\" \"src/vibeOS-lib/tests/session-metrics.test.mjs\" \"src/vibeOS-lib/tests/test_stress.test.mjs\"",
|
|
19
19
|
"codex:guard": "bash plugins/vibetheog-codex/scripts/run-guard.sh",
|
|
20
20
|
"codex:guard:full": "VIBETHEOG_GUARD_FULL=1 bash plugins/vibetheog-codex/scripts/run-guard.sh",
|
|
21
21
|
"codex:hook:precommit": "bash plugins/vibetheog-codex/hooks/pre-commit.sh",
|
package/src/lib/api-client.js
CHANGED
|
@@ -38,6 +38,66 @@ export class VibeOSNetworkError extends Error {
|
|
|
38
38
|
this.name = "VibeOSNetworkError";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
const ANOMALY_BURST_WINDOW_MS = 5000;
|
|
42
|
+
const ANOMALY_BURST_THRESHOLD = 10;
|
|
43
|
+
const ANOMALY_FREQ_WINDOW_MS = 600_000;
|
|
44
|
+
const ANOMALY_STDDEV_FACTOR = 3;
|
|
45
|
+
const ANOMALY_WARMUP_MS = 30_000;
|
|
46
|
+
const ANOMALY_COOLDOWN_MS = 120_000;
|
|
47
|
+
class TokenAnomalyDetector {
|
|
48
|
+
burstHistory = [];
|
|
49
|
+
freqHistory = [];
|
|
50
|
+
lastWarnTime = 0;
|
|
51
|
+
anomalyTriggered = false;
|
|
52
|
+
disabled = false;
|
|
53
|
+
startedAt = Date.now();
|
|
54
|
+
get isWarmup() {
|
|
55
|
+
return Date.now() - this.startedAt < ANOMALY_WARMUP_MS;
|
|
56
|
+
}
|
|
57
|
+
record() {
|
|
58
|
+
if (this.disabled || this.isWarmup)
|
|
59
|
+
return;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
this.burstHistory = this.burstHistory.filter(t => now - t < ANOMALY_BURST_WINDOW_MS);
|
|
62
|
+
this.burstHistory.push(now);
|
|
63
|
+
this.freqHistory.push(now);
|
|
64
|
+
}
|
|
65
|
+
checkBurst() {
|
|
66
|
+
return this.burstHistory.length > ANOMALY_BURST_THRESHOLD;
|
|
67
|
+
}
|
|
68
|
+
checkFrequency() {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const window = this.freqHistory.filter(t => now - t < ANOMALY_FREQ_WINDOW_MS);
|
|
71
|
+
if (window.length < 10)
|
|
72
|
+
return false;
|
|
73
|
+
const mean = window.length / (ANOMALY_FREQ_WINDOW_MS / 60_000);
|
|
74
|
+
const recent = this.burstHistory.length / (ANOMALY_BURST_WINDOW_MS / 1000);
|
|
75
|
+
return recent > mean * ANOMALY_STDDEV_FACTOR;
|
|
76
|
+
}
|
|
77
|
+
throttleIfAnomalous() {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
if (this.disabled || this.isWarmup)
|
|
80
|
+
return false;
|
|
81
|
+
if (this.anomalyTriggered)
|
|
82
|
+
return true;
|
|
83
|
+
if (this.checkBurst() || this.checkFrequency()) {
|
|
84
|
+
this.anomalyTriggered = true;
|
|
85
|
+
this.lastWarnTime = now;
|
|
86
|
+
console.error("[vibeOS] Token anomaly detected — throttling API calls");
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (this.lastWarnTime && now - this.lastWarnTime > ANOMALY_COOLDOWN_MS) {
|
|
90
|
+
this.anomalyTriggered = false;
|
|
91
|
+
}
|
|
92
|
+
return this.anomalyTriggered;
|
|
93
|
+
}
|
|
94
|
+
reset() {
|
|
95
|
+
this.burstHistory = [];
|
|
96
|
+
this.freqHistory = [];
|
|
97
|
+
this.anomalyTriggered = false;
|
|
98
|
+
this.lastWarnTime = 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
41
101
|
function normalizeApiToken(token, fallback = "") {
|
|
42
102
|
const clean = String(token || "").trim();
|
|
43
103
|
return API_TOKEN_RE.test(clean) ? clean : fallback;
|
|
@@ -433,6 +493,19 @@ export let VIBEOS_API_DISABLED = readApiDisabledFromDisk() || isTruthyFlag(proce
|
|
|
433
493
|
export let VIBEOS_API_TOKEN = VIBEOS_API_DISABLED ? "" : (readTokenFromDisk() || normalizeApiToken(process.env.VIBEOS_API_TOKEN, "") || EMBEDDED_API_TOKEN);
|
|
434
494
|
export let VIBEOS_API_BOOTSTRAP_TOKEN = VIBEOS_API_DISABLED ? "" : (readBootstrapTokenFromDisk() || process.env.VIBEOS_API_BOOTSTRAP_TOKEN || "");
|
|
435
495
|
export let VIBEOS_API_ENABLED = !VIBEOS_API_DISABLED && process.env.VIBEOS_API_ENABLED !== "false" && (!!VIBEOS_API_TOKEN || !!VIBEOS_API_BOOTSTRAP_TOKEN);
|
|
496
|
+
let _anomalyDetector = null;
|
|
497
|
+
function getAnomalyDetector() {
|
|
498
|
+
if (!_anomalyDetector)
|
|
499
|
+
_anomalyDetector = new TokenAnomalyDetector();
|
|
500
|
+
return _anomalyDetector;
|
|
501
|
+
}
|
|
502
|
+
export function setAnomalyDetection(enabled) {
|
|
503
|
+
const d = getAnomalyDetector();
|
|
504
|
+
d.disabled = !enabled;
|
|
505
|
+
if (enabled)
|
|
506
|
+
d.reset();
|
|
507
|
+
console.error(`[vibeOS] Anomaly detection ${enabled ? "enabled" : "disabled"}`);
|
|
508
|
+
}
|
|
436
509
|
function persistBootstrapToken(token) {
|
|
437
510
|
const clean = String(token || "").trim();
|
|
438
511
|
try {
|
|
@@ -460,6 +533,8 @@ export function setApiToken(newToken) {
|
|
|
460
533
|
VIBEOS_API_BOOTSTRAP_TOKEN = readBootstrapTokenFromDisk() || VIBEOS_API_BOOTSTRAP_TOKEN;
|
|
461
534
|
VIBEOS_API_ENABLED = process.env.VIBEOS_API_ENABLED !== "false" && (!!VIBEOS_API_TOKEN || !!VIBEOS_API_BOOTSTRAP_TOKEN);
|
|
462
535
|
persistPrimaryApiEnvState({ token: VIBEOS_API_TOKEN, disabled: false });
|
|
536
|
+
if (_anomalyDetector)
|
|
537
|
+
_anomalyDetector.reset();
|
|
463
538
|
console.error("[vibeOS] API token updated via setApiToken");
|
|
464
539
|
}
|
|
465
540
|
catch (e) {
|
|
@@ -475,6 +550,8 @@ export function invalidateApiToken() {
|
|
|
475
550
|
_apiClient = null;
|
|
476
551
|
_apiFallbackMode = false;
|
|
477
552
|
_apiFallbackSince = null;
|
|
553
|
+
if (_anomalyDetector)
|
|
554
|
+
_anomalyDetector.reset();
|
|
478
555
|
persistBootstrapToken("");
|
|
479
556
|
persistPrimaryApiEnvState({ token: "", disabled: true });
|
|
480
557
|
resetApiConnection();
|
|
@@ -624,6 +701,15 @@ export async function remoteCall(method, args, fallbackFn) {
|
|
|
624
701
|
return fallbackFn();
|
|
625
702
|
return null;
|
|
626
703
|
}
|
|
704
|
+
const detector = getAnomalyDetector();
|
|
705
|
+
detector.record();
|
|
706
|
+
if (detector.throttleIfAnomalous()) {
|
|
707
|
+
// Don't set _apiFallbackMode — detector's own cooldown resets it.
|
|
708
|
+
// This lets the API retry naturally after the throttle window.
|
|
709
|
+
if (fallbackFn)
|
|
710
|
+
return fallbackFn();
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
627
713
|
try {
|
|
628
714
|
const client = getApiClient();
|
|
629
715
|
if (!client) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
* SPDX-FileCopyrightText: 2026 vibeOS <https://github.com/DrunkkToys/vibeOS>
|
|
5
|
+
*
|
|
6
|
+
* Cost anomaly detector — monitors per-turn model costs and warns
|
|
7
|
+
* when a model cost spikes significantly above the running average.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const COST_WINDOW_SIZE = 20
|
|
11
|
+
const COST_ANOMALY_THRESHOLD = 3
|
|
12
|
+
const COST_WARMUP_SAMPLES = 5
|
|
13
|
+
|
|
14
|
+
export class CostAnomalyDetector {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.costHistory = []
|
|
17
|
+
this.disabled = false
|
|
18
|
+
this.currentAnomalyModel = null
|
|
19
|
+
this.currentAnomalyCost = 0
|
|
20
|
+
this.currentAnomalyMean = 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
record(cost) {
|
|
24
|
+
if (this.disabled) return
|
|
25
|
+
this.costHistory.push(cost)
|
|
26
|
+
if (this.costHistory.length > COST_WINDOW_SIZE) {
|
|
27
|
+
this.costHistory.shift()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get mean() {
|
|
32
|
+
if (this.costHistory.length === 0) return 0
|
|
33
|
+
return this.costHistory.reduce((a, b) => a + b, 0) / this.costHistory.length
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
checkAnomaly(model, cost) {
|
|
37
|
+
if (this.disabled) return false
|
|
38
|
+
if (this.costHistory.length < COST_WARMUP_SAMPLES) return false
|
|
39
|
+
const avg = this.mean
|
|
40
|
+
if (avg <= 0 || cost <= avg) return false
|
|
41
|
+
const ratio = cost / avg
|
|
42
|
+
if (ratio > COST_ANOMALY_THRESHOLD) {
|
|
43
|
+
this.currentAnomalyModel = model
|
|
44
|
+
this.currentAnomalyCost = cost
|
|
45
|
+
this.currentAnomalyMean = avg
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
clearAnomaly() {
|
|
52
|
+
this.currentAnomalyModel = null
|
|
53
|
+
this.currentAnomalyCost = 0
|
|
54
|
+
this.currentAnomalyMean = 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
reset() {
|
|
58
|
+
this.costHistory = []
|
|
59
|
+
this.clearAnomaly()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let _costDetector = null
|
|
64
|
+
|
|
65
|
+
export function getCostAnomalyDetector() {
|
|
66
|
+
if (!_costDetector) _costDetector = new CostAnomalyDetector()
|
|
67
|
+
return _costDetector
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function setCostAnomalyDetection(enabled) {
|
|
71
|
+
const d = getCostAnomalyDetector()
|
|
72
|
+
d.disabled = !enabled
|
|
73
|
+
if (enabled) d.reset()
|
|
74
|
+
console.error(`[vibeOS] Cost anomaly detection ${enabled ? "enabled" : "disabled"}`)
|
|
75
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
3
|
import { join, basename } from "node:path";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { currentModel, currentProjectFingerprint, currentProjectName, _blackboxEnabled, loadSelection, writeSelection, safeJsonParse, applyDecadence, getSessionScratchpadDir, ensureSessionScratchpadDirs, indexAppend, briefedProjects, getActiveJobForProject, loadTodos, promotedProjectPatterns, detectTechStack, projectFingerprint, TRINITY_OPENCODE_CONFIG, TIERS_FILE, loadGlobalLearning, setCurrentProjectFingerprint, setCurrentProjectName, stableJson, TOOL_NAME_NORMALIZE, _cacheDb, recordCacheSaving, } from "../state.js";
|
|
5
|
+
import { currentModel, currentProjectFingerprint, currentProjectName, _blackboxEnabled, loadSelection, writeSelection, safeJsonParse, applyDecadence, getSessionScratchpadDir, ensureSessionScratchpadDirs, indexAppend, briefedProjects, getActiveJobForProject, loadTodos, promotedProjectPatterns, detectTechStack, projectFingerprint, SCRATCHPAD_ROOT, TRINITY_OPENCODE_CONFIG, TIERS_FILE, loadGlobalLearning, setCurrentProjectFingerprint, setCurrentProjectName, stableJson, TOOL_NAME_NORMALIZE, _cacheDb, recordCacheSaving, } from "../state.js";
|
|
6
6
|
import { applySlot, TRINITY_CHEAP, TRINITY_MEDIUM, cacheSavePer1MInputTokens, } from "../pricing.js";
|
|
7
7
|
import { scoreStress, classifyTurnSimple, loadOptimizationMode, saveOptimizationMode, selectOptimizationModeRemote, computeControlVector, getBlackboxTracker, loadBlackboxState as loadBlackboxStateFromCtx, saveBlackboxState as saveBlackboxStateToCtx, extractLastUserText, isLikelyOffTopic, fetchBlackboxEnrichment, estimateContextBudget, buildControlHistoryEntry, } from "../turn-classify.js";
|
|
8
8
|
import { applyBudgetFirstMode, peekBudgetFirstMode } from "../mode-policy.js";
|
|
@@ -277,25 +277,31 @@ function compressToolOutputs(messages) {
|
|
|
277
277
|
continue;
|
|
278
278
|
const hash = createHash("sha256")
|
|
279
279
|
.update(`tool_result\n${raw}\n`).digest("hex").slice(0, 16);
|
|
280
|
-
const
|
|
280
|
+
const globalDir = join(SCRATCHPAD_ROOT, "by-hash");
|
|
281
|
+
const sessPath = join(getSessionScratchpadDir(), `${hash}.txt`);
|
|
282
|
+
const globalPath = join(globalDir, `${hash}.txt`);
|
|
281
283
|
try {
|
|
284
|
+
mkdirSync(globalDir, { recursive: true });
|
|
282
285
|
ensureSessionScratchpadDirs();
|
|
283
|
-
if (!existsSync(
|
|
284
|
-
writeFileSync(
|
|
286
|
+
if (!existsSync(globalPath)) {
|
|
287
|
+
writeFileSync(globalPath, raw);
|
|
285
288
|
indexAppend(hash, part.tool, raw.length);
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
// Clean up any existing session-local copy
|
|
290
|
+
if (existsSync(sessPath))
|
|
291
|
+
rmSync(sessPath, { force: true });
|
|
292
|
+
}
|
|
293
|
+
// Create pointer file for input-hash-based lookup
|
|
294
|
+
const invPart = parts.slice(0, parts.indexOf(part)).reverse().find((p) => p?.type === "tool" && p?.tool === part.tool && p?.state?.input && p?.state?.status !== "completed");
|
|
295
|
+
if (invPart?.state?.input) {
|
|
296
|
+
const toolKey = TOOL_NAME_NORMALIZE[part.tool] || part.tool;
|
|
297
|
+
const inputHash = createHash("sha256")
|
|
298
|
+
.update(`${toolKey}\n${stableJson(invPart.state.input)}\n`)
|
|
299
|
+
.digest("hex").slice(0, 16);
|
|
300
|
+
const ptrPath = join(getSessionScratchpadDir(), `${inputHash}.ptr`);
|
|
301
|
+
try {
|
|
302
|
+
writeFileSync(ptrPath, JSON.stringify({ contentHash: hash, tool: part.tool }));
|
|
298
303
|
}
|
|
304
|
+
catch { }
|
|
299
305
|
}
|
|
300
306
|
}
|
|
301
307
|
catch (err) {
|
|
@@ -305,7 +311,7 @@ function compressToolOutputs(messages) {
|
|
|
305
311
|
if (!isCold)
|
|
306
312
|
continue;
|
|
307
313
|
const summary = raw.slice(0, 200).replace(/\n+/g, " ").trim() + (raw.length > 200 ? "\u2026" : "");
|
|
308
|
-
const ref = `${COMPRESS_MARKER} [${raw.length} chars compressed -- cold storage at ${
|
|
314
|
+
const ref = `${COMPRESS_MARKER} [${raw.length} chars compressed -- cold storage at ${globalPath}] ` +
|
|
309
315
|
`[summary] ${summary}`;
|
|
310
316
|
state.output = ref;
|
|
311
317
|
compressedBytes += raw.length - ref.length;
|
|
@@ -9,6 +9,7 @@ import { scoreStress, extractFirstWordFromArgs, shouldLogWarn, isUserAskingForTe
|
|
|
9
9
|
import { saveReport } from "../reporting.js";
|
|
10
10
|
import { loadCredit } from "../credit-api.js";
|
|
11
11
|
import { remoteCall, VIBEOS_API_ENABLED } from "../api-client.js";
|
|
12
|
+
import { getCostAnomalyDetector } from "../cost-anomaly.js";
|
|
12
13
|
import { checkFlowRules } from "../../vibeOS-lib/flow-enforcer.js";
|
|
13
14
|
import { computeDifficulty, addRouteEdge, predictBestModel, hashQuery } from "../../vibeOS-lib/ml-router.js";
|
|
14
15
|
import { addCacheEntry, recordCacheStats, predictCacheHit } from "../../vibeOS-lib/smart-cache.js";
|
|
@@ -487,6 +488,25 @@ export const onToolExecuteBefore = async (input, output) => {
|
|
|
487
488
|
return;
|
|
488
489
|
}
|
|
489
490
|
}
|
|
491
|
+
// Cost anomaly detection: warn if this model's per-turn cost spikes
|
|
492
|
+
// significantly above the session rolling average.
|
|
493
|
+
const costDetector = getCostAnomalyDetector();
|
|
494
|
+
if (!costDetector.disabled && currentModel) {
|
|
495
|
+
const modelCost = modelCostPerTurn(currentModel);
|
|
496
|
+
const fullModelName = currentModel;
|
|
497
|
+
if (costDetector.checkAnomaly(fullModelName, modelCost)) {
|
|
498
|
+
const avg = costDetector.currentAnomalyMean;
|
|
499
|
+
const ratio = avg > 0 ? (modelCost / avg).toFixed(1) : "?";
|
|
500
|
+
const msg = `Cost spike: ${shortModelName(fullModelName)} at $${modelCost.toFixed(4)}/turn — ${ratio}x higher than recent avg of $${avg.toFixed(4)}. Run \`trinity cheap\` or \`trinity medium\` to save.`;
|
|
501
|
+
if (shouldLogWarn(`${t}|cost-anomaly|${fullModelName}|${modelCost.toFixed(4)}`)) {
|
|
502
|
+
console.error(`[vibeOS] [cost-anomaly] ${msg}`);
|
|
503
|
+
}
|
|
504
|
+
pendingUiNote = `🚨 ${msg}`;
|
|
505
|
+
enforcementBlocked = true;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
costDetector.record(modelCost);
|
|
509
|
+
}
|
|
490
510
|
// Credit < 40%: non-task tool — record and nudge to step aside.
|
|
491
511
|
if (_credit < 40 && !compatibilityMode) {
|
|
492
512
|
const total = recordSaving(t, "credit<40% high-tier", _estOpus, { firstWord: _firstWord });
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Mode Router — 10 modes, 4 tiers. Full type-safe hierarchy.
|
|
2
|
+
// Branded modes: user-selected strategy + tier pipeline.
|
|
3
|
+
// Runtime modes: classifier-selected behavior per query.
|
|
4
|
+
export const TIERS = {
|
|
5
|
+
brain: { cost: 0.002, desc: "v4 Pro tier — max quality" },
|
|
6
|
+
medium: { cost: 0.000182, desc: "v4 Flash tier — balanced" },
|
|
7
|
+
cheap: { cost: 0, desc: "Chat tier — free" },
|
|
8
|
+
local: { cost: 0, desc: "Ollama local model" },
|
|
9
|
+
};
|
|
10
|
+
export const BRANDED_MODES = [
|
|
11
|
+
{
|
|
12
|
+
id: "vibeultrax", index: 1, name: "VibeUltraX", icon: "\u{1F3C6}",
|
|
13
|
+
pipeline: ["local", "medium", "brain"],
|
|
14
|
+
thinking: "full", tdd: "quality", enforcement: "strict", flow: "strict",
|
|
15
|
+
qualityVsBrain: 107, costVsBrain: 58,
|
|
16
|
+
desc: "3-model debate: local proposes, medium reviews, brain refines.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "vibeqmax", index: 2, name: "VibeQMaX", icon: "\u{2B50}",
|
|
20
|
+
pipeline: ["brain"],
|
|
21
|
+
thinking: "full", tdd: "quality", enforcement: "strict", flow: "strict",
|
|
22
|
+
qualityVsBrain: 100, costVsBrain: 50,
|
|
23
|
+
desc: "Brain tier only. Same quality as Raw Brain at half cost.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "vibemax", index: 3, name: "VibeMaX", icon: "\u{26A1}",
|
|
27
|
+
pipeline: ["medium"],
|
|
28
|
+
thinking: "off", tdd: "lazy", enforcement: "relaxed", flow: "audit",
|
|
29
|
+
qualityVsBrain: 75, costVsBrain: 18, default: true,
|
|
30
|
+
desc: "Default mode. Medium tier auto-escalate. Speed-first.",
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
export const RUNTIME_MODES = [
|
|
34
|
+
{
|
|
35
|
+
id: "balanced", index: 4, name: "Balanced", icon: "\u{2696}\u{FE0F}",
|
|
36
|
+
pipeline: ["medium"],
|
|
37
|
+
thinking: "brief", tdd: "lazy", enforcement: "relaxed", flow: "audit",
|
|
38
|
+
qualityVsBrain: 70, costVsBrain: 30, defaultRuntime: true,
|
|
39
|
+
desc: "Default runtime. Auto-selects behavior per query.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "speed", index: 5, name: "Speed", icon: "\u{1F680}",
|
|
43
|
+
pipeline: ["medium"],
|
|
44
|
+
thinking: "off", tdd: "off", enforcement: "relaxed", flow: "off",
|
|
45
|
+
qualityVsBrain: 55, costVsBrain: 32,
|
|
46
|
+
desc: "Medium tier. Fast responses, no overhead.",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "budget", index: 6, name: "Budget", icon: "\u{1F4B8}",
|
|
50
|
+
pipeline: ["cheap"],
|
|
51
|
+
thinking: "off", tdd: "off", enforcement: "off", flow: "off",
|
|
52
|
+
qualityVsBrain: 40, costVsBrain: 100,
|
|
53
|
+
desc: "Cheap tier only. Zero cost.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "quality", index: 7, name: "Quality", icon: "\u{1F4AF}",
|
|
57
|
+
pipeline: ["brain"],
|
|
58
|
+
thinking: "full", tdd: "quality", enforcement: "strict", flow: "strict",
|
|
59
|
+
qualityVsBrain: 100, costVsBrain: 60,
|
|
60
|
+
desc: "Brain tier with full thinking and enforcement.",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "audit", index: 8, name: "Audit", icon: "\u{1F50D}",
|
|
64
|
+
pipeline: ["brain"],
|
|
65
|
+
thinking: "full", tdd: "quality", enforcement: "strict", flow: "strict",
|
|
66
|
+
qualityVsBrain: 100, costVsBrain: 55,
|
|
67
|
+
desc: "Brain tier security audit. OWASP validation.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "longrun", index: 9, name: "Longrun", icon: "\u{1F3C3}",
|
|
71
|
+
pipeline: ["brain"],
|
|
72
|
+
thinking: "full", tdd: "quality", enforcement: "strict", flow: "strict",
|
|
73
|
+
qualityVsBrain: 100, costVsBrain: 70,
|
|
74
|
+
desc: "Brain tier extended session. Full context.",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
export const RAW_MODE = {
|
|
78
|
+
id: "raw", index: 10, name: "Raw Brain", icon: "\u{1F9E0}",
|
|
79
|
+
pipeline: ["brain"],
|
|
80
|
+
thinking: "full", tdd: "\u2014", enforcement: "\u2014", flow: "\u2014",
|
|
81
|
+
qualityVsBrain: 100, costVsBrain: 0,
|
|
82
|
+
desc: "Pure v4 Pro baseline. No vibeOS overhead.",
|
|
83
|
+
};
|
|
84
|
+
export const ALL_MODES = [...BRANDED_MODES, ...RUNTIME_MODES, RAW_MODE];
|
|
85
|
+
export function getMode(id) {
|
|
86
|
+
return ALL_MODES.find(m => m.id === id) ?? getDefault();
|
|
87
|
+
}
|
|
88
|
+
export function getDefault() {
|
|
89
|
+
return BRANDED_MODES.find(m => m.default);
|
|
90
|
+
}
|
|
91
|
+
export function getDefaultRuntime() {
|
|
92
|
+
return RUNTIME_MODES.find(m => m.defaultRuntime);
|
|
93
|
+
}
|
|
94
|
+
export function getBrandedModes() { return BRANDED_MODES; }
|
|
95
|
+
export function getRuntimeModes() { return RUNTIME_MODES; }
|
|
96
|
+
export function resolveTierModels(mode, tierMap) {
|
|
97
|
+
const models = mode.pipeline.map(t => tierMap[t] ?? t);
|
|
98
|
+
const costs = mode.pipeline.map(t => TIERS[t]?.cost ?? 0);
|
|
99
|
+
return { models, totalCost: costs.reduce((s, c) => s + c, 0) };
|
|
100
|
+
}
|
package/src/lib/state.js
CHANGED
|
@@ -907,7 +907,7 @@ function getScratchpadHit(toolLower, args, baseDir = null) {
|
|
|
907
907
|
const globalDir = SCRATCHPAD_GLOBAL_DIR;
|
|
908
908
|
const sessionPath = join(sessionDir, `${hash}.txt`);
|
|
909
909
|
const globalPath = join(globalDir, `${hash}.txt`);
|
|
910
|
-
let fullPath = existsSync(
|
|
910
|
+
let fullPath = existsSync(globalPath) ? globalPath : (existsSync(sessionPath) ? sessionPath : null);
|
|
911
911
|
if (!fullPath) {
|
|
912
912
|
// Try pointer files (created by compressToolOutputs mapping input hash -> content hash)
|
|
913
913
|
const ptrSessionPath = join(sessionDir, `${hash}.ptr`);
|
|
@@ -921,7 +921,7 @@ function getScratchpadHit(toolLower, args, baseDir = null) {
|
|
|
921
921
|
resolvedHash = ptrData.contentHash;
|
|
922
922
|
const rSessionPath = join(sessionDir, `${resolvedHash}.txt`);
|
|
923
923
|
const rGlobalPath = join(globalDir, `${resolvedHash}.txt`);
|
|
924
|
-
fullPath = existsSync(
|
|
924
|
+
fullPath = existsSync(rGlobalPath) ? rGlobalPath : (existsSync(rSessionPath) ? rSessionPath : null);
|
|
925
925
|
}
|
|
926
926
|
}
|
|
927
927
|
catch { }
|
|
@@ -1454,9 +1454,9 @@ function recordDelegation(tool, saveEst, meta = {}) {
|
|
|
1454
1454
|
s.sessions ??= {};
|
|
1455
1455
|
const sid = _OC_SID;
|
|
1456
1456
|
s.sessions[sid] ??= { started: now, session_started_at: now, source: "opencode", tool_counts: {}, warns: [] };
|
|
1457
|
-
if (currentProjectFingerprint)
|
|
1457
|
+
if (currentProjectFingerprint && !s.sessions[sid].project_fingerprint)
|
|
1458
1458
|
s.sessions[sid].project_fingerprint = currentProjectFingerprint;
|
|
1459
|
-
if (currentProjectName)
|
|
1459
|
+
if (currentProjectName && !s.sessions[sid].project_name)
|
|
1460
1460
|
s.sessions[sid].project_name = currentProjectName;
|
|
1461
1461
|
s.sessions[sid].total_savings_usd = roundUsd(Number(s.sessions[sid].total_savings_usd || 0) + delta);
|
|
1462
1462
|
_pruneOldSessions(s);
|
|
@@ -1478,9 +1478,9 @@ function recordCacheSaving(tool, saveEst, meta = {}) {
|
|
|
1478
1478
|
s.sessions ??= {};
|
|
1479
1479
|
const sid = _OC_SID;
|
|
1480
1480
|
s.sessions[sid] ??= { started: now, session_started_at: now, source: "opencode", tool_counts: {}, warns: [] };
|
|
1481
|
-
if (currentProjectFingerprint)
|
|
1481
|
+
if (currentProjectFingerprint && !s.sessions[sid].project_fingerprint)
|
|
1482
1482
|
s.sessions[sid].project_fingerprint = currentProjectFingerprint;
|
|
1483
|
-
if (currentProjectName)
|
|
1483
|
+
if (currentProjectName && !s.sessions[sid].project_name)
|
|
1484
1484
|
s.sessions[sid].project_name = currentProjectName;
|
|
1485
1485
|
s.sessions[sid].session_cache_dir = getSessionScratchpadDir();
|
|
1486
1486
|
s.sessions[sid].tool_counts[tool] = (s.sessions[sid].tool_counts[tool] || 0) + 1;
|
package/src/lib/turn-classify.js
CHANGED
|
@@ -45,18 +45,20 @@ export function bootstrapOptimizationSession() {
|
|
|
45
45
|
const state = loadBlackboxState();
|
|
46
46
|
if (!state.sessions)
|
|
47
47
|
state.sessions = {};
|
|
48
|
-
if (
|
|
49
|
-
state.sessions[sid]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
if (sid && sid !== "undefined") {
|
|
49
|
+
if (!state.sessions[sid])
|
|
50
|
+
state.sessions[sid] = {};
|
|
51
|
+
state.sessions[sid].optimization_mode = resolvedMode;
|
|
52
|
+
state.sessions[sid].active_slot = resolvedSlot;
|
|
53
|
+
state.sessions[sid].sub_regime = state.sessions[sid].sub_regime || "INIT";
|
|
54
|
+
state.sessions[sid].regime = state.sessions[sid].regime || "INIT";
|
|
55
|
+
state.sessions[sid].resolution = state.sessions[sid].resolution || "unresolved";
|
|
56
|
+
state.sessions[sid].momentum = Number(state.sessions[sid].momentum || 0);
|
|
57
|
+
state.sessions[sid].loop_count = Number(state.sessions[sid].loop_count || 0);
|
|
58
|
+
state.sessions[sid].loop_intervention_level = state.sessions[sid].loop_intervention_level || "none";
|
|
59
|
+
state.sessions[sid].loop_start_turn = Number(state.sessions[sid].loop_start_turn || 0);
|
|
60
|
+
state.sessions[sid].loop_pattern_count = Number(state.sessions[sid].loop_pattern_count || 0);
|
|
61
|
+
}
|
|
60
62
|
saveBlackboxState(state);
|
|
61
63
|
}
|
|
62
64
|
catch { }
|
|
@@ -314,11 +316,11 @@ export function getBlackboxTracker() {
|
|
|
314
316
|
if (state.enabled !== undefined)
|
|
315
317
|
_setGlobalBlackboxEnabled(state.enabled);
|
|
316
318
|
const sid = _OC_SID;
|
|
317
|
-
if (state.sessions?.[sid]?.history) {
|
|
319
|
+
if (sid && sid !== "undefined" && state.sessions?.[sid]?.history) {
|
|
318
320
|
_blackboxTracker = _BlackboxStub.deserialize(state.sessions[sid]);
|
|
319
321
|
}
|
|
320
|
-
else if (currentProjectFingerprint) {
|
|
321
|
-
const projectKeys = Object.keys(state.sessions || {}).filter(k => state.sessions[k].project_fingerprint === currentProjectFingerprint);
|
|
322
|
+
else if (currentProjectFingerprint && sid && sid !== "undefined") {
|
|
323
|
+
const projectKeys = Object.keys(state.sessions || {}).filter(k => state.sessions[k].project_fingerprint === currentProjectFingerprint && k !== "undefined" && k !== null && k.trim() !== "");
|
|
322
324
|
const latest = projectKeys.sort().slice(-1)[0];
|
|
323
325
|
if (latest && state.sessions[latest]?.history) {
|
|
324
326
|
const data = state.sessions[latest];
|
|
@@ -589,12 +591,14 @@ export function incrementTurnCounter() {
|
|
|
589
591
|
const sid = _OC_SID;
|
|
590
592
|
if (!state.sessions)
|
|
591
593
|
state.sessions = {};
|
|
592
|
-
if (
|
|
593
|
-
state.sessions[sid]
|
|
594
|
-
|
|
595
|
-
|
|
594
|
+
if (sid && sid !== "undefined") {
|
|
595
|
+
if (!state.sessions[sid])
|
|
596
|
+
state.sessions[sid] = {};
|
|
597
|
+
const next = (state.sessions[sid].turn_counter || 0) + 1;
|
|
598
|
+
state.sessions[sid].turn_counter = next;
|
|
599
|
+
}
|
|
596
600
|
saveBlackboxState(state);
|
|
597
|
-
return
|
|
601
|
+
return 0;
|
|
598
602
|
}
|
|
599
603
|
catch {
|
|
600
604
|
return 0;
|