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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight keyword/regex topic classifier.
|
|
5
|
+
* No Claude call — pure pattern matching for fast context selection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const TOPIC_PATTERNS = {
|
|
9
|
+
people: {
|
|
10
|
+
// Names of known people, relationship words
|
|
11
|
+
keywords: ['who', 'person', 'team', 'report', 'manager', 'colleague', 'relationship', 'trust'],
|
|
12
|
+
regex: /\b(who\s+is|reports?\s+to|works?\s+with|relationship|team\s+member|direct\s+report|skip.?level)\b/i,
|
|
13
|
+
},
|
|
14
|
+
slack: {
|
|
15
|
+
keywords: ['slack', 'channel', 'message', 'dm', 'thread', 'mention', 'said', 'told', 'wrote', 'posted'],
|
|
16
|
+
regex: /\b(slack|channel|#\w+|message[sd]?|DM|thread|mention|said|told|wrote|posted|conversation)\b/i,
|
|
17
|
+
},
|
|
18
|
+
calendar: {
|
|
19
|
+
keywords: ['calendar', 'meeting', 'event', 'schedule', 'tomorrow', 'today', 'appointment', '1:1', 'standup'],
|
|
20
|
+
regex: /\b(calendar|meeting|event|schedule|tomorrow|today|tonight|this\s+week|next\s+week|appointment|1[:-]1|standup|sync|deadline)\b/i,
|
|
21
|
+
},
|
|
22
|
+
tasks: {
|
|
23
|
+
keywords: ['task', 'todo', 'reminder', 'deadline', 'priority', 'action item', 'backlog'],
|
|
24
|
+
regex: /\b(task|todo|to.?do|reminder|deadline|priority|action\s+item|backlog|sprint|jira|ticket)\b/i,
|
|
25
|
+
},
|
|
26
|
+
technical: {
|
|
27
|
+
keywords: ['code', 'bug', 'deploy', 'api', 'database', 'server', 'git', 'pr', 'review', 'test'],
|
|
28
|
+
regex: /\b(code|bug|deploy|api|database|server|git|pr|pull\s+request|review|test|build|pipeline|docker|kubernetes|aws|gcp)\b/i,
|
|
29
|
+
},
|
|
30
|
+
tools: {
|
|
31
|
+
keywords: ['tool', 'mcp', 'skill', 'fetch', 'search', 'run', 'execute', 'automation'],
|
|
32
|
+
regex: /\b(tool|mcp|skill|fetch|search\s+for|run\s+a|execute|automation|script|command|shell)\b/i,
|
|
33
|
+
},
|
|
34
|
+
weather: {
|
|
35
|
+
keywords: ['weather', 'temperature', 'rain', 'snow', 'forecast', 'outside'],
|
|
36
|
+
regex: /\b(weather|temperature|rain|snow|forecast|outside|humid|cold|hot|warm|sunny|cloudy)\b/i,
|
|
37
|
+
},
|
|
38
|
+
personal: {
|
|
39
|
+
keywords: ['feel', 'think about', 'opinion', 'preference', 'like', 'dislike', 'hobby', 'family'],
|
|
40
|
+
regex: /\b(feel|opinion|preference|like|dislike|hobby|family|personal|habit|routine|favorite)\b/i,
|
|
41
|
+
},
|
|
42
|
+
work: {
|
|
43
|
+
keywords: ['project', 'okr', 'goal', 'strategy', 'roadmap', 'planning', 'quarterly', 'perf'],
|
|
44
|
+
regex: /\b(project|okr|goal|strategy|roadmap|planning|quarterly|perf|review|promotion|career|leadership)\b/i,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Classify a user message into topic tags.
|
|
50
|
+
* Returns array of matching topic names, e.g. ['people', 'slack'].
|
|
51
|
+
* Always returns at least ['general'] if no specific match.
|
|
52
|
+
*/
|
|
53
|
+
function classifyTopics(message) {
|
|
54
|
+
if (!message || typeof message !== 'string') return ['general'];
|
|
55
|
+
|
|
56
|
+
const topics = [];
|
|
57
|
+
const lower = message.toLowerCase();
|
|
58
|
+
|
|
59
|
+
for (const [topic, { keywords, regex }] of Object.entries(TOPIC_PATTERNS)) {
|
|
60
|
+
if (regex.test(message)) {
|
|
61
|
+
topics.push(topic);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Fallback: check if any keyword appears as a substring
|
|
65
|
+
if (keywords.some(kw => lower.includes(kw))) {
|
|
66
|
+
topics.push(topic);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for people names — if message contains a capitalized word that looks like a name
|
|
71
|
+
if (!topics.includes('people') && /\b[A-Z][a-z]{2,}\b/.test(message)) {
|
|
72
|
+
// Could be a person name — add 'people' as a soft match
|
|
73
|
+
topics.push('people');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return topics.length > 0 ? topics : ['general'];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { classifyTopics, TOPIC_PATTERNS };
|
|
@@ -50,4 +50,28 @@ module.exports = [
|
|
|
50
50
|
skill_config: JSON.stringify({ days_back: 3650, sync_inbox: true }),
|
|
51
51
|
priority: 'normal',
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
title: 'Proactive Alerts',
|
|
55
|
+
description: 'Check for time-sensitive items: upcoming meetings, @mentions, approaching deadlines, failed tasks.',
|
|
56
|
+
type: 'recurring',
|
|
57
|
+
schedule: 'every 5m',
|
|
58
|
+
skill: 'proactive-alerts',
|
|
59
|
+
priority: 'normal',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: 'Slack: @walle Mentions',
|
|
63
|
+
description: 'Poll Slack for @walle mentions from the owner. Creates tasks for assignments, replies in-thread for questions.',
|
|
64
|
+
type: 'recurring',
|
|
65
|
+
schedule: 'every 30s',
|
|
66
|
+
skill: 'slack-mentions',
|
|
67
|
+
priority: 'normal',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: 'Weekly Reflection',
|
|
71
|
+
description: 'Generate a weekly reflection analyzing patterns, decisions, and insights from the past 7 days.',
|
|
72
|
+
type: 'recurring',
|
|
73
|
+
schedule: 'weekly Sunday at 8pm',
|
|
74
|
+
skill: 'weekly-reflection',
|
|
75
|
+
priority: 'normal',
|
|
76
|
+
},
|
|
53
77
|
];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const EventEmitter = require('events');
|
|
3
|
+
|
|
4
|
+
class WalleEventBus extends EventEmitter {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.setMaxListeners(20);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
emitMessage(channel, text, sender) {
|
|
11
|
+
this.emit('message', { channel, text, sender, ts: Date.now() });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
emitHighImportance(memoryId, content) {
|
|
15
|
+
this.emit('high_importance', { memoryId, content, ts: Date.now() });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
emitWebhook(source, payload) {
|
|
19
|
+
this.emit('webhook', { source, payload, ts: Date.now() });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = new WalleEventBus();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const brain = require('../brain');
|
|
2
|
+
const eventBus = require('../events/event-bus');
|
|
2
3
|
|
|
3
4
|
async function runOnce(adapters) {
|
|
4
5
|
const checkpoint = brain.getCheckpoint('ingest');
|
|
@@ -16,6 +17,9 @@ async function runOnce(adapters) {
|
|
|
16
17
|
if (!latestTimestamp || mem.timestamp > latestTimestamp) {
|
|
17
18
|
latestTimestamp = mem.timestamp;
|
|
18
19
|
}
|
|
20
|
+
if (mem.importance >= 0.8 || mem.memory_type === 'message_received') {
|
|
21
|
+
eventBus.emitHighImportance(result?.id, (mem.content || '').slice(0, 200));
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
25
|
} catch (err) {
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const brain = require('../brain');
|
|
4
|
+
const { buildStateSnapshot, isStateEmpty, formatSnapshot } = require('../context/state-snapshot');
|
|
5
|
+
const { canActAutonomously, getDomainConfidence } = require('../decision/confidence');
|
|
6
|
+
const { buildClientOpts } = require('../extraction/knowledge-extractor');
|
|
7
|
+
const Anthropic = require('@anthropic-ai/sdk');
|
|
8
|
+
const { v4: uuidv4 } = require('uuid');
|
|
9
|
+
|
|
10
|
+
const COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes between same decisions
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get an Anthropic client configured for Portkey gateway or direct API.
|
|
14
|
+
* Reuses the same pattern as knowledge-extractor.js
|
|
15
|
+
*/
|
|
16
|
+
function getInitiativeClient() {
|
|
17
|
+
const opts = buildClientOpts();
|
|
18
|
+
const isPortkey = opts.defaultHeaders && opts.defaultHeaders['x-portkey-api-key'];
|
|
19
|
+
|
|
20
|
+
if (isPortkey) {
|
|
21
|
+
return {
|
|
22
|
+
messages: {
|
|
23
|
+
async create(params, fetchOpts) {
|
|
24
|
+
const headers = {
|
|
25
|
+
'content-type': 'application/json',
|
|
26
|
+
'anthropic-version': '2023-06-01',
|
|
27
|
+
...opts.defaultHeaders,
|
|
28
|
+
};
|
|
29
|
+
const res = await fetch(opts.baseURL + '/messages', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers,
|
|
32
|
+
body: JSON.stringify(params),
|
|
33
|
+
signal: fetchOpts?.signal,
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
throw new Error(`${res.status} ${text}`);
|
|
38
|
+
}
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new Anthropic(opts);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the system prompt for the initiative engine.
|
|
50
|
+
*/
|
|
51
|
+
function buildInitiativePrompt(snapshot, recentDecisions) {
|
|
52
|
+
const snapshotText = formatSnapshot(snapshot);
|
|
53
|
+
|
|
54
|
+
const recentText = recentDecisions.length > 0
|
|
55
|
+
? recentDecisions.map(d => `- ${d.decision}: ${d.decision_data || '(no data)'}`).join('\n')
|
|
56
|
+
: '(none)';
|
|
57
|
+
|
|
58
|
+
return `You are WALL-E's initiative engine. You observe the owner's current state and decide whether to take action.
|
|
59
|
+
|
|
60
|
+
CURRENT STATE:
|
|
61
|
+
${snapshotText}
|
|
62
|
+
|
|
63
|
+
RECENT DECISIONS (avoid repeating):
|
|
64
|
+
${recentText}
|
|
65
|
+
|
|
66
|
+
PROACTIVITY RULES:
|
|
67
|
+
- Default to "noop" -- only act when there's clear value
|
|
68
|
+
- Time-sensitive items (calendar in <15min, direct @mention) are highest priority
|
|
69
|
+
- Never send the same notification twice within 30 minutes
|
|
70
|
+
- Consider: is this ACTUALLY useful right now, or just busy work?
|
|
71
|
+
|
|
72
|
+
Respond in this EXACT format:
|
|
73
|
+
DECISION: noop|notify|create_task|run_skill
|
|
74
|
+
DOMAIN: general|calendar|slack|tasks|knowledge
|
|
75
|
+
REASONING: <1-2 sentences explaining why>
|
|
76
|
+
DATA: <JSON with details if not noop>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse Claude's structured decision output.
|
|
81
|
+
*/
|
|
82
|
+
function parseDecision(text) {
|
|
83
|
+
const result = {
|
|
84
|
+
decision: 'noop',
|
|
85
|
+
domain: 'general',
|
|
86
|
+
reasoning: '',
|
|
87
|
+
data: null,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const decisionMatch = text.match(/^DECISION:\s*(\S+)/m);
|
|
91
|
+
if (decisionMatch) {
|
|
92
|
+
const d = decisionMatch[1].toLowerCase().trim();
|
|
93
|
+
if (['noop', 'notify', 'create_task', 'run_skill'].includes(d)) {
|
|
94
|
+
result.decision = d;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const domainMatch = text.match(/^DOMAIN:\s*(\S+)/m);
|
|
99
|
+
if (domainMatch) {
|
|
100
|
+
result.domain = domainMatch[1].toLowerCase().trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const reasoningMatch = text.match(/^REASONING:\s*(.+)/m);
|
|
104
|
+
if (reasoningMatch) {
|
|
105
|
+
result.reasoning = reasoningMatch[1].trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const dataMatch = text.match(/^DATA:\s*(.+)/ms);
|
|
109
|
+
if (dataMatch) {
|
|
110
|
+
const raw = dataMatch[1].trim();
|
|
111
|
+
if (raw && raw !== 'null' && raw !== 'none' && raw !== 'N/A') {
|
|
112
|
+
try {
|
|
113
|
+
// Try to extract JSON from the data line (may span multiple lines)
|
|
114
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
115
|
+
if (jsonMatch) {
|
|
116
|
+
result.data = JSON.parse(jsonMatch[0]);
|
|
117
|
+
} else {
|
|
118
|
+
// No JSON object found, store as raw string
|
|
119
|
+
result.data = { raw: raw.slice(0, 500) };
|
|
120
|
+
}
|
|
121
|
+
} catch (_) {
|
|
122
|
+
// If JSON parse fails, store as string
|
|
123
|
+
result.data = { raw: raw.slice(0, 500) };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the autonomy tier for a domain.
|
|
133
|
+
*/
|
|
134
|
+
function getTier(domain) {
|
|
135
|
+
try {
|
|
136
|
+
const dc = getDomainConfidence(domain);
|
|
137
|
+
return dc.current_tier;
|
|
138
|
+
} catch (_) {
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Log an initiative decision to the database.
|
|
145
|
+
*/
|
|
146
|
+
function logInitiative(trigger, snapshot, reasoning, decision, decisionData, autonomyTier) {
|
|
147
|
+
return brain.insertInitiativeLog({
|
|
148
|
+
trigger,
|
|
149
|
+
state_snapshot: JSON.stringify(snapshot).slice(0, 5000),
|
|
150
|
+
reasoning: reasoning ? reasoning.slice(0, 2000) : null,
|
|
151
|
+
decision,
|
|
152
|
+
decision_data: decisionData ? JSON.stringify(decisionData).slice(0, 5000) : null,
|
|
153
|
+
autonomy_tier: autonomyTier || 1,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Execute a decision based on its type.
|
|
159
|
+
*/
|
|
160
|
+
async function executeDecision(decision, snapshot) {
|
|
161
|
+
switch (decision.decision) {
|
|
162
|
+
case 'noop':
|
|
163
|
+
return;
|
|
164
|
+
|
|
165
|
+
case 'notify': {
|
|
166
|
+
// Send notification via chat message log (visible in CTM dashboard)
|
|
167
|
+
const msg = decision.data?.message || decision.reasoning || 'Initiative notification';
|
|
168
|
+
brain.insertChatMessage({
|
|
169
|
+
role: 'assistant',
|
|
170
|
+
content: `[Initiative] ${msg}`,
|
|
171
|
+
channel: 'initiative',
|
|
172
|
+
session_id: 'initiative-' + new Date().toISOString().slice(0, 10),
|
|
173
|
+
});
|
|
174
|
+
// Also send macOS notification for visibility
|
|
175
|
+
try {
|
|
176
|
+
const { sendNotification } = require('../tools/local-tools');
|
|
177
|
+
await sendNotification('WALL-E', msg.slice(0, 200));
|
|
178
|
+
} catch {}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'create_task': {
|
|
183
|
+
const taskData = decision.data || {};
|
|
184
|
+
brain.insertTask({
|
|
185
|
+
title: taskData.title || decision.reasoning || 'Auto-created task',
|
|
186
|
+
description: taskData.description || decision.reasoning,
|
|
187
|
+
priority: taskData.priority || 'normal',
|
|
188
|
+
type: 'once',
|
|
189
|
+
due_at: taskData.due_at || null,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'run_skill': {
|
|
195
|
+
// Attempt to run a skill by name
|
|
196
|
+
const skillName = decision.data?.skill;
|
|
197
|
+
if (!skillName) return;
|
|
198
|
+
try {
|
|
199
|
+
const { executeSkill } = require('../skills/skill-executor');
|
|
200
|
+
const skill = brain.getSkillByName(skillName);
|
|
201
|
+
if (skill && skill.enabled) {
|
|
202
|
+
await executeSkill(skill);
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error('[wall-e] Initiative skill execution failed:', err.message);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
default:
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check cooldown: is the same decision type + domain already in recent logs?
|
|
217
|
+
*/
|
|
218
|
+
function isOnCooldown(decision, recentDecisions) {
|
|
219
|
+
if (decision.decision === 'noop') return false;
|
|
220
|
+
return recentDecisions.some(d => {
|
|
221
|
+
if (d.decision !== decision.decision) return false;
|
|
222
|
+
// For notify, only cooldown if the message is the same
|
|
223
|
+
if (decision.decision === 'notify') {
|
|
224
|
+
try {
|
|
225
|
+
const prev = JSON.parse(d.decision_data);
|
|
226
|
+
return prev?.message && decision.data?.message &&
|
|
227
|
+
prev.message === decision.data.message;
|
|
228
|
+
} catch (_) {
|
|
229
|
+
return true; // Can't parse — assume duplicate
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// For other decision types, same type = on cooldown
|
|
233
|
+
return true;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Main initiative loop function. Called on an interval from agent.js
|
|
239
|
+
*/
|
|
240
|
+
async function runInitiativeLoop(opts = {}) {
|
|
241
|
+
const snapshot = buildStateSnapshot();
|
|
242
|
+
|
|
243
|
+
// Check if there's anything worth reasoning about
|
|
244
|
+
if (isStateEmpty(snapshot)) {
|
|
245
|
+
return { decision: 'noop', reasoning: 'Nothing to evaluate' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check cooldown -- avoid repeating recent decisions
|
|
249
|
+
const db = brain.getDb();
|
|
250
|
+
const recentDecisions = db.prepare(
|
|
251
|
+
"SELECT decision, decision_data FROM initiative_log WHERE created_at > datetime('now', '-30 minutes') ORDER BY created_at DESC LIMIT 10"
|
|
252
|
+
).all();
|
|
253
|
+
|
|
254
|
+
// Call Claude with a focused prompt
|
|
255
|
+
const client = opts.client || getInitiativeClient();
|
|
256
|
+
const model = opts.model || process.env.WALLE_MODEL || 'claude-haiku-4-5-20251001';
|
|
257
|
+
|
|
258
|
+
const controller = new AbortController();
|
|
259
|
+
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
260
|
+
|
|
261
|
+
let decision;
|
|
262
|
+
try {
|
|
263
|
+
const response = await client.messages.create({
|
|
264
|
+
model,
|
|
265
|
+
max_tokens: 1024,
|
|
266
|
+
system: buildInitiativePrompt(snapshot, recentDecisions),
|
|
267
|
+
messages: [{ role: 'user', content: 'Evaluate current state and decide your next action.' }],
|
|
268
|
+
}, { signal: controller.signal });
|
|
269
|
+
|
|
270
|
+
const text = response.content
|
|
271
|
+
.filter(b => b.type === 'text')
|
|
272
|
+
.map(b => b.text)
|
|
273
|
+
.join('');
|
|
274
|
+
|
|
275
|
+
decision = parseDecision(text);
|
|
276
|
+
} finally {
|
|
277
|
+
clearTimeout(timeout);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Gate by autonomy tier
|
|
281
|
+
const domain = decision.domain || 'general';
|
|
282
|
+
const isHighRisk = decision.decision === 'notify';
|
|
283
|
+
|
|
284
|
+
if (decision.decision !== 'noop') {
|
|
285
|
+
// Check cooldown
|
|
286
|
+
if (isOnCooldown(decision, recentDecisions)) {
|
|
287
|
+
logInitiative(opts.trigger || 'scheduled', snapshot, decision.reasoning, 'cooldown', decision.data, getTier(domain));
|
|
288
|
+
return { decision: 'cooldown', reasoning: decision.reasoning };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Proactivity model: start conservative, graduate
|
|
292
|
+
if (!canActAutonomously(domain, isHighRisk)) {
|
|
293
|
+
// Below tier threshold -- log observation but don't act
|
|
294
|
+
logInitiative(opts.trigger || 'scheduled', snapshot, decision.reasoning, 'observed', decision.data, getTier(domain));
|
|
295
|
+
return { decision: 'observed', reasoning: decision.reasoning, original: decision.decision };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Execute the decision
|
|
300
|
+
if (decision.decision !== 'noop') {
|
|
301
|
+
await executeDecision(decision, snapshot);
|
|
302
|
+
}
|
|
303
|
+
logInitiative(opts.trigger || 'scheduled', snapshot, decision.reasoning, decision.decision, decision.data, getTier(domain));
|
|
304
|
+
|
|
305
|
+
return decision;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
runInitiativeLoop,
|
|
310
|
+
parseDecision,
|
|
311
|
+
buildInitiativePrompt,
|
|
312
|
+
isOnCooldown,
|
|
313
|
+
getInitiativeClient,
|
|
314
|
+
// Exported for testing
|
|
315
|
+
_test: { logInitiative, executeDecision, getTier },
|
|
316
|
+
};
|
|
@@ -115,8 +115,8 @@ async function runDueTasks() {
|
|
|
115
115
|
console.error('[tasks] Briefing item consolidation failed:', e.message);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// Notify (skip frequent recurring)
|
|
119
|
-
if (task.type !== 'recurring' || !task.schedule?.match(/every\s+\d+\s*
|
|
118
|
+
// Notify (skip frequent recurring — minutes or seconds intervals)
|
|
119
|
+
if (task.type !== 'recurring' || !task.schedule?.match(/every\s+\d+\s*[ms]/i)) {
|
|
120
120
|
try {
|
|
121
121
|
const { sendNotification } = require('../tools/local-tools');
|
|
122
122
|
await sendNotification('WALL-E Task', task.title + ' — done');
|
|
@@ -326,6 +326,12 @@ function executeScript(taskId, script, checkpoint, extraEnv) {
|
|
|
326
326
|
// ── Chat execution with log streaming ──
|
|
327
327
|
|
|
328
328
|
async function executeChat(taskId, task) {
|
|
329
|
+
// Use multi-turn for complex tasks
|
|
330
|
+
if (task.execution === 'multi-turn') {
|
|
331
|
+
return executeMultiTurnChat(taskId, task);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Single-turn (existing behavior)
|
|
329
335
|
const chatModule = require('../chat');
|
|
330
336
|
const prompt = buildTaskPrompt(task);
|
|
331
337
|
|
|
@@ -344,6 +350,50 @@ async function executeChat(taskId, task) {
|
|
|
344
350
|
return result.reply || '';
|
|
345
351
|
}
|
|
346
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Execute a task using multiple chat turns, maintaining session state.
|
|
355
|
+
* Each turn can use tools, and progress is checkpointed between turns.
|
|
356
|
+
*/
|
|
357
|
+
async function executeMultiTurnChat(taskId, task) {
|
|
358
|
+
const chatModule = require('../chat');
|
|
359
|
+
const sessionId = `task-${task.id}`;
|
|
360
|
+
const MAX_TASK_TURNS = 5;
|
|
361
|
+
let lastReply = '';
|
|
362
|
+
|
|
363
|
+
for (let turn = 0; turn < MAX_TASK_TURNS; turn++) {
|
|
364
|
+
const prompt = turn === 0
|
|
365
|
+
? buildTaskPrompt(task)
|
|
366
|
+
: 'Continue working on this task. Review your progress so far and take the next step. If you are done, include [TASK COMPLETE] in your response.';
|
|
367
|
+
|
|
368
|
+
appendLog(taskId, `Multi-turn ${turn + 1}/${MAX_TASK_TURNS}...`);
|
|
369
|
+
|
|
370
|
+
const result = await chatModule.chat(prompt, {
|
|
371
|
+
channel: 'task',
|
|
372
|
+
session_id: sessionId,
|
|
373
|
+
onProgress: (event) => {
|
|
374
|
+
if (event.type === 'tool_call') appendLog(taskId, event.summary);
|
|
375
|
+
else if (event.type === 'tool_done') appendLog(taskId, `Done: ${event.summary}`);
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
lastReply = result.reply || '';
|
|
380
|
+
appendLog(taskId, `Turn ${turn + 1} result: ${lastReply.slice(0, 200)}`);
|
|
381
|
+
|
|
382
|
+
// Check if task declared itself complete
|
|
383
|
+
if (lastReply.includes('[TASK COMPLETE]') || lastReply.includes('[DONE]')) {
|
|
384
|
+
appendLog(taskId, 'Task self-declared complete');
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Checkpoint between turns
|
|
389
|
+
brain.updateTask(taskId, {
|
|
390
|
+
checkpoint: JSON.stringify({ turn: turn + 1, partial: lastReply.slice(0, 2000) }),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return lastReply;
|
|
395
|
+
}
|
|
396
|
+
|
|
347
397
|
function buildTaskPrompt(task) {
|
|
348
398
|
let prompt = `[TASK] ${task.title}`;
|
|
349
399
|
if (task.description) prompt += `\n\n${task.description}`;
|
|
@@ -394,11 +444,11 @@ function computeNextDue(schedule) {
|
|
|
394
444
|
if (schedule === 'daily') return new Date(now.getTime() + 86400000).toISOString();
|
|
395
445
|
if (schedule === 'weekly') return new Date(now.getTime() + 604800000).toISOString();
|
|
396
446
|
|
|
397
|
-
const everyMatch = schedule.match(/^every\s+(\d+)\s*(m|h|d)$/i);
|
|
447
|
+
const everyMatch = schedule.match(/^every\s+(\d+)\s*(s|m|h|d)$/i);
|
|
398
448
|
if (everyMatch) {
|
|
399
449
|
const n = parseInt(everyMatch[1]);
|
|
400
450
|
const unit = everyMatch[2].toLowerCase();
|
|
401
|
-
const ms = unit === 'h' ? n * 3600000 : unit === 'm' ? n * 60000 : n * 86400000;
|
|
451
|
+
const ms = unit === 's' ? n * 1000 : unit === 'h' ? n * 3600000 : unit === 'm' ? n * 60000 : n * 86400000;
|
|
402
452
|
return new Date(now.getTime() + ms).toISOString();
|
|
403
453
|
}
|
|
404
454
|
|
|
@@ -484,4 +534,4 @@ function consolidateBriefingItems(task, resultText, timestamp) {
|
|
|
484
534
|
}
|
|
485
535
|
}
|
|
486
536
|
|
|
487
|
-
module.exports = { runDueTasks, runTaskById, recoverInterruptedTasks, buildTaskPrompt, computeNextDue, getTaskLogs, clearTaskLogs, stopTask, executeSkill, consolidateBriefingItems, taskLogs };
|
|
537
|
+
module.exports = { runDueTasks, runTaskById, recoverInterruptedTasks, buildTaskPrompt, computeNextDue, getTaskLogs, clearTaskLogs, stopTask, executeSkill, executeMultiTurnChat, consolidateBriefingItems, taskLogs };
|
|
@@ -44,8 +44,10 @@ function readEmails() {
|
|
|
44
44
|
const jxaArgs = ['-l', 'JavaScript', JXA_SCRIPT, '--days-back', String(daysBack)];
|
|
45
45
|
if (syncInbox) jxaArgs.push('--inbox');
|
|
46
46
|
|
|
47
|
+
// Scale timeout with daysBack: 5 min for ≤7 days, up to 2 hours for a full year
|
|
48
|
+
const timeoutMs = daysBack <= 7 ? 300_000 : Math.min(daysBack * 20_000, 7_200_000);
|
|
47
49
|
const result = execFileSync('osascript', jxaArgs, {
|
|
48
|
-
timeout:
|
|
50
|
+
timeout: timeoutMs,
|
|
49
51
|
maxBuffer: 50 * 1024 * 1024,
|
|
50
52
|
});
|
|
51
53
|
|
|
@@ -71,6 +71,47 @@ async function main() {
|
|
|
71
71
|
}
|
|
72
72
|
sections.push(calSection);
|
|
73
73
|
|
|
74
|
+
// ── 1b. Initiative Activity (last 24h) ──
|
|
75
|
+
let initiativeSection = '';
|
|
76
|
+
try {
|
|
77
|
+
const yesterday = new Date(now.getTime() - 24 * 3600000).toISOString();
|
|
78
|
+
const initiatives = db.prepare(`
|
|
79
|
+
SELECT decision, reasoning, decision_data, created_at
|
|
80
|
+
FROM initiative_log
|
|
81
|
+
WHERE decision != 'noop' AND decision != 'cooldown' AND created_at > ?
|
|
82
|
+
ORDER BY created_at DESC LIMIT 10
|
|
83
|
+
`).all(yesterday);
|
|
84
|
+
|
|
85
|
+
if (initiatives.length > 0) {
|
|
86
|
+
initiativeSection = '## WALL-E Activity (last 24h)\n\n';
|
|
87
|
+
for (const init of initiatives) {
|
|
88
|
+
const time = formatTime(init.created_at);
|
|
89
|
+
const action = init.decision === 'observed' ? 'Observed'
|
|
90
|
+
: init.decision === 'notify' ? 'Notified'
|
|
91
|
+
: init.decision === 'create_task' ? 'Created task'
|
|
92
|
+
: init.decision === 'run_skill' ? 'Ran skill'
|
|
93
|
+
: init.decision;
|
|
94
|
+
initiativeSection += `- ${time} -- ${action}: ${init.reasoning || '(no reason)'}\n`;
|
|
95
|
+
}
|
|
96
|
+
initiativeSection += '\n';
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
if (initiativeSection) sections.push(initiativeSection);
|
|
100
|
+
|
|
101
|
+
// ── 1c. Weekly Reflection (Mondays only) ──
|
|
102
|
+
if (now.getDay() === 1) { // Monday
|
|
103
|
+
try {
|
|
104
|
+
const lastWeekly = db.prepare(`
|
|
105
|
+
SELECT summary FROM daily_summaries
|
|
106
|
+
WHERE date > datetime('now', '-3 days') AND summary LIKE '%Weekly%'
|
|
107
|
+
ORDER BY date DESC LIMIT 1
|
|
108
|
+
`).get();
|
|
109
|
+
if (lastWeekly?.summary) {
|
|
110
|
+
sections.push(`## Last Week's Reflection\n\n${lastWeekly.summary.slice(0, 500)}\n`);
|
|
111
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
|
|
74
115
|
// ── 2. Overnight Slack Activity ──
|
|
75
116
|
const hoursBack = 18;
|
|
76
117
|
const cutoff = new Date(now.getTime() - hoursBack * 3600000).toISOString();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: proactive-alerts
|
|
3
|
+
description: >
|
|
4
|
+
Detect and surface time-sensitive items: upcoming meetings, direct @mentions,
|
|
5
|
+
approaching deadlines, failed tasks. Routes alerts through the initiative engine.
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
author: wall-e
|
|
8
|
+
execution: script
|
|
9
|
+
entry: run.js
|
|
10
|
+
trigger:
|
|
11
|
+
type: interval
|
|
12
|
+
schedule: "every 5m"
|
|
13
|
+
tags: [alerts, proactive, mentions, calendar, deadlines]
|
|
14
|
+
permissions:
|
|
15
|
+
- brain:read
|
|
16
|
+
- calendar:read
|
|
17
|
+
---
|
|
18
|
+
# Proactive Alerts
|
|
19
|
+
|
|
20
|
+
Checks for time-sensitive items and outputs structured alert data.
|