@bitkyc08/opencodex 2.65.0 → 2.66.0-preview.20260925

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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-BVjA0T6O.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
  5. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  6. package/gui/dist/assets/index-vO4dgfiU.js +86 -0
  7. package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-DGLte6IR.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shadow-plan comparison (PF-12, `protocols.rollout.shadowPlan`): does the dispatch-basis plan
3
+ * for a request agree with what the request actually did?
4
+ *
5
+ * LEAF MODULE (see `contract.ts`) and PURE. It compares two finished records and nothing else:
6
+ * no second request, no config read, no state. The caller (`trace.ts`) contains any throw.
7
+ *
8
+ * What is compared, and why only that:
9
+ *
10
+ * - The candidate is the one the request settled on (provider and model of the final route),
11
+ * falling back to the plan's first eligible candidate. A combo that failed over is compared
12
+ * against the target that answered, not against the first one the plan lists.
13
+ * - Mode, upstream wire and request path. The response path is not compared: with
14
+ * `directEncoders` on, the client side leaves the internal Responses hop while the request
15
+ * side keeps it, and the planner does not model the encoder switch.
16
+ * - A Messages request forwarded with the caller's own Anthropic credential is native by the
17
+ * caller's choice; the plan says so with `caller-credential-required` and never predicts it.
18
+ * - A blocked trace agrees with a blocked plan. A compatibility reject is Claude-integration
19
+ * policy the planner does not model, so it is not compared.
20
+ */
21
+ import type { ProtocolHop } from "./contract";
22
+ import type { ProtocolPlanCandidateV1, ProtocolPlanV1, ProtocolTraceV1 } from "./dto";
23
+
24
+ /** The route the request settled on, as the request log context records it. */
25
+ export interface ShadowSettledRoute {
26
+ provider?: string;
27
+ model?: string;
28
+ }
29
+
30
+ function samePath(a: readonly ProtocolHop[], b: readonly ProtocolHop[]): boolean {
31
+ return a.length === b.length && a.every((hop, index) => hop === b[index]);
32
+ }
33
+
34
+ function settledCandidate(plan: ProtocolPlanV1, settled: ShadowSettledRoute): ProtocolPlanCandidateV1 | undefined {
35
+ const exact = settled.provider && settled.model
36
+ ? plan.candidates.find(candidate => candidate.provider === settled.provider && candidate.model === settled.model)
37
+ : undefined;
38
+ return exact ?? plan.candidates.find(candidate => candidate.eligible);
39
+ }
40
+
41
+ /** True when the plan and the observed trace disagree about the path the request took. */
42
+ export function shadowPlanMismatch(
43
+ plan: ProtocolPlanV1,
44
+ trace: ProtocolTraceV1,
45
+ settled: ShadowSettledRoute = {},
46
+ ): boolean {
47
+ if (plan.inbound !== trace.inbound) return true;
48
+ if (trace.mode === "blocked") {
49
+ if (trace.reasonCodes.includes("compatibility-reject")) return false;
50
+ return plan.mode !== "blocked";
51
+ }
52
+ if (trace.inbound === "messages" && trace.mode === "native"
53
+ && plan.reasonCodes.includes("caller-credential-required")
54
+ && samePath(trace.requestPath, ["messages", "messages"])) {
55
+ return false;
56
+ }
57
+ const candidate = settledCandidate(plan, settled);
58
+ if (!candidate || candidate.mode === "blocked") return true;
59
+ return candidate.mode !== trace.mode
60
+ || candidate.upstream !== trace.upstream
61
+ || !samePath(candidate.requestPath, trace.requestPath);
62
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Observed protocol path for one request (PF-02).
3
+ *
4
+ * SERVER SIDE: the dashboard never imports this file; it reads the finished `ProtocolTraceV1`
5
+ * through `dto.ts`. Marks live in WeakMaps keyed by the request log context and by the live
6
+ * attempt objects, so neither `RequestLogContext` nor the persisted attempt row grows a field,
7
+ * and a context that is never finalized takes its marks with it.
8
+ *
9
+ * Every function here is side-effect free apart from its own WeakMap and never throws into the
10
+ * request path: a trace is diagnostics, and a request must not fail because its trace could not
11
+ * be recorded. Nothing conversation-derived is kept — only fixed vocabulary.
12
+ *
13
+ * Shadow plan (PF-12): when `protocols.rollout.shadowPlan` is on, the ingress records the
14
+ * dispatch-basis plan input for the request (`src/protocols/shadow-plan.ts`); at finalize the
15
+ * plan is computed with the pure planner and compared with the observed trace, and a
16
+ * disagreement sets `planMismatch`. Nothing is recorded with the switch off, so the trace is
17
+ * unchanged, and a failed comparison leaves the trace as observed.
18
+ */
19
+ import {
20
+ PROTOCOL_CONTRACT_VERSION,
21
+ isProtocolReasonCode,
22
+ upstreamWireForAdapter,
23
+ type DeliveryMode,
24
+ type Protocol,
25
+ type ProtocolHop,
26
+ type ProtocolReasonCode,
27
+ type UpstreamWire,
28
+ } from "./contract";
29
+ import { PROTOCOL_DTO_LIMITS, PROTOCOL_TRACE_SCHEMA_VERSION, type ProtocolAttemptTraceV1, type ProtocolTraceV1 } from "./dto";
30
+ import { featureEffectsForPath, isProtocolFeature, type ProtocolFeature } from "./features";
31
+ import { deliveryModeForLane, requestPathForLane, responsePathForLane, type ProtocolLane } from "./path";
32
+ import { planProtocol, type ProtocolPlanInput } from "./plan";
33
+ import { shadowPlanMismatch } from "./shadow";
34
+
35
+ /** Features are computed inside the mark, so a thrown feature scan is contained there too. */
36
+ export type ProtocolFeatureSource = Iterable<ProtocolFeature> | (() => Iterable<ProtocolFeature>);
37
+
38
+ interface EntryMark {
39
+ kind: "entry";
40
+ inbound: Protocol;
41
+ lane: ProtocolLane;
42
+ reasonCodes: ProtocolReasonCode[];
43
+ features: ProtocolFeature[];
44
+ }
45
+
46
+ interface BlockedMark {
47
+ kind: "blocked";
48
+ inbound: Protocol;
49
+ reasonCodes: ProtocolReasonCode[];
50
+ features: ProtocolFeature[];
51
+ }
52
+
53
+ interface AttemptMark {
54
+ mode: Exclude<DeliveryMode, "blocked">;
55
+ requestPath: ProtocolHop[];
56
+ responsePath?: ProtocolHop[];
57
+ }
58
+
59
+ /** The attempt fields the trace reads. `PersistedUsageAttempt` satisfies it. */
60
+ export interface ProtocolTraceAttempt {
61
+ ordinal: number;
62
+ adapter: string;
63
+ }
64
+
65
+ const requestMarks = new WeakMap<object, EntryMark | BlockedMark>();
66
+ const attemptMarks = new WeakMap<object, AttemptMark>();
67
+ const shadowInputs = new WeakMap<object, ProtocolPlanInput>();
68
+
69
+ function boundedReasons(codes: Iterable<ProtocolReasonCode>): ProtocolReasonCode[] {
70
+ const out: ProtocolReasonCode[] = [];
71
+ for (const code of codes) {
72
+ if (!isProtocolReasonCode(code) || out.includes(code)) continue;
73
+ out.push(code);
74
+ if (out.length >= PROTOCOL_DTO_LIMITS.reasonCodes) break;
75
+ }
76
+ return out;
77
+ }
78
+
79
+ function collectFeatures(source: ProtocolFeatureSource | undefined): ProtocolFeature[] {
80
+ if (source === undefined) return [];
81
+ const iterable = typeof source === "function" ? source() : source;
82
+ const out: ProtocolFeature[] = [];
83
+ for (const feature of iterable) {
84
+ if (isProtocolFeature(feature) && !out.includes(feature)) out.push(feature);
85
+ if (out.length >= PROTOCOL_DTO_LIMITS.featureEffects) break;
86
+ }
87
+ return out;
88
+ }
89
+
90
+ /**
91
+ * Record which lane a Chat or Messages ingress chose. A later mark replaces an earlier one:
92
+ * the last decision before the send is the one that describes it.
93
+ */
94
+ export function markProtocolEntry(
95
+ logCtx: object,
96
+ mark: { inbound: Protocol; lane: ProtocolLane; reasonCodes?: Iterable<ProtocolReasonCode>; features?: ProtocolFeatureSource },
97
+ ): void {
98
+ try {
99
+ requestMarks.set(logCtx, {
100
+ kind: "entry",
101
+ inbound: mark.inbound,
102
+ lane: mark.lane,
103
+ reasonCodes: boundedReasons(mark.reasonCodes ?? []),
104
+ features: collectFeatures(mark.features),
105
+ });
106
+ } catch {
107
+ /* a trace must never fail the request it describes */
108
+ }
109
+ }
110
+
111
+ /** Features the request's entry or blocked mark recorded, for the shadow plan input. */
112
+ export function protocolMarkFeatures(logCtx: object): readonly ProtocolFeature[] | undefined {
113
+ try {
114
+ const mark = requestMarks.get(logCtx);
115
+ return mark ? [...mark.features] : undefined;
116
+ } catch {
117
+ return undefined;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Record the dispatch-basis plan input the shadow comparison runs at finalize. The input holds
123
+ * fixed vocabulary and the provider and model names the server already exposes, nothing else.
124
+ */
125
+ export function markProtocolShadowPlanInput(logCtx: object, input: ProtocolPlanInput): void {
126
+ try {
127
+ shadowInputs.set(logCtx, input);
128
+ } catch {
129
+ /* a trace must never fail the request it describes */
130
+ }
131
+ }
132
+
133
+ /** Record a refusal made before any upstream send. */
134
+ export function markProtocolBlocked(
135
+ logCtx: object,
136
+ mark: { inbound: Protocol; reasonCodes: Iterable<ProtocolReasonCode>; features?: ProtocolFeatureSource },
137
+ ): void {
138
+ try {
139
+ requestMarks.set(logCtx, {
140
+ kind: "blocked",
141
+ inbound: mark.inbound,
142
+ reasonCodes: boundedReasons(mark.reasonCodes),
143
+ features: collectFeatures(mark.features),
144
+ });
145
+ } catch {
146
+ /* a trace must never fail the request it describes */
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Add a reason to the request's entry mark: a combo candidate skipped before any send (PF-07).
152
+ * No-op without an entry mark, and a blocked mark is never reopened.
153
+ */
154
+ export function addProtocolEntryReason(logCtx: object, code: ProtocolReasonCode): void {
155
+ try {
156
+ const mark = requestMarks.get(logCtx);
157
+ if (mark?.kind !== "entry") return;
158
+ mark.reasonCodes = boundedReasons([...mark.reasonCodes, code]);
159
+ } catch {
160
+ /* a trace must never fail the request it describes */
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Record the path one physical attempt actually took, overriding the lane-derived one. For a
166
+ * send site that knows its path better than the ingress lane does.
167
+ */
168
+ export function markAttemptProtocolPath(
169
+ attempt: object,
170
+ mark: { mode: Exclude<DeliveryMode, "blocked">; requestPath: readonly ProtocolHop[]; responsePath?: readonly ProtocolHop[] },
171
+ ): void {
172
+ try {
173
+ if (mark.requestPath.length === 0 || mark.requestPath.length > PROTOCOL_DTO_LIMITS.pathHops) return;
174
+ const responsePath = mark.responsePath && mark.responsePath.length > 0
175
+ && mark.responsePath.length <= PROTOCOL_DTO_LIMITS.pathHops
176
+ ? [...mark.responsePath]
177
+ : undefined;
178
+ attemptMarks.set(attempt, {
179
+ mode: mark.mode,
180
+ requestPath: [...mark.requestPath],
181
+ ...(responsePath ? { responsePath } : {}),
182
+ });
183
+ } catch {
184
+ /* a trace must never fail the request it describes */
185
+ }
186
+ }
187
+
188
+ function isReverse(a: readonly ProtocolHop[], b: readonly ProtocolHop[]): boolean {
189
+ return a.length === b.length && a.every((hop, index) => hop === b[b.length - 1 - index]);
190
+ }
191
+
192
+ interface ResolvedPath {
193
+ upstream: UpstreamWire;
194
+ mode: Exclude<DeliveryMode, "blocked">;
195
+ requestPath: ProtocolHop[];
196
+ responsePath: ProtocolHop[];
197
+ }
198
+
199
+ function pathFor(inbound: Protocol, lane: ProtocolLane, upstream: UpstreamWire): ResolvedPath {
200
+ return {
201
+ upstream,
202
+ mode: deliveryModeForLane(inbound, lane, upstream),
203
+ requestPath: requestPathForLane(inbound, lane, upstream),
204
+ responsePath: responsePathForLane(inbound, lane, upstream),
205
+ };
206
+ }
207
+
208
+ function attemptPath(inbound: Protocol, lane: ProtocolLane, attempt: ProtocolTraceAttempt): ResolvedPath {
209
+ // A native Chat or Messages lane sends the ingress wire itself, whatever the adapter id says;
210
+ // Responses has no lane and follows its adapter.
211
+ const upstream = lane === "native" && inbound !== "responses" ? inbound : upstreamWireForAdapter(attempt.adapter);
212
+ const derived = pathFor(inbound, lane, upstream);
213
+ const mark = attemptMarks.get(attempt);
214
+ if (!mark) return derived;
215
+ return {
216
+ upstream,
217
+ mode: mark.mode,
218
+ requestPath: [...mark.requestPath],
219
+ responsePath: mark.responsePath ? [...mark.responsePath] : [...mark.requestPath].reverse(),
220
+ };
221
+ }
222
+
223
+ /** A reason code the path itself implies, so a trace is never reason-less. */
224
+ function pathReason(path: ResolvedPath): ProtocolReasonCode {
225
+ if (path.upstream === "other") return "upstream-other";
226
+ if (path.mode === "native") return "same-wire-native";
227
+ if (path.mode === "legacy-bridge") return "not-migrated";
228
+ return path.requestPath.includes("ir") ? "cross-wire-ir" : "cross-wire-codec";
229
+ }
230
+
231
+ /** The context fields the trace reads at finalize. */
232
+ export type ProtocolTraceContext = object & { inboundProtocol?: Protocol; provider?: string; model?: string };
233
+
234
+ /**
235
+ * Derive the observed trace at finalize. `attempts` are the live attempt objects (the ones
236
+ * `markAttemptProtocolPath` was keyed by), not detached copies.
237
+ *
238
+ * - blocked mark: `blocked`, empty paths.
239
+ * - Responses inbound without a mark: the final adapter's wire decides the path.
240
+ * - Chat or Messages: the entry lane decides, through `path.ts`.
241
+ * - no attempt and no native or blocked mark: `undefined`; nothing is guessed.
242
+ *
243
+ * With a shadow plan input recorded, a disagreeing plan adds `planMismatch: true`.
244
+ */
245
+ export function protocolTraceForRequest(
246
+ logCtx: ProtocolTraceContext,
247
+ attempts: readonly ProtocolTraceAttempt[] | undefined,
248
+ ): ProtocolTraceV1 | undefined {
249
+ const trace = observedTrace(logCtx, attempts);
250
+ return trace ? withShadowPlan(logCtx, trace) : undefined;
251
+ }
252
+
253
+ /**
254
+ * Compare the recorded dispatch plan with the observed trace. Pure and local: the planner reads
255
+ * only the recorded input. Any throw leaves the observed trace as it was.
256
+ */
257
+ function withShadowPlan(logCtx: ProtocolTraceContext, trace: ProtocolTraceV1): ProtocolTraceV1 {
258
+ try {
259
+ const input = shadowInputs.get(logCtx);
260
+ if (!input) return trace;
261
+ const settled = {
262
+ ...(typeof logCtx.provider === "string" ? { provider: logCtx.provider } : {}),
263
+ ...(typeof logCtx.model === "string" ? { model: logCtx.model } : {}),
264
+ };
265
+ return shadowPlanMismatch(planProtocol(input), trace, settled) ? { ...trace, planMismatch: true } : trace;
266
+ } catch {
267
+ return trace;
268
+ }
269
+ }
270
+
271
+ function observedTrace(
272
+ logCtx: ProtocolTraceContext,
273
+ attempts: readonly ProtocolTraceAttempt[] | undefined,
274
+ ): ProtocolTraceV1 | undefined {
275
+ try {
276
+ const mark = requestMarks.get(logCtx);
277
+ if (mark?.kind === "blocked") {
278
+ return {
279
+ v: PROTOCOL_TRACE_SCHEMA_VERSION,
280
+ inbound: mark.inbound,
281
+ mode: "blocked",
282
+ requestPath: [],
283
+ responsePath: [],
284
+ reasonCodes: mark.reasonCodes,
285
+ contractVersion: PROTOCOL_CONTRACT_VERSION,
286
+ };
287
+ }
288
+ let inbound: Protocol;
289
+ let lane: ProtocolLane;
290
+ if (mark) {
291
+ inbound = mark.inbound;
292
+ lane = mark.lane;
293
+ } else if (logCtx.inboundProtocol === "responses") {
294
+ inbound = "responses";
295
+ lane = "native";
296
+ } else {
297
+ return undefined;
298
+ }
299
+ const live = (attempts ?? []).filter(attempt => Number.isInteger(attempt.ordinal) && attempt.ordinal > 0);
300
+ const kept = live.slice(-PROTOCOL_DTO_LIMITS.attempts);
301
+ const attemptPaths = kept.map(attempt => ({ attempt, path: attemptPath(inbound, lane, attempt) }));
302
+ let final: ResolvedPath;
303
+ const last = attemptPaths.at(-1);
304
+ if (last) final = last.path;
305
+ else if (mark && lane === "native") final = pathFor(inbound, lane, inbound);
306
+ else return undefined;
307
+
308
+ const reasonCodes = boundedReasons([...(mark?.reasonCodes ?? []), pathReason(final)]);
309
+ const features = mark?.features ?? [];
310
+ const effects = features.length > 0
311
+ ? featureEffectsForPath(inbound, final.requestPath, features).effects.slice(0, PROTOCOL_DTO_LIMITS.featureEffects)
312
+ : [];
313
+ const attemptTraces: ProtocolAttemptTraceV1[] = attemptPaths.map(({ attempt, path }) => ({
314
+ ordinal: attempt.ordinal,
315
+ upstream: path.upstream,
316
+ mode: path.mode,
317
+ requestPath: path.requestPath,
318
+ ...(isReverse(path.requestPath, path.responsePath) ? {} : { responsePath: path.responsePath }),
319
+ }));
320
+ return {
321
+ v: PROTOCOL_TRACE_SCHEMA_VERSION,
322
+ inbound,
323
+ mode: final.mode,
324
+ upstream: final.upstream,
325
+ requestPath: final.requestPath,
326
+ responsePath: final.responsePath,
327
+ reasonCodes,
328
+ ...(effects.length > 0 ? { featureEffects: effects.map(effect => ({ ...effect })) } : {}),
329
+ ...(attemptTraces.length > 0 ? { attempts: attemptTraces } : {}),
330
+ contractVersion: PROTOCOL_CONTRACT_VERSION,
331
+ };
332
+ } catch {
333
+ return undefined;
334
+ }
335
+ }
@@ -18,7 +18,7 @@
18
18
  // with nothing left to carry forward (see `isRegistryResidue`, issue #5066).
19
19
  // Everything else in the row is left alone.
20
20
 
21
- import { PROVIDER_REGISTRY } from "./registry";
21
+ import { PROVIDER_REGISTRY, normalizedProviderEndpoint } from "./registry";
22
22
  import { MODEL_KEYED_RECORDS, MODEL_ID_LISTS, MODEL_ID_SCALARS, MODEL_NESTED_RECORDS } from "./model-rename-fields";
23
23
  import { providerConfigSeed } from "./derive";
24
24
  import type { OcxConfig, OcxProviderConfig } from "../types";
@@ -256,8 +256,8 @@ function providerStillMatchesRegistry(name: string, prov: OcxProviderConfig): bo
256
256
  const choices = entry.baseUrlChoices?.map(choice => choice.baseUrl) ?? [];
257
257
  const known = [entry.baseUrl, ...choices]
258
258
  .filter((url): url is string => typeof url === "string")
259
- .map(url => url.replace(/\/+$/, ""));
260
- return known.includes(prov.baseUrl.replace(/\/+$/, ""));
259
+ .map(normalizedProviderEndpoint);
260
+ return known.includes(normalizedProviderEndpoint(prov.baseUrl));
261
261
  }
262
262
 
263
263
  /** Guard against a stale rename: only apply when the registry actually seeds `to`. */
@@ -1263,7 +1263,12 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
1263
1263
  modelReasoningEfforts: Object.fromEntries(DEEPSEEK_NATIVE_THINKING_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
1264
1264
  modelReasoningEffortMap: Object.fromEntries(DEEPSEEK_NATIVE_THINKING_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
1265
1265
  modelSupportsReasoningSummaries: Object.fromEntries(DEEPSEEK_NATIVE_THINKING_MODELS.map(id => [id, true])),
1266
- preserveReasoningContentModels: DEEPSEEK_NATIVE_THINKING_MODELS,
1266
+ // deepseek-v4-pro is absent from the live sets on purpose (retired upstream, requests
1267
+ // route to V4.1-Flash — see model-seeds.ts), but it stays SELECTABLE in configs that
1268
+ // already carry it, and what serves it is a thinking-mode model. A saved config with
1269
+ // the alias selected would otherwise serialize a bare continuation the upstream 400s
1270
+ // on (#5421). The preserve list governs replay, not advertisement.
1271
+ preserveReasoningContentModels: [...DEEPSEEK_NATIVE_THINKING_MODELS, "deepseek-v4-pro"],
1267
1272
  // #4436: first-party deepseek-flash accepts native images on Chat and Responses.
1268
1273
  // Keep unprobed compatibility aliases on the #88 sidecar path. This must be fixed
1269
1274
  // here: router enrichment unions this list with saved config, so config cannot remove it.
@@ -117,6 +117,21 @@ import {
117
117
  } from "./model-seeds";
118
118
 
119
119
  export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
120
+ {
121
+ // Verified 2026-09-21: docs.typesafe.ai/introduction/quickstart and /api document the fixed
122
+ // endpoint, Bearer auth, jev-latest, and TYPESAFE_API_KEY; typesafe.ai/legal/mca permits API integration.
123
+ id: "jev",
124
+ label: "TypeSafe JEV",
125
+ baseUrl: "https://api.typesafe.ai/v1/systemone",
126
+ adapter: "jev-decision",
127
+ authKind: "key",
128
+ credentialOnly: true,
129
+ dashboardUrl: "https://console.typesafe.ai",
130
+ liveModels: false,
131
+ apiKeyValidation: "unknown",
132
+ preserveCustomDestination: true,
133
+ note: "TypeSafe JEV decision service for the optional JEV Combo strategy. This credential-only preset does not publish a directly routable model.",
134
+ },
120
135
  {
121
136
  id: "baseten",
122
137
  label: "Baseten Model APIs",
@@ -46,6 +46,7 @@ export const REGISTRY_FIELD_MODEL_ID_ROLES = {
46
46
  apiKeyTransport: NONE,
47
47
  alias: NONE,
48
48
  authKind: NONE,
49
+ credentialOnly: NONE,
49
50
  codexAccountMode: NONE,
50
51
  allowKeyAuthOverride: NONE,
51
52
  allowPrivateNetworkByDefault: NONE,
@@ -112,6 +112,12 @@ export interface ProviderRegistryEntry {
112
112
  apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
113
113
  alias?: string;
114
114
  authKind: ProviderAuthKind;
115
+ /**
116
+ * Credential preset for an auxiliary service rather than a model transport.
117
+ * Its adapter is an identity marker and is intentionally absent from the
118
+ * routable adapter registry.
119
+ */
120
+ credentialOnly?: boolean;
115
121
  codexAccountMode?: CodexAccountMode;
116
122
  /** OAuth preset may explicitly honor a persisted API-key billing mode. */
117
123
  allowKeyAuthOverride?: boolean;
@@ -81,7 +81,7 @@ export function registryModelServiceTierCapabilityApplies(
81
81
  return guard === undefined || guard(provider.baseUrl);
82
82
  }
83
83
 
84
- function normalizedProviderEndpoint(value: string): string {
84
+ export function normalizedProviderEndpoint(value: string): string {
85
85
  const trimmed = value.trim();
86
86
  try {
87
87
  const parsed = new URL(trimmed);
@@ -478,13 +478,14 @@ export class RemoteWorkspaceHub {
478
478
  }
479
479
 
480
480
  revokeDevice(deviceId: string): boolean {
481
- const before = this.state.devices.length;
482
- this.state = { ...this.state, devices: this.state.devices.filter(device => device.id !== deviceId) };
483
- if (this.state.devices.length === before) return false;
481
+ const next = { ...this.state, devices: this.state.devices.filter(device => device.id !== deviceId) };
482
+ if (next.devices.length === this.state.devices.length) return false;
483
+ // Publish only after durable removal: a failed save must remain visible and retryable.
484
+ this.store.save(next);
485
+ this.state = next;
484
486
  const connection = this.connections.get(deviceId);
485
487
  this.connections.delete(deviceId);
486
488
  connection?.close("remote workspace device was revoked");
487
- this.store.save(this.state);
488
489
  return true;
489
490
  }
490
491
 
@@ -28,6 +28,11 @@ const FREEFORM_FALLBACK_KEYS: Readonly<Record<string, readonly string[]>> = {
28
28
  apply_patch: ["patch", "content"],
29
29
  };
30
30
 
31
+ /** The field names a tool's freeform wrapper may unwrap through, besides canonical `input`. */
32
+ export function freeformFallbackKeys(toolName: string): readonly string[] {
33
+ return FREEFORM_FALLBACK_KEYS[toolName] ?? [];
34
+ }
35
+
31
36
  function stripMarkdownCodeFence(text: string, toolName: string): string {
32
37
  if (toolName !== "exec" && toolName !== "apply_patch") return text;
33
38
  const match = OUTER_MARKDOWN_CODE_FENCE.exec(text.trim());
@@ -46,7 +51,7 @@ export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""):
46
51
  ? stripMarkdownCodeFence(record.input, toolName)
47
52
  : argumentsText;
48
53
  }
49
- const fallbackKeys = FREEFORM_FALLBACK_KEYS[toolName] ?? [];
54
+ const fallbackKeys = freeformFallbackKeys(toolName);
50
55
  const candidates = fallbackKeys.filter(key => typeof record[key] === "string");
51
56
  if (candidates.length === 1) {
52
57
  return stripMarkdownCodeFence(record[candidates[0]] as string, toolName);
@@ -40,8 +40,10 @@ export function mayBecomeCodeModeShellInput(argumentsText: string, input: string
40
40
  const head = input.trimStart();
41
41
  if (head === "" || head.startsWith("{")) return true;
42
42
  // Canonical JavaScript streams progressively; avoid reparsing its growing wrapper on every
43
- // delta. The shared prefix scanner is bounded independently of the command's size.
44
- if (input === argumentsText || scanFreeformWrapper(argumentsText).kind === "input") return false;
43
+ // delta. The shared prefix scanner is bounded independently of the command's size. No fallback
44
+ // keys here: a fallback value is a shell command, not JavaScript, so only canonical `input`
45
+ // may short-circuit the parse below.
46
+ if (input === argumentsText || scanFreeformWrapper(argumentsText, []).kind === "input") return false;
45
47
  try {
46
48
  const args = JSON.parse(argumentsText);
47
49
  // A fallback cmd value becomes visible only when the outer object closes. Do not emit