create-walle 0.9.11 → 0.9.13
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 -3
- package/package.json +2 -2
- package/template/bin/dev.sh +7 -1
- package/template/bin/setup.js +53 -9
- package/template/bin/sync-images.js +53 -0
- package/template/builder-journal.md +17 -0
- package/template/claude-task-manager/api-prompts.js +98 -13
- package/template/claude-task-manager/api-reviews.js +82 -5
- package/template/claude-task-manager/db.js +32 -5
- package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
- package/template/claude-task-manager/lib/session-capture.js +421 -0
- package/template/claude-task-manager/lib/session-history.js +135 -15
- package/template/claude-task-manager/lib/session-jobs.js +10 -5
- package/template/claude-task-manager/lib/session-stream.js +87 -19
- package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
- package/template/claude-task-manager/lib/walle-session-context.js +61 -0
- package/template/claude-task-manager/lib/walle-transcript.js +176 -0
- package/template/claude-task-manager/public/css/setup.css +35 -8
- package/template/claude-task-manager/public/css/walle-session.css +56 -0
- package/template/claude-task-manager/public/css/walle.css +120 -0
- package/template/claude-task-manager/public/index.html +814 -181
- package/template/claude-task-manager/public/js/message-renderer.js +148 -19
- package/template/claude-task-manager/public/js/reviews.js +120 -62
- package/template/claude-task-manager/public/js/setup.js +75 -31
- package/template/claude-task-manager/public/js/stream-view.js +115 -55
- package/template/claude-task-manager/public/js/walle-session.js +84 -2
- package/template/claude-task-manager/public/js/walle.js +308 -54
- package/template/claude-task-manager/server.js +1092 -146
- package/template/claude-task-manager/session-integrity.js +181 -54
- package/template/claude-task-manager/session-utils.js +123 -41
- package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
- package/template/package.json +1 -1
- package/template/wall-e/adapters/ctm.js +39 -18
- package/template/wall-e/agent-runners/contract.js +17 -0
- package/template/wall-e/agent-runners/index.js +22 -0
- package/template/wall-e/agent-runtime/harness.js +212 -0
- package/template/wall-e/agent-runtime/index.js +8 -0
- package/template/wall-e/agent-runtime/registry.js +67 -0
- package/template/wall-e/agent-runtime/session-store.js +179 -0
- package/template/wall-e/agent-runtime/spawn.js +208 -0
- package/template/wall-e/api-walle.js +174 -7
- package/template/wall-e/brain.js +266 -28
- package/template/wall-e/channels/policy.js +88 -0
- package/template/wall-e/channels/registry.js +15 -1
- package/template/wall-e/channels/reply-dispatcher.js +70 -0
- package/template/wall-e/channels/session-bindings.js +51 -0
- package/template/wall-e/chat/code-review-context.js +29 -0
- package/template/wall-e/chat.js +188 -42
- package/template/wall-e/coding/acp-adapter.js +188 -0
- package/template/wall-e/coding/agent-catalog.js +129 -0
- package/template/wall-e/coding/compaction-service.js +247 -0
- package/template/wall-e/coding/execution-trace.js +3 -0
- package/template/wall-e/coding/instruction-service.js +224 -0
- package/template/wall-e/coding/model-message.js +67 -0
- package/template/wall-e/coding/permission-rules-store.js +111 -0
- package/template/wall-e/coding/permission-service.js +266 -0
- package/template/wall-e/coding/prompt-bundle.js +67 -0
- package/template/wall-e/coding/prompt-runtime.js +243 -0
- package/template/wall-e/coding/provider-transform.js +188 -0
- package/template/wall-e/coding/runtime-mode.js +132 -0
- package/template/wall-e/coding/snapshot-service.js +155 -0
- package/template/wall-e/coding/stream-processor.js +268 -0
- package/template/wall-e/coding/task-tool.js +255 -0
- package/template/wall-e/coding/tool-registry.js +361 -0
- package/template/wall-e/coding/transcript-writer.js +143 -0
- package/template/wall-e/coding/workspace-replay.js +324 -0
- package/template/wall-e/coding-context.js +4 -22
- package/template/wall-e/coding-orchestrator.js +307 -18
- package/template/wall-e/coding-prompts.js +44 -3
- package/template/wall-e/context/context-builder.js +43 -1
- package/template/wall-e/context/topic-matcher.js +1 -1
- package/template/wall-e/eval/agent-runner.js +59 -13
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
- package/template/wall-e/eval/benchmarks.js +100 -16
- package/template/wall-e/eval/eval-orchestrator.js +218 -8
- package/template/wall-e/eval/harvester.js +62 -5
- package/template/wall-e/eval/head-to-head.js +23 -2
- package/template/wall-e/eval/humaneval-adapter.js +30 -5
- package/template/wall-e/eval/livecodebench-adapter.js +29 -5
- package/template/wall-e/eval/manifest.js +186 -0
- package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
- package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
- package/template/wall-e/eval/session-transcripts.js +57 -4
- package/template/wall-e/eval/swebench-adapter.js +109 -3
- package/template/wall-e/evaluation/agent-router.js +53 -1
- package/template/wall-e/evaluation/coding-quorum.js +48 -1
- package/template/wall-e/evaluation/router.js +4 -2
- package/template/wall-e/evaluation/tier-selector.js +11 -1
- package/template/wall-e/extraction/contradiction.js +2 -2
- package/template/wall-e/extraction/indexer.js +2 -1
- package/template/wall-e/extraction/knowledge-extractor.js +2 -2
- package/template/wall-e/hooks/cli.js +92 -0
- package/template/wall-e/hooks/discovery.js +119 -0
- package/template/wall-e/hooks/index.js +7 -0
- package/template/wall-e/hooks/manifest.js +55 -0
- package/template/wall-e/hooks/runtime.js +84 -0
- package/template/wall-e/hooks/session-memory.js +225 -0
- package/template/wall-e/http/auth.js +6 -2
- package/template/wall-e/http/chat-api.js +54 -8
- package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
- package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
- package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
- package/template/wall-e/listening/calendar.js +3 -1
- package/template/wall-e/llm/client.js +64 -10
- package/template/wall-e/llm/google.js +39 -5
- package/template/wall-e/llm/ollama.js +1 -1
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/provider-availability.js +10 -0
- package/template/wall-e/llm/provider-error.js +269 -0
- package/template/wall-e/llm/tool-adapter.js +48 -12
- package/template/wall-e/loops/boot.js +2 -1
- package/template/wall-e/loops/initiative.js +2 -2
- package/template/wall-e/loops/tasks.js +8 -47
- package/template/wall-e/loops/workspace-prompts.js +20 -0
- package/template/wall-e/mcp-server.js +442 -1
- package/template/wall-e/memory/session-ingest-service.js +159 -0
- package/template/wall-e/memory/source-indexer.js +289 -0
- package/template/wall-e/plugins/discovery.js +83 -0
- package/template/wall-e/plugins/manifest-loader.js +50 -10
- package/template/wall-e/plugins/manifest-schema.js +69 -0
- package/template/wall-e/plugins/model-catalog.js +55 -0
- package/template/wall-e/prompts/coding/base.txt +2 -0
- package/template/wall-e/prompts/coding/deepseek.txt +1 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
- package/template/wall-e/prompts/coding/plan.txt +1 -0
- package/template/wall-e/runtime/execution-trace.js +220 -0
- package/template/wall-e/security/audit.js +266 -0
- package/template/wall-e/security/ssrf.js +236 -0
- package/template/wall-e/session-files.js +303 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
- package/template/wall-e/skills/internal-skill-registry.js +2 -2
- package/template/wall-e/skills/script-skill-runner.js +143 -0
- package/template/wall-e/skills/skill-executor.js +5 -6
- package/template/wall-e/skills/skill-fallback.js +3 -1
- package/template/wall-e/skills/skill-harness-registry.js +7 -8
- package/template/wall-e/skills/skill-planner.js +52 -4
- package/template/wall-e/skills/slack-ingest.js +11 -3
- package/template/wall-e/sources/base.js +90 -0
- package/template/wall-e/sources/builtin.js +33 -0
- package/template/wall-e/sources/claude-code-jsonl.js +78 -0
- package/template/wall-e/sources/codex-jsonl.js +125 -0
- package/template/wall-e/sources/coding-session-utils.js +117 -0
- package/template/wall-e/sources/contract-suite.js +59 -0
- package/template/wall-e/sources/gemini-jsonl.js +85 -0
- package/template/wall-e/sources/index.js +9 -0
- package/template/wall-e/sources/jsonl-utils.js +181 -0
- package/template/wall-e/sources/record-types.js +252 -0
- package/template/wall-e/sources/registry.js +92 -0
- package/template/wall-e/sources/transforms.js +100 -0
- package/template/wall-e/sources/walle-jsonl.js +108 -0
- package/template/wall-e/tools/coding-middleware.js +31 -1
- package/template/wall-e/tools/file-tracker.js +25 -1
- package/template/wall-e/tools/local-tools.js +75 -47
- package/template/wall-e/tools/session-sharing.js +68 -1
- package/template/wall-e/tools/shell-analyzer.js +1 -1
- package/template/wall-e/tools/shell-policy.js +47 -0
- package/template/wall-e/tools/snapshot.js +42 -0
- package/template/wall-e/training/harvester.js +62 -5
- package/template/wall-e/utils/repair.js +253 -1
- package/template/website/index.html +3 -3
- package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const { spawn } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const { createSessionIngestService } = require('../memory/session-ingest-service');
|
|
10
|
+
const { collectIngestRecords } = require('../sources/base');
|
|
11
|
+
const { ensureBuiltinSourceAdapters } = require('../sources/builtin');
|
|
12
|
+
const sourceRegistry = require('../sources/registry');
|
|
13
|
+
|
|
14
|
+
const SAVE_INTERVAL = 15;
|
|
15
|
+
const SUPPORTED_HARNESSES = new Set(['claude-code', 'codex', 'walle']);
|
|
16
|
+
|
|
17
|
+
function getHookStateDir() {
|
|
18
|
+
return process.env.WALL_E_HOOK_STATE_DIR || path.join(os.homedir(), '.walle', 'hook_state');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseHarnessInput(data = {}, harness = 'walle') {
|
|
22
|
+
if (!SUPPORTED_HARNESSES.has(harness)) throw new Error(`Unsupported harness: ${harness}`);
|
|
23
|
+
const transcriptPath = data.transcript_path
|
|
24
|
+
|| data.transcriptPath
|
|
25
|
+
|| data.transcript
|
|
26
|
+
|| data.session_file
|
|
27
|
+
|| data.sessionFile
|
|
28
|
+
|| data.path
|
|
29
|
+
|| data.file
|
|
30
|
+
|| '';
|
|
31
|
+
const cwd = data.cwd || data.workspace || data.project_dir || data.projectDir || '';
|
|
32
|
+
return {
|
|
33
|
+
harness,
|
|
34
|
+
transcriptPath,
|
|
35
|
+
cwd,
|
|
36
|
+
sessionId: data.session_id || data.sessionId || data.id || '',
|
|
37
|
+
stopHookActive: Boolean(data.stop_hook_active || data.stopHookActive),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateTranscriptPath(transcriptPath) {
|
|
42
|
+
if (!transcriptPath || typeof transcriptPath !== 'string') {
|
|
43
|
+
throw new Error('transcript path is required');
|
|
44
|
+
}
|
|
45
|
+
if (transcriptPath.split(/[\\/]+/).includes('..')) {
|
|
46
|
+
throw new Error('transcript path must not contain traversal segments');
|
|
47
|
+
}
|
|
48
|
+
if (!transcriptPath.endsWith('.jsonl')) {
|
|
49
|
+
throw new Error('transcript path must end in .jsonl');
|
|
50
|
+
}
|
|
51
|
+
return path.resolve(transcriptPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function adapterIdForHarness(harness, transcriptPath = '') {
|
|
55
|
+
if (harness === 'codex') return 'codex-jsonl';
|
|
56
|
+
if (harness === 'claude-code') return 'claude-code-jsonl';
|
|
57
|
+
if (harness === 'walle') return 'walle-jsonl';
|
|
58
|
+
if (transcriptPath.includes(`${path.sep}.codex${path.sep}`)) return 'codex-jsonl';
|
|
59
|
+
if (transcriptPath.includes(`${path.sep}.claude${path.sep}`)) return 'claude-code-jsonl';
|
|
60
|
+
return 'walle-jsonl';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sourceRefFromHarness(data = {}, harness = 'walle') {
|
|
64
|
+
const parsed = parseHarnessInput(data, harness);
|
|
65
|
+
const sourceFile = validateTranscriptPath(parsed.transcriptPath);
|
|
66
|
+
const adapterId = adapterIdForHarness(parsed.harness, sourceFile);
|
|
67
|
+
return {
|
|
68
|
+
adapterId,
|
|
69
|
+
uri: sourceFile,
|
|
70
|
+
sourceFile,
|
|
71
|
+
sourceId: parsed.sessionId ? `${adapterId}:${parsed.sessionId}` : undefined,
|
|
72
|
+
cwd: parsed.cwd,
|
|
73
|
+
metadata: { harness: parsed.harness },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function countHumanMessages(sourceRef) {
|
|
78
|
+
ensureBuiltinSourceAdapters();
|
|
79
|
+
const adapter = sourceRegistry.instantiate(sourceRef.adapterId);
|
|
80
|
+
if (!adapter) throw new Error(`Unknown source adapter: ${sourceRef.adapterId}`);
|
|
81
|
+
const { records } = await collectIngestRecords(adapter, sourceRef);
|
|
82
|
+
return records.filter((record) => record.type === 'memory_record' && record.role === 'user').length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readState(sourceRef) {
|
|
86
|
+
const statePath = stateFilePath(sourceRef);
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
89
|
+
} catch {
|
|
90
|
+
return { lastHumanCount: 0 };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function writeState(sourceRef, state) {
|
|
95
|
+
const statePath = stateFilePath(sourceRef);
|
|
96
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
97
|
+
fs.writeFileSync(statePath, JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleSessionStart(data = {}, { harness = 'walle' } = {}) {
|
|
101
|
+
const sourceRef = sourceRefFromHarness(data, harness);
|
|
102
|
+
writeState(sourceRef, { lastHumanCount: 0, sourceRef });
|
|
103
|
+
return { ok: true, action: 'session_start', sourceRef };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function handleStop(data = {}, {
|
|
107
|
+
harness = 'walle',
|
|
108
|
+
saveInterval = SAVE_INTERVAL,
|
|
109
|
+
service = null,
|
|
110
|
+
background = false,
|
|
111
|
+
} = {}) {
|
|
112
|
+
const parsed = parseHarnessInput(data, harness);
|
|
113
|
+
if (parsed.stopHookActive) return { ok: true, action: 'skip_active_stop_hook' };
|
|
114
|
+
const sourceRef = sourceRefFromHarness(data, harness);
|
|
115
|
+
const humanCount = await countHumanMessages(sourceRef);
|
|
116
|
+
const state = readState(sourceRef);
|
|
117
|
+
const sinceLast = humanCount - Number(state.lastHumanCount || 0);
|
|
118
|
+
if (humanCount === 0 || sinceLast < saveInterval) {
|
|
119
|
+
return { ok: true, action: 'skip_interval', humanCount, sinceLast, sourceRef };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
writeState(sourceRef, { ...state, lastHumanCount: humanCount, sourceRef });
|
|
123
|
+
if (background && !service) {
|
|
124
|
+
const spawned = spawnBackgroundIngest(sourceRef);
|
|
125
|
+
return { ok: true, action: 'background_ingest', humanCount, sinceLast, pid: spawned.pid, sourceRef };
|
|
126
|
+
}
|
|
127
|
+
const ingest = await runLockedIngest(sourceRef, { service });
|
|
128
|
+
return { ok: true, action: 'ingested', humanCount, sinceLast, ingest, sourceRef };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function handlePrecompact(data = {}, { harness = 'walle', service = null } = {}) {
|
|
132
|
+
const sourceRef = sourceRefFromHarness(data, harness);
|
|
133
|
+
const ingest = await runLockedIngest(sourceRef, { service });
|
|
134
|
+
return { ok: true, action: 'precompact_ingested', ingest, sourceRef };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runLockedIngest(sourceRef, { service = null, dryRun = false } = {}) {
|
|
138
|
+
const release = acquireLock(sourceRef);
|
|
139
|
+
if (!release.acquired) return { skipped: true, reason: 'lock_exists', lockPath: release.lockPath };
|
|
140
|
+
try {
|
|
141
|
+
const ingestService = service || createSessionIngestService();
|
|
142
|
+
return await ingestService.ingestSource(sourceRef, { dryRun });
|
|
143
|
+
} finally {
|
|
144
|
+
release();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function spawnBackgroundIngest(sourceRef) {
|
|
149
|
+
const cli = path.join(__dirname, 'cli.js');
|
|
150
|
+
const child = spawn(process.execPath, [
|
|
151
|
+
cli,
|
|
152
|
+
'ingest-file',
|
|
153
|
+
'--adapter',
|
|
154
|
+
sourceRef.adapterId,
|
|
155
|
+
'--file',
|
|
156
|
+
sourceRef.sourceFile,
|
|
157
|
+
'--source-id',
|
|
158
|
+
sourceRef.sourceId || '',
|
|
159
|
+
'--cwd',
|
|
160
|
+
sourceRef.cwd || '',
|
|
161
|
+
], {
|
|
162
|
+
detached: true,
|
|
163
|
+
stdio: 'ignore',
|
|
164
|
+
});
|
|
165
|
+
child.unref();
|
|
166
|
+
return child;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function acquireLock(sourceRef) {
|
|
170
|
+
const lockPath = lockFilePath(sourceRef);
|
|
171
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
172
|
+
let fd = null;
|
|
173
|
+
try {
|
|
174
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
175
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), sourceRef }));
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err.code === 'EEXIST') {
|
|
178
|
+
const skipped = () => false;
|
|
179
|
+
skipped.acquired = false;
|
|
180
|
+
skipped.lockPath = lockPath;
|
|
181
|
+
return skipped;
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
} finally {
|
|
185
|
+
if (fd != null) fs.closeSync(fd);
|
|
186
|
+
}
|
|
187
|
+
const release = () => {
|
|
188
|
+
try { fs.unlinkSync(lockPath); } catch {}
|
|
189
|
+
return true;
|
|
190
|
+
};
|
|
191
|
+
release.acquired = true;
|
|
192
|
+
release.lockPath = lockPath;
|
|
193
|
+
return release;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function stateFilePath(sourceRef) {
|
|
197
|
+
return path.join(getHookStateDir(), `${hashSource(sourceRef)}.json`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function lockFilePath(sourceRef) {
|
|
201
|
+
return path.join(getHookStateDir(), `${hashSource(sourceRef)}.lock`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function hashSource(sourceRef) {
|
|
205
|
+
return crypto.createHash('sha256')
|
|
206
|
+
.update(`${sourceRef.adapterId}:${sourceRef.sourceFile || sourceRef.uri || sourceRef.sourceId}`)
|
|
207
|
+
.digest('hex')
|
|
208
|
+
.slice(0, 24);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
SAVE_INTERVAL,
|
|
213
|
+
SUPPORTED_HARNESSES,
|
|
214
|
+
acquireLock,
|
|
215
|
+
adapterIdForHarness,
|
|
216
|
+
countHumanMessages,
|
|
217
|
+
getHookStateDir,
|
|
218
|
+
handlePrecompact,
|
|
219
|
+
handleSessionStart,
|
|
220
|
+
handleStop,
|
|
221
|
+
parseHarnessInput,
|
|
222
|
+
runLockedIngest,
|
|
223
|
+
sourceRefFromHarness,
|
|
224
|
+
validateTranscriptPath,
|
|
225
|
+
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Public routes are intentionally limited to
|
|
4
|
-
//
|
|
3
|
+
// Public routes are intentionally limited to external ingress. Telemetry admin
|
|
4
|
+
// routes are public only at this outer gate; api-walle enforces
|
|
5
|
+
// WALLE_TELEMETRY_SECRET before returning any data.
|
|
5
6
|
const PUBLIC_ROUTES = new Set([
|
|
6
7
|
'/api/wall-e/health',
|
|
7
8
|
'/api/wall-e/webhook',
|
|
9
|
+
'/api/wall-e/telemetry/ingest',
|
|
10
|
+
'/api/wall-e/telemetry/summary',
|
|
11
|
+
'/api/wall-e/telemetry/events',
|
|
8
12
|
]);
|
|
9
13
|
|
|
10
14
|
function getAllowedOrigin(req) {
|
|
@@ -47,9 +47,17 @@ function buildSmartRoutingCandidates(brain, opts = {}) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const currentProvider = String(opts.currentProvider || '').toLowerCase();
|
|
50
|
+
const currentModel = String(opts.currentModel || '').toLowerCase();
|
|
50
51
|
const providerOrder = [currentProvider, 'openai', 'google', 'deepseek', 'anthropic', 'ollama', 'mlx']
|
|
51
52
|
.filter(Boolean)
|
|
52
53
|
.filter((value, idx, arr) => arr.indexOf(value) === idx);
|
|
54
|
+
let providerHealth = null;
|
|
55
|
+
try {
|
|
56
|
+
const { default: providerAvailability } = require('../llm/provider-availability');
|
|
57
|
+
providerHealth = new Map(
|
|
58
|
+
providerAvailability.getConfiguredProviders().map((provider) => [provider.providerId, provider]),
|
|
59
|
+
);
|
|
60
|
+
} catch {}
|
|
53
61
|
|
|
54
62
|
const candidates = [];
|
|
55
63
|
const rows = brain.listAllModels() || [];
|
|
@@ -57,9 +65,12 @@ function buildSmartRoutingCandidates(brain, opts = {}) {
|
|
|
57
65
|
if (!model || model.enabled === 0 || model.enabled === false) continue;
|
|
58
66
|
const provider = brain.getModelProvider(model.provider_id);
|
|
59
67
|
if (!providerHasRuntimeAccess(provider)) continue;
|
|
68
|
+
const providerId = provider.id || model.provider_id;
|
|
69
|
+
if (providerHealth?.get(providerId)?.status === 'unhealthy') continue;
|
|
60
70
|
candidates.push({
|
|
61
71
|
registryId: model.id,
|
|
62
72
|
providerType: String(provider.type || model.provider_type || '').toLowerCase(),
|
|
73
|
+
providerId,
|
|
63
74
|
modelId: model.model_id,
|
|
64
75
|
displayName: model.display_name,
|
|
65
76
|
speedTier: model.speed_tier == null ? 3 : Number(model.speed_tier),
|
|
@@ -88,11 +99,24 @@ function buildSmartRoutingCandidates(brain, opts = {}) {
|
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
if (opts.includeAutoFirst) {
|
|
102
|
+
const isCurrentModel = (candidate) => {
|
|
103
|
+
if (!currentModel) return false;
|
|
104
|
+
return String(candidate.modelId || '').toLowerCase() === currentModel
|
|
105
|
+
|| String(candidate.registryId || '').toLowerCase() === currentModel;
|
|
106
|
+
};
|
|
107
|
+
const sameProviderAlternative = currentProvider && currentModel
|
|
108
|
+
? candidates.find((candidate) => candidate.providerType === currentProvider && !isCurrentModel(candidate))
|
|
109
|
+
: null;
|
|
91
110
|
const alternatives = selected
|
|
111
|
+
.filter((candidate) => !sameProviderAlternative || candidate.registryId !== sameProviderAlternative.registryId)
|
|
92
112
|
.filter((candidate) => !currentProvider || candidate.providerType !== currentProvider)
|
|
93
113
|
.map((candidate) => candidate.registryId)
|
|
94
114
|
.filter(Boolean);
|
|
95
|
-
|
|
115
|
+
const ordered = [
|
|
116
|
+
sameProviderAlternative?.registryId,
|
|
117
|
+
...alternatives,
|
|
118
|
+
].filter(Boolean);
|
|
119
|
+
return ordered.length > 0 ? [null, ...ordered.slice(0, 5)] : [];
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
return selected.map((candidate) => candidate.registryId).filter(Boolean).slice(0, 6);
|
|
@@ -114,9 +138,11 @@ function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
|
|
|
114
138
|
let requestFinished = false;
|
|
115
139
|
try {
|
|
116
140
|
const abortController = new AbortController();
|
|
117
|
-
res.on
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
if (typeof res.on === 'function') {
|
|
142
|
+
res.on('close', () => {
|
|
143
|
+
if (!requestFinished) abortController.abort();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
120
146
|
const chatModule = require('../chat');
|
|
121
147
|
// Validate + parse attachments before invoking chat. The validator
|
|
122
148
|
// throws AttachmentError with .code + .status so we can surface 4xx
|
|
@@ -198,10 +224,19 @@ function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
|
|
|
198
224
|
const shouldSmartFallback = !body.model && parsedAttachments.length === 0;
|
|
199
225
|
if (shouldSmartFallback) {
|
|
200
226
|
try {
|
|
201
|
-
const { getDefaultProviderType } = require('../llm/client');
|
|
202
|
-
const currentProvider = body.provider
|
|
227
|
+
const { getDefaultModel, getDefaultProviderType } = require('../llm/client');
|
|
228
|
+
const currentProvider = body.provider
|
|
229
|
+
|| brain.getKv?.('walle_provider')
|
|
230
|
+
|| process.env.WALLE_PROVIDER
|
|
231
|
+
|| getDefaultProviderType();
|
|
232
|
+
const currentModel = body.model
|
|
233
|
+
|| brain.getKv?.('walle_model')
|
|
234
|
+
|| brain.getKv?.('walle_model_' + currentProvider)
|
|
235
|
+
|| process.env.WALLE_MODEL
|
|
236
|
+
|| getDefaultModel();
|
|
203
237
|
const candidates = buildSmartRoutingCandidates(brain, {
|
|
204
238
|
currentProvider,
|
|
239
|
+
currentModel,
|
|
205
240
|
includeAutoFirst: true,
|
|
206
241
|
});
|
|
207
242
|
if (candidates.length > 1) {
|
|
@@ -241,11 +276,22 @@ function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
|
|
|
241
276
|
} catch (err) {
|
|
242
277
|
requestFinished = true;
|
|
243
278
|
try { require('../telemetry').trackError('chat-api', err); } catch {}
|
|
279
|
+
const isProviderError = err && (err.code === 'AI_PROVIDER_ERROR' || err.providerError);
|
|
280
|
+
const providerPayload = isProviderError
|
|
281
|
+
? require('../llm/provider-error').toApiPayload(err)
|
|
282
|
+
: null;
|
|
244
283
|
if (wantsStream) {
|
|
245
|
-
try {
|
|
284
|
+
try {
|
|
285
|
+
res.write('data: ' + JSON.stringify({
|
|
286
|
+
type: 'error',
|
|
287
|
+
error: providerPayload ? providerPayload.message : err.message,
|
|
288
|
+
...(providerPayload || {}),
|
|
289
|
+
}) + '\n\n');
|
|
290
|
+
} catch {}
|
|
246
291
|
try { res.end(); } catch {}
|
|
247
292
|
} else {
|
|
248
|
-
|
|
293
|
+
const status = providerPayload?.providerError?.status || err.status || 500;
|
|
294
|
+
jsonResponse(res, providerPayload || { error: err.message }, status);
|
|
249
295
|
}
|
|
250
296
|
}
|
|
251
297
|
}).catch(e => {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Wall-E session memory hooks",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"Stop": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "*",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/walle-stop-hook.sh"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"PreCompact": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "*",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/walle-precompact-hook.sh"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "${CODEX_PLUGIN_ROOT}/hooks/walle-hook.sh session-start"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"Stop": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "*",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "${CODEX_PLUGIN_ROOT}/hooks/walle-hook.sh stop"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"PreCompact": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "*",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "${CODEX_PLUGIN_ROOT}/hooks/walle-hook.sh precompact"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -6,6 +6,7 @@ const { execFileSync } = require('child_process');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { v4: uuidv4 } = require('uuid');
|
|
8
8
|
const brain = require('../brain');
|
|
9
|
+
const { createSafeFetch } = require('../security/ssrf');
|
|
9
10
|
|
|
10
11
|
const CAL_READER = path.join(__dirname, '..', 'skills', '_bundled', 'google-calendar', 'cal-reader');
|
|
11
12
|
|
|
@@ -200,7 +201,8 @@ function _readEventKit(hoursAhead) {
|
|
|
200
201
|
* @returns {Promise<Array<{uid, title, start, end, attendees, location}>>}
|
|
201
202
|
*/
|
|
202
203
|
async function _readICS(url, hoursAhead) {
|
|
203
|
-
const
|
|
204
|
+
const safeFetch = createSafeFetch({ fetchImpl: globalThis.fetch });
|
|
205
|
+
const resp = await safeFetch(url, { signal: AbortSignal.timeout(15000) });
|
|
204
206
|
if (!resp.ok) throw new Error('ICS fetch failed: ' + resp.status);
|
|
205
207
|
const text = await resp.text();
|
|
206
208
|
|
|
@@ -23,7 +23,20 @@ function createClient(type, config = {}) {
|
|
|
23
23
|
if (!provider) {
|
|
24
24
|
throw new Error(`Unknown provider type: ${type}`);
|
|
25
25
|
}
|
|
26
|
-
return provider;
|
|
26
|
+
return withStreamFallback(provider);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function withStreamFallback(provider) {
|
|
30
|
+
if (!provider || typeof provider.chat !== 'function' || typeof provider.chatStream === 'function') {
|
|
31
|
+
return provider;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
...provider,
|
|
35
|
+
async *chatStream(opts = {}) {
|
|
36
|
+
const { streamFromChat } = require('../coding/stream-processor');
|
|
37
|
+
yield* streamFromChat(provider, opts);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
// ============================================================
|
|
@@ -33,28 +46,60 @@ function createClient(type, config = {}) {
|
|
|
33
46
|
let _defaultClient = null;
|
|
34
47
|
|
|
35
48
|
function getDefaultProviderType() {
|
|
36
|
-
//
|
|
49
|
+
// Setup persists to .env and mirrors to process.env for the running daemon.
|
|
50
|
+
if (process.env.WALLE_PROVIDER) return process.env.WALLE_PROVIDER.toLowerCase();
|
|
51
|
+
// Fall back to Wall-E's kv_store for older/live setup writes.
|
|
37
52
|
try {
|
|
38
53
|
const brain = require('../brain');
|
|
39
|
-
const val = brain.
|
|
54
|
+
const val = typeof brain.getKv === 'function'
|
|
55
|
+
? brain.getKv('walle_provider')
|
|
56
|
+
: brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'walle_provider'").get()?.value;
|
|
40
57
|
if (val) return val.toLowerCase();
|
|
41
58
|
} catch {}
|
|
42
|
-
return
|
|
59
|
+
return 'anthropic';
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
function getDefaultModel() {
|
|
46
63
|
providerRegistry.ensureBootstrapped();
|
|
47
64
|
const providerType = getDefaultProviderType();
|
|
48
|
-
|
|
65
|
+
if (process.env.WALLE_MODEL && _modelMatchesProvider(process.env.WALLE_MODEL, providerType)) return process.env.WALLE_MODEL;
|
|
49
66
|
try {
|
|
50
67
|
const brain = require('../brain');
|
|
51
|
-
const val = brain.
|
|
68
|
+
const val = typeof brain.getKv === 'function'
|
|
69
|
+
? brain.getKv('walle_model')
|
|
70
|
+
: brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'walle_model'").get()?.value;
|
|
52
71
|
if (val && _modelMatchesProvider(val, providerType)) return val;
|
|
53
72
|
} catch {}
|
|
54
|
-
|
|
73
|
+
return getDefaultModelForProvider(providerType);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getDefaultModelForProvider(providerType) {
|
|
77
|
+
providerRegistry.ensureBootstrapped();
|
|
55
78
|
return providerRegistry.getDefaultModel(providerType) || providerRegistry.getDefaultModel('anthropic');
|
|
56
79
|
}
|
|
57
80
|
|
|
81
|
+
function getDefaultAuthMethod(providerType) {
|
|
82
|
+
const type = (providerType || getDefaultProviderType()).toLowerCase();
|
|
83
|
+
try {
|
|
84
|
+
const brain = require('../brain');
|
|
85
|
+
const dbMethod = typeof brain.getProviderAuthMethod === 'function'
|
|
86
|
+
? brain.getProviderAuthMethod(type)
|
|
87
|
+
: null;
|
|
88
|
+
if (dbMethod) return dbMethod;
|
|
89
|
+
} catch {}
|
|
90
|
+
if (type === 'anthropic' || type === 'openai') {
|
|
91
|
+
return process.env.WALLE_AUTH_METHOD || '';
|
|
92
|
+
}
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveCompatibleModel(model, providerType) {
|
|
97
|
+
providerRegistry.ensureBootstrapped();
|
|
98
|
+
const targetProvider = providerType || getDefaultProviderType();
|
|
99
|
+
if (model && _modelMatchesProvider(model, targetProvider)) return model;
|
|
100
|
+
return getDefaultModelForProvider(targetProvider);
|
|
101
|
+
}
|
|
102
|
+
|
|
58
103
|
/**
|
|
59
104
|
* Guard against model-provider mismatch. Delegates to the registry's
|
|
60
105
|
* manifest-driven prefix matching (replaces the old hardcoded
|
|
@@ -74,11 +119,12 @@ function getDefaultClient() {
|
|
|
74
119
|
providerRegistry.ensureBootstrapped();
|
|
75
120
|
const providerType = getDefaultProviderType();
|
|
76
121
|
const dbKey = _getProviderKeyFromDb(providerType);
|
|
122
|
+
const authMethod = getDefaultAuthMethod(providerType);
|
|
77
123
|
|
|
78
124
|
// Anthropic auth_method=claude_cli routes through the user's `claude`
|
|
79
125
|
// CLI (subscription billing) instead of the SDK against api.anthropic.com.
|
|
80
126
|
// Other providers ignore this — claude_cli only makes sense for Claude.
|
|
81
|
-
if (providerType === 'anthropic' &&
|
|
127
|
+
if (providerType === 'anthropic' && authMethod === 'claude_cli') {
|
|
82
128
|
_defaultClient = createClient('claude-cli', {});
|
|
83
129
|
return _defaultClient;
|
|
84
130
|
}
|
|
@@ -88,7 +134,7 @@ function getDefaultClient() {
|
|
|
88
134
|
// forwards requests with the user's claude.ai OAuth Bearer token. We
|
|
89
135
|
// pass an empty placeholder apiKey; the proxy ignores it and replaces
|
|
90
136
|
// auth headers with its own.
|
|
91
|
-
if (providerType === 'anthropic' &&
|
|
137
|
+
if (providerType === 'anthropic' && authMethod === 'oauth_proxy') {
|
|
92
138
|
const port = process.env.OAUTH_PROXY_PORT || '3458';
|
|
93
139
|
_defaultClient = createClient('anthropic', {
|
|
94
140
|
apiKey: 'oauth-proxy-placeholder',
|
|
@@ -100,7 +146,7 @@ function getDefaultClient() {
|
|
|
100
146
|
// OpenAI auth_method=codex_cli routes through the user's `codex` CLI
|
|
101
147
|
// (ChatGPT subscription billing) instead of api.openai.com. Mirrors the
|
|
102
148
|
// Anthropic claude_cli path but with codex-cli as the underlying provider.
|
|
103
|
-
if (providerType === 'openai' &&
|
|
149
|
+
if (providerType === 'openai' && authMethod === 'codex_cli') {
|
|
104
150
|
_defaultClient = createClient('codex-cli', {});
|
|
105
151
|
return _defaultClient;
|
|
106
152
|
}
|
|
@@ -158,9 +204,13 @@ function resetDefaultClient() {
|
|
|
158
204
|
|
|
159
205
|
module.exports = {
|
|
160
206
|
createClient,
|
|
207
|
+
withStreamFallback,
|
|
161
208
|
getDefaultClient,
|
|
162
209
|
getDefaultProviderType,
|
|
210
|
+
getDefaultAuthMethod,
|
|
163
211
|
getDefaultModel,
|
|
212
|
+
getDefaultModelForProvider,
|
|
213
|
+
resolveCompatibleModel,
|
|
164
214
|
resetDefaultClient,
|
|
165
215
|
DEFAULT_TIER_MODELS,
|
|
166
216
|
// Re-exported for callers that want to introspect the manifest
|
|
@@ -169,4 +219,8 @@ module.exports = {
|
|
|
169
219
|
providerRegistry.ensureBootstrapped();
|
|
170
220
|
return providerRegistry.detectProviderForModel(model);
|
|
171
221
|
},
|
|
222
|
+
modelMatchesProvider: (model, provider) => {
|
|
223
|
+
providerRegistry.ensureBootstrapped();
|
|
224
|
+
return providerRegistry.modelMatchesProvider(model, provider);
|
|
225
|
+
},
|
|
172
226
|
};
|