@bitkyc08/opencodex 2.10.2 → 2.11.0

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 (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -1,67 +1,365 @@
1
1
  /**
2
- * Observational ledger for pre-connection upstream reachability failures,
3
- * keyed by (provider, host). Records ONLY — no circuit breaker, no admission
4
- * change (issue #914 scope). Rotation decisions stay with account health;
5
- * this ledger exists so a host-wide outage is visible as host-wide.
2
+ * Provider-origin reachability health and the optional logical-request circuit.
6
3
  *
7
- * Retention: bounded at 128 entries; on overflow the stalest entries by
8
- * last-touch are pruned before insert, and failure timestamps older than the
9
- * window are reconciled away on the next record repeated provider/base-URL
10
- * churn cannot grow the map for the process lifetime.
4
+ * Issue #914 owns the observational ledger. The circuit is deliberately opt-in:
5
+ * a threshold of 0 preserves the ledger-only behavior. Only proven
6
+ * `connect_neutral` failures may settle a lease as host failure; timeout, reset,
7
+ * HTTP, redirect, authentication, and local failures remain outside this module.
11
8
  */
12
9
 
13
10
  export const UPSTREAM_HOST_HEALTH_MAX_ENTRIES = 128;
14
11
  export const UPSTREAM_HOST_FAILURE_WINDOW_MS = 10 * 60_000;
12
+ export const UPSTREAM_HOST_CIRCUIT_COOLDOWN_MS = 30_000;
13
+ export const UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD = 20;
15
14
 
16
15
  export type UpstreamHostHealthEntry = {
17
16
  consecutiveFailures: number;
18
17
  lastFailureAt: number;
19
18
  lastFailureCode?: string;
19
+ cooldownUntil?: number;
20
20
  /** Recency marker for stalest-first pruning (not health semantics). */
21
21
  lastTouch: number;
22
22
  };
23
23
 
24
- const hostHealth = new Map<string, UpstreamHostHealthEntry>();
24
+ /** Opaque ownership token for one admitted logical request. */
25
+ export type UpstreamHostAdmissionLease = Readonly<{
26
+ key: string;
27
+ leaseId: symbol;
28
+ generation: number;
29
+ halfOpen: boolean;
30
+ }>;
31
+
32
+ export type UpstreamHostAdmission =
33
+ | { kind: "admitted"; lease: UpstreamHostAdmissionLease | null }
34
+ | { kind: "blocked"; retryAfterSeconds: number };
35
+
36
+ type InternalUpstreamHostHealth = UpstreamHostHealthEntry & {
37
+ generation: number;
38
+ activeLeaseIds: Set<symbol>;
39
+ halfOpenLeaseId?: symbol;
40
+ /**
41
+ * Leases admitted in the generation that immediately preceded the current
42
+ * cooldown. They are stale for failure settlement, but a real HTTP response
43
+ * from one still proves the origin is reachable and may close this cooldown.
44
+ */
45
+ cooldownSuccessLeaseIds?: Set<symbol>;
46
+ /** True only when the entry is owned by opt-in circuit admissions. */
47
+ circuitManaged: boolean;
48
+ };
49
+
50
+ const hostHealth = new Map<string, InternalUpstreamHostHealth>();
51
+ let nextGenerationValue = 0;
25
52
 
26
53
  export function upstreamHostHealthKey(provider: string, host: string): string {
27
54
  return `${provider}|${host.toLowerCase()}`;
28
55
  }
29
56
 
30
- function pruneForInsert(): void {
31
- if (hostHealth.size < UPSTREAM_HOST_HEALTH_MAX_ENTRIES) return;
32
- const entries = [...hostHealth.entries()].sort((a, b) => a[1].lastTouch - b[1].lastTouch);
33
- for (const [key] of entries) {
34
- if (hostHealth.size < UPSTREAM_HOST_HEALTH_MAX_ENTRIES) return;
57
+ export function normalizeUpstreamHostCircuitThreshold(value: unknown): number {
58
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return 0;
59
+ return Math.min(value, UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD);
60
+ }
61
+
62
+ function nextGeneration(): number {
63
+ nextGenerationValue = nextGenerationValue >= Number.MAX_SAFE_INTEGER ? 1 : nextGenerationValue + 1;
64
+ return nextGenerationValue;
65
+ }
66
+
67
+ function snapshot(entry: InternalUpstreamHostHealth): UpstreamHostHealthEntry {
68
+ return {
69
+ consecutiveFailures: entry.consecutiveFailures,
70
+ lastFailureAt: entry.lastFailureAt,
71
+ lastTouch: entry.lastTouch,
72
+ ...(entry.lastFailureCode !== undefined ? { lastFailureCode: entry.lastFailureCode } : {}),
73
+ ...(entry.cooldownUntil !== undefined ? { cooldownUntil: entry.cooldownUntil } : {}),
74
+ };
75
+ }
76
+
77
+ function newEntry(now: number, circuitManaged: boolean): InternalUpstreamHostHealth {
78
+ return {
79
+ consecutiveFailures: 0,
80
+ lastFailureAt: 0,
81
+ lastTouch: now,
82
+ generation: nextGeneration(),
83
+ activeLeaseIds: new Set(),
84
+ circuitManaged,
85
+ };
86
+ }
87
+
88
+ function advanceGeneration(entry: InternalUpstreamHostHealth): void {
89
+ entry.generation = nextGeneration();
90
+ entry.activeLeaseIds.clear();
91
+ delete entry.halfOpenLeaseId;
92
+ delete entry.cooldownSuccessLeaseIds;
93
+ }
94
+
95
+ function removeExpiredUnleased(now: number): void {
96
+ for (const [key, entry] of hostHealth) {
97
+ if (entry.activeLeaseIds.size > 0) continue;
98
+ if (
99
+ entry.consecutiveFailures === 0
100
+ || (entry.cooldownUntil === undefined
101
+ && now - entry.lastFailureAt > UPSTREAM_HOST_FAILURE_WINDOW_MS)
102
+ ) {
103
+ hostHealth.delete(key);
104
+ }
105
+ }
106
+ }
107
+
108
+ function oldestUnleasedKey(now: number): string | undefined {
109
+ let preferred: [string, number] | undefined;
110
+ let cooling: [string, number] | undefined;
111
+ for (const [key, entry] of hostHealth) {
112
+ if (entry.activeLeaseIds.size > 0) continue;
113
+ const candidate: [string, number] = [key, entry.lastTouch];
114
+ if (entry.cooldownUntil !== undefined && entry.cooldownUntil > now) {
115
+ if (!cooling || candidate[1] < cooling[1]) cooling = candidate;
116
+ } else if (!preferred || candidate[1] < preferred[1]) {
117
+ preferred = candidate;
118
+ }
119
+ }
120
+ return preferred?.[0] ?? cooling?.[0];
121
+ }
122
+
123
+ function pruneTo(limit: number, now: number): void {
124
+ removeExpiredUnleased(now);
125
+ while (hostHealth.size > limit) {
126
+ const key = oldestUnleasedKey(now);
127
+ if (!key) return; // Active leases may temporarily exceed the retention cap.
35
128
  hostHealth.delete(key);
36
129
  }
37
130
  }
38
131
 
132
+ function makeRoom(now: number): void {
133
+ pruneTo(UPSTREAM_HOST_HEALTH_MAX_ENTRIES - 1, now);
134
+ }
135
+
136
+ function issueLease(
137
+ key: string,
138
+ entry: InternalUpstreamHostHealth,
139
+ halfOpen: boolean,
140
+ now: number,
141
+ ): UpstreamHostAdmissionLease {
142
+ const leaseId = Symbol(halfOpen ? "upstream-host-half-open" : "upstream-host-admission");
143
+ entry.activeLeaseIds.add(leaseId);
144
+ entry.lastTouch = now;
145
+ if (halfOpen) entry.halfOpenLeaseId = leaseId;
146
+ return { key, leaseId, generation: entry.generation, halfOpen };
147
+ }
148
+
149
+ function matchingEntry(lease: UpstreamHostAdmissionLease): InternalUpstreamHostHealth | null {
150
+ const entry = hostHealth.get(lease.key);
151
+ if (!entry || entry.generation !== lease.generation || !entry.activeLeaseIds.has(lease.leaseId)) {
152
+ return null;
153
+ }
154
+ if (lease.halfOpen && entry.halfOpenLeaseId !== lease.leaseId) return null;
155
+ return entry;
156
+ }
157
+
158
+ function settleLease(entry: InternalUpstreamHostHealth, lease: UpstreamHostAdmissionLease): void {
159
+ entry.activeLeaseIds.delete(lease.leaseId);
160
+ if (entry.halfOpenLeaseId === lease.leaseId) delete entry.halfOpenLeaseId;
161
+ }
162
+
163
+ /**
164
+ * Admit one logical request. A disabled threshold returns a null lease and has
165
+ * byte-for-byte compatible call-site behavior with the observational ledger.
166
+ */
167
+ export function acquireUpstreamHostAdmission(
168
+ key: string,
169
+ thresholdValue: unknown,
170
+ now = Date.now(),
171
+ ): UpstreamHostAdmission {
172
+ const threshold = normalizeUpstreamHostCircuitThreshold(thresholdValue);
173
+ if (threshold === 0) return { kind: "admitted", lease: null };
174
+
175
+ pruneTo(UPSTREAM_HOST_HEALTH_MAX_ENTRIES, now);
176
+ let entry = hostHealth.get(key);
177
+ if (
178
+ entry?.activeLeaseIds.size === 0
179
+ && entry.cooldownUntil === undefined
180
+ && entry.consecutiveFailures > 0
181
+ && now - entry.lastFailureAt > UPSTREAM_HOST_FAILURE_WINDOW_MS
182
+ ) {
183
+ hostHealth.delete(key);
184
+ entry = undefined;
185
+ }
186
+ if (!entry) {
187
+ makeRoom(now);
188
+ entry = newEntry(now, true);
189
+ hostHealth.set(key, entry);
190
+ } else if (!entry.circuitManaged) {
191
+ // Observational history predating opt-in admission must not count toward
192
+ // opening the circuit. Start a fresh fenced generation.
193
+ entry.consecutiveFailures = 0;
194
+ entry.lastFailureAt = 0;
195
+ entry.circuitManaged = true;
196
+ delete entry.lastFailureCode;
197
+ delete entry.cooldownUntil;
198
+ advanceGeneration(entry);
199
+ }
200
+
201
+ if (entry.cooldownUntil !== undefined) {
202
+ if (entry.cooldownUntil > now) {
203
+ return {
204
+ kind: "blocked",
205
+ retryAfterSeconds: Math.max(1, Math.ceil((entry.cooldownUntil - now) / 1_000)),
206
+ };
207
+ }
208
+ if (entry.halfOpenLeaseId !== undefined) {
209
+ return { kind: "blocked", retryAfterSeconds: 1 };
210
+ }
211
+ advanceGeneration(entry);
212
+ return { kind: "admitted", lease: issueLease(key, entry, true, now) };
213
+ }
214
+
215
+ return { kind: "admitted", lease: issueLease(key, entry, false, now) };
216
+ }
217
+
218
+ /** Release an admitted request without recording transport evidence. */
219
+ export function releaseUpstreamHostAdmission(
220
+ lease: UpstreamHostAdmissionLease | null | undefined,
221
+ now = Date.now(),
222
+ ): boolean {
223
+ if (!lease) return false;
224
+ const entry = matchingEntry(lease);
225
+ if (!entry) return false;
226
+ settleLease(entry, lease);
227
+ entry.lastTouch = now;
228
+ if (entry.activeLeaseIds.size === 0 && entry.consecutiveFailures === 0) {
229
+ hostHealth.delete(lease.key);
230
+ }
231
+ pruneTo(UPSTREAM_HOST_HEALTH_MAX_ENTRIES, now);
232
+ return true;
233
+ }
234
+
235
+ /**
236
+ * Downgrade circuit-owned state when the operator disables the circuit.
237
+ * Failure history remains observational, but cooldown and all in-flight lease
238
+ * authority are revoked so disabled-mode traffic can update the ledger normally.
239
+ */
240
+ export function disableUpstreamHostCircuitForKey(key: string, now = Date.now()): boolean {
241
+ const entry = hostHealth.get(key);
242
+ if (!entry?.circuitManaged) return false;
243
+ entry.circuitManaged = false;
244
+ delete entry.cooldownUntil;
245
+ advanceGeneration(entry);
246
+ entry.lastTouch = now;
247
+ if (entry.consecutiveFailures === 0) hostHealth.delete(key);
248
+ else pruneTo(UPSTREAM_HOST_HEALTH_MAX_ENTRIES, now);
249
+ return true;
250
+ }
251
+
252
+ /** Record one terminal logical `connect_neutral` failure. */
39
253
  export function recordUpstreamHostFailure(
40
254
  key: string,
41
- opts: { code?: string; now?: number } = {},
255
+ opts: {
256
+ code?: string;
257
+ now?: number;
258
+ threshold?: unknown;
259
+ lease?: UpstreamHostAdmissionLease | null;
260
+ } = {},
42
261
  ): void {
43
262
  const now = opts.now ?? Date.now();
44
- const prior = hostHealth.get(key);
45
- // Prune only for a genuinely new key: updating an existing entry must never
46
- // evict an unrelated one.
47
- if (prior === undefined) pruneForInsert();
48
- const stale = prior !== undefined && now - prior.lastFailureAt > UPSTREAM_HOST_FAILURE_WINDOW_MS;
49
- const code = typeof opts.code === "string" && opts.code !== "" ? opts.code : prior?.lastFailureCode;
50
- hostHealth.set(key, {
51
- consecutiveFailures: stale || prior === undefined ? 1 : prior.consecutiveFailures + 1,
52
- lastFailureAt: now,
53
- lastTouch: now,
54
- ...(code !== undefined ? { lastFailureCode: code } : {}),
55
- });
263
+ const hasCircuitSettlement = Object.hasOwn(opts, "lease");
264
+ // An integrated logical request may already have settled its lease on an
265
+ // earlier physical response. Later retry completions carry explicit null and
266
+ // must not mutate a newer generation opened by another request.
267
+ if (hasCircuitSettlement && !opts.lease) return;
268
+ // Legacy callers without an admission lease continue to populate only the
269
+ // observational ledger. Circuit mutation is fenced to explicitly admitted
270
+ // logical requests so an unwired side path cannot open it accidentally.
271
+ const threshold = opts.lease
272
+ ? normalizeUpstreamHostCircuitThreshold(opts.threshold)
273
+ : 0;
274
+ let entry: InternalUpstreamHostHealth | undefined;
275
+ if (opts.lease) {
276
+ entry = matchingEntry(opts.lease) ?? undefined;
277
+ if (!entry || opts.lease.key !== key) return; // stale completion cannot mutate a newer generation
278
+ settleLease(entry, opts.lease);
279
+ } else {
280
+ entry = hostHealth.get(key);
281
+ if (entry?.circuitManaged) {
282
+ const stale = entry.activeLeaseIds.size === 0
283
+ && entry.cooldownUntil === undefined
284
+ && now - entry.lastFailureAt > UPSTREAM_HOST_FAILURE_WINDOW_MS;
285
+ if (!stale) return;
286
+ hostHealth.delete(key);
287
+ entry = undefined;
288
+ }
289
+ }
290
+ if (!entry) {
291
+ makeRoom(now);
292
+ entry = newEntry(now, opts.lease !== undefined);
293
+ hostHealth.set(key, entry);
294
+ }
295
+
296
+ const reopens = opts.lease?.halfOpen === true || entry.cooldownUntil !== undefined;
297
+ const stale = entry.consecutiveFailures === 0
298
+ || (!reopens && now - entry.lastFailureAt > UPSTREAM_HOST_FAILURE_WINDOW_MS);
299
+ entry.consecutiveFailures = reopens && threshold > 0
300
+ ? Math.max(threshold, entry.consecutiveFailures + 1)
301
+ : stale ? 1 : entry.consecutiveFailures + 1;
302
+ entry.lastFailureAt = now;
303
+ entry.lastTouch = now;
304
+ const code = typeof opts.code === "string" && opts.code !== ""
305
+ ? opts.code
306
+ : entry.lastFailureCode;
307
+ if (code !== undefined) entry.lastFailureCode = code;
308
+
309
+ if (threshold > 0 && (reopens || entry.consecutiveFailures >= threshold)) {
310
+ entry.cooldownUntil = now + UPSTREAM_HOST_CIRCUIT_COOLDOWN_MS;
311
+ // The failing lease was settled above. Preserve only its still-in-flight
312
+ // same-generation peers as one-shot reachability proofs. advanceGeneration
313
+ // invalidates them for every other mutation and for any later half-open generation.
314
+ const concurrentSuccessLeaseIds = new Set(entry.activeLeaseIds);
315
+ advanceGeneration(entry);
316
+ if (concurrentSuccessLeaseIds.size > 0) {
317
+ entry.cooldownSuccessLeaseIds = concurrentSuccessLeaseIds;
318
+ }
319
+ } else {
320
+ delete entry.cooldownUntil;
321
+ }
322
+ pruneTo(UPSTREAM_HOST_HEALTH_MAX_ENTRIES, now);
56
323
  }
57
324
 
58
- /** Any real HTTP response from the host clears its reachability streak. */
59
- export function resetUpstreamHostHealth(key: string): void {
60
- hostHealth.delete(key);
325
+ /** Any real HTTP response from the admitted logical request proves reachability. */
326
+ export function resetUpstreamHostHealth(
327
+ key: string,
328
+ lease?: UpstreamHostAdmissionLease | null,
329
+ now = Date.now(),
330
+ ): boolean {
331
+ if (lease === null) return false;
332
+ if (lease === undefined) {
333
+ const entry = hostHealth.get(key);
334
+ if (entry?.circuitManaged) return false;
335
+ return hostHealth.delete(key);
336
+ }
337
+ if (lease.key !== key) return false;
338
+ let entry = matchingEntry(lease);
339
+ if (!entry) {
340
+ const coolingEntry = hostHealth.get(key);
341
+ if (
342
+ coolingEntry?.cooldownUntil === undefined
343
+ || !coolingEntry.cooldownSuccessLeaseIds?.delete(lease.leaseId)
344
+ ) return false;
345
+ entry = coolingEntry;
346
+ } else {
347
+ settleLease(entry, lease);
348
+ }
349
+ entry.consecutiveFailures = 0;
350
+ entry.lastFailureAt = 0;
351
+ entry.lastTouch = now;
352
+ delete entry.lastFailureCode;
353
+ delete entry.cooldownUntil;
354
+ if (entry.activeLeaseIds.size === 0) hostHealth.delete(key);
355
+ pruneTo(UPSTREAM_HOST_HEALTH_MAX_ENTRIES, now);
356
+ return true;
61
357
  }
62
358
 
63
359
  export function getUpstreamHostHealth(key: string): UpstreamHostHealthEntry | null {
64
- return hostHealth.get(key) ?? null;
360
+ const entry = hostHealth.get(key);
361
+ if (!entry || entry.consecutiveFailures === 0) return null;
362
+ return snapshot(entry);
65
363
  }
66
364
 
67
365
  /** Test hook: clear the whole ledger. */
@@ -40,6 +40,8 @@ export function concreteComboRequestBody(
40
40
  && !Object.prototype.hasOwnProperty.call(reasoning, "effort")
41
41
  );
42
42
  if (!needsDefault) return clone;
43
+ // Picker availability treats an unknown ladder as a wildcard, but runtime
44
+ // injection stays fail-closed until this concrete target advertises support.
43
45
  if (!targetReasoningEfforts?.includes(defaultEffort)) {
44
46
  const key = `${target.provider}/${target.model}:${defaultEffort}`;
45
47
  if (!warnedUnsupportedDefaults.has(key)) {