dual-brain 0.2.30 → 0.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/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/routing-advisor.mjs
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
// routing-advisor.mjs — EMA + epsilon-greedy routing advisor
|
|
2
|
-
// Learns which model works best for which task type from outcome signals.
|
|
3
|
-
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
5
|
-
import { checkFileSurvival } from './outcome.mjs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
|
|
8
|
-
const ALPHA = 0.3;
|
|
9
|
-
const MIN_EPSILON = 0.1;
|
|
10
|
-
const MIN_OBSERVATIONS = 5;
|
|
11
|
-
const PRIOR_WEIGHT = 5;
|
|
12
|
-
|
|
13
|
-
const STATIC_PRIORS = {
|
|
14
|
-
'search:haiku': 0.85, 'search:sonnet': 0.70, 'search:opus': 0.50,
|
|
15
|
-
'execute:haiku': 0.55, 'execute:sonnet': 0.80, 'execute:opus': 0.85,
|
|
16
|
-
'think:haiku': 0.30, 'think:sonnet': 0.70, 'think:opus': 0.90,
|
|
17
|
-
'review:haiku': 0.40, 'review:sonnet': 0.75, 'review:opus': 0.85,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const VALID_MODELS = {
|
|
21
|
-
search: ['haiku', 'sonnet'],
|
|
22
|
-
execute: ['haiku', 'sonnet', 'opus'],
|
|
23
|
-
think: ['sonnet', 'opus'],
|
|
24
|
-
review: ['sonnet', 'opus'],
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function stateFile(cwd) { return join(cwd || process.cwd(), '.dualbrain', 'routing-state.json'); }
|
|
28
|
-
|
|
29
|
-
function loadState(cwd) {
|
|
30
|
-
try {
|
|
31
|
-
const p = stateFile(cwd);
|
|
32
|
-
if (!existsSync(p)) return {};
|
|
33
|
-
const raw = readFileSync(p, 'utf8');
|
|
34
|
-
const parsed = JSON.parse(raw);
|
|
35
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
36
|
-
return parsed;
|
|
37
|
-
} catch { return {}; }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function saveState(state, cwd) {
|
|
41
|
-
try {
|
|
42
|
-
const dir = join(cwd || process.cwd(), '.dualbrain');
|
|
43
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
44
|
-
const p = stateFile(cwd), tmp = p + '.tmp';
|
|
45
|
-
writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf8');
|
|
46
|
-
renameSync(tmp, p);
|
|
47
|
-
} catch { /* non-throwing */ }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Cross-cell bias: average EMA from same-tier cells that have >= 8 observations. */
|
|
51
|
-
function getCrossCellBias(state, cellKey, model) {
|
|
52
|
-
const [tier] = cellKey.split(':');
|
|
53
|
-
let biasSum = 0, biasCount = 0;
|
|
54
|
-
for (const [key, models] of Object.entries(state)) {
|
|
55
|
-
if (key.startsWith(tier + ':') && key !== cellKey && models[model]) {
|
|
56
|
-
const entry = models[model];
|
|
57
|
-
if ((entry.observations ?? 0) >= 8) { biasSum += entry.ema; biasCount++; }
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return biasCount > 0 ? biasSum / biasCount : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const staticPrior = (tier, model) => STATIC_PRIORS[`${tier}:${model}`] ?? 0.5;
|
|
64
|
-
const cellObs = (state, key) => Object.values(state[key] ?? {}).reduce((s, m) => s + (m.observations ?? 0), 0);
|
|
65
|
-
const blended = (ema, n, tier, model) =>
|
|
66
|
-
(n / (n + PRIOR_WEIGHT)) * ema + (PRIOR_WEIGHT / (n + PRIOR_WEIGHT)) * staticPrior(tier, model);
|
|
67
|
-
|
|
68
|
-
// taskProfile: { intent, tier, risk, files?, complexity? }
|
|
69
|
-
// Returns: { model, reason, confidence, explored }
|
|
70
|
-
export function adviseModel(taskProfile, cwd) {
|
|
71
|
-
try {
|
|
72
|
-
const { tier, intent } = taskProfile ?? {};
|
|
73
|
-
const validTier = tier && VALID_MODELS[tier] ? tier : 'execute';
|
|
74
|
-
const cellKey = `${validTier}:${intent ?? 'implement'}`;
|
|
75
|
-
const models = VALID_MODELS[validTier];
|
|
76
|
-
|
|
77
|
-
const state = loadState(cwd);
|
|
78
|
-
const totalObs = cellObs(state, cellKey);
|
|
79
|
-
const grandTotal = Object.values(state).reduce((s, cell) =>
|
|
80
|
-
s + Object.values(cell).reduce((t, e) => t + (e.observations ?? 0), 0), 0);
|
|
81
|
-
|
|
82
|
-
if (totalObs < MIN_OBSERVATIONS) {
|
|
83
|
-
// When enough global data exists, blend cross-cell bias with static prior
|
|
84
|
-
if (grandTotal > 100) {
|
|
85
|
-
let bestModel = models[0], bestScore = -Infinity;
|
|
86
|
-
for (const m of models) {
|
|
87
|
-
const xbias = getCrossCellBias(state, cellKey, m);
|
|
88
|
-
const prior = staticPrior(validTier, m);
|
|
89
|
-
const score = xbias != null ? (xbias + prior) / 2 : prior;
|
|
90
|
-
if (score > bestScore) { bestScore = score; bestModel = m; }
|
|
91
|
-
}
|
|
92
|
-
return { model: bestModel, reason: 'cross-cell bias', confidence: 0.4, explored: false };
|
|
93
|
-
}
|
|
94
|
-
const best = models.reduce((a, b) => staticPrior(validTier, a) >= staticPrior(validTier, b) ? a : b);
|
|
95
|
-
return { model: best, reason: 'insufficient data, using heuristic', confidence: 0.3, explored: false };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const epsilon = Math.max(MIN_EPSILON, 0.5 * Math.pow(0.9, totalObs));
|
|
99
|
-
const explored = Math.random() < epsilon;
|
|
100
|
-
|
|
101
|
-
if (explored) {
|
|
102
|
-
const model = models[Math.floor(Math.random() * models.length)];
|
|
103
|
-
return { model, reason: 'exploration', confidence: epsilon, explored: true };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Exploitation: pick highest blended score
|
|
107
|
-
const cell = state[cellKey] ?? {};
|
|
108
|
-
let bestModel = models[0];
|
|
109
|
-
let bestScore = -Infinity;
|
|
110
|
-
for (const m of models) {
|
|
111
|
-
const entry = cell[m];
|
|
112
|
-
const ema = entry?.ema ?? staticPrior(validTier, m);
|
|
113
|
-
const n = entry?.observations ?? 0;
|
|
114
|
-
const score = blended(ema, n, validTier, m);
|
|
115
|
-
if (score > bestScore) { bestScore = score; bestModel = m; }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return { model: bestModel, reason: 'exploitation', confidence: 1 - epsilon, explored: false };
|
|
119
|
-
} catch {
|
|
120
|
-
return { model: 'sonnet', reason: 'error fallback', confidence: 0.1, explored: false };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// reward: number in [0, 1]
|
|
125
|
-
export function recordReward(cellKey, model, reward, cwd) {
|
|
126
|
-
try {
|
|
127
|
-
const state = loadState(cwd);
|
|
128
|
-
if (!state[cellKey]) state[cellKey] = {};
|
|
129
|
-
const entry = state[cellKey][model] ?? { ema: reward, observations: 0 };
|
|
130
|
-
entry.ema = ALPHA * reward + (1 - ALPHA) * entry.ema;
|
|
131
|
-
entry.observations = (entry.observations ?? 0) + 1;
|
|
132
|
-
entry.lastUpdated = new Date().toISOString();
|
|
133
|
-
entry.lastReward = reward;
|
|
134
|
-
state[cellKey][model] = entry;
|
|
135
|
-
saveState(state, cwd);
|
|
136
|
-
} catch {
|
|
137
|
-
// non-throwing
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function getRoutingStats(cwd) {
|
|
142
|
-
try {
|
|
143
|
-
const state = loadState(cwd);
|
|
144
|
-
const cells = {}, flat = [];
|
|
145
|
-
let totalObservations = 0;
|
|
146
|
-
for (const [cellKey, models] of Object.entries(state)) {
|
|
147
|
-
cells[cellKey] ??= {};
|
|
148
|
-
for (const [model, entry] of Object.entries(models)) {
|
|
149
|
-
const obs = entry.observations ?? 0;
|
|
150
|
-
cells[cellKey][model] = { ema: entry.ema, observations: obs };
|
|
151
|
-
totalObservations += obs;
|
|
152
|
-
flat.push({ cell: cellKey, model, ema: entry.ema, observations: obs });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
flat.sort((a, b) => b.ema - a.ema);
|
|
156
|
-
return { cells, totalObservations, topPerformers: flat.slice(0, 5), worstPerformers: flat.slice(-5).reverse() };
|
|
157
|
-
} catch {
|
|
158
|
-
return { cells: {}, totalObservations: 0, topPerformers: [], worstPerformers: [] };
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Loads cross-session routing state. If the state was last updated in a prior session,
|
|
164
|
-
* applies a mild decay (×0.95) to all EMA scores to account for staleness.
|
|
165
|
-
*/
|
|
166
|
-
export function loadCrossSessionPriors(cwd) {
|
|
167
|
-
try {
|
|
168
|
-
const state = loadState(cwd);
|
|
169
|
-
const sessionStart = state._sessionStart;
|
|
170
|
-
if (!sessionStart) return state; // no prior session marker
|
|
171
|
-
const lastMs = new Date(sessionStart).getTime();
|
|
172
|
-
if (isNaN(lastMs)) return state;
|
|
173
|
-
const stale = (Date.now() - lastMs) > 60_000; // more than 1 min old = different session
|
|
174
|
-
if (!stale) return state;
|
|
175
|
-
for (const [cellKey, models] of Object.entries(state)) {
|
|
176
|
-
if (cellKey.startsWith('_')) continue;
|
|
177
|
-
for (const entry of Object.values(models)) {
|
|
178
|
-
if (typeof entry.ema === 'number') entry.ema = entry.ema * 0.95;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return state;
|
|
182
|
-
} catch { return {}; }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Records session start timestamp and triggers file survival checks.
|
|
187
|
-
* Call once at CLI session start.
|
|
188
|
-
*/
|
|
189
|
-
export async function markSessionStart(cwd) {
|
|
190
|
-
try {
|
|
191
|
-
const state = loadState(cwd);
|
|
192
|
-
state._sessionStart = new Date().toISOString();
|
|
193
|
-
saveState(state, cwd);
|
|
194
|
-
await checkFileSurvival(cwd).catch(() => {});
|
|
195
|
-
} catch { /* non-throwing */ }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function resetAdvisor(cwd) {
|
|
199
|
-
try {
|
|
200
|
-
saveState({}, cwd);
|
|
201
|
-
} catch {
|
|
202
|
-
// non-throwing
|
|
203
|
-
}
|
|
204
|
-
}
|
package/src/self-correct.mjs
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// self-correct.mjs — Failure analysis and retry strategy selection
|
|
2
|
-
|
|
3
|
-
const MODEL_TIER = { 'haiku': 1, 'sonnet': 2, 'opus': 3 };
|
|
4
|
-
const TIER_MODEL = { 1: 'haiku', 2: 'sonnet', 3: 'opus' };
|
|
5
|
-
const MAX_ATTEMPTS = 3;
|
|
6
|
-
|
|
7
|
-
function modelTier(model = '') {
|
|
8
|
-
const m = model.toLowerCase();
|
|
9
|
-
if (m.includes('haiku')) return 1;
|
|
10
|
-
if (m.includes('opus')) return 3;
|
|
11
|
-
return 2; // sonnet default
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function matchesAny(text, keywords) {
|
|
15
|
-
const t = text.toLowerCase();
|
|
16
|
-
return keywords.some(k => t.includes(k));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Export 1: classifyFailure(result)
|
|
20
|
-
export function classifyFailure(result) {
|
|
21
|
-
try {
|
|
22
|
-
const err = String(result?.error || result?.stderr || '');
|
|
23
|
-
const out = String(result?.output || result?.stdout || '');
|
|
24
|
-
const combined = err + ' ' + out;
|
|
25
|
-
const duration = result?.durationMs ?? 0;
|
|
26
|
-
const timeoutThreshold = result?.timeoutMs ?? 60_000;
|
|
27
|
-
|
|
28
|
-
if (matchesAny(combined, ['rate limit', 'ratelimit', '429', 'quota exceeded', 'capacity'])) {
|
|
29
|
-
return { type: 'rate-limit', confidence: 0.95, retryable: true };
|
|
30
|
-
}
|
|
31
|
-
if (matchesAny(combined, ['timeout', 'timed out']) || duration > timeoutThreshold) {
|
|
32
|
-
return { type: 'timeout', confidence: 0.9, retryable: true };
|
|
33
|
-
}
|
|
34
|
-
if (matchesAny(combined, ['context length', 'token limit', 'too long', 'maximum context', 'context window'])) {
|
|
35
|
-
return { type: 'context-overflow', confidence: 0.9, retryable: true };
|
|
36
|
-
}
|
|
37
|
-
if (matchesAny(combined, ['ambiguous', 'unclear', 'did you mean', 'which one', 'could you clarify', 'please clarify'])) {
|
|
38
|
-
return { type: 'specification', confidence: 0.85, retryable: false };
|
|
39
|
-
}
|
|
40
|
-
if (matchesAny(combined, ['unable to', "i don't know how", 'beyond my', 'cannot complete', 'incomplete'])) {
|
|
41
|
-
return { type: 'capability', confidence: 0.8, retryable: true };
|
|
42
|
-
}
|
|
43
|
-
// Heuristic: low quality output without explicit error signals capability gap
|
|
44
|
-
const quality = result?.quality ?? result?.score ?? null;
|
|
45
|
-
if (quality !== null && quality < 0.5) {
|
|
46
|
-
return { type: 'capability', confidence: 0.7, retryable: true };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return { type: 'unknown', confidence: 0.5, retryable: true };
|
|
50
|
-
} catch {
|
|
51
|
-
return { type: 'unknown', confidence: 0, retryable: true };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Export 2: selectStrategy(failure, originalDecision, attemptNumber)
|
|
56
|
-
export function selectStrategy(failure, originalDecision, attemptNumber) {
|
|
57
|
-
try {
|
|
58
|
-
if (!failure.retryable) {
|
|
59
|
-
return { strategy: 'give-up', reason: `failure type '${failure.type}' requires user input` };
|
|
60
|
-
}
|
|
61
|
-
if (attemptNumber >= MAX_ATTEMPTS) {
|
|
62
|
-
return { strategy: 'give-up', reason: `max attempts (${MAX_ATTEMPTS}) reached` };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const tier = modelTier(originalDecision?.model);
|
|
66
|
-
|
|
67
|
-
if (attemptNumber === 1) {
|
|
68
|
-
switch (failure.type) {
|
|
69
|
-
case 'capability':
|
|
70
|
-
if (tier >= 3) return { strategy: 'split', newDecision: originalDecision, reason: 'already at max tier; decompose task' };
|
|
71
|
-
return { strategy: 'escalate', newDecision: originalDecision, reason: 'model lacked capability; escalating tier' };
|
|
72
|
-
case 'timeout':
|
|
73
|
-
return { strategy: 'wait-retry', newDecision: originalDecision, reason: 'timed out; retrying with delay' };
|
|
74
|
-
case 'rate-limit':
|
|
75
|
-
return { strategy: 'wait-retry', newDecision: originalDecision, reason: 'rate limited; retrying after delay' };
|
|
76
|
-
case 'context-overflow':
|
|
77
|
-
return { strategy: 'compress', newDecision: originalDecision, reason: 'context too large; compressing' };
|
|
78
|
-
case 'specification':
|
|
79
|
-
return { strategy: 'give-up', reason: 'ambiguous specification; user clarification needed' };
|
|
80
|
-
default: // unknown
|
|
81
|
-
if (tier >= 3) return { strategy: 'split', newDecision: originalDecision, reason: 'unknown failure at max tier; decomposing' };
|
|
82
|
-
return { strategy: 'escalate', newDecision: originalDecision, reason: 'unknown failure; escalating as precaution' };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (attemptNumber === 2) {
|
|
87
|
-
if (tier >= 3) {
|
|
88
|
-
return { strategy: 'split', newDecision: originalDecision, reason: 'max tier reached; splitting task' };
|
|
89
|
-
}
|
|
90
|
-
return { strategy: 'escalate', newDecision: originalDecision, reason: 'retry failed; escalating one final tier' };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { strategy: 'give-up', reason: 'exhausted retry budget' };
|
|
94
|
-
} catch {
|
|
95
|
-
return { strategy: 'give-up', reason: 'internal error in strategy selection' };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Export 3: buildRetryDecision(originalDecision, strategy, failure)
|
|
100
|
-
export function buildRetryDecision(originalDecision, strategy, failure) {
|
|
101
|
-
try {
|
|
102
|
-
const base = {
|
|
103
|
-
...originalDecision,
|
|
104
|
-
_retryAttempt: (originalDecision?._retryAttempt ?? 0) + 1,
|
|
105
|
-
_retryReason: failure.type,
|
|
106
|
-
_retryStrategy: strategy,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
switch (strategy) {
|
|
110
|
-
case 'escalate': {
|
|
111
|
-
const tier = modelTier(originalDecision?.model);
|
|
112
|
-
const nextTier = Math.min(tier + 1, 3);
|
|
113
|
-
return { ...base, model: TIER_MODEL[nextTier] };
|
|
114
|
-
}
|
|
115
|
-
case 'compress':
|
|
116
|
-
return { ...base, _contextBudget: 0.5 };
|
|
117
|
-
case 'wait-retry':
|
|
118
|
-
return { ...base, _delayMs: 5000 };
|
|
119
|
-
case 'rethink':
|
|
120
|
-
return { ...base, tier: 'think', _retryAsThink: true };
|
|
121
|
-
case 'split':
|
|
122
|
-
return { ...base, _shouldDecompose: true };
|
|
123
|
-
default:
|
|
124
|
-
return base;
|
|
125
|
-
}
|
|
126
|
-
} catch {
|
|
127
|
-
return { ...originalDecision, _retryAttempt: 1, _retryReason: 'error', _retryStrategy: strategy };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Export 4: shouldRetry(result, originalDecision, attemptNumber)
|
|
132
|
-
export function shouldRetry(result, originalDecision, attemptNumber = 1) {
|
|
133
|
-
try {
|
|
134
|
-
if (attemptNumber >= MAX_ATTEMPTS) return { retry: false, reason: `max attempts (${MAX_ATTEMPTS}) reached`, strategy: 'give-up' };
|
|
135
|
-
const failure = classifyFailure(result);
|
|
136
|
-
const { strategy, newDecision, reason } = selectStrategy(failure, originalDecision, attemptNumber);
|
|
137
|
-
|
|
138
|
-
if (strategy === 'give-up') {
|
|
139
|
-
return { retry: false, reason, strategy };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const decision = buildRetryDecision(newDecision ?? originalDecision, strategy, failure);
|
|
143
|
-
return { retry: true, decision, reason, strategy };
|
|
144
|
-
} catch {
|
|
145
|
-
return { retry: false, reason: 'internal error in shouldRetry', strategy: 'give-up' };
|
|
146
|
-
}
|
|
147
|
-
}
|
package/src/session-lock.mjs
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// session-lock.mjs — Ensures one active HEAD session at a time.
|
|
2
|
-
// If two shells/chats open, only one owns the cognitive state.
|
|
3
|
-
// The other gets read-only access (can observe but not dispatch).
|
|
4
|
-
//
|
|
5
|
-
// "One ring rules them all" — no split-brain.
|
|
6
|
-
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
|
|
10
|
-
const STATE_DIR = join(process.cwd(), '.dualbrain');
|
|
11
|
-
const LOCK_FILE = join(STATE_DIR, 'session.lock');
|
|
12
|
-
|
|
13
|
-
const STALE_THRESHOLD_MS = 90_000; // 90 seconds without heartbeat = stale
|
|
14
|
-
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
15
|
-
|
|
16
|
-
let _heartbeatTimer = null;
|
|
17
|
-
let _sessionId = null;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {object} LockResult
|
|
21
|
-
* @property {boolean} acquired - Whether this session owns HEAD
|
|
22
|
-
* @property {string} sessionId - This session's ID
|
|
23
|
-
* @property {string|null} existingSession - ID of the session that already holds the lock (if not acquired)
|
|
24
|
-
* @property {string} mode - 'primary' | 'takeover' | 'readonly'
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Attempt to acquire the session lock.
|
|
29
|
-
* - If no lock exists or lock is stale: acquire as primary
|
|
30
|
-
* - If lock is fresh and held by another: return readonly
|
|
31
|
-
*
|
|
32
|
-
* @param {object} opts
|
|
33
|
-
* @param {boolean} opts.force - Force takeover even if existing session is fresh
|
|
34
|
-
* @returns {LockResult}
|
|
35
|
-
*/
|
|
36
|
-
export function acquire({ force = false } = {}) {
|
|
37
|
-
mkdirSync(STATE_DIR, { recursive: true });
|
|
38
|
-
_sessionId = _generateSessionId();
|
|
39
|
-
|
|
40
|
-
const existing = _readLock();
|
|
41
|
-
|
|
42
|
-
if (!existing) {
|
|
43
|
-
// No lock — claim it
|
|
44
|
-
_writeLock(_sessionId);
|
|
45
|
-
_startHeartbeat();
|
|
46
|
-
return { acquired: true, sessionId: _sessionId, existingSession: null, mode: 'primary' };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Same process (re-entry within same session) — always grant
|
|
50
|
-
if (existing.pid === process.pid) {
|
|
51
|
-
_sessionId = existing.sessionId;
|
|
52
|
-
return { acquired: true, sessionId: _sessionId, existingSession: null, mode: 'primary' };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const age = Date.now() - existing.heartbeat;
|
|
56
|
-
|
|
57
|
-
if (age > STALE_THRESHOLD_MS || force) {
|
|
58
|
-
// Stale or forced takeover
|
|
59
|
-
_writeLock(_sessionId);
|
|
60
|
-
_startHeartbeat();
|
|
61
|
-
return { acquired: true, sessionId: _sessionId, existingSession: existing.sessionId, mode: 'takeover' };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Another session is active — go readonly
|
|
65
|
-
return { acquired: false, sessionId: _sessionId, existingSession: existing.sessionId, mode: 'readonly' };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Release the session lock (called at session end).
|
|
70
|
-
*/
|
|
71
|
-
export function release() {
|
|
72
|
-
if (_heartbeatTimer) {
|
|
73
|
-
clearInterval(_heartbeatTimer);
|
|
74
|
-
_heartbeatTimer = null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const existing = _readLock();
|
|
79
|
-
if (existing && existing.sessionId === _sessionId) {
|
|
80
|
-
unlinkSync(LOCK_FILE);
|
|
81
|
-
}
|
|
82
|
-
} catch {}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Check if this session currently holds the lock.
|
|
87
|
-
* @returns {boolean}
|
|
88
|
-
*/
|
|
89
|
-
export function isOwner() {
|
|
90
|
-
const existing = _readLock();
|
|
91
|
-
return existing?.sessionId === _sessionId;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get current lock status without modifying it.
|
|
96
|
-
* @returns {{active: boolean, sessionId: string|null, age: number|null}}
|
|
97
|
-
*/
|
|
98
|
-
export function status() {
|
|
99
|
-
const existing = _readLock();
|
|
100
|
-
if (!existing) return { active: false, sessionId: null, age: null };
|
|
101
|
-
return {
|
|
102
|
-
active: (Date.now() - existing.heartbeat) < STALE_THRESHOLD_MS,
|
|
103
|
-
sessionId: existing.sessionId,
|
|
104
|
-
age: Date.now() - existing.heartbeat,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Manually heartbeat (useful if the automatic timer isn't running).
|
|
110
|
-
*/
|
|
111
|
-
export function heartbeat() {
|
|
112
|
-
if (!_sessionId) return;
|
|
113
|
-
const existing = _readLock();
|
|
114
|
-
if (existing && existing.sessionId === _sessionId) {
|
|
115
|
-
_writeLock(_sessionId);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── Internal ──────────────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
function _generateSessionId() {
|
|
122
|
-
return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function _readLock() {
|
|
126
|
-
try {
|
|
127
|
-
if (!existsSync(LOCK_FILE)) return null;
|
|
128
|
-
return JSON.parse(readFileSync(LOCK_FILE, 'utf8'));
|
|
129
|
-
} catch {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function _writeLock(sessionId) {
|
|
135
|
-
const lock = {
|
|
136
|
-
sessionId,
|
|
137
|
-
heartbeat: Date.now(),
|
|
138
|
-
pid: process.pid,
|
|
139
|
-
};
|
|
140
|
-
writeFileSync(LOCK_FILE, JSON.stringify(lock));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function _startHeartbeat() {
|
|
144
|
-
if (_heartbeatTimer) clearInterval(_heartbeatTimer);
|
|
145
|
-
_heartbeatTimer = setInterval(() => {
|
|
146
|
-
try {
|
|
147
|
-
const existing = _readLock();
|
|
148
|
-
if (existing && existing.sessionId === _sessionId) {
|
|
149
|
-
_writeLock(_sessionId);
|
|
150
|
-
} else {
|
|
151
|
-
// Someone else took over — stop heartbeating
|
|
152
|
-
clearInterval(_heartbeatTimer);
|
|
153
|
-
_heartbeatTimer = null;
|
|
154
|
-
}
|
|
155
|
-
} catch {}
|
|
156
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
157
|
-
|
|
158
|
-
// Don't keep the process alive just for heartbeats
|
|
159
|
-
if (_heartbeatTimer.unref) _heartbeatTimer.unref();
|
|
160
|
-
}
|