dual-brain 0.2.26 → 0.2.27
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/bin/dual-brain.mjs +41 -0
- package/package.json +4 -2
- package/src/decide.mjs +45 -0
- package/src/dispatch.mjs +46 -0
- package/src/outcome.mjs +28 -0
- package/src/self-correct.mjs +145 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -1094,6 +1094,18 @@ async function cmdStatus(args = []) {
|
|
|
1094
1094
|
}
|
|
1095
1095
|
}
|
|
1096
1096
|
} catch { /* network unavailable — skip */ }
|
|
1097
|
+
|
|
1098
|
+
// Show top recommendation if available
|
|
1099
|
+
try {
|
|
1100
|
+
const { getTopRecommendation } = await import('../src/recommendations.mjs');
|
|
1101
|
+
const rec = getTopRecommendation(process.cwd());
|
|
1102
|
+
if (rec) {
|
|
1103
|
+
console.log('');
|
|
1104
|
+
console.log(` \x1b[33m💡 ${rec.title}\x1b[0m`);
|
|
1105
|
+
console.log(` ${rec.description}`);
|
|
1106
|
+
if (rec.action) console.log(` → ${rec.action}`);
|
|
1107
|
+
}
|
|
1108
|
+
} catch { /* non-blocking */ }
|
|
1097
1109
|
}
|
|
1098
1110
|
|
|
1099
1111
|
// ─── cmdHot / cmdCool ─────────────────────────────────────────────────────────
|
|
@@ -6532,6 +6544,18 @@ async function main() {
|
|
|
6532
6544
|
}
|
|
6533
6545
|
|
|
6534
6546
|
if (cmd === 'init') {
|
|
6547
|
+
// init --reconfigure: run setup-flow reconfiguration
|
|
6548
|
+
if (args.includes('--reconfigure')) {
|
|
6549
|
+
try {
|
|
6550
|
+
const { runSetup } = await import('../src/setup-flow.mjs');
|
|
6551
|
+
await runSetup(process.cwd(), { reconfigure: true });
|
|
6552
|
+
} catch (e) {
|
|
6553
|
+
console.error('setup-flow.mjs not available — skipping reconfigure');
|
|
6554
|
+
if (process.env.DEBUG) console.error(e.message);
|
|
6555
|
+
}
|
|
6556
|
+
return;
|
|
6557
|
+
}
|
|
6558
|
+
|
|
6535
6559
|
// init --reset: clear credentials.json and re-run wizard
|
|
6536
6560
|
if (args.includes('--reset')) {
|
|
6537
6561
|
const cwd = process.cwd();
|
|
@@ -6593,6 +6617,23 @@ async function main() {
|
|
|
6593
6617
|
return;
|
|
6594
6618
|
}
|
|
6595
6619
|
|
|
6620
|
+
if (cmd === 'setup') {
|
|
6621
|
+
const { runSetup } = await import('../src/setup-flow.mjs');
|
|
6622
|
+
await runSetup(process.cwd(), { reconfigure: args.includes('--reconfigure') });
|
|
6623
|
+
return;
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
if (cmd === 'advice' || cmd === 'recommend') {
|
|
6627
|
+
const { generateRecommendations, formatRecommendations } = await import('../src/recommendations.mjs');
|
|
6628
|
+
const recs = generateRecommendations(process.cwd());
|
|
6629
|
+
if (recs.length === 0) {
|
|
6630
|
+
console.log(' No recommendations yet. Need 20+ dispatches to generate advice.');
|
|
6631
|
+
} else {
|
|
6632
|
+
console.log(formatRecommendations(recs));
|
|
6633
|
+
}
|
|
6634
|
+
return;
|
|
6635
|
+
}
|
|
6636
|
+
|
|
6596
6637
|
// One-shot commands — run and exit
|
|
6597
6638
|
if (cmd === 'install') {
|
|
6598
6639
|
if (args.includes('--global')) { await installGlobal(); return; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"./routing-advisor": "./src/routing-advisor.mjs",
|
|
53
53
|
"./subscription": "./src/subscription.mjs",
|
|
54
54
|
"./recommendations": "./src/recommendations.mjs",
|
|
55
|
-
"./setup-flow": "./src/setup-flow.mjs"
|
|
55
|
+
"./setup-flow": "./src/setup-flow.mjs",
|
|
56
|
+
"./self-correct": "./src/self-correct.mjs"
|
|
56
57
|
},
|
|
57
58
|
"keywords": [
|
|
58
59
|
"claude-code",
|
|
@@ -144,6 +145,7 @@
|
|
|
144
145
|
"src/subscription.mjs",
|
|
145
146
|
"src/recommendations.mjs",
|
|
146
147
|
"src/setup-flow.mjs",
|
|
148
|
+
"src/self-correct.mjs",
|
|
147
149
|
"bin/*.mjs",
|
|
148
150
|
"hooks/enforce-tier.mjs",
|
|
149
151
|
"hooks/cost-logger.mjs",
|
package/src/decide.mjs
CHANGED
|
@@ -49,6 +49,28 @@ function _loadModelRegistry() {
|
|
|
49
49
|
// Kick off the load immediately so it is ready before the first routing call.
|
|
50
50
|
_loadModelRegistry();
|
|
51
51
|
|
|
52
|
+
// ─── Routing Advisor (optional, lazy-loaded) ──────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Cached reference to routing-advisor.mjs exports. Populated on first import.
|
|
56
|
+
* Remains null if unavailable — decideRoute skips advisor consultation in that case.
|
|
57
|
+
*/
|
|
58
|
+
let routingAdvisor = null;
|
|
59
|
+
let _advisorLoadAttempted = false;
|
|
60
|
+
|
|
61
|
+
function _loadRoutingAdvisor() {
|
|
62
|
+
if (_advisorLoadAttempted) return;
|
|
63
|
+
_advisorLoadAttempted = true;
|
|
64
|
+
import('./routing-advisor.mjs').then(mod => {
|
|
65
|
+
routingAdvisor = mod;
|
|
66
|
+
}).catch(() => {
|
|
67
|
+
// routing-advisor.mjs unavailable — skip learned routing
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Kick off the load immediately so it is ready before the first routing call.
|
|
72
|
+
_loadRoutingAdvisor();
|
|
73
|
+
|
|
52
74
|
// ─── Work Styles ─────────────────────────────────────────────────────────────
|
|
53
75
|
|
|
54
76
|
/**
|
|
@@ -890,6 +912,28 @@ export function decideRoute({ profile = {}, detection = {}, cwd, thinkResult, se
|
|
|
890
912
|
// If the pipeline changed the model (downgrade/bias/floor), resolve the new short name to a full ID.
|
|
891
913
|
model = toFullModelId(model, provider, tier);
|
|
892
914
|
|
|
915
|
+
// ── Routing advisor: consult learned EMA model for this task type ─────────
|
|
916
|
+
// Non-blocking: only overrides when advisor has enough observations (confidence > 0.3).
|
|
917
|
+
// Uses short model names; advisor only covers Claude models (haiku/sonnet/opus).
|
|
918
|
+
let _advisorOverride = null;
|
|
919
|
+
if (routingAdvisor && provider === 'claude') {
|
|
920
|
+
try {
|
|
921
|
+
const advice = routingAdvisor.adviseModel(
|
|
922
|
+
{ intent: detection.intent, tier, risk: detection.risk },
|
|
923
|
+
cwd
|
|
924
|
+
);
|
|
925
|
+
if (advice.confidence > 0.3 && advice.model) {
|
|
926
|
+
const advisorShort = advice.model; // advisor returns short names
|
|
927
|
+
const previousModel = toShortName(model, 'claude');
|
|
928
|
+
if (advisorShort !== previousModel && available.claude.includes(advisorShort)) {
|
|
929
|
+
const overrideFullId = toFullModelId(advisorShort, 'claude', tier);
|
|
930
|
+
_advisorOverride = { from: model, to: overrideFullId, reason: advice.reason, explored: advice.explored };
|
|
931
|
+
model = overrideFullId;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
} catch { /* non-blocking */ }
|
|
935
|
+
}
|
|
936
|
+
|
|
893
937
|
// ── Challenger / dual-brain decision ─────────────────────────────────────
|
|
894
938
|
const hasBothProviders = !!(
|
|
895
939
|
profile?.providers?.claude?.enabled &&
|
|
@@ -938,6 +982,7 @@ export function decideRoute({ profile = {}, detection = {}, cwd, thinkResult, se
|
|
|
938
982
|
explanation: '',
|
|
939
983
|
_healthScores: healthScores,
|
|
940
984
|
_workStyle: workStyle,
|
|
985
|
+
...(_advisorOverride && { _advisorOverride }),
|
|
941
986
|
};
|
|
942
987
|
|
|
943
988
|
decision.explanation = explainDecision(decision, detection, profileWithEffectiveBias);
|
package/src/dispatch.mjs
CHANGED
|
@@ -1181,6 +1181,29 @@ async function dispatch(input = {}) {
|
|
|
1181
1181
|
const { recordDispatchOutcome } = await import('./outcome.mjs');
|
|
1182
1182
|
recordDispatchOutcome(input, nativeResult);
|
|
1183
1183
|
} catch { /* never block */ }
|
|
1184
|
+
|
|
1185
|
+
// ── Self-correction: intelligent retry after failover exhaustion ──────────
|
|
1186
|
+
if (!success) {
|
|
1187
|
+
const attemptNumber = input._retryAttempt || 1;
|
|
1188
|
+
try {
|
|
1189
|
+
const { shouldRetry } = await import('./self-correct.mjs');
|
|
1190
|
+
const retry = shouldRetry(nativeResult, decision, attemptNumber);
|
|
1191
|
+
if (retry.retry && retry.decision) {
|
|
1192
|
+
if (verbose) process.stderr.write(`[dual-brain] self-correct: ${retry.strategy} (attempt ${attemptNumber + 1}, reason: ${retry.reason})\n`);
|
|
1193
|
+
return dispatch({
|
|
1194
|
+
...input,
|
|
1195
|
+
decision: retry.decision,
|
|
1196
|
+
_retryAttempt: attemptNumber + 1,
|
|
1197
|
+
_skipPreDispatchThink: retry.strategy !== 'rethink',
|
|
1198
|
+
_skipRelatedContext: true,
|
|
1199
|
+
});
|
|
1200
|
+
} else if (verbose) {
|
|
1201
|
+
process.stderr.write(`[dual-brain] self-correct: giving up (${retry.reason})\n`);
|
|
1202
|
+
}
|
|
1203
|
+
} catch { /* non-blocking — if self-correct fails, return original failure */ }
|
|
1204
|
+
}
|
|
1205
|
+
// ── End self-correction ───────────────────────────────────────────────────
|
|
1206
|
+
|
|
1184
1207
|
return nativeResult;
|
|
1185
1208
|
}
|
|
1186
1209
|
|
|
@@ -1303,6 +1326,29 @@ async function dispatch(input = {}) {
|
|
|
1303
1326
|
const { recordDispatchOutcome } = await import('./outcome.mjs');
|
|
1304
1327
|
recordDispatchOutcome(input, subResult);
|
|
1305
1328
|
} catch { /* never block */ }
|
|
1329
|
+
|
|
1330
|
+
// ── Self-correction: intelligent retry after failover exhaustion ──────────
|
|
1331
|
+
if (!success) {
|
|
1332
|
+
const attemptNumber = input._retryAttempt || 1;
|
|
1333
|
+
try {
|
|
1334
|
+
const { shouldRetry } = await import('./self-correct.mjs');
|
|
1335
|
+
const retry = shouldRetry(subResult, decision, attemptNumber);
|
|
1336
|
+
if (retry.retry && retry.decision) {
|
|
1337
|
+
if (verbose) process.stderr.write(`[dual-brain] self-correct: ${retry.strategy} (attempt ${attemptNumber + 1}, reason: ${retry.reason})\n`);
|
|
1338
|
+
return dispatch({
|
|
1339
|
+
...input,
|
|
1340
|
+
decision: retry.decision,
|
|
1341
|
+
_retryAttempt: attemptNumber + 1,
|
|
1342
|
+
_skipPreDispatchThink: retry.strategy !== 'rethink',
|
|
1343
|
+
_skipRelatedContext: true,
|
|
1344
|
+
});
|
|
1345
|
+
} else if (verbose) {
|
|
1346
|
+
process.stderr.write(`[dual-brain] self-correct: giving up (${retry.reason})\n`);
|
|
1347
|
+
}
|
|
1348
|
+
} catch { /* non-blocking — if self-correct fails, return original failure */ }
|
|
1349
|
+
}
|
|
1350
|
+
// ── End self-correction ───────────────────────────────────────────────────
|
|
1351
|
+
|
|
1306
1352
|
return subResult;
|
|
1307
1353
|
}
|
|
1308
1354
|
|
package/src/outcome.mjs
CHANGED
|
@@ -45,6 +45,16 @@ function last7DaysFiles(cwd) {
|
|
|
45
45
|
return files;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
const INTENT_KEYWORDS = ['implement', 'fix', 'refactor', 'review', 'search', 'test'];
|
|
49
|
+
|
|
50
|
+
function deriveIntent(prompt, tier) {
|
|
51
|
+
const lower = (prompt ?? '').toLowerCase();
|
|
52
|
+
for (const kw of INTENT_KEYWORDS) {
|
|
53
|
+
if (lower.includes(kw)) return kw;
|
|
54
|
+
}
|
|
55
|
+
return tier ?? 'execute';
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
export function recordDispatchOutcome(dispatchInput, result) {
|
|
49
59
|
try {
|
|
50
60
|
const cwd = dispatchInput.cwd ?? process.cwd();
|
|
@@ -69,6 +79,24 @@ export function recordDispatchOutcome(dispatchInput, result) {
|
|
|
69
79
|
|
|
70
80
|
const filePath = join(outcomesDir(cwd), `outcome_${id}.json`);
|
|
71
81
|
writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf8');
|
|
82
|
+
|
|
83
|
+
// Score the outcome for the routing advisor (non-blocking)
|
|
84
|
+
try {
|
|
85
|
+
import('./signal.mjs').then(({ scoreOutcome }) =>
|
|
86
|
+
import('./routing-advisor.mjs').then(({ recordReward }) => {
|
|
87
|
+
const scored = scoreOutcome(record);
|
|
88
|
+
const intent = deriveIntent(record.prompt, record.tier);
|
|
89
|
+
const cellKey = `${record.tier}:${intent}`;
|
|
90
|
+
// Normalize full model ID to short name for the advisor cell
|
|
91
|
+
const modelId = record.model ?? 'sonnet';
|
|
92
|
+
const shortModel = /haiku/i.test(modelId) ? 'haiku'
|
|
93
|
+
: /opus/i.test(modelId) ? 'opus'
|
|
94
|
+
: 'sonnet';
|
|
95
|
+
recordReward(cellKey, shortModel, scored.reward, cwd);
|
|
96
|
+
})
|
|
97
|
+
).catch(() => { /* non-blocking */ });
|
|
98
|
+
} catch { /* non-blocking */ }
|
|
99
|
+
|
|
72
100
|
return record;
|
|
73
101
|
} catch {
|
|
74
102
|
return null;
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
return { strategy: 'escalate', newDecision: originalDecision, reason: 'unknown failure; escalating as precaution' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (attemptNumber === 2) {
|
|
86
|
+
if (tier >= 3) {
|
|
87
|
+
return { strategy: 'split', newDecision: originalDecision, reason: 'max tier reached; splitting task' };
|
|
88
|
+
}
|
|
89
|
+
return { strategy: 'escalate', newDecision: originalDecision, reason: 'retry failed; escalating one final tier' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { strategy: 'give-up', reason: 'exhausted retry budget' };
|
|
93
|
+
} catch {
|
|
94
|
+
return { strategy: 'give-up', reason: 'internal error in strategy selection' };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Export 3: buildRetryDecision(originalDecision, strategy, failure)
|
|
99
|
+
export function buildRetryDecision(originalDecision, strategy, failure) {
|
|
100
|
+
try {
|
|
101
|
+
const base = {
|
|
102
|
+
...originalDecision,
|
|
103
|
+
_retryAttempt: (originalDecision?._retryAttempt ?? 0) + 1,
|
|
104
|
+
_retryReason: failure.type,
|
|
105
|
+
_retryStrategy: strategy,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
switch (strategy) {
|
|
109
|
+
case 'escalate': {
|
|
110
|
+
const tier = modelTier(originalDecision?.model);
|
|
111
|
+
const nextTier = Math.min(tier + 1, 3);
|
|
112
|
+
return { ...base, model: TIER_MODEL[nextTier] };
|
|
113
|
+
}
|
|
114
|
+
case 'compress':
|
|
115
|
+
return { ...base, _contextBudget: 0.5 };
|
|
116
|
+
case 'wait-retry':
|
|
117
|
+
return { ...base, _delayMs: 5000 };
|
|
118
|
+
case 'rethink':
|
|
119
|
+
return { ...base, tier: 'think', _retryAsThink: true };
|
|
120
|
+
case 'split':
|
|
121
|
+
return { ...base, _shouldDecompose: true };
|
|
122
|
+
default:
|
|
123
|
+
return base;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
return { ...originalDecision, _retryAttempt: 1, _retryReason: 'error', _retryStrategy: strategy };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Export 4: shouldRetry(result, originalDecision, attemptNumber)
|
|
131
|
+
export function shouldRetry(result, originalDecision, attemptNumber = 1) {
|
|
132
|
+
try {
|
|
133
|
+
const failure = classifyFailure(result);
|
|
134
|
+
const { strategy, newDecision, reason } = selectStrategy(failure, originalDecision, attemptNumber);
|
|
135
|
+
|
|
136
|
+
if (strategy === 'give-up') {
|
|
137
|
+
return { retry: false, reason, strategy };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const decision = buildRetryDecision(newDecision ?? originalDecision, strategy, failure);
|
|
141
|
+
return { retry: true, decision, reason, strategy };
|
|
142
|
+
} catch {
|
|
143
|
+
return { retry: false, reason: 'internal error in shouldRetry', strategy: 'give-up' };
|
|
144
|
+
}
|
|
145
|
+
}
|