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.
- package/README.md +35 -31
- package/package.json +3 -3
- package/template/CLAUDE.md +23 -1
- package/template/claude-task-manager/bin/restart-ctm.sh +3 -2
- package/template/claude-task-manager/db.js +38 -0
- package/template/claude-task-manager/public/css/walle.css +123 -0
- package/template/claude-task-manager/public/index.html +962 -69
- package/template/claude-task-manager/public/js/walle.js +374 -121
- package/template/claude-task-manager/public/prompts.html +84 -26
- package/template/claude-task-manager/public/walle-icon.svg +45 -0
- package/template/claude-task-manager/server.js +69 -4
- package/template/docs/openclaw-vs-walle-comparison.md +103 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +63 -3
- package/template/wall-e/api-walle.js +42 -0
- package/template/wall-e/brain.js +182 -5
- package/template/wall-e/channels/imessage-channel.js +4 -1
- package/template/wall-e/channels/slack-channel.js +3 -1
- package/template/wall-e/chat.js +106 -224
- package/template/wall-e/context/compactor.js +163 -0
- package/template/wall-e/context/context-builder.js +355 -0
- package/template/wall-e/context/state-snapshot.js +209 -0
- package/template/wall-e/context/token-counter.js +55 -0
- package/template/wall-e/context/topic-matcher.js +79 -0
- package/template/wall-e/core-tasks.js +24 -0
- package/template/wall-e/events/event-bus.js +23 -0
- package/template/wall-e/loops/ingest.js +4 -0
- package/template/wall-e/loops/initiative.js +316 -0
- package/template/wall-e/loops/tasks.js +55 -5
- package/template/wall-e/skills/_bundled/email-sync/run.js +3 -1
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +41 -0
- package/template/wall-e/skills/_bundled/proactive-alerts/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/proactive-alerts/run.js +144 -0
- package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +18 -0
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +4 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +52 -0
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +470 -0
- package/template/wall-e/skills/_bundled/weekly-reflection/SKILL.md +69 -0
- package/template/wall-e/tests/brain.test.js +4 -4
- package/template/wall-e/tests/compactor.test.js +323 -0
- package/template/wall-e/tests/context-builder.test.js +215 -0
- package/template/wall-e/tests/event-bus.test.js +74 -0
- package/template/wall-e/tests/initiative.test.js +354 -0
- package/template/wall-e/tests/proactive-alerts.test.js +140 -0
- 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 {
|
|
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:
|
|
222
|
-
padding: 2px
|
|
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.
|
|
227
|
+
transition: opacity 0.15s, color 0.15s;
|
|
226
228
|
}
|
|
227
|
-
.prompt-item:hover .drag-handle { opacity: 0.
|
|
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 {
|
|
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:
|
|
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.
|
|
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)
|
|
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
|
-
|
|
2416
|
-
|
|
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
|
-
|
|
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.
|
|
2515
|
-
|
|
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)
|
|
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
|
-
|
|
2533
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
package/template/package.json
CHANGED
package/template/wall-e/agent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|