@bitkyc08/opencodex 2.8.0 → 2.9.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 (175) hide show
  1. package/README.md +24 -0
  2. package/bin/ocx.mjs +32 -4
  3. package/gui/dist/assets/index-CHwf3tTD.css +1 -0
  4. package/gui/dist/assets/index-u5eFOv2y.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-image-normalize.ts +114 -20
  8. package/src/adapters/anthropic.ts +126 -10
  9. package/src/adapters/azure.ts +3 -3
  10. package/src/adapters/base.ts +7 -3
  11. package/src/adapters/cursor/discovery.ts +14 -4
  12. package/src/adapters/cursor/effort-map.ts +18 -6
  13. package/src/adapters/cursor/framing.ts +102 -27
  14. package/src/adapters/cursor/kv-store.ts +30 -3
  15. package/src/adapters/cursor/live-models.ts +22 -2
  16. package/src/adapters/cursor/live-transport.ts +245 -49
  17. package/src/adapters/cursor/mcp-manager.ts +105 -8
  18. package/src/adapters/cursor/native-exec-mcp.ts +5 -3
  19. package/src/adapters/cursor/native-exec-shell.ts +296 -14
  20. package/src/adapters/cursor/native-exec.ts +381 -33
  21. package/src/adapters/cursor/protobuf-events.ts +28 -1
  22. package/src/adapters/cursor/protobuf-request.ts +71 -39
  23. package/src/adapters/cursor/request-builder.ts +2 -2
  24. package/src/adapters/cursor/transport.ts +2 -0
  25. package/src/adapters/cursor.ts +13 -2
  26. package/src/adapters/google-antigravity-replay.ts +184 -17
  27. package/src/adapters/google.ts +58 -8
  28. package/src/adapters/kiro-thinking.ts +23 -9
  29. package/src/adapters/kiro-tools.ts +49 -18
  30. package/src/adapters/kiro.ts +377 -133
  31. package/src/adapters/mimo-free.ts +36 -4
  32. package/src/adapters/openai-chat.ts +143 -17
  33. package/src/adapters/openai-responses.ts +130 -14
  34. package/src/adapters/run-turn-queue.ts +7 -1
  35. package/src/bridge.ts +466 -69
  36. package/src/chat/outbound.ts +144 -38
  37. package/src/claude/inbound-debug.ts +53 -8
  38. package/src/claude/outbound.ts +224 -38
  39. package/src/cli/agent-driven.ts +34 -1
  40. package/src/cli/catalog-prewarm.ts +5 -2
  41. package/src/cli/claude-desktop.ts +2 -2
  42. package/src/cli/doctor.ts +12 -0
  43. package/src/cli/export-command.ts +187 -0
  44. package/src/cli/help.ts +11 -0
  45. package/src/cli/index.ts +13 -3
  46. package/src/cli/init.ts +129 -102
  47. package/src/cli/opencode.ts +36 -151
  48. package/src/cli/star-prompt.ts +13 -4
  49. package/src/cli/status-oauth.ts +12 -2
  50. package/src/clients/config-export.ts +377 -0
  51. package/src/codex/account-runtime-state.ts +19 -1
  52. package/src/codex/account-store.ts +162 -82
  53. package/src/codex/auth-api.ts +467 -159
  54. package/src/codex/auth-context.ts +15 -2
  55. package/src/codex/catalog/aggregation.ts +15 -0
  56. package/src/codex/catalog/effort.ts +16 -6
  57. package/src/codex/catalog/metadata.ts +6 -0
  58. package/src/codex/catalog/parsing.ts +3 -1
  59. package/src/codex/catalog/provider-fetch.ts +29 -0
  60. package/src/codex/catalog/sync.ts +64 -7
  61. package/src/codex/catalog.ts +2 -2
  62. package/src/codex/inject.ts +5 -5
  63. package/src/codex/main-account-cache.ts +8 -1
  64. package/src/codex/model-cache.ts +81 -2
  65. package/src/codex/pool-rotation.ts +39 -0
  66. package/src/codex/project-config-warnings.ts +12 -1
  67. package/src/codex/quota.ts +35 -3
  68. package/src/codex/routing.ts +46 -1
  69. package/src/codex/shim.ts +10 -4
  70. package/src/codex/subagent-model-fallback.ts +12 -0
  71. package/src/codex/websocket-registry.ts +27 -0
  72. package/src/combos/failover.ts +31 -1
  73. package/src/combos/request.ts +9 -0
  74. package/src/combos/resolve.ts +60 -4
  75. package/src/combos/types.ts +12 -0
  76. package/src/config.ts +510 -55
  77. package/src/github/star-state.ts +13 -1
  78. package/src/images/fulfill.ts +39 -1
  79. package/src/images/loop.ts +52 -12
  80. package/src/lib/admission.ts +83 -0
  81. package/src/lib/app-owned-memory-stores.ts +173 -0
  82. package/src/lib/app-owned-memory.ts +265 -0
  83. package/src/lib/bun-stream-caps.ts +31 -7
  84. package/src/lib/config-ownership.ts +33 -0
  85. package/src/lib/crash-guard.ts +65 -5
  86. package/src/lib/debug-log-buffer.ts +47 -6
  87. package/src/lib/destination-policy.ts +12 -1
  88. package/src/lib/errors.ts +3 -0
  89. package/src/lib/gcp-adc.ts +40 -2
  90. package/src/lib/injection-debug-log.ts +26 -2
  91. package/src/lib/provider-outbound.ts +3 -0
  92. package/src/lib/sidecar-tracker.ts +5 -2
  93. package/src/lib/sse-decoder.ts +257 -37
  94. package/src/lib/state-store-registrations.ts +109 -0
  95. package/src/lib/state-store-sweeper.ts +184 -0
  96. package/src/lib/translator-budget.ts +356 -0
  97. package/src/lib/windows-secret-acl.ts +33 -12
  98. package/src/lib/winsw.ts +14 -1
  99. package/src/oauth/anthropic-routing.ts +31 -7
  100. package/src/oauth/google-antigravity.ts +2 -1
  101. package/src/oauth/health.ts +30 -12
  102. package/src/oauth/index.ts +127 -23
  103. package/src/oauth/kiro-credentials.ts +72 -1
  104. package/src/oauth/kiro.ts +23 -4
  105. package/src/oauth/store.ts +165 -18
  106. package/src/oauth/token-guardian.ts +43 -4
  107. package/src/oauth/types.ts +2 -1
  108. package/src/providers/base-url-choices.ts +10 -0
  109. package/src/providers/derive.ts +12 -0
  110. package/src/providers/free-directory.ts +4 -1
  111. package/src/providers/key-failover.ts +12 -0
  112. package/src/providers/openai-sidecar.ts +4 -1
  113. package/src/providers/quota.ts +68 -7
  114. package/src/providers/registry.ts +279 -3
  115. package/src/responses/parser.ts +5 -1
  116. package/src/responses/spill-store.ts +394 -0
  117. package/src/responses/state.ts +520 -102
  118. package/src/router.ts +18 -1
  119. package/src/server/adapter-resolve.ts +20 -3
  120. package/src/server/auth-cors.ts +121 -28
  121. package/src/server/chat-completions.ts +57 -12
  122. package/src/server/claude-messages.ts +85 -13
  123. package/src/server/index.ts +242 -100
  124. package/src/server/lifecycle.ts +155 -25
  125. package/src/server/management/agent-settings-routes.ts +79 -36
  126. package/src/server/management/api-key-usage.ts +167 -0
  127. package/src/server/management/body.ts +35 -0
  128. package/src/server/management/combo-routes.ts +5 -1
  129. package/src/server/management/config-routes.ts +42 -12
  130. package/src/server/management/logs-usage-routes.ts +41 -21
  131. package/src/server/management/model-routes.ts +188 -54
  132. package/src/server/management/oauth-account-routes.ts +115 -26
  133. package/src/server/management/provider-routes.ts +56 -6
  134. package/src/server/management/shared.ts +16 -3
  135. package/src/server/management/sidebar-routes.ts +50 -1
  136. package/src/server/management/system-restart.ts +13 -6
  137. package/src/server/management/system-routes.ts +15 -3
  138. package/src/server/management/usage-summary-cache.ts +86 -0
  139. package/src/server/management-api.ts +39 -5
  140. package/src/server/management-auth.ts +65 -14
  141. package/src/server/port-reclaim.ts +58 -12
  142. package/src/server/ports.ts +2 -0
  143. package/src/server/proxy-liveness.ts +60 -14
  144. package/src/server/relay-eager.ts +20 -4
  145. package/src/server/relay.ts +548 -154
  146. package/src/server/request-decompress.ts +51 -4
  147. package/src/server/request-log.ts +134 -15
  148. package/src/server/responses/collaboration.ts +15 -4
  149. package/src/server/responses/compact.ts +3 -0
  150. package/src/server/responses/core.ts +241 -66
  151. package/src/server/responses-image-gen-repair.ts +19 -5
  152. package/src/server/responses-item-id-repair.ts +23 -5
  153. package/src/server/sse-payload-rewrite.ts +71 -12
  154. package/src/server/startup-health-cache.ts +14 -1
  155. package/src/server/system-env.ts +8 -1
  156. package/src/server/windows-tcp-drop.ts +15 -5
  157. package/src/server/ws-bridge.ts +25 -0
  158. package/src/service.ts +179 -13
  159. package/src/storage/policy-job.ts +93 -23
  160. package/src/storage/policy-worker.ts +6 -0
  161. package/src/storage/restore-job.ts +62 -16
  162. package/src/storage/restore-worker.ts +6 -0
  163. package/src/storage/storage-mutation-coordinator.ts +36 -6
  164. package/src/storage/worker-lifecycle.ts +181 -47
  165. package/src/tray/windows.ts +97 -25
  166. package/src/types.ts +39 -6
  167. package/src/update/index.ts +24 -5
  168. package/src/update/job.ts +598 -73
  169. package/src/usage/log.ts +115 -17
  170. package/src/usage/summary.ts +67 -2
  171. package/src/vision/index.ts +112 -22
  172. package/src/web-search/loop.ts +38 -6
  173. package/src/web-search/progress-stream.ts +14 -3
  174. package/gui/dist/assets/index-BDjpkcRN.js +0 -67
  175. package/gui/dist/assets/index-BHsKRFh9.css +0 -1
@@ -0,0 +1,265 @@
1
+ export type AppOwnedRetainedCategory = "logs" | "caches" | "blobs" | "continuation";
2
+ export type AppOwnedObservedCategory = "translator" | "serialized_tails";
3
+
4
+ export interface RetainedStoreSnapshot {
5
+ count: number;
6
+ bytes: number;
7
+ evictableBytes: number;
8
+ pinnedBytes: number;
9
+ oldestAt: number | null;
10
+ }
11
+
12
+ export interface RetainedStoreRegistration {
13
+ id: string;
14
+ category: AppOwnedRetainedCategory;
15
+ snapshot(): RetainedStoreSnapshot;
16
+ evictOldest(): number;
17
+ }
18
+
19
+ export interface ObservedBufferRegistration {
20
+ id: string;
21
+ category: AppOwnedObservedCategory;
22
+ snapshot(): { currentBytes: number; highWaterBytes: number; active: number };
23
+ }
24
+
25
+ export interface AppOwnedBytesSnapshot {
26
+ budgetBytes: number;
27
+ retainedBytes: number;
28
+ evictableBytes: number;
29
+ pinnedBytes: number;
30
+ overBudgetBytes: number;
31
+ stores: Record<string, RetainedStoreSnapshot>;
32
+ observedInFlight: Record<string, { currentBytes: number; highWaterBytes: number; active: number }>;
33
+ enforcement: {
34
+ runs: number;
35
+ entriesDemoted: number;
36
+ bytesReleased: number;
37
+ noEvictableCandidate: number;
38
+ snapshotFailures: number;
39
+ oldestAtContractViolations: number;
40
+ };
41
+ }
42
+
43
+ export const DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES = 256 * 1024 * 1024;
44
+ /**
45
+ * The budget above is an eviction target, not a hard resident limit: active/pinned
46
+ * state may temporarily keep retained bytes above it. Every pin-capable owner has
47
+ * its own finite admission cap; their documented aggregate must remain below this
48
+ * process-owned hard ceiling.
49
+ */
50
+ export const APP_OWNED_WORST_CASE_PINNED_BYTES = 512 * 1024 * 1024;
51
+ export const MIN_APP_OWNED_MEMORY_BUDGET_MB = 64;
52
+ export const MAX_APP_OWNED_MEMORY_BUDGET_MB = 4_096;
53
+
54
+ const CATEGORY_ORDER: readonly AppOwnedRetainedCategory[] = [
55
+ "logs",
56
+ "caches",
57
+ "blobs",
58
+ "continuation",
59
+ ];
60
+ const WARN_INTERVAL_MS = 60_000;
61
+ const ZERO_RETAINED_SNAPSHOT: RetainedStoreSnapshot = {
62
+ count: 0,
63
+ bytes: 0,
64
+ evictableBytes: 0,
65
+ pinnedBytes: 0,
66
+ oldestAt: null,
67
+ };
68
+ const ZERO_OBSERVED_SNAPSHOT = { currentBytes: 0, highWaterBytes: 0, active: 0 };
69
+
70
+ const retainedStores = new Map<string, RetainedStoreRegistration>();
71
+ const observedBuffers = new Map<string, ObservedBufferRegistration>();
72
+ let budgetBytes = DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES;
73
+ let lastSaturationWarningAt = Number.NEGATIVE_INFINITY;
74
+ const enforcementCounters = {
75
+ runs: 0,
76
+ entriesDemoted: 0,
77
+ bytesReleased: 0,
78
+ noEvictableCandidate: 0,
79
+ snapshotFailures: 0,
80
+ oldestAtContractViolations: 0,
81
+ };
82
+ let isEnforcing = false;
83
+
84
+ export function resolveAppOwnedMemoryBudgetBytes(value: unknown): number {
85
+ return typeof value === "number"
86
+ && Number.isInteger(value)
87
+ && value >= MIN_APP_OWNED_MEMORY_BUDGET_MB
88
+ && value <= MAX_APP_OWNED_MEMORY_BUDGET_MB
89
+ ? value * 1024 * 1024
90
+ : DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES;
91
+ }
92
+
93
+ export function registerRetainedStore(registration: RetainedStoreRegistration): () => void {
94
+ retainedStores.set(registration.id, registration);
95
+ return () => {
96
+ if (retainedStores.get(registration.id) === registration) retainedStores.delete(registration.id);
97
+ };
98
+ }
99
+
100
+ export function registerObservedBuffer(registration: ObservedBufferRegistration): () => void {
101
+ observedBuffers.set(registration.id, registration);
102
+ return () => {
103
+ if (observedBuffers.get(registration.id) === registration) observedBuffers.delete(registration.id);
104
+ };
105
+ }
106
+
107
+ export function configureAppOwnedMemoryBudget(bytes: number): void {
108
+ budgetBytes = bytes;
109
+ }
110
+
111
+ function retainedSnapshot(registration: RetainedStoreRegistration): RetainedStoreSnapshot {
112
+ try {
113
+ return registration.snapshot();
114
+ } catch {
115
+ enforcementCounters.snapshotFailures += 1;
116
+ return { ...ZERO_RETAINED_SNAPSHOT };
117
+ }
118
+ }
119
+
120
+ function observedSnapshot(
121
+ registration: ObservedBufferRegistration,
122
+ ): { currentBytes: number; highWaterBytes: number; active: number } {
123
+ try {
124
+ return registration.snapshot();
125
+ } catch {
126
+ enforcementCounters.snapshotFailures += 1;
127
+ return { ...ZERO_OBSERVED_SNAPSHOT };
128
+ }
129
+ }
130
+
131
+ export function appOwnedBytesSnapshot(): AppOwnedBytesSnapshot {
132
+ return appOwnedBytesSnapshotFrom(retainedSnapshots().stores);
133
+ }
134
+
135
+ function appOwnedBytesSnapshotFrom(
136
+ retained: ReadonlyMap<string, RetainedStoreSnapshot>,
137
+ ): AppOwnedBytesSnapshot {
138
+ const stores: Record<string, RetainedStoreSnapshot> = {};
139
+ let retainedBytes = 0;
140
+ let evictableBytes = 0;
141
+ let pinnedBytes = 0;
142
+ for (const registration of retainedStores.values()) {
143
+ const snapshot = retained.get(registration.id) ?? ZERO_RETAINED_SNAPSHOT;
144
+ stores[registration.id] = snapshot;
145
+ retainedBytes += snapshot.bytes;
146
+ evictableBytes += snapshot.evictableBytes;
147
+ pinnedBytes += snapshot.pinnedBytes;
148
+ }
149
+
150
+ const observedInFlight: AppOwnedBytesSnapshot["observedInFlight"] = {};
151
+ for (const registration of observedBuffers.values()) {
152
+ observedInFlight[registration.id] = observedSnapshot(registration);
153
+ }
154
+
155
+ return {
156
+ budgetBytes,
157
+ retainedBytes,
158
+ evictableBytes,
159
+ pinnedBytes,
160
+ overBudgetBytes: Math.max(0, retainedBytes - budgetBytes),
161
+ stores,
162
+ observedInFlight,
163
+ enforcement: { ...enforcementCounters },
164
+ };
165
+ }
166
+
167
+ function nextCandidate(
168
+ snapshots: ReadonlyMap<string, RetainedStoreSnapshot>,
169
+ ineligible: Set<string>,
170
+ ): RetainedStoreRegistration | undefined {
171
+ for (const category of CATEGORY_ORDER) {
172
+ let candidate: RetainedStoreRegistration | undefined;
173
+ let candidateAt = Number.POSITIVE_INFINITY;
174
+ for (const registration of retainedStores.values()) {
175
+ if (registration.category !== category || ineligible.has(registration.id)) continue;
176
+ const snapshot = snapshots.get(registration.id);
177
+ if (!snapshot || snapshot.evictableBytes <= 0) continue;
178
+ const oldestAt = snapshot.oldestAt;
179
+ if (oldestAt === null || !Number.isFinite(oldestAt)) {
180
+ enforcementCounters.oldestAtContractViolations += 1;
181
+ ineligible.add(registration.id);
182
+ continue;
183
+ }
184
+ if (!candidate || oldestAt < candidateAt) {
185
+ candidate = registration;
186
+ candidateAt = oldestAt;
187
+ }
188
+ }
189
+ if (candidate) return candidate;
190
+ }
191
+ return undefined;
192
+ }
193
+
194
+ function retainedSnapshots(): { total: number; stores: Map<string, RetainedStoreSnapshot> } {
195
+ const stores = new Map<string, RetainedStoreSnapshot>();
196
+ let total = 0;
197
+ for (const registration of retainedStores.values()) {
198
+ const snapshot = retainedSnapshot(registration);
199
+ stores.set(registration.id, snapshot);
200
+ total += snapshot.bytes;
201
+ }
202
+ return { total, stores };
203
+ }
204
+
205
+ function warnPinnedSaturation(): void {
206
+ const at = Date.now();
207
+ if (at - lastSaturationWarningAt < WARN_INTERVAL_MS) return;
208
+ lastSaturationWarningAt = at;
209
+ console.warn("[app-owned-memory] retained state remains over budget with no evictable candidate");
210
+ }
211
+
212
+ export function enforceAppOwnedMemoryBudget(): AppOwnedBytesSnapshot {
213
+ if (isEnforcing) return appOwnedBytesSnapshot();
214
+ isEnforcing = true;
215
+ enforcementCounters.runs += 1;
216
+ try {
217
+ const ineligible = new Set<string>();
218
+ const current = retainedSnapshots();
219
+ while (current.total > budgetBytes) {
220
+ const candidate = nextCandidate(current.stores, ineligible);
221
+ if (!candidate) {
222
+ enforcementCounters.noEvictableCandidate += 1;
223
+ warnPinnedSaturation();
224
+ break;
225
+ }
226
+
227
+ let reportedReleased = 0;
228
+ try {
229
+ reportedReleased = candidate.evictOldest();
230
+ } catch {
231
+ ineligible.add(candidate.id);
232
+ continue;
233
+ }
234
+ const previousSnapshot = current.stores.get(candidate.id) ?? ZERO_RETAINED_SNAPSHOT;
235
+ const nextSnapshot = retainedSnapshot(candidate);
236
+ current.stores.set(candidate.id, nextSnapshot);
237
+ const nextTotal = Math.max(0, current.total - previousSnapshot.bytes + nextSnapshot.bytes);
238
+ const actualReleased = Math.max(0, current.total - nextTotal);
239
+ if (reportedReleased <= 0 || actualReleased <= 0) {
240
+ ineligible.add(candidate.id);
241
+ } else {
242
+ enforcementCounters.entriesDemoted += 1;
243
+ enforcementCounters.bytesReleased += actualReleased;
244
+ }
245
+ current.total = nextTotal;
246
+ }
247
+ return appOwnedBytesSnapshotFrom(current.stores);
248
+ } finally {
249
+ isEnforcing = false;
250
+ }
251
+ }
252
+
253
+ export function resetAppOwnedMemoryForTests(): void {
254
+ retainedStores.clear();
255
+ observedBuffers.clear();
256
+ budgetBytes = DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES;
257
+ lastSaturationWarningAt = Number.NEGATIVE_INFINITY;
258
+ enforcementCounters.runs = 0;
259
+ enforcementCounters.entriesDemoted = 0;
260
+ enforcementCounters.bytesReleased = 0;
261
+ enforcementCounters.noEvictableCandidate = 0;
262
+ enforcementCounters.snapshotFailures = 0;
263
+ enforcementCounters.oldestAtContractViolations = 0;
264
+ isEnforcing = false;
265
+ }
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Bun runtime stream-capability gate for the Windows SSE passthrough path (#314).
2
+ * Bun runtime stream-capability gate for eager SSE passthrough (#314).
3
3
  *
4
4
  * The eager bounded relay (src/server/relay-eager.ts) uses a JS async producer
5
5
  * loop — the exact shape of the Bun#32111 use-after-free (fixed upstream by Bun
6
6
  * PR #32120, merged 2026-06-21). No RELEASED Bun version is proven to carry
7
7
  * that fix yet, so `MIN_FIXED_BUN_VERSION` is null: every runtime is
8
- * "known-bad" until a bundle-bump commit sets it. Config `streamMode` can force
9
- * either path (persisted in config.json because Windows services do not
10
- * inherit shell env see devlog/_plan/260723_win_mem_safestream/001).
8
+ * "known-bad" until a bundle-bump commit sets it. Windows no-rewrite traffic
9
+ * follows this runtime/config decision. Darwin no-rewrite traffic stays on tee
10
+ * for `auto` regardless of runtime capability and reaches eager relay only via
11
+ * explicit `streamMode: "eager-relay"` opt-in (see
12
+ * devlog/_plan/260731_macos_rss_retention/100_darwin_eager_optin.md).
11
13
  *
12
14
  * Prerelease conservatism: a version carrying a prerelease suffix (e.g.
13
15
  * `1.4.0-canary.3`) is NEVER treated as fixed even when its numeric triple
@@ -71,9 +73,8 @@ export type EagerRelayDecision = {
71
73
  };
72
74
 
73
75
  /**
74
- * Decide the win32 SSE client-path shape. `version`/`minFixed` are injectable
75
- * for tests. Non-win32 callers never consult this (their default path is
76
- * unchanged); the caller owns the platform check.
76
+ * Decide the runtime/config SSE client-path capability. `version`/`minFixed`
77
+ * are injectable for tests; `selectEagerPath` applies platform policy.
77
78
  */
78
79
  export function decideEagerRelay(
79
80
  mode: StreamMode,
@@ -86,3 +87,26 @@ export function decideEagerRelay(
86
87
  ? { useEagerRelay: true, reason: "auto-fixed-runtime" }
87
88
  : { useEagerRelay: false, reason: "auto-known-bad" };
88
89
  }
90
+
91
+ /**
92
+ * Apply the two-platform eager-relay policy to the runtime/config capability.
93
+ * Windows preserves the decision for no-rewrite traffic. Darwin permits only
94
+ * explicit config opt-in; `auto` remains tee even on a future fixed runtime.
95
+ * Returns the normalized effective decision, or null when platform policy,
96
+ * rewrite needs, or a Darwin non-config-eager mode selects tee.
97
+ */
98
+ export function selectEagerPath(
99
+ platform: NodeJS.Platform,
100
+ needsClientRewrite: boolean,
101
+ mode: StreamMode,
102
+ version: string = Bun.version,
103
+ minFixed: string | null = MIN_FIXED_BUN_VERSION,
104
+ ): EagerRelayDecision | null {
105
+ if (needsClientRewrite || (platform !== "win32" && platform !== "darwin")) {
106
+ return null;
107
+ }
108
+
109
+ const decision = decideEagerRelay(mode, version, minFixed);
110
+ if (platform === "win32") return decision;
111
+ return decision.reason === "config-eager" ? decision : null;
112
+ }
@@ -12,6 +12,7 @@ import {
12
12
  } from "node:fs";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
15
+ import type { GenerationContext } from "./state-store-sweeper";
15
16
 
16
17
  export const CONFIG_OWNER_FILE = ".opencodex-owner.json";
17
18
  export const CONFIG_UNINSTALL_MANIFEST = ".opencodex-uninstall.json";
@@ -47,6 +48,10 @@ const INITIAL_OWNED_PATHS = [
47
48
  "codex-shim.autorestore.lock",
48
49
  "codex-shim.json",
49
50
  "config.json",
51
+ "config-mutation.sqlite",
52
+ "config-mutation.sqlite-journal",
53
+ "config-mutation.sqlite-shm",
54
+ "config-mutation.sqlite-wal",
50
55
  "crash.log",
51
56
  "kimi-device-id",
52
57
  "mimo-client-id",
@@ -76,6 +81,34 @@ const ownershipCache = new Map<string, {
76
81
  owner: ConfigOwner;
77
82
  manifest: ConfigUninstallManifest;
78
83
  } | null>();
84
+ let lastReconciledGeneration = 0;
85
+
86
+ export function listLiveConfigOwnershipRoots(currentConfigDir: string): ReadonlySet<string> {
87
+ const currentRoot = ownershipCacheKey(currentConfigDir);
88
+ const roots = new Set<string>([currentRoot]);
89
+ for (const [root, ownership] of ownershipCache) {
90
+ if (root === currentRoot) continue;
91
+ if (ownership?.manifest.paths.some(rel => existsSync(join(root, ...rel.split("/"))))) {
92
+ roots.add(root);
93
+ }
94
+ }
95
+ return roots;
96
+ }
97
+
98
+ export function reconcileConfigOwnershipRoots(context: GenerationContext): number {
99
+ if (context.generation <= lastReconciledGeneration) return 0;
100
+ let removed = 0;
101
+ for (const [root, ownership] of ownershipCache) {
102
+ if (context.configRoots.has(root)) continue;
103
+ const hasLiveOwnedPath = ownership?.manifest.paths.some(rel =>
104
+ existsSync(join(root, ...rel.split("/"))));
105
+ if (hasLiveOwnedPath) continue;
106
+ ownershipCache.delete(root);
107
+ removed += 1;
108
+ }
109
+ lastReconciledGeneration = context.generation;
110
+ return removed;
111
+ }
79
112
 
80
113
  function ownershipCacheKey(configDir: string): string {
81
114
  const key = resolve(configDir);
@@ -4,6 +4,8 @@ import { getConfigDir } from "../config";
4
4
  import { recordOwnedConfigPath } from "./config-ownership";
5
5
  import { redactSecretString, redactUrlForLog } from "./redact";
6
6
  import { sidecarBreadcrumb, activityBreadcrumb } from "./sidecar-tracker";
7
+ import { retainedUtf8Bytes, truncateRetainedUtf8 } from "./admission";
8
+ import { enforceAppOwnedMemoryBudget } from "./app-owned-memory";
7
9
 
8
10
  /**
9
11
  * Process-level safety net for the long-running proxy daemon.
@@ -205,8 +207,42 @@ function record(kind: string, err: unknown, promise?: unknown): void {
205
207
 
206
208
  interface FetchTrace { url: string; at: number; origin: string; settled: boolean; rejected?: string }
207
209
  const FETCH_RING_MAX = 12;
210
+ const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
208
211
  const fetchRing: FetchTrace[] = [];
209
212
  let fetchInstrumented = false;
213
+ let fetchRingBytes = 0;
214
+
215
+ function fetchTraceBytes(trace: FetchTrace): number {
216
+ return retainedUtf8Bytes(trace.url) + retainedUtf8Bytes(trace.origin) + retainedUtf8Bytes(trace.rejected ?? "");
217
+ }
218
+
219
+ function removeOldestFetchTrace(): number {
220
+ const trace = fetchRing.shift();
221
+ if (!trace) return 0;
222
+ const bytes = fetchTraceBytes(trace);
223
+ fetchRingBytes -= bytes;
224
+ return bytes;
225
+ }
226
+
227
+ function setFetchTraceRejected(trace: FetchTrace, value: string): void {
228
+ const retained = fetchRing.includes(trace);
229
+ if (retained) fetchRingBytes -= retainedUtf8Bytes(trace.rejected ?? "");
230
+ trace.rejected = truncateRetainedUtf8(value, MAX_DIAGNOSTIC_VALUE_BYTES);
231
+ if (retained) fetchRingBytes += retainedUtf8Bytes(trace.rejected);
232
+ }
233
+
234
+ function appendFetchTrace(url: string, origin: string): FetchTrace {
235
+ const trace: FetchTrace = {
236
+ url: truncateRetainedUtf8(redactUrlForLog(url), MAX_DIAGNOSTIC_VALUE_BYTES),
237
+ at: Date.now(),
238
+ origin: truncateRetainedUtf8(origin, MAX_DIAGNOSTIC_VALUE_BYTES),
239
+ settled: false,
240
+ };
241
+ fetchRing.push(trace);
242
+ fetchRingBytes += fetchTraceBytes(trace);
243
+ while (fetchRing.length > FETCH_RING_MAX) removeOldestFetchTrace();
244
+ return trace;
245
+ }
210
246
 
211
247
  /**
212
248
  * The recurring native-only rejection carries no source location, and every JS `await fetch(...)`
@@ -228,24 +264,48 @@ function instrumentFetch(): void {
228
264
  url = typeof input === "string" ? input : input instanceof URL ? input.href : (input as Request)?.url ?? "";
229
265
  } catch { /* best-effort */ }
230
266
  const origin = (new Error().stack ?? "").split("\n").slice(2, 5).map(l => l.trim()).join(" <- ");
231
- const trace: FetchTrace = { url: redactUrlForLog(url), at: Date.now(), origin, settled: false };
232
- fetchRing.push(trace);
233
- if (fetchRing.length > FETCH_RING_MAX) fetchRing.shift();
267
+ const trace = appendFetchTrace(url, origin);
268
+ enforceAppOwnedMemoryBudget();
234
269
  let p: ReturnType<typeof fetch>;
235
270
  try {
236
271
  p = original.apply(this, args);
237
272
  } catch (e) {
238
273
  trace.settled = true;
239
- trace.rejected = redactDiagnosticText(e instanceof Error ? `${e.name}: ${e.message}` : String(e));
274
+ setFetchTraceRejected(trace, redactDiagnosticText(e instanceof Error ? `${e.name}: ${e.message}` : String(e)));
240
275
  throw e;
241
276
  }
242
277
  return p.then(
243
278
  r => { trace.settled = true; return r; },
244
- e => { trace.settled = true; trace.rejected = redactDiagnosticText(e instanceof Error ? `${e.name}: ${e.message}` : String(e)); throw e; },
279
+ e => { trace.settled = true; setFetchTraceRejected(trace, redactDiagnosticText(e instanceof Error ? `${e.name}: ${e.message}` : String(e))); throw e; },
245
280
  );
246
281
  } as typeof fetch;
247
282
  }
248
283
 
284
+ export function crashRingMetrics(): { entries: number; bytes: number; oldestAt: number | null } {
285
+ return { entries: fetchRing.length, bytes: fetchRingBytes, oldestAt: fetchRing[0]?.at ?? null };
286
+ }
287
+
288
+ export function evictOldestCrashTraceForBudget(): number {
289
+ return removeOldestFetchTrace();
290
+ }
291
+
292
+ export function appendCrashTraceForTests(url: string, origin: string, rejected?: string): void {
293
+ const trace = appendFetchTrace(url, origin);
294
+ if (rejected !== undefined) {
295
+ trace.settled = true;
296
+ setFetchTraceRejected(trace, rejected);
297
+ }
298
+ }
299
+
300
+ export function crashRingEntriesForTests(): readonly Readonly<FetchTrace>[] {
301
+ return fetchRing.map(trace => ({ ...trace }));
302
+ }
303
+
304
+ export function resetCrashRingForTests(): void {
305
+ fetchRing.length = 0;
306
+ fetchRingBytes = 0;
307
+ }
308
+
249
309
  /** Render the recent fetch ring (pending first) for the crash breadcrumb. */
250
310
  function recentFetches(): string {
251
311
  try {
@@ -8,15 +8,31 @@ export interface DebugLogEntry {
8
8
  }
9
9
 
10
10
  const MAX_LINES = 2_000;
11
+ const MAX_DEBUG_SUBSCRIBERS = 64;
12
+ const MAX_DEBUG_LINE_BYTES = 16 * 1024;
11
13
  const buffer: DebugLogEntry[] = [];
12
- const listeners = new Set<(entry: DebugLogEntry) => void>();
14
+ interface DebugSubscriberRegistration { lease: AdmissionLease }
15
+ const listeners = new Map<(entry: DebugLogEntry) => void, DebugSubscriberRegistration>();
16
+ const subscriberGate = createAdmissionGate("debug_subscribers", MAX_DEBUG_SUBSCRIBERS);
13
17
  let nextSeq = 1;
18
+ let bufferBytes = 0;
19
+
20
+ function removeOldestEntry(): number {
21
+ const entry = buffer.shift();
22
+ if (!entry) return 0;
23
+ const bytes = retainedUtf8Bytes(entry.line);
24
+ bufferBytes -= bytes;
25
+ return bytes;
26
+ }
14
27
 
15
28
  export function appendDebugLogLine(line: string): void {
16
- const entry: DebugLogEntry = { seq: nextSeq++, at: Date.now(), line };
29
+ const retainedLine = truncateRetainedUtf8(line, MAX_DEBUG_LINE_BYTES);
30
+ const entry: DebugLogEntry = { seq: nextSeq++, at: Date.now(), line: retainedLine };
17
31
  buffer.push(entry);
18
- if (buffer.length > MAX_LINES) buffer.splice(0, buffer.length - MAX_LINES);
19
- for (const listener of listeners) {
32
+ bufferBytes += retainedUtf8Bytes(retainedLine);
33
+ while (buffer.length > MAX_LINES) removeOldestEntry();
34
+ enforceAppOwnedMemoryBudget();
35
+ for (const listener of listeners.keys()) {
20
36
  try { listener(entry); } catch { /* listeners must not break logging */ }
21
37
  }
22
38
  }
@@ -30,13 +46,38 @@ export function getDebugLogEntries(options?: { after?: number; limit?: number })
30
46
  }
31
47
 
32
48
  export function subscribeDebugLogEntries(listener: (entry: DebugLogEntry) => void): () => void {
33
- listeners.add(listener);
34
- return () => listeners.delete(listener);
49
+ let registration = listeners.get(listener);
50
+ if (!registration) {
51
+ const lease = subscriberGate.tryAcquire();
52
+ if (!lease) throw new ResourceAdmissionError("debug_subscribers", MAX_DEBUG_SUBSCRIBERS);
53
+ registration = { lease };
54
+ listeners.set(listener, registration);
55
+ }
56
+ let disposed = false;
57
+ return () => {
58
+ if (disposed) return;
59
+ disposed = true;
60
+ if (listeners.get(listener) !== registration) return;
61
+ listeners.delete(listener);
62
+ registration.lease.release();
63
+ };
64
+ }
65
+
66
+ export function debugBufferMetrics(): { entries: number; bytes: number; subscribers: AdmissionMetrics; oldestAt: number | null } {
67
+ return { entries: buffer.length, bytes: bufferBytes, subscribers: subscriberGate.metrics(), oldestAt: buffer[0]?.at ?? null };
68
+ }
69
+
70
+ export function evictOldestDebugEntryForBudget(): number {
71
+ return removeOldestEntry();
35
72
  }
36
73
 
37
74
  /** Test isolation. */
38
75
  export function resetDebugLogBufferForTests(): void {
39
76
  buffer.length = 0;
77
+ bufferBytes = 0;
78
+ for (const registration of listeners.values()) registration.lease.release();
40
79
  listeners.clear();
41
80
  nextSeq = 1;
42
81
  }
82
+ import { createAdmissionGate, ResourceAdmissionError, retainedUtf8Bytes, truncateRetainedUtf8, type AdmissionLease, type AdmissionMetrics } from "./admission";
83
+ import { enforceAppOwnedMemoryBudget } from "./app-owned-memory";
@@ -130,7 +130,18 @@ function registryAllowsPrivateNetwork(name: string): boolean {
130
130
  return getProviderRegistryEntry(name)?.allowPrivateNetworkByDefault === true;
131
131
  }
132
132
 
133
- /** True when the provider config or registry default admits private/loopback destinations. */
133
+ /**
134
+ * Whether a provider may reach loopback/private addresses.
135
+ *
136
+ * Two sources, and both have to be consulted at every boundary: the operator's explicit
137
+ * `allowPrivateNetwork`, and the registry's `allowPrivateNetworkByDefault` for entries that are
138
+ * local BY DEFINITION (Ollama, vLLM, LM Studio, LiteLLM). Config validation already read both;
139
+ * outbound discovery read only the first, so a stock Ollama entry passed validation and was then
140
+ * refused at the fetch (#758).
141
+ *
142
+ * This grants nothing new. Metadata, link-local and unspecified destinations are rejected before
143
+ * this is consulted, and a provider without either source still cannot reach a private address.
144
+ */
134
145
  export function providerAllowsPrivateNetwork(
135
146
  name: string,
136
147
  provider: Pick<OcxProviderConfig, "allowPrivateNetwork">,
package/src/lib/errors.ts CHANGED
@@ -104,6 +104,9 @@ export function isClientClosedMessage(text: string): boolean {
104
104
 
105
105
  export function classifyError(status: number, type: string, message: string): OcxErrorPayload {
106
106
  const text = message.toLowerCase();
107
+ if (type === "previous_response_not_found") {
108
+ return { message, type: "invalid_request_error", code: "previous_response_not_found" };
109
+ }
107
110
  // Preserve explicit cancel types used by compact/combo JSON errors; unify message-inferred
108
111
  // client closes (web-search abort text) onto client_closed_request for /api/logs.
109
112
  if (type === "client_cancelled") {
@@ -18,6 +18,11 @@ import { Buffer } from "node:buffer";
18
18
  import * as os from "node:os";
19
19
  import * as path from "node:path";
20
20
  import { readFileSync, existsSync, statSync } from "node:fs";
21
+ import {
22
+ captureConfigGeneration,
23
+ sweepExpiredOnWrite,
24
+ type GenerationContext,
25
+ } from "./state-store-sweeper";
21
26
 
22
27
  /** Injectable fetch (tests pass a mock); defaults to the global fetch. */
23
28
  export type FetchImpl = typeof fetch;
@@ -60,6 +65,33 @@ interface TokenResponse {
60
65
 
61
66
  const tokenCache = new Map<string, CachedToken>();
62
67
  const inflight = new Map<string, Promise<string>>();
68
+ let lastReconciledGeneration = 0;
69
+ let liveSources = new Set<string>();
70
+
71
+ export function sweepExpiredGcpAdcTokens(now = Date.now()): number {
72
+ const skew = getRefreshSkewMs();
73
+ let removed = 0;
74
+ for (const [source, cached] of tokenCache) {
75
+ if (cached.expiresAtMs - skew > now) continue;
76
+ tokenCache.delete(source);
77
+ removed += 1;
78
+ }
79
+ return removed;
80
+ }
81
+
82
+ export function reconcileGcpAdcTokens(context: GenerationContext): number {
83
+ if (context.generation <= lastReconciledGeneration) return 0;
84
+ const currentSource = currentAdcSourceKey();
85
+ let removed = 0;
86
+ for (const source of tokenCache.keys()) {
87
+ if (source === currentSource) continue;
88
+ tokenCache.delete(source);
89
+ removed += 1;
90
+ }
91
+ liveSources = new Set([currentSource]);
92
+ lastReconciledGeneration = context.generation;
93
+ return removed;
94
+ }
63
95
 
64
96
  function getRefreshSkewMs(): number {
65
97
  const raw = Number(process.env.GOOGLE_VERTEX_REFRESH_SKEW_MS);
@@ -271,6 +303,7 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
271
303
  // Only serve a cached token that matches the source the next resolve would actually use; prune
272
304
  // expired or now-stale (different-source) entries so a credential-source change is honored.
273
305
  const expectedSource = currentAdcSourceKey();
306
+ const writerGeneration = captureConfigGeneration();
274
307
  for (const [source, cached] of tokenCache) {
275
308
  if (source === expectedSource && cached.expiresAtMs - skew > now) return cached.token;
276
309
  if (cached.expiresAtMs - skew <= now) tokenCache.delete(source);
@@ -286,7 +319,10 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
286
319
  try {
287
320
  const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
288
321
  const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
289
- tokenCache.set(source, { token: token.access_token, expiresAtMs });
322
+ if (writerGeneration >= lastReconciledGeneration || liveSources.has(source)) {
323
+ tokenCache.set(source, { token: token.access_token, expiresAtMs });
324
+ sweepExpiredOnWrite(Date.now());
325
+ }
290
326
  return token.access_token;
291
327
  } finally {
292
328
  inflight.delete(cacheKey);
@@ -296,8 +332,10 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
296
332
  return promise;
297
333
  }
298
334
 
299
- /** Test seam: clears every cached token + in-flight promise. */
335
+ /** Test seam: restores complete process-local token and reconciliation state. */
300
336
  export function __resetVertexTokenCache(): void {
301
337
  tokenCache.clear();
302
338
  inflight.clear();
339
+ lastReconciledGeneration = 0;
340
+ liveSources = new Set();
303
341
  }