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
@@ -56,6 +56,7 @@ const adapterRegistry = require('./adapters/registry');
56
56
  const channelRegistry = require('./channels/registry');
57
57
  const { chat: walleChat } = require('./chat');
58
58
  const telemetry = require('./telemetry');
59
+ const runtimeHealth = require('./lib/runtime-health');
59
60
  const fs = require('fs');
60
61
  const path = require('path');
61
62
 
@@ -140,6 +141,12 @@ function clampInterval(value, fallback) {
140
141
  return Math.max(MIN_INTERVAL_MS, Math.min(MAX_INTERVAL_MS, n));
141
142
  }
142
143
 
144
+ function clampTimeoutMs(value, fallback) {
145
+ const n = Number(value);
146
+ const ms = Number.isFinite(n) && n > 0 ? n : fallback;
147
+ return Math.max(1000, Math.min(15 * 60 * 1000, Math.trunc(ms)));
148
+ }
149
+
143
150
  function bootstrapSkills() {
144
151
  const existing = brain.listSkills({});
145
152
  if (existing.length > 0) return; // Already bootstrapped
@@ -302,11 +309,11 @@ async function checkForUpdates() {
302
309
  async function main() {
303
310
  console.log('[wall-e] Starting WALL-E agent daemon...');
304
311
 
305
- // Boot profiler per-phase wall-clock timing so we can SEE where startup time goes
306
- // (logged below + surfaced at GET /api/wall-e/boot-profile). brain.initDb() records
307
- // its own initDb.* sub-phases into the same profile.
312
+ // Boot profiler is opt-in developer diagnostics. It is disabled for normal
313
+ // npx/create-walle users unless WALL_E_RUNTIME_DIAGNOSTICS=1 is set.
308
314
  const _bootp = require('./lib/boot-profile');
309
315
  const { performance: _perf } = require('perf_hooks');
316
+ const { eventLoopMonitorEnabled, bootProfileEnabled, flagSnapshot } = require('./lib/diagnostics-flags');
310
317
  _bootp.reset();
311
318
  let _phaseLast = _perf.now();
312
319
  const _phase = (label) => { const n = _perf.now(); _bootp.mark(label, n - _phaseLast); _phaseLast = n; };
@@ -317,10 +324,40 @@ async function main() {
317
324
  const config = loadOrCreateConfig();
318
325
  _phase('loadConfig');
319
326
 
320
- // Start the event-loop delay monitor early so a synchronous DB query that wedges the
321
- // loop is surfaced in the logs (and on the health endpoint) instead of silently making
322
- // the daemon look alive while every HTTP request times out.
323
- try { require('./lib/event-loop-monitor').start(); } catch (e) { console.warn('[wall-e] event-loop monitor unavailable:', e.message); }
327
+ // Wire the shell-permission CTM bridge ONCE at boot so the coding agent (not just
328
+ // chat) routes run_shell through CTM's unified approver the user's Permission
329
+ // tab. Without this the coding path's permission-checker had no _ctmBaseUrl, fell
330
+ // to its stricter local DEFAULT_RULES, and parked on commands the tab allows
331
+ // (e.g. `cp`, which the tab allows but local rules default to `ask`). The
332
+ // permission-checker is a process singleton, so one configure() covers every
333
+ // entry point; chat.js still re-configures per turn (idempotent + token refresh).
334
+ // When CTM is unreachable the bridge fails closed to the local rules (no hang).
335
+ try {
336
+ const { configure: configurePermissions } = require('./tools/permission-checker');
337
+ const ctmPort = process.env.CTM_PORT || '3456';
338
+ configurePermissions({ ctmUrl: `http://localhost:${ctmPort}`, ctmToken: process.env.CTM_TOKEN });
339
+ } catch (e) { console.warn('[wall-e] permission bridge configure failed:', e.message); }
340
+
341
+ if (eventLoopMonitorEnabled()) {
342
+ // Developer-only monitor: surfaces synchronous DB/I/O stalls without adding
343
+ // timers/noise to packaged npx installs.
344
+ try {
345
+ require('./lib/event-loop-monitor').start({
346
+ getContext: () => {
347
+ let scheduler = null;
348
+ try { scheduler = Scheduler.getInstance()?.getActiveSnapshot?.() || null; } catch {}
349
+ let runtime = null;
350
+ try { runtime = runtimeHealth.snapshot({ recentLimit: 5, topLimit: 5 }); } catch {}
351
+ let workerPool = null;
352
+ try { workerPool = require('./lib/runtime-worker-pool').getRuntimeWorkerPoolStatus(); } catch {}
353
+ return { scheduler, runtime_active: runtime?.active || [], worker_pool: workerPool };
354
+ },
355
+ });
356
+ } catch (e) { console.warn('[wall-e] event-loop monitor unavailable:', e.message); }
357
+ }
358
+ if (flagSnapshot().runtime_diagnostics) {
359
+ console.log('[wall-e] Runtime diagnostics enabled:', JSON.stringify(flagSnapshot()));
360
+ }
324
361
 
325
362
  // Init database (auto-creates data directory if needed)
326
363
  brain.initDb();
@@ -382,8 +419,15 @@ async function main() {
382
419
  console.warn('[wall-e] Auth profile setup failed (non-fatal):', err.message);
383
420
  }
384
421
 
385
- // Version check (async, non-blocking) — alert if a newer version is on npm
386
- checkForUpdates().catch(() => {});
422
+ // Version check (async, non-blocking) — alert if a newer version is on npm.
423
+ // Track it so source/dev restarts can show whether npm/network checks are
424
+ // competing with the first scheduler ticks.
425
+ {
426
+ const op = runtimeHealth.beginOperation('agent.checkForUpdates');
427
+ checkForUpdates()
428
+ .then(() => op.end({ ok: true }))
429
+ .catch((err) => op.end({ ok: false, error: err }));
430
+ }
387
431
 
388
432
  // Skill-reference sync (Learn+Reference system) does per-skill filesystem reads + a
389
433
  // skill_reference scan of the memories table — hundreds of ms to seconds on a large
@@ -484,6 +528,7 @@ async function main() {
484
528
  // Always start HTTP server — WALL-E API should be independently reachable
485
529
  const { startServer } = require('./server');
486
530
  let httpServer;
531
+ const startServerOp = runtimeHealth.beginOperation('agent.startServer');
487
532
  try {
488
533
  const _serverT0 = _perf.now();
489
534
  httpServer = startServer();
@@ -491,42 +536,86 @@ async function main() {
491
536
  _bootp.mark('startServer(portBind)', _perf.now() - _serverT0);
492
537
  _phaseLast = _perf.now();
493
538
  _bootp.done();
494
- console.log(_bootp.summary());
539
+ if (bootProfileEnabled()) console.log(_bootp.summary());
540
+ startServerOp.end({ ok: true });
495
541
  } catch (e) {
542
+ startServerOp.end({ ok: false, error: e });
496
543
  console.error('[wall-e] HTTP server failed to start:', e.message);
497
544
  process.exit(e && e.code === 'EADDRINUSE' ? 98 : 1);
498
545
  }
499
546
 
547
+ const { getRuntimeWorkerPool } = require('./lib/runtime-worker-pool');
548
+ const { supportedSchedulerWorkerJobs } = require('./lib/scheduler-worker-jobs');
549
+ const runtimeWorkerPool = getRuntimeWorkerPool();
550
+ const runRuntimeJob = async (jobName, fallback, options = {}) => {
551
+ if (runtimeWorkerPool) {
552
+ try {
553
+ return await runtimeWorkerPool.request('scheduler.runJob', {
554
+ jobName,
555
+ trigger: options.trigger || 'agent',
556
+ modelOverride: options.modelOverride || null,
557
+ }, {
558
+ timeoutMs: options.timeoutMs || 0,
559
+ heavy: options.heavy !== false,
560
+ terminateOnTimeout: (options.timeoutMs || 0) > 0,
561
+ });
562
+ } catch (err) {
563
+ console.warn(`[wall-e] Runtime worker job "${jobName}" failed; falling back locally: ${err.message}`);
564
+ }
565
+ }
566
+ return await fallback();
567
+ };
568
+
500
569
  // Deferred, off the readiness path: heavy/optional init that must not delay port bind +
501
570
  // MCP readiness. Runs a few seconds after the server is up (the readiness probe completes
502
571
  // first). unref'd so it never holds the process. Sub-phase timings are still captured in
503
572
  // the boot profile (the marks append after startServer), so /api/wall-e/boot-profile shows
504
573
  // exactly which embeddings sub-step is slow on the live brain.
505
574
  setTimeout(() => {
575
+ (async () => {
506
576
  // 1) Embedding subsystem (optional; degrades gracefully — inserts queue for the backfill job)
577
+ const embeddingsOp = runtimeHealth.beginOperation('agent.deferred.embeddingsInit');
507
578
  try {
508
- require('./embeddings').init();
579
+ await runRuntimeJob('deferred:embeddings-init', async () => {
580
+ require('./embeddings').init();
581
+ return { ok: true };
582
+ }, { trigger: 'deferred-init', timeoutMs: 120000 });
583
+ embeddingsOp.end({ ok: true });
509
584
  } catch (e) {
585
+ embeddingsOp.end({ ok: false, error: e });
510
586
  console.log('[wall-e] Embeddings not available:', e.message);
511
587
  }
512
588
  // 2) One-time temporal-validity backfill (no-op on an already-migrated brain)
589
+ const temporalOp = runtimeHealth.beginOperation('agent.deferred.temporalBackfill');
513
590
  try {
514
- const n = brain.backfillTemporalValidity();
591
+ const temporalResult = await runRuntimeJob('deferred:temporal-backfill', async () => ({
592
+ rows: brain.backfillTemporalValidity() || 0,
593
+ }), { trigger: 'deferred-init', timeoutMs: 120000 });
594
+ const n = temporalResult?.rows || 0;
515
595
  if (n > 0) console.log(`[wall-e] Temporal-validity backfill completed (${n} rows, deferred off boot)`);
596
+ temporalOp.end({ ok: true, meta: { rows: n || 0 } });
516
597
  } catch (err) {
598
+ temporalOp.end({ ok: false, error: err });
517
599
  console.warn('[wall-e] Deferred temporal-validity backfill failed (non-fatal):', err.message);
518
600
  }
519
601
  // 3) Skill-reference sync — deferred off the readiness path (was on the boot path,
520
602
  // where its filesystem reads + skill_reference scan delayed HTTP port bind).
603
+ const skillRefOp = runtimeHealth.beginOperation('agent.deferred.skillReferenceSync');
521
604
  try {
522
- const { syncSkillReferences, checkImportedSkillUpdates } = require('./skills/claude-code-reader');
523
- const refResult = syncSkillReferences();
605
+ const refResult = await runRuntimeJob('deferred:skill-reference-sync', async () => {
606
+ const { syncSkillReferences, checkImportedSkillUpdates } = require('./skills/claude-code-reader');
607
+ const syncResult = syncSkillReferences();
608
+ const updates = checkImportedSkillUpdates();
609
+ return { synced: syncResult.synced || 0, updates: updates.length || 0, updateNames: updates.map(u => u.name + '(' + u.action + ')') };
610
+ }, { trigger: 'deferred-init', timeoutMs: 120000 });
524
611
  if (refResult.synced > 0) console.log(`[wall-e] Synced ${refResult.synced} skill references to brain (deferred off boot)`);
525
- const updates = checkImportedSkillUpdates();
526
- if (updates.length > 0) console.log(`[wall-e] Imported skill updates: ${updates.map(u => u.name + '(' + u.action + ')').join(', ')}`);
612
+ if (refResult.updates > 0 && Array.isArray(refResult.updateNames)) console.log(`[wall-e] Imported skill updates: ${refResult.updateNames.join(', ')}`);
613
+ skillRefOp.end({ ok: true, meta: { synced: refResult.synced || 0, updates: refResult.updates || 0 } });
527
614
  } catch (e) {
615
+ skillRefOp.end({ ok: false, error: e });
528
616
  console.warn('[wall-e] Skill reference sync error:', e.message);
529
617
  }
618
+ })().catch((e) => console.warn('[wall-e] Deferred runtime init failed:', e.message));
530
619
  }, 2000).unref?.();
531
620
 
532
621
  // Slack Socket Mode is optional and event-driven. It only starts when an
@@ -556,33 +645,59 @@ async function main() {
556
645
 
557
646
  // Initial ingest (wrapped — must not crash the daemon)
558
647
  let ingestResult = { memoriesIngested: 0 };
648
+ const initialIngestOp = runtimeHealth.beginOperation('agent.initialIngest');
559
649
  try {
560
650
  console.log('[wall-e] Running initial ingest...');
561
- ingestResult = await ingest.runOnce(adapters);
651
+ ingestResult = await runRuntimeJob('ingest', () => ingest.runOnce(adapters), {
652
+ trigger: 'initial-ingest',
653
+ timeoutMs: 120000,
654
+ });
562
655
  console.log(`[wall-e] Initial ingest: ${ingestResult.memoriesIngested} memories`);
563
656
  if (ingestResult.memoriesIngested > 0) {
564
657
  telemetry.track('ingest', { memories: ingestResult.memoriesIngested, initial: true });
565
658
  telemetry.trackFunnelStep('first_ingest');
566
659
  }
660
+ initialIngestOp.end({ ok: true, meta: { memoriesIngested: ingestResult.memoriesIngested || 0 } });
567
661
  } catch (e) {
662
+ initialIngestOp.end({ ok: false, error: e });
568
663
  console.error('[wall-e] Initial ingest failed (non-fatal):', e.message);
569
664
  }
570
665
 
571
666
  // Initial think (wrapped — must not crash the daemon)
572
667
  if (ingestResult.memoriesIngested > 0) {
668
+ const initialThinkOp = runtimeHealth.beginOperation('agent.initialThink');
573
669
  try {
574
670
  console.log('[wall-e] Running initial think...');
575
- const thinkResult = await think.runOnce();
671
+ const thinkResult = await runRuntimeJob('think', () => think.runOnce(), {
672
+ trigger: 'initial-think',
673
+ timeoutMs: 120000,
674
+ });
576
675
  console.log(`[wall-e] Initial think: ${thinkResult.memoriesProcessed} processed, ${thinkResult.knowledgeExtracted} knowledge`);
676
+ initialThinkOp.end({
677
+ ok: true,
678
+ meta: {
679
+ memoriesProcessed: thinkResult.memoriesProcessed || 0,
680
+ knowledgeExtracted: thinkResult.knowledgeExtracted || 0,
681
+ },
682
+ });
577
683
  } catch (e) {
684
+ initialThinkOp.end({ ok: false, error: e });
578
685
  console.error('[wall-e] Initial think failed (non-fatal):', e.message);
579
686
  }
580
687
  }
581
688
 
582
689
  // --- Central Scheduler ---
690
+ const schedulerDefaultWorkerTimeoutMs = clampTimeoutMs(
691
+ process.env.WALL_E_SCHEDULER_WORKER_TIMEOUT_MS ?? process.env.WALLE_SCHEDULER_WORKER_TIMEOUT_MS,
692
+ 180000
693
+ );
583
694
  const scheduler = new Scheduler({
584
695
  tickMs: 5000,
585
696
  pools: { llm: 2, ollama: 1, embedding: 1, io: 3 },
697
+ workerPool: runtimeWorkerPool,
698
+ workerJobs: supportedSchedulerWorkerJobs(),
699
+ defaultWorkerJobTimeoutMs: schedulerDefaultWorkerTimeoutMs,
700
+ closeWorkerPoolOnShutdown: true,
586
701
  shutdownTimeoutMs: 5000,
587
702
  // Failure-alert: after 2 consecutive errors on a single job (with 1h
588
703
  // cooldown), write a Service Alert that the CTM Wall-E tab surfaces.
@@ -1087,6 +1202,7 @@ async function main() {
1087
1202
  // its instructions. Best-effort; never crashes the daemon. Borrowed from
1088
1203
  // OpenClaw's gateway/boot.ts. Runs BEFORE scheduler.start() so the user's
1089
1204
  // contract is honored before the recurring loops fire.
1205
+ const bootCheckOp = runtimeHealth.beginOperation('agent.bootCheck');
1090
1206
  try {
1091
1207
  const { runBootCheck } = require('./loops/boot');
1092
1208
  const bootResult = await runBootCheck();
@@ -1103,7 +1219,9 @@ async function main() {
1103
1219
  console.warn(`[wall-e] BOOT.md run failed: ${bootResult.reason}${bootResult.error ? ' — ' + bootResult.error : ''}`);
1104
1220
  telemetry.track('boot_check', { status: 'failed', reason: bootResult.reason });
1105
1221
  }
1222
+ bootCheckOp.end({ ok: bootResult.status !== 'failed', meta: { status: bootResult.status || 'unknown' } });
1106
1223
  } catch (e) {
1224
+ bootCheckOp.end({ ok: false, error: e });
1107
1225
  console.error('[wall-e] BOOT.md run threw (non-fatal):', e.message);
1108
1226
  telemetry.trackError('boot_check', e);
1109
1227
  }
@@ -1112,6 +1230,7 @@ async function main() {
1112
1230
  // while the daemon was down. Cap of 5 immediate runs (5s stagger);
1113
1231
  // anything beyond that is deferred to the periodic tick. Borrowed
1114
1232
  // from OpenClaw's runMissedJobs (src/cron/service/timer.ts:962-1040).
1233
+ const restartCatchupOp = runtimeHealth.beginOperation('agent.restartCatchup');
1115
1234
  try {
1116
1235
  const catchup = await scheduler.runMissedJobs();
1117
1236
  if (catchup.ran > 0 || catchup.deferred > 0) {
@@ -1123,7 +1242,9 @@ async function main() {
1123
1242
  deferred: catchup.deferred,
1124
1243
  });
1125
1244
  }
1245
+ restartCatchupOp.end({ ok: true, meta: { ran: catchup.ran || 0, deferred: catchup.deferred || 0 } });
1126
1246
  } catch (e) {
1247
+ restartCatchupOp.end({ ok: false, error: e });
1127
1248
  console.warn('[wall-e] runMissedJobs threw (non-fatal):', e.message);
1128
1249
  }
1129
1250
 
@@ -382,6 +382,14 @@ function parseTelemetryOffset(value) {
382
382
  return Math.max(parseInt(value, 10) || 0, 0);
383
383
  }
384
384
 
385
+ function telemetryParamEnabled(params, names) {
386
+ for (const name of names) {
387
+ const raw = String(params.get(name) || '').trim().toLowerCase();
388
+ if (raw === '1' || raw === 'true' || raw === 'yes' || raw === 'full') return true;
389
+ }
390
+ return false;
391
+ }
392
+
385
393
  function telemetryAdminAuth(params) {
386
394
  const secret = process.env.WALLE_TELEMETRY_SECRET;
387
395
  if (!secret) return { ok: false, status: 503, error: 'Telemetry admin secret not configured' };
@@ -591,6 +599,56 @@ function telemetryEventsByTypeSince(tdb, since) {
591
599
  `).all(since, String(since || '').slice(0, 10));
592
600
  }
593
601
 
602
+ function telemetrySanitizeDiagnosticText(value, max = 220) {
603
+ return String(value || 'unknown')
604
+ .replace(/https?:\/\/\S+/g, '[url]')
605
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig, '[email]')
606
+ .replace(/\/Users\/[^\s:"']+/g, '<path>')
607
+ .replace(/\/private\/tmp\/[^\s:"']+/g, '<tmp>')
608
+ .replace(/\bpid=\d+/g, 'pid=<pid>')
609
+ .replace(/[0-9a-f]{8}-[0-9a-f-]{27,}/ig, '<id>')
610
+ .slice(0, max);
611
+ }
612
+
613
+ function telemetryErrorGroupsSince(tdb, since, limit = 20) {
614
+ const rows = tdb.prepare(`
615
+ SELECT version, meta
616
+ FROM telemetry_events
617
+ WHERE event = 'error' AND received_at >= ?
618
+ ORDER BY received_at DESC
619
+ LIMIT 500
620
+ `).all(since);
621
+ const groups = new Map();
622
+ for (const row of rows) {
623
+ let meta = {};
624
+ try { meta = row.meta ? JSON.parse(row.meta) : {}; } catch { meta = { message: row.meta }; }
625
+ const message = telemetrySanitizeDiagnosticText(meta.message || meta.error || meta.type || 'unknown');
626
+ const source = telemetrySanitizeDiagnosticText(meta.source || 'unknown', 80);
627
+ const key = `${message}\n${source}`;
628
+ const current = groups.get(key) || {
629
+ message,
630
+ source,
631
+ count: 0,
632
+ versions: new Set(),
633
+ skills: new Set(),
634
+ };
635
+ current.count += 1;
636
+ if (row.version) current.versions.add(String(row.version).slice(0, 80));
637
+ if (meta.skill) current.skills.add(telemetrySanitizeDiagnosticText(meta.skill, 80));
638
+ groups.set(key, current);
639
+ }
640
+ return Array.from(groups.values())
641
+ .sort((a, b) => b.count - a.count || a.message.localeCompare(b.message))
642
+ .slice(0, limit)
643
+ .map((row) => ({
644
+ message: row.message,
645
+ source: row.source,
646
+ count: row.count,
647
+ versions: Array.from(row.versions).sort(),
648
+ skills: Array.from(row.skills).sort(),
649
+ }));
650
+ }
651
+
594
652
  function telemetrySetIntersectionSize(left, right) {
595
653
  let count = 0;
596
654
  for (const value of left || []) {
@@ -1539,7 +1597,34 @@ function handleWalleApi(req, res, url) {
1539
1597
  if (p === '/api/wall-e/health' && m === 'GET') {
1540
1598
  let eventLoop = { enabled: false };
1541
1599
  try { eventLoop = require('./lib/event-loop-monitor').stats(); } catch {}
1542
- return jsonResponse(res, { status: 'ok', uptime: process.uptime(), version: '0.1.0', event_loop: eventLoop }), true;
1600
+ let runtime = null;
1601
+ try { runtime = require('./lib/runtime-health').snapshot({ recentLimit: 5, topLimit: 10 }); } catch {}
1602
+ let diagnostics = null;
1603
+ try { diagnostics = require('./lib/diagnostics-flags').flagSnapshot(); } catch {}
1604
+ let scheduler = null;
1605
+ try {
1606
+ const { Scheduler } = require('./lib/scheduler');
1607
+ const sched = Scheduler.getInstance();
1608
+ scheduler = sched ? sched.getMetrics({ jobLimit: 10 }) : { enabled: false, running: false };
1609
+ } catch {}
1610
+ let brainWriter = null;
1611
+ try { brainWriter = brain?.getOwnerWriteQueueStatus?.() || null; } catch {}
1612
+ let readiness = null;
1613
+ try {
1614
+ const { getServiceReadiness } = require('./lib/service-readiness');
1615
+ readiness = getServiceReadiness({ eventLoop, runtime, brainWriter });
1616
+ } catch {}
1617
+ return jsonResponse(res, {
1618
+ status: 'ok',
1619
+ uptime: process.uptime(),
1620
+ version: '0.1.0',
1621
+ diagnostics,
1622
+ event_loop: eventLoop,
1623
+ runtime,
1624
+ scheduler,
1625
+ brain_writer: brainWriter,
1626
+ readiness,
1627
+ }), true;
1543
1628
  }
1544
1629
 
1545
1630
  // GET /api/wall-e/alerts — service alerts (auth failures, skill disabled, update available)
@@ -1548,6 +1633,7 @@ function handleWalleApi(req, res, url) {
1548
1633
  res.setHeader('Cache-Control', 'no-store, max-age=0');
1549
1634
  const { getServiceAlerts } = require('./skills/skill-planner');
1550
1635
  const { buildServiceHealth } = require('./lib/service-health');
1636
+ const { getProviderHealthStatus } = require('./llm/provider-health-state');
1551
1637
  const alerts = getServiceAlerts({ includeVersionUpdate: true });
1552
1638
  let activeProvider = process.env.WALLE_PROVIDER || '';
1553
1639
  let activeModel = process.env.WALLE_MODEL || '';
@@ -1558,6 +1644,7 @@ function handleWalleApi(req, res, url) {
1558
1644
  const summary = buildServiceHealth(alerts, {
1559
1645
  activeProvider,
1560
1646
  activeModel,
1647
+ providerStatus: getProviderHealthStatus(activeProvider, activeModel),
1561
1648
  });
1562
1649
  return jsonResponse(res, { alerts, summary }), true;
1563
1650
  } catch (e) {
@@ -1761,7 +1848,11 @@ function handleWalleApi(req, res, url) {
1761
1848
  if (p.startsWith('/api/wall-e/alerts/') && m === 'DELETE') {
1762
1849
  try {
1763
1850
  const alertId = decodeURIComponent(p.split('/api/wall-e/alerts/')[1]);
1764
- if (alertId === 'version_update') {
1851
+ if (alertId.startsWith('provider_status:')) {
1852
+ const { clearProviderHealthStatus, parseProviderStatusIssueId } = require('./llm/provider-health-state');
1853
+ const target = parseProviderStatusIssueId(alertId);
1854
+ if (target) clearProviderHealthStatus(target);
1855
+ } else if (alertId === 'version_update') {
1765
1856
  try {
1766
1857
  const telemetry = require('./telemetry');
1767
1858
  const updateRaw = brain.getKv('update_available');
@@ -1802,6 +1893,7 @@ function handleWalleApi(req, res, url) {
1802
1893
  tickMs: sched._tickMs,
1803
1894
  jobs: sched.getState(),
1804
1895
  pools: sched.getPoolState(),
1896
+ metrics: sched.getMetrics(),
1805
1897
  }), true;
1806
1898
  }
1807
1899
 
@@ -2507,10 +2599,12 @@ function handleWalleApi(req, res, url) {
2507
2599
  try {
2508
2600
  const { getServiceAlerts } = require('./skills/skill-planner');
2509
2601
  const { buildServiceHealth } = require('./lib/service-health');
2602
+ const { getProviderHealthStatus } = require('./llm/provider-health-state');
2510
2603
  serviceAlerts = getServiceAlerts({ includeVersionUpdate: true });
2511
2604
  serviceAlertSummary = buildServiceHealth(serviceAlerts, {
2512
2605
  activeProvider: walleProvider,
2513
2606
  activeModel: walleModel,
2607
+ providerStatus: getProviderHealthStatus(walleProvider, walleModel),
2514
2608
  });
2515
2609
  } catch {}
2516
2610
 
@@ -2520,6 +2614,23 @@ function handleWalleApi(req, res, url) {
2520
2614
  slackSocketMode = getSlackSocketModeStatus();
2521
2615
  } catch {}
2522
2616
 
2617
+ let eventLoop = { enabled: false };
2618
+ try { eventLoop = require('./lib/event-loop-monitor').stats(); } catch {}
2619
+ let runtime = null;
2620
+ try { runtime = require('./lib/runtime-health').snapshot({ recentLimit: 10, topLimit: 20 }); } catch {}
2621
+ let brainWriter = null;
2622
+ try { brainWriter = brain?.getOwnerWriteQueueStatus?.() || null; } catch {}
2623
+ let readiness = null;
2624
+ try {
2625
+ const { getServiceReadiness } = require('./lib/service-readiness');
2626
+ readiness = getServiceReadiness({
2627
+ eventLoop,
2628
+ runtime,
2629
+ brainWriter,
2630
+ serviceHealth: serviceAlertSummary,
2631
+ });
2632
+ } catch {}
2633
+
2523
2634
  return jsonResponse(res, {
2524
2635
  data: {
2525
2636
  ...result,
@@ -2538,6 +2649,10 @@ function handleWalleApi(req, res, url) {
2538
2649
  service_alerts: serviceAlerts,
2539
2650
  service_alert_summary: serviceAlertSummary,
2540
2651
  slack_socket_mode: slackSocketMode,
2652
+ event_loop: eventLoop,
2653
+ runtime,
2654
+ brain_writer: brainWriter,
2655
+ readiness,
2541
2656
  }
2542
2657
  }), true;
2543
2658
  }
@@ -5909,6 +6024,7 @@ function handleWalleApi(req, res, url) {
5909
6024
  }
5910
6025
  const days = parseTelemetryDays(params.get('days'), 30);
5911
6026
  const since = telemetrySinceDateTime(days);
6027
+ const includeIdentifiers = telemetryParamEnabled(params, ['full', 'include_identifiers', 'identifiers', 'debug']);
5912
6028
  const dailyActivityDays = Math.min(days, 7);
5913
6029
  const dailyActivitySince = telemetrySinceDateTime(dailyActivityDays);
5914
6030
  const stableIdentitySql = "CASE WHEN machine_bucket IS NOT NULL AND machine_bucket != '' THEN 'm:' || machine_bucket ELSE 'i:' || install_id END";
@@ -5939,15 +6055,10 @@ function handleWalleApi(req, res, url) {
5939
6055
  unique_ips: tdb.prepare(
5940
6056
  "SELECT count(DISTINCT ip) as cnt FROM telemetry_installs WHERE ip IS NOT NULL AND ip != ''"
5941
6057
  ).get().cnt,
5942
- ip_breakdown: tdb.prepare(
5943
- "SELECT ip, count(*) as installs, group_concat(install_id) as install_ids FROM telemetry_installs WHERE ip IS NOT NULL AND ip != '' GROUP BY ip ORDER BY installs DESC"
5944
- ).all(),
5945
- owner_hash_breakdown: tdb.prepare(
5946
- "SELECT owner_name_hash, count(*) as installs, max(last_seen) as last_seen FROM telemetry_installs WHERE owner_name_hash IS NOT NULL AND owner_name_hash != '' GROUP BY owner_name_hash ORDER BY installs DESC LIMIT 100"
5947
- ).all(),
5948
- recent_errors: tdb.prepare(
5949
- "SELECT install_id, version, meta, received_at FROM telemetry_events WHERE event = 'error' AND received_at >= ? ORDER BY received_at DESC LIMIT 50"
5950
- ).all(since),
6058
+ unique_owner_hashes: tdb.prepare(
6059
+ "SELECT count(DISTINCT owner_name_hash) as cnt FROM telemetry_installs WHERE owner_name_hash IS NOT NULL AND owner_name_hash != ''"
6060
+ ).get().cnt,
6061
+ error_groups: telemetryErrorGroupsSince(tdb, since),
5951
6062
  daily_activity: tdb.prepare(
5952
6063
  `SELECT date(te.received_at) as day, count(DISTINCT ${eventStableIdentitySql}) as users, count(*) as events
5953
6064
  FROM telemetry_events te
@@ -5955,7 +6066,22 @@ function handleWalleApi(req, res, url) {
5955
6066
  WHERE te.received_at >= ?
5956
6067
  GROUP BY day ORDER BY day DESC LIMIT ?`
5957
6068
  ).all(dailyActivitySince, dailyActivityDays),
6069
+ privacy: {
6070
+ identifiers: includeIdentifiers ? 'included' : 'omitted',
6071
+ full: includeIdentifiers,
6072
+ },
5958
6073
  };
6074
+ if (includeIdentifiers) {
6075
+ summary.ip_breakdown = tdb.prepare(
6076
+ "SELECT ip, count(*) as installs, group_concat(install_id) as install_ids FROM telemetry_installs WHERE ip IS NOT NULL AND ip != '' GROUP BY ip ORDER BY installs DESC LIMIT 100"
6077
+ ).all();
6078
+ summary.owner_hash_breakdown = tdb.prepare(
6079
+ "SELECT owner_name_hash, count(*) as installs, max(last_seen) as last_seen FROM telemetry_installs WHERE owner_name_hash IS NOT NULL AND owner_name_hash != '' GROUP BY owner_name_hash ORDER BY installs DESC LIMIT 100"
6080
+ ).all();
6081
+ summary.recent_errors = tdb.prepare(
6082
+ "SELECT install_id, version, meta, received_at FROM telemetry_events WHERE event = 'error' AND received_at >= ? ORDER BY received_at DESC LIMIT 50"
6083
+ ).all(since);
6084
+ }
5959
6085
  summary.install_machine_ratio = summary.unique_machines > 0
5960
6086
  ? Math.round(summary.total_install_ids / summary.unique_machines * 100) / 100
5961
6087
  : null;
@@ -5968,18 +6094,23 @@ function handleWalleApi(req, res, url) {
5968
6094
  summary.feedback_by_status = tdb.prepare(
5969
6095
  'SELECT status, count(*) as cnt FROM feedback_reports WHERE received_at >= ? GROUP BY status ORDER BY cnt DESC'
5970
6096
  ).all(since);
5971
- summary.recent_feedback = tdb.prepare(`
5972
- SELECT feedback_id, type, status, severity, title, consent_level, version, received_at
5973
- FROM feedback_reports
5974
- WHERE received_at >= ?
5975
- ORDER BY received_at DESC
5976
- LIMIT 20
5977
- `).all(since);
6097
+ if (includeIdentifiers) {
6098
+ summary.recent_feedback = tdb.prepare(`
6099
+ SELECT feedback_id, type, status, severity, title, consent_level, version, received_at
6100
+ FROM feedback_reports
6101
+ WHERE received_at >= ?
6102
+ ORDER BY received_at DESC
6103
+ LIMIT 20
6104
+ `).all(since);
6105
+ } else {
6106
+ summary.recent_feedback_omitted = true;
6107
+ }
5978
6108
  } catch {
5979
6109
  summary.feedback_count = 0;
5980
6110
  summary.feedback_by_type = [];
5981
6111
  summary.feedback_by_status = [];
5982
- summary.recent_feedback = [];
6112
+ if (includeIdentifiers) summary.recent_feedback = [];
6113
+ else summary.recent_feedback_omitted = true;
5983
6114
  }
5984
6115
 
5985
6116
  try {
@@ -6655,10 +6786,24 @@ function handleWalleApi(req, res, url) {
6655
6786
  }
6656
6787
  }
6657
6788
 
6789
+ // POST /api/wall-e/permissions/reevaluate — re-check parked requests against the
6790
+ // current rules and auto-resolve any now-allowed. CTM calls this after adding an
6791
+ // allow rule (Pending-tab whitelist) so a matching park self-heals without a
6792
+ // poller. Must precede the /:id/reply matcher (which would otherwise swallow it).
6793
+ if (p === '/api/wall-e/permissions/reevaluate' && m === 'POST') {
6794
+ readBody(req).then(body => {
6795
+ const sessionId = body.session_id || body.sessionId || '';
6796
+ Promise.resolve(_permissionRegistry.reEvaluatePending({ sessionId }))
6797
+ .then(() => jsonResponse(res, { ok: true }))
6798
+ .catch(e => jsonResponse(res, { error: e.message }, 500));
6799
+ }).catch(e => jsonResponse(res, { error: e.message }, 500));
6800
+ return true;
6801
+ }
6802
+
6658
6803
  // POST /api/wall-e/permissions/:id/reply — reply once/always/reject
6659
6804
  const permissionReplyMatch = p.match(/^\/api\/wall-e\/permissions\/([^/]+)\/reply$/);
6660
6805
  if (permissionReplyMatch && m === 'POST') {
6661
- readBody(req).then(body => {
6806
+ readBody(req).then(async body => {
6662
6807
  const reply = body.reply || body.decision;
6663
6808
  if (!['once', 'always', 'reject'].includes(reply)) {
6664
6809
  return jsonResponse(res, { error: 'reply must be once, always, or reject' }, 400);
@@ -6668,8 +6813,42 @@ function handleWalleApi(req, res, url) {
6668
6813
  reply,
6669
6814
  message: body.message || '',
6670
6815
  });
6671
- if (!ok) return jsonResponse(res, { error: 'Permission request not found' }, 404);
6672
- jsonResponse(res, { ok: true });
6816
+ // A live parked turn accepted the reply -> it resumes immediately.
6817
+ if (ok) return jsonResponse(res, { ok: true, resumed: true });
6818
+
6819
+ // ORPHANED: the parked request is gone (Wall-E restart, the turn already
6820
+ // ended, or a card rehydrated from history). The original turn cannot be
6821
+ // resumed here. If the user chose "always", persist a durable rule derived
6822
+ // from the command so a FRESH continuation turn (started by CTM) won't
6823
+ // re-park on the same command. (CTM starts that turn; this just makes it
6824
+ // clean.) The rule store is brain-KV backed -> shared with the orchestrator's
6825
+ // permission checker and survives restart.
6826
+ let ruleAdded = false;
6827
+ if (reply === 'always' && body.command) {
6828
+ try {
6829
+ const { buildToolPermissionRequests } = require('./coding/permission-service');
6830
+ const requests = await buildToolPermissionRequests(
6831
+ body.tool || 'run_shell',
6832
+ { command: body.command },
6833
+ { cwd: body.projectRoot || '', projectRoot: body.projectRoot || '' },
6834
+ );
6835
+ for (const r of requests) {
6836
+ _permissionRegistry.rulesStore.addAlways({
6837
+ projectRoot: r.projectRoot,
6838
+ permission: r.permission,
6839
+ pattern: r.pattern,
6840
+ tool: r.tool,
6841
+ fingerprint: r.fingerprint,
6842
+ });
6843
+ ruleAdded = true;
6844
+ }
6845
+ // The new rule may also cover OTHER still-parked requests.
6846
+ if (ruleAdded) await _permissionRegistry.reEvaluatePending({});
6847
+ } catch (e) {
6848
+ console.error('[walle-permission] orphan rule-add failed:', e.message);
6849
+ }
6850
+ }
6851
+ return jsonResponse(res, { ok: false, resumed: false, orphaned: true, ruleAdded }, ruleAdded ? 200 : 404);
6673
6852
  }).catch(e => jsonResponse(res, { error: e.message }, 500));
6674
6853
  return true;
6675
6854
  }