@bitkyc08/opencodex 2.82.0 → 2.84.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 (266) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/cursor/catalog.ts +2 -2
  10. package/src/adapters/cursor/effort-map.ts +1 -1
  11. package/src/adapters/devin/live-models.ts +2 -2
  12. package/src/adapters/google-truncation.ts +22 -0
  13. package/src/adapters/google.ts +7 -3
  14. package/src/adapters/ollama-native.ts +47 -15
  15. package/src/adapters/openai-chat/messages.ts +8 -4
  16. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  17. package/src/adapters/openai-responses/passthrough.ts +5 -0
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/request-strips.ts +10 -2
  21. package/src/adapters/qoder/adapter.ts +16 -14
  22. package/src/bridge/errors.ts +26 -1
  23. package/src/bridge.ts +1 -1
  24. package/src/claude/context-windows.ts +57 -4
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/intercept/connect-proxy.ts +10 -3
  27. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  28. package/src/claude/intercept/picker-listener.ts +49 -1
  29. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  30. package/src/claude/intercept/settings.ts +101 -9
  31. package/src/cli/account-api.ts +65 -0
  32. package/src/cli/account-auth.ts +44 -12
  33. package/src/cli/account.ts +41 -5
  34. package/src/cli/capabilities-access-remote.ts +8 -0
  35. package/src/cli/capabilities-base.ts +13 -1
  36. package/src/cli/catalog-command-result.ts +2 -1
  37. package/src/cli/claude.ts +15 -6
  38. package/src/cli/config-command.ts +8 -2
  39. package/src/cli/debug.ts +8 -0
  40. package/src/cli/dispatch.ts +16 -3
  41. package/src/cli/export-command.ts +14 -1
  42. package/src/cli/hub-disable.ts +99 -0
  43. package/src/cli/hub.ts +4 -2
  44. package/src/cli/index.ts +13 -4
  45. package/src/cli/link-child.ts +104 -0
  46. package/src/cli/link-enrollment.ts +96 -0
  47. package/src/cli/link.ts +39 -24
  48. package/src/cli/observe-stream.ts +6 -1
  49. package/src/cli/observe.ts +10 -2
  50. package/src/cli/provider.ts +12 -0
  51. package/src/cli/registry.ts +5 -4
  52. package/src/cli/resolve.ts +1 -1
  53. package/src/cli/runtime-api.ts +27 -2
  54. package/src/cli/status.ts +104 -20
  55. package/src/client/connect.ts +39 -4
  56. package/src/client/hub-client.ts +14 -7
  57. package/src/client/hub-state.ts +25 -14
  58. package/src/client/link-home.ts +248 -0
  59. package/src/client/link-management-relay.ts +61 -0
  60. package/src/client/link-relay.ts +6 -1
  61. package/src/client/link-status.ts +14 -0
  62. package/src/client/machine-api.ts +14 -7
  63. package/src/client/machine-listener.ts +29 -4
  64. package/src/clients/config-export/model-metadata.ts +16 -0
  65. package/src/clients/config-export/zcode-store.ts +46 -11
  66. package/src/clients/config-export/zcode.ts +3 -5
  67. package/src/clients/config-export.ts +47 -17
  68. package/src/clients/omo-role-models.ts +6 -3
  69. package/src/codex/agent-role-models.ts +26 -8
  70. package/src/codex/auth-api/account-list.ts +17 -2
  71. package/src/codex/auth-api/account-selection.ts +17 -7
  72. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  73. package/src/codex/auth-api/routes.ts +5 -1
  74. package/src/codex/auth-context.ts +8 -3
  75. package/src/codex/autostart-health.ts +3 -1
  76. package/src/codex/catalog/bundled.ts +5 -1
  77. package/src/codex/catalog/effort.ts +2 -2
  78. package/src/codex/catalog/metadata.ts +4 -2
  79. package/src/codex/catalog/native-models.ts +17 -3
  80. package/src/codex/catalog/provider-models.ts +25 -0
  81. package/src/codex/desktop-app/darwin.ts +1 -1
  82. package/src/codex/desktop-app/handoff.ts +1 -1
  83. package/src/codex/desktop-app/linux.ts +1 -1
  84. package/src/codex/desktop-app/lock.ts +1 -1
  85. package/src/codex/desktop-app/types.ts +1 -1
  86. package/src/codex/desktop-app/windows.ts +1 -1
  87. package/src/codex/desktop-app-restart.ts +1 -1
  88. package/src/codex/main-account.ts +6 -7
  89. package/src/codex/native-profile-startup.ts +4 -1
  90. package/src/codex/prompt-journal.ts +20 -2
  91. package/src/codex/prompt-layers/encoding.ts +49 -0
  92. package/src/codex/prompt-layers/import-source.ts +30 -0
  93. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  94. package/src/codex/prompt-layers/toml-read.ts +52 -4
  95. package/src/codex/prompt-layers.ts +383 -20
  96. package/src/codex/prompt-lock-claim.ts +13 -3
  97. package/src/codex/prompt-lock-evidence.ts +3 -3
  98. package/src/codex/prompt-lock-owner.ts +14 -6
  99. package/src/codex/prompt-text-probe.ts +34 -5
  100. package/src/codex/quota.ts +13 -6
  101. package/src/codex/role-auto-assign.ts +2 -5
  102. package/src/codex/routing/active-account.ts +21 -0
  103. package/src/codex/routing.ts +1 -1
  104. package/src/codex/runtime.ts +196 -8
  105. package/src/codex/shim-overlay.ts +8 -4
  106. package/src/codex/shim.ts +1 -1
  107. package/src/combos/failover.ts +3 -0
  108. package/src/config/api-key-reconcile.ts +34 -0
  109. package/src/config/diagnostics.ts +16 -0
  110. package/src/config/feature-flags.ts +5 -0
  111. package/src/config/live-reconcile.ts +43 -29
  112. package/src/config/live-topology.ts +29 -0
  113. package/src/config/load-degrade.ts +2 -1
  114. package/src/config/paths.ts +3 -0
  115. package/src/config/salvage.ts +3 -1
  116. package/src/config/schema/config-schema.ts +2 -0
  117. package/src/generated/compatibility-version.json +373 -217
  118. package/src/grok/grpc-web.ts +23 -1
  119. package/src/grok/inject.ts +245 -221
  120. package/src/grok/reset-coupon-ledger.ts +293 -57
  121. package/src/grok/reset-coupons.ts +15 -6
  122. package/src/integrations/cursor-detect.ts +1 -1
  123. package/src/integrations/mutation-plan.ts +13 -7
  124. package/src/integrations/registry.ts +45 -11
  125. package/src/integrations/state.ts +19 -7
  126. package/src/integrations/target.ts +42 -5
  127. package/src/integrations/writer.ts +14 -6
  128. package/src/lib/app-owned-memory-stores.ts +4 -0
  129. package/src/lib/app-owned-memory.ts +11 -0
  130. package/src/lib/errors.ts +25 -0
  131. package/src/lib/json-byte-size.ts +31 -0
  132. package/src/lib/optional-shutdown-hooks.ts +35 -0
  133. package/src/lib/outbound-credential-registry.ts +210 -0
  134. package/src/lib/passive-read.ts +35 -0
  135. package/src/lib/redact.ts +2 -2
  136. package/src/lib/shadow-call.ts +44 -5
  137. package/src/link/codex-settings.ts +155 -0
  138. package/src/link/management-routes.ts +89 -0
  139. package/src/link/remote-probe.ts +38 -0
  140. package/src/link/ssh-argv.ts +26 -5
  141. package/src/link/ssh-runner.ts +58 -27
  142. package/src/link/status-projection.ts +22 -4
  143. package/src/link/supervisor.ts +144 -49
  144. package/src/link/tunnel-state.ts +11 -4
  145. package/src/oauth/anthropic-continuity.ts +4 -3
  146. package/src/oauth/anthropic-identity.ts +13 -2
  147. package/src/oauth/anthropic-model-quota.ts +13 -1
  148. package/src/oauth/anthropic-routing.ts +230 -23
  149. package/src/oauth/anthropic.ts +18 -4
  150. package/src/oauth/google-antigravity.ts +24 -7
  151. package/src/oauth/index.ts +25 -16
  152. package/src/oauth/kiro-account-identity.ts +106 -0
  153. package/src/oauth/kiro.ts +48 -5
  154. package/src/oauth/meta-muse-device.ts +1 -1
  155. package/src/oauth/plan.ts +29 -0
  156. package/src/oauth/store.ts +40 -3
  157. package/src/oauth/types.ts +3 -1
  158. package/src/plugins/loader.ts +11 -0
  159. package/src/providers/anthropic-billing-ownership.ts +74 -0
  160. package/src/providers/anthropic-billing-recovery.ts +31 -0
  161. package/src/providers/anthropic-billing-refusal.ts +55 -0
  162. package/src/providers/anthropic-fast.ts +1 -1
  163. package/src/providers/anthropic-reset-admission.ts +62 -0
  164. package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
  165. package/src/providers/api-key-resolve.ts +17 -2
  166. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  167. package/src/providers/devin-provider-merge-migration.ts +1 -1
  168. package/src/providers/github-copilot-auto.ts +169 -0
  169. package/src/providers/key-failover.ts +52 -5
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/model-rename-startup.ts +5 -3
  172. package/src/providers/muse-subscription-usage.ts +1 -1
  173. package/src/providers/quota/account-cache.ts +11 -0
  174. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  175. package/src/providers/quota/report-cache.ts +14 -0
  176. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  177. package/src/providers/quota.ts +4 -1
  178. package/src/providers/registry/entries-core.ts +20 -20
  179. package/src/providers/registry/entries-extended.ts +4 -1
  180. package/src/providers/registry/model-seeds.ts +20 -3
  181. package/src/providers/request-pacing.ts +9 -0
  182. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  183. package/src/providers/xai-fast-model.ts +1 -1
  184. package/src/responses/compaction.ts +14 -0
  185. package/src/responses/parser.ts +7 -1
  186. package/src/server/auth-cors.ts +42 -0
  187. package/src/server/configured-credentials.ts +194 -0
  188. package/src/server/inbound-body-admission.ts +24 -7
  189. package/src/server/index/link-listener.ts +30 -17
  190. package/src/server/index/link-management.ts +58 -0
  191. package/src/server/index/optional-listeners.ts +5 -1
  192. package/src/server/index/serve-options.ts +21 -2
  193. package/src/server/index.ts +1 -1
  194. package/src/server/lifecycle.ts +33 -5
  195. package/src/server/management/agent-settings-routes.ts +28 -10
  196. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  197. package/src/server/management/codex-agent-role-routes.ts +19 -6
  198. package/src/server/management/codex-prompt-routes.ts +136 -5
  199. package/src/server/management/codex-role-auto-assign.ts +1 -1
  200. package/src/server/management/grok-coupon-routes.ts +333 -57
  201. package/src/server/management/link-enrollment.ts +192 -0
  202. package/src/server/management/link-route-state.ts +185 -0
  203. package/src/server/management/link-routes.ts +92 -390
  204. package/src/server/management/model-rows.ts +3 -1
  205. package/src/server/management/provider-overwrite-carry.ts +10 -0
  206. package/src/server/management/provider-routes.ts +1 -0
  207. package/src/server/management/route-registry.ts +7 -5
  208. package/src/server/management/shared.ts +3 -0
  209. package/src/server/management-api.ts +3 -1
  210. package/src/server/messages-native.ts +68 -42
  211. package/src/server/relay-eager.ts +4 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-failure-attribution.ts +3 -0
  214. package/src/server/request-log-filter.ts +6 -1
  215. package/src/server/request-log-terminal-status.ts +243 -0
  216. package/src/server/request-log.ts +15 -118
  217. package/src/server/responses/adapter-continuation.ts +9 -1
  218. package/src/server/responses/adapter-dispatch.ts +293 -81
  219. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  220. package/src/server/responses/codex-ws-exchange.ts +147 -6
  221. package/src/server/responses/codex-ws-wire.ts +203 -0
  222. package/src/server/responses/collaboration.ts +36 -7
  223. package/src/server/responses/compact.ts +129 -50
  224. package/src/server/responses/context-overflow.ts +13 -1
  225. package/src/server/responses/core-codex-account.ts +105 -1
  226. package/src/server/responses/core-combo-failure.ts +14 -0
  227. package/src/server/responses/core-combo.ts +7 -2
  228. package/src/server/responses/core-errors.ts +29 -1
  229. package/src/server/responses/core-opaque-recovery.ts +5 -5
  230. package/src/server/responses/core-options.ts +2 -0
  231. package/src/server/responses/core.ts +6 -6
  232. package/src/server/responses/deferred-collab-surface.ts +158 -0
  233. package/src/server/responses/deferred-send-reporter.ts +25 -0
  234. package/src/server/responses/empty-completion-guard.ts +18 -2
  235. package/src/server/responses/fetch-helpers.ts +12 -1
  236. package/src/server/responses/jev-model-invoke.ts +1 -1
  237. package/src/server/responses/passthrough-delivery.ts +4 -1
  238. package/src/server/responses/passthrough-dispatch.ts +130 -64
  239. package/src/server/responses/passthrough-execution.ts +3 -3
  240. package/src/server/responses/request-prepare.ts +6 -4
  241. package/src/server/responses/request-transport.ts +131 -13
  242. package/src/server/responses/run-turn-execution.ts +13 -5
  243. package/src/server/responses/sidecar-execution.ts +16 -2
  244. package/src/server/responses/spend-storage-error.ts +33 -0
  245. package/src/server/responses/ws-upstream.ts +9 -1
  246. package/src/server/startup-action-control.ts +22 -2
  247. package/src/server/startup-health-cache.ts +4 -0
  248. package/src/server/system-env-shell.ts +7 -2
  249. package/src/server/system-env.ts +11 -1
  250. package/src/service/cli.ts +5 -1
  251. package/src/service/desktop-startup.ts +147 -34
  252. package/src/service/desktop-supervision.d.mts +27 -1
  253. package/src/service/desktop-supervision.mjs +111 -5
  254. package/src/service/health-sentinel-launch.ts +176 -0
  255. package/src/service/health-sentinel-main.ts +447 -0
  256. package/src/service/health-sentinel-policy.ts +241 -0
  257. package/src/service/health-sentinel-probe.ts +54 -0
  258. package/src/service/health-sentinel-windows.ts +87 -0
  259. package/src/service/linux-linger.ts +20 -0
  260. package/src/types/config.ts +5 -0
  261. package/src/types/request.ts +5 -0
  262. package/src/usage/expected-prices.ts +5 -1
  263. package/src/usage/log.ts +19 -1
  264. package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
  265. package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
  266. package/gui/dist/assets/index-D3FBCNiP.js +0 -94
@@ -0,0 +1,85 @@
1
+ import { sanitizeLogMetadataString } from "../../lib/redact";
2
+ import { isOpenAiOperatedResponsesDestination } from "../../providers/openai-tiers";
3
+ import { normalizedProviderEndpoint, PROVIDER_REGISTRY } from "../../providers/registry";
4
+ import type { OcxProviderConfig } from "../../types";
5
+
6
+ /**
7
+ * #6675: a custom Responses provider drops replayed plaintext reasoning unless it sets
8
+ * `preserveResponsesReasoningContent`. That default is deliberate (the ChatGPT backend rejects
9
+ * echoed raw reasoning, and forwarding it costs tokens), but it is silent, and the similarly named
10
+ * Chat-wire `preserveReasoningContentModels` does not opt in. Say so once per destination.
11
+ *
12
+ * Only a hand-configured `openai-responses` row with key auth qualifies. Registry presets (matched
13
+ * by endpoint on either OpenAI wire, since a preset may be routed to Responses per model), OAuth and
14
+ * forward rows, Azure, and OpenAI-operated destinations are skipped: their replay contract is
15
+ * curated, so blanking there is a decision rather than a missing setting. The notice carries only
16
+ * the destination host and option names, never request content.
17
+ */
18
+ const MAX_NOTICED_DESTINATIONS = 64;
19
+ const noticedDestinations = new Set<string>();
20
+
21
+ function destinationHost(baseUrl: unknown): string | undefined {
22
+ if (typeof baseUrl !== "string") return undefined;
23
+ try {
24
+ return sanitizeLogMetadataString(new URL(baseUrl).host, 128);
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
29
+
30
+ /** Azure OpenAI and Azure AI Foundry serve OpenAI-operated models under per-resource hosts. */
31
+ const AZURE_OPENAI_HOST_SUFFIXES = [".openai.azure.com", ".services.ai.azure.com", ".cognitiveservices.azure.com"];
32
+
33
+ function hostnameOf(url: string): string | undefined {
34
+ try {
35
+ return new URL(url).hostname.toLowerCase();
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ /** A templated preset (`https://{resource}.openai.azure.com/...`) matches by host pattern. */
42
+ function templatedHostMatches(template: string, hostname: string): boolean {
43
+ const host = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i.exec(template)?.[1];
44
+ if (!host || !/\{[^}]*\}/.test(host)) return false;
45
+ const pattern = host.toLowerCase().split(/\{[^}]*\}/).map(part => part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("[^.]+");
46
+ return new RegExp(`^${pattern}$`).test(hostname);
47
+ }
48
+
49
+ function isRegistryDestination(baseUrl: string): boolean {
50
+ const endpoint = normalizedProviderEndpoint(baseUrl);
51
+ const hostname = hostnameOf(baseUrl);
52
+ if (hostname && AZURE_OPENAI_HOST_SUFFIXES.some(suffix => hostname.endsWith(suffix))) return true;
53
+ return PROVIDER_REGISTRY.some(entry =>
54
+ normalizedProviderEndpoint(entry.baseUrl) === endpoint
55
+ || (hostname !== undefined && templatedHostMatches(entry.baseUrl, hostname))
56
+ || (entry.destinationAliases ?? []).some(alias => normalizedProviderEndpoint(alias.baseUrl) === endpoint));
57
+ }
58
+
59
+ function isCustomResponsesProvider(provider: OcxProviderConfig): boolean {
60
+ if (provider.adapter !== "openai-responses") return false;
61
+ if (provider.authMode !== undefined && provider.authMode !== "key") return false;
62
+ if (typeof provider.baseUrl !== "string" || !provider.baseUrl) return false;
63
+ return !isOpenAiOperatedResponsesDestination(provider) && !isRegistryDestination(provider.baseUrl);
64
+ }
65
+
66
+ export function plaintextReasoningDroppedNotice(host: string): string {
67
+ return `[opencodex] Replayed plaintext reasoning was dropped for the custom Responses provider at ${host}. `
68
+ + "If that endpoint accepts reasoning_text replay, set \"preserveResponsesReasoningContent\": true on the provider; "
69
+ + "\"preserveReasoningContentModels\" applies only to the Chat wire.";
70
+ }
71
+
72
+ export function notePlaintextReasoningDropped(
73
+ provider: OcxProviderConfig,
74
+ warn: (message: string) => void = message => console.warn(message),
75
+ ): void {
76
+ if (!isCustomResponsesProvider(provider)) return;
77
+ const host = destinationHost(provider.baseUrl);
78
+ if (!host || noticedDestinations.has(host) || noticedDestinations.size >= MAX_NOTICED_DESTINATIONS) return;
79
+ noticedDestinations.add(host);
80
+ warn(plaintextReasoningDroppedNotice(host));
81
+ }
82
+
83
+ export function resetPlaintextReasoningNoticesForTests(): void {
84
+ noticedDestinations.clear();
85
+ }
@@ -78,6 +78,11 @@ export function sanitizeReasoningInputContent(
78
78
  * placeholder, rather than emitted in the shape the upstream 400s on.
79
79
  */
80
80
  requirePlaintextReasoning?: boolean;
81
+ /**
82
+ * Called once per sanitized body when a reasoning item that carried nonempty plaintext
83
+ * `reasoning_text` was blanked. Observability only; it never changes the body (#6675).
84
+ */
85
+ onPlaintextReasoningBlanked?: () => void;
81
86
  },
82
87
  ): unknown {
83
88
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
@@ -85,6 +90,7 @@ export function sanitizeReasoningInputContent(
85
90
  if (!Array.isArray(raw.input)) return body;
86
91
 
87
92
  let changed = false;
93
+ let blankedPlaintext = false;
88
94
  const input = raw.input.map(item => {
89
95
  if (!item || typeof item !== "object" || Array.isArray(item)) return item;
90
96
  const rec = item as Record<string, unknown>;
@@ -148,11 +154,15 @@ export function sanitizeReasoningInputContent(
148
154
  // guide merges reasoning items into the adjacent assistant message), so providers flagged
149
155
  // `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
150
156
  // continuations after tool calls (issue #875 family).
151
- if (blankContent) next.content = [];
157
+ if (blankContent) {
158
+ if (hasPlaintextReasoningContent(rec.content)) blankedPlaintext = true;
159
+ next.content = [];
160
+ }
152
161
  if (missingPlaintextReasoning) next.content = plaintextReasoningBackfill(next);
153
162
  return next;
154
163
  });
155
164
 
165
+ if (blankedPlaintext) opts?.onPlaintextReasoningBlanked?.();
156
166
  return changed ? { ...raw, input } : body;
157
167
  }
158
168
 
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
2
+ import { COMPACT_PROMPT, appendCheckpointContinuationNote, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
3
3
  import { debugProviderDiagnostic } from "../../lib/debug";
4
4
  import { modelInList } from "../../types";
5
5
  import { isPlainObject } from "./internal";
@@ -260,10 +260,12 @@ export function scrubOcxCompactionItems(
260
260
  body: unknown,
261
261
  destinationDecodesNativeBlob: boolean,
262
262
  threadServingIdentityChanged: boolean,
263
+ addContinuationNote = false,
263
264
  ): unknown {
264
265
  if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
265
266
 
266
267
  let changed = false;
268
+ let decodedCheckpoint = false;
267
269
  const input = body.input.map(item => {
268
270
  if (!isPlainObject(item) || !isCompactionItemType(item.type)) return item;
269
271
  const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : undefined;
@@ -274,6 +276,7 @@ export function scrubOcxCompactionItems(
274
276
  && !threadServingIdentityChanged
275
277
  ) return item;
276
278
  changed = true;
279
+ if (decodeCompactionSummary(encrypted) !== null) decodedCheckpoint = true;
277
280
  return {
278
281
  type: "message",
279
282
  role: "user",
@@ -281,5 +284,10 @@ export function scrubOcxCompactionItems(
281
284
  };
282
285
  });
283
286
 
284
- return changed ? { ...body, input } : body;
287
+ if (!changed) return body;
288
+ // #6764: a decoded checkpoint is plain user text from here on, so name its role in the
289
+ // instructions. Compaction requests skip this; the summarizer must not be told to continue.
290
+ return addContinuationNote && decodedCheckpoint
291
+ ? { ...body, input, instructions: appendCheckpointContinuationNote(body.instructions) }
292
+ : { ...body, input };
285
293
  }
@@ -5,7 +5,7 @@ import { mapReasoningEffort } from "../../reasoning-effort";
5
5
  import { buildSystemPrompt, MAX_TOOL_BLOCK_STARTS } from "../coding-agent/protocol";
6
6
  import { buildCodingAgentToolCatalog, CodingAgentToolCatalogError } from "../coding-agent/tool-catalog";
7
7
  import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type CodingAgentToolBridgeInput } from "../coding-agent/turn";
8
- import { QODER_PROFILES, type QoderProfile } from "./profiles";
8
+ import { QODER_PROFILES, resolveQoderProfile, type QoderProfile } from "./profiles";
9
9
  import { QoderScaffoldFilter, QODER_SCAFFOLD_ERROR_CODE, qoderScaffoldErrorMessage } from "./scaffold-guard";
10
10
 
11
11
  export type QoderAdapterDeps = CodingAgentDeps;
@@ -114,19 +114,21 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
114
114
  yield { type: "error", message: "Qoder adapter uses runTurn; the fetch/parseStream path is disabled." };
115
115
  },
116
116
  async runTurn(parsed, incoming, emit): Promise<void> {
117
- const hasImage = parsed.context.messages.some(message =>
118
- Array.isArray(message.content) && message.content.some(part => part.type === "image"),
119
- );
120
- if (hasImage) {
121
- emit({
122
- type: "error",
123
- message: "Qoder image input is not enabled because the CLI provider route has no verified multimodal contract.",
124
- status: 400,
125
- errorType: "invalid_request_error",
126
- code: "unsupported_input_modality",
127
- retryable: false,
128
- });
129
- return;
117
+ if (resolveQoderProfile(provider.baseUrl)?.region === "cn") {
118
+ const hasImage = parsed.context.messages.some(message =>
119
+ Array.isArray(message.content) && message.content.some(part => part.type === "image"),
120
+ );
121
+ if (hasImage) {
122
+ emit({
123
+ type: "error",
124
+ message: "Qoder image input is not enabled because the CLI provider route has no verified multimodal contract.",
125
+ status: 400,
126
+ errorType: "invalid_request_error",
127
+ code: "unsupported_input_modality",
128
+ retryable: false,
129
+ });
130
+ return;
131
+ }
130
132
  }
131
133
  let catalog: ReturnType<typeof buildCodingAgentToolCatalog>;
132
134
  try {
@@ -12,6 +12,7 @@ import {
12
12
  cyberPolicyErrorType,
13
13
  CYBER_POLICY_ERROR_CODE,
14
14
  isCyberPolicyCode,
15
+ SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE,
15
16
  type OcxErrorPayload,
16
17
  } from "../lib/errors";
17
18
 
@@ -22,6 +23,10 @@ export function formatErrorResponse(
22
23
  options?: { code?: string | null; retryAfter?: string | null },
23
24
  ): Response {
24
25
  const error = classifyError(status, type, message);
26
+ if (options?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) {
27
+ error.type = "server_error";
28
+ error.code = SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
29
+ }
25
30
  if (isCyberPolicyCode(options?.code)) {
26
31
  error.code = CYBER_POLICY_ERROR_CODE;
27
32
  error.type = cyberPolicyErrorType(type);
@@ -36,7 +41,8 @@ export function formatErrorResponse(
36
41
  // that would hand the client a 5xx it is configured to retry four times.
37
42
  const finalStatus = error.code === CYBER_POLICY_ERROR_CODE
38
43
  ? 400
39
- : isReplayRefusalCode(error.code) ? REPLAY_REFUSED_STATUS : status;
44
+ : error.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503
45
+ : isReplayRefusalCode(error.code) ? REPLAY_REFUSED_STATUS : status;
40
46
  const headers = new Headers({ "Content-Type": "application/json" });
41
47
  const retryAfter = options?.retryAfter?.trim();
42
48
  if (error.code !== CYBER_POLICY_ERROR_CODE
@@ -62,3 +68,22 @@ export function formatErrorResponse(
62
68
  if (refusal) retainReplayRefusal(response);
63
69
  return response;
64
70
  }
71
+
72
+ /**
73
+ * A spent usage window, in the shape the Codex client treats as final (#6991): HTTP 429 with
74
+ * `error.type: "usage_limit_reached"` and, when the reset is known, `resets_at` in epoch
75
+ * seconds. Codex stops (and shows the reset time when present) instead of backing off through its
76
+ * retry budget, which is what it does with `rate_limit_exceeded`. No `plan_type` is sent: Codex
77
+ * would read it as a ChatGPT plan and print that plan's upgrade advice. `formatErrorResponse`
78
+ * cannot build this, because every 429 it classifies becomes `rate_limit_exceeded` and its
79
+ * payload has no `resets_at`.
80
+ */
81
+ export function formatUsageLimitReachedResponse(message: string, resetsAtMs: number | null, retryAfterSeconds: number | null): Response {
82
+ const error = {
83
+ message, type: "usage_limit_reached", code: "usage_limit_reached",
84
+ ...(resetsAtMs !== null ? { resets_at: Math.ceil(resetsAtMs / 1000) } : {}),
85
+ };
86
+ const headers = new Headers({ "Content-Type": "application/json" });
87
+ if (retryAfterSeconds !== null) headers.set("Retry-After", String(retryAfterSeconds));
88
+ return new Response(JSON.stringify({ error }), { status: 429, headers });
89
+ }
package/src/bridge.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { adapterFailureFromMessage } from "./lib/errors";
2
2
 
3
- export { formatErrorResponse } from "./bridge/errors";
3
+ export { formatErrorResponse, formatUsageLimitReachedResponse } from "./bridge/errors";
4
4
  export { setOwnedBudgetAbandonedMsForTests } from "./bridge/internal";
5
5
  export { buildResponseJSON } from "./bridge/response-json";
6
6
  export { bridgeToResponsesSSE } from "./bridge/sse";
@@ -10,7 +10,7 @@
10
10
  * adapter-reported CatalogModel.contextWindow / the Anthropic registry for passthrough ids);
11
11
  * nothing is guessed.
12
12
  */
13
- import { aliasForNative, aliasForRoute, currentClaudeAliasSpelling, legacyAliasForNative, legacyAliasForRoute } from "./alias";
13
+ import { aliasForNative, aliasForRoute, currentClaudeAliasSpelling, legacyAliasForNative, legacyAliasForRoute, resolveAlias } from "./alias";
14
14
  import { desktop3pAlias } from "./desktop-3p";
15
15
  import { nativeOpenAiContextWindow, type CatalogModel, type NativeContextLimitsInput } from "../codex/catalog";
16
16
  import { ANTHROPIC_MODEL_CONTEXT_WINDOWS } from "../providers/registry/model-seeds";
@@ -20,13 +20,13 @@ import { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR, ONE_MILLION } from "./
20
20
  export { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR } from "./long-context";
21
21
 
22
22
  /**
23
- * The native id each Claude Code tier alias resolves to (2.1.282: `--model opus` sends
24
- * `claude-opus-5-5`, `sonnet` sends `claude-sonnet-5`, `fable` sends `claude-fable-5-1`).
23
+ * The native id each Claude Code tier alias resolves to (2.1.296: `--model opus` sends
24
+ * `claude-opus-5-5`, `sonnet` sends `claude-sonnet-5-5`, `fable` sends `claude-fable-5-1`).
25
25
  * Behind a gateway Claude Code accounts an unmarked id at 200k, so an unset tier slot is filled
26
26
  * with this id when the map marks it (#5755). A marked alias cannot stand in: Claude Code sends
27
27
  * `sonnet[1m]` upstream unresolved. Haiku has no entry; its 200k window never marks.
28
28
  */
29
- const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5", fable: "claude-fable-5-1" } as const;
29
+ const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5-5", fable: "claude-fable-5-1" } as const;
30
30
 
31
31
  /** Binary-verified accepted range for CLAUDE_CODE_AUTO_COMPACT_WINDOW (2.1.207: pSo=1e5, yDs=1e6). */
32
32
  export const AUTO_COMPACT_WINDOW_MIN = 100_000;
@@ -277,6 +277,59 @@ export function effectiveModelEnv(
277
277
  return out;
278
278
  }
279
279
 
280
+ /**
281
+ * Tier slots Claude Code pairs with a `<slot>_NAME` label (#6926). Its /model tier row is
282
+ * `label: <slot>_NAME ?? <first-party display name> ?? <slot value>` (2.1.296), and an
283
+ * `ocx-claude-*` alias is not a first-party id, so without the label the row prints the alias.
284
+ * The alias itself stays the submitted id: saved selections and inbound resolution depend on it.
285
+ */
286
+ export const CLAUDE_TIER_LABEL_SLOTS = [
287
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
288
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
289
+ "ANTHROPIC_DEFAULT_FABLE_MODEL",
290
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
291
+ ] as const;
292
+
293
+ export type ClaudeTierLabelSlot = (typeof CLAUDE_TIER_LABEL_SLOTS)[number];
294
+
295
+ export function isClaudeTierLabelSlot(name: string): name is ClaudeTierLabelSlot {
296
+ return (CLAUDE_TIER_LABEL_SLOTS as readonly string[]).includes(name);
297
+ }
298
+
299
+ /**
300
+ * The picker label for a tier slot holding an OpenCodex alias, spelled like the discovery
301
+ * row's `display_name` (model-info.ts): `<slug> (native)`, `<model> (<provider>)` and a
302
+ * ` · 1M` variant. The native-namespaced `claude-*` selector is the Anthropic Fable picker
303
+ * alias, which discovery labels `(anthropic)`. A trailing `--fast` stays in the model name:
304
+ * without the catalog this cannot tell a synthetic Fast sibling from a real model whose id ends
305
+ * in `--fast`, and naming the exact model beats a prettier label that could name the wrong one.
306
+ * Anything that is not an alias (a first-party id, `provider/model`, a bare slug) gets no
307
+ * label: Claude Code either names it itself or the id is already what the user typed.
308
+ */
309
+ export function claudeTierDisplayName(selector: string): string | undefined {
310
+ const trimmed = selector.trim();
311
+ const oneMillion = hasOneMillionMarker(trimmed);
312
+ const route = resolveAlias(stripOneMillionMarker(trimmed));
313
+ if (route === null) return undefined;
314
+ const slash = route.indexOf("/");
315
+ let provider = slash > 0 ? route.slice(0, slash) : "native";
316
+ const model = slash > 0 ? route.slice(slash + 1) : route;
317
+ if (!model) return undefined;
318
+ if (provider === "native" && model.startsWith("claude-")) provider = "anthropic";
319
+ return `${model} (${provider})${oneMillion ? " · 1M" : ""}`;
320
+ }
321
+
322
+ /** `{ "<slot>_NAME": label }` for each tier slot in `env` that holds an OpenCodex alias. */
323
+ export function claudeTierDisplayNameEnv(env: Readonly<Record<string, string | undefined>>): Record<string, string> {
324
+ const out: Record<string, string> = {};
325
+ for (const slot of CLAUDE_TIER_LABEL_SLOTS) {
326
+ const value = env[slot];
327
+ const label = value ? claudeTierDisplayName(value) : undefined;
328
+ if (label !== undefined) out[`${slot}_NAME`] = label;
329
+ }
330
+ return out;
331
+ }
332
+
280
333
  /** Shared 3s bounded acquisition (audit R4#3) for context-window sources. */
281
334
  export async function boundedContextWindows(
282
335
  acquire: () => Promise<Record<string, number>>,
@@ -2,7 +2,7 @@
2
2
  * Claude Desktop picker mode: the one server-side controller for every picker mutation.
3
3
  *
4
4
  * While a server runs, enable, disable and Desktop mode transitions run here, serialized by one
5
- * lock (devlog/_plan/260924_claude_desktop_picker_mode/030_wp4_picker_activation.md, D11).
5
+ * lock (devlog/_fin/260924_claude_desktop_picker_mode/030_wp4_picker_activation.md, D11).
6
6
  */
7
7
  import type { OcxConfig } from "../types";
8
8
  import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
@@ -113,12 +113,19 @@ export function isLoopbackTarget(host: string): boolean {
113
113
  return /^(0x[0-9a-f]+|\d+)(\.(0x[0-9a-f]+|\d+))*$/.test(host);
114
114
  }
115
115
 
116
- function respond(socket: Socket, status: number, reason: string, headers: Readonly<Record<string, string>> = {}): void {
116
+ function respond(
117
+ socket: Socket, status: number, reason: string, headers: Readonly<Record<string, string>> = {}, body = "",
118
+ ): void {
117
119
  if (socket.destroyed) return;
118
120
  const extra = Object.entries(headers).map(([name, value]) => `${name}: ${value}\r\n`).join("");
119
- socket.end(`HTTP/1.1 ${status} ${reason}\r\n${extra}Connection: close\r\nContent-Length: 0\r\n\r\n`);
121
+ const type = body ? "Content-Type: text/plain; charset=utf-8\r\n" : "";
122
+ socket.end(`HTTP/1.1 ${status} ${reason}\r\n${extra}${type}Connection: close\r\nContent-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
120
123
  }
121
124
 
125
+ // Plain proxied HTTP (often loopback MCP traffic missing a NO_PROXY bypass) lands here; say why.
126
+ const CONNECT_ONLY_BODY = "OpenCodex Claude intercept proxy accepts only CONNECT tunnels. "
127
+ + "Exclude this host via NO_PROXY (loopback and unspecified hosts: 127.0.0.1,localhost,::1,0.0.0.0).\n";
128
+
122
129
  function splice(client: Socket, upstream: Socket, pending: Uint8Array): void {
123
130
  const teardown = () => {
124
131
  client.destroy();
@@ -173,7 +180,7 @@ function handleConnection(socket: Socket, options: ResolvedConnectProxyOptions):
173
180
  // Bytes after the head belong to the tunnel (a client may pipeline its TLS ClientHello).
174
181
  let pending = head.subarray(end + 4);
175
182
  if (!target) {
176
- respond(socket, 405, "Method Not Allowed");
183
+ respond(socket, 405, "Method Not Allowed", { Allow: "CONNECT" }, CONNECT_ONLY_BODY);
177
184
  return;
178
185
  }
179
186
  if (options.authToken !== undefined) {
@@ -22,6 +22,7 @@ export function canonicalPickerConfigDir(configDir: string): string {
22
22
  /** Public records still cannot follow symlinks or accept oversized/malformed recovery data. */
23
23
  function readState(path: string, configId: string, initialization: boolean): Initialization | AuthorityMetadata | null {
24
24
  // BigInt stats: a Windows file ID exceeds 2^53, so Number ino loses low bits and two files can compare equal.
25
+ // Refuse zero identity or metadata changes between the path snapshot and opened descriptor.
25
26
  let expected: BigIntStats;
26
27
  try { expected = lstatSync(path, { bigint: true }); }
27
28
  catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw new Error("picker_ca_metadata_unsafe"); }
@@ -31,7 +32,8 @@ function readState(path: string, configId: string, initialization: boolean): Ini
31
32
  catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw new Error("picker_ca_metadata_unsafe"); }
32
33
  try {
33
34
  const stat = fstatSync(fd, { bigint: true });
34
- if (stat.dev !== expected.dev || stat.ino !== expected.ino) throw new Error("picker_ca_metadata_unsafe");
35
+ if (stat.dev !== expected.dev || stat.ino !== expected.ino || stat.ino === 0n
36
+ || stat.ctimeNs !== expected.ctimeNs) throw new Error("picker_ca_metadata_unsafe");
35
37
  if (!stat.isFile() || stat.nlink !== 1n || stat.size > BigInt(MAX_PUBLIC_STATE_BYTES)
36
38
  || (process.platform !== "win32" && stat.uid !== BigInt(process.getuid!()))) throw new Error();
37
39
  const bytes = Buffer.alloc(MAX_PUBLIC_STATE_BYTES + 1);
@@ -34,6 +34,11 @@ export interface PickerListenerOptions {
34
34
  maxEncodedBytes?: number;
35
35
  /** Test seam: concurrent upstream requests; production uses PICKER_MAX_ACTIVE_UPSTREAMS. */
36
36
  maxActiveUpstreams?: number;
37
+ /**
38
+ * Test seam: arms the drop of a connection whose backend ended, after PICKER_ABANDONED_INPUT_LINGER_MS;
39
+ * returns a cancel function. Production uses a timer.
40
+ */
41
+ scheduleAbandonedInputDrop?: (drop: () => void, ms: number) => () => void;
37
42
  log?: (line: string) => void;
38
43
  }
39
44
  export interface PickerListenerHandle { port: number; close(): Promise<void> }
@@ -51,6 +56,12 @@ export const PICKER_MAX_ACTIVE_UPSTREAMS = 256;
51
56
  export const PICKER_MAX_CONCURRENT_STREAMS = 100;
52
57
  /** A connection that has not sent a complete ClientHello by then is dropped. */
53
58
  const CLIENT_HELLO_TIMEOUT_MS = 10_000;
59
+ /**
60
+ * Once the backend has ended a connection and its reply has been flushed, input the client still
61
+ * sends is discarded for at most this long before the connection is dropped. A cooperative client
62
+ * closes as soon as it reads a Connection: close reply; this only bounds one that keeps uploading.
63
+ */
64
+ export const PICKER_ABANDONED_INPUT_LINGER_MS = 30_000;
54
65
 
55
66
  const HOP_HEADERS = new Set([
56
67
  "connection", "keep-alive", "proxy-connection", "proxy-authenticate", "proxy-authorization",
@@ -152,6 +163,10 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
152
163
  const upstream = options.upstream ?? { host: "claude.ai", port: 443, servername: "claude.ai" };
153
164
  const cap = options.maxEncodedBytes ?? BOOTSTRAP_MAX_ENCODED_BYTES;
154
165
  const maxActiveUpstreams = options.maxActiveUpstreams ?? PICKER_MAX_ACTIVE_UPSTREAMS;
166
+ const scheduleAbandonedInputDrop = options.scheduleAbandonedInputDrop ?? ((drop: () => void, ms: number) => {
167
+ const timer = setTimeout(drop, ms);
168
+ return () => clearTimeout(timer);
169
+ });
155
170
  let activeUpstreams = 0;
156
171
  const upgrades = new Set<Duplex>();
157
172
  const spliced = new Set<Socket>();
@@ -205,8 +220,18 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
205
220
  } else req.destroy();
206
221
  }
207
222
  };
223
+ // An HTTP/1.1 reply sent while the upload is unfinished advertises Connection: close. Bun 1.4.3
224
+ // ends such a response with socket.destroySoon() before our finish listeners run, which resets
225
+ // the splice before the reply reaches the client; keep it a graceful FIN that discards input.
226
+ // Only that closing socket is changed, and destroy() stays available for errors and shutdown.
227
+ const closesUnfinishedUpload = () => !h2 && uploading && !req.complete;
228
+ const keepClosingReplyIntact = () => {
229
+ const socket = req.socket;
230
+ socket.destroySoon = () => { req.resume(); socket.end(); return socket; };
231
+ };
208
232
  // Refusals answer with an empty response and a fixed log line that carries no request data.
209
233
  const refuse = (status: 400 | 503) => {
234
+ if (closesUnfinishedUpload()) keepClosingReplyIntact();
210
235
  // Do not leave a refused, still-uploading stream behind after delivering its empty reply.
211
236
  if (uploading && !req.complete) {
212
237
  req.once("error", () => res.destroy());
@@ -236,6 +261,7 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
236
261
  if (clientGone) return;
237
262
  if (res.headersSent) res.destroy();
238
263
  else {
264
+ if (closesUnfinishedUpload()) keepClosingReplyIntact();
239
265
  res.writeHead(502, { "Content-Length": "0", ...(!h2 && uploading && !req.complete ? { Connection: "close" } : {}) });
240
266
  res.end(); log(502);
241
267
  }
@@ -260,7 +286,10 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
260
286
  // HTTP/2 has no reason phrase; its compat writeHead takes the same flat raw header array.
261
287
  if (h2) (relayRes as Http2ServerResponse).writeHead(status, raw as unknown as Record<string, string>);
262
288
  else {
263
- if (uploading && !req.complete) raw.push("Connection", "close");
289
+ if (uploading && !req.complete) {
290
+ keepClosingReplyIntact();
291
+ raw.push("Connection", "close");
292
+ }
264
293
  res.writeHead(status, upRes.statusMessage, raw);
265
294
  }
266
295
  log(status);
@@ -438,6 +467,8 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
438
467
  const offersH2 = clientHelloOffersH2(head);
439
468
  if (offersH2 === null) return;
440
469
  client.off("data", onData);
470
+ // Keep reading an aborted upload after the backend reply FIN.
471
+ client.allowHalfOpen = true;
441
472
  // Paused, a FIN or later bytes wait in the readable buffer and follow `head` once piped.
442
473
  client.pause();
443
474
  clearTimeout(deadline);
@@ -450,6 +481,23 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
450
481
  // its peer (reset, dial failure) tears the peer down instead of leaving it open.
451
482
  inner.once("close", () => { spliced.delete(inner); if (!client.writableEnded) client.destroy(); });
452
483
  client.once("close", () => { if (!inner.writableEnded) inner.destroy(); });
484
+ // The backend reply ended; discard ciphertext still coming from the aborted upload.
485
+ // Unpipe before resuming, so backpressure cannot hide the peer FIN. The discard is bounded
486
+ // from the moment the reply has been flushed to the socket (finish), never before, so the
487
+ // drop cannot cut a reply still being written; finish is not peer receipt, so a peer that
488
+ // also stops reading for the whole budget can lose unread bytes. A peer that keeps sending
489
+ // past the budget is dropped instead of holding the connection until shutdown (#6984).
490
+ inner.once("end", () => {
491
+ client.unpipe(inner);
492
+ client.resume();
493
+ const linger = () => {
494
+ if (client.destroyed) return;
495
+ const cancel = scheduleAbandonedInputDrop(() => client.destroy(), PICKER_ABANDONED_INPUT_LINGER_MS);
496
+ client.once("close", cancel);
497
+ };
498
+ if (client.writableFinished) linger();
499
+ else client.once("finish", linger);
500
+ });
453
501
  inner.once("connect", () => {
454
502
  inner.setNoDelay(true);
455
503
  client.setNoDelay(true);