create-walle 0.9.11 → 0.9.13

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.
Files changed (167) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +62 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/hooks/cli.js +92 -0
  96. package/template/wall-e/hooks/discovery.js +119 -0
  97. package/template/wall-e/hooks/index.js +7 -0
  98. package/template/wall-e/hooks/manifest.js +55 -0
  99. package/template/wall-e/hooks/runtime.js +84 -0
  100. package/template/wall-e/hooks/session-memory.js +225 -0
  101. package/template/wall-e/http/auth.js +6 -2
  102. package/template/wall-e/http/chat-api.js +54 -8
  103. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  104. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  106. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  108. package/template/wall-e/listening/calendar.js +3 -1
  109. package/template/wall-e/llm/client.js +64 -10
  110. package/template/wall-e/llm/google.js +39 -5
  111. package/template/wall-e/llm/ollama.js +1 -1
  112. package/template/wall-e/llm/ollama.plugin.json +1 -1
  113. package/template/wall-e/llm/provider-availability.js +10 -0
  114. package/template/wall-e/llm/provider-error.js +269 -0
  115. package/template/wall-e/llm/tool-adapter.js +48 -12
  116. package/template/wall-e/loops/boot.js +2 -1
  117. package/template/wall-e/loops/initiative.js +2 -2
  118. package/template/wall-e/loops/tasks.js +8 -47
  119. package/template/wall-e/loops/workspace-prompts.js +20 -0
  120. package/template/wall-e/mcp-server.js +442 -1
  121. package/template/wall-e/memory/session-ingest-service.js +159 -0
  122. package/template/wall-e/memory/source-indexer.js +289 -0
  123. package/template/wall-e/plugins/discovery.js +83 -0
  124. package/template/wall-e/plugins/manifest-loader.js +50 -10
  125. package/template/wall-e/plugins/manifest-schema.js +69 -0
  126. package/template/wall-e/plugins/model-catalog.js +55 -0
  127. package/template/wall-e/prompts/coding/base.txt +2 -0
  128. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  129. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  130. package/template/wall-e/prompts/coding/plan.txt +1 -0
  131. package/template/wall-e/runtime/execution-trace.js +220 -0
  132. package/template/wall-e/security/audit.js +266 -0
  133. package/template/wall-e/security/ssrf.js +236 -0
  134. package/template/wall-e/session-files.js +303 -0
  135. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  136. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  137. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  138. package/template/wall-e/skills/script-skill-runner.js +143 -0
  139. package/template/wall-e/skills/skill-executor.js +5 -6
  140. package/template/wall-e/skills/skill-fallback.js +3 -1
  141. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  142. package/template/wall-e/skills/skill-planner.js +52 -4
  143. package/template/wall-e/skills/slack-ingest.js +11 -3
  144. package/template/wall-e/sources/base.js +90 -0
  145. package/template/wall-e/sources/builtin.js +33 -0
  146. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  147. package/template/wall-e/sources/codex-jsonl.js +125 -0
  148. package/template/wall-e/sources/coding-session-utils.js +117 -0
  149. package/template/wall-e/sources/contract-suite.js +59 -0
  150. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  151. package/template/wall-e/sources/index.js +9 -0
  152. package/template/wall-e/sources/jsonl-utils.js +181 -0
  153. package/template/wall-e/sources/record-types.js +252 -0
  154. package/template/wall-e/sources/registry.js +92 -0
  155. package/template/wall-e/sources/transforms.js +100 -0
  156. package/template/wall-e/sources/walle-jsonl.js +108 -0
  157. package/template/wall-e/tools/coding-middleware.js +31 -1
  158. package/template/wall-e/tools/file-tracker.js +25 -1
  159. package/template/wall-e/tools/local-tools.js +75 -47
  160. package/template/wall-e/tools/session-sharing.js +68 -1
  161. package/template/wall-e/tools/shell-analyzer.js +1 -1
  162. package/template/wall-e/tools/shell-policy.js +47 -0
  163. package/template/wall-e/tools/snapshot.js +42 -0
  164. package/template/wall-e/training/harvester.js +62 -5
  165. package/template/wall-e/utils/repair.js +253 -1
  166. package/template/website/index.html +3 -3
  167. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
@@ -9,7 +9,37 @@ const GEMINI_CLIENT_SECRET = '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';
9
9
  let _cachedToken = null;
10
10
  let _tokenExpiresAt = 0;
11
11
 
12
- async function refreshOAuthToken(refreshToken) {
12
+ function providerAbortError() {
13
+ const err = new Error('provider request aborted');
14
+ err.name = 'AbortError';
15
+ return err;
16
+ }
17
+
18
+ function throwIfAborted(signal) {
19
+ if (signal?.aborted) throw providerAbortError();
20
+ }
21
+
22
+ function delayWithAbort(ms, signal) {
23
+ throwIfAborted(signal);
24
+ return new Promise((resolve, reject) => {
25
+ let timer = null;
26
+ const cleanup = () => {
27
+ if (timer) clearTimeout(timer);
28
+ if (signal) signal.removeEventListener('abort', onAbort);
29
+ };
30
+ const onAbort = () => {
31
+ cleanup();
32
+ reject(providerAbortError());
33
+ };
34
+ timer = setTimeout(() => {
35
+ cleanup();
36
+ resolve();
37
+ }, ms);
38
+ if (signal) signal.addEventListener('abort', onAbort, { once: true });
39
+ });
40
+ }
41
+
42
+ async function refreshOAuthToken(refreshToken, signal) {
13
43
  if (_cachedToken && Date.now() < _tokenExpiresAt - 60000) return _cachedToken;
14
44
  const resp = await fetch('https://oauth2.googleapis.com/token', {
15
45
  method: 'POST',
@@ -18,6 +48,7 @@ async function refreshOAuthToken(refreshToken) {
18
48
  client_id: GEMINI_CLIENT_ID, client_secret: GEMINI_CLIENT_SECRET,
19
49
  refresh_token: refreshToken, grant_type: 'refresh_token',
20
50
  }),
51
+ ...(signal ? { signal } : {}),
21
52
  });
22
53
  const data = await resp.json();
23
54
  if (!data.access_token) throw new Error('Token refresh failed: ' + (data.error_description || data.error));
@@ -107,6 +138,7 @@ function messagesToGemini(messages) {
107
138
  */
108
139
  function createGoogleProvider(config = {}) {
109
140
  const isOAuth = config.authMode === 'oauth';
141
+ const GenAI = config._GoogleGenAI || GoogleGenAI;
110
142
  if (!isOAuth && config.apiKey && config.apiKey.startsWith('ya29.')) {
111
143
  console.warn('[google] Stored key is a Gemini CLI OAuth token (ya29.*). ' +
112
144
  'These tokens use Google\'s private Code Assist API and cannot authenticate ' +
@@ -117,7 +149,7 @@ function createGoogleProvider(config = {}) {
117
149
  const httpOptions = accessToken
118
150
  ? { headers: { 'Authorization': `Bearer ${accessToken}` } }
119
151
  : undefined;
120
- return new GoogleGenAI({ apiKey: 'unused', httpOptions });
152
+ return new GenAI({ apiKey: 'unused', httpOptions });
121
153
  }
122
154
 
123
155
  // Build GenAI client — for OAuth we pass Bearer header instead of apiKey
@@ -125,7 +157,7 @@ function createGoogleProvider(config = {}) {
125
157
  if (isOAuth) {
126
158
  genai = makeOAuthClient(config.apiKey);
127
159
  } else {
128
- genai = new GoogleGenAI({ apiKey: config.apiKey });
160
+ genai = new GenAI({ apiKey: config.apiKey });
129
161
  }
130
162
 
131
163
  return {
@@ -170,7 +202,7 @@ function createGoogleProvider(config = {}) {
170
202
  let activeGenai = genai;
171
203
  if (isOAuth && config.refreshToken) {
172
204
  try {
173
- const freshToken = await refreshOAuthToken(config.refreshToken);
205
+ const freshToken = await refreshOAuthToken(config.refreshToken, signal);
174
206
  activeGenai = makeOAuthClient(freshToken);
175
207
  } catch (e) {
176
208
  console.warn('[google] OAuth token refresh failed:', e.message);
@@ -186,16 +218,18 @@ function createGoogleProvider(config = {}) {
186
218
  let raw;
187
219
  const MAX_RETRIES = 3;
188
220
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
221
+ throwIfAborted(signal);
189
222
  try {
190
223
  raw = await activeGenai.models.generateContent(requestConfig);
191
224
  break;
192
225
  } catch (err) {
226
+ throwIfAborted(signal);
193
227
  const status = err.status || err.code || 0;
194
228
  const message = err.message || '';
195
229
  if (attempt < MAX_RETRIES && isRetryable(status, message)) {
196
230
  const delay = parseRetryDelay(null, attempt);
197
231
  console.log(`[google] Retrying (${attempt + 1}/${MAX_RETRIES}) after ${delay}ms: ${message.slice(0, 100)}`);
198
- await new Promise(r => setTimeout(r, delay));
232
+ await delayWithAbort(delay, signal);
199
233
  continue;
200
234
  }
201
235
  throw err;
@@ -148,7 +148,7 @@ function createOllamaProvider(config = {}) {
148
148
  .map((m) => ({
149
149
  id: m.name,
150
150
  name: m.name,
151
- capabilities: ['code'],
151
+ capabilities: ['chat', 'code', 'tools'],
152
152
  size: m.size,
153
153
  }));
154
154
  } catch {
@@ -11,7 +11,7 @@
11
11
  "modelPrefixes": [],
12
12
  "capabilities": {
13
13
  "chat": true,
14
- "tools": false,
14
+ "tools": true,
15
15
  "vision": false,
16
16
  "thinking": false,
17
17
  "streaming": true,
@@ -42,6 +42,16 @@ class ProviderAvailability {
42
42
  return this._providers.get(id) || null;
43
43
  }
44
44
 
45
+ getProviderForType(providerType) {
46
+ return [...this._providers.values()].find(p => p.providerType === providerType || p.providerId === providerType) || null;
47
+ }
48
+
49
+ isProviderUsable(idOrType) {
50
+ const state = this.getProviderState(idOrType) || this.getProviderForType(idOrType);
51
+ if (!state) return true;
52
+ return state.status !== 'unhealthy';
53
+ }
54
+
45
55
  /**
46
56
  * Return all registered providers.
47
57
  */
@@ -0,0 +1,269 @@
1
+ 'use strict';
2
+
3
+ const MAX_RAW_MESSAGE_CHARS = 2000;
4
+
5
+ const PROVIDER_LABELS = {
6
+ anthropic: 'Anthropic',
7
+ openai: 'OpenAI',
8
+ google: 'Google',
9
+ deepseek: 'DeepSeek',
10
+ ollama: 'Ollama',
11
+ mlx: 'MLX',
12
+ };
13
+
14
+ function providerLabel(provider) {
15
+ const key = String(provider || '').toLowerCase();
16
+ return PROVIDER_LABELS[key] || provider || 'AI provider';
17
+ }
18
+
19
+ function redactSecrets(value) {
20
+ return String(value || '')
21
+ .replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, 'sk-...redacted')
22
+ .replace(/\bya29\.[A-Za-z0-9._-]{12,}\b/g, 'ya29...redacted')
23
+ .replace(/\bAIza[A-Za-z0-9_-]{20,}\b/g, 'AIza...redacted')
24
+ .replace(/(api[_-]?key["'\s:=]+)[^"',\s]+/gi, '$1...redacted')
25
+ .slice(0, MAX_RAW_MESSAGE_CHARS);
26
+ }
27
+
28
+ function tryJson(value) {
29
+ if (!value || typeof value !== 'string') return null;
30
+ try { return JSON.parse(value); } catch { return null; }
31
+ }
32
+
33
+ function collectStrings(value, out = []) {
34
+ if (value == null) return out;
35
+ if (typeof value === 'string') {
36
+ out.push(value);
37
+ const parsed = tryJson(value);
38
+ if (parsed) collectStrings(parsed, out);
39
+ return out;
40
+ }
41
+ if (typeof value === 'number' || typeof value === 'boolean') {
42
+ out.push(String(value));
43
+ return out;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ value.forEach((item) => collectStrings(item, out));
47
+ return out;
48
+ }
49
+ if (typeof value === 'object') {
50
+ for (const key of ['message', 'error', 'type', 'code', 'status', 'statusCode', 'details', 'body']) {
51
+ if (Object.prototype.hasOwnProperty.call(value, key)) collectStrings(value[key], out);
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function extractStatus(err) {
58
+ const candidates = [
59
+ err?.status,
60
+ err?.statusCode,
61
+ err?.response?.status,
62
+ err?.response?.statusCode,
63
+ err?.error?.status,
64
+ err?.error?.statusCode,
65
+ ];
66
+ for (const candidate of candidates) {
67
+ const n = Number(candidate);
68
+ if (Number.isFinite(n) && n >= 100 && n < 600) return n;
69
+ }
70
+ const message = String(err?.message || err || '');
71
+ const m = message.match(/\b(?:api error|http|status)\s*[:#]?\s*(\d{3})\b/i);
72
+ return m ? Number(m[1]) : null;
73
+ }
74
+
75
+ function extractRetryAfter(err) {
76
+ const headers = err?.headers || err?.response?.headers;
77
+ if (!headers) return null;
78
+ const get = typeof headers.get === 'function'
79
+ ? (name) => headers.get(name)
80
+ : (name) => headers[name] || headers[name.toLowerCase()];
81
+ const retryAfterMs = get('retry-after-ms');
82
+ if (retryAfterMs && !Number.isNaN(Number(retryAfterMs))) return `${Math.round(Number(retryAfterMs) / 1000)}s`;
83
+ const retryAfter = get('retry-after');
84
+ if (!retryAfter) return null;
85
+ const seconds = Number(retryAfter);
86
+ if (!Number.isNaN(seconds)) return `${Math.round(seconds)}s`;
87
+ const date = Date.parse(retryAfter);
88
+ if (!Number.isNaN(date)) {
89
+ const delta = Math.max(0, date - Date.now());
90
+ return `${Math.ceil(delta / 1000)}s`;
91
+ }
92
+ return String(retryAfter).slice(0, 80);
93
+ }
94
+
95
+ function buildRawMessage(err) {
96
+ const pieces = [];
97
+ collectStrings(err?.responseBody, pieces);
98
+ collectStrings(err?.body, pieces);
99
+ collectStrings(err?.error, pieces);
100
+ collectStrings(err?.cause, pieces);
101
+ collectStrings(err?.message || err, pieces);
102
+ const unique = [...new Set(pieces.map((part) => String(part || '').trim()).filter(Boolean))];
103
+ return redactSecrets(unique.join('\n'));
104
+ }
105
+
106
+ function classifyType(status, lower) {
107
+ if (/insufficient[_ -]?quota|quota exceeded|quota_exceeded|resource[_ -]?exhausted|billing|credit|prepayment credits are depleted/i.test(lower)) return 'quota_exceeded';
108
+ if (status === 429 || /rate[_ -]?limit|too many requests|too_many_requests|rate_limit/i.test(lower)) return 'rate_limited';
109
+ if (status === 401 || status === 403 || /invalid[_ -]?(api[_ -]?)?key|unauthori[sz]ed|forbidden|authentication|auth token|token refresh failed|permission denied/i.test(lower)) return 'auth_error';
110
+ if (status === 404 || /model .*not found|not_found_error|model_not_found|does not exist|unknown model/i.test(lower)) return 'model_unavailable';
111
+ if (/context length exceeded|context_length_exceeded|maximum context|token limit/i.test(lower)) return 'context_window';
112
+ if (status === 408 || /timeout|timed out|etimedout|aborterror|aborted/i.test(lower)) return 'timeout';
113
+ if (status === 529 || status === 503 || /overloaded|service unavailable|temporarily unavailable|unavailable/i.test(lower)) return 'provider_unavailable';
114
+ if (/fetch failed|network|eai_again|enotfound|econnreset|socket hang up|connection refused|econnrefused/i.test(lower)) return 'network';
115
+ if (status && status >= 500) return 'provider_unavailable';
116
+ if (status && status >= 400) return 'provider_rejected';
117
+ return 'provider_error';
118
+ }
119
+
120
+ function messageForType(type) {
121
+ switch (type) {
122
+ case 'rate_limited':
123
+ return {
124
+ title: 'AI provider rate limited',
125
+ body: 'The provider rejected the request because it is rate limited. Wait and retry, or switch provider/model in Setup.',
126
+ };
127
+ case 'quota_exceeded':
128
+ return {
129
+ title: 'AI provider quota exhausted',
130
+ body: 'The provider rejected the request because the account quota or credits are exhausted. Update billing or switch provider/model in Setup.',
131
+ };
132
+ case 'auth_error':
133
+ return {
134
+ title: 'AI provider authentication failed',
135
+ body: 'The provider rejected the configured credential. Update the API key/token in Setup, then retry.',
136
+ };
137
+ case 'model_unavailable':
138
+ return {
139
+ title: 'AI model unavailable',
140
+ body: 'The selected model is not available from the configured provider. Pick another model in Setup, then retry.',
141
+ };
142
+ case 'context_window':
143
+ return {
144
+ title: 'AI request exceeded context',
145
+ body: 'The provider rejected the request because the conversation or attachments exceed the model context window. Shorten the prompt or switch to a larger-context model.',
146
+ };
147
+ case 'timeout':
148
+ return {
149
+ title: 'AI provider timed out',
150
+ body: 'The provider did not answer before the timeout. Retry, or switch provider/model if this keeps happening.',
151
+ };
152
+ case 'network':
153
+ return {
154
+ title: 'AI provider network error',
155
+ body: 'Wall-E could not reach the provider endpoint. Check connectivity, base URL, or local provider status, then retry.',
156
+ };
157
+ case 'provider_unavailable':
158
+ return {
159
+ title: 'AI provider unavailable',
160
+ body: 'The provider is currently unavailable or overloaded. Retry later, or switch provider/model in Setup.',
161
+ };
162
+ case 'provider_unavailable_local':
163
+ return {
164
+ title: 'No AI provider available',
165
+ body: 'Providers are configured, but every provider is currently marked unhealthy. Check Setup or wait for health checks to recover.',
166
+ };
167
+ default:
168
+ return {
169
+ title: 'AI provider failed',
170
+ body: 'Wall-E could not get a response from the configured provider. Review the raw provider error, then retry or switch provider/model in Setup.',
171
+ };
172
+ }
173
+ }
174
+
175
+ function classifyProviderError(err, context = {}) {
176
+ if (err?.providerError && typeof err.providerError === 'object') return err.providerError;
177
+ const status = extractStatus(err);
178
+ const rawMessage = buildRawMessage(err);
179
+ const lower = rawMessage.toLowerCase();
180
+ const type = context.type || classifyType(status, lower);
181
+ const copy = messageForType(type);
182
+ const provider = context.provider || err?.provider || err?.providerType || null;
183
+ const model = context.model || err?.model || null;
184
+ const providerName = providerLabel(provider);
185
+ const modelSuffix = model ? ` (${model})` : '';
186
+ const retryAfter = extractRetryAfter(err);
187
+ const retrySuffix = retryAfter ? ` Retry after ${retryAfter}.` : '';
188
+ return {
189
+ code: 'AI_PROVIDER_ERROR',
190
+ type,
191
+ severity: type === 'context_window' ? 'warning' : 'error',
192
+ title: copy.title,
193
+ message: copy.body,
194
+ userMessage: `${providerName}${modelSuffix}: ${copy.body}${retrySuffix}`,
195
+ rawMessage,
196
+ status,
197
+ provider,
198
+ model,
199
+ retryAfter,
200
+ actionLabel: 'Open Setup',
201
+ actionUrl: '/setup.html',
202
+ createdAt: new Date().toISOString(),
203
+ };
204
+ }
205
+
206
+ function decorateProviderError(err, context = {}) {
207
+ const base = err instanceof Error ? err : new Error(String(err || 'AI provider failed'));
208
+ base.providerError = classifyProviderError(base, context);
209
+ base.code = 'AI_PROVIDER_ERROR';
210
+ if (base.providerError.status && !base.status) base.status = base.providerError.status;
211
+ return base;
212
+ }
213
+
214
+ function unavailableProviderError(providers = [], context = {}) {
215
+ const details = providers
216
+ .map((provider) => {
217
+ const label = provider.providerType || provider.providerId || 'provider';
218
+ return `${label}: ${provider.lastError || provider.status || 'unhealthy'}`;
219
+ })
220
+ .join('\n');
221
+ const err = new Error(details || 'No configured AI provider is currently available');
222
+ err.status = 503;
223
+ return decorateProviderError(err, {
224
+ ...context,
225
+ type: 'provider_unavailable_local',
226
+ provider: context.provider || providers[0]?.providerType || null,
227
+ });
228
+ }
229
+
230
+ function toApiPayload(err, context = {}) {
231
+ const providerError = classifyProviderError(err, context);
232
+ return {
233
+ error: providerError.title,
234
+ message: providerError.userMessage,
235
+ code: 'AI_PROVIDER_ERROR',
236
+ providerError,
237
+ };
238
+ }
239
+
240
+ function recordProviderFailureAlert(providerError, brain) {
241
+ if (!providerError) return { alerted: false };
242
+ try {
243
+ const planner = require('../skills/skill-planner');
244
+ planner.addServiceAlert({
245
+ service: 'ai_provider',
246
+ type: `ai_provider:${providerError.provider || 'default'}:${providerError.type}`,
247
+ title: providerError.title,
248
+ severity: providerError.severity || 'error',
249
+ provider: providerError.provider || null,
250
+ model: providerError.model || null,
251
+ message: `${providerError.title}: ${providerError.userMessage}`,
252
+ action_url: providerError.actionUrl || '/setup.html',
253
+ provider_error: providerError,
254
+ });
255
+ return { alerted: true };
256
+ } catch (err) {
257
+ return { alerted: false, error: err.message };
258
+ }
259
+ }
260
+
261
+ module.exports = {
262
+ classifyProviderError,
263
+ decorateProviderError,
264
+ providerLabel,
265
+ recordProviderFailureAlert,
266
+ redactSecrets,
267
+ toApiPayload,
268
+ unavailableProviderError,
269
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  let geminiToolCallSeq = 0;
4
+ let openAIToolCallSeq = 0;
4
5
 
5
6
  // ============================================================
6
7
  // Tool definition converters
@@ -63,10 +64,19 @@ function toAnthropic(tools) {
63
64
  */
64
65
  function messagesToOpenAI(messages) {
65
66
  const out = [];
67
+ const pendingToolCallIds = [];
68
+
66
69
  for (const msg of messages) {
67
70
  // Simple string content — pass through
68
71
  if (typeof msg.content === 'string') {
69
- out.push({ role: msg.role, content: msg.content });
72
+ const converted = { role: msg.role, content: msg.content };
73
+ if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
74
+ converted.tool_calls = normalizeOpenAIToolCalls(msg.tool_calls, pendingToolCallIds);
75
+ }
76
+ if (msg.role === 'tool') {
77
+ converted.tool_call_id = consumeToolCallId(msg.tool_call_id, pendingToolCallIds);
78
+ }
79
+ out.push(converted);
70
80
  continue;
71
81
  }
72
82
 
@@ -82,7 +92,7 @@ function messagesToOpenAI(messages) {
82
92
  for (const tr of toolResults) {
83
93
  out.push({
84
94
  role: 'tool',
85
- tool_call_id: tr.tool_use_id,
95
+ tool_call_id: consumeToolCallId(tr.tool_use_id || tr.id || tr.toolCallId, pendingToolCallIds),
86
96
  content: typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content),
87
97
  });
88
98
  }
@@ -138,14 +148,18 @@ function messagesToOpenAI(messages) {
138
148
  }
139
149
 
140
150
  if (toolUses.length > 0) {
141
- converted.tool_calls = toolUses.map((tu) => ({
142
- id: tu.id,
143
- type: 'function',
144
- function: {
145
- name: tu.name,
146
- arguments: JSON.stringify(tu.input),
147
- },
148
- }));
151
+ converted.tool_calls = toolUses.map((tu, index) => {
152
+ const id = tu.id || tu.tool_use_id || tu.toolCallId || synthesizeOpenAIToolCallId(index);
153
+ pendingToolCallIds.push(id);
154
+ return {
155
+ id,
156
+ type: 'function',
157
+ function: {
158
+ name: tu.name,
159
+ arguments: JSON.stringify(tu.input ?? {}),
160
+ },
161
+ };
162
+ });
149
163
  }
150
164
 
151
165
  if (reasoningParts.length > 0) {
@@ -191,8 +205,8 @@ function responseFromOpenAI(resp) {
191
205
 
192
206
  return {
193
207
  content: msg.content || null,
194
- toolCalls: (msg.tool_calls || []).map((tc) => ({
195
- id: tc.id,
208
+ toolCalls: (msg.tool_calls || []).map((tc, index) => ({
209
+ id: tc.id || synthesizeOpenAIToolCallId(index),
196
210
  name: tc.function.name,
197
211
  input: safeJsonParse(tc.function.arguments),
198
212
  })),
@@ -242,6 +256,28 @@ function safeJsonParse(str) {
242
256
  }
243
257
  }
244
258
 
259
+ function synthesizeOpenAIToolCallId(index = 0) {
260
+ openAIToolCallSeq += 1;
261
+ return `call_walle_${openAIToolCallSeq}_${index}`;
262
+ }
263
+
264
+ function normalizeOpenAIToolCalls(toolCalls = [], pendingToolCallIds = []) {
265
+ return toolCalls.map((tc, index) => {
266
+ const id = tc.id || synthesizeOpenAIToolCallId(index);
267
+ pendingToolCallIds.push(id);
268
+ return { ...tc, id };
269
+ });
270
+ }
271
+
272
+ function consumeToolCallId(explicitId, pendingToolCallIds = []) {
273
+ if (explicitId) {
274
+ const pendingIndex = pendingToolCallIds.indexOf(explicitId);
275
+ if (pendingIndex >= 0) pendingToolCallIds.splice(pendingIndex, 1);
276
+ return explicitId;
277
+ }
278
+ return pendingToolCallIds.shift() || synthesizeOpenAIToolCallId(0);
279
+ }
280
+
245
281
  module.exports = {
246
282
  toOpenAI,
247
283
  toGemini,
@@ -84,7 +84,8 @@ async function runBootCheck(opts = {}) {
84
84
  } catch (e) {
85
85
  return { status: 'failed', reason: 'no-client', error: e.message };
86
86
  }
87
- const model = opts.model || process.env.WALLE_MODEL || 'claude-haiku-4-5-20251001';
87
+ const { getDefaultModel, resolveCompatibleModel } = require('../llm/client');
88
+ const model = resolveCompatibleModel(opts.model || process.env.WALLE_MODEL || getDefaultModel(), provider.type);
88
89
 
89
90
  const prompt = _buildBootPrompt(file.content);
90
91
  const controller = new AbortController();
@@ -3,7 +3,7 @@
3
3
  const brain = require('../brain');
4
4
  const { buildStateSnapshot, isStateEmpty, formatSnapshot } = require('../context/state-snapshot');
5
5
  const { canActAutonomously, getDomainConfidence } = require('../decision/confidence');
6
- const { getDefaultClient } = require('../llm/client');
6
+ const { getDefaultClient, getDefaultModel, resolveCompatibleModel } = require('../llm/client');
7
7
  const { v4: uuidv4 } = require('uuid');
8
8
  const { buildDisciplineHeader } = require('./loop-prompt-discipline');
9
9
  const { augmentSystemPrompt } = require('./loop-directives');
@@ -287,7 +287,7 @@ async function runInitiativeLoop(opts = {}) {
287
287
 
288
288
  // Call Claude with a focused prompt
289
289
  const provider = opts.client || getDefaultClient();
290
- const model = opts.model || process.env.WALLE_MODEL || 'claude-haiku-4-5-20251001';
290
+ const model = resolveCompatibleModel(opts.model || process.env.WALLE_MODEL || getDefaultModel(), provider.type);
291
291
 
292
292
  const controller = new AbortController();
293
293
  const timeout = setTimeout(() => controller.abort(), 30000);
@@ -293,53 +293,14 @@ async function executeSkill(taskId, task) {
293
293
  appendLog(taskId, `Skill: ${skill.name} v${skill.version} (${skill.execution})`);
294
294
 
295
295
  if (skill.execution === 'script') {
296
- // Resolve entry point relative to the skill directory
297
- const entryPath = path.resolve(skill.dir, skill.entry || 'run.js');
298
- if (!require('fs').existsSync(entryPath)) {
299
- throw new Error(`Skill entry point not found: ${entryPath}`);
300
- }
301
-
302
- // Build the shell command: node <entry> <args...>
303
- const args = [...(skill.args || [])];
304
-
305
- // Merge task-level skill_config with skill defaults
306
- const skillConfig = {};
307
- if (skill.config) {
308
- for (const [k, v] of Object.entries(skill.config)) {
309
- if (v && typeof v === 'object' && v.default !== undefined) {
310
- skillConfig[k] = v.default;
311
- }
312
- }
313
- }
314
- // Task overrides
315
- let taskConfig = {};
316
- if (task.skill_config) {
317
- try {
318
- taskConfig = typeof task.skill_config === 'string'
319
- ? JSON.parse(task.skill_config)
320
- : task.skill_config;
321
- } catch {}
322
- }
323
- Object.assign(skillConfig, taskConfig);
324
-
325
- // For slack-backfill: pass mode/month as CLI args if configured
326
- if (skillConfig.mode && !args.includes(skillConfig.mode)) {
327
- // Only add mode as arg if the script expects it as argv[2]
328
- // (slack-backfill uses process.argv[2] as mode)
329
- if (skill.name === 'slack-backfill') {
330
- args.push(skillConfig.mode);
331
- if (skillConfig.month) args.push(skillConfig.month);
332
- }
333
- }
334
-
335
- const scriptCmd = `node ${entryPath} ${args.join(' ')}`.trim();
336
- appendLog(taskId, `Running: ${scriptCmd}`);
337
-
338
- // Pass config as env var + checkpoint support
339
- return executeScript(taskId, scriptCmd, task.checkpoint, {
340
- WALL_E_SKILL_CONFIG: JSON.stringify(skillConfig),
341
- WALL_E_SKILL_DIR: skill.dir,
342
- WALL_E_SRC_DIR: WALL_E_DIR,
296
+ const { runScriptSkill } = require('../skills/script-skill-runner');
297
+ return runScriptSkill(skill, task, {
298
+ log: line => appendLog(taskId, line),
299
+ onCheckpoint: checkpoint => brain.updateTask(taskId, { checkpoint }),
300
+ onProcess: child => taskProcesses.set(taskId, child),
301
+ onClose: child => {
302
+ if (taskProcesses.get(taskId) === child) taskProcesses.delete(taskId);
303
+ },
343
304
  });
344
305
  }
345
306
 
@@ -151,6 +151,25 @@ function composePersonaBlock() {
151
151
  return sections.length ? sections.join('\n\n') : '';
152
152
  }
153
153
 
154
+ function getWorkspacePersonaSections({ recipient = 'agent' } = {}) {
155
+ const sections = [];
156
+ for (const name of ['IDENTITY', 'SOUL', 'USER', 'TOOLS', 'AGENTS']) {
157
+ const content = readWorkspacePrompt(name);
158
+ if (!content || isContentEffectivelyEmpty(content)) continue;
159
+ sections.push({
160
+ id: `workspace-${name.toLowerCase()}`,
161
+ source: `workspace:${name}.md`,
162
+ content: content.trim(),
163
+ recipient,
164
+ cacheable: true,
165
+ visibility: 'llm',
166
+ mutableByHooks: true,
167
+ redactionPolicy: name === 'USER' ? 'standard' : 'secrets',
168
+ });
169
+ }
170
+ return sections;
171
+ }
172
+
154
173
  /** Test helper. */
155
174
  function clearWorkspacePromptsCache() {
156
175
  _cache.clear();
@@ -204,6 +223,7 @@ module.exports = {
204
223
  readWorkspacePrompt,
205
224
  isContentEffectivelyEmpty,
206
225
  composePersonaBlock,
226
+ getWorkspacePersonaSections,
207
227
  initWorkspaceFromTemplates,
208
228
  clearWorkspacePromptsCache,
209
229
  _resolveWorkspacePath,