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
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { makePrincipalKey, normalizePrincipal } = require('./policy');
4
+
5
+ class ChannelSessionBindings {
6
+ constructor({ initial = [] } = {}) {
7
+ this.bindings = new Map();
8
+ for (const binding of initial || []) {
9
+ this.bind(binding.principal || binding, binding.sessionId, binding.metadata || {});
10
+ }
11
+ }
12
+
13
+ bind(principal, sessionId, metadata = {}) {
14
+ if (!sessionId) throw new Error('sessionId is required');
15
+ const normalized = normalizePrincipal(principal);
16
+ const key = makePrincipalKey(normalized);
17
+ const record = {
18
+ key,
19
+ principal: normalized,
20
+ sessionId,
21
+ metadata: { ...metadata },
22
+ updatedAt: new Date().toISOString(),
23
+ };
24
+ this.bindings.set(key, record);
25
+ return { ...record, principal: { ...record.principal }, metadata: { ...record.metadata } };
26
+ }
27
+
28
+ resolve(principal) {
29
+ const key = makePrincipalKey(principal);
30
+ const record = this.bindings.get(key);
31
+ return record ? { ...record, principal: { ...record.principal }, metadata: { ...record.metadata } } : null;
32
+ }
33
+
34
+ unbind(principal) {
35
+ return this.bindings.delete(makePrincipalKey(principal));
36
+ }
37
+
38
+ list({ channel = null } = {}) {
39
+ return [...this.bindings.values()]
40
+ .filter(record => !channel || record.principal.channel === channel)
41
+ .map(record => ({ ...record, principal: { ...record.principal }, metadata: { ...record.metadata } }));
42
+ }
43
+
44
+ toJSON() {
45
+ return this.list();
46
+ }
47
+ }
48
+
49
+ module.exports = {
50
+ ChannelSessionBindings,
51
+ };
@@ -12,6 +12,10 @@ function isCodeReviewRequest(message) {
12
12
  const text = String(message || '').toLowerCase();
13
13
  return /\bcode\s+review\b/.test(text)
14
14
  || /\breview\s+(?:the\s+)?(?:code|diff|patch|changes?|changed files|pull request|pr)\b/.test(text)
15
+ || /\breview\s+(?:my\s+|the\s+)?(?:local\s+|uncommitted\s+|unstaged\s+|staged\s+|working\s+tree\s+|workspace\s+)?changes?\b/.test(text)
16
+ || /\b(?:review|inspect|check|audit)\s+(?:my\s+|the\s+)?(?:local\s+|working\s+tree\s+|workspace\s+|uncommitted\s+|unstaged\s+|staged\s+)?(?:diff|patch|changes?)\b/.test(text)
17
+ || /\b(?:what|show|tell\s+me)\s+(?:changed|is\s+changed|local\s+changes|unstaged\s+changes|uncommitted\s+changes)\b/.test(text)
18
+ || /\b(?:check|show|run)\s+(?:git\s+)?status\b/.test(text)
15
19
  || /\b(?:do|run|perform|take)\s+(?:a\s+)?(?:close\s+|thorough\s+)?code\s+review\b/.test(text)
16
20
  || /\b(?:review|audit)\s+my\s+(?:changes?|diff|patch|code)\b/.test(text);
17
21
  }
@@ -25,6 +29,27 @@ function looksLikePrematureCodeReviewReply(text) {
25
29
  return promisesInspection && !hasFindingsShape;
26
30
  }
27
31
 
32
+ function snapshotIndicatesDirty(codeReviewContextBlock) {
33
+ const block = String(codeReviewContextBlock || '');
34
+ if (!/## Code Review Workspace Snapshot/.test(block)) return false;
35
+ const statusMatch = block.match(/### Git status\s*\n([\s\S]*?)(?:\n### |\n\n### |$)/);
36
+ if (!statusMatch) return false;
37
+ const status = statusMatch[1].trim();
38
+ return !!status && status !== '(clean)';
39
+ }
40
+
41
+ function looksLikeContradictoryCleanReviewReply(text, codeReviewContextBlock) {
42
+ if (!snapshotIndicatesDirty(codeReviewContextBlock)) return false;
43
+ const lower = String(text || '').toLowerCase();
44
+ if (!lower.trim()) return false;
45
+ return /\bworking tree (?:is )?clean\b/.test(lower)
46
+ || /\bworktree (?:is )?clean\b/.test(lower)
47
+ || /\bno (?:local |uncommitted |unstaged |staged )?changes\b/.test(lower)
48
+ || /\bnothing (?:is )?(?:staged|unstaged|changed|to review)\b/.test(lower)
49
+ || /\bno staged,?\s+unstaged,?\s+(?:or\s+)?untracked\b/.test(lower)
50
+ || /\bindex (?:is )?clean\b/.test(lower);
51
+ }
52
+
28
53
  function runGit(cwd, args, opts = {}) {
29
54
  try {
30
55
  return execFileSync('git', args, {
@@ -166,6 +191,7 @@ function buildCodeReviewContextBlock({ message, cwd, maxContextBytes = DEFAULT_M
166
191
 
167
192
  const block = [
168
193
  '## Code Review Workspace Snapshot',
194
+ `Requested cwd: ${path.resolve(cwd)}`,
169
195
  `Project: ${root}`,
170
196
  `Branch: ${branch}`,
171
197
  `HEAD: ${head}`,
@@ -177,6 +203,7 @@ function buildCodeReviewContextBlock({ message, cwd, maxContextBytes = DEFAULT_M
177
203
  '',
178
204
  '### Code Review Instructions',
179
205
  'You already have the git status and diff above. Do not say you will inspect, read, or fetch diffs.',
206
+ `Start with: Checked ${root} (requested cwd ${path.resolve(cwd)}, branch ${branch}, status ${status === '(clean)' ? 'clean' : 'dirty'}).`,
180
207
  'Produce the review now. Lead with findings ordered by severity. Cite file paths and line references when the diff gives enough context.',
181
208
  'If there are no issues, say that clearly and include remaining test gaps or residual risk.',
182
209
  ].join('\n');
@@ -187,6 +214,8 @@ function buildCodeReviewContextBlock({ message, cwd, maxContextBytes = DEFAULT_M
187
214
  module.exports = {
188
215
  isCodeReviewRequest,
189
216
  looksLikePrematureCodeReviewReply,
217
+ snapshotIndicatesDirty,
218
+ looksLikeContradictoryCleanReviewReply,
190
219
  buildCodeReviewContextBlock,
191
220
  gitRootFor,
192
221
  };
@@ -4,7 +4,9 @@ const {
4
4
  createClient,
5
5
  detectProviderForModel,
6
6
  getDefaultClient,
7
+ getDefaultModelForProvider,
7
8
  getDefaultProviderType,
9
+ resolveCompatibleModel,
8
10
  } = require('./llm/client');
9
11
  const { executeLocalTool, LOCAL_TOOL_DEFINITIONS, resolveProjectPath } = require('./tools/local-tools');
10
12
  const slackMcp = require('./tools/slack-mcp');
@@ -17,8 +19,15 @@ const { runShadow } = require('./eval/shadow');
17
19
  const {
18
20
  buildCodeReviewContextBlock,
19
21
  isCodeReviewRequest,
22
+ looksLikeContradictoryCleanReviewReply,
20
23
  looksLikePrematureCodeReviewReply,
21
24
  } = require('./chat/code-review-context');
25
+ const { createSessionRecorder } = require('./session-files');
26
+ const {
27
+ decorateProviderError,
28
+ recordProviderFailureAlert,
29
+ unavailableProviderError,
30
+ } = require('./llm/provider-error');
22
31
  let _telemetry;
23
32
  try { _telemetry = require('./telemetry'); } catch { _telemetry = { trackError() {}, track() {} }; }
24
33
  let _embeddings;
@@ -96,8 +105,12 @@ function _getCommandRegistry() {
96
105
  _commandRegistry.registerBuiltins();
97
106
  try {
98
107
  const { findSkill } = require('./skills/skill-loader');
99
- const { executeSkill } = require('./loops/tasks');
100
- _commandRegistry.registerSkills(findSkill, executeSkill);
108
+ const { runScriptSkillByName } = require('./skills/script-skill-runner');
109
+ _commandRegistry.registerSkills(findSkill, async (taskId, task) => {
110
+ return runScriptSkillByName(task.skill, task, {
111
+ log: line => console.error(`[skill:${taskId}] ${line}`),
112
+ });
113
+ });
101
114
  } catch { /* skill loader not available */ }
102
115
  return _commandRegistry;
103
116
  }
@@ -294,6 +307,20 @@ function resolveModelSelection(model, explicitProvider) {
294
307
  LIMIT 1
295
308
  `).get(raw, raw, explicitProvider || null, explicitProvider || null, raw);
296
309
  if (row) {
310
+ if (explicitProvider) {
311
+ const compatibleModel = resolveCompatibleModel(row.model_id, explicitProvider);
312
+ if (compatibleModel !== row.model_id) {
313
+ return {
314
+ input: raw,
315
+ model: compatibleModel,
316
+ provider: explicitProvider,
317
+ providerConfig: null,
318
+ registryId: null,
319
+ coercedFrom: row.model_id,
320
+ detectedProvider: row.provider_type || detectProviderForModel(row.model_id) || null,
321
+ };
322
+ }
323
+ }
297
324
  return {
298
325
  input: raw,
299
326
  model: row.model_id,
@@ -306,10 +333,24 @@ function resolveModelSelection(model, explicitProvider) {
306
333
  }
307
334
  } catch {}
308
335
 
336
+ const detectedProvider = detectProviderForModel(raw);
337
+ if (explicitProvider) {
338
+ const compatibleModel = resolveCompatibleModel(raw, explicitProvider);
339
+ return {
340
+ input: raw,
341
+ model: compatibleModel,
342
+ provider: explicitProvider,
343
+ providerConfig: null,
344
+ registryId: null,
345
+ coercedFrom: compatibleModel === raw ? null : raw,
346
+ detectedProvider,
347
+ };
348
+ }
349
+
309
350
  return {
310
351
  input: raw,
311
352
  model: raw,
312
- provider: explicitProvider || detectProviderForModel(raw) || null,
353
+ provider: detectedProvider || null,
313
354
  providerConfig: null,
314
355
  registryId: null,
315
356
  };
@@ -363,28 +404,12 @@ async function chat(message, opts = {}) {
363
404
  // backward compatibility (the LLM client will use its own env-based config).
364
405
  const { default: providerAvailability } = require('./llm/provider-availability');
365
406
  if (providerAvailability.getConfiguredProviders().length > 0 && !providerAvailability.isAnyProviderAvailable()) {
366
- let guidance = 'I need an AI provider to think, but none are configured yet.\n\n';
367
- try {
368
- const { detectAll } = require('./llm/provider-detector');
369
- const detection = await detectAll();
370
- if (detection.setupAction === 'auto-register-detected') {
371
- guidance += `Good news — I found existing API keys on your system (${detection.detected.map(d => d.type).join(', ')}). Open the Wall-E settings to activate them.`;
372
- } else if (detection.setupAction === 'ollama-running') {
373
- guidance += 'I see Ollama is already running on your system. Use the setup button to connect it.';
374
- } else if (detection.ollamaRecommendation) {
375
- guidance += `I can set up a free local AI (${detection.ollamaRecommendation.label}, ${detection.ollamaRecommendation.size} download). Use the setup button in the toolbar.`;
376
- } else {
377
- guidance += 'Please add an API key in the setup page, or install Ollama for free local AI.';
378
- }
379
- } catch {
380
- guidance += 'Please configure an AI provider in the setup page.';
381
- }
382
- return {
383
- reply: guidance,
384
- model: 'system', provider: 'none', latencyMs: 0,
385
- tokens: { input: 0, output: 0 }, cost: 0, toolCalls: [],
386
- providerStatus: { configured: false },
387
- };
407
+ const unavailableErr = unavailableProviderError(providerAvailability.getConfiguredProviders(), {
408
+ provider: opts.provider || getDefaultProviderType(),
409
+ model: opts.model || null,
410
+ });
411
+ recordProviderFailureAlert(unavailableErr.providerError, brain);
412
+ throw unavailableErr;
388
413
  }
389
414
 
390
415
  // Detect user signals from follow-up messages (retroactive quality scoring)
@@ -429,7 +454,7 @@ async function chat(message, opts = {}) {
429
454
  const modelSelection = explicitModelSelection
430
455
  ? { model: explicitModelSelection.model, modelTier: 'balanced', scorecardUsed: false }
431
456
  : selectModelForMessage(message, opts.taskType);
432
- const selectedModel = modelSelection.model;
457
+ let selectedModel = modelSelection.model;
433
458
  const selectedRoute = explicitModelSelection || resolveModelSelection(selectedModel, opts.provider);
434
459
  const effectiveCwd = opts.cwd || opts.context?.cwd || opts.context?.projectPath || null;
435
460
 
@@ -471,6 +496,25 @@ async function chat(message, opts = {}) {
471
496
  brain,
472
497
  });
473
498
  const existingSession = brain.getSession(sessionId);
499
+ const sessionRecorder = createSessionRecorder({
500
+ sessionId,
501
+ channel,
502
+ cwd: effectiveCwd || opts.cwd || process.cwd(),
503
+ metadata: {
504
+ taskType: opts.taskType || 'chat',
505
+ source: opts.source || channel || 'chat',
506
+ model: selectedModel,
507
+ provider: selectedRoute.provider || opts.provider || null,
508
+ },
509
+ });
510
+ const recordSessionMessage = (role, content, extra) => {
511
+ try {
512
+ return sessionRecorder.appendMessage(role, content, extra);
513
+ } catch (err) {
514
+ console.error('[chat] Failed to append session message:', err.message);
515
+ return null;
516
+ }
517
+ };
474
518
 
475
519
  // Review channel: build a focused code review system prompt
476
520
  let reviewContextBlock = '';
@@ -554,6 +598,8 @@ async function chat(message, opts = {}) {
554
598
  }
555
599
  }
556
600
 
601
+ selectedModel = resolveCompatibleModel(selectedModel || getDefaultModelForProvider(targetProviderType), provider.type || targetProviderType);
602
+
557
603
  const codeReviewFastPath = codeReviewRequested
558
604
  && isUsableCodeReviewSnapshot(codeReviewContextBlock)
559
605
  && opts.codeReviewMode !== 'deep';
@@ -571,19 +617,31 @@ async function chat(message, opts = {}) {
571
617
  };
572
618
  }
573
619
 
574
- // Per-turn abort controller each API call gets its own 2-min timeout.
575
- // This prevents long multi-turn tasks (e.g., Morning Briefing) from aborting
576
- // as long as each individual turn completes within the timeout.
620
+ // Per-turn abort controller. Each API call is capped by the remaining
621
+ // per-message budget, with a 2-minute ceiling for long-running intents.
577
622
  let controller = new AbortController();
578
623
  let timeout = null;
579
- function resetTurnTimeout() {
624
+ let messageDeadline = null;
625
+ const MAX_TURN_TIMEOUT_MS = 120000;
626
+ const MIN_TURN_TIMEOUT_MS = 50;
627
+ function resolveTurnTimeoutMs(requestedMs) {
628
+ if (Number.isFinite(requestedMs)) {
629
+ return Math.max(MIN_TURN_TIMEOUT_MS, Math.min(MAX_TURN_TIMEOUT_MS, requestedMs));
630
+ }
631
+ if (!messageDeadline) return MAX_TURN_TIMEOUT_MS;
632
+ return Math.max(
633
+ MIN_TURN_TIMEOUT_MS,
634
+ Math.min(MAX_TURN_TIMEOUT_MS, messageDeadline - Date.now()),
635
+ );
636
+ }
637
+ function resetTurnTimeout(requestedMs) {
580
638
  if (timeout) clearTimeout(timeout);
581
639
  controller = new AbortController();
582
640
  if (opts.abortSignal?.aborted) controller.abort();
583
641
  else if (opts.abortSignal) {
584
642
  opts.abortSignal.addEventListener('abort', () => controller.abort(), { once: true });
585
643
  }
586
- timeout = setTimeout(() => controller.abort(), 120000); // 2 min per turn
644
+ timeout = setTimeout(() => controller.abort(), resolveTurnTimeoutMs(requestedMs));
587
645
  }
588
646
 
589
647
  // Define internal tools WALL-E can use during chat
@@ -823,7 +881,13 @@ async function chat(message, opts = {}) {
823
881
  return tools;
824
882
  }
825
883
 
826
- const onProgress = opts.onProgress || (() => {});
884
+ const progressSink = opts.onProgress || (() => {});
885
+ const onProgress = (event) => {
886
+ try { sessionRecorder.appendProgress(event); } catch (err) {
887
+ console.error('[chat] Failed to append session progress:', err.message);
888
+ }
889
+ return progressSink(event);
890
+ };
827
891
 
828
892
  // Execute a chat tool call
829
893
  async function executeChatTool(name, input) {
@@ -909,9 +973,12 @@ async function chat(message, opts = {}) {
909
973
  const bundledSkill = findSkill(input.skill_name);
910
974
  if (bundledSkill && bundledSkill.execution === 'script') {
911
975
  try {
912
- const { executeSkill } = require('./loops/tasks');
976
+ const { runScriptSkillByName } = require('./skills/script-skill-runner');
913
977
  const fakeTaskId = `chat-${Date.now()}`;
914
- const result = await executeSkill(fakeTaskId, { id: fakeTaskId, skill: input.skill_name, title: bundledSkill.name });
978
+ const result = await runScriptSkillByName(input.skill_name, { id: fakeTaskId, skill: input.skill_name, title: bundledSkill.name }, {
979
+ skill: bundledSkill,
980
+ log: line => console.error(`[skill:${fakeTaskId}] ${line}`),
981
+ });
915
982
  return { success: true, output: result.slice(0, 5000) };
916
983
  } catch (err) {
917
984
  return { error: err.message };
@@ -1324,8 +1391,13 @@ async function chat(message, opts = {}) {
1324
1391
  channel, session_id: sessionId,
1325
1392
  attachments: persisted,
1326
1393
  });
1394
+ recordSessionMessage('user', message, {
1395
+ dbMessageId: _userMessageId,
1396
+ attachments: persisted,
1397
+ });
1327
1398
  } else if (persistUserTurn) {
1328
1399
  brain.insertChatMessage({ role: 'user', content: message, channel, session_id: sessionId });
1400
+ recordSessionMessage('user', message);
1329
1401
  }
1330
1402
  if (persistUserTurn) {
1331
1403
  brain.insertMemory({
@@ -1391,6 +1463,7 @@ async function chat(message, opts = {}) {
1391
1463
  let finalText = '';
1392
1464
  let lastTurnText = ''; // Only the last turn's text (prevents error accumulation across turns)
1393
1465
  let lastTurn = 0;
1466
+ let finalResponseMeta = null;
1394
1467
 
1395
1468
  // Adaptive limits based on intent classification
1396
1469
  const INTENT_LIMITS = {
@@ -1403,7 +1476,7 @@ async function chat(message, opts = {}) {
1403
1476
  const MAX_TOOL_CALLS = opts.maxToolCalls != null ? opts.maxToolCalls : limits.maxTools;
1404
1477
  let toolCallCount = 0;
1405
1478
  const MESSAGE_TIMEOUT_MS = opts.timeoutMs || limits.timeoutMs;
1406
- const messageDeadline = Date.now() + MESSAGE_TIMEOUT_MS;
1479
+ messageDeadline = Date.now() + MESSAGE_TIMEOUT_MS;
1407
1480
  console.log('[chat] Intent:', intent, '| topics:', queryTopics.join(','), '| limits: turns=', MAX_TURNS, 'tools=', MAX_TOOL_CALLS, 'timeout=', MESSAGE_TIMEOUT_MS, 'ms');
1408
1481
 
1409
1482
  const chatStart = Date.now();
@@ -1500,11 +1573,16 @@ async function chat(message, opts = {}) {
1500
1573
  } catch (llmErr) {
1501
1574
  // Track provider health — failed LLM call
1502
1575
  try {
1503
- const provType = usedProvider || getDefaultProviderType();
1576
+ const provType = usedProvider || targetProviderType || getDefaultProviderType();
1504
1577
  const registeredProv = providerAvailability.getConfiguredProviders().find(p => p.providerType === provType);
1505
1578
  if (registeredProv) providerAvailability.recordFailure(registeredProv.providerId, llmErr.message);
1506
1579
  } catch {}
1507
- throw llmErr;
1580
+ const decorated = decorateProviderError(llmErr, {
1581
+ provider: usedProvider || targetProviderType || getDefaultProviderType(),
1582
+ model: selectedModel,
1583
+ });
1584
+ recordProviderFailureAlert(decorated.providerError, brain);
1585
+ throw decorated;
1508
1586
  }
1509
1587
  const modelElapsed = Date.now() - turnStart;
1510
1588
  timings.modelMs += modelElapsed;
@@ -1537,6 +1615,12 @@ async function chat(message, opts = {}) {
1537
1615
  // If no tool calls, we have our final answer
1538
1616
  if (response.toolCalls.length === 0) {
1539
1617
  finalText = response.content || '';
1618
+ finalResponseMeta = {
1619
+ model: response.model || selectedModel,
1620
+ provider: response.provider || usedProvider || targetProviderType,
1621
+ usage: response.usage || null,
1622
+ stopReason: response.stopReason || null,
1623
+ };
1540
1624
  console.log('[chat] Total time:', Date.now() - chatStart, 'ms across', turn + 1, 'turns');
1541
1625
  break;
1542
1626
  }
@@ -1653,6 +1737,16 @@ async function chat(message, opts = {}) {
1653
1737
  for (const tc of response.toolCalls) {
1654
1738
  assistantContent.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.input });
1655
1739
  }
1740
+ recordSessionMessage('assistant', assistantContent, {
1741
+ model: response.model || selectedModel,
1742
+ provider: response.provider || usedProvider || targetProviderType,
1743
+ usage: response.usage || null,
1744
+ stopReason: response.stopReason || null,
1745
+ });
1746
+ recordSessionMessage('user', toolResults, {
1747
+ synthetic: true,
1748
+ reason: 'tool_results',
1749
+ });
1656
1750
  messages.push({ role: 'assistant', content: assistantContent });
1657
1751
  messages.push({ role: 'user', content: toolResults });
1658
1752
 
@@ -1678,9 +1772,12 @@ async function chat(message, opts = {}) {
1678
1772
  // Otherwise, continue: the model needs to see tool results before responding.
1679
1773
  }
1680
1774
 
1681
- if (codeReviewContextBlock && looksLikePrematureCodeReviewReply(finalText)) {
1775
+ const needsCodeReviewRepair = codeReviewContextBlock
1776
+ && (looksLikePrematureCodeReviewReply(finalText)
1777
+ || looksLikeContradictoryCleanReviewReply(finalText, codeReviewContextBlock));
1778
+ if (needsCodeReviewRepair) {
1682
1779
  try {
1683
- console.log('[chat] Premature code-review reply detected; forcing final review follow-up');
1780
+ console.log('[chat] Invalid code-review reply detected; forcing final review follow-up');
1684
1781
  resetTurnTimeout();
1685
1782
  const repairStart = Date.now();
1686
1783
  const repairMessages = [
@@ -1706,13 +1803,29 @@ async function chat(message, opts = {}) {
1706
1803
  }
1707
1804
  timings.repairMs += Date.now() - repairStart;
1708
1805
  if (!usedModel) { usedModel = repairResponse.model; usedProvider = repairResponse.provider; }
1709
- if (repairResponse.content && !looksLikePrematureCodeReviewReply(repairResponse.content)) {
1806
+ if (repairResponse.content
1807
+ && !looksLikePrematureCodeReviewReply(repairResponse.content)
1808
+ && !looksLikeContradictoryCleanReviewReply(repairResponse.content, codeReviewContextBlock)) {
1710
1809
  finalText = repairResponse.content;
1711
1810
  lastTurnText = repairResponse.content;
1811
+ finalResponseMeta = {
1812
+ model: repairResponse.model || selectedModel,
1813
+ provider: repairResponse.provider || usedProvider || targetProviderType,
1814
+ usage: repairResponse.usage || null,
1815
+ stopReason: repairResponse.stopReason || null,
1816
+ repair: true,
1817
+ };
1712
1818
  }
1713
1819
  } catch (repairErr) {
1714
1820
  timings.repairMs = timings.repairMs || 0;
1715
1821
  console.error('[chat] Code-review repair call failed:', repairErr.message);
1822
+ try {
1823
+ const decorated = decorateProviderError(repairErr, {
1824
+ provider: usedProvider || targetProviderType || getDefaultProviderType(),
1825
+ model: selectedModel,
1826
+ });
1827
+ recordProviderFailureAlert(decorated.providerError, brain);
1828
+ } catch {}
1716
1829
  }
1717
1830
  }
1718
1831
 
@@ -1728,15 +1841,36 @@ async function chat(message, opts = {}) {
1728
1841
  signal: controller.signal,
1729
1842
  });
1730
1843
  finalText = summaryResponse.content || '';
1844
+ finalResponseMeta = {
1845
+ model: summaryResponse.model || selectedModel,
1846
+ provider: summaryResponse.provider || usedProvider || targetProviderType,
1847
+ usage: summaryResponse.usage || null,
1848
+ stopReason: summaryResponse.stopReason || null,
1849
+ summaryFallback: true,
1850
+ };
1731
1851
  } catch (summaryErr) {
1732
1852
  console.error('[chat] Summary call failed:', summaryErr.message);
1853
+ let summaryProviderError = null;
1854
+ try {
1855
+ const decorated = decorateProviderError(summaryErr, {
1856
+ provider: usedProvider || targetProviderType || getDefaultProviderType(),
1857
+ model: selectedModel,
1858
+ });
1859
+ summaryProviderError = decorated.providerError;
1860
+ recordProviderFailureAlert(summaryProviderError, brain);
1861
+ } catch {}
1733
1862
  // Include the tool results directly so the user at least sees what the tool returned
1734
1863
  const lastToolResults = messages.filter(m => m.role === 'user' && Array.isArray(m.content))
1735
1864
  .pop()?.content?.map(r => {
1736
1865
  try { const parsed = JSON.parse(r.content); return JSON.stringify(parsed, null, 2); } catch { return r.content; }
1737
1866
  }).join('\n');
1867
+ const summaryFailureText = summaryProviderError
1868
+ ? `${summaryProviderError.title}: ${summaryProviderError.userMessage}`
1869
+ : `Summary generation failed: ${summaryErr.message}`;
1738
1870
  if (lastToolResults) {
1739
- finalText = 'Here are the raw results (summary generation failed):\n\n```\n' + lastToolResults.slice(0, 3000) + '\n```';
1871
+ finalText = summaryFailureText + '\n\nHere are the raw tool results:\n\n```\n' + lastToolResults.slice(0, 3000) + '\n```';
1872
+ } else {
1873
+ finalText = summaryFailureText;
1740
1874
  }
1741
1875
  }
1742
1876
  }
@@ -1796,7 +1930,13 @@ async function chat(message, opts = {}) {
1796
1930
 
1797
1931
  // Save assistant response (user message was already saved before calling Claude)
1798
1932
  const persistStart = Date.now();
1799
- brain.insertChatMessage({ role: 'assistant', content: text, channel, session_id: sessionId });
1933
+ const assistantMessage = brain.insertChatMessage({ role: 'assistant', content: text, channel, session_id: sessionId });
1934
+ recordSessionMessage('assistant', text, {
1935
+ ...(finalResponseMeta || {}),
1936
+ dbMessageId: assistantMessage.id,
1937
+ selfCritique: selfCritiqueOutcome,
1938
+ final: true,
1939
+ });
1800
1940
 
1801
1941
  brain.insertMemory({
1802
1942
  source: 'wall-e-chat',
@@ -1926,7 +2066,13 @@ async function chat(message, opts = {}) {
1926
2066
  cost, toolCalls: allToolCalls,
1927
2067
  latencyBreakdown: timings,
1928
2068
  silentReply,
2069
+ ...(opts.includeSessionFile ? { sessionFile: sessionRecorder.filePath } : {}),
1929
2070
  };
2071
+ } catch (err) {
2072
+ try { sessionRecorder.appendError(err); } catch (sessionErr) {
2073
+ console.error('[chat] Failed to append session error:', sessionErr.message);
2074
+ }
2075
+ throw err;
1930
2076
  } finally {
1931
2077
  clearTimeout(timeout);
1932
2078
  }