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
@@ -0,0 +1,796 @@
1
+ 'use strict';
2
+
3
+ const providerRegistry = require('./registry');
4
+ const { isCliBinaryAvailable, _internals: fallbackInternals } = require('./default-fallback');
5
+ const { getProviderHealthStatus } = require('./provider-health-state');
6
+
7
+ const POLICY_KEYS = {
8
+ mode: 'model_routing_policy_mode',
9
+ cliTail: 'model_routing_policy_cli_tail',
10
+ finalizationFallback: 'model_routing_policy_finalization_fallback',
11
+ lastRefreshAt: 'model_routing_policy_last_refresh_at',
12
+ };
13
+
14
+ const VALID_MODES = new Set(['smart', 'strict']);
15
+ const LOCAL_PROVIDER_TYPES = new Set(['ollama', 'mlx', 'lmstudio']);
16
+ const GATEWAY_ONLY_TYPES = new Set(['portkey_gateway']);
17
+
18
+ function nowIso() {
19
+ return new Date().toISOString();
20
+ }
21
+
22
+ function normalizeProviderType(value) {
23
+ const text = String(value || '').trim().toLowerCase().replace(/_/g, '-');
24
+ if (text === 'kimi' || text === 'kimi-k2' || text === 'moonshot-ai') return 'moonshot';
25
+ if (text === 'openai-compatible' || text.startsWith('openai-compatible-')) return 'openai';
26
+ return text;
27
+ }
28
+
29
+ function providerDisplayName(type) {
30
+ providerRegistry.ensureBootstrapped();
31
+ const entry = providerRegistry.get(normalizeProviderType(type));
32
+ if (entry?.manifest?.label) return entry.manifest.label;
33
+ return String(type || 'Provider')
34
+ .split(/[-_]+/)
35
+ .filter(Boolean)
36
+ .map((part) => part.length <= 3 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1))
37
+ .join(' ') || 'Provider';
38
+ }
39
+
40
+ function getKv(brain, key) {
41
+ try { return typeof brain?.getKv === 'function' ? brain.getKv(key) : ''; } catch { return ''; }
42
+ }
43
+
44
+ function setKv(brain, key, value) {
45
+ try {
46
+ if (typeof brain?.setKv === 'function') {
47
+ brain.setKv(key, value);
48
+ return true;
49
+ }
50
+ } catch {}
51
+ return false;
52
+ }
53
+
54
+ function kvBool(brain, key, fallback) {
55
+ const raw = getKv(brain, key);
56
+ if (raw === '') return fallback;
57
+ const text = String(raw).trim().toLowerCase();
58
+ if (['1', 'true', 'yes', 'on'].includes(text)) return true;
59
+ if (['0', 'false', 'no', 'off'].includes(text)) return false;
60
+ return fallback;
61
+ }
62
+
63
+ function getRoutingPolicySettings(brain) {
64
+ if (typeof brain?.ensureModelRoutingPolicy === 'function') {
65
+ try {
66
+ const row = brain.ensureModelRoutingPolicy('default');
67
+ if (row) {
68
+ return {
69
+ mode: row.mode || 'smart',
70
+ fallback_enabled: (row.mode || 'smart') !== 'strict',
71
+ cli_tail: row.cli_tail !== false,
72
+ finalization_fallback: row.finalization_fallback !== false,
73
+ last_refreshed_at: row.last_generated_at || null,
74
+ last_effective_at: row.last_effective_at || null,
75
+ last_error: row.last_error || null,
76
+ stale_reason: row.stale_reason || null,
77
+ };
78
+ }
79
+ } catch {}
80
+ }
81
+ const rawMode = String(getKv(brain, POLICY_KEYS.mode) || 'smart').trim().toLowerCase();
82
+ const mode = VALID_MODES.has(rawMode) ? rawMode : 'smart';
83
+ return {
84
+ mode,
85
+ fallback_enabled: mode !== 'strict',
86
+ cli_tail: kvBool(brain, POLICY_KEYS.cliTail, true),
87
+ finalization_fallback: kvBool(brain, POLICY_KEYS.finalizationFallback, true),
88
+ last_refreshed_at: getKv(brain, POLICY_KEYS.lastRefreshAt) || null,
89
+ };
90
+ }
91
+
92
+ function setRoutingPolicySettings(brain, input = {}) {
93
+ const current = getRoutingPolicySettings(brain);
94
+ const next = { ...current };
95
+ if (Object.prototype.hasOwnProperty.call(input, 'mode')) {
96
+ const mode = String(input.mode || '').trim().toLowerCase();
97
+ if (!VALID_MODES.has(mode)) throw new Error(`Invalid routing policy mode: ${input.mode}`);
98
+ next.mode = mode;
99
+ next.fallback_enabled = mode !== 'strict';
100
+ }
101
+ if (Object.prototype.hasOwnProperty.call(input, 'cli_tail')) next.cli_tail = input.cli_tail !== false;
102
+ if (Object.prototype.hasOwnProperty.call(input, 'cliTail')) next.cli_tail = input.cliTail !== false;
103
+ if (Object.prototype.hasOwnProperty.call(input, 'finalization_fallback')) next.finalization_fallback = input.finalization_fallback !== false;
104
+ if (Object.prototype.hasOwnProperty.call(input, 'finalizationFallback')) next.finalization_fallback = input.finalizationFallback !== false;
105
+
106
+ if (typeof brain?.updateModelRoutingPolicy === 'function') {
107
+ try {
108
+ const row = brain.updateModelRoutingPolicy('default', {
109
+ mode: next.mode,
110
+ cli_tail: next.cli_tail,
111
+ finalization_fallback: next.finalization_fallback,
112
+ });
113
+ return getRoutingPolicySettings({
114
+ ...brain,
115
+ ensureModelRoutingPolicy: () => row,
116
+ });
117
+ } catch {}
118
+ }
119
+ setKv(brain, POLICY_KEYS.mode, next.mode);
120
+ setKv(brain, POLICY_KEYS.cliTail, next.cli_tail ? '1' : '0');
121
+ setKv(brain, POLICY_KEYS.finalizationFallback, next.finalization_fallback ? '1' : '0');
122
+ return getRoutingPolicySettings(brain);
123
+ }
124
+
125
+ function modelMatchesProvider(model, type) {
126
+ try {
127
+ providerRegistry.ensureBootstrapped();
128
+ return providerRegistry.modelMatchesProvider(model, normalizeProviderType(type));
129
+ } catch {
130
+ return true;
131
+ }
132
+ }
133
+
134
+ function defaultModelForProvider(type) {
135
+ providerRegistry.ensureBootstrapped();
136
+ return providerRegistry.getDefaultModel(normalizeProviderType(type)) || providerRegistry.getDefaultModel('anthropic') || '';
137
+ }
138
+
139
+ function modelForProvider(brain, type, fallbackModel) {
140
+ const providerType = normalizeProviderType(type);
141
+ const specific = getKv(brain, `walle_model_${providerType}`);
142
+ if (specific && modelMatchesProvider(specific, providerType)) return specific;
143
+ if (fallbackModel && modelMatchesProvider(fallbackModel, providerType)) return fallbackModel;
144
+ const global = getKv(brain, 'walle_model');
145
+ if (global && modelMatchesProvider(global, providerType)) return global;
146
+ return defaultModelForProvider(providerType);
147
+ }
148
+
149
+ function providerRuntimeType(row = {}) {
150
+ const type = normalizeProviderType(row.type || row.provider_type);
151
+ if (type === 'anthropic' && row.auth_method === 'claude_cli') return 'claude-cli';
152
+ if (type === 'openai' && row.auth_method === 'codex_cli') return 'codex-cli';
153
+ return type;
154
+ }
155
+
156
+ function providerRuntimeAccess(row = {}, env = process.env) {
157
+ const type = normalizeProviderType(row.type || row.provider_type);
158
+ const authMethod = row.auth_method || 'api_key';
159
+ if (!row.enabled) return { ok: false, kind: 'disabled', reason: 'Provider is disabled.' };
160
+ if (LOCAL_PROVIDER_TYPES.has(type)) return { ok: true, kind: 'local', reason: 'Local runtime provider.' };
161
+ if (authMethod && authMethod !== 'api_key') return { ok: true, kind: authMethod, reason: 'Configured provider auth method.' };
162
+ if (row.api_key_encrypted) return { ok: true, kind: 'stored_credential', reason: 'Stored provider credential.' };
163
+ if (fallbackInternals._hasEnvKey(type, env)) return { ok: true, kind: 'environment', reason: 'Credential available from environment.' };
164
+ return { ok: false, kind: 'missing_credential', reason: 'No stored credential or environment key.' };
165
+ }
166
+
167
+ function publicRuntimeHealth(id, type, model, health, brain) {
168
+ const runtime = id && health?.getProviderHealth ? health.getProviderHealth(id) : null;
169
+ const persisted = getProviderHealthStatus(type, model, { brain });
170
+ const runtimeHasEvidence = runtime && Number(runtime.totalCalls || 0) > 0;
171
+ const status = runtimeHasEvidence ? runtime.status : (persisted ? persisted.status : 'unknown');
172
+ return {
173
+ status,
174
+ consecutive_failures: runtimeHasEvidence ? (runtime?.consecutiveFailures || 0) : 0,
175
+ last_error: runtime?.lastError?.message || persisted?.error || '',
176
+ last_success_at: runtimeHasEvidence && runtime?.lastSuccess ? runtime.lastSuccess.toISOString?.() || String(runtime.lastSuccess) : null,
177
+ persisted_status: persisted || null,
178
+ };
179
+ }
180
+
181
+ function loadProviderRows(brain) {
182
+ const rows = [];
183
+ try {
184
+ for (const row of brain?.listModelProviders?.() || []) {
185
+ if (!row || GATEWAY_ONLY_TYPES.has(normalizeProviderType(row.type))) continue;
186
+ try {
187
+ rows.push(brain.getModelProviderWithKey?.(row.id) || row);
188
+ } catch {
189
+ rows.push(row);
190
+ }
191
+ }
192
+ } catch {}
193
+ return rows;
194
+ }
195
+
196
+ function firstEnabledModelForProvider(brain, providerId) {
197
+ try {
198
+ const model = (brain?.listModelsByProvider?.(providerId) || []).find((item) => item && item.enabled !== 0);
199
+ return model?.model_id || model?.modelId || '';
200
+ } catch {
201
+ return '';
202
+ }
203
+ }
204
+
205
+ function rowCandidate(row, context) {
206
+ const type = normalizeProviderType(row.type);
207
+ const runtimeType = providerRuntimeType(row);
208
+ const access = providerRuntimeAccess(row, context.env);
209
+ const firstModel = firstEnabledModelForProvider(context.brain, row.id);
210
+ const model = runtimeType.endsWith('-cli')
211
+ ? defaultModelForProvider(runtimeType)
212
+ : modelForProvider(context.brain, type, firstModel);
213
+ const health = publicRuntimeHealth(row.id, runtimeType, model, context.health, context.brain);
214
+ const isDown = health.status === 'down' || health.status === 'error';
215
+ const routePolicy = typeof context.brain?.getProviderRoutePolicy === 'function'
216
+ ? context.brain.getProviderRoutePolicy(type)
217
+ : 'auto';
218
+ const included = access.ok && !isDown;
219
+ return {
220
+ id: row.id || `${type}-default`,
221
+ provider_id: row.id || null,
222
+ provider_type: type,
223
+ runtime_type: runtimeType,
224
+ provider_name: row.name || providerDisplayName(type),
225
+ label: row.name || providerDisplayName(type),
226
+ model,
227
+ source: 'provider-card',
228
+ connection_kind: row.auth_method && row.auth_method !== 'api_key' ? row.auth_method : 'direct',
229
+ route_policy: routePolicy,
230
+ runtime_access: access.kind,
231
+ included,
232
+ excluded_reason: included ? '' : (isDown ? 'Provider is marked down by runtime health.' : access.reason),
233
+ health,
234
+ updated_at: row.updated_at || '',
235
+ };
236
+ }
237
+
238
+ function envCandidate(type, context) {
239
+ const providerType = normalizeProviderType(type);
240
+ if (!fallbackInternals._hasEnvKey(providerType, context.env)) return null;
241
+ const model = modelForProvider(context.brain, providerType, '');
242
+ const id = `${providerType}-env`;
243
+ return {
244
+ id,
245
+ provider_id: null,
246
+ provider_type: providerType,
247
+ runtime_type: providerType,
248
+ provider_name: providerDisplayName(providerType),
249
+ label: `${providerDisplayName(providerType)} env`,
250
+ model,
251
+ source: 'environment',
252
+ connection_kind: 'environment',
253
+ route_policy: 'auto',
254
+ runtime_access: 'environment',
255
+ included: true,
256
+ excluded_reason: '',
257
+ health: publicRuntimeHealth(id, providerType, model, context.health, context.brain),
258
+ updated_at: '',
259
+ };
260
+ }
261
+
262
+ function cliCandidate(id, bin, context) {
263
+ const available = context.settings.cli_tail && context.isCliAvailable(bin);
264
+ const model = defaultModelForProvider(id);
265
+ return {
266
+ id,
267
+ provider_id: null,
268
+ provider_type: id,
269
+ runtime_type: id,
270
+ provider_name: providerDisplayName(id),
271
+ label: providerDisplayName(id),
272
+ model,
273
+ source: 'cli-tail',
274
+ connection_kind: 'cli',
275
+ route_policy: 'auto',
276
+ runtime_access: available ? 'cli' : 'missing_cli',
277
+ included: available,
278
+ excluded_reason: available ? '' : `${bin} CLI not found or CLI fallback is disabled.`,
279
+ health: publicRuntimeHealth(id, id, model, context.health, context.brain),
280
+ updated_at: '',
281
+ };
282
+ }
283
+
284
+ function providerTypeOrderScore(candidate, defaultType) {
285
+ if (candidate.provider_type === defaultType || candidate.runtime_type === defaultType) return -1000;
286
+ if (!candidate.included) return 1000;
287
+ if (candidate.health?.status === 'healthy') return 0;
288
+ if (candidate.health?.status === 'degraded') return 10;
289
+ if (candidate.source === 'provider-card') return 20;
290
+ if (candidate.source === 'environment') return 30;
291
+ if (candidate.source === 'cli-tail') return 70;
292
+ return 50;
293
+ }
294
+
295
+ function dedupeProviderCandidates(candidates, brain) {
296
+ const byType = new Map();
297
+ const preferredByType = new Map();
298
+ for (const row of loadProviderRows(brain)) {
299
+ const type = normalizeProviderType(row.type);
300
+ if (!preferredByType.has(type)) {
301
+ try { preferredByType.set(type, brain.getPreferredModelProviderForType?.(type)?.id || ''); } catch {}
302
+ }
303
+ }
304
+ for (const candidate of candidates) {
305
+ if (candidate.source !== 'provider-card') {
306
+ byType.set(candidate.id, candidate);
307
+ continue;
308
+ }
309
+ const key = candidate.provider_type;
310
+ const existing = byType.get(key);
311
+ if (!existing) {
312
+ byType.set(key, candidate);
313
+ continue;
314
+ }
315
+ const preferredId = preferredByType.get(key);
316
+ if (preferredId && candidate.provider_id === preferredId) {
317
+ byType.set(key, candidate);
318
+ continue;
319
+ }
320
+ if (candidate.included && !existing.included) {
321
+ byType.set(key, candidate);
322
+ continue;
323
+ }
324
+ if (candidate.included === existing.included && String(candidate.updated_at || '') > String(existing.updated_at || '')) {
325
+ byType.set(key, candidate);
326
+ }
327
+ }
328
+ return Array.from(byType.values());
329
+ }
330
+
331
+ function orderCandidates(candidates, defaultType) {
332
+ return candidates.slice().sort((a, b) => {
333
+ const ar = providerTypeOrderScore(a, defaultType);
334
+ const br = providerTypeOrderScore(b, defaultType);
335
+ if (ar !== br) return ar - br;
336
+ if (String(b.updated_at || '') !== String(a.updated_at || '')) return String(b.updated_at || '').localeCompare(String(a.updated_at || ''));
337
+ return String(a.label || a.id).localeCompare(String(b.label || b.id));
338
+ });
339
+ }
340
+
341
+ function primaryFromCandidates(candidates, defaultType) {
342
+ if (defaultType) {
343
+ const exact = candidates.find((candidate) => candidate.included && (
344
+ candidate.provider_type === defaultType || candidate.runtime_type === defaultType
345
+ ));
346
+ if (exact) return exact;
347
+ }
348
+ return candidates.find((candidate) => candidate.included && candidate.source !== 'cli-tail')
349
+ || candidates.find((candidate) => candidate.included)
350
+ || null;
351
+ }
352
+
353
+ function applyDefaultRepair(brain, policy) {
354
+ const actions = [];
355
+ const currentType = policy.default?.configured_provider_type || '';
356
+ const primary = policy.default?.candidate || null;
357
+ if (!primary || primary.source === 'cli-tail') return actions;
358
+ const needsProvider = !currentType || policy.default.status === 'unusable';
359
+ const needsModel = primary.model && !getKv(brain, 'walle_model');
360
+ if (!needsProvider && !needsModel) return actions;
361
+ try {
362
+ if (typeof brain.setDefaultProviderSelection === 'function') {
363
+ brain.setDefaultProviderSelection({
364
+ type: primary.provider_type,
365
+ requestedModel: primary.model,
366
+ targetModel: primary.model,
367
+ });
368
+ } else {
369
+ setKv(brain, 'walle_provider', primary.provider_type);
370
+ if (primary.model) setKv(brain, 'walle_model', primary.model);
371
+ }
372
+ actions.push({
373
+ type: needsProvider ? 'default_provider_selected' : 'default_model_selected',
374
+ provider_type: primary.provider_type,
375
+ model: primary.model,
376
+ });
377
+ } catch (err) {
378
+ actions.push({ type: 'default_repair_failed', error: err.message || String(err) });
379
+ }
380
+ return actions;
381
+ }
382
+
383
+ function buildRoutingPolicy(options = {}) {
384
+ providerRegistry.ensureBootstrapped();
385
+ const brain = options.brain;
386
+ const env = options.env || process.env;
387
+ const health = options.health || safeDefaultHealth();
388
+ const settings = getRoutingPolicySettings(brain);
389
+ const context = {
390
+ brain,
391
+ env,
392
+ health,
393
+ settings,
394
+ isCliAvailable: options.isCliAvailable || isCliBinaryAvailable,
395
+ };
396
+ const rawDefault = normalizeProviderType(getKv(brain, 'walle_provider') || env.WALLE_PROVIDER || '');
397
+ const rawDefaultSource = getKv(brain, 'walle_provider') ? 'provider-card' : (env.WALLE_PROVIDER ? 'environment' : 'auto');
398
+ const rowCandidates = dedupeProviderCandidates(loadProviderRows(brain).map((row) => rowCandidate(row, context)), brain);
399
+ const rowTypes = new Set(rowCandidates.map((candidate) => candidate.provider_type));
400
+ const envCandidates = providerRegistry.ids()
401
+ .filter((id) => !rowTypes.has(normalizeProviderType(id)) && !String(id).endsWith('-cli'))
402
+ .map((id) => envCandidate(id, context))
403
+ .filter(Boolean);
404
+ const cliCandidates = [
405
+ cliCandidate('claude-cli', 'claude', context),
406
+ cliCandidate('codex-cli', 'codex', context),
407
+ ];
408
+ let candidates = rowCandidates.concat(envCandidates, cliCandidates);
409
+ if (settings.mode === 'strict' && rawDefault) {
410
+ candidates = candidates.map((candidate) => {
411
+ const isDefault = rawDefault && (candidate.provider_type === rawDefault || candidate.runtime_type === rawDefault);
412
+ if (isDefault) return candidate;
413
+ return { ...candidate, included: false, excluded_reason: 'Strict mode only uses the provider-card default.' };
414
+ });
415
+ }
416
+ let ordered = orderCandidates(candidates, rawDefault);
417
+ const primary = primaryFromCandidates(ordered, rawDefault);
418
+ if (settings.mode === 'strict' && !rawDefault && primary) {
419
+ ordered = ordered.map((candidate) => {
420
+ if (candidate.id === primary.id) return candidate;
421
+ return { ...candidate, included: false, excluded_reason: 'Strict mode will use the initialized provider-card default.' };
422
+ });
423
+ }
424
+ let chain = ordered.filter((candidate) => candidate.included);
425
+ if (primary) {
426
+ chain = [primary].concat(chain.filter((candidate) => candidate.id !== primary.id));
427
+ }
428
+ chain = chain.map((candidate, index) => ({
429
+ ...candidate,
430
+ order: index + 1,
431
+ role: index === 0 ? 'primary' : 'fallback',
432
+ }));
433
+ const defaultCandidate = rawDefault
434
+ ? ordered.find((candidate) => candidate.provider_type === rawDefault || candidate.runtime_type === rawDefault) || null
435
+ : primary;
436
+ const policy = {
437
+ generated_at: nowIso(),
438
+ settings,
439
+ trigger_classes: Array.from(fallbackInternals.FALLTHROUGH_CLASSES || []),
440
+ default: {
441
+ configured_provider_type: rawDefault || null,
442
+ source: rawDefaultSource,
443
+ status: rawDefault
444
+ ? (defaultCandidate?.included ? 'usable' : 'unusable')
445
+ : (primary ? 'auto-selected' : 'missing'),
446
+ provider_type: (defaultCandidate || primary)?.provider_type || null,
447
+ runtime_type: (defaultCandidate || primary)?.runtime_type || null,
448
+ provider_name: (defaultCandidate || primary)?.provider_name || null,
449
+ model: (defaultCandidate || primary)?.model || null,
450
+ reason: defaultCandidate?.included
451
+ ? 'Provider-card default is usable.'
452
+ : (defaultCandidate?.excluded_reason || (primary ? 'No explicit default; using best available provider.' : 'No usable provider found.')),
453
+ candidate: defaultCandidate || primary || null,
454
+ },
455
+ chain,
456
+ candidates: ordered,
457
+ excluded: ordered.filter((candidate) => !candidate.included),
458
+ recommendations: [],
459
+ actions: [],
460
+ };
461
+ if (!rawDefault && primary && primary.source !== 'cli-tail') {
462
+ policy.recommendations.push({
463
+ type: 'select_default_provider',
464
+ provider_type: primary.provider_type,
465
+ model: primary.model,
466
+ message: 'No provider-card default is set. CTM can initialize it from the best available provider.',
467
+ });
468
+ } else if (rawDefault && defaultCandidate && !defaultCandidate.included && primary && primary.provider_type !== rawDefault) {
469
+ policy.recommendations.push({
470
+ type: 'repair_unusable_default',
471
+ provider_type: primary.provider_type,
472
+ model: primary.model,
473
+ message: 'The current default is not usable. Refresh can move the default to the best available provider.',
474
+ });
475
+ }
476
+
477
+ if (options.repairDefault) {
478
+ policy.actions = applyDefaultRepair(brain, policy);
479
+ const refreshStamp = nowIso();
480
+ setKv(brain, POLICY_KEYS.lastRefreshAt, refreshStamp);
481
+ if (typeof brain.updateModelRoutingPolicy === 'function') {
482
+ try {
483
+ brain.updateModelRoutingPolicy('default', { last_generated_at: refreshStamp });
484
+ } catch {}
485
+ }
486
+ if (policy.actions.some((action) => !/failed$/.test(action.type))) {
487
+ const recomputed = buildRoutingPolicy({ ...options, repairDefault: false });
488
+ recomputed.actions = policy.actions;
489
+ return recomputed;
490
+ }
491
+ policy.settings = getRoutingPolicySettings(brain);
492
+ }
493
+ return policy;
494
+ }
495
+
496
+ function _routeKey(route = {}) {
497
+ return [
498
+ route.provider_id || '',
499
+ route.runtime_type || route.provider_type || '',
500
+ route.provider_type || '',
501
+ route.model || route.model_id || '',
502
+ ].join('|');
503
+ }
504
+
505
+ function _mergeEffectiveRoutes(generatedRoutes = [], userRoutes = []) {
506
+ const generated = Array.isArray(generatedRoutes) ? generatedRoutes : [];
507
+ const manualAll = (Array.isArray(userRoutes) ? userRoutes : []).filter(Boolean);
508
+ const out = [];
509
+ const seen = new Set();
510
+ const primary = generated.find((route) => route && route.role === 'primary') || generated[0] || null;
511
+ const primaryKey = primary ? _routeKey(primary) : '';
512
+ const disabledKeys = new Set(manualAll
513
+ .filter((route) => route.enabled === false && _routeKey(route) !== primaryKey)
514
+ .map(_routeKey)
515
+ .filter(Boolean));
516
+ const manual = manualAll.filter((route) => route.enabled !== false);
517
+ function add(route, role) {
518
+ if (!route) return;
519
+ const key = _routeKey(route);
520
+ if (seen.has(key) || (disabledKeys.has(key) && key !== primaryKey)) return;
521
+ seen.add(key);
522
+ out.push({
523
+ ...route,
524
+ role: role || route.role || (out.length === 0 ? 'primary' : 'fallback'),
525
+ order: out.length + 1,
526
+ });
527
+ }
528
+ if (primary) add(primary, 'primary');
529
+ for (const route of manual) add(route, out.length === 0 ? 'primary' : 'fallback');
530
+ for (const route of generated) add(route, out.length === 0 ? 'primary' : 'fallback');
531
+ return out.map((route, index) => ({
532
+ ...route,
533
+ order: index + 1,
534
+ role: index === 0 ? 'primary' : 'fallback',
535
+ }));
536
+ }
537
+
538
+ function _persistedSettings(policyRow = {}) {
539
+ const mode = VALID_MODES.has(policyRow.mode) ? policyRow.mode : 'smart';
540
+ return {
541
+ mode,
542
+ fallback_enabled: mode !== 'strict',
543
+ cli_tail: policyRow.cli_tail !== false,
544
+ finalization_fallback: policyRow.finalization_fallback !== false,
545
+ last_refreshed_at: policyRow.last_generated_at || null,
546
+ last_effective_at: policyRow.last_effective_at || null,
547
+ last_error: policyRow.last_error || null,
548
+ stale_reason: policyRow.stale_reason || null,
549
+ };
550
+ }
551
+
552
+ function _defaultFromPersisted(brain, chain = [], settings = {}) {
553
+ const primary = chain[0] || null;
554
+ const configuredType = normalizeProviderType(getKv(brain, 'walle_provider') || process.env.WALLE_PROVIDER || '');
555
+ if (primary) {
556
+ const matchesDefault = configuredType && (
557
+ primary.provider_type === configuredType || primary.runtime_type === configuredType
558
+ );
559
+ return {
560
+ configured_provider_type: configuredType || null,
561
+ source: getKv(brain, 'walle_provider') ? 'provider-card' : (process.env.WALLE_PROVIDER ? 'environment' : 'auto'),
562
+ status: configuredType ? (matchesDefault ? 'usable' : 'fallback-ready') : 'auto-selected',
563
+ provider_type: primary.provider_type || null,
564
+ runtime_type: primary.runtime_type || primary.provider_type || null,
565
+ provider_name: primary.provider_name || primary.label || providerDisplayName(primary.provider_type),
566
+ model: primary.model || primary.model_id || null,
567
+ reason: matchesDefault
568
+ ? 'Provider-card default is usable.'
569
+ : (settings.mode === 'strict'
570
+ ? 'Strict mode uses the persisted default route.'
571
+ : 'Persisted effective route is ready.'),
572
+ candidate: primary,
573
+ };
574
+ }
575
+ return {
576
+ configured_provider_type: configuredType || null,
577
+ source: configuredType ? 'provider-card' : 'auto',
578
+ status: 'missing',
579
+ provider_type: null,
580
+ runtime_type: null,
581
+ provider_name: null,
582
+ model: null,
583
+ reason: settings.last_error || 'No usable provider route has been materialized yet.',
584
+ candidate: null,
585
+ };
586
+ }
587
+
588
+ function _publicPolicyFromPersisted(brain, snapshot = {}, extras = {}) {
589
+ const policyRow = snapshot.policy || {};
590
+ const routes = snapshot.routes || {};
591
+ const settings = _persistedSettings(policyRow);
592
+ const effective = Array.isArray(routes.effective) ? routes.effective : [];
593
+ const generated = Array.isArray(routes.generated) ? routes.generated : [];
594
+ const user = Array.isArray(routes.user) ? routes.user : [];
595
+ const chainSource = effective.length ? 'effective' : (generated.length ? 'generated' : 'empty');
596
+ const chain = (effective.length ? effective : generated).map((route, index) => ({
597
+ ...route,
598
+ route_key: _routeKey(route),
599
+ order: index + 1,
600
+ role: index === 0 ? 'primary' : 'fallback',
601
+ }));
602
+ const publicUserRoutes = user.map((route, index) => ({
603
+ ...route,
604
+ route_key: _routeKey(route),
605
+ order: index + 1,
606
+ role: route.role || 'fallback',
607
+ }));
608
+ const publicGeneratedRoutes = generated.map((route, index) => ({
609
+ ...route,
610
+ route_key: _routeKey(route),
611
+ order: index + 1,
612
+ role: index === 0 ? 'primary' : 'fallback',
613
+ }));
614
+ return {
615
+ generated_at: policyRow.last_generated_at || null,
616
+ persisted: true,
617
+ policy_id: policyRow.id || 'default',
618
+ chain_source: chainSource,
619
+ settings,
620
+ trigger_classes: Array.from(fallbackInternals.FALLTHROUGH_CLASSES || []),
621
+ default: _defaultFromPersisted(brain, chain, settings),
622
+ chain,
623
+ candidates: chain,
624
+ excluded: [],
625
+ recommendations: extras.recommendations || [],
626
+ actions: extras.actions || [],
627
+ sources: {
628
+ user_count: user.length,
629
+ generated_count: generated.length,
630
+ effective_count: effective.length,
631
+ },
632
+ user_routes: publicUserRoutes,
633
+ generated_routes: publicGeneratedRoutes,
634
+ };
635
+ }
636
+
637
+ function getPersistedRoutingPolicy(options = {}) {
638
+ const brain = options.brain;
639
+ if (!brain || typeof brain.getModelRoutingPolicy !== 'function') {
640
+ return buildRoutingPolicy(options);
641
+ }
642
+ const snapshot = brain.getModelRoutingPolicy('default');
643
+ const hasEffective = Array.isArray(snapshot.routes?.effective) && snapshot.routes.effective.length > 0;
644
+ const hasGenerated = Array.isArray(snapshot.routes?.generated) && snapshot.routes.generated.length > 0;
645
+ if (!hasEffective && !hasGenerated && options.generateIfMissing !== false) {
646
+ return materializeRoutingPolicy(options);
647
+ }
648
+ return _publicPolicyFromPersisted(brain, snapshot);
649
+ }
650
+
651
+ function materializeRoutingPolicy(options = {}) {
652
+ const brain = options.brain;
653
+ const computed = buildRoutingPolicy(options);
654
+ if (!brain || typeof brain.replaceModelRoutingPolicyRoutes !== 'function') return computed;
655
+ try {
656
+ const userRoutes = typeof brain.listModelRoutingPolicyRoutes === 'function'
657
+ ? brain.listModelRoutingPolicyRoutes('default', 'user')
658
+ : [];
659
+ const generatedRoutes = Array.isArray(computed.chain) ? computed.chain : [];
660
+ const effectiveRoutes = _mergeEffectiveRoutes(generatedRoutes, userRoutes);
661
+ brain.replaceModelRoutingPolicyRoutes('default', 'generated', generatedRoutes);
662
+ brain.replaceModelRoutingPolicyRoutes('default', 'effective', effectiveRoutes);
663
+ const stamp = computed.generated_at || nowIso();
664
+ if (typeof brain.updateModelRoutingPolicy === 'function') {
665
+ brain.updateModelRoutingPolicy('default', {
666
+ mode: computed.settings?.mode || 'smart',
667
+ cli_tail: computed.settings?.cli_tail !== false,
668
+ finalization_fallback: computed.settings?.finalization_fallback !== false,
669
+ last_generated_at: stamp,
670
+ last_effective_at: nowIso(),
671
+ last_error: null,
672
+ stale_reason: null,
673
+ });
674
+ } else {
675
+ setKv(brain, POLICY_KEYS.lastRefreshAt, stamp);
676
+ }
677
+ const persisted = getPersistedRoutingPolicy({ ...options, generateIfMissing: false });
678
+ persisted.actions = computed.actions || [];
679
+ persisted.recommendations = computed.recommendations || [];
680
+ persisted.excluded = computed.excluded || [];
681
+ persisted.candidates = computed.candidates || persisted.chain;
682
+ return persisted;
683
+ } catch (err) {
684
+ try {
685
+ if (typeof brain.updateModelRoutingPolicy === 'function') {
686
+ brain.updateModelRoutingPolicy('default', {
687
+ last_error: err.message || String(err),
688
+ stale_reason: 'materialization_failed',
689
+ });
690
+ }
691
+ } catch {}
692
+ throw err;
693
+ }
694
+ }
695
+
696
+ function _indexedRoutes(routes = []) {
697
+ const map = new Map();
698
+ for (const route of Array.isArray(routes) ? routes : []) {
699
+ if (!route) continue;
700
+ const key = route.route_key || _routeKey(route);
701
+ if (key && !map.has(key)) map.set(key, route);
702
+ }
703
+ return map;
704
+ }
705
+
706
+ function _routeInputKey(route = {}) {
707
+ return route.route_key || route.key || _routeKey(route);
708
+ }
709
+
710
+ function setUserRoutingPolicyRoutes(options = {}) {
711
+ const brain = options.brain;
712
+ if (!brain || typeof brain.replaceModelRoutingPolicyRoutes !== 'function') {
713
+ throw new Error('Wall-E brain does not support routing policy route edits');
714
+ }
715
+ const requestedRoutes = Array.isArray(options.routes) ? options.routes : [];
716
+ let snapshot = typeof brain.getModelRoutingPolicy === 'function'
717
+ ? brain.getModelRoutingPolicy('default')
718
+ : { routes: {} };
719
+ if (!Array.isArray(snapshot.routes?.generated) || snapshot.routes.generated.length === 0) {
720
+ materializeRoutingPolicy({ ...options, repairDefault: false });
721
+ snapshot = brain.getModelRoutingPolicy('default');
722
+ }
723
+ const generatedRoutes = Array.isArray(snapshot.routes?.generated) ? snapshot.routes.generated : [];
724
+ const effectiveRoutes = Array.isArray(snapshot.routes?.effective) && snapshot.routes.effective.length
725
+ ? snapshot.routes.effective
726
+ : generatedRoutes;
727
+ const userRoutes = Array.isArray(snapshot.routes?.user) ? snapshot.routes.user : [];
728
+ const candidatesByKey = _indexedRoutes([].concat(effectiveRoutes, generatedRoutes, userRoutes));
729
+ const primary = generatedRoutes.find((route) => route && route.role === 'primary') || generatedRoutes[0] || effectiveRoutes[0] || null;
730
+ const primaryKey = primary ? _routeKey(primary) : '';
731
+ const manualRoutes = [];
732
+ const seen = new Set();
733
+
734
+ for (const requested of requestedRoutes) {
735
+ if (!requested) continue;
736
+ const key = _routeInputKey(requested);
737
+ if (!key || key === primaryKey || seen.has(key)) continue;
738
+ const base = candidatesByKey.get(key);
739
+ if (!base) throw new Error(`Unknown fallback route: ${key}`);
740
+ const providerType = base.provider_type || base.providerType || base.type;
741
+ if (!providerType) continue;
742
+ const enabled = requested.enabled !== false && requested.enabled !== 0;
743
+ manualRoutes.push({
744
+ ...base,
745
+ route_key: key,
746
+ order: manualRoutes.length + 1,
747
+ role: 'fallback',
748
+ enabled,
749
+ reason: enabled
750
+ ? (requested.reason || base.reason || '')
751
+ : (requested.reason || 'Disabled by user'),
752
+ metadata: {
753
+ ...(base.metadata && typeof base.metadata === 'object' ? base.metadata : {}),
754
+ manual_route: true,
755
+ },
756
+ });
757
+ seen.add(key);
758
+ }
759
+
760
+ brain.replaceModelRoutingPolicyRoutes('default', 'user', manualRoutes);
761
+ const storedUserRoutes = typeof brain.listModelRoutingPolicyRoutes === 'function'
762
+ ? brain.listModelRoutingPolicyRoutes('default', 'user')
763
+ : manualRoutes;
764
+ const effectiveRoutesNext = _mergeEffectiveRoutes(generatedRoutes, storedUserRoutes);
765
+ brain.replaceModelRoutingPolicyRoutes('default', 'effective', effectiveRoutesNext);
766
+ if (typeof brain.updateModelRoutingPolicy === 'function') {
767
+ brain.updateModelRoutingPolicy('default', {
768
+ last_effective_at: nowIso(),
769
+ last_error: null,
770
+ stale_reason: null,
771
+ });
772
+ }
773
+ return getPersistedRoutingPolicy({ ...options, generateIfMissing: false });
774
+ }
775
+
776
+ function safeDefaultHealth() {
777
+ try { return require('./health').default; } catch { return null; }
778
+ }
779
+
780
+ module.exports = {
781
+ buildRoutingPolicy,
782
+ getPersistedRoutingPolicy,
783
+ materializeRoutingPolicy,
784
+ getRoutingPolicySettings,
785
+ setRoutingPolicySettings,
786
+ setUserRoutingPolicyRoutes,
787
+ _internals: {
788
+ POLICY_KEYS,
789
+ normalizeProviderType,
790
+ providerRuntimeAccess,
791
+ providerRuntimeType,
792
+ defaultModelForProvider,
793
+ _mergeEffectiveRoutes,
794
+ _routeKey,
795
+ },
796
+ };