create-walle 0.9.13 → 0.9.15
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 +8 -3
- package/bin/create-walle.js +232 -32
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/api-prompts.js +11 -2
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/db.js +94 -75
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
- package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
- package/template/claude-task-manager/fuzzy-utils.js +10 -2
- package/template/claude-task-manager/git-utils.js +140 -10
- package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +38 -5
- package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
- package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
- package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
- package/template/claude-task-manager/lib/session-history.js +309 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/session-stream.js +253 -20
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
- package/template/claude-task-manager/lib/walle-transcript.js +1 -3
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/package.json +1 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +71 -0
- package/template/claude-task-manager/public/index.html +2388 -429
- package/template/claude-task-manager/public/js/message-renderer.js +314 -35
- package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
- package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +396 -55
- package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
- package/template/claude-task-manager/public/js/walle-session.js +234 -26
- package/template/claude-task-manager/public/js/walle.js +143 -2
- package/template/claude-task-manager/server.js +1402 -433
- package/template/claude-task-manager/session-integrity.js +77 -28
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent-runners/claude-code.js +2 -0
- package/template/wall-e/agent.js +63 -8
- package/template/wall-e/api-walle.js +330 -52
- package/template/wall-e/brain.js +291 -42
- package/template/wall-e/chat.js +172 -15
- package/template/wall-e/coding/compaction-service.js +19 -5
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding/workspace-replay.js +1 -4
- package/template/wall-e/coding-orchestrator.js +250 -80
- package/template/wall-e/compat.js +0 -28
- package/template/wall-e/context/context-builder.js +3 -1
- package/template/wall-e/embeddings.js +2 -7
- package/template/wall-e/eval/agent-runner.js +30 -9
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/cc-replay.js +1 -0
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/debug-agent003.js +1 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/eval/run-model-comparison.js +1 -0
- package/template/wall-e/eval/swebench-adapter.js +1 -0
- package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
- package/template/wall-e/extraction/knowledge-extractor.js +1 -2
- package/template/wall-e/lib/mcp-integration.js +336 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/loops/initiative.js +87 -2
- package/template/wall-e/mcp-server.js +872 -19
- package/template/wall-e/memory/ctm-context-client.js +230 -0
- package/template/wall-e/memory/ctm-session-context.js +1376 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
- package/template/wall-e/server.js +30 -1
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
- package/template/wall-e/skills/skill-planner.js +86 -4
- package/template/wall-e/slack/socket-mode-listener.js +276 -0
- package/template/wall-e/telemetry.js +70 -2
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +4 -4
- package/template/builder-journal.md +0 -17
|
@@ -6,6 +6,11 @@ const { validateSkillRequirements } = require('./skill-validator');
|
|
|
6
6
|
const { decideSkillDispatch } = require('./skill-dispatch-decision');
|
|
7
7
|
const { getCurrentSkillSnapshot } = require('./skill-snapshot');
|
|
8
8
|
const { resolveInternalSkill } = require('./internal-skill-registry');
|
|
9
|
+
const {
|
|
10
|
+
OWNER_ENV_KEY: SLACK_OWNER_ENV_KEY,
|
|
11
|
+
getSlackOwnerRepairState,
|
|
12
|
+
repairSlackOwnerIdentity,
|
|
13
|
+
} = require('../tools/slack-owner');
|
|
9
14
|
let telemetry;
|
|
10
15
|
try { telemetry = require('../telemetry'); } catch { telemetry = { trackError() {}, track() {} }; }
|
|
11
16
|
|
|
@@ -59,7 +64,11 @@ function getServiceAlerts({ includeVersionUpdate = false } = {}) {
|
|
|
59
64
|
if (update.latest && update.current && update.latest !== update.current) {
|
|
60
65
|
alerts.push({
|
|
61
66
|
id: 'version_update', service: 'system', type: 'update_available',
|
|
62
|
-
message: `Wall-E ${update.latest} is available (you have ${update.current})
|
|
67
|
+
message: `CTM / Wall-E ${update.latest} is available (you have ${update.current}).`,
|
|
68
|
+
action: 'show_update_wizard',
|
|
69
|
+
action_label: 'Upgrade',
|
|
70
|
+
current: update.current,
|
|
71
|
+
latest: update.latest,
|
|
63
72
|
created_at: update.checked_at,
|
|
64
73
|
});
|
|
65
74
|
}
|
|
@@ -95,10 +104,63 @@ function dismissServiceAlert(alertId) {
|
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
function clearServiceAlerts(service) {
|
|
98
|
-
const alerts = getServiceAlerts().filter(a =>
|
|
107
|
+
const alerts = getServiceAlerts().filter((a) => {
|
|
108
|
+
if (service === 'slack') return !(a.service === 'slack' || (typeof a.service === 'string' && a.service.startsWith('slack-')));
|
|
109
|
+
return a.service !== service;
|
|
110
|
+
});
|
|
99
111
|
brain.setKv('service_alerts', JSON.stringify(alerts));
|
|
100
112
|
}
|
|
101
113
|
|
|
114
|
+
function clearResolvedSlackHealthAlerts({ authenticated = false, ownerConfigured = false } = {}) {
|
|
115
|
+
if (!authenticated) return { cleared: 0 };
|
|
116
|
+
const before = getServiceAlerts();
|
|
117
|
+
const after = before.filter((a) => {
|
|
118
|
+
const service = String(a.service || '');
|
|
119
|
+
const type = String(a.type || '');
|
|
120
|
+
const message = String(a.message || '');
|
|
121
|
+
const slackFamily = service === 'slack' || service.startsWith('slack-');
|
|
122
|
+
if (!slackFamily) return true;
|
|
123
|
+
if (type === 'auth_expired') return false;
|
|
124
|
+
if (ownerConfigured && type === 'owner_identity_missing') return false;
|
|
125
|
+
// `slack-sync` was an older Slack path. A disabled legacy alert should not
|
|
126
|
+
// keep current Slack MCP/OAuth status red once slack-mentions is healthy.
|
|
127
|
+
if (type === 'skill_disabled' && /\bslack-sync\b/.test(message + ' ' + service)) return false;
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
if (after.length !== before.length) {
|
|
131
|
+
brain.setKv('service_alerts', JSON.stringify(after));
|
|
132
|
+
}
|
|
133
|
+
return { cleared: before.length - after.length };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _hasMissingSlackOwner(validation) {
|
|
137
|
+
return !!(validation && Array.isArray(validation.missing)
|
|
138
|
+
&& validation.missing.some(m => m.type === 'env' && m.name === SLACK_OWNER_ENV_KEY));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _addSlackOwnerIdentityAlert() {
|
|
142
|
+
const state = getSlackOwnerRepairState();
|
|
143
|
+
if (state.canRepair) {
|
|
144
|
+
addServiceAlert({
|
|
145
|
+
service: 'slack',
|
|
146
|
+
type: 'owner_identity_missing',
|
|
147
|
+
message: 'Slack sync needs to know which Slack user is you. Wall-E can fix this from your existing Slack connection.',
|
|
148
|
+
action_label: 'Fix automatically',
|
|
149
|
+
action: 'repair_slack_owner',
|
|
150
|
+
action_endpoint: '/api/wall-e/slack/repair-owner',
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
addServiceAlert({
|
|
156
|
+
service: 'slack',
|
|
157
|
+
type: 'owner_identity_missing',
|
|
158
|
+
message: 'Slack sync needs to know which Slack user is you. Connect Slack so Wall-E can configure this automatically.',
|
|
159
|
+
action_label: 'Connect Slack',
|
|
160
|
+
action_url: '/setup.html#slack',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
102
164
|
/**
|
|
103
165
|
* One-time migration: alerts fired before the attemptAuthRecovery fix used
|
|
104
166
|
* the skill name as the service (e.g. service='slack-mentions') which meant
|
|
@@ -307,8 +369,21 @@ async function runDueSkills(opts = {}) {
|
|
|
307
369
|
const hasRequires = loaderSkill && loaderSkill.requires &&
|
|
308
370
|
(loaderSkill.requires.bins || loaderSkill.requires.env || loaderSkill.requires.mcp);
|
|
309
371
|
if (hasRequires) {
|
|
310
|
-
|
|
372
|
+
let validation = validateSkillRequirements(loaderSkill);
|
|
373
|
+
if (!validation.valid && _hasMissingSlackOwner(validation)) {
|
|
374
|
+
const repaired = repairSlackOwnerIdentity({ persist: true });
|
|
375
|
+
if (repaired.ok) {
|
|
376
|
+
console.log(`[skill-planner] Repaired ${SLACK_OWNER_ENV_KEY} from Slack OAuth token for "${skill.name}"`);
|
|
377
|
+
clearServiceAlerts('slack');
|
|
378
|
+
validation = validateSkillRequirements(loaderSkill);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
311
381
|
if (!validation.valid) {
|
|
382
|
+
if (_hasMissingSlackOwner(validation)) {
|
|
383
|
+
console.log(`[skill-planner] Preflight failed for "${skill.name}": Slack owner identity missing`);
|
|
384
|
+
_addSlackOwnerIdentityAlert();
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
312
387
|
const items = validation.missing
|
|
313
388
|
.map(m => `${m.type}=${m.name}`)
|
|
314
389
|
.join(', ');
|
|
@@ -506,4 +581,11 @@ async function runDueSkills(opts = {}) {
|
|
|
506
581
|
|
|
507
582
|
// `isSkillDue` was inlined into `decideSkillDispatch` (Item C).
|
|
508
583
|
|
|
509
|
-
module.exports = {
|
|
584
|
+
module.exports = {
|
|
585
|
+
runDueSkills,
|
|
586
|
+
getServiceAlerts,
|
|
587
|
+
addServiceAlert,
|
|
588
|
+
dismissServiceAlert,
|
|
589
|
+
clearServiceAlerts,
|
|
590
|
+
clearResolvedSlackHealthAlerts,
|
|
591
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_RECONNECT_MS = 5000;
|
|
4
|
+
const MAX_RECONNECT_MS = 60000;
|
|
5
|
+
|
|
6
|
+
function getAppToken(env = process.env) {
|
|
7
|
+
return env.WALLE_SLACK_APP_TOKEN || env.SLACK_APP_TOKEN || env.SLACK_SOCKET_MODE_APP_TOKEN || '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function socketModeDisabled(env = process.env) {
|
|
11
|
+
const value = String(env.WALLE_SLACK_SOCKET_MODE || env.SLACK_SOCKET_MODE || '').toLowerCase();
|
|
12
|
+
return value === '0' || value === 'false' || value === 'off';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isConfigured(env = process.env) {
|
|
16
|
+
const token = getAppToken(env);
|
|
17
|
+
return !socketModeDisabled(env) && /^xapp-/.test(token);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveWebSocketCtor() {
|
|
21
|
+
if (typeof WebSocket === 'function') return WebSocket;
|
|
22
|
+
try { return require('ws'); } catch {}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractSlackEvent(envelope) {
|
|
27
|
+
if (!envelope || envelope.type !== 'events_api') return null;
|
|
28
|
+
const payload = envelope.payload || {};
|
|
29
|
+
if (payload.type !== 'event_callback') return null;
|
|
30
|
+
return payload.event || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isRealtimeMessageEvent(event) {
|
|
34
|
+
if (!event || !event.type) return false;
|
|
35
|
+
if (event.bot_id || event.subtype) return false;
|
|
36
|
+
if (event.type === 'app_mention') return true;
|
|
37
|
+
if (event.type !== 'message') return false;
|
|
38
|
+
return event.channel_type === 'im' || event.channel_type === 'mpim';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class SlackSocketModeListener {
|
|
42
|
+
constructor(opts = {}) {
|
|
43
|
+
this.env = opts.env || process.env;
|
|
44
|
+
this.fetchImpl = opts.fetch || global.fetch;
|
|
45
|
+
this.WebSocketCtor = opts.WebSocketCtor || resolveWebSocketCtor();
|
|
46
|
+
this.handleEvent = opts.handleEvent || defaultHandleEvent;
|
|
47
|
+
this.logger = opts.logger || console;
|
|
48
|
+
this.reconnectBaseMs = opts.reconnectBaseMs || DEFAULT_RECONNECT_MS;
|
|
49
|
+
this.maxReconnectMs = opts.maxReconnectMs || MAX_RECONNECT_MS;
|
|
50
|
+
this.started = false;
|
|
51
|
+
this.running = false;
|
|
52
|
+
this.ws = null;
|
|
53
|
+
this.reconnectTimer = null;
|
|
54
|
+
this.reconnectAttempts = 0;
|
|
55
|
+
this.lastConnectedAt = null;
|
|
56
|
+
this.lastEventAt = null;
|
|
57
|
+
this.lastError = null;
|
|
58
|
+
this._seenEventIds = new Map();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get configured() {
|
|
62
|
+
return isConfigured(this.env);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getStatus() {
|
|
66
|
+
return {
|
|
67
|
+
configured: this.configured,
|
|
68
|
+
enabled: !socketModeDisabled(this.env),
|
|
69
|
+
running: this.running,
|
|
70
|
+
started: this.started,
|
|
71
|
+
last_connected_at: this.lastConnectedAt,
|
|
72
|
+
last_event_at: this.lastEventAt,
|
|
73
|
+
last_error: this.lastError,
|
|
74
|
+
reconnect_attempts: this.reconnectAttempts,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
start() {
|
|
79
|
+
if (this.started) return this.getStatus();
|
|
80
|
+
this.started = true;
|
|
81
|
+
if (!this.configured) {
|
|
82
|
+
this.logger.log?.('[slack-socket] Not configured; set WALLE_SLACK_APP_TOKEN or SLACK_APP_TOKEN to enable Socket Mode');
|
|
83
|
+
return this.getStatus();
|
|
84
|
+
}
|
|
85
|
+
if (!this.WebSocketCtor) {
|
|
86
|
+
this.lastError = 'No WebSocket implementation available';
|
|
87
|
+
this.logger.warn?.('[slack-socket] No WebSocket implementation available');
|
|
88
|
+
return this.getStatus();
|
|
89
|
+
}
|
|
90
|
+
this._connect().catch((err) => {
|
|
91
|
+
this._recordError(err);
|
|
92
|
+
this._scheduleReconnect();
|
|
93
|
+
});
|
|
94
|
+
return this.getStatus();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
stop() {
|
|
98
|
+
this.started = false;
|
|
99
|
+
this.running = false;
|
|
100
|
+
if (this.reconnectTimer) {
|
|
101
|
+
clearTimeout(this.reconnectTimer);
|
|
102
|
+
this.reconnectTimer = null;
|
|
103
|
+
}
|
|
104
|
+
if (this.ws) {
|
|
105
|
+
try { this.ws.close(); } catch {}
|
|
106
|
+
this.ws = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async _connect() {
|
|
111
|
+
if (!this.started || !this.configured) return;
|
|
112
|
+
const appToken = getAppToken(this.env);
|
|
113
|
+
if (!this.fetchImpl) throw new Error('fetch is not available');
|
|
114
|
+
|
|
115
|
+
const resp = await this.fetchImpl('https://slack.com/api/apps.connections.open', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${appToken}`,
|
|
119
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const data = await resp.json();
|
|
123
|
+
if (!data.ok || !data.url) {
|
|
124
|
+
throw new Error(`apps.connections.open failed: ${data.error || resp.status}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ws = new this.WebSocketCtor(data.url);
|
|
128
|
+
this.ws = ws;
|
|
129
|
+
this._bindSocket(ws);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_bindSocket(ws) {
|
|
133
|
+
const onOpen = () => {
|
|
134
|
+
this.running = true;
|
|
135
|
+
this.reconnectAttempts = 0;
|
|
136
|
+
this.lastConnectedAt = new Date().toISOString();
|
|
137
|
+
this.lastError = null;
|
|
138
|
+
this.logger.log?.('[slack-socket] Connected');
|
|
139
|
+
};
|
|
140
|
+
const onMessage = (eventOrData) => {
|
|
141
|
+
const data = eventOrData && Object.prototype.hasOwnProperty.call(eventOrData, 'data')
|
|
142
|
+
? eventOrData.data
|
|
143
|
+
: eventOrData;
|
|
144
|
+
this._handleMessage(data).catch((err) => this._recordError(err));
|
|
145
|
+
};
|
|
146
|
+
const onError = (err) => this._recordError(err);
|
|
147
|
+
const onClose = () => {
|
|
148
|
+
this.running = false;
|
|
149
|
+
if (this.ws === ws) this.ws = null;
|
|
150
|
+
if (this.started) this._scheduleReconnect();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (typeof ws.on === 'function') {
|
|
154
|
+
ws.on('open', onOpen);
|
|
155
|
+
ws.on('message', onMessage);
|
|
156
|
+
ws.on('error', onError);
|
|
157
|
+
ws.on('close', onClose);
|
|
158
|
+
} else {
|
|
159
|
+
ws.onopen = onOpen;
|
|
160
|
+
ws.onmessage = onMessage;
|
|
161
|
+
ws.onerror = onError;
|
|
162
|
+
ws.onclose = onClose;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async _handleMessage(data) {
|
|
167
|
+
const text = Buffer.isBuffer(data)
|
|
168
|
+
? data.toString('utf8')
|
|
169
|
+
: (data instanceof ArrayBuffer)
|
|
170
|
+
? Buffer.from(data).toString('utf8')
|
|
171
|
+
: ArrayBuffer.isView(data)
|
|
172
|
+
? Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8')
|
|
173
|
+
: String(data || '');
|
|
174
|
+
if (!text) return;
|
|
175
|
+
let envelope;
|
|
176
|
+
try {
|
|
177
|
+
envelope = JSON.parse(text);
|
|
178
|
+
} catch {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (envelope.envelope_id) this._ack(envelope.envelope_id);
|
|
183
|
+
if (envelope.type === 'hello') return;
|
|
184
|
+
if (envelope.type === 'disconnect') {
|
|
185
|
+
this.lastError = envelope.reason || 'Slack requested disconnect';
|
|
186
|
+
this._scheduleReconnect();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const event = extractSlackEvent(envelope);
|
|
191
|
+
if (!isRealtimeMessageEvent(event)) return;
|
|
192
|
+
if (this._seenRecently(envelope.payload?.event_id || event.event_ts || event.ts)) return;
|
|
193
|
+
|
|
194
|
+
this.lastEventAt = new Date().toISOString();
|
|
195
|
+
await this.handleEvent(event, { envelope });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_ack(envelopeId) {
|
|
199
|
+
if (!this.ws || typeof this.ws.send !== 'function') return;
|
|
200
|
+
try {
|
|
201
|
+
this.ws.send(JSON.stringify({ envelope_id: envelopeId }));
|
|
202
|
+
} catch (err) {
|
|
203
|
+
this._recordError(err);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_seenRecently(id) {
|
|
208
|
+
if (!id) return false;
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
for (const [key, ts] of this._seenEventIds) {
|
|
211
|
+
if (now - ts > 10 * 60_000) this._seenEventIds.delete(key);
|
|
212
|
+
}
|
|
213
|
+
if (this._seenEventIds.has(id)) return true;
|
|
214
|
+
this._seenEventIds.set(id, now);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_recordError(err) {
|
|
219
|
+
const msg = err && err.message ? err.message : String(err || 'unknown error');
|
|
220
|
+
this.lastError = msg.slice(0, 300);
|
|
221
|
+
this.logger.warn?.(`[slack-socket] ${this.lastError}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_scheduleReconnect() {
|
|
225
|
+
if (!this.started || this.reconnectTimer) return;
|
|
226
|
+
this.running = false;
|
|
227
|
+
this.reconnectAttempts += 1;
|
|
228
|
+
const delay = Math.min(this.reconnectBaseMs * Math.pow(2, Math.max(0, this.reconnectAttempts - 1)), this.maxReconnectMs);
|
|
229
|
+
this.reconnectTimer = setTimeout(() => {
|
|
230
|
+
this.reconnectTimer = null;
|
|
231
|
+
this._connect().catch((err) => {
|
|
232
|
+
this._recordError(err);
|
|
233
|
+
this._scheduleReconnect();
|
|
234
|
+
});
|
|
235
|
+
}, delay);
|
|
236
|
+
if (typeof this.reconnectTimer.unref === 'function') this.reconnectTimer.unref();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function defaultHandleEvent(event) {
|
|
241
|
+
const slackMentions = require('../skills/_bundled/slack-mentions/run');
|
|
242
|
+
return slackMentions.processIncomingSlackEvent(event);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let singleton = null;
|
|
246
|
+
|
|
247
|
+
function startSlackSocketModeListener(opts = {}) {
|
|
248
|
+
if (singleton) return singleton;
|
|
249
|
+
singleton = new SlackSocketModeListener(opts);
|
|
250
|
+
singleton.start();
|
|
251
|
+
return singleton;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getSlackSocketModeStatus() {
|
|
255
|
+
if (singleton) return singleton.getStatus();
|
|
256
|
+
return {
|
|
257
|
+
configured: isConfigured(process.env),
|
|
258
|
+
enabled: !socketModeDisabled(process.env),
|
|
259
|
+
running: false,
|
|
260
|
+
started: false,
|
|
261
|
+
last_connected_at: null,
|
|
262
|
+
last_event_at: null,
|
|
263
|
+
last_error: null,
|
|
264
|
+
reconnect_attempts: 0,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
SlackSocketModeListener,
|
|
270
|
+
startSlackSocketModeListener,
|
|
271
|
+
getSlackSocketModeStatus,
|
|
272
|
+
getAppToken,
|
|
273
|
+
isConfigured,
|
|
274
|
+
extractSlackEvent,
|
|
275
|
+
isRealtimeMessageEvent,
|
|
276
|
+
};
|
|
@@ -9,6 +9,8 @@ const FLUSH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
9
9
|
const MAX_BATCH_SIZE = 100;
|
|
10
10
|
const DATA_DIR = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME || '/tmp', '.walle', 'data');
|
|
11
11
|
const INSTALL_ID_PATH = path.join(DATA_DIR, '.install-id');
|
|
12
|
+
const CLI_LIFECYCLE_PATH = path.join(DATA_DIR, '.cli-lifecycle.jsonl');
|
|
13
|
+
const MACHINE_ID_PATH = process.env.WALL_E_MACHINE_ID_PATH || path.join(path.dirname(DATA_DIR), '.machine-id');
|
|
12
14
|
|
|
13
15
|
// --- Opt-out ---
|
|
14
16
|
function isDisabled() {
|
|
@@ -33,6 +35,27 @@ function getInstallId() {
|
|
|
33
35
|
return _installId;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
// --- Machine bucket (anonymous, stable across reinstall churn) ---
|
|
39
|
+
let _machineBucket = null;
|
|
40
|
+
function getMachineBucket() {
|
|
41
|
+
if (_machineBucket) return _machineBucket;
|
|
42
|
+
let machineId = '';
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(MACHINE_ID_PATH)) {
|
|
45
|
+
machineId = fs.readFileSync(MACHINE_ID_PATH, 'utf8').trim();
|
|
46
|
+
}
|
|
47
|
+
if (!machineId) {
|
|
48
|
+
machineId = crypto.randomUUID();
|
|
49
|
+
fs.mkdirSync(path.dirname(MACHINE_ID_PATH), { recursive: true });
|
|
50
|
+
fs.writeFileSync(MACHINE_ID_PATH, machineId, { encoding: 'utf8', mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
machineId = getInstallId();
|
|
54
|
+
}
|
|
55
|
+
_machineBucket = crypto.createHash('sha256').update(machineId).digest('hex').slice(0, 16);
|
|
56
|
+
return _machineBucket;
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
// --- Local JSONL log (survives crashes, unlike the in-memory buffer) ---
|
|
37
60
|
const LOCAL_LOG_DIR = path.join(process.env.HOME || '/tmp', '.walle', 'logs');
|
|
38
61
|
const LOCAL_LOG_PATH = path.join(LOCAL_LOG_DIR, 'telemetry.jsonl');
|
|
@@ -74,6 +97,7 @@ async function flush() {
|
|
|
74
97
|
const version = getVersion();
|
|
75
98
|
const payload = {
|
|
76
99
|
id: getInstallId(),
|
|
100
|
+
machine: getMachineBucket(),
|
|
77
101
|
v: version,
|
|
78
102
|
os: process.platform,
|
|
79
103
|
node: process.version,
|
|
@@ -102,7 +126,18 @@ async function flush() {
|
|
|
102
126
|
|
|
103
127
|
// --- First-run funnel tracking ---
|
|
104
128
|
const FUNNEL_PATH = path.join(DATA_DIR, '.telemetry-funnel.json');
|
|
105
|
-
const FUNNEL_STEPS = [
|
|
129
|
+
const FUNNEL_STEPS = [
|
|
130
|
+
'install',
|
|
131
|
+
'boot',
|
|
132
|
+
'first_ingest',
|
|
133
|
+
'first_chat',
|
|
134
|
+
'first_skill',
|
|
135
|
+
'upgrade_available',
|
|
136
|
+
'upgrade_prompt_shown',
|
|
137
|
+
'upgrade_prompt_dismissed',
|
|
138
|
+
'upgrade_prompt_command_copied',
|
|
139
|
+
'upgrade_completed',
|
|
140
|
+
];
|
|
106
141
|
|
|
107
142
|
function trackFunnelStep(step) {
|
|
108
143
|
if (isDisabled()) return;
|
|
@@ -133,10 +168,43 @@ function getVersion() {
|
|
|
133
168
|
return _version;
|
|
134
169
|
}
|
|
135
170
|
|
|
171
|
+
function drainCliLifecycleEvents() {
|
|
172
|
+
if (isDisabled()) return;
|
|
173
|
+
let lines = [];
|
|
174
|
+
try {
|
|
175
|
+
if (!fs.existsSync(CLI_LIFECYCLE_PATH)) return;
|
|
176
|
+
lines = fs.readFileSync(CLI_LIFECYCLE_PATH, 'utf8')
|
|
177
|
+
.split(/\r?\n/)
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.slice(-100);
|
|
180
|
+
fs.unlinkSync(CLI_LIFECYCLE_PATH);
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const allowed = new Set([
|
|
185
|
+
'cli_install_started',
|
|
186
|
+
'cli_install_completed',
|
|
187
|
+
'cli_update_started',
|
|
188
|
+
'cli_update_completed',
|
|
189
|
+
]);
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
try {
|
|
192
|
+
const entry = JSON.parse(line);
|
|
193
|
+
if (!entry || !allowed.has(entry.event)) continue;
|
|
194
|
+
const meta = entry.meta && typeof entry.meta === 'object' ? entry.meta : {};
|
|
195
|
+
track(entry.event, {
|
|
196
|
+
...meta,
|
|
197
|
+
age_seconds: entry.t ? Math.max(0, Math.round((Date.now() - entry.t) / 1000)) : undefined,
|
|
198
|
+
});
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
136
203
|
// --- Lifecycle ---
|
|
137
204
|
function start() {
|
|
138
205
|
if (isDisabled()) return;
|
|
139
206
|
if (flushTimer) return;
|
|
207
|
+
drainCliLifecycleEvents();
|
|
140
208
|
track('startup', {
|
|
141
209
|
uptime: process.uptime(),
|
|
142
210
|
});
|
|
@@ -207,4 +275,4 @@ function trackCompatUsage(features) {
|
|
|
207
275
|
track('compat_usage', features);
|
|
208
276
|
}
|
|
209
277
|
|
|
210
|
-
module.exports = { track, trackError, flush, start, stop, isDisabled, getInstallId, getVersion, printNoticeIfFirstRun, trackFunnelStep, trackCompatUsage };
|
|
278
|
+
module.exports = { track, trackError, flush, start, stop, isDisabled, getInstallId, getMachineBucket, getVersion, printNoticeIfFirstRun, trackFunnelStep, trackCompatUsage, drainCliLifecycleEvents };
|
|
@@ -32,8 +32,32 @@ function registerBuiltinMiddleware(mw, opts = {}) {
|
|
|
32
32
|
if (!ctx.params) ctx.params = {};
|
|
33
33
|
// Deterministic output for coding
|
|
34
34
|
ctx.params.temperature = 0;
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const provider = ctx.provider || opts.provider;
|
|
36
|
+
const model = String(ctx.model || opts.model || '');
|
|
37
|
+
const isOllama = provider === 'ollama';
|
|
38
|
+
const isGemma4 = isOllama && /^gemma4:/i.test(model);
|
|
39
|
+
if (isGemma4) {
|
|
40
|
+
const benchmarkMode = Boolean(opts.benchmark || ctx.benchmark);
|
|
41
|
+
const noToolsTurn = ctx.toolsAvailable === false;
|
|
42
|
+
// Gemma4's Ollama thinking trace shares the output budget with final
|
|
43
|
+
// content. Small coding budgets can be consumed entirely by reasoning.
|
|
44
|
+
const floor = /26b/i.test(model) ? 6144 : 8192;
|
|
45
|
+
if (benchmarkMode && noToolsTurn) {
|
|
46
|
+
ctx.params.maxTokens = Math.min(ctx.params.maxTokens || 1024, 1024);
|
|
47
|
+
} else {
|
|
48
|
+
ctx.params.maxTokens = Math.max(ctx.params.maxTokens || 4096, floor);
|
|
49
|
+
}
|
|
50
|
+
// Keep structured tool-call turns direct. Thinking traces improve
|
|
51
|
+
// no-tool planning/wrap-up turns but can crowd out or destabilize JSON
|
|
52
|
+
// function-call arguments during active edit/test loops.
|
|
53
|
+
if (ctx.params.thinking == null) ctx.params.thinking = noToolsTurn && !benchmarkMode;
|
|
54
|
+
ctx.params.options = {
|
|
55
|
+
...(ctx.params.options || {}),
|
|
56
|
+
num_ctx: Math.max(ctx.params.options?.num_ctx || 0, 16384),
|
|
57
|
+
};
|
|
58
|
+
} else if (isOllama) {
|
|
59
|
+
// Cap generic local models; many smaller tags degrade if asked for huge
|
|
60
|
+
// coding turns. Gemma4 is handled separately above.
|
|
37
61
|
ctx.params.maxTokens = Math.min(ctx.params.maxTokens || 4096, 2048);
|
|
38
62
|
}
|
|
39
63
|
});
|
|
@@ -72,6 +96,18 @@ Do not make any edits. Review the diff and assess quality.`);
|
|
|
72
96
|
- minimize output tokens. Use tools, not words.
|
|
73
97
|
- Never introduce code that exposes, logs, or commits secrets.`);
|
|
74
98
|
|
|
99
|
+
const provider = ctx.provider || opts.provider;
|
|
100
|
+
const model = String(ctx.model || opts.model || '');
|
|
101
|
+
if (provider === 'ollama' && /^gemma4:/i.test(model)) {
|
|
102
|
+
sections.push(`## Gemma4 Coding Guardrails
|
|
103
|
+
- Before each edit, silently identify the smallest complete syntactic unit you are changing.
|
|
104
|
+
- Insert new sibling code between complete blocks. Do not replace a function, route, class, or object entry header with unrelated code.
|
|
105
|
+
- If the task says add, preserve existing behavior. For route/handler additions, keep every existing route/handler block and insert the new one as a sibling.
|
|
106
|
+
- For edit_file, use an old_string that includes enough surrounding context to be unique and preserves neighboring declarations.
|
|
107
|
+
- Do not run long-lived servers in the foreground with run_shell. Prefer the project test command; if a server must be started, use a bounded/background command.
|
|
108
|
+
- After one failed or timed-out verification command, read the error/output and inspect the edited code before running the same command again.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
75
111
|
// Tool workflow patterns (loaded once at startup)
|
|
76
112
|
if (workflowDocs) sections.push(workflowDocs);
|
|
77
113
|
|
|
@@ -137,7 +173,18 @@ Do not make any edits. Review the diff and assess quality.`);
|
|
|
137
173
|
let portsBefore = null;
|
|
138
174
|
mw.use('tool.before', async (ctx, toolName, input) => {
|
|
139
175
|
if (toolName !== 'run_shell') return input;
|
|
176
|
+
const provider = ctx.provider || opts.provider;
|
|
177
|
+
const model = String(ctx.model || opts.model || '');
|
|
140
178
|
const { isLikelyServerCommand, getListeningPorts } = require('./port-detector');
|
|
179
|
+
if (opts.benchmark && provider === 'ollama' && /^gemma4:/i.test(model) &&
|
|
180
|
+
isLikelyServerCommand(input.command) && !isBoundedServerCommand(input.command)) {
|
|
181
|
+
portsBefore = null;
|
|
182
|
+
return {
|
|
183
|
+
...input,
|
|
184
|
+
command: 'node -e "console.error(\'Blocked foreground server command in benchmark; run npm test or use a bounded/background server command instead.\'); process.exit(1)"',
|
|
185
|
+
timeout_ms: Math.min(input.timeout_ms || 5000, 5000),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
141
188
|
if (isLikelyServerCommand(input.command)) {
|
|
142
189
|
portsBefore = getListeningPorts();
|
|
143
190
|
} else {
|
|
@@ -164,4 +211,10 @@ Do not make any edits. Review the diff and assess quality.`);
|
|
|
164
211
|
});
|
|
165
212
|
}
|
|
166
213
|
|
|
214
|
+
function isBoundedServerCommand(command = '') {
|
|
215
|
+
const text = String(command || '');
|
|
216
|
+
return /(?:^|[;&|]\s*)(?:timeout|gtimeout)\s+\d+/i.test(text) ||
|
|
217
|
+
/(?:&\s*$|\bnohup\b|\bsetsid\b)/i.test(text);
|
|
218
|
+
}
|
|
219
|
+
|
|
167
220
|
module.exports = { registerBuiltinMiddleware };
|
|
@@ -17,7 +17,7 @@ const SHELL_ALLOWLIST = new Set([
|
|
|
17
17
|
// Network
|
|
18
18
|
'curl', 'wget', 'ping', 'dig', 'host', 'nslookup',
|
|
19
19
|
// Dev tools
|
|
20
|
-
'git', 'node', 'npm', 'npx', 'python3', 'pip3', 'bun', 'deno',
|
|
20
|
+
'git', 'node', 'npm', 'npx', 'python', 'python3', 'pip', 'pip3', 'pytest', 'bun', 'deno',
|
|
21
21
|
'make', 'cargo', 'go', 'ruby', 'perl', 'tsc',
|
|
22
22
|
// Cloud / infra
|
|
23
23
|
'fly', 'docker', 'kubectl',
|