create-walle 0.9.30 → 0.9.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/claude-task-manager/api-prompts.js +410 -5
- package/template/claude-task-manager/api-reviews.js +251 -3
- package/template/claude-task-manager/approval-agent.js +322 -24
- package/template/claude-task-manager/db.js +77 -13
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +35 -1
- package/template/claude-task-manager/docs/postgres-concurrency-dgx-spark.html +407 -0
- package/template/claude-task-manager/docs/prompt-manager-redesign-proposal.html +667 -0
- package/template/claude-task-manager/docs/resume-ux-redesign.html +493 -0
- package/template/claude-task-manager/docs/review-redesign-proposal.html +892 -0
- package/template/claude-task-manager/docs/unified-auto-approver-proposal.html +520 -0
- package/template/claude-task-manager/git-utils.js +164 -26
- package/template/claude-task-manager/lib/agent-capabilities.js +26 -5
- package/template/claude-task-manager/lib/agent-cli-cache.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +12 -0
- package/template/claude-task-manager/lib/agent-version-service.js +650 -0
- package/template/claude-task-manager/lib/approval-drift.js +57 -0
- package/template/claude-task-manager/lib/approval-hook.js +13 -0
- package/template/claude-task-manager/lib/auth-rules.js +6 -0
- package/template/claude-task-manager/lib/broadcast-payload-memo.js +109 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +89 -8
- package/template/claude-task-manager/lib/db-owner-cooperative-scheduler.js +10 -1
- package/template/claude-task-manager/lib/document-review.js +75 -1
- package/template/claude-task-manager/lib/escalation-review.js +33 -3
- package/template/claude-task-manager/lib/headless-term-service.js +54 -9
- package/template/claude-task-manager/lib/mcp-risk.js +128 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +51 -2
- package/template/claude-task-manager/lib/native-agent-model-args.js +149 -0
- package/template/claude-task-manager/lib/opencode-conversation-store.js +269 -0
- package/template/claude-task-manager/lib/permission-match.js +77 -5
- package/template/claude-task-manager/lib/read-pool-client.js +15 -6
- package/template/claude-task-manager/lib/restore-backoff.js +82 -0
- package/template/claude-task-manager/lib/runtime-registry.js +1 -0
- package/template/claude-task-manager/lib/scheduled-wake.js +42 -0
- package/template/claude-task-manager/lib/session-capture.js +33 -6
- package/template/claude-task-manager/lib/session-history.js +60 -3
- package/template/claude-task-manager/lib/session-jobs.js +19 -0
- package/template/claude-task-manager/lib/session-restore.js +5 -4
- package/template/claude-task-manager/lib/session-standup.js +2 -0
- package/template/claude-task-manager/lib/session-stream.js +9 -0
- package/template/claude-task-manager/lib/state-sync/cell-diff.js +31 -4
- package/template/claude-task-manager/lib/state-sync/restore-frame-hold.js +39 -0
- package/template/claude-task-manager/lib/tui-input-modes.js +40 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +103 -36
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +7 -1
- package/template/claude-task-manager/lib/walle-token-chip.js +65 -0
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/providers/claude-code.js +20 -1
- package/template/claude-task-manager/providers/codex-mcp-extract.js +33 -0
- package/template/claude-task-manager/providers/codex-mcp.js +23 -20
- package/template/claude-task-manager/providers/codex.js +65 -1
- package/template/claude-task-manager/public/css/prompts.css +345 -0
- package/template/claude-task-manager/public/css/reviews.css +248 -40
- package/template/claude-task-manager/public/css/walle-session.css +90 -26
- package/template/claude-task-manager/public/index.html +2031 -428
- package/template/claude-task-manager/public/js/document-review-links.js +255 -13
- package/template/claude-task-manager/public/js/file-context-menu.js +185 -0
- package/template/claude-task-manager/public/js/message-renderer.js +46 -1
- package/template/claude-task-manager/public/js/mobile-review-core.js +54 -0
- package/template/claude-task-manager/public/js/prompt-diff.js +55 -0
- package/template/claude-task-manager/public/js/prompt-editor-chrome.js +108 -0
- package/template/claude-task-manager/public/js/prompt-editor-modes.js +87 -0
- package/template/claude-task-manager/public/js/prompt-organize.js +179 -0
- package/template/claude-task-manager/public/js/prompt-reuse.js +77 -0
- package/template/claude-task-manager/public/js/prompts.js +683 -215
- package/template/claude-task-manager/public/js/resume-state.js +186 -0
- package/template/claude-task-manager/public/js/reviews.js +402 -121
- package/template/claude-task-manager/public/js/screenshot-router.js +91 -0
- package/template/claude-task-manager/public/js/session-search-utils.js +81 -0
- package/template/claude-task-manager/public/js/state-sync-client.js +134 -17
- package/template/claude-task-manager/public/js/stream-view.js +93 -20
- package/template/claude-task-manager/public/js/walle-session.js +183 -32
- package/template/claude-task-manager/public/js/walle.js +21 -9
- package/template/claude-task-manager/public/m/app.css +117 -0
- package/template/claude-task-manager/public/m/app.js +309 -3
- package/template/claude-task-manager/public/m/index.html +56 -1
- package/template/claude-task-manager/server.js +724 -305
- package/template/claude-task-manager/workers/db-owner-worker.js +8 -0
- package/template/claude-task-manager/workers/read-pool-worker.js +4 -0
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +10 -1
- package/template/docs/proposals/2026-06-23-local-model-right-path.html +340 -0
- package/template/docs/proposals/2026-06-24-qlora-done-right.md +255 -0
- package/template/package.json +1 -1
- package/template/wall-e/api-walle.js +21 -3
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +27 -4
- package/template/wall-e/brain.js +50 -5
- package/template/wall-e/chat/force-compact.js +73 -0
- package/template/wall-e/chat.js +137 -9
- package/template/wall-e/coding/action-memory-policy.js +120 -1
- package/template/wall-e/coding/compaction-service.js +10 -3
- package/template/wall-e/coding/model-router.js +116 -0
- package/template/wall-e/coding-orchestrator.js +303 -11
- package/template/wall-e/coding-prompts.js +1 -1
- package/template/wall-e/docs/skill-self-heal-design.html +429 -0
- package/template/wall-e/llm/client.js +115 -6
- package/template/wall-e/llm/codex-cli.js +71 -23
- package/template/wall-e/llm/codex-cli.plugin.json +1 -0
- package/template/wall-e/llm/default-fallback.js +71 -11
- package/template/wall-e/llm/mlx-worker.js +0 -0
- package/template/wall-e/llm/mlx.js +46 -12
- package/template/wall-e/llm/provider-error.js +9 -3
- package/template/wall-e/llm/provider-health-state.js +561 -11
- package/template/wall-e/llm/registry.js +6 -0
- package/template/wall-e/llm/routing-policy.js +255 -25
- package/template/wall-e/llm/tool-call-validator.js +156 -0
- package/template/wall-e/skills/skill-dispatch-decision.js +82 -2
- package/template/wall-e/skills/skill-planner.js +72 -5
- package/template/wall-e/tools/command-registry.js +34 -0
- package/template/wall-e/training/gemma-e4b-qlora.js +197 -1
- package/template/wall-e/training/mlx_lora_launch.py +196 -0
- package/template/wall-e/training/training-coexistence.sh +87 -0
- package/template/wall-e/training/training-mem-guard.sh +76 -0
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const providerRegistry = require('./registry');
|
|
4
4
|
const { isCliBinaryAvailable, _internals: fallbackInternals } = require('./default-fallback');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
DEFAULT_PROVIDER_STATUS_MAX_AGE_MS,
|
|
7
|
+
getFirstProviderModelCircuit,
|
|
8
|
+
getFirstProviderModelCircuitStats,
|
|
9
|
+
getProviderHealthStatus,
|
|
10
|
+
listProviderModelCircuits,
|
|
11
|
+
} = require('./provider-health-state');
|
|
6
12
|
|
|
7
13
|
const POLICY_KEYS = {
|
|
8
14
|
mode: 'model_routing_policy_mode',
|
|
@@ -11,6 +17,7 @@ const POLICY_KEYS = {
|
|
|
11
17
|
lastRefreshAt: 'model_routing_policy_last_refresh_at',
|
|
12
18
|
};
|
|
13
19
|
|
|
20
|
+
const ROUTING_POLICY_DISPLAY_HEALTH_MAX_AGE_MS = DEFAULT_PROVIDER_STATUS_MAX_AGE_MS;
|
|
14
21
|
const VALID_MODES = new Set(['smart', 'strict']);
|
|
15
22
|
const LOCAL_PROVIDER_TYPES = new Set(['ollama', 'mlx', 'lmstudio']);
|
|
16
23
|
const GATEWAY_ONLY_TYPES = new Set(['portkey_gateway']);
|
|
@@ -164,17 +171,126 @@ function providerRuntimeAccess(row = {}, env = process.env) {
|
|
|
164
171
|
return { ok: false, kind: 'missing_credential', reason: 'No stored credential or environment key.' };
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
function
|
|
168
|
-
const
|
|
169
|
-
const
|
|
174
|
+
function uniqueTruthy(values = []) {
|
|
175
|
+
const out = [];
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
for (const value of values) {
|
|
178
|
+
const text = String(value || '').trim();
|
|
179
|
+
if (!text) continue;
|
|
180
|
+
const key = text.toLowerCase();
|
|
181
|
+
if (seen.has(key)) continue;
|
|
182
|
+
seen.add(key);
|
|
183
|
+
out.push(text);
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function firstRuntimeHealthWithEvidence(ids = [], health) {
|
|
189
|
+
if (!health || typeof health.getProviderHealth !== 'function') return null;
|
|
190
|
+
for (const id of uniqueTruthy(ids)) {
|
|
191
|
+
let runtime = null;
|
|
192
|
+
try { runtime = health.getProviderHealth(id); } catch {}
|
|
193
|
+
if (runtime && Number(runtime.totalCalls || 0) > 0) return runtime;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function firstPersistedHealthStatus(providers = [], model, brain, options = {}) {
|
|
199
|
+
for (const provider of uniqueTruthy(providers)) {
|
|
200
|
+
const persisted = getProviderHealthStatus(provider, model, {
|
|
201
|
+
brain,
|
|
202
|
+
maxAgeMs: options.persistedMaxAgeMs,
|
|
203
|
+
});
|
|
204
|
+
if (persisted) return persisted;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function failureRateSummary({ circuit, circuitStats, runtime, persisted, runtimeHasEvidence } = {}) {
|
|
210
|
+
const fromCircuit = circuit || circuitStats;
|
|
211
|
+
if (fromCircuit && Number(fromCircuit.total || 0) > 0) {
|
|
212
|
+
return {
|
|
213
|
+
failures: Number(fromCircuit.failures || 0),
|
|
214
|
+
total: Number(fromCircuit.total || 0),
|
|
215
|
+
failure_rate: Number(fromCircuit.failure_rate || 0),
|
|
216
|
+
failure_rate_pct: Number(fromCircuit.failure_rate_pct || Math.round(Number(fromCircuit.failure_rate || 0) * 100)),
|
|
217
|
+
source: fromCircuit.state === 'open' ? 'circuit_open' : 'recent_window',
|
|
218
|
+
window_ms: Number(fromCircuit.window_ms || 0) || null,
|
|
219
|
+
threshold: Number(fromCircuit.threshold || 0),
|
|
220
|
+
min_calls: Number(fromCircuit.min_calls || 0),
|
|
221
|
+
min_failures: Number(fromCircuit.min_failures || 0),
|
|
222
|
+
low_sample: fromCircuit.low_sample === true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (runtimeHasEvidence) {
|
|
226
|
+
const failureRate = Number(runtime?.failureRate || 0);
|
|
227
|
+
const total = Number(runtime?.totalCalls || 0);
|
|
228
|
+
return {
|
|
229
|
+
failures: Number.isFinite(failureRate) ? Math.round(failureRate * total) : null,
|
|
230
|
+
total,
|
|
231
|
+
failure_rate: Number.isFinite(failureRate) ? failureRate : null,
|
|
232
|
+
failure_rate_pct: Number.isFinite(failureRate) ? Math.round(failureRate * 100) : null,
|
|
233
|
+
source: 'runtime',
|
|
234
|
+
window_ms: null,
|
|
235
|
+
threshold: null,
|
|
236
|
+
min_calls: null,
|
|
237
|
+
min_failures: null,
|
|
238
|
+
low_sample: false,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (persisted) {
|
|
242
|
+
const ok = persisted.ok === true;
|
|
243
|
+
return {
|
|
244
|
+
failures: ok ? 0 : 1,
|
|
245
|
+
total: 1,
|
|
246
|
+
failure_rate: ok ? 0 : 1,
|
|
247
|
+
failure_rate_pct: ok ? 0 : 100,
|
|
248
|
+
source: 'last_check',
|
|
249
|
+
window_ms: null,
|
|
250
|
+
threshold: null,
|
|
251
|
+
min_calls: null,
|
|
252
|
+
min_failures: null,
|
|
253
|
+
low_sample: true,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function publicRuntimeHealth(id, type, model, health, brain, options = {}) {
|
|
260
|
+
const runtimeType = options.runtimeType || type;
|
|
261
|
+
const providerType = options.providerType || type;
|
|
262
|
+
const nowMs = options.nowMs || Date.now();
|
|
263
|
+
const runtime = firstRuntimeHealthWithEvidence([id, runtimeType, providerType], health);
|
|
264
|
+
const persisted = firstPersistedHealthStatus([runtimeType, providerType, id], model, brain, options);
|
|
265
|
+
const circuit = getFirstProviderModelCircuit([runtimeType, providerType, id], model, {
|
|
266
|
+
brain,
|
|
267
|
+
nowMs,
|
|
268
|
+
});
|
|
269
|
+
const circuitStats = circuit || getFirstProviderModelCircuitStats([runtimeType, providerType, id], model, {
|
|
270
|
+
brain,
|
|
271
|
+
nowMs,
|
|
272
|
+
});
|
|
170
273
|
const runtimeHasEvidence = runtime && Number(runtime.totalCalls || 0) > 0;
|
|
171
|
-
const
|
|
274
|
+
const rate = failureRateSummary({ circuit, circuitStats, runtime, persisted, runtimeHasEvidence });
|
|
275
|
+
const status = circuit ? 'error' : (runtimeHasEvidence ? runtime.status : (persisted ? persisted.status : 'unknown'));
|
|
172
276
|
return {
|
|
173
277
|
status,
|
|
174
278
|
consecutive_failures: runtimeHasEvidence ? (runtime?.consecutiveFailures || 0) : 0,
|
|
175
|
-
|
|
279
|
+
failures: rate?.failures ?? null,
|
|
280
|
+
total: rate?.total ?? 0,
|
|
281
|
+
failure_rate: rate?.failure_rate ?? null,
|
|
282
|
+
failure_rate_pct: rate?.failure_rate_pct ?? null,
|
|
283
|
+
failure_rate_source: rate?.source || '',
|
|
284
|
+
failure_rate_window_ms: rate?.window_ms ?? null,
|
|
285
|
+
failure_rate_threshold: rate?.threshold ?? null,
|
|
286
|
+
min_calls: rate?.min_calls ?? null,
|
|
287
|
+
min_failures: rate?.min_failures ?? null,
|
|
288
|
+
low_sample: rate?.low_sample === true,
|
|
289
|
+
last_error: circuit?.error || runtime?.lastError?.message || persisted?.error || '',
|
|
176
290
|
last_success_at: runtimeHasEvidence && runtime?.lastSuccess ? runtime.lastSuccess.toISOString?.() || String(runtime.lastSuccess) : null,
|
|
177
291
|
persisted_status: persisted || null,
|
|
292
|
+
circuit_breaker: circuit || null,
|
|
293
|
+
circuit_stats: circuitStats || null,
|
|
178
294
|
};
|
|
179
295
|
}
|
|
180
296
|
|
|
@@ -207,11 +323,16 @@ function rowCandidate(row, context) {
|
|
|
207
323
|
const runtimeType = providerRuntimeType(row);
|
|
208
324
|
const access = providerRuntimeAccess(row, context.env);
|
|
209
325
|
const firstModel = firstEnabledModelForProvider(context.brain, row.id);
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
326
|
+
const providerModel = modelForProvider(context.brain, type, firstModel);
|
|
327
|
+
const model = runtimeType !== type && runtimeType.endsWith('-cli')
|
|
328
|
+
? (modelMatchesProvider(providerModel, runtimeType) ? providerModel : defaultModelForProvider(runtimeType))
|
|
329
|
+
: providerModel;
|
|
330
|
+
const health = publicRuntimeHealth(row.id, runtimeType, model, context.health, context.brain, {
|
|
331
|
+
providerType: type,
|
|
332
|
+
runtimeType,
|
|
333
|
+
});
|
|
334
|
+
const circuitOpen = !!health.circuit_breaker;
|
|
335
|
+
const isDown = circuitOpen || health.status === 'down';
|
|
215
336
|
const routePolicy = typeof context.brain?.getProviderRoutePolicy === 'function'
|
|
216
337
|
? context.brain.getProviderRoutePolicy(type)
|
|
217
338
|
: 'auto';
|
|
@@ -229,7 +350,7 @@ function rowCandidate(row, context) {
|
|
|
229
350
|
route_policy: routePolicy,
|
|
230
351
|
runtime_access: access.kind,
|
|
231
352
|
included,
|
|
232
|
-
excluded_reason: included ? '' : (isDown ? 'Provider is marked down by runtime health.' : access.reason),
|
|
353
|
+
excluded_reason: included ? '' : (circuitOpen ? 'Rolling error rate crossed the fallback threshold; auto-fallback is active until recovery probes succeed.' : (isDown ? 'Provider is marked down by runtime health.' : access.reason)),
|
|
233
354
|
health,
|
|
234
355
|
updated_at: row.updated_at || '',
|
|
235
356
|
};
|
|
@@ -240,6 +361,9 @@ function envCandidate(type, context) {
|
|
|
240
361
|
if (!fallbackInternals._hasEnvKey(providerType, context.env)) return null;
|
|
241
362
|
const model = modelForProvider(context.brain, providerType, '');
|
|
242
363
|
const id = `${providerType}-env`;
|
|
364
|
+
const health = publicRuntimeHealth(id, providerType, model, context.health, context.brain);
|
|
365
|
+
const circuitOpen = !!health.circuit_breaker;
|
|
366
|
+
const isDown = circuitOpen || health.status === 'down';
|
|
243
367
|
return {
|
|
244
368
|
id,
|
|
245
369
|
provider_id: null,
|
|
@@ -252,9 +376,9 @@ function envCandidate(type, context) {
|
|
|
252
376
|
connection_kind: 'environment',
|
|
253
377
|
route_policy: 'auto',
|
|
254
378
|
runtime_access: 'environment',
|
|
255
|
-
included:
|
|
256
|
-
excluded_reason: '',
|
|
257
|
-
health
|
|
379
|
+
included: !isDown,
|
|
380
|
+
excluded_reason: isDown ? (circuitOpen ? 'Rolling error rate crossed the fallback threshold; auto-fallback is active until recovery probes succeed.' : 'Provider is marked down by runtime health.') : '',
|
|
381
|
+
health,
|
|
258
382
|
updated_at: '',
|
|
259
383
|
};
|
|
260
384
|
}
|
|
@@ -262,6 +386,9 @@ function envCandidate(type, context) {
|
|
|
262
386
|
function cliCandidate(id, bin, context) {
|
|
263
387
|
const available = context.settings.cli_tail && context.isCliAvailable(bin);
|
|
264
388
|
const model = defaultModelForProvider(id);
|
|
389
|
+
const health = publicRuntimeHealth(id, id, model, context.health, context.brain);
|
|
390
|
+
const circuitOpen = !!health.circuit_breaker;
|
|
391
|
+
const isDown = circuitOpen || health.status === 'down';
|
|
265
392
|
return {
|
|
266
393
|
id,
|
|
267
394
|
provider_id: null,
|
|
@@ -274,9 +401,11 @@ function cliCandidate(id, bin, context) {
|
|
|
274
401
|
connection_kind: 'cli',
|
|
275
402
|
route_policy: 'auto',
|
|
276
403
|
runtime_access: available ? 'cli' : 'missing_cli',
|
|
277
|
-
included: available,
|
|
278
|
-
excluded_reason: available
|
|
279
|
-
|
|
404
|
+
included: available && !isDown,
|
|
405
|
+
excluded_reason: available
|
|
406
|
+
? (isDown ? (circuitOpen ? 'Rolling error rate crossed the fallback threshold; auto-fallback is active until recovery probes succeed.' : 'Provider is marked down by runtime health.') : '')
|
|
407
|
+
: `${bin} CLI not found or CLI fallback is disabled.`,
|
|
408
|
+
health,
|
|
280
409
|
updated_at: '',
|
|
281
410
|
};
|
|
282
411
|
}
|
|
@@ -455,6 +584,7 @@ function buildRoutingPolicy(options = {}) {
|
|
|
455
584
|
chain,
|
|
456
585
|
candidates: ordered,
|
|
457
586
|
excluded: ordered.filter((candidate) => !candidate.included),
|
|
587
|
+
circuit_breakers: listProviderModelCircuits({ brain }),
|
|
458
588
|
recommendations: [],
|
|
459
589
|
actions: [],
|
|
460
590
|
};
|
|
@@ -585,6 +715,49 @@ function _defaultFromPersisted(brain, chain = [], settings = {}) {
|
|
|
585
715
|
};
|
|
586
716
|
}
|
|
587
717
|
|
|
718
|
+
function _hydratePersistedRouteHealth(brain, route = {}, options = {}) {
|
|
719
|
+
const runtimeType = route.runtime_type || route.runtimeType || route.provider_type || route.providerType;
|
|
720
|
+
const providerType = route.provider_type || route.providerType || route.type;
|
|
721
|
+
const model = route.model || route.model_id || route.modelId || '';
|
|
722
|
+
const health = publicRuntimeHealth(
|
|
723
|
+
route.provider_id || route.providerId || route.id,
|
|
724
|
+
runtimeType || providerType,
|
|
725
|
+
model,
|
|
726
|
+
options.health,
|
|
727
|
+
brain,
|
|
728
|
+
{
|
|
729
|
+
providerType,
|
|
730
|
+
runtimeType,
|
|
731
|
+
persistedMaxAgeMs: options.persistedMaxAgeMs ?? ROUTING_POLICY_DISPLAY_HEALTH_MAX_AGE_MS,
|
|
732
|
+
}
|
|
733
|
+
);
|
|
734
|
+
return {
|
|
735
|
+
...route,
|
|
736
|
+
health_status: health.status,
|
|
737
|
+
health,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function _routeCircuitOpen(route = {}) {
|
|
742
|
+
return route?.health?.circuit_breaker?.state === 'open';
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function _circuitExcludedRoute(route = {}) {
|
|
746
|
+
const circuit = route?.health?.circuit_breaker || {};
|
|
747
|
+
const failures = Number(circuit.failures || 0);
|
|
748
|
+
const total = Number(circuit.total || 0);
|
|
749
|
+
const rate = circuit.failure_rate_pct != null
|
|
750
|
+
? Number(circuit.failure_rate_pct)
|
|
751
|
+
: Math.round(Number(circuit.failure_rate || 0) * 100);
|
|
752
|
+
const hours = Number(circuit.window_ms || 0) / (60 * 60 * 1000);
|
|
753
|
+
const windowLabel = hours >= 1 ? `${Math.round(hours)}h` : 'rolling window';
|
|
754
|
+
return {
|
|
755
|
+
...route,
|
|
756
|
+
included: false,
|
|
757
|
+
excluded_reason: `Auto-fallback active: ${rate}% errors over ${windowLabel} (${failures}/${total}); route stays skipped until recovery probes succeed.`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
588
761
|
function _publicPolicyFromPersisted(brain, snapshot = {}, extras = {}) {
|
|
589
762
|
const policyRow = snapshot.policy || {};
|
|
590
763
|
const routes = snapshot.routes || {};
|
|
@@ -593,20 +766,28 @@ function _publicPolicyFromPersisted(brain, snapshot = {}, extras = {}) {
|
|
|
593
766
|
const generated = Array.isArray(routes.generated) ? routes.generated : [];
|
|
594
767
|
const user = Array.isArray(routes.user) ? routes.user : [];
|
|
595
768
|
const chainSource = effective.length ? 'effective' : (generated.length ? 'generated' : 'empty');
|
|
596
|
-
const
|
|
597
|
-
...route,
|
|
769
|
+
const hydratedEffective = (effective.length ? effective : generated).map((route, index) => ({
|
|
770
|
+
..._hydratePersistedRouteHealth(brain, route, extras),
|
|
598
771
|
route_key: _routeKey(route),
|
|
599
772
|
order: index + 1,
|
|
600
773
|
role: index === 0 ? 'primary' : 'fallback',
|
|
601
774
|
}));
|
|
775
|
+
const circuitExcluded = hydratedEffective.filter(_routeCircuitOpen).map(_circuitExcludedRoute);
|
|
776
|
+
const chain = hydratedEffective
|
|
777
|
+
.filter((route) => !_routeCircuitOpen(route))
|
|
778
|
+
.map((route, index) => ({
|
|
779
|
+
...route,
|
|
780
|
+
order: index + 1,
|
|
781
|
+
role: index === 0 ? 'primary' : 'fallback',
|
|
782
|
+
}));
|
|
602
783
|
const publicUserRoutes = user.map((route, index) => ({
|
|
603
|
-
...route,
|
|
784
|
+
..._hydratePersistedRouteHealth(brain, route, extras),
|
|
604
785
|
route_key: _routeKey(route),
|
|
605
786
|
order: index + 1,
|
|
606
787
|
role: route.role || 'fallback',
|
|
607
788
|
}));
|
|
608
789
|
const publicGeneratedRoutes = generated.map((route, index) => ({
|
|
609
|
-
...route,
|
|
790
|
+
..._hydratePersistedRouteHealth(brain, route, extras),
|
|
610
791
|
route_key: _routeKey(route),
|
|
611
792
|
order: index + 1,
|
|
612
793
|
role: index === 0 ? 'primary' : 'fallback',
|
|
@@ -620,8 +801,9 @@ function _publicPolicyFromPersisted(brain, snapshot = {}, extras = {}) {
|
|
|
620
801
|
trigger_classes: Array.from(fallbackInternals.FALLTHROUGH_CLASSES || []),
|
|
621
802
|
default: _defaultFromPersisted(brain, chain, settings),
|
|
622
803
|
chain,
|
|
623
|
-
candidates: chain,
|
|
624
|
-
excluded:
|
|
804
|
+
candidates: chain.concat(circuitExcluded),
|
|
805
|
+
excluded: circuitExcluded,
|
|
806
|
+
circuit_breakers: listProviderModelCircuits({ brain }),
|
|
625
807
|
recommendations: extras.recommendations || [],
|
|
626
808
|
actions: extras.actions || [],
|
|
627
809
|
sources: {
|
|
@@ -634,6 +816,44 @@ function _publicPolicyFromPersisted(brain, snapshot = {}, extras = {}) {
|
|
|
634
816
|
};
|
|
635
817
|
}
|
|
636
818
|
|
|
819
|
+
function _configuredDefaultCircuit(brain) {
|
|
820
|
+
const configuredType = normalizeProviderType(getKv(brain, 'walle_provider') || process.env.WALLE_PROVIDER || '');
|
|
821
|
+
if (!configuredType) return null;
|
|
822
|
+
const rows = loadProviderRows(brain);
|
|
823
|
+
const row = rows.find((candidate) => normalizeProviderType(candidate.type || candidate.provider_type) === configuredType) || null;
|
|
824
|
+
const runtimeType = row ? providerRuntimeType(row) : configuredType;
|
|
825
|
+
const configuredModel = getKv(brain, 'walle_model') || process.env.WALLE_MODEL || '';
|
|
826
|
+
const firstModel = row?.id ? firstEnabledModelForProvider(brain, row.id) : '';
|
|
827
|
+
let model = configuredModel || modelForProvider(brain, configuredType, firstModel);
|
|
828
|
+
if (runtimeType !== configuredType && runtimeType.endsWith('-cli')) {
|
|
829
|
+
model = modelMatchesProvider(model, runtimeType) ? model : defaultModelForProvider(runtimeType);
|
|
830
|
+
}
|
|
831
|
+
return getFirstProviderModelCircuit([runtimeType, configuredType, row?.id], model, { brain });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function _persistedPolicyIsStale(brain, snapshot = {}) {
|
|
835
|
+
const effective = Array.isArray(snapshot.routes?.effective) ? snapshot.routes.effective : [];
|
|
836
|
+
const generated = Array.isArray(snapshot.routes?.generated) ? snapshot.routes.generated : [];
|
|
837
|
+
const primary = effective[0] || generated[0] || null;
|
|
838
|
+
const configuredType = normalizeProviderType(getKv(brain, 'walle_provider') || process.env.WALLE_PROVIDER || '');
|
|
839
|
+
if (!primary || !configuredType) return false;
|
|
840
|
+
const primaryProvider = normalizeProviderType(primary.provider_type || primary.providerType || primary.type || '');
|
|
841
|
+
const primaryRuntime = normalizeProviderType(primary.runtime_type || primary.runtimeType || primaryProvider || '');
|
|
842
|
+
const configuredCircuit = _configuredDefaultCircuit(brain);
|
|
843
|
+
if (primaryProvider !== configuredType && primaryRuntime !== configuredType) return !configuredCircuit;
|
|
844
|
+
const configuredModel = getKv(brain, 'walle_model') || process.env.WALLE_MODEL || '';
|
|
845
|
+
if (!configuredModel) return false;
|
|
846
|
+
const primaryModel = primary.model || primary.model_id || primary.modelId || '';
|
|
847
|
+
let effectiveConfiguredModel = configuredModel;
|
|
848
|
+
if (primaryRuntime !== configuredType && primaryRuntime.endsWith('-cli')) {
|
|
849
|
+
effectiveConfiguredModel = modelMatchesProvider(configuredModel, primaryRuntime)
|
|
850
|
+
? configuredModel
|
|
851
|
+
: defaultModelForProvider(primaryRuntime);
|
|
852
|
+
}
|
|
853
|
+
if (!modelMatchesProvider(effectiveConfiguredModel, primaryRuntime || configuredType)) return false;
|
|
854
|
+
return !configuredCircuit && !!primaryModel && primaryModel !== effectiveConfiguredModel;
|
|
855
|
+
}
|
|
856
|
+
|
|
637
857
|
function getPersistedRoutingPolicy(options = {}) {
|
|
638
858
|
const brain = options.brain;
|
|
639
859
|
if (!brain || typeof brain.getModelRoutingPolicy !== 'function') {
|
|
@@ -645,7 +865,16 @@ function getPersistedRoutingPolicy(options = {}) {
|
|
|
645
865
|
if (!hasEffective && !hasGenerated && options.generateIfMissing !== false) {
|
|
646
866
|
return materializeRoutingPolicy(options);
|
|
647
867
|
}
|
|
648
|
-
|
|
868
|
+
if (
|
|
869
|
+
options.generateIfMissing !== false
|
|
870
|
+
&& options.repairStale !== false
|
|
871
|
+
&& _persistedPolicyIsStale(brain, snapshot)
|
|
872
|
+
) {
|
|
873
|
+
return materializeRoutingPolicy({ ...options, repairDefault: false });
|
|
874
|
+
}
|
|
875
|
+
return _publicPolicyFromPersisted(brain, snapshot, {
|
|
876
|
+
health: options.health || safeDefaultHealth(),
|
|
877
|
+
});
|
|
649
878
|
}
|
|
650
879
|
|
|
651
880
|
function materializeRoutingPolicy(options = {}) {
|
|
@@ -790,6 +1019,7 @@ module.exports = {
|
|
|
790
1019
|
providerRuntimeAccess,
|
|
791
1020
|
providerRuntimeType,
|
|
792
1021
|
defaultModelForProvider,
|
|
1022
|
+
_persistedPolicyIsStale,
|
|
793
1023
|
_mergeEffectiveRoutes,
|
|
794
1024
|
_routeKey,
|
|
795
1025
|
},
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema-driven tool-call validation + repair prompts.
|
|
5
|
+
*
|
|
6
|
+
* Small local models (e.g. Gemma E4B) fail tool-use in two avoidable ways the
|
|
7
|
+
* training log documented repeatedly:
|
|
8
|
+
* - they emit a tool call with empty `{}` args (v1: "six run_shell with empty
|
|
9
|
+
* arguments"), or missing required fields;
|
|
10
|
+
* - they emit a no-op edit where old_string === new_string (v6).
|
|
11
|
+
*
|
|
12
|
+
* Both are fixable at the harness level — no fine-tuning — by validating the
|
|
13
|
+
* call against the tool's own JSON schema (`input_schema.required`) and, when a
|
|
14
|
+
* turn produced only invalid calls, feeding back a corrective prompt that names
|
|
15
|
+
* the tool and the exact fields it must supply. This is provider/tool-agnostic:
|
|
16
|
+
* it reads whatever tool catalog the run was given.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const EDIT_TOOLS = new Set(['edit_file', 'multi_edit', 'apply_patch']);
|
|
20
|
+
|
|
21
|
+
function toolByName(tools = [], name = '') {
|
|
22
|
+
return (Array.isArray(tools) ? tools : []).find((tool) => (tool?.name || tool?.tool) === name) || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Required field names for a tool, or null if the tool is unknown. */
|
|
26
|
+
function toolRequiredFields(tools = [], name = '') {
|
|
27
|
+
const tool = toolByName(tools, name);
|
|
28
|
+
if (!tool) return null;
|
|
29
|
+
const schema = tool.input_schema || tool.inputSchema || tool.parameters || {};
|
|
30
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
31
|
+
return required.map(String);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isBlank(value) {
|
|
35
|
+
if (value === undefined || value === null) return true;
|
|
36
|
+
if (typeof value === 'string') return value.trim() === '';
|
|
37
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
38
|
+
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function callInput(call = {}) {
|
|
43
|
+
const input = call.input || call.args || call.arguments || {};
|
|
44
|
+
return input && typeof input === 'object' && !Array.isArray(input) ? input : {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate one tool call against the catalog.
|
|
49
|
+
* @returns {{ ok: boolean, problem: string|null, missing: string[], name: string }}
|
|
50
|
+
*/
|
|
51
|
+
function validateToolCall(call = {}, tools = []) {
|
|
52
|
+
const name = String(call.name || call.tool || '').trim();
|
|
53
|
+
const input = callInput(call);
|
|
54
|
+
|
|
55
|
+
if (!name) return { ok: false, problem: 'unknown_tool', missing: [], name };
|
|
56
|
+
const required = toolRequiredFields(tools, name);
|
|
57
|
+
if (required === null) return { ok: false, problem: 'unknown_tool', missing: [], name };
|
|
58
|
+
|
|
59
|
+
const missing = required.filter((field) => isBlank(input[field]));
|
|
60
|
+
if (missing.length) {
|
|
61
|
+
const allMissing = missing.length === required.length;
|
|
62
|
+
return { ok: false, problem: allMissing ? 'empty_args' : 'missing_required', missing, name };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (EDIT_TOOLS.has(name) && typeof input.old_string === 'string' && input.old_string === input.new_string) {
|
|
66
|
+
return { ok: false, problem: 'noop_edit', missing: [], name };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { ok: true, problem: null, missing: [], name };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate a batch of calls from one turn.
|
|
74
|
+
* @returns {{ valid, invalid, proseOnly, allInvalid, malformedCount, problems }}
|
|
75
|
+
*/
|
|
76
|
+
function validateToolCalls(calls = [], tools = []) {
|
|
77
|
+
const list = Array.isArray(calls) ? calls : [];
|
|
78
|
+
const valid = [];
|
|
79
|
+
const invalid = [];
|
|
80
|
+
for (const call of list) {
|
|
81
|
+
const verdict = validateToolCall(call, tools);
|
|
82
|
+
if (verdict.ok) valid.push({ call, verdict });
|
|
83
|
+
else invalid.push({ call, verdict });
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
valid,
|
|
87
|
+
invalid,
|
|
88
|
+
proseOnly: list.length === 0,
|
|
89
|
+
allInvalid: list.length > 0 && valid.length === 0,
|
|
90
|
+
malformedCount: invalid.length,
|
|
91
|
+
problems: invalid.map((entry) => entry.verdict),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function describeRequired(tools, name) {
|
|
96
|
+
const required = toolRequiredFields(tools, name);
|
|
97
|
+
if (!required || !required.length) return '';
|
|
98
|
+
return ` Required fields for ${name}: ${required.join(', ')}.`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A corrective message to feed back to the model after a bad turn. */
|
|
102
|
+
function buildToolRepairMessage(validation = {}, tools = []) {
|
|
103
|
+
if (validation.proseOnly) {
|
|
104
|
+
return '[SYSTEM] You did not call any tool. Respond with exactly one tool call (not prose) '
|
|
105
|
+
+ 'to make progress, using a tool from the provided catalog with all required fields filled in.';
|
|
106
|
+
}
|
|
107
|
+
const lines = ['[SYSTEM] Your previous tool call(s) were invalid and were not executed. Fix and retry:'];
|
|
108
|
+
for (const problem of validation.problems || []) {
|
|
109
|
+
if (problem.problem === 'unknown_tool') {
|
|
110
|
+
lines.push(`- "${problem.name || 'unknown'}" is not a known tool. Use a tool from the provided catalog.`);
|
|
111
|
+
} else if (problem.problem === 'empty_args') {
|
|
112
|
+
lines.push(`- ${problem.name} was called with empty arguments.${describeRequired(tools, problem.name)}`);
|
|
113
|
+
} else if (problem.problem === 'missing_required') {
|
|
114
|
+
lines.push(`- ${problem.name} is missing required field(s): ${problem.missing.join(', ')}.${describeRequired(tools, problem.name)}`);
|
|
115
|
+
} else if (problem.problem === 'noop_edit') {
|
|
116
|
+
lines.push(`- ${problem.name} had old_string identical to new_string (no-op). Provide a real change.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Decide whether a turn should trigger a corrective re-prompt. Pure so the
|
|
124
|
+
* orchestrator's wiring is unit-testable without a live model.
|
|
125
|
+
* @returns {{ shouldRepair: boolean, reason: string, message: string|null, problems: object[] }}
|
|
126
|
+
*/
|
|
127
|
+
function planToolCallRepair({ toolCalls = [], tools = [], repairs = 0, maxRepairs = 2, enabled = true } = {}) {
|
|
128
|
+
if (!enabled) return { shouldRepair: false, reason: 'disabled', message: null, problems: [] };
|
|
129
|
+
// Prose-only (no calls) is handled by the no-action guard, not here.
|
|
130
|
+
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
131
|
+
return { shouldRepair: false, reason: 'no_calls', message: null, problems: [] };
|
|
132
|
+
}
|
|
133
|
+
const validation = validateToolCalls(toolCalls, tools);
|
|
134
|
+
if (!validation.allInvalid) {
|
|
135
|
+
return { shouldRepair: false, reason: 'has_valid_call', message: null, problems: validation.problems };
|
|
136
|
+
}
|
|
137
|
+
if (repairs >= maxRepairs) {
|
|
138
|
+
return { shouldRepair: false, reason: 'budget_exhausted', message: null, problems: validation.problems };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
shouldRepair: true,
|
|
142
|
+
reason: 'all_invalid',
|
|
143
|
+
message: buildToolRepairMessage(validation, tools),
|
|
144
|
+
problems: validation.problems,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
EDIT_TOOLS,
|
|
150
|
+
toolByName,
|
|
151
|
+
toolRequiredFields,
|
|
152
|
+
validateToolCall,
|
|
153
|
+
validateToolCalls,
|
|
154
|
+
buildToolRepairMessage,
|
|
155
|
+
planToolCallRepair,
|
|
156
|
+
};
|
|
@@ -33,6 +33,58 @@ function backoffForFailureCount(consecutiveFailures, schedule = DEFAULT_BACKOFF_
|
|
|
33
33
|
return schedule[Math.max(0, idx)] ?? schedule[schedule.length - 1];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Self-heal (half-open circuit breaker) defaults. Once a skill is
|
|
38
|
+
* auto-disabled (10 consecutive failures), the dispatcher periodically runs a
|
|
39
|
+
* single "probe" — one real execution — to test whether the underlying fault
|
|
40
|
+
* has cleared. The planner closes the breaker on success (re-enable + reset)
|
|
41
|
+
* and, after MAX_PROBE_ATTEMPTS failed probes, escalates to a human and stops
|
|
42
|
+
* probing. From skill-self-heal design 2026-06-24.
|
|
43
|
+
*
|
|
44
|
+
* The schedule grows so transient faults (expired token, network blip) heal
|
|
45
|
+
* within the hour while permanent faults (revoked permission, missing binary)
|
|
46
|
+
* don't thrash: 1h → 6h → 24h.
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_PROBE_SCHEDULE_MS = [
|
|
49
|
+
60 * 60_000, // probe 1: 1h after auto-disable
|
|
50
|
+
6 * 60 * 60_000, // probe 2: 6h after probe 1
|
|
51
|
+
24 * 60 * 60_000, // probe 3: 24h after probe 2
|
|
52
|
+
];
|
|
53
|
+
const DEFAULT_MAX_PROBE_ATTEMPTS = 3;
|
|
54
|
+
const PROBE_JITTER_FRACTION = 0.1; // ±10% spread to avoid a thundering herd
|
|
55
|
+
|
|
56
|
+
// Small deterministic string hash (FNV-1a, 32-bit) used to derive STABLE
|
|
57
|
+
// per-(skill, attempt) jitter. Stable matters: if jitter used Math.random the
|
|
58
|
+
// computed next-probe time would jump every tick and the "is it due yet" gate
|
|
59
|
+
// would flap. Deterministic jitter still de-synchronizes different skills.
|
|
60
|
+
function _hashString(str) {
|
|
61
|
+
let h = 0x811c9dc5;
|
|
62
|
+
for (let i = 0; i < str.length; i++) {
|
|
63
|
+
h ^= str.charCodeAt(i);
|
|
64
|
+
h = Math.imul(h, 0x01000193);
|
|
65
|
+
}
|
|
66
|
+
return h >>> 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Delay (ms) before the next recovery probe, with deterministic jitter.
|
|
71
|
+
* @param {number} attempts - probes already made (0 = first probe)
|
|
72
|
+
* @param {Object} [opts]
|
|
73
|
+
* @param {string} [opts.seed] - stable per-skill seed (id/name); omit for no jitter
|
|
74
|
+
* @param {Array<number>} [opts.schedule]
|
|
75
|
+
* @returns {number}
|
|
76
|
+
*/
|
|
77
|
+
function probeDelayFor(attempts, opts = {}) {
|
|
78
|
+
const schedule = Array.isArray(opts.schedule) && opts.schedule.length
|
|
79
|
+
? opts.schedule
|
|
80
|
+
: DEFAULT_PROBE_SCHEDULE_MS;
|
|
81
|
+
const base = schedule[Math.min(Math.max(0, attempts), schedule.length - 1)];
|
|
82
|
+
if (!opts.seed) return base;
|
|
83
|
+
const frac = (_hashString(`${opts.seed}:${attempts}`) % 1000) / 1000; // 0..1
|
|
84
|
+
const delta = base * PROBE_JITTER_FRACTION * (frac * 2 - 1); // ±10%
|
|
85
|
+
return Math.round(base + delta);
|
|
86
|
+
}
|
|
87
|
+
|
|
36
88
|
/**
|
|
37
89
|
* Pure decision function for skill dispatch.
|
|
38
90
|
*
|
|
@@ -113,10 +165,35 @@ function decideSkillDispatch(skill, ctx) {
|
|
|
113
165
|
// one was an explicit choice.
|
|
114
166
|
if (!skill.enabled) {
|
|
115
167
|
if (skill.auto_disabled_at) {
|
|
168
|
+
// Half-open self-heal: after a growing cooldown, allow ONE probe run to
|
|
169
|
+
// test whether the fault cleared. Gated by ctx.selfHealEnabled (default
|
|
170
|
+
// ON). Only auto_disabled rows are probed; user-disabled / deprecated
|
|
171
|
+
// rows (no auto_disabled_at) are never resurrected. From skill-self-heal
|
|
172
|
+
// design 2026-06-24.
|
|
173
|
+
const selfHeal = ctx?.selfHealEnabled !== false;
|
|
174
|
+
const maxAttempts = ctx?.maxProbeAttempts ?? DEFAULT_MAX_PROBE_ATTEMPTS;
|
|
175
|
+
const attempts = skill.auto_disabled_attempts || 0;
|
|
176
|
+
const base = { since: skill.auto_disabled_at, why: skill.auto_disabled_reason || 'consecutive_failures' };
|
|
177
|
+
if (selfHeal && attempts < maxAttempts) {
|
|
178
|
+
const anchorRaw = new Date(skill.last_probe_at || skill.auto_disabled_at).getTime();
|
|
179
|
+
const anchor = Number.isFinite(anchorRaw) ? anchorRaw : now;
|
|
180
|
+
const delay = probeDelayFor(attempts, { seed: skill.id || skill.name, schedule: ctx?.probeSchedule });
|
|
181
|
+
const nextProbeAt = anchor + delay;
|
|
182
|
+
if (now >= nextProbeAt) {
|
|
183
|
+
return { decision: 'run', reason: 'probe', details: { attempt: attempts + 1, max: maxAttempts } };
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
decision: 'skip',
|
|
187
|
+
reason: 'auto_disabled',
|
|
188
|
+
details: { ...base, attempts, next_probe_at: new Date(nextProbeAt).toISOString() },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// Probes exhausted (or self-heal disabled) → stay disabled. The
|
|
192
|
+
// exhausted reason lets the UI show "needs attention" vs "recovering".
|
|
116
193
|
return {
|
|
117
194
|
decision: 'skip',
|
|
118
|
-
reason: 'auto_disabled',
|
|
119
|
-
details: {
|
|
195
|
+
reason: (selfHeal && attempts >= maxAttempts) ? 'auto_disabled_exhausted' : 'auto_disabled',
|
|
196
|
+
details: { ...base, attempts },
|
|
120
197
|
};
|
|
121
198
|
}
|
|
122
199
|
return { decision: 'skip', reason: 'disabled' };
|
|
@@ -189,4 +266,7 @@ module.exports = {
|
|
|
189
266
|
decideSkillDispatch,
|
|
190
267
|
DEFAULT_BACKOFF_SCHEDULE_MS,
|
|
191
268
|
backoffForFailureCount,
|
|
269
|
+
DEFAULT_PROBE_SCHEDULE_MS,
|
|
270
|
+
DEFAULT_MAX_PROBE_ATTEMPTS,
|
|
271
|
+
probeDelayFor,
|
|
192
272
|
};
|