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.
Files changed (179) hide show
  1. package/README.md +8 -0
  2. package/bin/create-walle.js +815 -45
  3. package/package.json +2 -2
  4. package/template/bin/ctm-dev-cleanup.js +90 -4
  5. package/template/bin/ctm-launch.sh +49 -1
  6. package/template/bin/dev.sh +45 -1
  7. package/template/bin/ensure-stable-node.js +132 -0
  8. package/template/bin/install-service.sh +9 -0
  9. package/template/claude-task-manager/api-prompts.js +899 -119
  10. package/template/claude-task-manager/approval-agent.js +360 -40
  11. package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
  12. package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
  13. package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
  14. package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
  15. package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
  16. package/template/claude-task-manager/db.js +399 -48
  17. package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
  18. package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
  19. package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
  20. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
  21. package/template/claude-task-manager/lib/approval-hook.js +200 -0
  22. package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
  23. package/template/claude-task-manager/lib/auth-rules.js +11 -0
  24. package/template/claude-task-manager/lib/background-llm.js +32 -4
  25. package/template/claude-task-manager/lib/codesign-identity.js +140 -0
  26. package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
  27. package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
  28. package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
  29. package/template/claude-task-manager/lib/codex-paths.js +73 -0
  30. package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
  31. package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
  32. package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
  33. package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
  34. package/template/claude-task-manager/lib/command-targets.js +163 -0
  35. package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
  36. package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
  37. package/template/claude-task-manager/lib/escalation-review.js +80 -3
  38. package/template/claude-task-manager/lib/flow-control.js +52 -0
  39. package/template/claude-task-manager/lib/fs-watcher.js +24 -15
  40. package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
  41. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
  42. package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
  43. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
  44. package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
  45. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
  46. package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
  47. package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
  48. package/template/claude-task-manager/lib/perf-tracker.js +29 -2
  49. package/template/claude-task-manager/lib/permission-match.js +146 -16
  50. package/template/claude-task-manager/lib/project-slug.js +33 -0
  51. package/template/claude-task-manager/lib/prompt-intent.js +51 -4
  52. package/template/claude-task-manager/lib/read-pool-client.js +48 -3
  53. package/template/claude-task-manager/lib/real-node.js +73 -0
  54. package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
  55. package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
  56. package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
  57. package/template/claude-task-manager/lib/session-history.js +5 -7
  58. package/template/claude-task-manager/lib/session-host-manager.js +19 -0
  59. package/template/claude-task-manager/lib/session-jobs.js +6 -0
  60. package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
  61. package/template/claude-task-manager/lib/session-messages-page.js +211 -0
  62. package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
  63. package/template/claude-task-manager/lib/session-standup.js +8 -0
  64. package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
  65. package/template/claude-task-manager/lib/session-token-usage.js +30 -8
  66. package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
  67. package/template/claude-task-manager/lib/storage-migration.js +2 -1
  68. package/template/claude-task-manager/lib/transcript-store.js +179 -12
  69. package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
  70. package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
  71. package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
  72. package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
  73. package/template/claude-task-manager/package.json +5 -2
  74. package/template/claude-task-manager/prompt-harvest.js +31 -11
  75. package/template/claude-task-manager/providers/claude-code.js +29 -1
  76. package/template/claude-task-manager/providers/codex.js +13 -1
  77. package/template/claude-task-manager/public/css/setup.css +11 -0
  78. package/template/claude-task-manager/public/css/walle-session.css +132 -4
  79. package/template/claude-task-manager/public/css/walle.css +89 -0
  80. package/template/claude-task-manager/public/icon-16.png +0 -0
  81. package/template/claude-task-manager/public/icon-32.png +0 -0
  82. package/template/claude-task-manager/public/icon-512.png +0 -0
  83. package/template/claude-task-manager/public/index.html +2483 -165
  84. package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
  85. package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
  86. package/template/claude-task-manager/public/js/message-renderer.js +60 -1
  87. package/template/claude-task-manager/public/js/prompts.js +13 -1
  88. package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
  89. package/template/claude-task-manager/public/js/setup.js +54 -10
  90. package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
  91. package/template/claude-task-manager/public/js/stream-view.js +78 -0
  92. package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
  93. package/template/claude-task-manager/public/js/tool-state.js +155 -0
  94. package/template/claude-task-manager/public/js/walle-session.js +887 -326
  95. package/template/claude-task-manager/public/js/walle.js +306 -195
  96. package/template/claude-task-manager/public/m/app.css +1 -0
  97. package/template/claude-task-manager/public/m/app.js +33 -3
  98. package/template/claude-task-manager/queue-engine.js +45 -1
  99. package/template/claude-task-manager/server.js +3367 -540
  100. package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
  101. package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
  102. package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
  103. package/template/claude-task-manager/workers/session-host-process.js +10 -0
  104. package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
  105. package/template/package.json +2 -3
  106. package/template/shared/icons/AppIcon-ctm.icns +0 -0
  107. package/template/shared/icons/AppIcon-walle.icns +0 -0
  108. package/template/wall-e/agent.js +139 -18
  109. package/template/wall-e/api-walle.js +201 -22
  110. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
  111. package/template/wall-e/brain.js +1049 -39
  112. package/template/wall-e/chat.js +427 -86
  113. package/template/wall-e/coding/acceptance-contract.js +26 -1
  114. package/template/wall-e/coding/action-memory-policy.js +353 -0
  115. package/template/wall-e/coding/action-memory-store.js +814 -0
  116. package/template/wall-e/coding/initial-messages.js +197 -0
  117. package/template/wall-e/coding/no-progress-guard.js +327 -0
  118. package/template/wall-e/coding/permission-service.js +88 -22
  119. package/template/wall-e/coding/session-workspaces.js +81 -0
  120. package/template/wall-e/coding/shell-sandbox.js +124 -0
  121. package/template/wall-e/coding/stream-processor.js +63 -2
  122. package/template/wall-e/coding/tool-execution-controller.js +14 -1
  123. package/template/wall-e/coding/tool-registry.js +1 -1
  124. package/template/wall-e/coding/transcript-writer.js +3 -0
  125. package/template/wall-e/coding-orchestrator.js +636 -35
  126. package/template/wall-e/coding-prompts.js +51 -2
  127. package/template/wall-e/docs/model-routing-policy.md +59 -0
  128. package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
  129. package/template/wall-e/extraction/knowledge-extractor.js +76 -23
  130. package/template/wall-e/http/chat-api.js +30 -12
  131. package/template/wall-e/http/model-admin.js +93 -1
  132. package/template/wall-e/lib/background-lanes.js +133 -0
  133. package/template/wall-e/lib/boot-profile.js +11 -0
  134. package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
  135. package/template/wall-e/lib/brain-read-pool-client.js +311 -0
  136. package/template/wall-e/lib/diagnostics-flags.js +87 -0
  137. package/template/wall-e/lib/event-loop-monitor.js +74 -3
  138. package/template/wall-e/lib/mcp-integration.js +7 -1
  139. package/template/wall-e/lib/real-node.js +98 -0
  140. package/template/wall-e/lib/runtime-health.js +206 -0
  141. package/template/wall-e/lib/runtime-worker-pool.js +101 -0
  142. package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
  143. package/template/wall-e/lib/scheduler.js +446 -17
  144. package/template/wall-e/lib/service-health.js +61 -2
  145. package/template/wall-e/lib/service-readiness.js +258 -0
  146. package/template/wall-e/lib/usage.js +152 -0
  147. package/template/wall-e/lib/worker-thread-pool.js +389 -0
  148. package/template/wall-e/llm/client.js +81 -4
  149. package/template/wall-e/llm/default-fallback.js +54 -8
  150. package/template/wall-e/llm/mlx.js +536 -73
  151. package/template/wall-e/llm/mlx.plugin.json +1 -1
  152. package/template/wall-e/llm/ollama.js +342 -43
  153. package/template/wall-e/llm/provider-error.js +18 -1
  154. package/template/wall-e/llm/provider-health-state.js +176 -0
  155. package/template/wall-e/llm/routing-policy.js +796 -0
  156. package/template/wall-e/llm/supported-models.js +5 -0
  157. package/template/wall-e/loops/tasks.js +60 -14
  158. package/template/wall-e/loops/think.js +89 -24
  159. package/template/wall-e/mcp-server.js +192 -28
  160. package/template/wall-e/server.js +32 -7
  161. package/template/wall-e/skills/script-skill-runner.js +8 -1
  162. package/template/wall-e/skills/skill-planner.js +64 -1
  163. package/template/wall-e/tools/builtin-middleware.js +67 -2
  164. package/template/wall-e/tools/local-tools.js +116 -26
  165. package/template/wall-e/tools/permission-checker.js +52 -4
  166. package/template/wall-e/tools/permission-rules.js +36 -0
  167. package/template/wall-e/tools/shell-analyzer.js +46 -1
  168. package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
  169. package/template/wall-e/training/real-trajectory-miner.js +2617 -0
  170. package/template/wall-e/training/replay-eval-analysis.js +151 -0
  171. package/template/wall-e/training/run-shell-command-selector.js +277 -0
  172. package/template/wall-e/training/tool-sft-dataset.js +312 -0
  173. package/template/wall-e/training/tool-sft-renderers.js +144 -0
  174. package/template/wall-e/training/tool-trace-harvester.js +1440 -0
  175. package/template/wall-e/training/trajectory-action-selector.js +364 -0
  176. package/template/wall-e/weather-runtime.js +232 -0
  177. package/template/wall-e/workers/brain-owner-worker.js +162 -0
  178. package/template/wall-e/workers/brain-read-worker.js +148 -0
  179. 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
- Working directory: ${resolvedCwd || '(not set)'}
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 truncated = content.length > MAX_CONTENT_LENGTH
18
- ? content.slice(0, MAX_CONTENT_LENGTH) + '...[truncated]'
19
- : content;
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.filter(entry => {
91
- if (!REQUIRED_FIELDS.every(field => entry[field] !== undefined && entry[field] !== null)) {
92
- return false;
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
- if (typeof entry[field] === 'string' && entry[field].length > MAX_FIELD_LENGTH) {
96
- entry[field] = entry[field].slice(0, MAX_FIELD_LENGTH);
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
- entry.confidence = Number(entry.confidence);
101
- if (isNaN(entry.confidence) || entry.confidence < 0 || entry.confidence > 1) {
102
- entry.confidence = 0.5;
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 true;
106
- });
153
+ return normalized;
154
+ }).filter(Boolean);
107
155
 
108
156
  // Validate classifications
109
- const validClassifications = classifications.filter(c =>
110
- c.memory_id && c.domain && VALID_DOMAINS.includes(c.domain)
111
- ).map(c => ({
112
- memory_id: c.memory_id,
113
- domain: c.domain,
114
- topic: typeof c.topic === 'string' ? c.topic.slice(0, 100).toLowerCase() : null,
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.listChatMessages({ session_id: sessionId, limit });
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
- jsonResponse(res, { data: brain.loadChatBranches(sid) });
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/branches' && m === 'POST') {
535
+ if (p === '/api/wall-e/chat/branch-state' && m === 'POST') {
518
536
  if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
519
- // A branch snapshot serializes the whole conversation (active history plus
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
- const updatedAtMs = Number(body.updated_at_ms || body.saved_at_ms || body.updatedAtMs || body.savedAtMs || 0) || Date.now();
526
- brain.saveChatBranches(body.session_id || 'default', body.branches || {}, body.active || {}, body.history || [], updatedAtMs);
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: brainApi.getModelScorecard(model.id, days),
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' });