create-walle 0.9.0 → 0.9.3

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 (45) hide show
  1. package/README.md +35 -31
  2. package/package.json +3 -3
  3. package/template/CLAUDE.md +23 -1
  4. package/template/claude-task-manager/bin/restart-ctm.sh +3 -2
  5. package/template/claude-task-manager/db.js +38 -0
  6. package/template/claude-task-manager/public/css/walle.css +123 -0
  7. package/template/claude-task-manager/public/index.html +962 -69
  8. package/template/claude-task-manager/public/js/walle.js +374 -121
  9. package/template/claude-task-manager/public/prompts.html +84 -26
  10. package/template/claude-task-manager/public/walle-icon.svg +45 -0
  11. package/template/claude-task-manager/server.js +69 -4
  12. package/template/docs/openclaw-vs-walle-comparison.md +103 -0
  13. package/template/package.json +1 -1
  14. package/template/wall-e/agent.js +63 -3
  15. package/template/wall-e/api-walle.js +42 -0
  16. package/template/wall-e/brain.js +182 -5
  17. package/template/wall-e/channels/imessage-channel.js +4 -1
  18. package/template/wall-e/channels/slack-channel.js +3 -1
  19. package/template/wall-e/chat.js +106 -224
  20. package/template/wall-e/context/compactor.js +163 -0
  21. package/template/wall-e/context/context-builder.js +355 -0
  22. package/template/wall-e/context/state-snapshot.js +209 -0
  23. package/template/wall-e/context/token-counter.js +55 -0
  24. package/template/wall-e/context/topic-matcher.js +79 -0
  25. package/template/wall-e/core-tasks.js +24 -0
  26. package/template/wall-e/events/event-bus.js +23 -0
  27. package/template/wall-e/loops/ingest.js +4 -0
  28. package/template/wall-e/loops/initiative.js +316 -0
  29. package/template/wall-e/loops/tasks.js +55 -5
  30. package/template/wall-e/skills/_bundled/email-sync/run.js +3 -1
  31. package/template/wall-e/skills/_bundled/morning-briefing/run.js +41 -0
  32. package/template/wall-e/skills/_bundled/proactive-alerts/SKILL.md +20 -0
  33. package/template/wall-e/skills/_bundled/proactive-alerts/run.js +144 -0
  34. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +18 -0
  35. package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +4 -0
  36. package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +52 -0
  37. package/template/wall-e/skills/_bundled/slack-mentions/run.js +470 -0
  38. package/template/wall-e/skills/_bundled/weekly-reflection/SKILL.md +69 -0
  39. package/template/wall-e/tests/brain.test.js +4 -4
  40. package/template/wall-e/tests/compactor.test.js +323 -0
  41. package/template/wall-e/tests/context-builder.test.js +215 -0
  42. package/template/wall-e/tests/event-bus.test.js +74 -0
  43. package/template/wall-e/tests/initiative.test.js +354 -0
  44. package/template/wall-e/tests/proactive-alerts.test.js +140 -0
  45. package/template/wall-e/tests/session-persistence.test.js +335 -0
@@ -213,19 +213,22 @@
213
213
  .prompt-item.active { background: var(--bg-lighter); border-left-color: var(--accent); }
214
214
  .prompt-item.pinned { border-left-color: var(--yellow); }
215
215
  .prompt-item.active.pinned { border-left-color: var(--yellow); }
216
- .prompt-item.drag-over { border-top: 2px solid var(--accent); }
216
+ .prompt-item.drag-over-top { box-shadow: 0 -2px 0 0 var(--accent); }
217
+ .prompt-item.drag-over-bottom { box-shadow: 0 2px 0 0 var(--accent); }
218
+ .prompt-item.dragging { opacity: 0.35; }
217
219
  .prompt-item .drag-handle {
218
220
  cursor: grab;
219
221
  color: var(--fg-dim);
220
- opacity: 0;
221
- font-size: 10px;
222
- padding: 2px 0;
222
+ opacity: 0.25;
223
+ font-size: 12px;
224
+ padding: 4px 2px;
223
225
  flex-shrink: 0;
224
226
  user-select: none;
225
- transition: opacity 0.1s;
227
+ transition: opacity 0.15s, color 0.15s;
226
228
  }
227
- .prompt-item:hover .drag-handle { opacity: 0.5; }
228
- .prompt-item .drag-handle:hover { opacity: 1; }
229
+ .prompt-item:hover .drag-handle { opacity: 0.6; }
230
+ .prompt-item .drag-handle:hover { opacity: 1; color: var(--accent); }
231
+ .prompt-item .drag-handle:active { cursor: grabbing; }
229
232
  .prompt-item .prompt-content { flex: 1; min-width: 0; }
230
233
  .prompt-item .prompt-title {
231
234
  font-weight: 500;
@@ -288,17 +291,21 @@
288
291
  transition: all 0.1s;
289
292
  }
290
293
  .folder-item .folder-actions .act-btn:hover { background: var(--bg); color: var(--fg); }
291
- .folder-item.drag-over { border-top: 2px solid var(--accent); }
294
+ .folder-item.drag-over-top { box-shadow: 0 -2px 0 0 var(--accent); }
295
+ .folder-item.drag-over-bottom { box-shadow: 0 2px 0 0 var(--accent); }
296
+ .folder-item.dragging { opacity: 0.35; }
292
297
  .folder-item .drag-handle {
293
298
  cursor: grab;
294
299
  color: var(--fg-dim);
295
- opacity: 0;
296
- font-size: 10px;
300
+ opacity: 0.25;
301
+ font-size: 12px;
297
302
  flex-shrink: 0;
298
303
  user-select: none;
304
+ transition: opacity 0.15s, color 0.15s;
299
305
  }
300
- .folder-item:hover .drag-handle { opacity: 0.5; }
301
- .folder-item .drag-handle:hover { opacity: 1; }
306
+ .folder-item:hover .drag-handle { opacity: 0.6; }
307
+ .folder-item .drag-handle:hover { opacity: 1; color: var(--accent); }
308
+ .folder-item .drag-handle:active { cursor: grabbing; }
302
309
  .folder-rename-input {
303
310
  background: var(--bg);
304
311
  color: var(--fg);
@@ -2333,6 +2340,7 @@ function renderFolderTree() {
2333
2340
  html += `<div class="folder-item ${state.currentFolderId === f.id ? 'active' : ''}"
2334
2341
  data-folder-id="${f.id}" draggable="true"
2335
2342
  ondragstart="onFolderDragStart(event, ${f.id})"
2343
+ ondragend="onFolderDragEnd(event)"
2336
2344
  ondragover="onFolderDragOver(event)"
2337
2345
  ondragleave="onFolderDragLeave(event)"
2338
2346
  ondrop="onFolderDrop(event, ${f.id})"
@@ -2397,31 +2405,50 @@ function onFolderDragStart(e, id) {
2397
2405
  dragFolderId = id;
2398
2406
  e.dataTransfer.effectAllowed = 'move';
2399
2407
  e.dataTransfer.setData('text/plain', id);
2408
+ const item = e.target.closest('.folder-item');
2409
+ if (item) requestAnimationFrame(() => item.classList.add('dragging'));
2400
2410
  }
2401
2411
 
2402
2412
  function onFolderDragOver(e) {
2403
2413
  e.preventDefault();
2414
+ document.querySelectorAll('.folder-item.drag-over-top, .folder-item.drag-over-bottom').forEach(el => {
2415
+ el.classList.remove('drag-over-top', 'drag-over-bottom');
2416
+ });
2404
2417
  const item = e.target.closest('.folder-item[data-folder-id]');
2405
- if (item) item.classList.add('drag-over');
2418
+ if (item) {
2419
+ const rect = item.getBoundingClientRect();
2420
+ const isTop = e.clientY < rect.top + rect.height / 2;
2421
+ item.classList.add(isTop ? 'drag-over-top' : 'drag-over-bottom');
2422
+ }
2406
2423
  }
2407
2424
 
2408
2425
  function onFolderDragLeave(e) {
2409
2426
  const item = e.target.closest('.folder-item[data-folder-id]');
2410
- if (item) item.classList.remove('drag-over');
2427
+ if (item) item.classList.remove('drag-over-top', 'drag-over-bottom');
2411
2428
  }
2412
2429
 
2413
2430
  async function onFolderDrop(e, targetId) {
2414
2431
  e.preventDefault();
2415
- const item = e.target.closest('.folder-item[data-folder-id]');
2416
- if (item) item.classList.remove('drag-over');
2432
+ document.querySelectorAll('.folder-item.drag-over-top, .folder-item.drag-over-bottom, .folder-item.dragging').forEach(el => {
2433
+ el.classList.remove('drag-over-top', 'drag-over-bottom', 'dragging');
2434
+ });
2417
2435
  if (dragFolderId === null || dragFolderId === targetId) return;
2418
2436
 
2437
+ const item = e.target.closest('.folder-item[data-folder-id]');
2438
+ let dropAfter = false;
2439
+ if (item) {
2440
+ const rect = item.getBoundingClientRect();
2441
+ dropAfter = e.clientY >= rect.top + rect.height / 2;
2442
+ }
2443
+
2419
2444
  const ids = state.folders.map(f => f.id);
2420
2445
  const fromIdx = ids.indexOf(dragFolderId);
2421
- const toIdx = ids.indexOf(targetId);
2446
+ let toIdx = ids.indexOf(targetId);
2422
2447
  if (fromIdx === -1 || toIdx === -1) return;
2423
2448
 
2424
2449
  ids.splice(fromIdx, 1);
2450
+ toIdx = ids.indexOf(targetId);
2451
+ if (dropAfter) toIdx++;
2425
2452
  ids.splice(toIdx, 0, dragFolderId);
2426
2453
 
2427
2454
  await fetch(API('/folders/reorder'), {
@@ -2433,6 +2460,12 @@ async function onFolderDrop(e, targetId) {
2433
2460
  dragFolderId = null;
2434
2461
  }
2435
2462
 
2463
+ function onFolderDragEnd(e) {
2464
+ document.querySelectorAll('.folder-item.dragging, .folder-item.drag-over-top, .folder-item.drag-over-bottom').forEach(el => {
2465
+ el.classList.remove('dragging', 'drag-over-top', 'drag-over-bottom');
2466
+ });
2467
+ }
2468
+
2436
2469
  function renderPromptList() {
2437
2470
  const list = document.getElementById('prompt-list');
2438
2471
  if (state.prompts.length === 0) {
@@ -2448,6 +2481,7 @@ function renderPromptList() {
2448
2481
  return `<div class="prompt-item ${isActive ? 'active' : ''} ${p.starred ? 'starred' : ''} ${isPinned ? 'pinned' : ''}"
2449
2482
  data-id="${p.id}" draggable="true"
2450
2483
  ondragstart="onPromptDragStart(event, ${p.id})"
2484
+ ondragend="onPromptDragEnd(event)"
2451
2485
  ondragover="onPromptDragOver(event)"
2452
2486
  ondragleave="onPromptDragLeave(event)"
2453
2487
  ondrop="onPromptDrop(event, ${p.id})"
@@ -2511,38 +2545,56 @@ function onPromptDragStart(e, id) {
2511
2545
  dragPromptId = id;
2512
2546
  e.dataTransfer.effectAllowed = 'move';
2513
2547
  e.dataTransfer.setData('text/plain', id);
2514
- e.target.style.opacity = '0.5';
2515
- setTimeout(() => { e.target.style.opacity = ''; }, 0);
2548
+ const item = e.target.closest('.prompt-item');
2549
+ if (item) requestAnimationFrame(() => item.classList.add('dragging'));
2516
2550
  }
2517
2551
 
2518
2552
  function onPromptDragOver(e) {
2519
2553
  e.preventDefault();
2520
2554
  e.dataTransfer.dropEffect = 'move';
2555
+ // Clear previous indicators
2556
+ document.querySelectorAll('.prompt-item.drag-over-top, .prompt-item.drag-over-bottom').forEach(el => {
2557
+ el.classList.remove('drag-over-top', 'drag-over-bottom');
2558
+ });
2521
2559
  const item = e.target.closest('.prompt-item');
2522
- if (item) item.classList.add('drag-over');
2560
+ if (item) {
2561
+ const rect = item.getBoundingClientRect();
2562
+ const isTop = e.clientY < rect.top + rect.height / 2;
2563
+ item.classList.add(isTop ? 'drag-over-top' : 'drag-over-bottom');
2564
+ }
2523
2565
  }
2524
2566
 
2525
2567
  function onPromptDragLeave(e) {
2526
2568
  const item = e.target.closest('.prompt-item');
2527
- if (item) item.classList.remove('drag-over');
2569
+ if (item) item.classList.remove('drag-over-top', 'drag-over-bottom');
2528
2570
  }
2529
2571
 
2530
2572
  async function onPromptDrop(e, targetId) {
2531
2573
  e.preventDefault();
2532
- const item = e.target.closest('.prompt-item');
2533
- if (item) item.classList.remove('drag-over');
2574
+ document.querySelectorAll('.prompt-item.drag-over-top, .prompt-item.drag-over-bottom, .prompt-item.dragging').forEach(el => {
2575
+ el.classList.remove('drag-over-top', 'drag-over-bottom', 'dragging');
2576
+ });
2534
2577
  if (dragPromptId === null || dragPromptId === targetId) return;
2535
2578
 
2536
- // Reorder in local state
2579
+ // Determine if dropping above or below target
2580
+ const item = e.target.closest('.prompt-item');
2581
+ let dropAfter = false;
2582
+ if (item) {
2583
+ const rect = item.getBoundingClientRect();
2584
+ dropAfter = e.clientY >= rect.top + rect.height / 2;
2585
+ }
2586
+
2537
2587
  const ids = state.prompts.map(p => p.id);
2538
2588
  const fromIdx = ids.indexOf(dragPromptId);
2539
- const toIdx = ids.indexOf(targetId);
2589
+ let toIdx = ids.indexOf(targetId);
2540
2590
  if (fromIdx === -1 || toIdx === -1) return;
2541
2591
 
2542
2592
  ids.splice(fromIdx, 1);
2593
+ // Adjust index after removal
2594
+ toIdx = ids.indexOf(targetId);
2595
+ if (dropAfter) toIdx++;
2543
2596
  ids.splice(toIdx, 0, dragPromptId);
2544
2597
 
2545
- // Save to server
2546
2598
  await fetch(API('/prompts/reorder'), {
2547
2599
  method: 'POST',
2548
2600
  headers: { 'Content-Type': 'application/json' },
@@ -2552,6 +2604,12 @@ async function onPromptDrop(e, targetId) {
2552
2604
  dragPromptId = null;
2553
2605
  }
2554
2606
 
2607
+ function onPromptDragEnd(e) {
2608
+ document.querySelectorAll('.prompt-item.dragging, .prompt-item.drag-over-top, .prompt-item.drag-over-bottom').forEach(el => {
2609
+ el.classList.remove('dragging', 'drag-over-top', 'drag-over-bottom');
2610
+ });
2611
+ }
2612
+
2555
2613
  function selectFolder(id) {
2556
2614
  state.currentFolderId = id;
2557
2615
  renderFolderTree();
@@ -0,0 +1,45 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120">
2
+ <defs>
3
+ <linearGradient id="body" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#e0af68"/>
5
+ <stop offset="100%" stop-color="#d49a4e"/>
6
+ </linearGradient>
7
+ <linearGradient id="eye-bg" x1="0%" y1="0%" x2="0%" y2="100%">
8
+ <stop offset="0%" stop-color="#c0caf5"/>
9
+ <stop offset="100%" stop-color="#a9b1d6"/>
10
+ </linearGradient>
11
+ </defs>
12
+ <!-- Body/head - compact cube shape -->
13
+ <rect x="20" y="38" width="80" height="58" rx="10" fill="url(#body)"/>
14
+ <!-- Treads -->
15
+ <rect x="14" y="92" width="36" height="16" rx="8" fill="#565f89"/>
16
+ <rect x="70" y="92" width="36" height="16" rx="8" fill="#565f89"/>
17
+ <!-- Tread detail lines -->
18
+ <line x1="22" y1="100" x2="42" y2="100" stroke="#3b4261" stroke-width="1.5" stroke-linecap="round"/>
19
+ <line x1="78" y1="100" x2="98" y2="100" stroke="#3b4261" stroke-width="1.5" stroke-linecap="round"/>
20
+ <!-- Binocular eyes - the iconic WALL-E feature -->
21
+ <!-- Left eye housing -->
22
+ <circle cx="44" cy="42" r="18" fill="#565f89"/>
23
+ <circle cx="44" cy="42" r="14" fill="url(#eye-bg)"/>
24
+ <circle cx="44" cy="42" r="8" fill="#1a1b26"/>
25
+ <circle cx="44" cy="39" r="3" fill="#7aa2f7" opacity="0.9"/>
26
+ <!-- Right eye housing -->
27
+ <circle cx="76" cy="42" r="18" fill="#565f89"/>
28
+ <circle cx="76" cy="42" r="14" fill="url(#eye-bg)"/>
29
+ <circle cx="76" cy="42" r="8" fill="#1a1b26"/>
30
+ <circle cx="76" cy="39" r="3" fill="#7aa2f7" opacity="0.9"/>
31
+ <!-- Eye bridge/connector -->
32
+ <rect x="56" y="36" width="8" height="12" rx="2" fill="#565f89"/>
33
+ <!-- Neck/periscope -->
34
+ <rect x="56" y="24" width="8" height="16" rx="2" fill="#565f89"/>
35
+ <!-- Solar panel detail on body -->
36
+ <rect x="30" y="56" width="60" height="4" rx="2" fill="#c49340" opacity="0.6"/>
37
+ <rect x="30" y="64" width="60" height="4" rx="2" fill="#c49340" opacity="0.6"/>
38
+ <rect x="30" y="72" width="60" height="4" rx="2" fill="#c49340" opacity="0.6"/>
39
+ <!-- Arms (small) -->
40
+ <rect x="8" y="58" width="14" height="6" rx="3" fill="#565f89"/>
41
+ <rect x="98" y="58" width="14" height="6" rx="3" fill="#565f89"/>
42
+ <!-- Hand grippers -->
43
+ <circle cx="8" cy="61" r="4" fill="#3b4261"/>
44
+ <circle cx="112" cy="61" r="4" fill="#3b4261"/>
45
+ </svg>
@@ -2112,7 +2112,8 @@ function handleCreate(ws, msg) {
2112
2112
  // If a session with this ID is still alive (e.g. resume of a stale entry), attach instead
2113
2113
  const existing = sessions.get(id);
2114
2114
  if (existing && existing.ptyProcess) {
2115
- return handleAttach(ws, { id });
2115
+ if (ws) return handleAttach(ws, { id });
2116
+ return; // No client to attach — session already running
2116
2117
  }
2117
2118
 
2118
2119
  const cwd = (msg.cwd || process.env.HOME).replace(/^~/, process.env.HOME);
@@ -2158,7 +2159,7 @@ function handleCreate(ws, msg) {
2158
2159
  cols, rows, cwd, env,
2159
2160
  });
2160
2161
  } catch (e) {
2161
- ws.send(JSON.stringify({ type: 'error', message: `Failed to spawn: ${e.message}` }));
2162
+ if (ws) ws.send(JSON.stringify({ type: 'error', message: `Failed to spawn: ${e.message}` }));
2162
2163
  return;
2163
2164
  }
2164
2165
 
@@ -2170,7 +2171,7 @@ function handleCreate(ws, msg) {
2170
2171
  cwd,
2171
2172
  pid: ptyProcess.pid,
2172
2173
  ptyProcess,
2173
- clients: [ws],
2174
+ clients: ws ? [ws] : [],
2174
2175
  scrollback: [],
2175
2176
  createdAt: Date.now(),
2176
2177
  lastActivity: Date.now(),
@@ -2178,6 +2179,9 @@ function handleCreate(ws, msg) {
2178
2179
 
2179
2180
  sessions.set(id, session);
2180
2181
 
2182
+ // Persist to startup_tasks for crash-safe restore
2183
+ try { dbModule.addStartupTask(id, label, cmd, args, cwd); } catch {}
2184
+
2181
2185
  ptyProcess.onData((data) => {
2182
2186
  session.lastActivity = Date.now();
2183
2187
  // Only strip \e[3J (Erase Scrollback) — it destroys scroll history on replay.
@@ -2208,6 +2212,7 @@ function handleCreate(ws, msg) {
2208
2212
  if (client.readyState === 1) client.send(payload);
2209
2213
  }
2210
2214
  sessions.delete(id);
2215
+ try { dbModule.removeStartupTask(id); } catch {}
2211
2216
  queueEngine.onSessionExit(id);
2212
2217
  cleanAutoApprovalBuffer(id);
2213
2218
  cleanIdleNotify(id);
@@ -2222,7 +2227,7 @@ function handleCreate(ws, msg) {
2222
2227
  dbModule.setSessionTitle(id, label, false);
2223
2228
  }
2224
2229
 
2225
- ws.send(JSON.stringify({ type: 'created', id, pid: ptyProcess.pid, label, cwd }));
2230
+ if (ws) ws.send(JSON.stringify({ type: 'created', id, pid: ptyProcess.pid, label, cwd }));
2226
2231
  broadcastSessionList();
2227
2232
  }
2228
2233
 
@@ -2369,6 +2374,7 @@ setInterval(async () => {
2369
2374
  // --- Graceful Shutdown ---
2370
2375
  function shutdown() {
2371
2376
  console.log('\n Shutting down...');
2377
+ // startup_tasks table is already up-to-date (written in real-time), no save needed
2372
2378
  dbModule.checkpointWal();
2373
2379
  dbModule.closeDb();
2374
2380
  for (const [id, session] of sessions) {
@@ -2402,7 +2408,10 @@ function apiServicesStatus(req, res) {
2402
2408
  });
2403
2409
  }
2404
2410
 
2411
+ let _walleIntentionallyStopped = false;
2412
+
2405
2413
  function apiStopWalle(req, res) {
2414
+ _walleIntentionallyStopped = true;
2406
2415
  execFile('lsof', ['-ti', ':' + WALLE_PORT], (err, stdout) => {
2407
2416
  const pids = (stdout || '').trim().split('\n').filter(Boolean);
2408
2417
  if (pids.length === 0) {
@@ -2419,6 +2428,7 @@ function apiStopWalle(req, res) {
2419
2428
  }
2420
2429
 
2421
2430
  function apiStartWalle(req, res) {
2431
+ _walleIntentionallyStopped = false;
2422
2432
  const walleDir = path.join(__dirname, '..', 'wall-e');
2423
2433
  const agentScript = path.join(walleDir, 'agent.js');
2424
2434
  // Check if already running
@@ -2459,6 +2469,18 @@ function _restartWalleQuiet() {
2459
2469
  }
2460
2470
 
2461
2471
  function apiRestartCtm(req, res) {
2472
+ const reqUrl = new URL(req.url, 'http://localhost');
2473
+ const force = reqUrl.searchParams.get('force') === 'true';
2474
+ const activeCount = sessions.size;
2475
+ if (activeCount > 0 && !force) {
2476
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2477
+ return res.end(JSON.stringify({
2478
+ ok: false,
2479
+ error: `Blocked: ${activeCount} active session(s) would be killed. Use ?force=true to override, or use the dev instance (bash bin/dev.sh) for testing.`,
2480
+ active_sessions: activeCount
2481
+ }));
2482
+ }
2483
+
2462
2484
  res.writeHead(200, { 'Content-Type': 'application/json' });
2463
2485
  res.end(JSON.stringify({ ok: true, message: 'CTM server restarting...' }));
2464
2486
 
@@ -2605,6 +2627,30 @@ const setup = require('../bin/setup');
2605
2627
  // Auto-detect owner and create .env if missing (no interactive prompts)
2606
2628
  setup.runIfNeeded();
2607
2629
 
2630
+ // Restore sessions BEFORE listening — prevents race where clients connect
2631
+ // and get an empty session list before startup_tasks sessions are restored.
2632
+ try {
2633
+ const tasks = dbModule.listStartupTasks();
2634
+ if (tasks.length > 0) {
2635
+ console.log(` Restoring ${tasks.length} session(s)...`);
2636
+ // Clear table first — handleCreate will re-insert each session
2637
+ dbModule.clearStartupTasks();
2638
+ for (const t of tasks) {
2639
+ try {
2640
+ handleCreate(null, {
2641
+ id: t.session_id, label: t.label, cmd: t.cmd,
2642
+ args: t.args, cwd: t.cwd,
2643
+ });
2644
+ console.log(` ✓ ${t.label || t.session_id.slice(0, 8)}`);
2645
+ } catch (e) {
2646
+ console.log(` ✗ ${t.label}: ${e.message}`);
2647
+ }
2648
+ }
2649
+ }
2650
+ } catch (e) {
2651
+ console.log(` Failed to restore sessions: ${e.message}`);
2652
+ }
2653
+
2608
2654
  server.listen(PORT, HOST, () => {
2609
2655
  console.log(`\n Claude Task Manager running at:`);
2610
2656
  console.log(` Local: http://localhost:${PORT}/`);
@@ -2617,4 +2663,23 @@ server.listen(PORT, HOST, () => {
2617
2663
  } else {
2618
2664
  console.log('');
2619
2665
  }
2666
+
2667
+ // Wall-E watchdog — auto-restart if it dies unexpectedly.
2668
+ // _walleIntentionallyStopped is set by apiStopWalle, cleared by apiStartWalle.
2669
+ setInterval(() => {
2670
+ if (_walleIntentionallyStopped) return;
2671
+ execFile('lsof', ['-ti', ':' + WALLE_PORT], (err, stdout) => {
2672
+ const pids = (stdout || '').trim().split('\n').filter(Boolean);
2673
+ if (pids.length > 0) return; // Still alive
2674
+ // Wall-E is not running and wasn't intentionally stopped — restart
2675
+ const walleDir = path.join(__dirname, '..', 'wall-e');
2676
+ const agentScript = path.join(walleDir, 'agent.js');
2677
+ const child = require('child_process').spawn(
2678
+ process.execPath, [agentScript],
2679
+ { cwd: walleDir, detached: true, stdio: 'ignore', env: { ...process.env, WALL_E_PORT: String(WALLE_PORT) } }
2680
+ );
2681
+ child.unref();
2682
+ console.log('[watchdog] Wall-E died unexpectedly, restarted (PID ' + child.pid + ')');
2683
+ });
2684
+ }, 30000);
2620
2685
  });
@@ -0,0 +1,103 @@
1
+ # OpenClaw vs. Wall-E: Intelligence Comparison
2
+
3
+ *Date: 2026-04-03*
4
+
5
+ ## The Fundamental Architectural Difference
6
+
7
+ **OpenClaw is a *routing plane*. Wall-E is a *cognitive loop*.**
8
+
9
+ They solve different problems, and that shapes everything about how "intelligent" they feel.
10
+
11
+ | | OpenClaw | Wall-E |
12
+ |---|---|---|
13
+ | **Core metaphor** | Gateway / message router | Digital twin / brain |
14
+ | **Intelligence source** | Claude (or any LLM) on every turn | Claude for extraction + pre-computed brain state |
15
+ | **Proactivity** | Cron jobs -> spawn agent turn | Ingest/think/reflect loops + skill planner |
16
+ | **Memory** | Vector embeddings + session transcripts | SPO knowledge triples + raw memories + FTS |
17
+ | **Identity** | Configurable name/emoji prefix | Deep identity from 28k+ ingested memories |
18
+ | **Channels** | 23+ messaging platforms | iMessage, Slack DM, CTM chat |
19
+ | **Architecture** | ~200k+ lines TypeScript monorepo | ~10k lines Node.js |
20
+
21
+ ## Where OpenClaw Feels "Smarter"
22
+
23
+ ### 1. The Gateway is the brain -- every message gets a full Claude turn
24
+
25
+ OpenClaw's Pi agent runtime (`pi-embedded-runner.ts`) runs a full LLM call for every single incoming message. The agent has access to tools (bash, file read/write, web search, canvas, image gen, cron management, subagent spawning, session management) and Claude decides what to do. This means:
26
+
27
+ - It can reason about *anything* in real-time
28
+ - It can chain tool calls naturally (search -> read -> respond)
29
+ - It doesn't need pre-computed knowledge -- it reasons on the fly
30
+
31
+ Wall-E, by contrast, pre-computes knowledge in batch loops (think every 2 min, reflect every hour). The chat handler builds a static system prompt from brain state. This means Wall-E's "intelligence" at chat time is limited to what's already been extracted and stored.
32
+
33
+ ### 2. Subagent orchestration
34
+
35
+ OpenClaw can spawn child agents (`subagent-spawn.ts`) -- isolated sessions with their own tools, models, and timeouts. This enables multi-step autonomous workflows where one agent delegates to another. Wall-E has no equivalent -- it's a single-threaded daemon.
36
+
37
+ ### 3. Cron = proactive muscle
38
+
39
+ OpenClaw's cron system (`server-cron.ts`) spawns full agent turns on a schedule. The agent gets the same tools as a chat turn -- so a cron job can browse the web, check APIs, write files, and send messages autonomously.
40
+
41
+ Wall-E's `runDueSkills()` is similar in concept but far simpler -- it runs predefined prompt templates through Claude tool-use. The prompts are static, not adaptive.
42
+
43
+ ### 4. Compaction and context management
44
+
45
+ OpenClaw has a sophisticated compaction system (`compaction.ts`) that progressively summarizes conversation history, preserves identifiers and decisions, drops old chunks first, and handles tool-result pairing. This means long conversations stay coherent.
46
+
47
+ Wall-E has no compaction -- chat context is built fresh each time from brain state. Long conversations will eventually hit token limits and simply fail.
48
+
49
+ ### 5. Memory search with embeddings
50
+
51
+ OpenClaw's `memory-search.ts` uses hybrid search: vector embeddings + full-text with MMR (Maximal Marginal Relevance) and temporal decay. This means it can find semantically relevant memories, not just keyword matches.
52
+
53
+ Wall-E uses BM25 full-text search only (via SQLite FTS5). Good for exact matches, but misses semantic connections.
54
+
55
+ ## Where Wall-E Has Unique Strengths
56
+
57
+ ### 1. Deep identity model
58
+
59
+ Wall-E has something OpenClaw doesn't: a *persistent cognitive identity*. The knowledge triples (subject-predicate-object), relationship graph with trust levels, behavioral patterns, and 28k+ memories create a model of *who Juncao is*. The system prompt in `chat.js` includes actual Slack messages as voice samples, people interaction summaries, and topic frequencies.
60
+
61
+ OpenClaw's identity is a configurable name/emoji + optional SOUL.md persona file. It has no self-model.
62
+
63
+ ### 2. The think loop is genuine cognition
64
+
65
+ Wall-E's `think.js` does something unique: it takes raw memories and *extracts structured knowledge* (SPO triples), detects contradictions between new and existing knowledge, and generates questions when it's uncertain. This is closer to how a brain actually learns -- not just retrieving, but *processing and integrating*.
66
+
67
+ OpenClaw has no equivalent batch learning process.
68
+
69
+ ### 3. Autonomy tiers
70
+
71
+ Wall-E's graduated trust system (Tier 1-4 per domain, with demotion on rejection) is a thoughtful approach to autonomous action. OpenClaw has approval mechanisms, but they're per-tool policy, not a learned trust model.
72
+
73
+ ## Why Wall-E Feels "Pulled Back"
74
+
75
+ ### 1. Static system prompt, no dynamic reasoning chain
76
+
77
+ Wall-E builds one big system prompt with everything pre-loaded (knowledge, people, memories, skills). Claude sees a snapshot, not a live workspace. OpenClaw's agent gets tools and *discovers* what it needs on each turn.
78
+
79
+ ### 2. No autonomous action loop
80
+
81
+ Wall-E's daemon loops (ingest/think/reflect) are *passive processing*. They crunch data but never take initiative. The skill planner runs due skills on a timer, but the skills are pre-defined prompt templates. There's no "look at what's happening and decide to do something" loop.
82
+
83
+ OpenClaw's cron + subagent spawning means it can proactively decide "it's Monday morning, let me check your calendar, summarize your Slack, and send you a briefing."
84
+
85
+ ### 3. No multi-turn tool orchestration
86
+
87
+ Wall-E's chat does tool-use (search_memories, run_skill, mcp_call), but each chat turn is essentially independent. There's no concept of a long-running task that spans multiple turns. OpenClaw's session model preserves full conversation state and allows agent turns to build on each other.
88
+
89
+ ### 4. No event-driven triggers
90
+
91
+ Wall-E runs on fixed timers. OpenClaw can react to webhooks, channel events, and cron triggers. If someone mentions you in Slack, OpenClaw can immediately spawn a response. Wall-E would only notice on its next poll cycle.
92
+
93
+ ## Key Upgrades to Make Wall-E More Proactive
94
+
95
+ 1. **Event-driven wake**: Instead of polling every 60s, react to channel events immediately.
96
+ 2. **Autonomous reasoning turn**: Periodic "what should I do?" turn where Wall-E decides whether to take action.
97
+ 3. **Persistent conversation context**: Maintain session state with compaction instead of fresh system prompt each time.
98
+ 4. **Dynamic tool discovery**: Let Wall-E discover what it needs via tool calls instead of pre-loading everything.
99
+ 5. **Background tasks / subagents**: Spawn long-running tasks that run independently.
100
+
101
+ ## Summary
102
+
103
+ OpenClaw is a mature *infrastructure* project -- it solves the "connect to everything, route to Claude, let Claude figure it out" problem extremely well. Wall-E is a more ambitious *cognitive* project -- it's trying to actually *be* you, not just respond on your behalf. The gap is that Wall-E has the brain but not the muscles. It knows you deeply but doesn't act on that knowledge. The fix isn't to copy OpenClaw's architecture wholesale, but to give Wall-E's brain an *agency layer* -- the ability to decide, act, and iterate autonomously.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "walle",
3
- "version": "0.9.0",
3
+ "version": "0.9.3",
4
4
  "private": true,
5
5
  "description": "Wall-E — your personal digital twin",
6
6
  "scripts": {
@@ -12,6 +12,14 @@ const { chat: walleChat } = require('./chat');
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
 
15
+ // Prevent unhandled errors from silently killing the daemon
16
+ process.on('uncaughtException', (err) => {
17
+ console.error('[wall-e] UNCAUGHT EXCEPTION (keeping alive):', err);
18
+ });
19
+ process.on('unhandledRejection', (reason) => {
20
+ console.error('[wall-e] UNHANDLED REJECTION (keeping alive):', reason);
21
+ });
22
+
15
23
  const CONFIG_PATH = path.join(__dirname, 'wall-e-config.json');
16
24
  const MIN_INTERVAL_MS = 10000; // 10 seconds minimum
17
25
  const MAX_INTERVAL_MS = 3600000; // 1 hour maximum
@@ -21,6 +29,7 @@ let ingestRunning = false;
21
29
  let thinkRunning = false;
22
30
  let reflectRunning = false;
23
31
  let skillsRunning = false;
32
+ let initiativeRunning = false;
24
33
  let shuttingDown = false;
25
34
 
26
35
  function loadOrCreateConfig() {
@@ -284,7 +293,54 @@ async function main() {
284
293
  }
285
294
  }, taskMs);
286
295
 
287
- console.log(`[wall-e] Daemon running. Ingest every ${ingestMs / 1000}s, Think every ${thinkMs / 1000}s, Reflect every ${reflectMs / 1000}s, Skills every ${skillMs / 1000}s, Tasks every ${taskMs / 1000}s.`);
296
+ // Initiative loop autonomous reasoning
297
+ const initiativeMs = clampInterval(config.intervals?.initiative_ms, 300000); // 5min default
298
+ const initiativeLoop = setInterval(async () => {
299
+ if (initiativeRunning || shuttingDown) return;
300
+ initiativeRunning = true;
301
+ try {
302
+ const { runInitiativeLoop } = require('./loops/initiative');
303
+ const result = await runInitiativeLoop();
304
+ if (result.decision !== 'noop' && result.decision !== 'observed') {
305
+ console.log(`[wall-e] Initiative: ${result.decision} — ${result.reasoning?.slice(0, 100)}`);
306
+ }
307
+ } catch (err) {
308
+ console.error('[wall-e] Initiative error:', err.message);
309
+ } finally {
310
+ initiativeRunning = false;
311
+ }
312
+ }, initiativeMs);
313
+
314
+ // Event-driven initiative wake — respond to important events immediately
315
+ const eventBus = require('./events/event-bus');
316
+ let lastInitiativeWake = 0;
317
+ const INITIATIVE_DEBOUNCE_MS = 30000; // Don't wake more than once per 30s
318
+
319
+ function wakeInitiative(reason) {
320
+ const now = Date.now();
321
+ if (now - lastInitiativeWake < INITIATIVE_DEBOUNCE_MS) return;
322
+ if (initiativeRunning || shuttingDown) return;
323
+ lastInitiativeWake = now;
324
+
325
+ console.log(`[wall-e] Initiative wake: ${reason}`);
326
+ initiativeRunning = true;
327
+ const { runInitiativeLoop } = require('./loops/initiative');
328
+ runInitiativeLoop({ trigger: reason }).then(result => {
329
+ if (result.decision !== 'noop' && result.decision !== 'observed') {
330
+ console.log(`[wall-e] Initiative (event): ${result.decision} — ${result.reasoning?.slice(0, 100)}`);
331
+ }
332
+ }).catch(err => {
333
+ console.error('[wall-e] Initiative wake error:', err.message);
334
+ }).finally(() => {
335
+ initiativeRunning = false;
336
+ });
337
+ }
338
+
339
+ eventBus.on('message', (evt) => wakeInitiative(`message from ${evt.channel}`));
340
+ eventBus.on('high_importance', (evt) => wakeInitiative('high importance memory'));
341
+ eventBus.on('webhook', (evt) => wakeInitiative(`webhook from ${evt.source}`));
342
+
343
+ console.log(`[wall-e] Daemon running. Ingest every ${ingestMs / 1000}s, Think every ${thinkMs / 1000}s, Reflect every ${reflectMs / 1000}s, Skills every ${skillMs / 1000}s, Tasks every ${taskMs / 1000}s, Initiative every ${initiativeMs / 1000}s.`);
288
344
  console.log('[wall-e] Press Ctrl+C to stop.');
289
345
 
290
346
  // Graceful shutdown — wait for in-flight operations
@@ -297,6 +353,10 @@ async function main() {
297
353
  clearInterval(reflectLoop);
298
354
  clearInterval(skillsLoop);
299
355
  clearInterval(tasksLoop);
356
+ clearInterval(initiativeLoop);
357
+
358
+ // Remove event listeners
359
+ eventBus.removeAllListeners();
300
360
 
301
361
  // Stop all channels
302
362
  for (const ch of channels) {
@@ -305,10 +365,10 @@ async function main() {
305
365
 
306
366
  // Wait up to 5s for in-flight operations
307
367
  const deadline = Date.now() + 5000;
308
- while ((ingestRunning || thinkRunning || reflectRunning || skillsRunning || tasksRunning) && Date.now() < deadline) {
368
+ while ((ingestRunning || thinkRunning || reflectRunning || skillsRunning || tasksRunning || initiativeRunning) && Date.now() < deadline) {
309
369
  await new Promise(r => setTimeout(r, 100));
310
370
  }
311
- if (ingestRunning || thinkRunning || reflectRunning || skillsRunning) {
371
+ if (ingestRunning || thinkRunning || reflectRunning || skillsRunning || tasksRunning || initiativeRunning) {
312
372
  console.warn('[wall-e] Force closing with operations still in-flight');
313
373
  }
314
374