create-walle 0.9.25 → 0.9.26

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 (179) hide show
  1. package/README.md +8 -0
  2. package/bin/create-walle.js +815 -45
  3. package/package.json +2 -2
  4. package/template/bin/ctm-dev-cleanup.js +90 -4
  5. package/template/bin/ctm-launch.sh +49 -1
  6. package/template/bin/dev.sh +45 -1
  7. package/template/bin/ensure-stable-node.js +132 -0
  8. package/template/bin/install-service.sh +9 -0
  9. package/template/claude-task-manager/api-prompts.js +899 -119
  10. package/template/claude-task-manager/approval-agent.js +360 -40
  11. package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
  12. package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
  13. package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
  14. package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
  15. package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
  16. package/template/claude-task-manager/db.js +399 -48
  17. package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
  18. package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
  19. package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
  20. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
  21. package/template/claude-task-manager/lib/approval-hook.js +200 -0
  22. package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
  23. package/template/claude-task-manager/lib/auth-rules.js +11 -0
  24. package/template/claude-task-manager/lib/background-llm.js +32 -4
  25. package/template/claude-task-manager/lib/codesign-identity.js +140 -0
  26. package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
  27. package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
  28. package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
  29. package/template/claude-task-manager/lib/codex-paths.js +73 -0
  30. package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
  31. package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
  32. package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
  33. package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
  34. package/template/claude-task-manager/lib/command-targets.js +163 -0
  35. package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
  36. package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
  37. package/template/claude-task-manager/lib/escalation-review.js +80 -3
  38. package/template/claude-task-manager/lib/flow-control.js +52 -0
  39. package/template/claude-task-manager/lib/fs-watcher.js +24 -15
  40. package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
  41. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
  42. package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
  43. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
  44. package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
  45. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
  46. package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
  47. package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
  48. package/template/claude-task-manager/lib/perf-tracker.js +29 -2
  49. package/template/claude-task-manager/lib/permission-match.js +146 -16
  50. package/template/claude-task-manager/lib/project-slug.js +33 -0
  51. package/template/claude-task-manager/lib/prompt-intent.js +51 -4
  52. package/template/claude-task-manager/lib/read-pool-client.js +48 -3
  53. package/template/claude-task-manager/lib/real-node.js +73 -0
  54. package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
  55. package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
  56. package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
  57. package/template/claude-task-manager/lib/session-history.js +5 -7
  58. package/template/claude-task-manager/lib/session-host-manager.js +19 -0
  59. package/template/claude-task-manager/lib/session-jobs.js +6 -0
  60. package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
  61. package/template/claude-task-manager/lib/session-messages-page.js +211 -0
  62. package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
  63. package/template/claude-task-manager/lib/session-standup.js +8 -0
  64. package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
  65. package/template/claude-task-manager/lib/session-token-usage.js +30 -8
  66. package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
  67. package/template/claude-task-manager/lib/storage-migration.js +2 -1
  68. package/template/claude-task-manager/lib/transcript-store.js +179 -12
  69. package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
  70. package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
  71. package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
  72. package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
  73. package/template/claude-task-manager/package.json +5 -2
  74. package/template/claude-task-manager/prompt-harvest.js +31 -11
  75. package/template/claude-task-manager/providers/claude-code.js +29 -1
  76. package/template/claude-task-manager/providers/codex.js +13 -1
  77. package/template/claude-task-manager/public/css/setup.css +11 -0
  78. package/template/claude-task-manager/public/css/walle-session.css +132 -4
  79. package/template/claude-task-manager/public/css/walle.css +89 -0
  80. package/template/claude-task-manager/public/icon-16.png +0 -0
  81. package/template/claude-task-manager/public/icon-32.png +0 -0
  82. package/template/claude-task-manager/public/icon-512.png +0 -0
  83. package/template/claude-task-manager/public/index.html +2483 -165
  84. package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
  85. package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
  86. package/template/claude-task-manager/public/js/message-renderer.js +60 -1
  87. package/template/claude-task-manager/public/js/prompts.js +13 -1
  88. package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
  89. package/template/claude-task-manager/public/js/setup.js +54 -10
  90. package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
  91. package/template/claude-task-manager/public/js/stream-view.js +78 -0
  92. package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
  93. package/template/claude-task-manager/public/js/tool-state.js +155 -0
  94. package/template/claude-task-manager/public/js/walle-session.js +887 -326
  95. package/template/claude-task-manager/public/js/walle.js +306 -195
  96. package/template/claude-task-manager/public/m/app.css +1 -0
  97. package/template/claude-task-manager/public/m/app.js +33 -3
  98. package/template/claude-task-manager/queue-engine.js +45 -1
  99. package/template/claude-task-manager/server.js +3367 -540
  100. package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
  101. package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
  102. package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
  103. package/template/claude-task-manager/workers/session-host-process.js +10 -0
  104. package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
  105. package/template/package.json +2 -3
  106. package/template/shared/icons/AppIcon-ctm.icns +0 -0
  107. package/template/shared/icons/AppIcon-walle.icns +0 -0
  108. package/template/wall-e/agent.js +139 -18
  109. package/template/wall-e/api-walle.js +201 -22
  110. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
  111. package/template/wall-e/brain.js +1049 -39
  112. package/template/wall-e/chat.js +427 -86
  113. package/template/wall-e/coding/acceptance-contract.js +26 -1
  114. package/template/wall-e/coding/action-memory-policy.js +353 -0
  115. package/template/wall-e/coding/action-memory-store.js +814 -0
  116. package/template/wall-e/coding/initial-messages.js +197 -0
  117. package/template/wall-e/coding/no-progress-guard.js +327 -0
  118. package/template/wall-e/coding/permission-service.js +88 -22
  119. package/template/wall-e/coding/session-workspaces.js +81 -0
  120. package/template/wall-e/coding/shell-sandbox.js +124 -0
  121. package/template/wall-e/coding/stream-processor.js +63 -2
  122. package/template/wall-e/coding/tool-execution-controller.js +14 -1
  123. package/template/wall-e/coding/tool-registry.js +1 -1
  124. package/template/wall-e/coding/transcript-writer.js +3 -0
  125. package/template/wall-e/coding-orchestrator.js +636 -35
  126. package/template/wall-e/coding-prompts.js +51 -2
  127. package/template/wall-e/docs/model-routing-policy.md +59 -0
  128. package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
  129. package/template/wall-e/extraction/knowledge-extractor.js +76 -23
  130. package/template/wall-e/http/chat-api.js +30 -12
  131. package/template/wall-e/http/model-admin.js +93 -1
  132. package/template/wall-e/lib/background-lanes.js +133 -0
  133. package/template/wall-e/lib/boot-profile.js +11 -0
  134. package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
  135. package/template/wall-e/lib/brain-read-pool-client.js +311 -0
  136. package/template/wall-e/lib/diagnostics-flags.js +87 -0
  137. package/template/wall-e/lib/event-loop-monitor.js +74 -3
  138. package/template/wall-e/lib/mcp-integration.js +7 -1
  139. package/template/wall-e/lib/real-node.js +98 -0
  140. package/template/wall-e/lib/runtime-health.js +206 -0
  141. package/template/wall-e/lib/runtime-worker-pool.js +101 -0
  142. package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
  143. package/template/wall-e/lib/scheduler.js +446 -17
  144. package/template/wall-e/lib/service-health.js +61 -2
  145. package/template/wall-e/lib/service-readiness.js +258 -0
  146. package/template/wall-e/lib/usage.js +152 -0
  147. package/template/wall-e/lib/worker-thread-pool.js +389 -0
  148. package/template/wall-e/llm/client.js +81 -4
  149. package/template/wall-e/llm/default-fallback.js +54 -8
  150. package/template/wall-e/llm/mlx.js +536 -73
  151. package/template/wall-e/llm/mlx.plugin.json +1 -1
  152. package/template/wall-e/llm/ollama.js +342 -43
  153. package/template/wall-e/llm/provider-error.js +18 -1
  154. package/template/wall-e/llm/provider-health-state.js +176 -0
  155. package/template/wall-e/llm/routing-policy.js +796 -0
  156. package/template/wall-e/llm/supported-models.js +5 -0
  157. package/template/wall-e/loops/tasks.js +60 -14
  158. package/template/wall-e/loops/think.js +89 -24
  159. package/template/wall-e/mcp-server.js +192 -28
  160. package/template/wall-e/server.js +32 -7
  161. package/template/wall-e/skills/script-skill-runner.js +8 -1
  162. package/template/wall-e/skills/skill-planner.js +64 -1
  163. package/template/wall-e/tools/builtin-middleware.js +67 -2
  164. package/template/wall-e/tools/local-tools.js +116 -26
  165. package/template/wall-e/tools/permission-checker.js +52 -4
  166. package/template/wall-e/tools/permission-rules.js +36 -0
  167. package/template/wall-e/tools/shell-analyzer.js +46 -1
  168. package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
  169. package/template/wall-e/training/real-trajectory-miner.js +2617 -0
  170. package/template/wall-e/training/replay-eval-analysis.js +151 -0
  171. package/template/wall-e/training/run-shell-command-selector.js +277 -0
  172. package/template/wall-e/training/tool-sft-dataset.js +312 -0
  173. package/template/wall-e/training/tool-sft-renderers.js +144 -0
  174. package/template/wall-e/training/tool-trace-harvester.js +1440 -0
  175. package/template/wall-e/training/trajectory-action-selector.js +364 -0
  176. package/template/wall-e/weather-runtime.js +232 -0
  177. package/template/wall-e/workers/brain-owner-worker.js +162 -0
  178. package/template/wall-e/workers/brain-read-worker.js +148 -0
  179. package/template/wall-e/workers/runtime-worker.js +145 -0
@@ -150,6 +150,11 @@ const SUPPORTED_MODEL_CATALOG = Object.freeze({
150
150
  },
151
151
  ]),
152
152
  ollama: Object.freeze([
153
+ {
154
+ id: 'gemma4:e4b-it-qat',
155
+ name: 'Gemma 4 E4B IT QAT',
156
+ capabilities: Object.freeze(['chat', 'code', 'tools', 'reasoning']),
157
+ },
153
158
  {
154
159
  id: 'gemma4:e4b',
155
160
  name: 'Gemma 4 E4B',
@@ -3,6 +3,8 @@ const brain = require('../brain');
3
3
  const { spawn } = require('child_process');
4
4
  const path = require('path');
5
5
  const { findSkill } = require('../skills/skill-loader');
6
+ const runtimeHealth = require('../lib/runtime-health');
7
+ const { retryOnWriteLockBusy } = require('../shared/sqlite-write-lock');
6
8
 
7
9
  const WALL_E_DIR = path.join(__dirname, '..');
8
10
 
@@ -39,6 +41,42 @@ function appendLog(taskId, line) {
39
41
  if (entry.lines.length > 500) entry.lines.shift();
40
42
  }
41
43
 
44
+ async function safeUpdateTask(taskId, updates, options = {}) {
45
+ const reason = options.reason || 'task-update';
46
+ const bestEffort = options.bestEffort === true;
47
+ const op = runtimeHealth.beginOperation('tasks.updateTask', { reason, bestEffort });
48
+ let retries = 0;
49
+ try {
50
+ await retryOnWriteLockBusy(
51
+ () => brain.updateTask(taskId, updates),
52
+ {
53
+ retries: Number.isInteger(options.retries) ? options.retries : 4,
54
+ backoffMs: Number.isFinite(options.backoffMs) ? options.backoffMs : 75,
55
+ onRetry: ({ attempt, error }) => {
56
+ retries = attempt;
57
+ runtimeHealth.recordOperation('tasks.updateTask.retry', 0, {
58
+ meta: {
59
+ reason,
60
+ attempt,
61
+ holderPid: error?.lockInfo?.pid || 0,
62
+ holderLabel: error?.lockInfo?.label || '',
63
+ },
64
+ });
65
+ },
66
+ },
67
+ );
68
+ op.end({ meta: { reason, retries } });
69
+ return true;
70
+ } catch (err) {
71
+ op.end({ ok: false, error: err, meta: { reason, retries, bestEffort } });
72
+ if (bestEffort) {
73
+ console.warn(`[tasks] Skipped best-effort task update (${reason}): ${err.message}`);
74
+ return false;
75
+ }
76
+ throw err;
77
+ }
78
+ }
79
+
42
80
  /**
43
81
  * On daemon startup, recover tasks that were interrupted (stuck in 'running').
44
82
  * Resets them to 'pending' so they get re-picked up.
@@ -80,11 +118,11 @@ async function tryResumeSlackTasks() {
80
118
 
81
119
  // Token is valid — un-pause all Slack tasks
82
120
  for (const t of pausedSlack) {
83
- brain.updateTask(t.id, {
121
+ await safeUpdateTask(t.id, {
84
122
  status: 'pending',
85
123
  error: null,
86
124
  next_run_at: new Date().toISOString(),
87
- });
125
+ }, { reason: 'slack-auto-resume' });
88
126
  console.log(`[tasks] Auto-resumed: ${t.title} (Slack token refreshed)`);
89
127
  }
90
128
  } catch {}
@@ -106,7 +144,7 @@ async function runDueTasks() {
106
144
 
107
145
  const now = new Date().toISOString();
108
146
  const hasCheckpoint = task.checkpoint ? ' (resuming from checkpoint)' : '';
109
- brain.updateTask(task.id, { status: 'running', started_at: now });
147
+ await safeUpdateTask(task.id, { status: 'running', started_at: now }, { reason: 'task-start' });
110
148
  appendLog(task.id, `[${now.slice(11, 19)}] Starting: ${task.title}${hasCheckpoint}`);
111
149
 
112
150
  try {
@@ -148,7 +186,7 @@ async function runDueTasks() {
148
186
  }
149
187
  }
150
188
 
151
- brain.updateTask(task.id, {
189
+ await safeUpdateTask(task.id, {
152
190
  status: slackDeliveryFailed ? 'pending' : (task.type === 'recurring' ? 'pending' : 'completed'),
153
191
  completed_at: completedAt,
154
192
  last_run_at: completedAt,
@@ -160,7 +198,7 @@ async function runDueTasks() {
160
198
  ? new Date(Date.now() + 2 * 60 * 1000).toISOString() // retry in 2 min
161
199
  : (task.type === 'recurring' ? computeNextDue(task.schedule) : null),
162
200
  // Keep started_at so duration can be computed as completed_at - started_at
163
- });
201
+ }, { reason: 'task-complete' });
164
202
 
165
203
  // Consolidate briefing items if present
166
204
  try {
@@ -234,7 +272,7 @@ async function runDueTasks() {
234
272
  const allTasks = brain.listTasks({});
235
273
  for (const t of allTasks) {
236
274
  if (t.skill && t.skill.includes('slack') && (t.status === 'pending' || t.status === 'running')) {
237
- brain.updateTask(t.id, { status: 'paused' });
275
+ await safeUpdateTask(t.id, { status: 'paused' }, { bestEffort: true, reason: 'slack-auth-pause-peer' });
238
276
  console.log(`[tasks] Auto-paused: ${t.title} (Slack auth expired, refresh failed)`);
239
277
  }
240
278
  }
@@ -263,7 +301,7 @@ async function runDueTasks() {
263
301
  }
264
302
  }
265
303
 
266
- brain.updateTask(task.id, {
304
+ await safeUpdateTask(task.id, {
267
305
  status: task.type === 'recurring' ? 'pending' : 'failed',
268
306
  error: isSlackAuth
269
307
  ? (slackRefreshSucceeded ? 'Slack token refreshed — retrying.' : 'Slack token expired. Reconnect Slack to retry.')
@@ -272,11 +310,11 @@ async function runDueTasks() {
272
310
  last_run_at: new Date().toISOString(),
273
311
  run_count: (task.run_count || 0) + 1,
274
312
  next_run_at: task.type === 'recurring' ? computeNextDue(task.schedule) : null,
275
- });
313
+ }, { reason: 'task-fail' });
276
314
 
277
315
  // Only pause recurring tasks if refresh truly failed
278
316
  if (isSlackAuth && !slackRefreshSucceeded && task.type === 'recurring') {
279
- brain.updateTask(task.id, { status: 'paused' });
317
+ await safeUpdateTask(task.id, { status: 'paused' }, { bestEffort: true, reason: 'slack-auth-pause-current' });
280
318
  }
281
319
  }
282
320
  }
@@ -296,7 +334,12 @@ async function executeSkill(taskId, task) {
296
334
  const { runScriptSkill } = require('../skills/script-skill-runner');
297
335
  return runScriptSkill(skill, task, {
298
336
  log: line => appendLog(taskId, line),
299
- onCheckpoint: checkpoint => brain.updateTask(taskId, { checkpoint }),
337
+ onCheckpoint: checkpoint => {
338
+ void safeUpdateTask(taskId, { checkpoint }, {
339
+ bestEffort: true,
340
+ reason: 'script-skill-checkpoint',
341
+ });
342
+ },
300
343
  onProcess: child => taskProcesses.set(taskId, child),
301
344
  onClose: child => {
302
345
  if (taskProcesses.get(taskId) === child) taskProcesses.delete(taskId);
@@ -345,7 +388,10 @@ function executeScript(taskId, script, checkpoint, extraEnv) {
345
388
  appendLog(taskId, line);
346
389
  // Lines starting with CHECKPOINT: are saved as resume points
347
390
  if (line.startsWith('CHECKPOINT:')) {
348
- brain.updateTask(taskId, { checkpoint: line.slice(11).trim() });
391
+ void safeUpdateTask(taskId, { checkpoint: line.slice(11).trim() }, {
392
+ bestEffort: true,
393
+ reason: 'shell-script-checkpoint',
394
+ });
349
395
  }
350
396
  });
351
397
  });
@@ -438,9 +484,9 @@ async function executeMultiTurnChat(taskId, task) {
438
484
  }
439
485
 
440
486
  // Checkpoint between turns
441
- brain.updateTask(taskId, {
487
+ await safeUpdateTask(taskId, {
442
488
  checkpoint: JSON.stringify({ turn: turn + 1, partial: lastReply.slice(0, 2000) }),
443
- });
489
+ }, { bestEffort: true, reason: 'multi-turn-checkpoint' });
444
490
  }
445
491
 
446
492
  return lastReply;
@@ -515,7 +561,7 @@ async function runTaskById(taskId) {
515
561
  const task = brain.getTask(taskId);
516
562
  if (!task) throw new Error('Task not found');
517
563
  if (task.status === 'running') throw new Error('Task is already running');
518
- brain.updateTask(taskId, { status: 'pending', next_run_at: new Date().toISOString(), error: null, result: null });
564
+ await safeUpdateTask(taskId, { status: 'pending', next_run_at: new Date().toISOString(), error: null, result: null }, { reason: 'manual-run' });
519
565
  return runDueTasks();
520
566
  }
521
567
 
@@ -3,10 +3,49 @@ const { extractKnowledge } = require('../extraction/knowledge-extractor');
3
3
  const { detectContradictions } = require('../extraction/contradiction');
4
4
  const { linkKnowledgeToEntities, learnEntitiesFromText } = require('../extraction/entity-normalizer');
5
5
  const { shouldRunLoop, skipped } = require('./loop-precheck');
6
+ const runtimeHealth = require('../lib/runtime-health');
7
+ const { perfLogsEnabled } = require('../lib/diagnostics-flags');
6
8
  let embeddings;
7
9
  try { embeddings = require('../embeddings'); } catch { embeddings = null; }
8
10
 
9
11
  const BATCH_SIZE = 20;
12
+ const THINK_PHASE_LOG_THRESHOLD_MS = Math.max(0, Number(process.env.WALLE_THINK_PHASE_LOG_THRESHOLD_MS || 1000));
13
+
14
+ function _phaseMeta(meta = {}) {
15
+ const out = {};
16
+ for (const [key, value] of Object.entries(meta || {})) {
17
+ if (value == null) continue;
18
+ if (typeof value === 'number' || typeof value === 'boolean') out[key] = value;
19
+ else out[key] = String(value).slice(0, 80);
20
+ }
21
+ return out;
22
+ }
23
+
24
+ async function _measurePhase(name, meta, fn) {
25
+ const started = Date.now();
26
+ let ok = false;
27
+ try {
28
+ const result = await fn();
29
+ ok = true;
30
+ return result;
31
+ } catch (error) {
32
+ const durationMs = Date.now() - started;
33
+ runtimeHealth.recordOperation(`think.phase.${name}`, durationMs, { ok: false, error, meta: _phaseMeta(meta) });
34
+ if (perfLogsEnabled()) {
35
+ console.warn(`[think-perf] phase=${name} failed durationMs=${durationMs} error=${error && (error.code || error.name || error.message)}`);
36
+ }
37
+ throw error;
38
+ } finally {
39
+ if (ok) {
40
+ const durationMs = Date.now() - started;
41
+ runtimeHealth.recordOperation(`think.phase.${name}`, durationMs, { ok: true, meta: _phaseMeta(meta) });
42
+ if (perfLogsEnabled() && durationMs >= THINK_PHASE_LOG_THRESHOLD_MS) {
43
+ const details = Object.entries(_phaseMeta(meta)).map(([k, v]) => `${k}=${v}`).join(' ');
44
+ console.log(`[think-perf] phase=${name} durationMs=${durationMs}${details ? ` ${details}` : ''}`);
45
+ }
46
+ }
47
+ }
48
+ }
10
49
 
11
50
  // Contradiction → question policy. Historically EVERY detected contradiction created a
12
51
  // pending_question AND superseded the old knowledge — yielding ~2,000 questions/day that
@@ -63,11 +102,15 @@ async function runOnce(opts = {}) {
63
102
 
64
103
  try {
65
104
  // Step 1: Extract knowledge + classifications (async API call)
66
- const entries = await extractFn(pending, ownerName, { model: opts.model });
105
+ const entries = await _measurePhase('extract', { pending: pending.length }, () => (
106
+ extractFn(pending, ownerName, { model: opts.model })
107
+ ));
67
108
  const classifications = entries._classifications || [];
68
109
 
69
110
  // Step 2: Detect contradictions (async API call, outside transaction)
70
- const contradictions = await detectContradictions(entries, ownerName, { detectFn: opts.detectFn });
111
+ const contradictions = await _measurePhase('contradictions', { entries: entries.length }, () => (
112
+ detectContradictions(entries, ownerName, { detectFn: opts.detectFn })
113
+ ));
71
114
 
72
115
  // Step 3: DB transaction: insert knowledge + handle contradictions + classify + mark done
73
116
  const db = brain.getDb();
@@ -84,8 +127,6 @@ async function runOnce(opts = {}) {
84
127
  });
85
128
  insertedIds.push({ ...entry, _id: result.id });
86
129
  knowledgeExtracted++;
87
- // Link to entities (rule-based, no API cost)
88
- try { linkKnowledgeToEntities({ ...entry, id: result.id }, ownerName); } catch {}
89
130
  }
90
131
 
91
132
  // Handle contradictions
@@ -128,47 +169,71 @@ async function runOnce(opts = {}) {
128
169
  brain.updateMemoryExtraction(mem.id, 'done');
129
170
  }
130
171
  });
131
- writeResults();
172
+ await _measurePhase('db_write', {
173
+ pending: pending.length,
174
+ entries: entries.length,
175
+ contradictions: contradictions.length,
176
+ classifications: classifications.length,
177
+ }, () => writeResults());
178
+
179
+ // Entity links are derived from inserted knowledge, so keep them out of the
180
+ // extraction-status transaction. This shortens the critical SQLite write lock
181
+ // while preserving best-effort entity graph enrichment.
182
+ await _measurePhase('entity_linking', { entries: insertedIds.length }, () => {
183
+ for (const entry of insertedIds) {
184
+ try { linkKnowledgeToEntities({ ...entry, id: entry._id }, ownerName); } catch {}
185
+ }
186
+ });
132
187
  } catch (err) {
133
188
  console.error('[think] Extraction failed:', err.message);
134
- for (const mem of pending) {
135
- try {
136
- brain.updateMemoryExtraction(mem.id, 'failed');
137
- } catch (updateErr) {
138
- console.error('[think] Failed to mark memory as failed:', updateErr.message);
189
+ await _measurePhase('mark_failed', { pending: pending.length }, () => {
190
+ for (const mem of pending) {
191
+ try {
192
+ brain.updateMemoryExtraction(mem.id, 'failed');
193
+ } catch (updateErr) {
194
+ console.error('[think] Failed to mark memory as failed:', updateErr.message);
195
+ }
139
196
  }
140
- }
197
+ });
141
198
  }
142
199
 
143
200
  // Step 4a: Learn entities from memory content (rule-based, no API cost)
144
- try {
201
+ await _measurePhase('entity_learning', { pending: pending.length }, () => {
145
202
  for (const mem of pending) {
146
203
  if (mem.content && mem.content.length > 50) {
147
204
  learnEntitiesFromText(mem.content, ownerName);
148
205
  }
149
206
  }
150
- } catch {}
207
+ }).catch(() => {});
151
208
 
152
209
  // Step 4: Compute embeddings for processed memories + new knowledge (async, non-blocking)
153
210
  if (embeddings && embeddings.isAvailable()) {
154
211
  try {
155
212
  const textsToEmbed = pending.map(m => (m.content || '').substring(0, 512));
156
- const memEmbeddings = await embeddings.batchComputeEmbeddings(textsToEmbed);
213
+ const memEmbeddings = await _measurePhase('embeddings.memory', { pending: pending.length }, () => (
214
+ embeddings.batchComputeEmbeddings(textsToEmbed)
215
+ ));
157
216
  if (memEmbeddings) {
158
- for (let i = 0; i < pending.length; i++) {
159
- embeddings.storeEmbedding(pending[i].id, 'memory', memEmbeddings[i]);
160
- }
217
+ await _measurePhase('embeddings.memory_store', { pending: pending.length }, () => {
218
+ for (let i = 0; i < pending.length; i++) {
219
+ embeddings.storeEmbedding(pending[i].id, 'memory', memEmbeddings[i]);
220
+ }
221
+ });
161
222
  console.log(`[think] Stored ${pending.length} memory embeddings`);
162
223
  }
163
224
 
164
225
  // Embed new knowledge triples
165
226
  if (insertedIds.length > 0) {
166
227
  const kTexts = insertedIds.map(k => `${k.subject} ${k.predicate} ${k.object}`);
167
- const kEmbeddings = await embeddings.batchComputeEmbeddings(kTexts);
228
+ const kEmbeddings = await _measurePhase('embeddings.knowledge', { entries: insertedIds.length }, () => (
229
+ embeddings.batchComputeEmbeddings(kTexts)
230
+ ));
168
231
  if (kEmbeddings) {
169
- for (let i = 0; i < insertedIds.length; i++) {
170
- embeddings.storeEmbedding(insertedIds[i]._id, 'knowledge', kEmbeddings[i]);
171
- }
232
+ await _measurePhase('embeddings.knowledge_store', { entries: insertedIds.length }, () => {
233
+ for (let i = 0; i < insertedIds.length; i++) {
234
+ embeddings.storeEmbedding(insertedIds[i]._id, 'knowledge', kEmbeddings[i]);
235
+ }
236
+ });
172
237
  console.log(`[think] Stored ${insertedIds.length} knowledge embeddings`);
173
238
  }
174
239
  }
@@ -177,13 +242,13 @@ async function runOnce(opts = {}) {
177
242
  }
178
243
  }
179
244
 
180
- brain.upsertCheckpoint('think', {
245
+ await _measurePhase('checkpoint', { pending: pending.length, entries: insertedIds.length }, () => brain.upsertCheckpoint('think', {
181
246
  last_memory_at: pending[pending.length - 1].timestamp,
182
247
  metadata: JSON.stringify({ memories_processed: pending.length, knowledge_extracted: knowledgeExtracted }),
183
- });
248
+ }));
184
249
 
185
250
  // Task 5: Self-resolve pending questions
186
- await selfResolveQuestions(ownerName, opts);
251
+ await _measurePhase('self_resolve', {}, () => selfResolveQuestions(ownerName, opts));
187
252
 
188
253
  return { memoriesProcessed: pending.length, knowledgeExtracted };
189
254
  }
@@ -17,6 +17,10 @@ let _embeddings;
17
17
  try { _embeddings = require('./embeddings'); } catch { _embeddings = null; }
18
18
 
19
19
  const PROTOCOL_VERSION = '2025-03-26';
20
+ const DEFAULT_MCP_LOOKUP_SOURCE_TIMEOUT_MS = 2000;
21
+ const DEFAULT_MCP_LOOKUP_CONTEXT_PACK_TIMEOUT_MS = 1200;
22
+ const DEFAULT_MCP_LOOKUP_EMBEDDING_TIMEOUT_MS = 700;
23
+ const MAX_MCP_LOOKUP_TIMEOUT_MS = 10_000;
20
24
 
21
25
  const MCP_RESOURCES = [
22
26
  {
@@ -718,6 +722,20 @@ async function executeTool(name, args) {
718
722
  return routeWallEQuery(args.query, { context: args.context });
719
723
  }
720
724
  case 'walle_lookup_context': {
725
+ const canOffload =
726
+ process.env.WALL_E_PROCESS_ROLE !== 'wall-e-runtime-worker' &&
727
+ (process.env.NODE_ENV !== 'test' || process.env.WALL_E_RUNTIME_WORKER_POOL_TEST === '1');
728
+ if (canOffload) {
729
+ try {
730
+ return await require('./lib/runtime-worker-pool').requestRuntimeWorker(
731
+ 'mcp.lookupWallEContext',
732
+ { args },
733
+ { heavy: true, timeoutMs: Number(args?.timeout_ms) || 45000 }
734
+ );
735
+ } catch (err) {
736
+ console.warn(`[mcp] walle_lookup_context worker failed; falling back local: ${err.message}`);
737
+ }
738
+ }
721
739
  return lookupWallEContext(args);
722
740
  }
723
741
  case 'walle_context_hint': {
@@ -1215,6 +1233,7 @@ async function lookupWallEContext(args = {}) {
1215
1233
  if (!query) throw new Error('query is required');
1216
1234
  const route = routeWallEQuery(query, { context: args.context });
1217
1235
  const limit = clampLimit(args.limit, 5, 20);
1236
+ const timeouts = lookupSourceTimeouts(args);
1218
1237
  const shouldUseWallE = Boolean(route.should_use_walle || args.force_memory);
1219
1238
  const isProfileLookup = route.matched?.some(match => match.name === 'self_profile');
1220
1239
  const includeMemories = args.include_memories === true || (args.include_memories !== false && shouldUseWallE);
@@ -1243,18 +1262,36 @@ async function lookupWallEContext(args = {}) {
1243
1262
  };
1244
1263
 
1245
1264
  if (includeCtmOperational) {
1246
- const operational = await ctmOperationalContext.lookupCtmOperationalContext({
1247
- query,
1248
- context: args.context,
1249
- limit,
1250
- force: true,
1251
- });
1265
+ const operational = await withMcpDeadline(
1266
+ () => ctmOperationalContext.lookupCtmOperationalContext({
1267
+ query,
1268
+ context: args.context,
1269
+ limit,
1270
+ force: true,
1271
+ }),
1272
+ timeouts.source,
1273
+ ({ duration_ms, timeout_ms }) => ({
1274
+ ok: false,
1275
+ required: true,
1276
+ session_results: [],
1277
+ sources: {},
1278
+ timed_out: true,
1279
+ duration_ms,
1280
+ timeout_ms,
1281
+ reason: 'ctm_operational_context_timeout',
1282
+ error: `CTM operational context lookup exceeded ${timeout_ms}ms`,
1283
+ })
1284
+ );
1252
1285
  result.ctm_operational_context = operational;
1253
1286
  result.sources.ctm_operational = {
1254
1287
  searched: true,
1255
1288
  ok: Boolean(operational.ok),
1256
1289
  required: Boolean(operational.required),
1257
- reason: operational.ok ? '' : 'ctm_operational_context_unavailable',
1290
+ reason: operational.ok ? '' : (operational.reason || 'ctm_operational_context_unavailable'),
1291
+ error: operational.error || '',
1292
+ timed_out: Boolean(operational.timed_out),
1293
+ duration_ms: operational.duration_ms,
1294
+ timeout_ms: operational.timeout_ms,
1258
1295
  remote_access: operational.sources?.remote_access || null,
1259
1296
  ctm_sessions: operational.sources?.ctm_sessions || null,
1260
1297
  count: (operational.session_results || []).length,
@@ -1263,27 +1300,71 @@ async function lookupWallEContext(args = {}) {
1263
1300
 
1264
1301
  if (includeSessions) {
1265
1302
  const sessionQuery = isProfileLookup ? 'thorough review fix issues commit high-agency' : query;
1266
- const sessionSearch = await safeCtmSearchSessions({
1267
- query: sessionQuery,
1268
- limit,
1269
- prefer_db: args.prefer_db !== false,
1270
- });
1303
+ const sessionSearchPromise = withMcpDeadline(
1304
+ () => safeCtmSearchSessions({
1305
+ query: sessionQuery,
1306
+ limit,
1307
+ prefer_db: args.prefer_db !== false,
1308
+ }),
1309
+ timeouts.source,
1310
+ ({ duration_ms, timeout_ms }) => ({
1311
+ ok: false,
1312
+ source: 'ctm-db',
1313
+ results: [],
1314
+ reason: 'ctm_session_search_timeout',
1315
+ error: `CTM session search exceeded ${timeout_ms}ms`,
1316
+ timed_out: true,
1317
+ duration_ms,
1318
+ timeout_ms,
1319
+ })
1320
+ );
1321
+ const contextPackPromise = withMcpDeadline(
1322
+ () => safeCtmContextPack({
1323
+ task: query,
1324
+ query: sessionQuery,
1325
+ limit: Math.min(limit, 5),
1326
+ token_budget: 6000,
1327
+ mode: 'compact',
1328
+ include_raw: false,
1329
+ }),
1330
+ timeouts.context_pack,
1331
+ ({ duration_ms, timeout_ms }) => ({
1332
+ ok: false,
1333
+ source: 'ctm-db',
1334
+ sessions: [],
1335
+ messages: [],
1336
+ reason: 'ctm_context_pack_timeout',
1337
+ error: `CTM context pack lookup exceeded ${timeout_ms}ms`,
1338
+ timed_out: true,
1339
+ duration_ms,
1340
+ timeout_ms,
1341
+ })
1342
+ );
1343
+ const [sessionSearch, contextPack] = await Promise.all([sessionSearchPromise, contextPackPromise]);
1271
1344
  result.session_results = (sessionSearch.results || []).slice(0, limit).map(stripRank);
1272
1345
  result.sources.ctm_sessions = {
1273
1346
  searched: true,
1274
1347
  ok: Boolean(sessionSearch.ok),
1275
1348
  source: sessionSearch.source || '',
1276
1349
  reason: sessionSearch.reason || '',
1350
+ error: sessionSearch.error || '',
1351
+ timed_out: Boolean(sessionSearch.timed_out),
1352
+ duration_ms: sessionSearch.duration_ms,
1353
+ timeout_ms: sessionSearch.timeout_ms,
1277
1354
  count: result.session_results.length,
1278
1355
  };
1279
- result.context_pack = await safeCtmContextPack({
1280
- task: query,
1281
- query: sessionQuery,
1282
- limit: Math.min(limit, 5),
1283
- token_budget: 6000,
1284
- mode: 'compact',
1285
- include_raw: false,
1286
- });
1356
+ result.context_pack = contextPack;
1357
+ result.sources.ctm_context_pack = {
1358
+ searched: true,
1359
+ ok: contextPack?.ok !== false,
1360
+ source: contextPack?.source || 'ctm-db',
1361
+ reason: contextPack?.reason || '',
1362
+ error: contextPack?.error || '',
1363
+ timed_out: Boolean(contextPack?.timed_out),
1364
+ duration_ms: contextPack?.duration_ms,
1365
+ timeout_ms: contextPack?.timeout_ms,
1366
+ count: (contextPack?.messages || []).length,
1367
+ };
1287
1368
  }
1288
1369
 
1289
1370
  if (includeMemories) {
@@ -1293,12 +1374,28 @@ async function lookupWallEContext(args = {}) {
1293
1374
  since: args.since,
1294
1375
  until: args.until,
1295
1376
  };
1296
- const memorySearch = await searchWallEMemories({
1297
- query: memoryQuery,
1298
- limit,
1299
- filters,
1300
- hybrid: !Object.values(filters).some(Boolean),
1301
- });
1377
+ const memorySearch = await withMcpDeadline(
1378
+ () => searchWallEMemories({
1379
+ query: memoryQuery,
1380
+ limit,
1381
+ filters,
1382
+ hybrid: !Object.values(filters).some(Boolean),
1383
+ embeddingTimeoutMs: timeouts.embedding,
1384
+ }),
1385
+ timeouts.source,
1386
+ ({ duration_ms, timeout_ms }) => ({
1387
+ results: [],
1388
+ vector_search_used: false,
1389
+ source: sourceStatus('wall-e-memory', {
1390
+ ok: false,
1391
+ reason: 'wall_e_memory_search_timeout',
1392
+ error: `Wall-E memory search exceeded ${timeout_ms}ms`,
1393
+ timed_out: true,
1394
+ duration_ms,
1395
+ timeout_ms,
1396
+ }),
1397
+ })
1398
+ );
1302
1399
  result.memory_results = memorySearch.results.map(formatMemoryResult);
1303
1400
  result.sources.memories = {
1304
1401
  searched: true,
@@ -1308,7 +1405,11 @@ async function lookupWallEContext(args = {}) {
1308
1405
  source: memorySearch.source.source,
1309
1406
  reason: memorySearch.source.reason || '',
1310
1407
  error: memorySearch.source.error || '',
1408
+ timed_out: Boolean(memorySearch.source.timed_out),
1409
+ duration_ms: memorySearch.source.duration_ms,
1410
+ timeout_ms: memorySearch.source.timeout_ms,
1311
1411
  vector_search_used: memorySearch.vector_search_used,
1412
+ vector: memorySearch.source.vector || null,
1312
1413
  count: result.memory_results.length,
1313
1414
  };
1314
1415
  }
@@ -1416,7 +1517,7 @@ function safeBrainSearchMemories(args = {}) {
1416
1517
  }
1417
1518
  }
1418
1519
 
1419
- async function searchWallEMemories({ query, limit, filters = {}, hybrid = true, terms } = {}) {
1520
+ async function searchWallEMemories({ query, limit, filters = {}, hybrid = true, terms, embeddingTimeoutMs } = {}) {
1420
1521
  const base = safeBrainSearchMemories({ query, limit, terms, ...filters });
1421
1522
  let results = base.results;
1422
1523
  let vectorSearchUsed = false;
@@ -1425,7 +1526,19 @@ async function searchWallEMemories({ query, limit, filters = {}, hybrid = true,
1425
1526
  if (base.source.ok && hybrid && _embeddings && _embeddings.isAvailable()) {
1426
1527
  vectorStatus.attempted = true;
1427
1528
  try {
1428
- const queryEmb = await _embeddings.computeEmbedding(query);
1529
+ const queryEmb = await withMcpDeadline(
1530
+ () => _embeddings.computeEmbedding(query),
1531
+ clampMilliseconds(embeddingTimeoutMs, DEFAULT_MCP_LOOKUP_EMBEDDING_TIMEOUT_MS, MAX_MCP_LOOKUP_TIMEOUT_MS),
1532
+ ({ duration_ms, timeout_ms }) => {
1533
+ vectorStatus.ok = false;
1534
+ vectorStatus.reason = 'wall_e_vector_search_timeout';
1535
+ vectorStatus.error = `Embedding search exceeded ${timeout_ms}ms`;
1536
+ vectorStatus.timed_out = true;
1537
+ vectorStatus.duration_ms = duration_ms;
1538
+ vectorStatus.timeout_ms = timeout_ms;
1539
+ return null;
1540
+ }
1541
+ );
1429
1542
  if (queryEmb) {
1430
1543
  const vectorResults = _embeddings.searchSimilarMemories(queryEmb, limit);
1431
1544
  if (vectorResults.length > 0) {
@@ -1800,6 +1913,57 @@ function clampLimit(value, fallback, max) {
1800
1913
  return Math.min(Math.max(Math.trunc(n), 1), max);
1801
1914
  }
1802
1915
 
1916
+ function clampMilliseconds(value, fallback, max = MAX_MCP_LOOKUP_TIMEOUT_MS) {
1917
+ const n = Number(value || fallback);
1918
+ if (!Number.isFinite(n)) return fallback;
1919
+ return Math.min(Math.max(Math.trunc(n), 25), max);
1920
+ }
1921
+
1922
+ function lookupSourceTimeouts(args = {}) {
1923
+ const source = clampMilliseconds(
1924
+ args.source_timeout_ms || process.env.WALLE_MCP_LOOKUP_SOURCE_TIMEOUT_MS,
1925
+ DEFAULT_MCP_LOOKUP_SOURCE_TIMEOUT_MS
1926
+ );
1927
+ return {
1928
+ source,
1929
+ context_pack: Math.min(source, clampMilliseconds(
1930
+ args.context_pack_timeout_ms || process.env.WALLE_MCP_LOOKUP_CONTEXT_PACK_TIMEOUT_MS,
1931
+ DEFAULT_MCP_LOOKUP_CONTEXT_PACK_TIMEOUT_MS
1932
+ )),
1933
+ embedding: Math.min(source, clampMilliseconds(
1934
+ args.embedding_timeout_ms || process.env.WALLE_MCP_LOOKUP_EMBEDDING_TIMEOUT_MS,
1935
+ DEFAULT_MCP_LOOKUP_EMBEDDING_TIMEOUT_MS
1936
+ )),
1937
+ };
1938
+ }
1939
+
1940
+ async function withMcpDeadline(factory, timeoutMs, fallbackFactory) {
1941
+ const startedAt = Date.now();
1942
+ const timeout = clampMilliseconds(timeoutMs, DEFAULT_MCP_LOOKUP_SOURCE_TIMEOUT_MS);
1943
+ let timer;
1944
+ const timedOut = Symbol('mcp_deadline_timeout');
1945
+ const task = Promise.resolve().then(factory);
1946
+ task.catch(() => {});
1947
+ let raced;
1948
+ try {
1949
+ raced = await Promise.race([
1950
+ task,
1951
+ new Promise(resolve => {
1952
+ timer = setTimeout(() => resolve(timedOut), timeout);
1953
+ }),
1954
+ ]);
1955
+ } finally {
1956
+ if (timer) clearTimeout(timer);
1957
+ }
1958
+ if (raced !== timedOut) {
1959
+ return raced;
1960
+ }
1961
+ return fallbackFactory({
1962
+ duration_ms: Date.now() - startedAt,
1963
+ timeout_ms: timeout,
1964
+ });
1965
+ }
1966
+
1803
1967
  function formatMemoryResult(memory) {
1804
1968
  return {
1805
1969
  id: memory.id,