dual-brain 3.1.0 → 3.3.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/CLAUDE.md +33 -1
- package/hooks/budget-balancer.mjs +45 -6
- package/hooks/control-panel.mjs +489 -0
- package/hooks/cost-logger.mjs +51 -26
- package/hooks/decision-ledger.mjs +299 -0
- package/hooks/dual-brain-review.mjs +106 -17
- package/hooks/dual-brain-think.mjs +81 -17
- package/hooks/enforce-tier.mjs +103 -10
- package/hooks/gpt-work-dispatcher.mjs +50 -6
- package/hooks/profiles.mjs +203 -0
- package/hooks/quality-gate.mjs +34 -6
- package/hooks/summary-checkpoint.mjs +231 -0
- package/install.mjs +402 -33
- package/package.json +2 -2
- package/hooks/usage-2026-05-14.jsonl +0 -5
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* profiles.mjs — Profile system for the Dual-Brain Orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* Profiles configure routing posture, budget limits, and quality gate behavior.
|
|
6
|
+
* Active profile persists to .claude/dual-brain.profile.json.
|
|
7
|
+
*
|
|
8
|
+
* Exported API:
|
|
9
|
+
* PROFILES → built-in profile definitions
|
|
10
|
+
* getActiveProfile() → current profile name + merged settings
|
|
11
|
+
* setActiveProfile(name) → switch profile, returns success/error
|
|
12
|
+
* getProfileOverrides(key) → profile-driven overrides for a specific system
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
16
|
+
import { dirname, join } from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
|
|
21
|
+
const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
22
|
+
|
|
23
|
+
const PROFILES = {
|
|
24
|
+
balanced: {
|
|
25
|
+
description: 'Standard routing — best model for each tier, normal budgets',
|
|
26
|
+
routing: {
|
|
27
|
+
prefer_provider: 'auto',
|
|
28
|
+
think_threshold: 'normal',
|
|
29
|
+
gpt_dispatch_bias: 0,
|
|
30
|
+
},
|
|
31
|
+
budgets: {
|
|
32
|
+
session_warn_usd: 5.00,
|
|
33
|
+
session_limit_usd: 10.00,
|
|
34
|
+
daily_warn_usd: 20.00,
|
|
35
|
+
daily_limit_usd: 50.00,
|
|
36
|
+
},
|
|
37
|
+
quality_gate: {
|
|
38
|
+
sensitivity_floor: 'medium',
|
|
39
|
+
dual_brain_minimum: 'high',
|
|
40
|
+
},
|
|
41
|
+
tier_overrides: null,
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
'cost-saver': {
|
|
45
|
+
description: 'Minimize spend — prefer cheaper models, skip GPT for low risk',
|
|
46
|
+
routing: {
|
|
47
|
+
prefer_provider: 'cheapest',
|
|
48
|
+
think_threshold: 'strict',
|
|
49
|
+
gpt_dispatch_bias: -20,
|
|
50
|
+
},
|
|
51
|
+
budgets: {
|
|
52
|
+
session_warn_usd: 2.00,
|
|
53
|
+
session_limit_usd: 5.00,
|
|
54
|
+
daily_warn_usd: 8.00,
|
|
55
|
+
daily_limit_usd: 20.00,
|
|
56
|
+
},
|
|
57
|
+
quality_gate: {
|
|
58
|
+
sensitivity_floor: 'high',
|
|
59
|
+
dual_brain_minimum: 'critical',
|
|
60
|
+
},
|
|
61
|
+
tier_overrides: {
|
|
62
|
+
promote_execute_to_think: false,
|
|
63
|
+
demote_think_to_execute: true,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
'quality-first': {
|
|
68
|
+
description: 'Maximum quality — dual-brain for medium+, stricter reviews',
|
|
69
|
+
routing: {
|
|
70
|
+
prefer_provider: 'most-capable',
|
|
71
|
+
think_threshold: 'relaxed',
|
|
72
|
+
gpt_dispatch_bias: 10,
|
|
73
|
+
},
|
|
74
|
+
budgets: {
|
|
75
|
+
session_warn_usd: 15.00,
|
|
76
|
+
session_limit_usd: 30.00,
|
|
77
|
+
daily_warn_usd: 50.00,
|
|
78
|
+
daily_limit_usd: 100.00,
|
|
79
|
+
},
|
|
80
|
+
quality_gate: {
|
|
81
|
+
sensitivity_floor: 'low',
|
|
82
|
+
dual_brain_minimum: 'medium',
|
|
83
|
+
},
|
|
84
|
+
tier_overrides: {
|
|
85
|
+
promote_execute_to_think: true,
|
|
86
|
+
demote_think_to_execute: false,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function loadProfileFile() {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function loadConfig() {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
102
|
+
} catch {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getActiveProfile() {
|
|
108
|
+
const saved = loadProfileFile();
|
|
109
|
+
const name = saved?.active || 'balanced';
|
|
110
|
+
const profile = PROFILES[name] || PROFILES.balanced;
|
|
111
|
+
const customOverrides = saved?.custom_overrides || {};
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
name: PROFILES[name] ? name : 'balanced',
|
|
115
|
+
...profile,
|
|
116
|
+
budgets: { ...profile.budgets, ...customOverrides.budgets },
|
|
117
|
+
routing: { ...profile.routing, ...customOverrides.routing },
|
|
118
|
+
switched_at: saved?.switched_at || null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setActiveProfile(name, customOverrides = null) {
|
|
123
|
+
if (!PROFILES[name]) {
|
|
124
|
+
return { ok: false, error: `Unknown profile: ${name}. Available: ${Object.keys(PROFILES).join(', ')}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const data = {
|
|
128
|
+
active: name,
|
|
129
|
+
switched_at: new Date().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
if (customOverrides) data.custom_overrides = customOverrides;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
135
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
136
|
+
renameSync(tmp, PROFILE_FILE);
|
|
137
|
+
return { ok: true, profile: PROFILES[name] };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return { ok: false, error: `Failed to write profile: ${err.message}` };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function setBudgetOverrides(sessionLimit, dailyLimit) {
|
|
144
|
+
const saved = loadProfileFile() || { active: 'balanced' };
|
|
145
|
+
saved.custom_overrides = saved.custom_overrides || {};
|
|
146
|
+
saved.custom_overrides.budgets = {};
|
|
147
|
+
|
|
148
|
+
if (sessionLimit != null) {
|
|
149
|
+
saved.custom_overrides.budgets.session_warn_usd = sessionLimit * 0.6;
|
|
150
|
+
saved.custom_overrides.budgets.session_limit_usd = sessionLimit;
|
|
151
|
+
}
|
|
152
|
+
if (dailyLimit != null) {
|
|
153
|
+
saved.custom_overrides.budgets.daily_warn_usd = dailyLimit * 0.6;
|
|
154
|
+
saved.custom_overrides.budgets.daily_limit_usd = dailyLimit;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
saved.switched_at = saved.switched_at || new Date().toISOString();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
161
|
+
writeFileSync(tmp, JSON.stringify(saved, null, 2) + '\n');
|
|
162
|
+
renameSync(tmp, PROFILE_FILE);
|
|
163
|
+
return { ok: true };
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return { ok: false, error: err.message };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getProfileOverrides(system) {
|
|
170
|
+
const profile = getActiveProfile();
|
|
171
|
+
|
|
172
|
+
switch (system) {
|
|
173
|
+
case 'enforce-tier':
|
|
174
|
+
return {
|
|
175
|
+
think_threshold: profile.routing.think_threshold,
|
|
176
|
+
tier_overrides: profile.tier_overrides,
|
|
177
|
+
gpt_dispatch_bias: profile.routing.gpt_dispatch_bias,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
case 'budget-balancer':
|
|
181
|
+
return {
|
|
182
|
+
budgets: profile.budgets,
|
|
183
|
+
prefer_provider: profile.routing.prefer_provider,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
case 'quality-gate':
|
|
187
|
+
return {
|
|
188
|
+
sensitivity_floor: profile.quality_gate.sensitivity_floor,
|
|
189
|
+
dual_brain_minimum: profile.quality_gate.dual_brain_minimum,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
default:
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export {
|
|
198
|
+
PROFILES,
|
|
199
|
+
getActiveProfile,
|
|
200
|
+
setActiveProfile,
|
|
201
|
+
setBudgetOverrides,
|
|
202
|
+
getProfileOverrides,
|
|
203
|
+
};
|
package/hooks/quality-gate.mjs
CHANGED
|
@@ -23,9 +23,31 @@ import { fileURLToPath } from 'url';
|
|
|
23
23
|
|
|
24
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
25
|
const ORCHESTRATOR_CONFIG = resolve(__dirname, '..', 'orchestrator.json');
|
|
26
|
+
const PROFILE_FILE = resolve(__dirname, '..', 'dual-brain.profile.json');
|
|
26
27
|
const REVIEWS_DIR = resolve(__dirname, '..', 'reviews');
|
|
27
28
|
const DUAL_BRAIN = resolve(__dirname, 'dual-brain-review.mjs');
|
|
28
29
|
|
|
30
|
+
const RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
|
|
31
|
+
|
|
32
|
+
function loadProfileGateSettings() {
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
35
|
+
const name = data.active || 'balanced';
|
|
36
|
+
const defaults = {
|
|
37
|
+
balanced: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
|
|
38
|
+
'cost-saver': { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
|
|
39
|
+
'quality-first': { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
|
|
40
|
+
};
|
|
41
|
+
return defaults[name] || defaults.balanced;
|
|
42
|
+
} catch {
|
|
43
|
+
return { sensitivity_floor: 'medium', dual_brain_minimum: 'high' };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function riskMeetsFloor(risk, floor) {
|
|
48
|
+
return RISK_LEVELS.indexOf(risk) >= RISK_LEVELS.indexOf(floor);
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
function exit(obj) {
|
|
30
52
|
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
31
53
|
process.exit(0);
|
|
@@ -162,14 +184,16 @@ function main() {
|
|
|
162
184
|
// 5a. Score sensitivity BEFORE running any external review
|
|
163
185
|
const sensitivity = scoreSensitivity(qualifyingFiles, config);
|
|
164
186
|
|
|
165
|
-
// 5b.
|
|
166
|
-
|
|
187
|
+
// 5b. Apply profile-driven sensitivity floor
|
|
188
|
+
const profileGate = loadProfileGateSettings();
|
|
189
|
+
if (!riskMeetsFloor(sensitivity.risk, profileGate.sensitivity_floor)) {
|
|
167
190
|
exit({
|
|
168
191
|
gate: 'pass',
|
|
169
|
-
risk:
|
|
192
|
+
risk: sensitivity.risk,
|
|
170
193
|
sensitivity_score: sensitivity.score,
|
|
171
194
|
sensitivity_reasons: sensitivity.reasons,
|
|
172
|
-
reason:
|
|
195
|
+
reason: `${sensitivity.risk} risk — below profile floor (${profileGate.sensitivity_floor})`,
|
|
196
|
+
profile_floor: profileGate.sensitivity_floor,
|
|
173
197
|
files: qualifyingFiles,
|
|
174
198
|
});
|
|
175
199
|
}
|
|
@@ -232,14 +256,18 @@ function main() {
|
|
|
232
256
|
reviewResult.error === true ||
|
|
233
257
|
!reviewResult.review;
|
|
234
258
|
|
|
259
|
+
// Profile can lower the dual-brain threshold
|
|
260
|
+
const needsDualBrain = riskMeetsFloor(sensitivity.risk, profileGate.dual_brain_minimum);
|
|
261
|
+
|
|
235
262
|
let gateStatus;
|
|
236
|
-
if (sensitivity.gate === 'dual-brain-required') {
|
|
237
|
-
// Critical: always flag for dual-brain + user attention regardless of review outcome
|
|
263
|
+
if (sensitivity.gate === 'dual-brain-required' || (needsDualBrain && sensitivity.risk === 'critical')) {
|
|
238
264
|
gateStatus = 'needs_dual_think';
|
|
239
265
|
} else if (reviewUnavailable) {
|
|
240
266
|
gateStatus = 'needs_human_review';
|
|
241
267
|
} else if (reviewResult.issues_found) {
|
|
242
268
|
gateStatus = 'issues_found';
|
|
269
|
+
} else if (needsDualBrain) {
|
|
270
|
+
gateStatus = 'reviewed';
|
|
243
271
|
} else {
|
|
244
272
|
gateStatus = sensitivity.gate === 'dual-brain-recommended' ? 'reviewed' : 'pass';
|
|
245
273
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* summary-checkpoint.mjs — Fast derived state for the hot path.
|
|
4
|
+
*
|
|
5
|
+
* Maintains a summary file (usage-summary-YYYY-MM-DD.json) that hooks
|
|
6
|
+
* can read in O(1) instead of scanning the full JSONL log.
|
|
7
|
+
*
|
|
8
|
+
* The summary is rebuilt from JSONL truth if missing or corrupt.
|
|
9
|
+
*
|
|
10
|
+
* Exported API:
|
|
11
|
+
* readSummary(date?) → current summary object
|
|
12
|
+
* updateSummary(newEntry) → incrementally update summary with one entry
|
|
13
|
+
* rebuildSummary(date?) → full rebuild from JSONL
|
|
14
|
+
* getRecentPromptHashes() → last 10min of prompt hashes (for dupe detection)
|
|
15
|
+
* getPressureBuckets() → provider/tier call counts for rolling window
|
|
16
|
+
* getTokenAverages() → moving averages of actual tokens by tier
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
20
|
+
import { dirname, join } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
|
|
25
|
+
function summaryPath(date) {
|
|
26
|
+
const d = date || new Date().toISOString().slice(0, 10);
|
|
27
|
+
return join(__dirname, `usage-summary-${d}.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function usagePath(date) {
|
|
31
|
+
const d = date || new Date().toISOString().slice(0, 10);
|
|
32
|
+
return join(__dirname, `usage-${d}.jsonl`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function emptySummary() {
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
date: new Date().toISOString().slice(0, 10),
|
|
39
|
+
updated_at: new Date().toISOString(),
|
|
40
|
+
last_offset: 0,
|
|
41
|
+
|
|
42
|
+
totals: {
|
|
43
|
+
calls: 0,
|
|
44
|
+
cost_estimate: 0,
|
|
45
|
+
by_tier: {},
|
|
46
|
+
by_provider: {},
|
|
47
|
+
by_model: {},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
pressure: {
|
|
51
|
+
claude: { think: [], execute: [], search: [] },
|
|
52
|
+
openai: { think: [], execute: [], search: [] },
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
recent_hashes: [],
|
|
56
|
+
|
|
57
|
+
token_averages: {},
|
|
58
|
+
|
|
59
|
+
codex_latencies: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const COST_PER_CALL = { search: 0.003, execute: 0.012, think: 0.055 };
|
|
64
|
+
|
|
65
|
+
function atomicWrite(path, data) {
|
|
66
|
+
const tmp = path + '.tmp.' + process.pid;
|
|
67
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
68
|
+
renameSync(tmp, path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readSummary(date) {
|
|
72
|
+
const path = summaryPath(date);
|
|
73
|
+
try {
|
|
74
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
75
|
+
if (data.version === 1) return data;
|
|
76
|
+
} catch {}
|
|
77
|
+
return rebuildSummary(date);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rebuildSummary(date) {
|
|
81
|
+
const d = date || new Date().toISOString().slice(0, 10);
|
|
82
|
+
const logPath = usagePath(d);
|
|
83
|
+
const summary = emptySummary();
|
|
84
|
+
summary.date = d;
|
|
85
|
+
|
|
86
|
+
if (!existsSync(logPath)) {
|
|
87
|
+
atomicWrite(summaryPath(d), summary);
|
|
88
|
+
return summary;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let raw;
|
|
92
|
+
try { raw = readFileSync(logPath, 'utf8'); } catch { return summary; }
|
|
93
|
+
|
|
94
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
try {
|
|
97
|
+
const entry = JSON.parse(line);
|
|
98
|
+
applyEntry(summary, entry);
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
summary.last_offset = Buffer.byteLength(raw, 'utf8');
|
|
103
|
+
summary.updated_at = new Date().toISOString();
|
|
104
|
+
atomicWrite(summaryPath(d), summary);
|
|
105
|
+
return summary;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function applyEntry(summary, entry) {
|
|
109
|
+
const tier = entry.tier || 'execute';
|
|
110
|
+
const provider = entry.provider || 'claude';
|
|
111
|
+
const model = entry.model || 'unknown';
|
|
112
|
+
const cost = COST_PER_CALL[tier] || COST_PER_CALL.execute;
|
|
113
|
+
|
|
114
|
+
summary.totals.calls++;
|
|
115
|
+
summary.totals.cost_estimate += cost;
|
|
116
|
+
|
|
117
|
+
summary.totals.by_tier[tier] = (summary.totals.by_tier[tier] || 0) + 1;
|
|
118
|
+
summary.totals.by_provider[provider] = (summary.totals.by_provider[provider] || 0) + 1;
|
|
119
|
+
summary.totals.by_model[model] = (summary.totals.by_model[model] || 0) + 1;
|
|
120
|
+
|
|
121
|
+
// Pressure: store timestamps for rolling window lookups
|
|
122
|
+
const ts = entry.timestamp || new Date().toISOString();
|
|
123
|
+
if (summary.pressure[provider]?.[tier]) {
|
|
124
|
+
summary.pressure[provider][tier].push(ts);
|
|
125
|
+
// Keep only last 5 hours of timestamps to bound size
|
|
126
|
+
const cutoff = Date.now() - 5 * 60 * 60 * 1000;
|
|
127
|
+
summary.pressure[provider][tier] = summary.pressure[provider][tier].filter(
|
|
128
|
+
t => Date.parse(t) >= cutoff
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Recent prompt hashes (for duplicate detection)
|
|
133
|
+
if (entry.type === 'tier_recommendation' && entry.prompt_hash) {
|
|
134
|
+
summary.recent_hashes.push({ hash: entry.prompt_hash, ts });
|
|
135
|
+
const tenMinAgo = Date.now() - 10 * 60 * 1000;
|
|
136
|
+
summary.recent_hashes = summary.recent_hashes.filter(
|
|
137
|
+
h => Date.parse(h.ts) >= tenMinAgo
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Token moving averages
|
|
142
|
+
if (entry.input_tokens != null && entry.output_tokens != null) {
|
|
143
|
+
const key = `${provider}:${tier}`;
|
|
144
|
+
if (!summary.token_averages[key]) {
|
|
145
|
+
summary.token_averages[key] = { count: 0, avg_input: 0, avg_output: 0 };
|
|
146
|
+
}
|
|
147
|
+
const avg = summary.token_averages[key];
|
|
148
|
+
avg.count++;
|
|
149
|
+
avg.avg_input += (entry.input_tokens - avg.avg_input) / avg.count;
|
|
150
|
+
avg.avg_output += (entry.output_tokens - avg.avg_output) / avg.count;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Codex latencies
|
|
154
|
+
if (entry.codex_startup_ms != null) {
|
|
155
|
+
summary.codex_latencies.push({
|
|
156
|
+
startup_ms: entry.codex_startup_ms,
|
|
157
|
+
total_ms: entry.codex_total_ms || null,
|
|
158
|
+
model: model,
|
|
159
|
+
ts,
|
|
160
|
+
});
|
|
161
|
+
// Keep last 50
|
|
162
|
+
if (summary.codex_latencies.length > 50) {
|
|
163
|
+
summary.codex_latencies = summary.codex_latencies.slice(-50);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function updateSummary(newEntry, date) {
|
|
169
|
+
const summary = readSummary(date);
|
|
170
|
+
applyEntry(summary, newEntry);
|
|
171
|
+
summary.updated_at = new Date().toISOString();
|
|
172
|
+
atomicWrite(summaryPath(date), summary);
|
|
173
|
+
return summary;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getRecentPromptHashes(date) {
|
|
177
|
+
const summary = readSummary(date);
|
|
178
|
+
const tenMinAgo = Date.now() - 10 * 60 * 1000;
|
|
179
|
+
return summary.recent_hashes.filter(h => Date.parse(h.ts) >= tenMinAgo);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getPressureBuckets(date) {
|
|
183
|
+
const summary = readSummary(date);
|
|
184
|
+
const cutoff = Date.now() - 5 * 60 * 60 * 1000;
|
|
185
|
+
const result = {};
|
|
186
|
+
|
|
187
|
+
for (const provider of ['claude', 'openai']) {
|
|
188
|
+
result[provider] = {};
|
|
189
|
+
for (const tier of ['think', 'execute', 'search']) {
|
|
190
|
+
const timestamps = summary.pressure[provider]?.[tier] || [];
|
|
191
|
+
result[provider][tier] = timestamps.filter(t => Date.parse(t) >= cutoff).length;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getTokenAverages(date) {
|
|
198
|
+
const summary = readSummary(date);
|
|
199
|
+
return summary.token_averages;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getAdaptiveCodexThreshold(date) {
|
|
203
|
+
const summary = readSummary(date);
|
|
204
|
+
const latencies = summary.codex_latencies || [];
|
|
205
|
+
if (latencies.length < 5) return { threshold_ms: 180_000, confidence: 'low', samples: latencies.length };
|
|
206
|
+
|
|
207
|
+
const startups = latencies.map(l => l.startup_ms).filter(Boolean).sort((a, b) => a - b);
|
|
208
|
+
if (startups.length < 3) return { threshold_ms: 180_000, confidence: 'low', samples: startups.length };
|
|
209
|
+
|
|
210
|
+
const p75idx = Math.floor(startups.length * 0.75);
|
|
211
|
+
const p75 = startups[p75idx];
|
|
212
|
+
const threshold = Math.max(90_000, p75 * 4);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
threshold_ms: Math.round(threshold),
|
|
216
|
+
p75_startup_ms: Math.round(p75),
|
|
217
|
+
confidence: startups.length >= 20 ? 'high' : 'medium',
|
|
218
|
+
samples: startups.length,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export {
|
|
223
|
+
readSummary,
|
|
224
|
+
updateSummary,
|
|
225
|
+
rebuildSummary,
|
|
226
|
+
getRecentPromptHashes,
|
|
227
|
+
getPressureBuckets,
|
|
228
|
+
getTokenAverages,
|
|
229
|
+
getAdaptiveCodexThreshold,
|
|
230
|
+
atomicWrite,
|
|
231
|
+
};
|