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
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Wall-E Skill Self-Heal — Design Review</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{
|
|
9
|
+
--bg:#0d1117; --panel:#161b22; --panel2:#1c2230; --border:#2a3240;
|
|
10
|
+
--ink:#e6edf3; --muted:#8b97a7; --faint:#5c6675;
|
|
11
|
+
--blue:#4c8dff; --green:#3fb950; --amber:#d29922; --red:#f85149;
|
|
12
|
+
--purple:#a371f7; --teal:#39c5cf;
|
|
13
|
+
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
14
|
+
}
|
|
15
|
+
*{box-sizing:border-box}
|
|
16
|
+
body{margin:0;background:var(--bg);color:var(--ink);
|
|
17
|
+
font:15px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
|
|
18
|
+
.wrap{max-width:960px;margin:0 auto;padding:40px 28px 96px;}
|
|
19
|
+
h1{font-size:30px;font-weight:700;letter-spacing:-.02em;margin:0 0 6px;}
|
|
20
|
+
h2{font-size:21px;font-weight:650;margin:46px 0 14px;padding-bottom:8px;
|
|
21
|
+
border-bottom:1px solid var(--border);letter-spacing:-.01em;}
|
|
22
|
+
h3{font-size:16px;font-weight:650;margin:26px 0 8px;color:var(--ink);}
|
|
23
|
+
p{margin:10px 0;color:#cdd6e0;}
|
|
24
|
+
a{color:var(--blue);text-decoration:none;} a:hover{text-decoration:underline;}
|
|
25
|
+
.sub{color:var(--muted);font-size:15px;margin:0 0 4px;}
|
|
26
|
+
.meta{color:var(--faint);font-size:13px;margin-top:8px;}
|
|
27
|
+
code{font-family:var(--mono);font-size:.86em;background:#222b38;color:#c8d3e0;
|
|
28
|
+
padding:1.5px 5px;border-radius:5px;border:1px solid #2c3645;}
|
|
29
|
+
pre{background:#0a0e14;border:1px solid var(--border);border-radius:10px;
|
|
30
|
+
padding:14px 16px;overflow:auto;font-family:var(--mono);font-size:13px;line-height:1.55;color:#c8d3e0;}
|
|
31
|
+
pre .c{color:var(--faint);} pre .k{color:var(--purple);} pre .s{color:var(--green);} pre .f{color:var(--teal);}
|
|
32
|
+
.panel{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:16px 0;}
|
|
33
|
+
.grid{display:grid;gap:14px;}
|
|
34
|
+
.cols-2{grid-template-columns:1fr 1fr;} .cols-3{grid-template-columns:1fr 1fr 1fr;}
|
|
35
|
+
@media(max-width:720px){.cols-2,.cols-3{grid-template-columns:1fr;}}
|
|
36
|
+
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:13.5px;}
|
|
37
|
+
th,td{text-align:left;padding:9px 11px;border-bottom:1px solid var(--border);}
|
|
38
|
+
th{color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;}
|
|
39
|
+
td code{font-size:.92em;}
|
|
40
|
+
.tag{display:inline-block;font-size:11px;font-weight:600;padding:2px 8px;border-radius:20px;
|
|
41
|
+
border:1px solid;vertical-align:middle;}
|
|
42
|
+
.t-red{color:var(--red);border-color:#5a2027;background:#2a1418;}
|
|
43
|
+
.t-amber{color:var(--amber);border-color:#5a4416;background:#241d0c;}
|
|
44
|
+
.t-green{color:var(--green);border-color:#1d4427;background:#0e2415;}
|
|
45
|
+
.t-blue{color:var(--blue);border-color:#1d3a66;background:#0d1d36;}
|
|
46
|
+
.t-purple{color:var(--purple);border-color:#3d2a66;background:#1a1230;}
|
|
47
|
+
.t-gray{color:var(--muted);border-color:#333b48;background:#1a212c;}
|
|
48
|
+
.callout{border-left:3px solid var(--blue);background:var(--panel2);border-radius:0 10px 10px 0;
|
|
49
|
+
padding:12px 16px;margin:16px 0;}
|
|
50
|
+
.callout.warn{border-color:var(--amber);} .callout.bad{border-color:var(--red);}
|
|
51
|
+
.callout.good{border-color:var(--green);}
|
|
52
|
+
.callout b{color:var(--ink);}
|
|
53
|
+
.lead{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:4px;}
|
|
54
|
+
ul{margin:8px 0;padding-left:22px;} li{margin:5px 0;color:#cdd6e0;}
|
|
55
|
+
.kpill{font-family:var(--mono);font-size:12px;color:var(--amber);}
|
|
56
|
+
.num{font-variant-numeric:tabular-nums;}
|
|
57
|
+
.step{display:flex;gap:12px;align-items:flex-start;margin:10px 0;}
|
|
58
|
+
.step .n{flex:none;width:26px;height:26px;border-radius:50%;background:var(--panel2);
|
|
59
|
+
border:1px solid var(--border);display:grid;place-items:center;font-size:13px;font-weight:700;color:var(--blue);}
|
|
60
|
+
.mock{background:#10151d;border:1px solid var(--border);border-radius:10px;padding:14px 16px;font-size:13px;}
|
|
61
|
+
.mock .row{display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid #1d2430;}
|
|
62
|
+
.mock .row:last-child{border-bottom:none;}
|
|
63
|
+
.mock .nm{font-weight:650;} .mock .desc{color:var(--muted);font-size:12px;}
|
|
64
|
+
.dot{width:8px;height:8px;border-radius:50%;flex:none;}
|
|
65
|
+
.legend{display:flex;flex-wrap:wrap;gap:14px;margin:10px 0;font-size:12.5px;color:var(--muted);}
|
|
66
|
+
.legend span{display:inline-flex;align-items:center;gap:6px;}
|
|
67
|
+
footer{margin-top:60px;padding-top:20px;border-top:1px solid var(--border);color:var(--faint);font-size:13px;}
|
|
68
|
+
.toc{columns:2;font-size:13.5px;color:var(--muted);} .toc a{display:block;margin:3px 0;}
|
|
69
|
+
@media(max-width:720px){.toc{columns:1;}}
|
|
70
|
+
svg{max-width:100%;height:auto;display:block;margin:8px auto;}
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<div class="wrap">
|
|
75
|
+
|
|
76
|
+
<p class="sub">Wall-E · Background Skills · Reliability Design</p>
|
|
77
|
+
<h1>Self-Healing Skills</h1>
|
|
78
|
+
<p class="sub">A half-open circuit breaker so auto-disabled background skills recover on their own
|
|
79
|
+
— instead of dying silently and staying dead for months.</p>
|
|
80
|
+
<p class="meta">Design review · 2026-06-24 · status: <span class="tag t-amber">awaiting your sign-off</span>
|
|
81
|
+
· scope: <code>wall-e/skills</code>, <code>brain.js</code>, <code>walle.js</code></p>
|
|
82
|
+
|
|
83
|
+
<div class="panel">
|
|
84
|
+
<div class="lead">On this page</div>
|
|
85
|
+
<div class="toc">
|
|
86
|
+
<a href="#problem">1 · The problem (live data)</a>
|
|
87
|
+
<a href="#root">2 · Root cause</a>
|
|
88
|
+
<a href="#goal">3 · What "self-heal" should mean</a>
|
|
89
|
+
<a href="#design">4 · Design: half-open breaker</a>
|
|
90
|
+
<a href="#machine">5 · State machine</a>
|
|
91
|
+
<a href="#code">6 · Where it plugs in</a>
|
|
92
|
+
<a href="#diag">7 · Fixing blind failures</a>
|
|
93
|
+
<a href="#setup">8 · "Needs setup" vs "failure"</a>
|
|
94
|
+
<a href="#retire">9 · Retiring google-calendar</a>
|
|
95
|
+
<a href="#ui">10 · UI surfacing</a>
|
|
96
|
+
<a href="#safety">11 · Safety, flags, tests</a>
|
|
97
|
+
<a href="#sources">12 · Research basis</a>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- ───────────────────────── 1 ───────────────────────── -->
|
|
102
|
+
<h2 id="problem">1 · The problem, in live numbers</h2>
|
|
103
|
+
<p>You flagged two skills showing <span class="tag t-red">skipped: auto disabled</span> in the Skills tab.
|
|
104
|
+
A read-only query of the live brain DB (<code>~/.walle/live-data/wall-e-brain.db</code>) shows it's not
|
|
105
|
+
two — it's <b>four</b>, all tripped by the same <code>consecutive_failures</code> breaker:</p>
|
|
106
|
+
|
|
107
|
+
<table>
|
|
108
|
+
<thead><tr><th>Skill</th><th>Auto-disabled</th><th>Dead for</th><th class="num">ok / fail</th><th>Last captured error</th></tr></thead>
|
|
109
|
+
<tbody>
|
|
110
|
+
<tr><td><code>google-calendar</code></td><td>2026-06-17</td><td>7 days</td><td class="num">11678 / 156</td><td><code>exited code 1</code> · ~10.15s</td></tr>
|
|
111
|
+
<tr><td><code>gws-workspace</code></td><td>2026-04-27</td><td><b style="color:var(--red)">58 days</b></td><td class="num">1674 / 10</td><td><span class="meta">history pruned</span></td></tr>
|
|
112
|
+
<tr><td><code>slack-sync</code></td><td>2026-04-27</td><td>58 days</td><td class="num">0 / 12</td><td><span class="meta">never once succeeded</span></td></tr>
|
|
113
|
+
<tr><td><code>mcp-scan</code></td><td>2026-04-27</td><td>58 days</td><td class="num">2350 / 12</td><td><span class="meta">history pruned</span></td></tr>
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
|
|
117
|
+
<div class="callout bad">
|
|
118
|
+
<b>The headline:</b> three of these have been dead for <b>58 days</b> — across many Wall-E restarts —
|
|
119
|
+
and never came back on their own. The breaker only ever <b>opens</b>. It has no path to ever <b>close</b>.
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="grid cols-2">
|
|
123
|
+
<div class="panel">
|
|
124
|
+
<div class="lead">Why "99% success" is a lie here</div>
|
|
125
|
+
<p style="margin:6px 0">The UI's success rate is a <b>lifetime</b> average:
|
|
126
|
+
<code>success / (success + failure)</code>. <code>google-calendar</code> shows 99% because 11.6k old
|
|
127
|
+
successes drown out a <b>recent unbroken streak of failures</b>. The breaker fired correctly; the
|
|
128
|
+
headline metric just can't see it.</p>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="panel">
|
|
131
|
+
<div class="lead">Why a human can't debug it either</div>
|
|
132
|
+
<p style="margin:6px 0">The only stored error is <code>bundled skill "google-calendar" exited
|
|
133
|
+
code 1</code>. The skill's real stderr — token expired? <code>gws</code> CLI gone? Calendar
|
|
134
|
+
permission revoked? — is <b>thrown away</b>. The flat ~10.15s runtime even hints at a silent
|
|
135
|
+
timeout. You can't fix what you can't see.</p>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- ───────────────────────── 2 ───────────────────────── -->
|
|
140
|
+
<h2 id="root">2 · Root cause — the recovery path is hardcoded to one skill</h2>
|
|
141
|
+
<p>Wall-E <i>does</i> have an auto-recovery path. It just isn't general. On startup,
|
|
142
|
+
<code>syncBundledSkills()</code> clears the auto-disabled flag — but only inside an
|
|
143
|
+
<code>if</code> that names a single skill:</p>
|
|
144
|
+
<pre><span class="c">// agent.js:276 — the recovery gate</span>
|
|
145
|
+
<span class="k">if</span> (skill.name === <span class="s">'scan-ctm-sessions'</span> && skill.execution === <span class="s">'script'</span> && existing) {
|
|
146
|
+
<span class="c">// ...</span>
|
|
147
|
+
<span class="k">if</span> (existing.auto_disabled_at) { <span class="c">// agent.js:282-286</span>
|
|
148
|
+
updates.enabled = <span class="f">1</span>;
|
|
149
|
+
updates.auto_disabled_at = <span class="k">null</span>; <span class="c">// ← only scan-ctm-sessions ever reaches this</span>
|
|
150
|
+
}
|
|
151
|
+
}</pre>
|
|
152
|
+
<p>So <code>scan-ctm-sessions</code> resurrects every boot; everyone else stays disabled until you
|
|
153
|
+
manually flip the toggle (<code>PUT /api/wall-e/skills/:id {enabled:1}</code>, which clears
|
|
154
|
+
<code>auto_disabled_at</code> at <code>api-walle.js:4084-4092</code>). That's the entire reason three
|
|
155
|
+
skills sat dead for two months. <b>There is no time-based, general recovery anywhere.</b></p>
|
|
156
|
+
|
|
157
|
+
<!-- ───────────────────────── 3 ───────────────────────── -->
|
|
158
|
+
<h2 id="goal">3 · What "self-heal" should actually mean</h2>
|
|
159
|
+
<p>The failures here split into two kinds, and a good design must treat them differently:</p>
|
|
160
|
+
<div class="grid cols-2">
|
|
161
|
+
<div class="panel"><div class="lead" style="color:var(--green)">Transient — should auto-recover</div>
|
|
162
|
+
<ul><li>OAuth access token expired (refreshes fine next run)</li>
|
|
163
|
+
<li>Network blip / Google API 5xx</li><li>Brain DB briefly locked</li>
|
|
164
|
+
<li>Rate-limit cooldown</li></ul>
|
|
165
|
+
<p style="font-size:13px;color:var(--muted)">→ A retry minutes-to-hours later just works. No human needed.</p>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="panel"><div class="lead" style="color:var(--red)">Permanent — must escalate, not thrash</div>
|
|
168
|
+
<ul><li>macOS Calendar permission revoked</li><li><code>gws</code> CLI uninstalled / not on PATH</li>
|
|
169
|
+
<li>Refresh token revoked — re-auth required</li><li><code>swiftc</code> / toolchain missing</li></ul>
|
|
170
|
+
<p style="font-size:13px;color:var(--muted)">→ Retrying forever is pointless & noisy. Tell the human, with the real error.</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="callout good"><b>The textbook answer</b> (and your chosen direction): a
|
|
174
|
+
<b>half-open circuit breaker</b>. Probe cheaply after a cooldown; close on success; back off and
|
|
175
|
+
finally <b>escalate to a human</b> after a few failed probes. Transient breakage heals with zero
|
|
176
|
+
clicks; permanent breakage stops thrashing and surfaces a real, actionable error.</div>
|
|
177
|
+
|
|
178
|
+
<!-- ───────────────────────── 4 ───────────────────────── -->
|
|
179
|
+
<h2 id="design">4 · The design — a half-open breaker, expressed as data</h2>
|
|
180
|
+
<p>Wall-E's architecture has a hard rule (<code>docs/architecture.md</code> → Non-goals): loops
|
|
181
|
+
coordinate <b>via the brain DB and scheduler — never a manager-of-managers</b>. This design honors
|
|
182
|
+
that completely: <b>no new loop, no supervisor.</b> The half-open state is just two columns that the
|
|
183
|
+
existing pure decision function already reads on every tick.</p>
|
|
184
|
+
|
|
185
|
+
<div class="grid cols-3">
|
|
186
|
+
<div class="panel"><div class="lead">Add: 2 columns</div>
|
|
187
|
+
<p style="font-size:13.5px"><code>auto_disabled_attempts INTEGER DEFAULT 0</code><br><br>
|
|
188
|
+
<code>last_probe_at TEXT</code></p>
|
|
189
|
+
<p style="font-size:12.5px;color:var(--muted)"><code>auto_disabled_at</code> is preserved untouched
|
|
190
|
+
for display ("disabled 7d ago"); the new columns drive scheduling.</p>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="panel"><div class="lead">Probe schedule</div>
|
|
193
|
+
<p style="font-size:13.5px"><span class="kpill">[ 1h, 6h, 24h ]</span> exponential, <b>+ jitter</b><br><br>
|
|
194
|
+
<code>MAX_PROBE_ATTEMPTS = 3</code></p>
|
|
195
|
+
<p style="font-size:12.5px;color:var(--muted)">Mirrors the <i>existing</i> pre-disable
|
|
196
|
+
<code>DEFAULT_BACKOFF_SCHEDULE_MS</code> table — same idea, applied on the post-disable side.</p>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="panel"><div class="lead">Outcome</div>
|
|
199
|
+
<p style="font-size:13.5px"><span class="tag t-green">probe ok</span> → re-enable + reset<br><br>
|
|
200
|
+
<span class="tag t-amber">probe fail</span> → back off<br><br>
|
|
201
|
+
<span class="tag t-red">3× fail</span> → escalate & stop</p>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- ───────────────────────── 5 ───────────────────────── -->
|
|
206
|
+
<h2 id="machine">5 · State machine</h2>
|
|
207
|
+
<div class="panel">
|
|
208
|
+
<svg viewBox="0 0 820 300" role="img" aria-label="Half-open circuit breaker state machine">
|
|
209
|
+
<defs>
|
|
210
|
+
<marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
|
|
211
|
+
<path d="M0,0 L7,3 L0,6 Z" fill="#5c6675"/>
|
|
212
|
+
</marker>
|
|
213
|
+
<marker id="arrG" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
|
|
214
|
+
<path d="M0,0 L7,3 L0,6 Z" fill="#3fb950"/>
|
|
215
|
+
</marker>
|
|
216
|
+
<marker id="arrR" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
|
|
217
|
+
<path d="M0,0 L7,3 L0,6 Z" fill="#f85149"/>
|
|
218
|
+
</marker>
|
|
219
|
+
</defs>
|
|
220
|
+
<!-- CLOSED -->
|
|
221
|
+
<rect x="40" y="120" width="150" height="58" rx="11" fill="#0e2415" stroke="#3fb950" stroke-width="1.5"/>
|
|
222
|
+
<text x="115" y="146" text-anchor="middle" fill="#3fb950" font-size="15" font-weight="700">CLOSED</text>
|
|
223
|
+
<text x="115" y="164" text-anchor="middle" fill="#8b97a7" font-size="11">enabled · running</text>
|
|
224
|
+
<!-- OPEN -->
|
|
225
|
+
<rect x="335" y="20" width="150" height="58" rx="11" fill="#2a1418" stroke="#f85149" stroke-width="1.5"/>
|
|
226
|
+
<text x="410" y="46" text-anchor="middle" fill="#f85149" font-size="15" font-weight="700">OPEN</text>
|
|
227
|
+
<text x="410" y="64" text-anchor="middle" fill="#8b97a7" font-size="11">auto_disabled · waiting</text>
|
|
228
|
+
<!-- HALF-OPEN -->
|
|
229
|
+
<rect x="335" y="120" width="150" height="58" rx="11" fill="#241d0c" stroke="#d29922" stroke-width="1.5"/>
|
|
230
|
+
<text x="410" y="146" text-anchor="middle" fill="#d29922" font-size="15" font-weight="700">HALF-OPEN</text>
|
|
231
|
+
<text x="410" y="164" text-anchor="middle" fill="#8b97a7" font-size="11">one silent probe</text>
|
|
232
|
+
<!-- ESCALATED -->
|
|
233
|
+
<rect x="630" y="120" width="150" height="58" rx="11" fill="#1a1230" stroke="#a371f7" stroke-width="1.5"/>
|
|
234
|
+
<text x="705" y="146" text-anchor="middle" fill="#a371f7" font-size="15" font-weight="700">ESCALATED</text>
|
|
235
|
+
<text x="705" y="164" text-anchor="middle" fill="#8b97a7" font-size="11">human alert · stop</text>
|
|
236
|
+
|
|
237
|
+
<!-- CLOSED -> OPEN -->
|
|
238
|
+
<path d="M180,128 C250,90 280,60 333,52" fill="none" stroke="#f85149" stroke-width="1.6" marker-end="url(#arrR)"/>
|
|
239
|
+
<text x="232" y="78" fill="#f85149" font-size="11">10 consec. fails</text>
|
|
240
|
+
<!-- OPEN -> HALF-OPEN -->
|
|
241
|
+
<path d="M410,80 L410,118" fill="none" stroke="#5c6675" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
242
|
+
<text x="420" y="103" fill="#8b97a7" font-size="11">cooldown elapsed</text>
|
|
243
|
+
<!-- HALF-OPEN -> CLOSED (success) -->
|
|
244
|
+
<path d="M333,158 C260,196 240,180 190,162" fill="none" stroke="#3fb950" stroke-width="1.6" marker-end="url(#arrG)"/>
|
|
245
|
+
<text x="208" y="200" fill="#3fb950" font-size="11">probe ok → re-enable</text>
|
|
246
|
+
<!-- HALF-OPEN -> OPEN (fail, attempts left) -->
|
|
247
|
+
<path d="M428,118 C455,100 455,80 440,80" fill="none" stroke="#d29922" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
248
|
+
<text x="470" y="100" fill="#d29922" font-size="11">probe fail → back off</text>
|
|
249
|
+
<!-- HALF-OPEN -> ESCALATED -->
|
|
250
|
+
<path d="M485,149 L628,149" fill="none" stroke="#a371f7" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
251
|
+
<text x="500" y="140" fill="#a371f7" font-size="11">3rd probe fails</text>
|
|
252
|
+
<!-- ESCALATED -> CLOSED (manual) -->
|
|
253
|
+
<path d="M630,176 C500,250 250,250 115,180" fill="none" stroke="#5c6675" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#arr)"/>
|
|
254
|
+
<text x="360" y="262" fill="#8b97a7" font-size="11">manual re-enable (toggle) — unchanged escape hatch</text>
|
|
255
|
+
</svg>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="legend">
|
|
258
|
+
<span><span class="dot" style="background:#3fb950"></span>healthy / recovered</span>
|
|
259
|
+
<span><span class="dot" style="background:#d29922"></span>recovering (transient)</span>
|
|
260
|
+
<span><span class="dot" style="background:#f85149"></span>open / failing</span>
|
|
261
|
+
<span><span class="dot" style="background:#a371f7"></span>needs a human</span>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- ───────────────────────── 6 ───────────────────────── -->
|
|
265
|
+
<h2 id="code">6 · Where it plugs in (two surgical edits)</h2>
|
|
266
|
+
<p>The decision logic is already a <b>pure function</b> that sees every skill — including disabled
|
|
267
|
+
ones — on every scheduler tick. Today its auto-disabled branch is a dead end. We make it bloom a probe.</p>
|
|
268
|
+
|
|
269
|
+
<h3>a · <code>skills/skill-dispatch-decision.js</code> — the disabled branch (currently lines 114-121)</h3>
|
|
270
|
+
<pre><span class="c">// BEFORE — skips forever</span>
|
|
271
|
+
<span class="k">if</span> (!skill.enabled) {
|
|
272
|
+
<span class="k">if</span> (skill.auto_disabled_at) {
|
|
273
|
+
<span class="k">return</span> { decision:<span class="s">'skip'</span>, reason:<span class="s">'auto_disabled'</span>, details:{ since:skill.auto_disabled_at } };
|
|
274
|
+
}
|
|
275
|
+
<span class="k">return</span> { decision:<span class="s">'skip'</span>, reason:<span class="s">'disabled'</span> };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
<span class="c">// AFTER — half-open probe (only for auto_disabled, never user/deprecated-disabled)</span>
|
|
279
|
+
<span class="k">if</span> (!skill.enabled) {
|
|
280
|
+
<span class="k">if</span> (skill.auto_disabled_at) {
|
|
281
|
+
<span class="k">const</span> attempts = skill.auto_disabled_attempts || <span class="f">0</span>;
|
|
282
|
+
<span class="k">if</span> (attempts >= MAX_PROBE_ATTEMPTS)
|
|
283
|
+
<span class="k">return</span> { decision:<span class="s">'skip'</span>, reason:<span class="s">'auto_disabled_exhausted'</span>, details:{ attempts } };
|
|
284
|
+
<span class="k">const</span> anchor = <span class="k">new</span> Date(skill.last_probe_at || skill.auto_disabled_at).getTime();
|
|
285
|
+
<span class="k">const</span> nextProbeAt = anchor + probeScheduleFor(attempts); <span class="c">// 1h,6h,24h +jitter</span>
|
|
286
|
+
<span class="k">if</span> (now >= nextProbeAt)
|
|
287
|
+
<span class="k">return</span> { decision:<span class="s">'run'</span>, reason:<span class="s">'probe'</span>, details:{ attempt: attempts + <span class="f">1</span> } };
|
|
288
|
+
<span class="k">return</span> { decision:<span class="s">'skip'</span>, reason:<span class="s">'auto_disabled'</span>,
|
|
289
|
+
details:{ since:skill.auto_disabled_at, next_probe_at:<span class="k">new</span> Date(nextProbeAt).toISOString(), attempts } };
|
|
290
|
+
}
|
|
291
|
+
<span class="k">return</span> { decision:<span class="s">'skip'</span>, reason:<span class="s">'disabled'</span> }; <span class="c">// user/deprecated — untouched</span>
|
|
292
|
+
}</pre>
|
|
293
|
+
|
|
294
|
+
<h3>b · <code>skills/skill-planner.js</code> — handle the probe outcome (next to the existing 733-748 block)</h3>
|
|
295
|
+
<pre><span class="c">// after a `reason:'probe'` dispatch finishes, branch on the result:</span>
|
|
296
|
+
<span class="k">if</span> (decision.reason === <span class="s">'probe'</span>) {
|
|
297
|
+
<span class="k">if</span> (result.ok) { <span class="c">// CLOSE the breaker</span>
|
|
298
|
+
brain.updateSkill(skill.id, { enabled:<span class="f">1</span>, auto_disabled_at:<span class="k">null</span>,
|
|
299
|
+
auto_disabled_reason:<span class="k">null</span>, auto_disabled_attempts:<span class="f">0</span>, last_probe_at:<span class="k">null</span> });
|
|
300
|
+
clearServiceAlerts(skill.name);
|
|
301
|
+
recoveryLog.info(<span class="s">'skill-self-heal'</span>, <span class="s">'recovered'</span>, { skill: skill.name });
|
|
302
|
+
} <span class="k">else</span> { <span class="c">// stay OPEN, back off</span>
|
|
303
|
+
<span class="k">const</span> attempts = (skill.auto_disabled_attempts || <span class="f">0</span>) + <span class="f">1</span>;
|
|
304
|
+
brain.updateSkill(skill.id, { auto_disabled_attempts: attempts, last_probe_at: nowIso });
|
|
305
|
+
<span class="k">if</span> (attempts >= MAX_PROBE_ATTEMPTS)
|
|
306
|
+
addServiceAlert({ service:skill.name, type:<span class="s">'skill_needs_attention'</span>,
|
|
307
|
+
message:<span class="s">`"${skill.name}" failed ${attempts} recovery attempts: ${result.errorTail}`</span>,
|
|
308
|
+
action_url:<span class="s">'/setup.html'</span> });
|
|
309
|
+
}
|
|
310
|
+
<span class="k">continue</span>;
|
|
311
|
+
}</pre>
|
|
312
|
+
<div class="callout"><b>That's the whole engine.</b> Everything else — the scheduler tick, the
|
|
313
|
+
per-skill decision recording (<code>recordSkillLastDecision</code>), the existing Service-Alert
|
|
314
|
+
plumbing — is reused as-is.</div>
|
|
315
|
+
|
|
316
|
+
<!-- ───────────────────────── 7 ───────────────────────── -->
|
|
317
|
+
<h2 id="diag">7 · Fixing blind failures (so probes are meaningful)</h2>
|
|
318
|
+
<p>A probe is only useful if a <i>failed</i> probe tells us <i>why</i>. Today the script runner
|
|
319
|
+
collapses everything to <code>exited code 1</code>. We capture the tail of the skill's stderr/stdout
|
|
320
|
+
into <code>skill_executions.error</code> and <code>skills.last_result</code>, so:</p>
|
|
321
|
+
<div class="grid cols-2">
|
|
322
|
+
<div class="panel"><div class="lead t-red" style="color:var(--red)">Today</div>
|
|
323
|
+
<pre style="margin:6px 0"><span class="c">error:</span> bundled skill "google-calendar" exited code 1</pre></div>
|
|
324
|
+
<div class="panel"><div class="lead" style="color:var(--green)">After</div>
|
|
325
|
+
<pre style="margin:6px 0"><span class="c">error:</span> Calendar access denied (status=2).
|
|
326
|
+
Grant access in System Settings ›
|
|
327
|
+
Privacy & Security › Calendars.</pre></div>
|
|
328
|
+
</div>
|
|
329
|
+
<p style="font-size:13.5px;color:var(--muted)">This also lets the escalation alert quote the real cause,
|
|
330
|
+
and lets us confirm the suspicious ~10.15s flat runtime (a hidden timeout vs the documented 30s binary timeout).</p>
|
|
331
|
+
|
|
332
|
+
<!-- ───────────────────────── 8 ───────────────────────── -->
|
|
333
|
+
<h2 id="setup">8 · "Needs setup" is not "failure" (secondary)</h2>
|
|
334
|
+
<p>Some skills aren't <i>broken</i> — they're <i>unconfigured</i>. <code>gws-workspace</code> returns
|
|
335
|
+
"no accounts configured"; that currently exits non-zero and burns the failure budget until the breaker
|
|
336
|
+
trips. Wall-E already has a <code>requires</code> preflight (<code>skill-planner.js:757-779</code>) that
|
|
337
|
+
raises actionable alerts. We extend it: an unconfigured/missing-dependency outcome reports
|
|
338
|
+
<span class="tag t-blue">needs_setup</span> — it does <b>not</b> count as a failure and does <b>not</b>
|
|
339
|
+
auto-disable; it surfaces a one-click setup prompt instead of dying silently.</p>
|
|
340
|
+
|
|
341
|
+
<!-- ───────────────────────── 9 ───────────────────────── -->
|
|
342
|
+
<h2 id="retire">9 · Retiring <code>google-calendar</code></h2>
|
|
343
|
+
<p>Per your call — <i>"just use gws-workspace, I don't think you need google-calendar anymore."</i>
|
|
344
|
+
<code>gws-workspace</code>'s own SKILL.md says it <b>"replaces separate google-calendar, email-sync, and
|
|
345
|
+
gdrive skills"</b> with no macOS-only dependencies. Running both also double-writes <code>calendar</code>
|
|
346
|
+
memories under the same dedup key.</p>
|
|
347
|
+
<div class="step"><div class="n">1</div><div>One-time migration (#28): set <code>enabled=0</code>,
|
|
348
|
+
<code>auto_disabled_reason='deprecated'</code>, and crucially <code>auto_disabled_at=NULL</code>.</div></div>
|
|
349
|
+
<div class="step"><div class="n">2</div><div>Because the prober only probes rows with a non-null
|
|
350
|
+
<code>auto_disabled_at</code>, the deprecated skill is <b>deliberately never resurrected</b> — it reads as a clean "disabled", not a recovering one.</div></div>
|
|
351
|
+
<div class="step"><div class="n">3</div><div><b>Keep the bundled dir.</b> The implementation deliberately does
|
|
352
|
+
<i>not</i> delete <code>skills/_bundled/google-calendar/</code>: <code>listening/calendar.js</code> reuses
|
|
353
|
+
its compiled <code>cal-reader</code> binary, so retirement is a DB-row deprecation, not a file removal.</div></div>
|
|
354
|
+
<div class="callout warn"><b>Note:</b> <code>gws-workspace</code> itself is currently auto-disabled.
|
|
355
|
+
Once self-heal ships, it gets probed automatically. If its OAuth / <code>gws</code> CLI is genuinely
|
|
356
|
+
broken, it will escalate after 3 probes with the <i>real</i> error (per §7) so you can fix the setup once — then it heals itself.</div>
|
|
357
|
+
|
|
358
|
+
<!-- ───────────────────────── 10 ───────────────────────── -->
|
|
359
|
+
<h2 id="ui">10 · What you'll see in the UI</h2>
|
|
360
|
+
<p>No new screens — just richer status on the existing skill cards and health panel.</p>
|
|
361
|
+
<div class="mock">
|
|
362
|
+
<div class="row"><span class="dot" style="background:#d29922"></span>
|
|
363
|
+
<span class="nm">gws-workspace</span><span class="tag t-amber">recovering</span>
|
|
364
|
+
<span class="desc">probe 2 / 3 · next attempt in ~6h</span></div>
|
|
365
|
+
<div class="row"><span class="dot" style="background:#3fb950"></span>
|
|
366
|
+
<span class="nm">slack-sync</span><span class="tag t-green">self-healed</span>
|
|
367
|
+
<span class="desc">recovered automatically 3m ago</span></div>
|
|
368
|
+
<div class="row"><span class="dot" style="background:#a371f7"></span>
|
|
369
|
+
<span class="nm">mcp-scan</span><span class="tag t-purple">needs attention</span>
|
|
370
|
+
<span class="desc">3 recovery attempts failed · "MCP endpoint refused connection"</span></div>
|
|
371
|
+
<div class="row"><span class="dot" style="background:#5c6675"></span>
|
|
372
|
+
<span class="nm">google-calendar</span><span class="tag t-gray">deprecated</span>
|
|
373
|
+
<span class="desc">superseded by gws-workspace</span></div>
|
|
374
|
+
</div>
|
|
375
|
+
<ul>
|
|
376
|
+
<li><b>Card chip</b> (<code>renderSkillCard</code>, walle.js:5582) gains <code>recovering</code> /
|
|
377
|
+
<code>needs attention</code> / <code>deprecated</code> states.</li>
|
|
378
|
+
<li><b>Recovery toast</b> when a skill self-heals — positive signal, not just silence.</li>
|
|
379
|
+
<li><b>Health panel</b> (<code>renderServiceHealthPanel</code>, walle.js:6946) splits "recovering"
|
|
380
|
+
(transient, no action) from "needs attention" (escalated, click to fix).</li>
|
|
381
|
+
</ul>
|
|
382
|
+
|
|
383
|
+
<!-- ───────────────────────── 11 ───────────────────────── -->
|
|
384
|
+
<h2 id="safety">11 · Safety, flags & tests</h2>
|
|
385
|
+
<div class="grid cols-2">
|
|
386
|
+
<div class="panel"><div class="lead">Why this can't make things worse</div>
|
|
387
|
+
<ul>
|
|
388
|
+
<li>Probes are <b>rate-limited</b> (1h→6h→24h) and <b>capped</b> (3) — no thrash.</li>
|
|
389
|
+
<li>A probe is one normal run; it touches nothing a manual re-enable wouldn't.</li>
|
|
390
|
+
<li>Permanent breakage <b>stops & escalates</b> instead of looping.</li>
|
|
391
|
+
<li>User-disabled & <code>deprecated</code> skills are never probed (no <code>auto_disabled_at</code>).</li>
|
|
392
|
+
<li>It only ever <i>adds</i> recovery; worst case it does what you'd do by hand, slower.</li>
|
|
393
|
+
</ul>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="panel"><div class="lead">Flag & rollback</div>
|
|
396
|
+
<p style="font-size:13.5px"><code>WALLE_SKILL_SELF_HEAL</code> — default <b>ON</b>.<br>
|
|
397
|
+
Probe schedule & <code>MAX_PROBE_ATTEMPTS</code> configurable.<br>
|
|
398
|
+
<code>=0</code> restores today's open-forever behavior exactly.</p>
|
|
399
|
+
<div class="lead" style="margin-top:14px">Tests</div>
|
|
400
|
+
<ul style="font-size:13.5px">
|
|
401
|
+
<li>decision fn: cooldown-elapsed → <code>probe</code>; pending → <code>auto_disabled</code> w/ <code>next_probe_at</code>; capped → <code>exhausted</code>.</li>
|
|
402
|
+
<li>planner: probe ok re-enables & clears; probe fail increments & escalates at 3.</li>
|
|
403
|
+
<li>migration: <code>google-calendar</code> → deprecated, never probed.</li>
|
|
404
|
+
</ul>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
<div class="callout"><b>Verification</b> — on a <code>/ctm-dev</code> instance only (never primary
|
|
408
|
+
3456/3457): seed an <code>auto_disabled</code> skill, advance the clock, confirm probe → recover, and
|
|
409
|
+
exhaustion → escalate. Then <code>cd wall-e && npm test</code>. Server-side changes deploy on the
|
|
410
|
+
next primary restart, which you trigger.</div>
|
|
411
|
+
|
|
412
|
+
<!-- ───────────────────────── 12 ───────────────────────── -->
|
|
413
|
+
<h2 id="sources">12 · Research basis</h2>
|
|
414
|
+
<p style="font-size:13.5px;color:var(--muted)">The pattern is standard reliability engineering, adapted to Wall-E's data-driven, no-supervisor constraint: half-open probing, exponential backoff <i>with jitter</i>, a cheap health probe, and escalation-to-human after N attempts.</p>
|
|
415
|
+
<ul style="font-size:13.5px">
|
|
416
|
+
<li><a href="https://blog.meganova.ai/circuit-breakers-in-ai-agent-systems-reliability-at-scale/">Circuit Breakers in AI Agent Systems — Reliability at Scale</a> (half-open auto-recovery, cheap probe)</li>
|
|
417
|
+
<li><a href="https://planetscale.com/blog/how-we-made-planetscale-background-jobs-self-healing-with-sidekiq">PlanetScale — making background jobs self-healing</a> (data-driven re-queue, no supervisor)</li>
|
|
418
|
+
<li><a href="https://medium.com/@atulbanwar/smart-jenkins-management-auto-disabling-jobs-on-consecutive-failures-6dd80b15553b">Jenkins — auto-disabling jobs on consecutive failures</a> (the open side, and why it needs a close side)</li>
|
|
419
|
+
<li><a href="https://learn.microsoft.com/en-us/azure/well-architected/reliability/self-preservation">Azure Well-Architected — self-healing & self-preservation</a> (escalate, don't thrash)</li>
|
|
420
|
+
</ul>
|
|
421
|
+
|
|
422
|
+
<footer>
|
|
423
|
+
Wall-E Skill Self-Heal · design review · 2026-06-24 · awaiting sign-off before any code is written.<br>
|
|
424
|
+
Next step on approval: implement §6 behind <code>WALLE_SKILL_SELF_HEAL</code>, validate on <code>/ctm-dev</code>, then <code>/tl-commit</code>.
|
|
425
|
+
</footer>
|
|
426
|
+
|
|
427
|
+
</div>
|
|
428
|
+
</body>
|
|
429
|
+
</html>
|
|
@@ -6,6 +6,15 @@ const { isPortkeyProviderConfig } = require('./portkey');
|
|
|
6
6
|
const { withProviderMessageGuard } = require('./message-guard');
|
|
7
7
|
|
|
8
8
|
const LOCAL_PROVIDER_TYPES = new Set(['ollama', 'mlx', 'lmstudio']);
|
|
9
|
+
const DEFAULT_PROVIDER_HEALTH_FAILURE_TYPES = new Set([
|
|
10
|
+
'network',
|
|
11
|
+
'timeout',
|
|
12
|
+
'provider_unavailable',
|
|
13
|
+
'rate_limited',
|
|
14
|
+
'auth_error',
|
|
15
|
+
'quota_exceeded',
|
|
16
|
+
'model_unavailable',
|
|
17
|
+
]);
|
|
9
18
|
|
|
10
19
|
function normalizeProviderType(type) {
|
|
11
20
|
const text = String(type || '').trim().toLowerCase().replace(/_/g, '-');
|
|
@@ -201,7 +210,7 @@ function _resolvePrimaryDefault() {
|
|
|
201
210
|
// Anthropic auth_method=claude_cli routes through the user's `claude`
|
|
202
211
|
// CLI (subscription billing) instead of the SDK against api.anthropic.com.
|
|
203
212
|
if (providerType === 'anthropic' && authMethod === 'claude_cli') {
|
|
204
|
-
return { id: 'claude-cli', type: 'claude-cli', client: createClient('claude-cli', {}) };
|
|
213
|
+
return { id: 'claude-cli', type: 'claude-cli', client: createClient('claude-cli', {}), model: getDefaultModel() };
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
// Anthropic auth_method=oauth_proxy points the SDK at the local proxy
|
|
@@ -213,13 +222,14 @@ function _resolvePrimaryDefault() {
|
|
|
213
222
|
id: `${providerType}-oauth`,
|
|
214
223
|
type: providerType,
|
|
215
224
|
client: createClient('anthropic', { apiKey: 'oauth-proxy-placeholder', baseUrl: `http://127.0.0.1:${port}` }),
|
|
225
|
+
model: getDefaultModel(),
|
|
216
226
|
};
|
|
217
227
|
}
|
|
218
228
|
|
|
219
229
|
// OpenAI auth_method=codex_cli routes through the user's `codex` CLI
|
|
220
230
|
// (ChatGPT subscription billing) instead of api.openai.com.
|
|
221
231
|
if (providerType === 'openai' && authMethod === 'codex_cli') {
|
|
222
|
-
return { id: 'codex-cli', type: 'codex-cli', client: createClient('codex-cli', {}) };
|
|
232
|
+
return { id: 'codex-cli', type: 'codex-cli', client: createClient('codex-cli', {}), model: resolveCompatibleModel(getDefaultModel(), 'codex-cli') };
|
|
223
233
|
}
|
|
224
234
|
|
|
225
235
|
let client;
|
|
@@ -266,7 +276,7 @@ function _resolvePrimaryDefault() {
|
|
|
266
276
|
const preferred = brain.getPreferredModelProviderForType?.(providerType);
|
|
267
277
|
if (preferred?.id) id = preferred.id;
|
|
268
278
|
} catch {}
|
|
269
|
-
return { id, type: providerType, client };
|
|
279
|
+
return { id, type: providerType, client, model: getDefaultModel() };
|
|
270
280
|
}
|
|
271
281
|
|
|
272
282
|
// Surface a fallback to the user/operator: the live default was unreachable and a
|
|
@@ -295,6 +305,17 @@ function _notifyDefaultFallback(from, to, error) {
|
|
|
295
305
|
} catch {}
|
|
296
306
|
}
|
|
297
307
|
try { console.warn(`[llm] ${message}${detail ? ' — ' + detail : ''}`); } catch {}
|
|
308
|
+
if (error) {
|
|
309
|
+
try {
|
|
310
|
+
const providerErrorMod = require('./provider-error');
|
|
311
|
+
const providerError = providerErrorMod.classifyProviderError(error, {
|
|
312
|
+
provider: from?.type || from?.id || null,
|
|
313
|
+
model: from?.model || null,
|
|
314
|
+
providerId: from?.id || null,
|
|
315
|
+
});
|
|
316
|
+
providerErrorMod.recordProviderFailureAlert(providerError, require('../brain'), { recordHealth: false });
|
|
317
|
+
} catch {}
|
|
318
|
+
}
|
|
298
319
|
try {
|
|
299
320
|
const health = require('./health').default;
|
|
300
321
|
health.emit('default-provider-fallback', { from: { id: from?.id, type: from?.type }, to: { id: to?.id, type: to?.type }, detail });
|
|
@@ -307,8 +328,20 @@ function _notifyDefaultExhausted(error) {
|
|
|
307
328
|
// Secret-redacted detail (replaces the old raw error.message, which could echo creds).
|
|
308
329
|
let detail = '';
|
|
309
330
|
try { detail = require('./provider-error').fallbackErrorDetail(error); } catch {}
|
|
310
|
-
const
|
|
331
|
+
const localUnavailable = error?.suppressProviderHealthRecord === true
|
|
332
|
+
|| error?.providerError?.type === 'provider_unavailable_local'
|
|
333
|
+
|| error?.code === 'AI_PROVIDER_UNAVAILABLE_LOCAL';
|
|
334
|
+
const message = localUnavailable
|
|
335
|
+
? `Every configured provider/model route is currently skipped by health policy. Wall-E will wait for recovery probes or Setup changes.${detail ? ' (' + detail + ')' : ''}`
|
|
336
|
+
: `Every configured provider is unreachable. Check connectivity or Setup.${detail ? ' (' + detail + ')' : ''}`;
|
|
311
337
|
try { console.error(`[llm] ${message}`); } catch {}
|
|
338
|
+
if (error && !localUnavailable) {
|
|
339
|
+
try {
|
|
340
|
+
const providerErrorMod = require('./provider-error');
|
|
341
|
+
const providerError = providerErrorMod.classifyProviderError(error);
|
|
342
|
+
providerErrorMod.recordProviderFailureAlert(providerError, require('../brain'));
|
|
343
|
+
} catch {}
|
|
344
|
+
}
|
|
312
345
|
try {
|
|
313
346
|
const health = require('./health').default;
|
|
314
347
|
health.emit('default-provider-unreachable', { error: detail || String(error?.message || error || '') });
|
|
@@ -320,6 +353,68 @@ function _clearDefaultRoutingNotice() {
|
|
|
320
353
|
try { require('./health').default.clearRoutingNotice(); } catch {}
|
|
321
354
|
}
|
|
322
355
|
|
|
356
|
+
function _recordDefaultProviderHealthFailure({ provider, model, error, type, willRetry }) {
|
|
357
|
+
if (willRetry) return;
|
|
358
|
+
const providerType = provider?.type || provider?.id || '';
|
|
359
|
+
if (!providerType) return;
|
|
360
|
+
let providerError = null;
|
|
361
|
+
try {
|
|
362
|
+
const providerErrorMod = require('./provider-error');
|
|
363
|
+
providerError = providerErrorMod.classifyProviderError(error, {
|
|
364
|
+
provider: providerType,
|
|
365
|
+
model: model || provider?.model || '',
|
|
366
|
+
providerId: provider?.id || null,
|
|
367
|
+
});
|
|
368
|
+
} catch {}
|
|
369
|
+
const errorType = type || providerError?.type || 'provider_error';
|
|
370
|
+
if (!DEFAULT_PROVIDER_HEALTH_FAILURE_TYPES.has(errorType)) return;
|
|
371
|
+
try {
|
|
372
|
+
require('./provider-health-state').recordProviderHealthStatus({
|
|
373
|
+
provider: providerType,
|
|
374
|
+
provider_id: provider?.id || null,
|
|
375
|
+
runtime_type: providerType,
|
|
376
|
+
provider_type: providerType,
|
|
377
|
+
model: model || provider?.model || '',
|
|
378
|
+
ok: false,
|
|
379
|
+
source: 'default-client',
|
|
380
|
+
error_type: errorType,
|
|
381
|
+
error: providerError?.rawMessage || error?.message || String(error || ''),
|
|
382
|
+
checked_at: providerError?.createdAt || new Date().toISOString(),
|
|
383
|
+
}, { brain: require('../brain'), clearTransientAlerts: false });
|
|
384
|
+
} catch {}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function _recordDefaultProviderHealthOk(provider, model) {
|
|
388
|
+
try {
|
|
389
|
+
require('./provider-health-state').recordProviderHealthStatus({
|
|
390
|
+
provider: provider?.type || provider?.id || 'default',
|
|
391
|
+
provider_id: provider?.id || null,
|
|
392
|
+
runtime_type: provider?.type || '',
|
|
393
|
+
provider_type: provider?.type || '',
|
|
394
|
+
model: model || provider?.model || '',
|
|
395
|
+
ok: true,
|
|
396
|
+
source: 'default-client',
|
|
397
|
+
}, { brain: require('../brain') });
|
|
398
|
+
} catch {}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function _defaultProviderModelCircuitBlocks(id, cand, callOpts, brain) {
|
|
402
|
+
try {
|
|
403
|
+
const model = callOpts?.model || cand?.model || '';
|
|
404
|
+
const providers = [cand?.type, id].filter(Boolean);
|
|
405
|
+
const healthState = require('./provider-health-state');
|
|
406
|
+
const circuit = healthState.getFirstProviderModelCircuit(providers, model, { brain });
|
|
407
|
+
if (!circuit) return false;
|
|
408
|
+
if (circuit.probe_due) {
|
|
409
|
+
healthState.reserveProviderModelCircuitProbe(circuit.provider, circuit.model, { brain });
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
323
418
|
function getDefaultClient() {
|
|
324
419
|
if (_defaultClient) return _defaultClient;
|
|
325
420
|
|
|
@@ -351,6 +446,8 @@ function getDefaultClient() {
|
|
|
351
446
|
if (routingSettings.mode === 'strict' || routingSettings.fallback_enabled === false) {
|
|
352
447
|
_defaultClient = makeResilientClient([primary], {
|
|
353
448
|
health,
|
|
449
|
+
isDown: (id, cand, callOpts) => _defaultProviderModelCircuitBlocks(id, cand, callOpts, brain),
|
|
450
|
+
onCandidateFailure: _recordDefaultProviderHealthFailure,
|
|
354
451
|
onExhausted: ({ error }) => _notifyDefaultExhausted(error),
|
|
355
452
|
});
|
|
356
453
|
return _defaultClient;
|
|
@@ -371,15 +468,24 @@ function getDefaultClient() {
|
|
|
371
468
|
// Nothing to fall back to — still add transient retries around the primary.
|
|
372
469
|
_defaultClient = makeResilientClient([primary], {
|
|
373
470
|
health,
|
|
471
|
+
isDown: (id, cand, callOpts) => _defaultProviderModelCircuitBlocks(id, cand, callOpts, brain),
|
|
472
|
+
onCandidateFailure: _recordDefaultProviderHealthFailure,
|
|
374
473
|
onExhausted: ({ error }) => _notifyDefaultExhausted(error),
|
|
375
474
|
});
|
|
376
475
|
return _defaultClient;
|
|
377
476
|
}
|
|
378
477
|
_defaultClient = makeResilientClient(chain, {
|
|
379
478
|
health,
|
|
380
|
-
isDown: (id) => {
|
|
381
|
-
try {
|
|
479
|
+
isDown: (id, cand, callOpts) => {
|
|
480
|
+
try {
|
|
481
|
+
if (health?.getProviderHealth?.(id)?.status === 'down') return true;
|
|
482
|
+
} catch {}
|
|
483
|
+
return _defaultProviderModelCircuitBlocks(id, cand, callOpts, brain);
|
|
484
|
+
},
|
|
485
|
+
onCandidateOk: ({ provider, model }) => {
|
|
486
|
+
_recordDefaultProviderHealthOk(provider, model);
|
|
382
487
|
},
|
|
488
|
+
onCandidateFailure: _recordDefaultProviderHealthFailure,
|
|
383
489
|
onFallback: ({ from, to, error }) => _notifyDefaultFallback(from, to, error),
|
|
384
490
|
onPrimaryOk: () => _clearDefaultRoutingNotice(),
|
|
385
491
|
onExhausted: ({ error }) => _notifyDefaultExhausted(error),
|
|
@@ -513,4 +619,7 @@ module.exports = {
|
|
|
513
619
|
providerRegistry.ensureBootstrapped();
|
|
514
620
|
return providerRegistry.modelMatchesProvider(model, provider);
|
|
515
621
|
},
|
|
622
|
+
_internals: {
|
|
623
|
+
_resolvePrimaryDefault,
|
|
624
|
+
},
|
|
516
625
|
};
|