create-walle 0.9.5 → 0.9.7
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 +3 -5
- package/package.json +1 -1
- package/template/claude-task-manager/public/index.html +3 -2
- package/template/package.json +1 -1
- package/template/wall-e/api-walle.js +26 -5
- package/template/wall-e/chat.js +2 -2
- package/template/wall-e/loops/tasks.js +154 -3
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +79 -55
- package/template/website/index.html +5 -5
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ An always-on AI agent that learns from your Slack, email, calendar, and coding s
|
|
|
31
31
|
## Install
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npx create-walle install ./
|
|
34
|
+
npx create-walle install ./walle
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
This copies the project, installs dependencies, auto-detects your name and timezone, and starts the server. Open **http://localhost:3456** to finish setup in the browser.
|
|
@@ -60,7 +60,7 @@ On first launch, the browser setup page guides you through:
|
|
|
60
60
|
## Custom Port
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
CTM_PORT=5000 npx create-walle install ./
|
|
63
|
+
CTM_PORT=5000 npx create-walle install ./walle
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Wall-E runs on `CTM_PORT + 1` (e.g., 5001).
|
|
@@ -76,9 +76,7 @@ Everything runs locally. CTM serves the dashboard, Wall-E runs as a background a
|
|
|
76
76
|
|
|
77
77
|
## Links
|
|
78
78
|
|
|
79
|
-
- [
|
|
80
|
-
- [Configuration Reference](https://walle.sh/docs/guides/configuration/)
|
|
81
|
-
- [Skill Catalog](https://walle.sh/docs/skills/)
|
|
79
|
+
- [Homepage](https://walle.sh)
|
|
82
80
|
|
|
83
81
|
## License
|
|
84
82
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-walle",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"description": "CTM + Wall-E — AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini & Aider, plus prompt editor, task queue, and an agent that learns from Slack, email & calendar.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-walle": "bin/create-walle.js"
|
|
@@ -4422,7 +4422,8 @@ function renderModelProviders(providers, models) {
|
|
|
4422
4422
|
var costs = pModels.map(function(m) { return m.cost_per_1m_input || 0; }).filter(function(c) { return c > 0; });
|
|
4423
4423
|
var minCost = costs.length ? Math.min.apply(null, costs) : 0;
|
|
4424
4424
|
var maxCost = costs.length ? Math.max.apply(null, costs) : 0;
|
|
4425
|
-
var
|
|
4425
|
+
var isLocal = p.type === 'ollama' || p.type === 'local';
|
|
4426
|
+
var costRange = isLocal ? 'Free (local)' : (costs.length ? _costExact(minCost) + ' - ' + _costExact(maxCost) : 'No pricing');
|
|
4426
4427
|
// Aggregate capabilities
|
|
4427
4428
|
var allCaps = new Set();
|
|
4428
4429
|
pModels.forEach(function(m) {
|
|
@@ -4443,7 +4444,7 @@ function renderModelProviders(providers, models) {
|
|
|
4443
4444
|
'<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:8px;">' +
|
|
4444
4445
|
'<div style="background:var(--bg,#1a1b26);border-radius:6px;padding:8px 10px;">' +
|
|
4445
4446
|
'<div style="font-size:10px;color:var(--fg-dim,#565f89);text-transform:uppercase;letter-spacing:0.5px;">Cost Range</div>' +
|
|
4446
|
-
'<div style="font-size:12px;color
|
|
4447
|
+
'<div style="font-size:12px;color:' + (isLocal ? '#9ece6a' : '#e0af68') + ';margin-top:2px;">' + costRange + '</div>' +
|
|
4447
4448
|
'</div>' +
|
|
4448
4449
|
'<div style="background:var(--bg,#1a1b26);border-radius:6px;padding:8px 10px;">' +
|
|
4449
4450
|
'<div style="font-size:10px;color:var(--fg-dim,#565f89);text-transform:uppercase;letter-spacing:0.5px;">Capabilities</div>' +
|
package/template/package.json
CHANGED
|
@@ -27,6 +27,7 @@ function getTelemetryDb() {
|
|
|
27
27
|
event TEXT NOT NULL,
|
|
28
28
|
meta TEXT,
|
|
29
29
|
client_ts INTEGER,
|
|
30
|
+
ip TEXT,
|
|
30
31
|
received_at TEXT DEFAULT (datetime('now'))
|
|
31
32
|
);
|
|
32
33
|
CREATE INDEX IF NOT EXISTS idx_te_install ON telemetry_events(install_id);
|
|
@@ -39,9 +40,13 @@ function getTelemetryDb() {
|
|
|
39
40
|
version TEXT,
|
|
40
41
|
os TEXT,
|
|
41
42
|
node_version TEXT,
|
|
43
|
+
ip TEXT,
|
|
42
44
|
event_count INTEGER DEFAULT 0
|
|
43
45
|
);
|
|
44
46
|
`);
|
|
47
|
+
// Migrate: add ip column if missing
|
|
48
|
+
try { telemetryDb.prepare('ALTER TABLE telemetry_events ADD COLUMN ip TEXT').run(); } catch {}
|
|
49
|
+
try { telemetryDb.prepare('ALTER TABLE telemetry_installs ADD COLUMN ip TEXT').run(); } catch {}
|
|
45
50
|
return telemetryDb;
|
|
46
51
|
} catch (e) {
|
|
47
52
|
console.error('[wall-e] Failed to init telemetry DB:', e.message);
|
|
@@ -1821,28 +1826,38 @@ function handleWalleApi(req, res, url) {
|
|
|
1821
1826
|
if (p === '/api/wall-e/telemetry/ingest' && m === 'POST') {
|
|
1822
1827
|
const tdb = getTelemetryDb();
|
|
1823
1828
|
if (!tdb) return jsonResponse(res, { error: 'Telemetry not enabled' }, 404), true;
|
|
1829
|
+
// Basic rate limiting: max 60 requests per minute per IP
|
|
1830
|
+
const clientIpRL = (req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket?.remoteAddress || 'unknown';
|
|
1831
|
+
if (!handleWalleApi._rlMap) { handleWalleApi._rlMap = new Map(); setInterval(() => handleWalleApi._rlMap.clear(), 60000); }
|
|
1832
|
+
const rlCount = (handleWalleApi._rlMap.get(clientIpRL) || 0) + 1;
|
|
1833
|
+
handleWalleApi._rlMap.set(clientIpRL, rlCount);
|
|
1834
|
+
if (rlCount > 60) return jsonResponse(res, { error: 'Rate limited' }, 429), true;
|
|
1824
1835
|
readBody(req).then(body => {
|
|
1825
1836
|
if (!body || !body.id || !Array.isArray(body.events)) {
|
|
1826
1837
|
return jsonResponse(res, { error: 'Invalid payload' }, 400);
|
|
1827
1838
|
}
|
|
1839
|
+
// Capture client IP from request (supports reverse proxy headers)
|
|
1840
|
+
const clientIp = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
|
1841
|
+
|| req.socket?.remoteAddress || '';
|
|
1828
1842
|
const insertEvent = tdb.prepare(
|
|
1829
|
-
'INSERT INTO telemetry_events (install_id, version, os, node_version, event, meta, client_ts) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
1843
|
+
'INSERT INTO telemetry_events (install_id, version, os, node_version, event, meta, client_ts, ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
|
1830
1844
|
);
|
|
1831
1845
|
const upsertInstall = tdb.prepare(`
|
|
1832
|
-
INSERT INTO telemetry_installs (install_id, version, os, node_version, event_count)
|
|
1833
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1846
|
+
INSERT INTO telemetry_installs (install_id, version, os, node_version, ip, event_count)
|
|
1847
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1834
1848
|
ON CONFLICT(install_id) DO UPDATE SET
|
|
1835
1849
|
last_seen = datetime('now'),
|
|
1836
1850
|
version = excluded.version,
|
|
1837
1851
|
os = excluded.os,
|
|
1838
1852
|
node_version = excluded.node_version,
|
|
1853
|
+
ip = excluded.ip,
|
|
1839
1854
|
event_count = telemetry_installs.event_count + excluded.event_count
|
|
1840
1855
|
`);
|
|
1841
1856
|
const runBatch = tdb.transaction((events) => {
|
|
1842
1857
|
for (const ev of events) {
|
|
1843
|
-
insertEvent.run(body.id, body.v, body.os, body.node, ev.e, JSON.stringify(ev.m || {}), ev.t);
|
|
1858
|
+
insertEvent.run(body.id, body.v, body.os, body.node, ev.e, JSON.stringify(ev.m || {}), ev.t, clientIp);
|
|
1844
1859
|
}
|
|
1845
|
-
upsertInstall.run(body.id, body.v, body.os, body.node, events.length);
|
|
1860
|
+
upsertInstall.run(body.id, body.v, body.os, body.node, clientIp, events.length);
|
|
1846
1861
|
});
|
|
1847
1862
|
runBatch(body.events.slice(0, 500)); // cap at 500 events per request
|
|
1848
1863
|
jsonResponse(res, { ok: true, received: Math.min(body.events.length, 500) });
|
|
@@ -1873,6 +1888,12 @@ function handleWalleApi(req, res, url) {
|
|
|
1873
1888
|
os_breakdown: tdb.prepare(
|
|
1874
1889
|
'SELECT os, count(*) as cnt FROM telemetry_installs GROUP BY os ORDER BY cnt DESC'
|
|
1875
1890
|
).all(),
|
|
1891
|
+
unique_ips: tdb.prepare(
|
|
1892
|
+
"SELECT count(DISTINCT ip) as cnt FROM telemetry_installs WHERE ip IS NOT NULL AND ip != ''"
|
|
1893
|
+
).get().cnt,
|
|
1894
|
+
ip_breakdown: tdb.prepare(
|
|
1895
|
+
"SELECT ip, count(*) as installs, group_concat(install_id) as install_ids FROM telemetry_installs WHERE ip IS NOT NULL AND ip != '' GROUP BY ip ORDER BY installs DESC"
|
|
1896
|
+
).all(),
|
|
1876
1897
|
recent_errors: tdb.prepare(
|
|
1877
1898
|
"SELECT install_id, version, meta, received_at FROM telemetry_events WHERE event = 'error' AND received_at >= ? ORDER BY received_at DESC LIMIT 50"
|
|
1878
1899
|
).all(since),
|
package/template/wall-e/chat.js
CHANGED
|
@@ -598,7 +598,7 @@ async function chat(message, opts = {}) {
|
|
|
598
598
|
const MAX_TURNS = 8; // search(2-3) + think(1) + response(1) + possible follow-up tools
|
|
599
599
|
|
|
600
600
|
// Guardrails
|
|
601
|
-
const MAX_TOOL_CALLS = opts.maxToolCalls
|
|
601
|
+
const MAX_TOOL_CALLS = opts.maxToolCalls != null ? opts.maxToolCalls : 15;
|
|
602
602
|
let toolCallCount = 0;
|
|
603
603
|
const MESSAGE_TIMEOUT_MS = opts.timeoutMs || 180000; // 3 minutes
|
|
604
604
|
const messageDeadline = Date.now() + MESSAGE_TIMEOUT_MS;
|
|
@@ -655,7 +655,7 @@ async function chat(message, opts = {}) {
|
|
|
655
655
|
maxTokens: 4096,
|
|
656
656
|
system: systemPrompt,
|
|
657
657
|
messages,
|
|
658
|
-
tools: chatTools,
|
|
658
|
+
tools: opts.allowedTools ? chatTools.filter(t => opts.allowedTools.includes(t.name)) : chatTools,
|
|
659
659
|
signal: controller.signal,
|
|
660
660
|
});
|
|
661
661
|
|
|
@@ -96,15 +96,36 @@ async function runDueTasks() {
|
|
|
96
96
|
// Strip BRIEFING_ITEMS block from displayed result, store items separately
|
|
97
97
|
const cleanResult = resultText.replace(/<!--\s*BRIEFING_ITEMS[\s\S]*?-->/, '').trim();
|
|
98
98
|
|
|
99
|
+
// Slack delivery verification: if the task originated from Slack but the
|
|
100
|
+
// LLM result suggests the reply was never delivered, retry once directly.
|
|
101
|
+
let slackDeliveryFailed = false;
|
|
102
|
+
if (task.source === 'slack' && (task.run_count || 0) < 3) {
|
|
103
|
+
const deliveryIssue = looksLikeSlackDeliveryFailure(cleanResult);
|
|
104
|
+
if (deliveryIssue) {
|
|
105
|
+
console.log(`[tasks] Slack delivery issue for task ${task.id}: ${deliveryIssue}`);
|
|
106
|
+
appendLog(task.id, `Slack delivery issue: ${deliveryIssue} — retrying send...`);
|
|
107
|
+
slackDeliveryFailed = !(await retrySlackDelivery(task, cleanResult));
|
|
108
|
+
if (!slackDeliveryFailed) {
|
|
109
|
+
appendLog(task.id, 'Slack retry succeeded');
|
|
110
|
+
console.log(`[tasks] Slack retry succeeded for task ${task.id}`);
|
|
111
|
+
} else {
|
|
112
|
+
appendLog(task.id, 'Slack retry also failed — marking task for retry');
|
|
113
|
+
console.log(`[tasks] Slack retry failed for task ${task.id}, will retry later`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
99
118
|
brain.updateTask(task.id, {
|
|
100
|
-
status: task.type === 'recurring' ? 'pending' : 'completed',
|
|
119
|
+
status: slackDeliveryFailed ? 'pending' : (task.type === 'recurring' ? 'pending' : 'completed'),
|
|
101
120
|
completed_at: completedAt,
|
|
102
121
|
last_run_at: completedAt,
|
|
103
122
|
run_count: (task.run_count || 0) + 1,
|
|
104
123
|
result: cleanResult.slice(0, 10000),
|
|
105
|
-
error: null,
|
|
106
124
|
checkpoint: null, // clear checkpoint on success
|
|
107
|
-
|
|
125
|
+
error: slackDeliveryFailed ? 'Slack reply delivery failed — scheduled for retry' : null,
|
|
126
|
+
next_run_at: slackDeliveryFailed
|
|
127
|
+
? new Date(Date.now() + 2 * 60 * 1000).toISOString() // retry in 2 min
|
|
128
|
+
: (task.type === 'recurring' ? computeNextDue(task.schedule) : null),
|
|
108
129
|
// Keep started_at so duration can be computed as completed_at - started_at
|
|
109
130
|
});
|
|
110
131
|
|
|
@@ -534,4 +555,134 @@ function consolidateBriefingItems(task, resultText, timestamp) {
|
|
|
534
555
|
}
|
|
535
556
|
}
|
|
536
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Detect if a task result text indicates the Slack reply was never delivered.
|
|
560
|
+
* Returns a string describing the issue, or null if delivery looks OK.
|
|
561
|
+
*/
|
|
562
|
+
function looksLikeSlackDeliveryFailure(resultText) {
|
|
563
|
+
if (!resultText) return 'empty result';
|
|
564
|
+
const lower = resultText.toLowerCase();
|
|
565
|
+
|
|
566
|
+
// Explicit failure patterns
|
|
567
|
+
const failurePatterns = [
|
|
568
|
+
'mcp server error',
|
|
569
|
+
'error sending',
|
|
570
|
+
'failed to send',
|
|
571
|
+
"couldn't send",
|
|
572
|
+
'could not send',
|
|
573
|
+
'unable to send',
|
|
574
|
+
'slack_send_message failed',
|
|
575
|
+
'failed to deliver',
|
|
576
|
+
'error posting message',
|
|
577
|
+
'connection refused',
|
|
578
|
+
'mcp call failed',
|
|
579
|
+
'encountering a technical issue sending',
|
|
580
|
+
];
|
|
581
|
+
for (const p of failurePatterns) {
|
|
582
|
+
if (lower.includes(p)) return `explicit failure: ${p}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// No reply was attempted — model researched but never sent
|
|
586
|
+
// Success patterns: if any of these appear, the reply was likely sent
|
|
587
|
+
const successPatterns = [
|
|
588
|
+
'replied to your slack',
|
|
589
|
+
'sent the message',
|
|
590
|
+
'message was sent',
|
|
591
|
+
'reply sent',
|
|
592
|
+
'posted to slack',
|
|
593
|
+
'slack_send_message',
|
|
594
|
+
'sent in thread',
|
|
595
|
+
'responded in the thread',
|
|
596
|
+
'replied in the slack thread',
|
|
597
|
+
'sent to the thread',
|
|
598
|
+
'delivered the answer',
|
|
599
|
+
];
|
|
600
|
+
if (!successPatterns.some(p => lower.includes(p))) {
|
|
601
|
+
return 'no reply sent (model exhausted turns without calling slack_send_message)';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Attempt to retry sending the Slack reply directly via MCP.
|
|
609
|
+
* Extracts the answer from the result text and sends it to the channel/thread
|
|
610
|
+
* referenced in the task description.
|
|
611
|
+
* Returns true if the retry succeeded, false otherwise.
|
|
612
|
+
*/
|
|
613
|
+
async function retrySlackDelivery(task, resultText) {
|
|
614
|
+
try {
|
|
615
|
+
const slackMcp = require('../tools/slack-mcp');
|
|
616
|
+
|
|
617
|
+
// Extract channel_id and thread_ts from the task description
|
|
618
|
+
const channelMatch = task.description?.match(/"channel_id":\s*"([^"]+)"/);
|
|
619
|
+
const threadMatch = task.description?.match(/"thread_ts":\s*"([^"]+)"/);
|
|
620
|
+
console.log(`[tasks] retrySlackDelivery: channel=${channelMatch?.[1]}, thread=${threadMatch?.[1]}`);
|
|
621
|
+
if (!channelMatch) {
|
|
622
|
+
console.error('[tasks] Cannot retry Slack delivery: no channel_id in task description');
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Extract the useful content from the result, stripping failure/meta messages
|
|
627
|
+
let answer = resultText;
|
|
628
|
+
|
|
629
|
+
// Remove lines that are just research narration ("Let me...", "I'll...")
|
|
630
|
+
answer = answer.replace(/^(?:Let me|I'll|I will|Now let me|Let's)[^\n]*$/gm, '').trim();
|
|
631
|
+
|
|
632
|
+
// Strip error messages about delivery failure
|
|
633
|
+
answer = answer.replace(/(?:However|Unfortunately|I'm currently)[^.]*(?:unable to send|MCP server|authentication issue|technical issue|couldn't send|could not send)[^.]*\./gi, '').trim();
|
|
634
|
+
|
|
635
|
+
// Try to find substantive answer (before any error part)
|
|
636
|
+
const errorIdx = answer.search(/unfortunately.*(?:technical issue|error|couldn't send|MCP server)/i);
|
|
637
|
+
if (errorIdx > 50) {
|
|
638
|
+
answer = answer.slice(0, errorIdx).trim();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
console.log(`[tasks] retrySlackDelivery: extracted answer (${answer.length} chars): ${answer.slice(0, 200)}`);
|
|
642
|
+
|
|
643
|
+
// If the answer is still just meta-narration with no real content, try to
|
|
644
|
+
// generate a brief reply using the chat engine with strict instructions
|
|
645
|
+
if (answer.length < 20 || !/[a-z]{3,}/i.test(answer)) {
|
|
646
|
+
console.log('[tasks] No substantive answer in result — generating a brief reply via chat...');
|
|
647
|
+
try {
|
|
648
|
+
const chatModule = require('../chat');
|
|
649
|
+
const briefReply = await chatModule.chat(
|
|
650
|
+
`[URGENT] You MUST reply to a Slack question immediately. The question was:\n\n${task.description?.match(/(?:Question|task) from.*?:\n\n([\s\S]*?)(?:\n\nFull thread|\n\n---)/i)?.[1] || task.title}\n\nGive a helpful 2-3 sentence answer. Do NOT use any tools. Just reply with your best answer based on what you know.`,
|
|
651
|
+
{ channel: 'task', session_id: `retry-${task.id}`, maxToolCalls: 0, timeoutMs: 30000 }
|
|
652
|
+
);
|
|
653
|
+
answer = briefReply.reply || '';
|
|
654
|
+
} catch (chatErr) {
|
|
655
|
+
console.error(`[tasks] Brief reply generation failed: ${chatErr.message}`);
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Truncate if too long for Slack
|
|
661
|
+
if (answer.length > 3000) answer = answer.slice(0, 3000) + '...';
|
|
662
|
+
if (answer.length < 10) {
|
|
663
|
+
console.error('[tasks] Cannot retry Slack delivery: no substantive answer to send');
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const args = { channel_id: channelMatch[1], message: answer };
|
|
668
|
+
if (threadMatch) args.thread_ts = threadMatch[1];
|
|
669
|
+
|
|
670
|
+
console.log(`[tasks] retrySlackDelivery: sending ${answer.length} chars to ${args.channel_id} thread=${args.thread_ts || 'none'}`);
|
|
671
|
+
const result = await slackMcp.callSlackMcp('slack_send_message', args);
|
|
672
|
+
const resultStr = JSON.stringify(result).slice(0, 200);
|
|
673
|
+
console.log(`[tasks] retrySlackDelivery: MCP result: ${resultStr}`);
|
|
674
|
+
|
|
675
|
+
// Check for actual success — look for message_link in response
|
|
676
|
+
const hasMessageLink = resultStr.includes('message_link') || resultStr.includes('message_ts');
|
|
677
|
+
if (!hasMessageLink) {
|
|
678
|
+
console.error(`[tasks] retrySlackDelivery: no message_link in response — likely failed`);
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
return true;
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.error(`[tasks] Slack delivery retry failed: ${err.message}`);
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
537
688
|
module.exports = { runDueTasks, runTaskById, recoverInterruptedTasks, buildTaskPrompt, computeNextDue, getTaskLogs, clearTaskLogs, stopTask, executeSkill, executeMultiTurnChat, consolidateBriefingItems, taskLogs };
|
|
@@ -71,6 +71,65 @@ function sleep(ms) {
|
|
|
71
71
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Generate an answer using chat (with limited tools) and send it directly to Slack.
|
|
76
|
+
* Returns the reply text on success, or null on failure.
|
|
77
|
+
*
|
|
78
|
+
* This bypasses the task→chat→mcp_call pipeline which is unreliable because:
|
|
79
|
+
* 1. The model ignores "reply first" instructions and calls slow tools
|
|
80
|
+
* 2. MCP tool calls from chat can hang indefinitely
|
|
81
|
+
* 3. There's no guarantee the model will call slack_send_message
|
|
82
|
+
*
|
|
83
|
+
* Instead: we call chat with search_memories + think only, get the answer text,
|
|
84
|
+
* then send it ourselves via callSlackMcp.
|
|
85
|
+
*/
|
|
86
|
+
async function generateAndSendReply(msg, threadTs, contextBlock, kind) {
|
|
87
|
+
const chatModule = require(path.resolve(__dirname, '..', '..', '..', 'chat'));
|
|
88
|
+
const question = (msg.content || '').replace(/@wall-?e/gi, '').trim();
|
|
89
|
+
|
|
90
|
+
// Step 1: Generate the answer with limited tools (search_memories + think only)
|
|
91
|
+
let answer;
|
|
92
|
+
try {
|
|
93
|
+
const prompt = `[SLACK ${kind.toUpperCase()}] ${OWNER_NAME} asked in Slack:\n\n${question}${contextBlock}\n\nSearch your memories for relevant info, then give a concise, helpful answer (2-6 sentences). Do NOT call any tools except search_memories and think. Just answer the question directly.`;
|
|
94
|
+
|
|
95
|
+
const result = await chatModule.chat(prompt, {
|
|
96
|
+
channel: 'task',
|
|
97
|
+
session_id: `slack-reply-${msg.ts || Date.now()}`,
|
|
98
|
+
allowedTools: ['search_memories', 'think', 'lookup_person'],
|
|
99
|
+
timeoutMs: 60000, // 1 minute max
|
|
100
|
+
});
|
|
101
|
+
answer = (result.reply || '').trim();
|
|
102
|
+
console.log(`[slack-mentions] Generated answer (${answer.length} chars): ${answer.slice(0, 150)}`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`[slack-mentions] Failed to generate answer: ${err.message}`);
|
|
105
|
+
answer = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Step 2: Send the reply directly via Slack MCP
|
|
109
|
+
if (!answer || answer.length < 5) {
|
|
110
|
+
answer = `I'm not sure about that — let me look into it and get back to you.`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Truncate for Slack (4000 char limit)
|
|
114
|
+
if (answer.length > 3500) answer = answer.slice(0, 3500) + '...';
|
|
115
|
+
|
|
116
|
+
if (msg.channelId) {
|
|
117
|
+
try {
|
|
118
|
+
await slackMcp.callSlackMcp('slack_send_message', {
|
|
119
|
+
channel_id: msg.channelId,
|
|
120
|
+
thread_ts: threadTs,
|
|
121
|
+
message: answer,
|
|
122
|
+
});
|
|
123
|
+
console.log(`[slack-mentions] Reply sent to Slack thread ${threadTs}`);
|
|
124
|
+
return answer;
|
|
125
|
+
} catch (sendErr) {
|
|
126
|
+
console.error(`[slack-mentions] Failed to send reply to Slack: ${sendErr.message}`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return answer;
|
|
131
|
+
}
|
|
132
|
+
|
|
74
133
|
function classifyMention(text) {
|
|
75
134
|
const trimmed = (text || '').trim();
|
|
76
135
|
const cleaned = trimmed.replace(/@wall-?e/gi, '').trim();
|
|
@@ -424,77 +483,42 @@ async function _processMention(msg, seenTs, stats) {
|
|
|
424
483
|
return;
|
|
425
484
|
}
|
|
426
485
|
|
|
427
|
-
if (kind === 'task') {
|
|
486
|
+
if (kind === 'task' || kind === 'question') {
|
|
487
|
+
stats.questionsFound += kind === 'question' ? 1 : 0;
|
|
488
|
+
stats.tasksCreated += kind === 'task' ? 1 : 0;
|
|
428
489
|
const title = extractTaskTitle(msg.content);
|
|
429
|
-
|
|
430
|
-
const { id } = brain.insertTask({
|
|
431
|
-
title,
|
|
432
|
-
description: `Slack task from ${OWNER_NAME}:\n\n${msg.content}${contextBlock}\n\n---\n**CRITICAL INSTRUCTIONS — follow exactly:**\n1. Do focused research (max 3 search calls)\n2. Draft your response\n3. IMMEDIATELY reply in Slack — the reply is the #1 priority. Do NOT over-research.\n\nReply using mcp_call with:\n- server: slack\n- tool: slack_send_message\n- arguments: { "channel_id": "${msg.channelId}", "thread_ts": "${threadTs}", "message": "<your results>" }\n\nBetter to reply with what you know than to exhaust turns researching and never reply.`,
|
|
433
|
-
priority: 'normal',
|
|
434
|
-
type: 'once',
|
|
435
|
-
execution: 'chat',
|
|
436
|
-
source: 'slack',
|
|
437
|
-
source_ref: sourceRef,
|
|
438
|
-
});
|
|
439
|
-
stats.tasksCreated++;
|
|
440
|
-
console.log(`[slack-mentions] Task created: ${id} — ${title}`);
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
brain.upsertSlackThread({ channelId: msg.channelId, threadTs, taskId: id, sessionId: `task-${id}` });
|
|
444
|
-
} catch (upsertErr) {
|
|
445
|
-
console.error(`[slack-mentions] FAILED to upsert watched thread: ${upsertErr.message}`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (msg.channelId) {
|
|
449
|
-
try {
|
|
450
|
-
await slackMcp.callSlackMcp('slack_send_message', {
|
|
451
|
-
channel_id: msg.channelId,
|
|
452
|
-
message: `Got it! I've created a task for this. I'll update you here when it's done.`,
|
|
453
|
-
thread_ts: threadTs,
|
|
454
|
-
});
|
|
455
|
-
} catch (replyErr) {
|
|
456
|
-
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
457
|
-
}
|
|
458
|
-
await sleep(PAUSE_MS);
|
|
459
|
-
}
|
|
460
|
-
} catch (taskErr) {
|
|
461
|
-
console.error(`[slack-mentions] Failed to create task: ${taskErr.message}`);
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
stats.questionsFound++;
|
|
465
|
-
console.log(`[slack-mentions] Question detected: ${msg.content.slice(0, 80)}...`);
|
|
490
|
+
console.log(`[slack-mentions] ${kind} detected: ${title.slice(0, 80)}`);
|
|
466
491
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
message: `Let me look into that — I'll get back to you shortly.`,
|
|
472
|
-
thread_ts: threadTs,
|
|
473
|
-
});
|
|
474
|
-
} catch (replyErr) {
|
|
475
|
-
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
476
|
-
}
|
|
477
|
-
await sleep(PAUSE_MS);
|
|
478
|
-
}
|
|
492
|
+
// Phase 1: Generate answer directly and send it — no task intermediary.
|
|
493
|
+
// This is the most reliable approach: we control the chat call and send the
|
|
494
|
+
// reply ourselves, instead of hoping the task's chat engine will do it.
|
|
495
|
+
const reply = await generateAndSendReply(msg, threadTs, contextBlock, kind);
|
|
479
496
|
|
|
497
|
+
// Track as a completed task for history/dedup
|
|
480
498
|
try {
|
|
481
499
|
const { id } = brain.insertTask({
|
|
482
|
-
title: `Answer Slack question: ${
|
|
483
|
-
description:
|
|
484
|
-
priority: 'high',
|
|
500
|
+
title: kind === 'question' ? `Answer Slack question: ${title}` : title,
|
|
501
|
+
description: `${kind === 'question' ? 'Question' : 'Task'} from ${OWNER_NAME} in Slack:\n\n${msg.content}${contextBlock}`,
|
|
502
|
+
priority: kind === 'question' ? 'high' : 'normal',
|
|
485
503
|
type: 'once',
|
|
486
504
|
execution: 'chat',
|
|
487
505
|
source: 'slack',
|
|
488
506
|
source_ref: sourceRef,
|
|
489
507
|
});
|
|
490
|
-
|
|
508
|
+
// Mark completed/failed immediately since we handled it inline
|
|
509
|
+
brain.updateTask(id, {
|
|
510
|
+
status: reply ? 'completed' : 'failed',
|
|
511
|
+
result: (reply || 'Failed to generate or send reply').slice(0, 10000),
|
|
512
|
+
completed_at: new Date().toISOString(),
|
|
513
|
+
run_count: 1,
|
|
514
|
+
});
|
|
491
515
|
try {
|
|
492
516
|
brain.upsertSlackThread({ channelId: msg.channelId, threadTs, taskId: id, sessionId: `task-${id}` });
|
|
493
517
|
} catch (upsertErr) {
|
|
494
518
|
console.error(`[slack-mentions] FAILED to upsert watched thread: ${upsertErr.message}`);
|
|
495
519
|
}
|
|
496
520
|
} catch (taskErr) {
|
|
497
|
-
console.error(`[slack-mentions] Failed to create
|
|
521
|
+
console.error(`[slack-mentions] Failed to create task record: ${taskErr.message}`);
|
|
498
522
|
}
|
|
499
523
|
}
|
|
500
524
|
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
<a href="#how">Get Started</a>
|
|
232
232
|
<a href="#architecture">Architecture</a>
|
|
233
233
|
<a href="https://www.npmjs.com/package/create-walle">npm</a>
|
|
234
|
-
<a href="https://
|
|
234
|
+
<a href="https://www.npmjs.com/package/create-walle?activeTab=versions">Changelog</a>
|
|
235
235
|
</div>
|
|
236
236
|
</div>
|
|
237
237
|
</nav>
|
|
@@ -243,8 +243,8 @@
|
|
|
243
243
|
Run Claude Code, Codex, Gemini, and Aider sessions side by side. Manage prompts,
|
|
244
244
|
queue tasks, and let an AI agent build a second brain from your work life.
|
|
245
245
|
</p>
|
|
246
|
-
<div class="install-box" onclick="navigator.clipboard.writeText('npx create-walle install ./
|
|
247
|
-
<code>npx create-walle install ./
|
|
246
|
+
<div class="install-box" onclick="navigator.clipboard.writeText('npx create-walle install ./walle');this.querySelector('.copy-hint').textContent='Copied!'">
|
|
247
|
+
<code>npx create-walle install ./walle</code>
|
|
248
248
|
<span class="copy-hint">click to copy</span>
|
|
249
249
|
</div>
|
|
250
250
|
<div class="badge-row">
|
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
|
260
260
|
<span class="title mono">Terminal</span>
|
|
261
261
|
</div>
|
|
262
|
-
<pre><span class="prompt">$</span> <span class="cmd">npx create-walle install ./
|
|
262
|
+
<pre><span class="prompt">$</span> <span class="cmd">npx create-walle install ./walle</span>
|
|
263
263
|
|
|
264
264
|
<span class="dim"> Installing CTM + Wall-E...</span>
|
|
265
265
|
<span class="dim"> Auto-detected owner:</span> <span class="hi">Your Name</span>
|
|
@@ -365,7 +365,7 @@
|
|
|
365
365
|
<div class="steps">
|
|
366
366
|
<div class="step">
|
|
367
367
|
<h3>Install</h3>
|
|
368
|
-
<p><code>npx create-walle install ./
|
|
368
|
+
<p><code>npx create-walle install ./walle</code><br>Copies the project, installs deps, and starts the server.</p>
|
|
369
369
|
</div>
|
|
370
370
|
<div class="step">
|
|
371
371
|
<h3>Open the browser</h3>
|