create-walle 0.9.25 → 0.9.26
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 -0
- package/bin/create-walle.js +815 -45
- package/package.json +2 -2
- package/template/bin/ctm-dev-cleanup.js +90 -4
- package/template/bin/ctm-launch.sh +49 -1
- package/template/bin/dev.sh +45 -1
- package/template/bin/ensure-stable-node.js +132 -0
- package/template/bin/install-service.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +899 -119
- package/template/claude-task-manager/approval-agent.js +360 -40
- package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
- package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
- package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
- package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
- package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
- package/template/claude-task-manager/db.js +399 -48
- package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
- package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
- package/template/claude-task-manager/lib/approval-hook.js +200 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
- package/template/claude-task-manager/lib/auth-rules.js +11 -0
- package/template/claude-task-manager/lib/background-llm.js +32 -4
- package/template/claude-task-manager/lib/codesign-identity.js +140 -0
- package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
- package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
- package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
- package/template/claude-task-manager/lib/codex-paths.js +73 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
- package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
- package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
- package/template/claude-task-manager/lib/command-targets.js +163 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
- package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
- package/template/claude-task-manager/lib/escalation-review.js +80 -3
- package/template/claude-task-manager/lib/flow-control.js +52 -0
- package/template/claude-task-manager/lib/fs-watcher.js +24 -15
- package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
- package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
- package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
- package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
- package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
- package/template/claude-task-manager/lib/perf-tracker.js +29 -2
- package/template/claude-task-manager/lib/permission-match.js +146 -16
- package/template/claude-task-manager/lib/project-slug.js +33 -0
- package/template/claude-task-manager/lib/prompt-intent.js +51 -4
- package/template/claude-task-manager/lib/read-pool-client.js +48 -3
- package/template/claude-task-manager/lib/real-node.js +73 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
- package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
- package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
- package/template/claude-task-manager/lib/session-history.js +5 -7
- package/template/claude-task-manager/lib/session-host-manager.js +19 -0
- package/template/claude-task-manager/lib/session-jobs.js +6 -0
- package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
- package/template/claude-task-manager/lib/session-messages-page.js +211 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
- package/template/claude-task-manager/lib/session-standup.js +8 -0
- package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
- package/template/claude-task-manager/lib/session-token-usage.js +30 -8
- package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
- package/template/claude-task-manager/lib/storage-migration.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +179 -12
- package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
- package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
- package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
- package/template/claude-task-manager/package.json +5 -2
- package/template/claude-task-manager/prompt-harvest.js +31 -11
- package/template/claude-task-manager/providers/claude-code.js +29 -1
- package/template/claude-task-manager/providers/codex.js +13 -1
- package/template/claude-task-manager/public/css/setup.css +11 -0
- package/template/claude-task-manager/public/css/walle-session.css +132 -4
- package/template/claude-task-manager/public/css/walle.css +89 -0
- package/template/claude-task-manager/public/icon-16.png +0 -0
- package/template/claude-task-manager/public/icon-32.png +0 -0
- package/template/claude-task-manager/public/icon-512.png +0 -0
- package/template/claude-task-manager/public/index.html +2483 -165
- package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
- package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
- package/template/claude-task-manager/public/js/message-renderer.js +60 -1
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
- package/template/claude-task-manager/public/js/setup.js +54 -10
- package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
- package/template/claude-task-manager/public/js/stream-view.js +78 -0
- package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
- package/template/claude-task-manager/public/js/tool-state.js +155 -0
- package/template/claude-task-manager/public/js/walle-session.js +887 -326
- package/template/claude-task-manager/public/js/walle.js +306 -195
- package/template/claude-task-manager/public/m/app.css +1 -0
- package/template/claude-task-manager/public/m/app.js +33 -3
- package/template/claude-task-manager/queue-engine.js +45 -1
- package/template/claude-task-manager/server.js +3367 -540
- package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
- package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
- package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
- package/template/claude-task-manager/workers/session-host-process.js +10 -0
- package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
- package/template/package.json +2 -3
- package/template/shared/icons/AppIcon-ctm.icns +0 -0
- package/template/shared/icons/AppIcon-walle.icns +0 -0
- package/template/wall-e/agent.js +139 -18
- package/template/wall-e/api-walle.js +201 -22
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
- package/template/wall-e/brain.js +1049 -39
- package/template/wall-e/chat.js +427 -86
- package/template/wall-e/coding/acceptance-contract.js +26 -1
- package/template/wall-e/coding/action-memory-policy.js +353 -0
- package/template/wall-e/coding/action-memory-store.js +814 -0
- package/template/wall-e/coding/initial-messages.js +197 -0
- package/template/wall-e/coding/no-progress-guard.js +327 -0
- package/template/wall-e/coding/permission-service.js +88 -22
- package/template/wall-e/coding/session-workspaces.js +81 -0
- package/template/wall-e/coding/shell-sandbox.js +124 -0
- package/template/wall-e/coding/stream-processor.js +63 -2
- package/template/wall-e/coding/tool-execution-controller.js +14 -1
- package/template/wall-e/coding/tool-registry.js +1 -1
- package/template/wall-e/coding/transcript-writer.js +3 -0
- package/template/wall-e/coding-orchestrator.js +636 -35
- package/template/wall-e/coding-prompts.js +51 -2
- package/template/wall-e/docs/model-routing-policy.md +59 -0
- package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
- package/template/wall-e/extraction/knowledge-extractor.js +76 -23
- package/template/wall-e/http/chat-api.js +30 -12
- package/template/wall-e/http/model-admin.js +93 -1
- package/template/wall-e/lib/background-lanes.js +133 -0
- package/template/wall-e/lib/boot-profile.js +11 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
- package/template/wall-e/lib/brain-read-pool-client.js +311 -0
- package/template/wall-e/lib/diagnostics-flags.js +87 -0
- package/template/wall-e/lib/event-loop-monitor.js +74 -3
- package/template/wall-e/lib/mcp-integration.js +7 -1
- package/template/wall-e/lib/real-node.js +98 -0
- package/template/wall-e/lib/runtime-health.js +206 -0
- package/template/wall-e/lib/runtime-worker-pool.js +101 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
- package/template/wall-e/lib/scheduler.js +446 -17
- package/template/wall-e/lib/service-health.js +61 -2
- package/template/wall-e/lib/service-readiness.js +258 -0
- package/template/wall-e/lib/usage.js +152 -0
- package/template/wall-e/lib/worker-thread-pool.js +389 -0
- package/template/wall-e/llm/client.js +81 -4
- package/template/wall-e/llm/default-fallback.js +54 -8
- package/template/wall-e/llm/mlx.js +536 -73
- package/template/wall-e/llm/mlx.plugin.json +1 -1
- package/template/wall-e/llm/ollama.js +342 -43
- package/template/wall-e/llm/provider-error.js +18 -1
- package/template/wall-e/llm/provider-health-state.js +176 -0
- package/template/wall-e/llm/routing-policy.js +796 -0
- package/template/wall-e/llm/supported-models.js +5 -0
- package/template/wall-e/loops/tasks.js +60 -14
- package/template/wall-e/loops/think.js +89 -24
- package/template/wall-e/mcp-server.js +192 -28
- package/template/wall-e/server.js +32 -7
- package/template/wall-e/skills/script-skill-runner.js +8 -1
- package/template/wall-e/skills/skill-planner.js +64 -1
- package/template/wall-e/tools/builtin-middleware.js +67 -2
- package/template/wall-e/tools/local-tools.js +116 -26
- package/template/wall-e/tools/permission-checker.js +52 -4
- package/template/wall-e/tools/permission-rules.js +36 -0
- package/template/wall-e/tools/shell-analyzer.js +46 -1
- package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
- package/template/wall-e/training/real-trajectory-miner.js +2617 -0
- package/template/wall-e/training/replay-eval-analysis.js +151 -0
- package/template/wall-e/training/run-shell-command-selector.js +277 -0
- package/template/wall-e/training/tool-sft-dataset.js +312 -0
- package/template/wall-e/training/tool-sft-renderers.js +144 -0
- package/template/wall-e/training/tool-trace-harvester.js +1440 -0
- package/template/wall-e/training/trajectory-action-selector.js +364 -0
- package/template/wall-e/weather-runtime.js +232 -0
- package/template/wall-e/workers/brain-owner-worker.js +162 -0
- package/template/wall-e/workers/brain-read-worker.js +148 -0
- package/template/wall-e/workers/runtime-worker.js +145 -0
|
@@ -222,9 +222,48 @@ function buildAgentSystemPrompt({ resolvedCwd, projectInfo, projectSkills, taskF
|
|
|
222
222
|
userMessage: runtimeContext.userTask || '',
|
|
223
223
|
}).replace(/\s+/g, ' ').trim();
|
|
224
224
|
|
|
225
|
+
// Per-turn environment grounding (mirrors OpenCode's sys.environment): re-state
|
|
226
|
+
// the working directory, git root, and repo status every turn so the model never
|
|
227
|
+
// loses track of where it is.
|
|
228
|
+
const env = runtimeContext.environment || {};
|
|
229
|
+
const gitRoot = env.gitRoot || '';
|
|
230
|
+
const isGitRepo = env.isGitRepo != null ? !!env.isGitRepo : !!gitRoot;
|
|
231
|
+
const platform = env.platform || process.platform;
|
|
232
|
+
const envBlock = [
|
|
233
|
+
'<env>',
|
|
234
|
+
`Working directory: ${resolvedCwd || '(not set)'}`,
|
|
235
|
+
`Workspace root (git): ${gitRoot || resolvedCwd || '(not set)'}`,
|
|
236
|
+
`Is git repo: ${isGitRepo ? 'yes' : 'no'}`,
|
|
237
|
+
`Platform: ${platform}`,
|
|
238
|
+
'</env>',
|
|
239
|
+
].join('\n');
|
|
240
|
+
// When a session has been reused across more than one directory, surface every
|
|
241
|
+
// root it has touched so "the page/site/file you built" resolves to where it
|
|
242
|
+
// actually lives — not a glob of the current cwd (session c3f3af97).
|
|
243
|
+
const sessionWorkspaces = Array.isArray(runtimeContext.sessionWorkspaces)
|
|
244
|
+
? runtimeContext.sessionWorkspaces.filter(Boolean)
|
|
245
|
+
: [];
|
|
246
|
+
const workspacesBlock = sessionWorkspaces.length > 1
|
|
247
|
+
? `\n\n<session_workspaces>
|
|
248
|
+
This coding session has worked in MORE THAN ONE directory. Files you created earlier may live in a different directory than the current working directory above. When the user refers to something you built ("the web page", "the site", "the file you created") and it is not under the current working directory, look under these other roots — read/glob the specific path — before answering or globbing the cwd:
|
|
249
|
+
${sessionWorkspaces.map((w) => `- ${w}${w === resolvedCwd ? ' (current working directory)' : ''}`).join('\n')}
|
|
250
|
+
</session_workspaces>`
|
|
251
|
+
: '';
|
|
252
|
+
// Anchor: the session's home project (its first root). Gives the agent a stable
|
|
253
|
+
// "this session belongs to project X" even when a turn is pointed elsewhere, so a
|
|
254
|
+
// bare "the project / the site / what you built" resolves to the anchor, not the cwd.
|
|
255
|
+
const sessionAnchor = typeof runtimeContext.sessionAnchor === 'string' ? runtimeContext.sessionAnchor : '';
|
|
256
|
+
const currentRoot = gitRoot || resolvedCwd || '';
|
|
257
|
+
const anchorBlock = sessionAnchor
|
|
258
|
+
? `\n\n<session_anchor>
|
|
259
|
+
This coding session is anchored to its home project: ${sessionAnchor}
|
|
260
|
+
Treat that as the session's primary project. When the user refers to "the project", "the app", "the site", or something you built earlier without naming a directory, resolve it under the anchor — not whichever directory this turn happens to point at.${currentRoot && sessionAnchor && currentRoot !== sessionAnchor ? `\nNOTE: the current working directory (${currentRoot}) is a DIFFERENT project than the anchor. Say which project you are acting on before you change files.` : ''}
|
|
261
|
+
</session_anchor>`
|
|
262
|
+
: '';
|
|
263
|
+
|
|
225
264
|
const body = `You are an expert software engineer executing a coding task. Use the provided tools to actually do the work — describing what to do is not completing the task.${projectCtx}${projectSkillCtx}
|
|
226
265
|
|
|
227
|
-
|
|
266
|
+
${envBlock}${anchorBlock}${workspacesBlock}
|
|
228
267
|
${largeTaskCtx}
|
|
229
268
|
|
|
230
269
|
# Runtime role
|
|
@@ -238,11 +277,21 @@ ${memoryProtocolCtx ? `${memoryProtocolCtx}\n\n` : ''}${frontendDesignCtx ? `${f
|
|
|
238
277
|
3. Edit, don't create. Prefer edit_file (or multi_edit / apply_patch) on existing files. Only write_file when a new file is genuinely required by the task. NEVER create README/docs files unless explicitly asked.
|
|
239
278
|
4. Verify with run_shell. Run the test command, lint, or build that proves the change works. Report the actual output — "tests pass" without showing them is not verification.
|
|
240
279
|
|
|
280
|
+
# Finishing — apply, don't ask to apply
|
|
281
|
+
|
|
282
|
+
- You are in BUILD mode, not plan mode: you have the authority to edit this project directly, and the edit tools (edit_file/multi_edit/apply_patch/write_file) write the change immediately. You do NOT need the user's permission to apply a change you've already designed. Asking "Want me to proceed / apply these edits?" is plan-mode behavior — you are not in plan mode.
|
|
283
|
+
- When you say you'll make a change, ACTUALLY make the tool call THIS turn. Never end a turn describing an edit you intend to make, or asking whether to apply it — make it, then report what changed. A turn that ends with "I investigated; confirm and I'll apply" while you already know the edit is a FAILURE, even when the analysis is correct.
|
|
284
|
+
- Keep going until the request is actually resolved before yielding back. If part of the task is ambiguous, pick the reasonable default, APPLY it, and note your assumption in the summary — don't hand the whole task back over one open detail. You are a capable autonomous agent; finish the work instead of asking for permission to act on it.
|
|
285
|
+
|
|
241
286
|
# Doing tasks
|
|
242
287
|
|
|
243
288
|
- Find root causes, not symptoms. If a test fails, understand why before patching the test. If error handling fires, understand the underlying error before swallowing it.
|
|
244
289
|
- If the requested symbol, file, or behavior does not exist after you have searched the codebase, do not invent code just to make an edit. Report the blocker, the searches you ran, and leave the tree unchanged.
|
|
245
290
|
- For large multi-file tasks, first identify the target file set, then batch work aggressively: use apply_patch or multi_edit to update several files per editing turn, and defer broad test suites until after the first complete code pass. Do not spend most of the run budget on exploration when the prompt already names target files.
|
|
291
|
+
- Bank the certain fix first. When a task mixes fixes you're sure of (root cause found, the change is unambiguous) with parts that need a decision or more digging, APPLY the sure fixes this turn before doing anything expensive on the open parts. Never end a turn with zero edits when you already know a correct, in-scope edit — a turn that lands the certain fix and flags the open question beats one that investigates everything and changes nothing.
|
|
292
|
+
- Cheap before expensive. Land the edits you're sure of before sinking the budget into costly verification — building a viewer/harness page, screenshotting every asset, scraping. Verify AFTER the known edits are in, with the cheapest check that proves the point.
|
|
293
|
+
- Batch probes; don't loop. To inspect many files (dimensions, sizes, types), run ONE batched command — most CLIs take multiple paths (\`sips -g pixelWidth -g pixelHeight a.jpg b.jpg …\`), or use a glob or a tiny script — never a per-file \`for\` loop of separate run_shell calls. Push genuinely slow verification to \`background: true\` and poll bg_output instead of blocking the turn. And don't re-measure: reuse what you already ran this turn — don't repeat the same \`git diff\` / \`ls\` / screenshot; cite your earlier result.
|
|
294
|
+
- Stay in the named scope. Work the one project the task is about. Don't wander into sibling repos, and don't spin up static servers or screenshot harnesses unless the task genuinely can't be verified any cheaper.
|
|
246
295
|
- Don't add features, refactors, or abstractions beyond what the task requires. Three similar lines is fine — premature abstraction is worse than duplication.
|
|
247
296
|
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal call shapes and framework guarantees. Validate at system boundaries (user input, network, filesystem) only.
|
|
248
297
|
- Don't add backwards-compat shims when you can just change the code: no \`_unused\` renames, no \`// removed X\` comments, no re-exports of deleted types. If a thing is unused, delete it.
|
|
@@ -270,7 +319,7 @@ ${memoryProtocolCtx ? `${memoryProtocolCtx}\n\n` : ''}${frontendDesignCtx ? `${f
|
|
|
270
319
|
- ${responseLanguagePolicy}
|
|
271
320
|
- If the task is ambiguous, make a best-guess interpretation and note it explicitly: "I interpreted X as Y; tell me if you meant Z." Don't refuse and ask — answer + clarify.
|
|
272
321
|
- If you genuinely cannot complete the task, say what you tried, what you ruled out, and the narrowed-down problem. That's a useful failure; "I couldn't" alone is not.
|
|
273
|
-
- Use ask_user only when the answer is genuinely undeterminable from the codebase + reasonable defaults. Most "ambiguous" tasks aren't — pick the obvious interpretation and proceed.`;
|
|
322
|
+
- Use ask_user only when the answer is genuinely undeterminable from the codebase + reasonable defaults. Most "ambiguous" tasks aren't — pick the obvious interpretation and proceed. When there IS one real decision (a product choice with no obvious default), first bank every safe edit, then ask that ONE focused question mid-task — don't turn it into a turn-ending "I investigated; confirm and I'll apply everything" wrap-up that hands the work back to the user. ask_user is for a product decision you cannot default — NOT for permission to apply edits you've already designed. If the only thing left is whether to apply the change, the answer is yes: apply it and report; never ask that.`;
|
|
274
323
|
|
|
275
324
|
const bundle = buildPromptBundle({ body, mode, provider, model, extraInstructions, returnEnvelope: true });
|
|
276
325
|
const envelope = buildRuntimePromptEnvelope({
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Wall-E Model Routing Policy
|
|
2
|
+
|
|
3
|
+
Wall-E keeps model fallback routing as durable policy data in the brain DB. UI reads should not recompute fallback order on every page load.
|
|
4
|
+
|
|
5
|
+
## Precedence
|
|
6
|
+
|
|
7
|
+
1. Task-specific model override from `model_defaults`
|
|
8
|
+
2. Provider-card default (`walle_provider` / `walle_model`)
|
|
9
|
+
3. User-defined fallback routes
|
|
10
|
+
4. Generated fallback routes from currently available providers and local CLIs
|
|
11
|
+
5. Failure with a surfaced provider-health notice
|
|
12
|
+
|
|
13
|
+
## Storage
|
|
14
|
+
|
|
15
|
+
`model_routing_policies` stores the policy header:
|
|
16
|
+
|
|
17
|
+
- mode: `smart` or `strict`
|
|
18
|
+
- CLI-tail and final-answer fallback toggles
|
|
19
|
+
- generation/effective timestamps
|
|
20
|
+
- last materialization error
|
|
21
|
+
|
|
22
|
+
`model_routing_policy_routes` stores ordered route rows. The `source` column separates:
|
|
23
|
+
|
|
24
|
+
- `user`: manual user order
|
|
25
|
+
- `generated`: Wall-E suggestions from provider availability, credentials, health, and CLI checks
|
|
26
|
+
- `effective`: materialized runtime route after merging provider-card default, user routes, and generated routes
|
|
27
|
+
|
|
28
|
+
Runtime and CTM UI use the `effective` chain when present. If no effective chain exists, Wall-E materializes one once, persists it, and subsequent reads are cheap DB reads.
|
|
29
|
+
|
|
30
|
+
## Manual Editing
|
|
31
|
+
|
|
32
|
+
The Model Operations fallback editor writes only the `user` layer. The provider-card default remains locked as route 1 so users cannot accidentally demote the configured default provider from the fallback editor.
|
|
33
|
+
|
|
34
|
+
When the user saves fallback order:
|
|
35
|
+
|
|
36
|
+
1. CTM posts ordered user route keys to `/api/models/routing-policy/routes`.
|
|
37
|
+
2. Wall-E validates the route keys against the current persisted generated/effective routes.
|
|
38
|
+
3. Wall-E stores enabled and disabled user rows with `source = 'user'`.
|
|
39
|
+
4. Wall-E rematerializes `effective` as:
|
|
40
|
+
- generated primary/default route;
|
|
41
|
+
- enabled user routes in saved order;
|
|
42
|
+
- remaining generated routes that were not already used and were not disabled by a user row.
|
|
43
|
+
|
|
44
|
+
Resetting the editor clears all `user` rows and rebuilds `effective` from generated suggestions.
|
|
45
|
+
|
|
46
|
+
## Refresh Rules
|
|
47
|
+
|
|
48
|
+
Recompute only when:
|
|
49
|
+
|
|
50
|
+
- the policy has no materialized routes yet;
|
|
51
|
+
- the user explicitly refreshes fallback policy;
|
|
52
|
+
- the user changes routing mode/toggles;
|
|
53
|
+
- a later provider/model/credential workflow marks the policy stale and triggers a background refresh.
|
|
54
|
+
|
|
55
|
+
Normal `GET /api/models/routing-policy` returns the persisted policy. It should not rescore the catalog on every page render.
|
|
56
|
+
|
|
57
|
+
## Runtime
|
|
58
|
+
|
|
59
|
+
The resilient default client reads the persisted effective chain when available and uses it to order fallback candidates. Existing provider resolution remains the source of truth for credentials and client construction; the route table only controls order and visibility.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Wall-E coding-agent shell sandbox (`WALLE_SHELL_SANDBOX`)
|
|
2
|
+
|
|
3
|
+
Opt-in OS sandbox for the coding agent's `run_shell` tool. **Default OFF.**
|
|
4
|
+
|
|
5
|
+
## The problem it solves
|
|
6
|
+
|
|
7
|
+
`run_shell` gates commands with a static allowlist; in a headless coding turn an *unlisted*
|
|
8
|
+
command (e.g. `sips`, ImageMagick, `ffmpeg`, `pandoc`, `pdftoppm`) evaluates to "ask" and is
|
|
9
|
+
auto-**denied** — the agent dead-ends ("I can't, that tool isn't allowed"). A static allowlist both
|
|
10
|
+
blocks legitimate tools and can't prompt mid-run.
|
|
11
|
+
|
|
12
|
+
This is the same trade-off Claude Code and Codex solve with an **OS sandbox**: run the command
|
|
13
|
+
**contained** (filesystem writes bounded to the workspace) and auto-allow it, instead of blocking by
|
|
14
|
+
name. Anthropic measured ~84% fewer approval prompts with sandboxing vs prompting.
|
|
15
|
+
|
|
16
|
+
## What it does (when on)
|
|
17
|
+
|
|
18
|
+
- Every non-denylisted `run_shell` command (foreground and background) runs wrapped in the host OS
|
|
19
|
+
sandbox: **macOS Seatbelt** (`sandbox-exec`) or **Linux bubblewrap** (`bwrap`).
|
|
20
|
+
- **Filesystem writes** are contained to the workspace (`cwd` + `os.tmpdir()`, realpath-expanded so
|
|
21
|
+
macOS `/var`→`/private/var` symlinks don't leak). **Reads, process-exec, and network are allowed**
|
|
22
|
+
(so `npm install` / fetches keep working).
|
|
23
|
+
- The permission layer stops dead-ending unlisted shell commands (returns `allow` with
|
|
24
|
+
`source: 'sandbox'`) so they reach the runner; the runner does the actual containment.
|
|
25
|
+
- The hard **denylist** (`sudo`, `su`, `dd`, `mkfs`, `fdisk`, `diskutil`, `shutdown`, `reboot`,
|
|
26
|
+
`chmod`, `chown`, `kill`, `killall`, `rm*`…) is **still refused** — a filesystem sandbox does not
|
|
27
|
+
neutralize privilege/device/system operations.
|
|
28
|
+
- **Graceful fallback:** if no sandbox backend is present (`sandbox-exec`/`bwrap` missing), behavior
|
|
29
|
+
is unchanged (unlisted commands stay denied in headless) — no false sense of safety.
|
|
30
|
+
|
|
31
|
+
## Enable it
|
|
32
|
+
|
|
33
|
+
Add to the launchd plist `EnvironmentVariables` (then bootout/bootstrap the service), or `.env`:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
WALLE_SHELL_SANDBOX=1
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
It only takes effect after a Wall-E restart. Validate on `/ctm-dev` before enabling on the primary
|
|
40
|
+
(same discipline as `CTM_CODEX_SANDBOX` / `CTM_APPROVAL_HOOK_SANDBOX`).
|
|
41
|
+
|
|
42
|
+
## Caveats
|
|
43
|
+
|
|
44
|
+
- **macOS `sandbox-exec` is deprecated** (but still shipped + used by Claude Code and Codex; there is
|
|
45
|
+
no public replacement). Linux uses `bwrap` (install `bubblewrap`).
|
|
46
|
+
- **Network is allowed** in v1 — writes are contained but a command can still reach the network.
|
|
47
|
+
Network containment is a future toggle.
|
|
48
|
+
- Reads are unrestricted, so a command can still *read* `~/.ssh` / `~/.aws`. Tighten with a deny-read
|
|
49
|
+
layer if your threat model needs it.
|
|
50
|
+
|
|
51
|
+
## Implementation
|
|
52
|
+
|
|
53
|
+
- `coding/shell-sandbox.js` — `sandboxAvailable()`, `buildSeatbeltProfile()`, `wrapShellArgv()`,
|
|
54
|
+
`shellSandboxEnabled()`.
|
|
55
|
+
- `coding/permission-service.js` — headless block returns `allow + source:'sandbox'` for an unlisted
|
|
56
|
+
`run_shell` when the flag + a backend are present.
|
|
57
|
+
- `tools/local-tools.js` — `runShell` / `runShellBackground` wrap the spawn via `wrapShellArgv` for
|
|
58
|
+
non-denylisted commands; the static allowlist gate is skipped under the sandbox (the OS is the
|
|
59
|
+
boundary), the denylist is honored as a floor.
|
|
60
|
+
- Tests: `tests/shell-sandbox.test.js` (incl. a real Seatbelt containment check),
|
|
61
|
+
`tests/shell-sandbox-runner.test.js`, `tests/permission-service-sandbox.test.js`.
|
|
@@ -7,6 +7,53 @@ const VALID_CATEGORIES = ['technical', 'preference', 'relationship', 'work', 'pe
|
|
|
7
7
|
const REQUIRED_FIELDS = ['category', 'subject', 'predicate', 'object'];
|
|
8
8
|
const MAX_CONTENT_LENGTH = 2000;
|
|
9
9
|
const MAX_FIELD_LENGTH = 500;
|
|
10
|
+
const MAX_STRUCTURED_PREVIEW_LENGTH = 4000;
|
|
11
|
+
const MAX_SOURCE_MEMORY_IDS = 20;
|
|
12
|
+
|
|
13
|
+
function scalarString(value) {
|
|
14
|
+
if (typeof value === 'string') return value;
|
|
15
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeKnowledgeField(value) {
|
|
20
|
+
const scalar = scalarString(value);
|
|
21
|
+
if (scalar == null) return null;
|
|
22
|
+
const normalized = scalar.replace(/\s+/g, ' ').trim();
|
|
23
|
+
if (!normalized) return null;
|
|
24
|
+
return normalized.length > MAX_FIELD_LENGTH ? normalized.slice(0, MAX_FIELD_LENGTH) : normalized;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeSourceMemoryIds(value) {
|
|
28
|
+
const raw = Array.isArray(value) ? value : value == null ? [] : [value];
|
|
29
|
+
const ids = [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const item of raw) {
|
|
32
|
+
const id = normalizeKnowledgeField(item);
|
|
33
|
+
if (!id || seen.has(id)) continue;
|
|
34
|
+
seen.add(id);
|
|
35
|
+
ids.push(id);
|
|
36
|
+
if (ids.length >= MAX_SOURCE_MEMORY_IDS) break;
|
|
37
|
+
}
|
|
38
|
+
return ids;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeMemoryContent(content) {
|
|
42
|
+
if (content == null) return '';
|
|
43
|
+
if (typeof content === 'string') return content;
|
|
44
|
+
if (Buffer.isBuffer(content)) return content.toString('utf8');
|
|
45
|
+
if (typeof content === 'number' || typeof content === 'boolean') return String(content);
|
|
46
|
+
try {
|
|
47
|
+
return JSON.stringify(content, (_key, value) => {
|
|
48
|
+
if (typeof value === 'string' && value.length > MAX_STRUCTURED_PREVIEW_LENGTH) {
|
|
49
|
+
return value.slice(0, MAX_STRUCTURED_PREVIEW_LENGTH) + '...[truncated]';
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
});
|
|
53
|
+
} catch {
|
|
54
|
+
return String(content);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
10
57
|
|
|
11
58
|
/**
|
|
12
59
|
* Escape memory content to prevent prompt injection.
|
|
@@ -14,9 +61,10 @@ const MAX_FIELD_LENGTH = 500;
|
|
|
14
61
|
*/
|
|
15
62
|
function escapeMemoryContent(content) {
|
|
16
63
|
if (!content) return '';
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
64
|
+
const normalized = normalizeMemoryContent(content);
|
|
65
|
+
const truncated = normalized.length > MAX_CONTENT_LENGTH
|
|
66
|
+
? normalized.slice(0, MAX_CONTENT_LENGTH) + '...[truncated]'
|
|
67
|
+
: normalized;
|
|
20
68
|
return truncated.replace(/```/g, '` ` `');
|
|
21
69
|
}
|
|
22
70
|
|
|
@@ -87,32 +135,37 @@ function parseExtractionResponse(text) {
|
|
|
87
135
|
return { knowledge: [], classifications: [] };
|
|
88
136
|
}
|
|
89
137
|
|
|
90
|
-
const validKnowledge = knowledgeEntries.
|
|
91
|
-
if (!
|
|
92
|
-
|
|
93
|
-
}
|
|
138
|
+
const validKnowledge = knowledgeEntries.map(entry => {
|
|
139
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
140
|
+
const normalized = { ...entry };
|
|
94
141
|
for (const field of REQUIRED_FIELDS) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
142
|
+
const value = normalizeKnowledgeField(entry[field]);
|
|
143
|
+
if (value == null) return null;
|
|
144
|
+
normalized[field] = value;
|
|
98
145
|
}
|
|
146
|
+
normalized.source_memory_ids = normalizeSourceMemoryIds(entry.source_memory_ids);
|
|
99
147
|
if (entry.confidence !== undefined) {
|
|
100
|
-
|
|
101
|
-
if (isNaN(
|
|
102
|
-
|
|
148
|
+
normalized.confidence = Number(entry.confidence);
|
|
149
|
+
if (isNaN(normalized.confidence) || normalized.confidence < 0 || normalized.confidence > 1) {
|
|
150
|
+
normalized.confidence = 0.5;
|
|
103
151
|
}
|
|
104
152
|
}
|
|
105
|
-
return
|
|
106
|
-
});
|
|
153
|
+
return normalized;
|
|
154
|
+
}).filter(Boolean);
|
|
107
155
|
|
|
108
156
|
// Validate classifications
|
|
109
|
-
const validClassifications = classifications.
|
|
110
|
-
c
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
domain
|
|
114
|
-
|
|
115
|
-
|
|
157
|
+
const validClassifications = classifications.map(c => {
|
|
158
|
+
if (!c || typeof c !== 'object') return null;
|
|
159
|
+
const memoryId = normalizeKnowledgeField(c.memory_id);
|
|
160
|
+
const domain = normalizeKnowledgeField(c.domain);
|
|
161
|
+
if (!memoryId || !domain || !VALID_DOMAINS.includes(domain)) return null;
|
|
162
|
+
const topic = normalizeKnowledgeField(c.topic);
|
|
163
|
+
return {
|
|
164
|
+
memory_id: memoryId,
|
|
165
|
+
domain,
|
|
166
|
+
topic: topic ? topic.slice(0, 100).toLowerCase() : null,
|
|
167
|
+
};
|
|
168
|
+
}).filter(Boolean);
|
|
116
169
|
|
|
117
170
|
return { knowledge: validKnowledge, classifications: validClassifications };
|
|
118
171
|
} catch (_) {
|
|
@@ -162,4 +215,4 @@ async function extractKnowledge(memories, ownerName, opts = {}) {
|
|
|
162
215
|
// Re-export buildClientOpts for backward compatibility (used by contradiction.js, skill-executor.js, etc.)
|
|
163
216
|
const buildClientOpts = buildClientOptsFromEnv;
|
|
164
217
|
|
|
165
|
-
module.exports = { buildExtractionPrompt, parseExtractionResponse, extractKnowledge, buildClientOpts, VALID_DOMAINS };
|
|
218
|
+
module.exports = { buildExtractionPrompt, parseExtractionResponse, extractKnowledge, buildClientOpts, VALID_DOMAINS, normalizeMemoryContent };
|
|
@@ -166,6 +166,14 @@ function buildChatOptionsFromRequestBody(body = {}, {
|
|
|
166
166
|
allowNetworkLocationInference: true,
|
|
167
167
|
abortSignal,
|
|
168
168
|
attachments: parsedAttachments.length ? parsedAttachments : undefined,
|
|
169
|
+
// Durable branch-tree identity for the default chat: the client mints a stable
|
|
170
|
+
// message id (so the optimistic bubble, the durable jsonl/db record, and the echo
|
|
171
|
+
// share identity) and, for an edit-resend, the parent the new prompt is a sibling
|
|
172
|
+
// under. Absent on a normal turn → the user message gets a uuid + the session head
|
|
173
|
+
// as parent (linear append).
|
|
174
|
+
clientMessageId: body.clientMessageId || body.client_message_id || undefined,
|
|
175
|
+
parentUuid: body.parentUuid !== undefined ? body.parentUuid
|
|
176
|
+
: (body.parent_uuid !== undefined ? body.parent_uuid : undefined),
|
|
169
177
|
};
|
|
170
178
|
}
|
|
171
179
|
|
|
@@ -447,7 +455,9 @@ function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
|
|
|
447
455
|
try {
|
|
448
456
|
const sessionId = params.get('session_id') || 'default';
|
|
449
457
|
const limit = parseLimit(params, 100);
|
|
450
|
-
const data = brain.
|
|
458
|
+
const data = typeof brain.listChatMessagesWithParts === 'function'
|
|
459
|
+
? brain.listChatMessagesWithParts({ session_id: sessionId, limit })
|
|
460
|
+
: brain.listChatMessages({ session_id: sessionId, limit });
|
|
451
461
|
jsonResponse(res, { data });
|
|
452
462
|
} catch (e) {
|
|
453
463
|
jsonResponse(res, { error: e.message }, 500);
|
|
@@ -503,34 +513,42 @@ function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
|
|
|
503
513
|
return true;
|
|
504
514
|
}
|
|
505
515
|
|
|
516
|
+
// Branch state is tree-only now: the conversation tree is rebuilt from the
|
|
517
|
+
// chat_messages parent_uuid edges, so the ONLY persisted branch state is the
|
|
518
|
+
// head-leaf pointer (KV walle_session_head:<sid>). No index blob.
|
|
506
519
|
if (p === '/api/wall-e/chat/branches' && m === 'GET') {
|
|
507
520
|
if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
|
|
508
521
|
const sid = params.get('session_id') || 'default';
|
|
509
522
|
try {
|
|
510
|
-
|
|
523
|
+
let headUuid = '';
|
|
524
|
+
let headFrozen = false;
|
|
525
|
+
try { if (typeof brain.getKv === 'function') headUuid = brain.getKv('walle_session_head:' + sid) || ''; } catch (_) { /* best-effort */ }
|
|
526
|
+
// The "stop here" marker set by an explicit delete; absent (falsy) on legacy heads.
|
|
527
|
+
try { if (typeof brain.getKv === 'function') headFrozen = brain.getKv('walle_session_head_frozen:' + sid) === '1'; } catch (_) { /* best-effort */ }
|
|
528
|
+
jsonResponse(res, { data: { head_uuid: headUuid, head_frozen: headFrozen } });
|
|
511
529
|
} catch (e) {
|
|
512
530
|
jsonResponse(res, { error: e.message }, 500);
|
|
513
531
|
}
|
|
514
532
|
return true;
|
|
515
533
|
}
|
|
516
534
|
|
|
517
|
-
if (p === '/api/wall-e/chat/
|
|
535
|
+
if (p === '/api/wall-e/chat/branch-state' && m === 'POST') {
|
|
518
536
|
if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
|
|
519
|
-
|
|
520
|
-
// every stored branch version), so it routinely outgrows the 1MB default —
|
|
521
|
-
// use the same large limit as the chat-send endpoint, which carries the
|
|
522
|
-
// same message payloads.
|
|
523
|
-
readBody(req, { maxBytes: BODY_LIMIT_LARGE }).then(body => {
|
|
537
|
+
readBody(req).then(body => {
|
|
524
538
|
try {
|
|
525
|
-
|
|
526
|
-
|
|
539
|
+
if (typeof body.head_uuid === 'string' && typeof brain.setKv === 'function') {
|
|
540
|
+
try { brain.setKv('walle_session_head:' + (body.session_id || 'default'), body.head_uuid); } catch (_) { /* best-effort */ }
|
|
541
|
+
}
|
|
542
|
+
// Persist the explicit-delete "stop here" marker alongside the head so a frozen
|
|
543
|
+
// delete-view survives reload (and clears the instant the head moves forward).
|
|
544
|
+
if (typeof body.head_frozen !== 'undefined' && typeof brain.setKv === 'function') {
|
|
545
|
+
try { brain.setKv('walle_session_head_frozen:' + (body.session_id || 'default'), body.head_frozen ? '1' : ''); } catch (_) { /* best-effort */ }
|
|
546
|
+
}
|
|
527
547
|
jsonResponse(res, { data: { ok: true } });
|
|
528
548
|
} catch (e) {
|
|
529
549
|
jsonResponse(res, { error: e.message }, 500);
|
|
530
550
|
}
|
|
531
551
|
}).catch(e => {
|
|
532
|
-
// An oversize body is a 413, not a generic 400, so the client can tell
|
|
533
|
-
// "too big" from a malformed/aborted request.
|
|
534
552
|
if (e && e.code === 'BODY_TOO_LARGE') { send413(res, e); return; }
|
|
535
553
|
jsonResponse(res, { error: e.message }, 400);
|
|
536
554
|
});
|
|
@@ -714,6 +714,55 @@ async function handleModelAdminApi(req, res, url) {
|
|
|
714
714
|
}
|
|
715
715
|
}
|
|
716
716
|
|
|
717
|
+
if (p === '/api/models/routing-policy' && m === 'GET') {
|
|
718
|
+
if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
|
|
719
|
+
try {
|
|
720
|
+
const { getPersistedRoutingPolicy } = require('../llm/routing-policy');
|
|
721
|
+
return jsonResponse(res, 200, getPersistedRoutingPolicy({ brain: brainApi }));
|
|
722
|
+
} catch (e) {
|
|
723
|
+
return jsonResponse(res, 500, { error: e.message });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (p === '/api/models/routing-policy' && m === 'POST') {
|
|
728
|
+
if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
|
|
729
|
+
try {
|
|
730
|
+
const body = await readSmallJsonBody(req);
|
|
731
|
+
const { materializeRoutingPolicy, setRoutingPolicySettings } = require('../llm/routing-policy');
|
|
732
|
+
const settings = setRoutingPolicySettings(brainApi, body || {});
|
|
733
|
+
try { require('../llm/client').resetDefaultClient(); } catch {}
|
|
734
|
+
return jsonResponse(res, 200, { ok: true, settings, policy: materializeRoutingPolicy({ brain: brainApi }) });
|
|
735
|
+
} catch (e) {
|
|
736
|
+
return jsonResponse(res, 400, { error: e.message });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (p === '/api/models/routing-policy/routes' && m === 'POST') {
|
|
741
|
+
if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
|
|
742
|
+
try {
|
|
743
|
+
const body = await readSmallJsonBody(req);
|
|
744
|
+
const { setUserRoutingPolicyRoutes } = require('../llm/routing-policy');
|
|
745
|
+
const routes = body && body.reset ? [] : (body && body.routes);
|
|
746
|
+
const policy = setUserRoutingPolicyRoutes({ brain: brainApi, routes });
|
|
747
|
+
try { require('../llm/client').resetDefaultClient(); } catch {}
|
|
748
|
+
return jsonResponse(res, 200, { ok: true, policy });
|
|
749
|
+
} catch (e) {
|
|
750
|
+
return jsonResponse(res, 400, { error: e.message });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (p === '/api/models/routing-policy/refresh' && m === 'POST') {
|
|
755
|
+
if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
|
|
756
|
+
try {
|
|
757
|
+
const { materializeRoutingPolicy } = require('../llm/routing-policy');
|
|
758
|
+
const policy = materializeRoutingPolicy({ brain: brainApi, repairDefault: true });
|
|
759
|
+
try { require('../llm/client').resetDefaultClient(); } catch {}
|
|
760
|
+
return jsonResponse(res, 200, { ok: true, policy });
|
|
761
|
+
} catch (e) {
|
|
762
|
+
return jsonResponse(res, 400, { error: e.message });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
717
766
|
if (p === '/api/models/provider-route-policy' && m === 'POST') {
|
|
718
767
|
if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
|
|
719
768
|
try {
|
|
@@ -998,9 +1047,12 @@ async function handleModelAdminApi(req, res, url) {
|
|
|
998
1047
|
if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
|
|
999
1048
|
try {
|
|
1000
1049
|
const days = parseScorecardDays(url.searchParams.get('days'), 7);
|
|
1050
|
+
// One GROUP BY for all models instead of one query per model (thousands).
|
|
1051
|
+
const statsByModel = brainApi.getModelScorecardsBatch(days);
|
|
1052
|
+
const emptyStats = brainApi.emptyModelScorecardStats();
|
|
1001
1053
|
const scorecard = brainApi.listAllModels().map((model) => ({
|
|
1002
1054
|
...model,
|
|
1003
|
-
stats:
|
|
1055
|
+
stats: statsByModel.get(model.id) || emptyStats,
|
|
1004
1056
|
}));
|
|
1005
1057
|
let benchmarks = [];
|
|
1006
1058
|
let benchmarkCoverage = null;
|
|
@@ -1038,6 +1090,20 @@ async function handleModelAdminApi(req, res, url) {
|
|
|
1038
1090
|
}
|
|
1039
1091
|
}
|
|
1040
1092
|
|
|
1093
|
+
if (p === '/api/models/session-token-summary' && m === 'GET') {
|
|
1094
|
+
if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
|
|
1095
|
+
try {
|
|
1096
|
+
const sessionId = url.searchParams.get('session_id') || url.searchParams.get('sessionId') || '';
|
|
1097
|
+
if (!String(sessionId || '').trim()) return jsonResponse(res, 400, { error: 'session_id required' });
|
|
1098
|
+
const summary = typeof brainApi.summarizeSessionModelUsage === 'function'
|
|
1099
|
+
? brainApi.summarizeSessionModelUsage({ sessionId })
|
|
1100
|
+
: null;
|
|
1101
|
+
return jsonResponse(res, 200, { summary });
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
return jsonResponse(res, 500, { error: e.message });
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1041
1107
|
if (p === '/api/models/test-connection' && m === 'POST') {
|
|
1042
1108
|
try {
|
|
1043
1109
|
const body = await readSmallJsonBody(req);
|
|
@@ -1063,6 +1129,32 @@ async function handleModelAdminApi(req, res, url) {
|
|
|
1063
1129
|
baseUrl,
|
|
1064
1130
|
customHeaders: parseCustomHeaders(customHeaders),
|
|
1065
1131
|
});
|
|
1132
|
+
if (typeof client.probeConnection === 'function') {
|
|
1133
|
+
const probe = await client.probeConnection({
|
|
1134
|
+
model: body.model || body.modelId || body.model_id,
|
|
1135
|
+
timeoutMs: Number(body.timeoutMs || body.timeout_ms || 25000),
|
|
1136
|
+
});
|
|
1137
|
+
try {
|
|
1138
|
+
const { recordProviderHealthStatus } = require('../llm/provider-health-state');
|
|
1139
|
+
recordProviderHealthStatus({
|
|
1140
|
+
provider: providerType,
|
|
1141
|
+
model: probe.model || body.model || '',
|
|
1142
|
+
ok: probe.ok === true,
|
|
1143
|
+
source: 'model-admin-test',
|
|
1144
|
+
error_type: probe.error_type || '',
|
|
1145
|
+
error: probe.error || '',
|
|
1146
|
+
latency_ms: probe.latency_ms,
|
|
1147
|
+
diagnostics: probe.diagnostics || null,
|
|
1148
|
+
}, { brain });
|
|
1149
|
+
} catch {}
|
|
1150
|
+
if (!probe.ok) {
|
|
1151
|
+
return jsonResponse(res, 400, { error: probe.error || 'Provider not available', probe });
|
|
1152
|
+
}
|
|
1153
|
+
const models = Array.isArray(probe.models)
|
|
1154
|
+
? probe.models.map((id) => ({ id, name: id }))
|
|
1155
|
+
: await client.listModels().catch(() => []);
|
|
1156
|
+
return jsonResponse(res, 200, { ok: true, models, probe });
|
|
1157
|
+
}
|
|
1066
1158
|
if (client.isAvailable) {
|
|
1067
1159
|
const available = await client.isAvailable();
|
|
1068
1160
|
if (!available) return jsonResponse(res, 400, { error: 'Provider not available' });
|