@bitkyc08/opencodex 2.56.0 → 2.58.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 (206) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
  3. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
  8. package/src/adapters/command-code.ts +12 -3
  9. package/src/adapters/cursor/cursor-errors.ts +15 -0
  10. package/src/adapters/cursor/discovery.ts +65 -1
  11. package/src/adapters/cursor/envelope-echo.ts +8 -2
  12. package/src/adapters/cursor/live-transport.ts +5 -1
  13. package/src/adapters/cursor/protobuf-events.ts +110 -11
  14. package/src/adapters/cursor/protobuf-request.ts +19 -1
  15. package/src/adapters/cursor/text-toolcall.ts +230 -0
  16. package/src/adapters/cursor/thread-continuity.ts +67 -0
  17. package/src/adapters/cursor/types.ts +5 -0
  18. package/src/adapters/cursor.ts +55 -5
  19. package/src/adapters/google-http.ts +38 -13
  20. package/src/adapters/google.ts +7 -7
  21. package/src/adapters/kiro/payload.ts +17 -3
  22. package/src/adapters/kiro/reasoning.ts +70 -7
  23. package/src/adapters/kiro/stream.ts +8 -2
  24. package/src/adapters/kiro/wire.ts +2 -1
  25. package/src/adapters/kiro-events.ts +21 -13
  26. package/src/adapters/mimo-free.ts +32 -17
  27. package/src/adapters/ollama-native.ts +42 -8
  28. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  29. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  30. package/src/adapters/openai-chat.ts +8 -8
  31. package/src/adapters/openai-responses/passthrough.ts +62 -5
  32. package/src/adapters/openai-responses/request-strips.ts +43 -0
  33. package/src/adapters/physical-send.ts +50 -0
  34. package/src/bridge/errors.ts +26 -2
  35. package/src/bridge/response-json.ts +8 -2
  36. package/src/bridge/sse.ts +20 -2
  37. package/src/claude/desktop-profile.ts +66 -9
  38. package/src/claude/outbound.ts +32 -4
  39. package/src/cli/account-main.ts +1 -1
  40. package/src/cli/capabilities.ts +2 -2
  41. package/src/cli/combo.ts +10 -1
  42. package/src/cli/config-command.ts +35 -18
  43. package/src/cli/dispatch.ts +17 -4
  44. package/src/cli/index.ts +92 -7
  45. package/src/cli/registry.ts +2 -1
  46. package/src/cli/system-command.ts +74 -5
  47. package/src/cli/uninstall-client-state.ts +12 -0
  48. package/src/clients/config-export.ts +7 -3
  49. package/src/codex/account-label.ts +14 -3
  50. package/src/codex/account-store.ts +113 -26
  51. package/src/codex/account-usability.ts +21 -0
  52. package/src/codex/auth-api/login-flow.ts +14 -2
  53. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  54. package/src/codex/auth-context.ts +199 -15
  55. package/src/codex/catalog/aggregation.ts +80 -1
  56. package/src/codex/catalog/model-visibility.ts +1 -0
  57. package/src/codex/catalog/remote.ts +30 -0
  58. package/src/codex/catalog/retained-sync.ts +9 -1
  59. package/src/codex/catalog/routed-gather.ts +38 -1
  60. package/src/codex/cli-install-provenance.ts +7 -1
  61. package/src/codex/convergence.ts +7 -2
  62. package/src/codex/desktop-app/types.ts +11 -2
  63. package/src/codex/desktop-app/windows.ts +5 -5
  64. package/src/codex/desktop-switches.ts +145 -0
  65. package/src/codex/history-job.ts +5 -1
  66. package/src/codex/history-provider.ts +33 -4
  67. package/src/codex/history-worker.ts +14 -1
  68. package/src/codex/inject/remove.ts +145 -7
  69. package/src/codex/inject/restore.ts +231 -32
  70. package/src/codex/inject.ts +12 -16
  71. package/src/codex/loopback-target.ts +9 -0
  72. package/src/codex/model-entitlements.ts +152 -15
  73. package/src/codex/native-profile-startup.ts +64 -20
  74. package/src/codex/pool-refresh-backoff.ts +12 -3
  75. package/src/codex/quota-rejection.ts +104 -15
  76. package/src/codex/routing/cache-affinity.ts +70 -0
  77. package/src/codex/routing/cooldown-math.ts +10 -0
  78. package/src/codex/routing/selection.ts +79 -2
  79. package/src/codex/routing/thread-affinity.ts +50 -2
  80. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  81. package/src/codex/routing.ts +29 -49
  82. package/src/codex/warmup.ts +1 -1
  83. package/src/combos/failover.ts +85 -0
  84. package/src/combos/request.ts +17 -10
  85. package/src/combos/types.ts +23 -2
  86. package/src/config/atomic-write.ts +83 -8
  87. package/src/config/pending-teardown.ts +31 -0
  88. package/src/config/schema/config-schema.ts +2 -0
  89. package/src/config/schema/leaf-validators.ts +1 -0
  90. package/src/generated/compatibility-version.json +272 -180
  91. package/src/images/loop.ts +1 -1
  92. package/src/lib/bounded-subprocess.ts +62 -10
  93. package/src/lib/errors.ts +17 -0
  94. package/src/lib/request-execution-budget.ts +147 -21
  95. package/src/lib/spend-reservation-ledger.ts +18 -0
  96. package/src/lib/state-store-registrations.ts +6 -2
  97. package/src/lib/test-home-guard.ts +85 -1
  98. package/src/lib/upstream-retry.ts +77 -10
  99. package/src/lib/windows-elevation.ts +76 -14
  100. package/src/lib/windows-secret-acl.ts +151 -15
  101. package/src/lib/windows-user-principal.ts +5 -1
  102. package/src/oauth/index.ts +2 -2
  103. package/src/oauth/key-providers.ts +2 -2
  104. package/src/providers/derive.ts +6 -0
  105. package/src/providers/kiro-models.ts +4 -3
  106. package/src/providers/label.ts +19 -1
  107. package/src/providers/model-discovery.ts +35 -7
  108. package/src/providers/registry/entries-core.ts +18 -0
  109. package/src/providers/registry/entries-extended.ts +59 -28
  110. package/src/providers/registry/model-seeds.ts +71 -17
  111. package/src/providers/registry/types.ts +9 -0
  112. package/src/responses/reasoning-envelope.ts +6 -3
  113. package/src/responses/spill-store.ts +17 -0
  114. package/src/responses/state/body-policy.ts +25 -0
  115. package/src/responses/state/spill-queue.ts +8 -6
  116. package/src/responses/state.ts +3 -22
  117. package/src/router.ts +4 -0
  118. package/src/routing/identity-domains.ts +21 -14
  119. package/src/routing/probe-lease.ts +103 -1
  120. package/src/server/auth-cors.ts +1 -0
  121. package/src/server/chat-completions.ts +3 -1
  122. package/src/server/chat-native.ts +37 -9
  123. package/src/server/index/live-sideband.ts +37 -1
  124. package/src/server/index/websocket-handler.ts +54 -3
  125. package/src/server/index.ts +5 -5
  126. package/src/server/inspection-tee.ts +107 -0
  127. package/src/server/live.ts +46 -1
  128. package/src/server/management/combo-routes.ts +10 -1
  129. package/src/server/management/config-routes.ts +27 -5
  130. package/src/server/models-capabilities.ts +24 -3
  131. package/src/server/relay-eager.ts +2 -0
  132. package/src/server/relay.ts +14 -19
  133. package/src/server/request-log.ts +127 -3
  134. package/src/server/response-log-body.ts +153 -0
  135. package/src/server/responses/account-change-state.ts +74 -0
  136. package/src/server/responses/adapter-continuation.ts +33 -7
  137. package/src/server/responses/adapter-delivery.ts +5 -11
  138. package/src/server/responses/adapter-dispatch.ts +84 -13
  139. package/src/server/responses/codex-ws-exchange.ts +65 -4
  140. package/src/server/responses/codex-ws-wire.ts +5 -0
  141. package/src/server/responses/collaboration.ts +74 -4
  142. package/src/server/responses/combo-session-recall.ts +68 -8
  143. package/src/server/responses/combo-stream-preflight.ts +68 -5
  144. package/src/server/responses/compact.ts +54 -13
  145. package/src/server/responses/core-auth.ts +2 -0
  146. package/src/server/responses/core-codex-account.ts +51 -3
  147. package/src/server/responses/core-combo.ts +129 -23
  148. package/src/server/responses/core-errors.ts +18 -0
  149. package/src/server/responses/core-options.ts +3 -0
  150. package/src/server/responses/core-replay.ts +105 -32
  151. package/src/server/responses/core.ts +3 -3
  152. package/src/server/responses/encrypted-payload.ts +0 -1
  153. package/src/server/responses/fetch-helpers.ts +4 -1
  154. package/src/server/responses/input-admission.ts +126 -6
  155. package/src/server/responses/native-injection-protocol.ts +42 -0
  156. package/src/server/responses/native-injection-replay.ts +105 -0
  157. package/src/server/responses/native-injection.ts +242 -0
  158. package/src/server/responses/native-response-control.ts +56 -0
  159. package/src/server/responses/native-response-json.ts +14 -0
  160. package/src/server/responses/native-response-output.ts +37 -0
  161. package/src/server/responses/native-steering-log.ts +44 -0
  162. package/src/server/responses/native-steering-policy.ts +49 -0
  163. package/src/server/responses/native-steering-replay.ts +126 -0
  164. package/src/server/responses/native-steering-settings.ts +76 -0
  165. package/src/server/responses/native-steering.ts +400 -0
  166. package/src/server/responses/native-tool-results.ts +130 -0
  167. package/src/server/responses/passthrough-delivery.ts +30 -6
  168. package/src/server/responses/passthrough-dispatch.ts +61 -11
  169. package/src/server/responses/passthrough-error.ts +38 -2
  170. package/src/server/responses/request-prepare.ts +173 -22
  171. package/src/server/responses/request-send-budget.ts +97 -2
  172. package/src/server/responses/request-spend.ts +147 -0
  173. package/src/server/responses/request-transport.ts +62 -3
  174. package/src/server/responses/run-turn-execution.ts +59 -31
  175. package/src/server/responses/sidecar-execution.ts +7 -13
  176. package/src/server/responses/terminal-guard.ts +65 -4
  177. package/src/server/responses/ws-upstream.ts +21 -1
  178. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  179. package/src/server/stop-teardown.ts +8 -1
  180. package/src/server/ws-bridge.ts +16 -1
  181. package/src/service/cli.ts +13 -1
  182. package/src/service/windows-ops.ts +210 -16
  183. package/src/service/windows-scheduler.ts +28 -21
  184. package/src/service.ts +1 -1
  185. package/src/types/config.ts +8 -1
  186. package/src/types/provider.ts +13 -0
  187. package/src/types/request.ts +8 -5
  188. package/src/types/tools.ts +24 -0
  189. package/src/types.ts +2 -0
  190. package/src/update/index.ts +10 -0
  191. package/src/update/stop-contract.d.mts +1 -0
  192. package/src/update/stop-contract.mjs +19 -0
  193. package/src/update/stop-decision.d.mts +1 -1
  194. package/src/update/stop-decision.mjs +12 -3
  195. package/src/usage/log.ts +1 -1
  196. package/src/vision/anthropic-describe.ts +1 -1
  197. package/src/vision/describe.ts +5 -5
  198. package/src/web-search/anthropic-executor.ts +1 -1
  199. package/src/web-search/exa-executor.ts +1 -1
  200. package/src/web-search/executor.ts +1 -1
  201. package/src/web-search/gemini-executor.ts +1 -1
  202. package/src/web-search/loop.ts +1 -1
  203. package/src/web-search/ollama-executor.ts +1 -1
  204. package/src/web-search/parse.ts +67 -14
  205. package/src/web-search/passthrough-bridge.ts +64 -31
  206. package/src/web-search/xai-executor.ts +1 -1
@@ -13,6 +13,9 @@ const PLAN_OR_COMPLETION_RE = /(?:\b(?:i(?:'|’)m going to|i will|i(?:'|’)ll|
13
13
  const WAITING_FOR_USER_RE = /(?:[??]\s*$|需要我|请(?:确认|选择|提供)|是否|要不要|可以吗|\b(?:do you want|should i|which file|please confirm|please provide)\b)/iu;
14
14
  const EXPLICIT_CONTINUE_RE = /^(?:继续|接着|往下|go on|continue|proceed|keep going)\s*[.!。!]?$/iu;
15
15
  const MAX_ANNOUNCEMENT_CHARS = 280;
16
+ const MAX_RETAINED_EVENTS = 1_024;
17
+ // JavaScript string code units, not UTF-8 bytes or a process-wide memory limit.
18
+ const MAX_RETAINED_CONTENT_CHARS = 64 * 1_024;
16
19
 
17
20
  export const TERMINAL_GUARD_NUDGE =
18
21
  "你刚才只描述了计划,没有执行任何工具。不要再次解释计划,现在立即调用必要工具执行用户任务。" +
@@ -195,7 +198,17 @@ function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undefined):
195
198
  };
196
199
  }
197
200
 
198
- /** Preserve normal terminals, but withhold one suspicious no-tool terminal for a bounded re-ask. */
201
+ /**
202
+ * Forward adapter events and re-ask only short, suspicious no-tool completions.
203
+ * Retention is bounded per turn; tools or overflow disable analysis without truncating output.
204
+ * Reported usage from completed legs survives a continuation-factory failure. Unreported
205
+ * usage stays absent, and source-iteration failures propagate to the caller's transport handler.
206
+ *
207
+ * @param options Initial stream, parsed history, and continuation callback. The caller owns
208
+ * provider opt-in; the continuation limit defaults to one and is clamped to at most two.
209
+ * @yields Unchanged content events, internal assistant boundaries, and terminal events with
210
+ * accumulated reported usage when available. Returning the iterator closes its active source.
211
+ */
199
212
  export async function* guardTerminalEventStream(options: GuardedEventStreamOptions): AsyncGenerator<AdapterEvent> {
200
213
  const maxContinuations = Math.max(0, Math.min(2, Math.floor(options.maxAutoContinuations ?? 1)));
201
214
  let parsed = options.parsed;
@@ -205,6 +218,10 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
205
218
 
206
219
  while (true) {
207
220
  const seen: AdapterEvent[] = [];
221
+ let retainedContentChars = 0;
222
+ let retainedText = "";
223
+ let analysisEnabled = (options.adapterName === "anthropic" || options.adapterName === "openai-chat")
224
+ && continuations < maxContinuations;
208
225
  let terminalSeen = false;
209
226
  for await (const event of source) {
210
227
  // Liveness markers and tool argument fragments are passed through to the bridge, but
@@ -216,7 +233,7 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
216
233
  }
217
234
  if (event.type === "done") {
218
235
  terminalSeen = true;
219
- const analysis = (options.adapterName === "anthropic" || options.adapterName === "openai-chat")
236
+ const analysis = analysisEnabled
220
237
  ? analyzeTerminalTurn(parsed, seen)
221
238
  : { decision: "pass" as const };
222
239
  const normalStop = event.stopReason !== "max_tokens" && event.stopReason !== "content_filter";
@@ -224,11 +241,17 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
224
241
  accumulatedUsage = mergeUsage(accumulatedUsage, event.usage);
225
242
  continuations += 1;
226
243
  parsed = buildContinuationRequest(parsed, seen);
244
+ seen.length = 0;
245
+ retainedText = "";
227
246
  yield { type: "assistant_boundary" };
228
247
  try {
229
248
  source = await options.continuation(parsed);
230
249
  } catch (error) {
231
- yield { type: "error", message: error instanceof Error ? error.message : String(error) };
250
+ yield {
251
+ type: "error",
252
+ message: error instanceof Error ? error.message : String(error),
253
+ ...(accumulatedUsage ? { usage: accumulatedUsage } : {}),
254
+ };
232
255
  return;
233
256
  }
234
257
  break;
@@ -243,7 +266,45 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
243
266
  yield usage ? { ...event, usage } : event;
244
267
  return;
245
268
  }
246
- seen.push(event);
269
+ if (analysisEnabled) {
270
+ if (event.type === "tool_call_start") {
271
+ // A real tool call permanently rules out a no-tool continuation for this turn.
272
+ analysisEnabled = false;
273
+ } else if (
274
+ event.type === "text_delta"
275
+ || event.type === "thinking_delta"
276
+ || event.type === "thinking_signature"
277
+ || event.type === "redacted_thinking"
278
+ ) {
279
+ const content = event.type === "text_delta"
280
+ ? event.text
281
+ : event.type === "thinking_delta"
282
+ ? event.thinking
283
+ : event.type === "thinking_signature"
284
+ ? event.signature
285
+ : event.data;
286
+ if (
287
+ seen.length >= MAX_RETAINED_EVENTS
288
+ || content.length > MAX_RETAINED_CONTENT_CHARS - retainedContentChars
289
+ ) {
290
+ analysisEnabled = false;
291
+ } else {
292
+ retainedContentChars += content.length;
293
+ if (event.type === "text_delta") retainedText += content;
294
+ // Match analyzeTerminalTurn's trimmed-text semantics, including split padding.
295
+ if (retainedText.trim().length > MAX_ANNOUNCEMENT_CHARS) {
296
+ analysisEnabled = false;
297
+ } else {
298
+ seen.push(event);
299
+ }
300
+ }
301
+ }
302
+ if (!analysisEnabled) {
303
+ // Never rebuild a continuation from truncated thinking or a partial turn.
304
+ seen.length = 0;
305
+ retainedText = "";
306
+ }
307
+ }
247
308
  yield event;
248
309
  }
249
310
  if (!terminalSeen) return;
@@ -1,3 +1,6 @@
1
+ import { OPENAI_API_RESPONSES_URL } from "./native-response-control";
2
+ import { isInjectionRequest } from "./native-injection-protocol";
3
+ import type { NativeResponseControl } from "./native-response-control";
1
4
  // Upstream WebSocket transport for the ChatGPT Codex backend.
2
5
  //
3
6
  // Why this exists: the Codex backend serves the responses_websockets path from
@@ -129,6 +132,8 @@ export function codexWsUpstreamFetch(
129
132
  runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
130
133
  onQuota?: CodexWsQuotaObserver,
131
134
  beforeDispatch?: (headers: Headers) => void,
135
+ nativeControl?: NativeResponseControl,
136
+ beforeContinuation?: () => Promise<void>,
132
137
  ): Promise<Response> {
133
138
  const prepared = prepareCodexWsRequest(url, init);
134
139
  if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
@@ -142,6 +147,17 @@ export function codexWsUpstreamFetch(
142
147
  }
143
148
 
144
149
  const { frameText, headers } = prepared;
150
+ // Never infer backend support from a model name or enable controls on a gateway.
151
+ const control = nativeControl?.kind === "injection"
152
+ ? ((prepared.canonical || url === OPENAI_API_RESPONSES_URL) && isInjectionRequest(JSON.parse(frameText)) ? nativeControl : undefined)
153
+ : (prepared.canonical || url === OPENAI_API_RESPONSES_URL) ? nativeControl : undefined;
154
+ if (control?.kind === "injection" && url === OPENAI_API_RESPONSES_URL) {
155
+ const beta = headers["openai-beta"];
156
+ if (!beta?.split(",").some(value => value.trim() === "responses_multi_agent=v1")) {
157
+ headers["openai-beta"] = beta ? `${beta}, responses_multi_agent=v1` : "responses_multi_agent=v1";
158
+ }
159
+ }
160
+
145
161
 
146
162
  // Decide before dialing. Once the socket is open the caller already holds a
147
163
  // streaming Response, so the oversized close can only be surfaced as a stream
@@ -169,7 +185,9 @@ export function codexWsUpstreamFetch(
169
185
  }
170
186
  let session: CodexWsSession;
171
187
  try {
172
- const identity = codexWsReuseIdentity(url, headers, frameText, proxy);
188
+ // Steering keeps a private physical connection across successor responses; it
189
+ // must never enter the idle-socket pool or move to a different credential.
190
+ const identity = control ? null : codexWsReuseIdentity(url, headers, frameText, proxy);
173
191
  session = (identity ? codexWsPool.acquire(identity, wsUrl, headers, proxy) : null)
174
192
  ?? new CodexWsSession(wsUrl, headers, false, undefined, proxy);
175
193
  if (!session.busy && !session.reserve()) {
@@ -181,6 +199,8 @@ export function codexWsUpstreamFetch(
181
199
  }
182
200
  return codexWsExchange({
183
201
  session, url, init, prepared, sseFallback, onQuota, beforeDispatch,
202
+ nativeControl: control,
203
+ beforeContinuation,
184
204
  bunVersion: typeof runtime === "string" ? runtime : runtime.version,
185
205
  });
186
206
  }
@@ -1,7 +1,7 @@
1
1
  import { collectAmbiguousDottedAliases, dottedAliasIsUnambiguous, wireToolInnerName } from "../responses/tool-name-aliases";
2
2
  import {
3
- CODE_MODE_EXEC_TOOL_NAME,
4
3
  dottedToolName,
4
+ NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
5
5
  namespacedToolName,
6
6
  normalizeDeclaredToolName,
7
7
  } from "../types";
@@ -104,10 +104,14 @@ function addWireToolName(
104
104
  if (dottedAliasIsUnambiguous(namespace, name) && !ambiguousDottedAliases?.has(dotted)) {
105
105
  names.add(dotted);
106
106
  }
107
- // `exec` is the one name that also switches on nested-helper normalization, so a bare alias
108
- // for a namespaced MCP tool would silently authorize `exec_command`/`shell_command`/
109
- // `apply_patch`/`view_image` the request never declared. Every other inner name keeps the bare alias.
110
- if (name !== CODE_MODE_EXEC_TOOL_NAME) names.add(name);
107
+ // The code-mode helper spellings do not get a bare alias for a namespaced tool. Bare `exec`
108
+ // switches nested-helper normalization on for a catalog that never declared the shell; bare
109
+ // `exec_command`/`shell_command` switch it off for one that did; bare `write_stdin`/
110
+ // `apply_patch`/`view_image` are simply accepted as declared under a name the caller only ever
111
+ // authorized inside a namespace. This guard named only `exec` and let the other five through,
112
+ // which is the same drift the bridge-side copy had; both now read one list
113
+ // (src/types/tools.ts). Every other inner name keeps the bare alias.
114
+ if (!NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES.has(name)) names.add(name);
111
115
  }
112
116
 
113
117
  /**
@@ -67,7 +67,14 @@ export async function performStopTeardown(url: URL, io: StopTeardownIo = {}): Pr
67
67
  // undone (#3008).
68
68
  const grokNote = grok.ok ? "" : ` Grok config cleanup failed: ${grok.message}`;
69
69
  if (restore.success && grok.ok) {
70
- return { success: true, message: "Proxy stopping, native Codex restored.", sharedTeardown: "performed" };
70
+ // A degraded restore is a success — routing is out and the client is no longer aimed at
71
+ // a port that is about to disappear — but it left a provider table behind on purpose.
72
+ // Reporting a bare "restored" would put the caller in exactly the position #4812
73
+ // describes: a config they did not expect and no idea why it is there.
74
+ const retained = restore.retainedCodexProviderTable
75
+ ? ` ${(await import("../codex/inject/restore")).describeRetainedCodexProviderTable(restore.retainedCodexProviderTable)}`
76
+ : "";
77
+ return { success: true, message: `Proxy stopping, native Codex restored.${retained}`, sharedTeardown: "performed" };
71
78
  }
72
79
  if (restore.success) {
73
80
  return {
@@ -1,3 +1,4 @@
1
+ import type { NativeResponseControl } from "./responses/native-response-control";
1
2
  import type { ServerWebSocket } from "bun";
2
3
  import { responsesJsonEventSequence } from "./responses-json-events";
3
4
  import { FORWARD_HEADERS } from "../adapters/openai-responses";
@@ -17,6 +18,9 @@ type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
17
18
  type ResponsesPayloadObserver = (payload: string) => void;
18
19
 
19
20
  export interface WsData {
21
+ nativeControl?: NativeResponseControl;
22
+ /** Content-free per-turn explanation; never a model capability assertion. */
23
+ nativeSteeringUnavailable?: string;
20
24
  headers?: Headers; // base inbound forward headers only; per-turn auth refresh injects current pool tokens
21
25
  /**
22
26
  * Resolved once at the handshake. Auth is handshake-time only on this path, so
@@ -229,6 +233,7 @@ export async function pumpResponsesSseToWebSocket(
229
233
  sseStream: ReadableStream<Uint8Array>,
230
234
  options: {
231
235
  isCurrent?: () => boolean;
236
+ untilEof?: boolean;
232
237
  onTerminal?: ResponsesTerminalReporter;
233
238
  onSsePayload?: ResponsesPayloadObserver;
234
239
  } = {},
@@ -251,6 +256,7 @@ export async function pumpResponsesSseToWebSocket(
251
256
  const decoder = new TextDecoder();
252
257
  const framer = new BoundedSseFrameBuffer();
253
258
  let terminalSeen = false;
259
+ let lastTerminal: ResponsesTerminalStatus | undefined;
254
260
 
255
261
  const handlePayload = (payload: string): boolean => {
256
262
  if (!isCurrent()) return true;
@@ -270,8 +276,10 @@ export async function pumpResponsesSseToWebSocket(
270
276
  }
271
277
  if (terminalSeen) return true;
272
278
  sendTextFrame(ws, payload);
273
- const terminalStatus = terminalStatusFromType(type);
279
+ if (options.untilEof && type === "response.created") lastTerminal = undefined;
280
+ const terminalStatus = type === "error" && options.untilEof ? "failed" : terminalStatusFromType(type);
274
281
  if (terminalStatus) {
282
+ if (options.untilEof) { lastTerminal = terminalStatus; return false; }
275
283
  reportTerminal(terminalStatus);
276
284
  terminalSeen = true;
277
285
  void reader.cancel().catch(() => {});
@@ -294,6 +302,10 @@ export async function pumpResponsesSseToWebSocket(
294
302
  const payload = parseSseBlock(decoder.decode(tail));
295
303
  if (payload) handlePayload(payload);
296
304
  }
305
+ if (options.untilEof && lastTerminal && isCurrent() && !clientCancelled) {
306
+ reportTerminal(lastTerminal);
307
+ terminalSeen = true;
308
+ }
297
309
  if (!terminalSeen && isCurrent() && !clientCancelled) {
298
310
  reportTerminal("incomplete");
299
311
  sendProtocolError(ws, 502, "Upstream stream ended before response terminal event");
@@ -368,6 +380,7 @@ export async function sendResponseToWebSocket(
368
380
  response: Response,
369
381
  isCurrent: () => boolean,
370
382
  options: {
383
+ untilEof?: boolean;
371
384
  onTerminal?: ResponsesTerminalReporter;
372
385
  onSsePayload?: ResponsesPayloadObserver;
373
386
  } = {},
@@ -398,6 +411,7 @@ export async function sendResponseToWebSocket(
398
411
  if (contentType.includes("text/event-stream")) {
399
412
  await pumpResponsesSseToWebSocket(ws, response.body, {
400
413
  isCurrent,
414
+ untilEof: options.untilEof,
401
415
  onTerminal: options.onTerminal,
402
416
  onSsePayload: options.onSsePayload,
403
417
  });
@@ -420,6 +434,7 @@ export async function sendResponseToWebSocket(
420
434
  if (looksLikeSse(prefix)) {
421
435
  await pumpResponsesSseToWebSocket(ws, stream, {
422
436
  isCurrent,
437
+ untilEof: options.untilEof,
423
438
  onTerminal: options.onTerminal,
424
439
  onSsePayload: options.onSsePayload,
425
440
  });
@@ -1,6 +1,7 @@
1
1
  import { existsSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { restoreNativeCodexAsync } from "../codex/inject";
4
+ import { describeRetainedCodexProviderTable } from "../codex/inject/restore";
4
5
  import { stripGrokConfig } from "../grok/inject";
5
6
  import { serviceApiTokenFilePath } from "../lib/service-secrets";
6
7
  import { statusWinswRaw, type WinswStatus } from "../lib/winsw";
@@ -291,7 +292,15 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
291
292
  break;
292
293
  }
293
294
  const restore = await restoreNativeCodexAsync();
294
- if (restore.success) console.log("✅ service stopped + native Codex restored.");
295
+ if (restore.success) {
296
+ console.log("✅ service stopped + native Codex restored.");
297
+ // Success is not the whole answer when routing came down but the provider table
298
+ // stayed. Saying only "restored" here is how a user finds an unexplained
299
+ // opencodex table in their config weeks later (#4812).
300
+ if (restore.retainedCodexProviderTable) {
301
+ console.log(` ${describeRetainedCodexProviderTable(restore.retainedCodexProviderTable)}`);
302
+ }
303
+ }
295
304
  else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
296
305
  if (!restore.success) process.exitCode = 1;
297
306
  // The Grok fence is the other managed config this command owns. Leaving it behind
@@ -339,6 +348,9 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
339
348
  console.error(`⚠️ native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` before using native Codex.`);
340
349
  process.exitCode = 1;
341
350
  }
351
+ else if (restore.retainedCodexProviderTable) {
352
+ console.log(`↩️ ${describeRetainedCodexProviderTable(restore.retainedCodexProviderTable)}`);
353
+ }
342
354
  const grok = stripGrokConfig();
343
355
  if (grok.changed) console.log(`↩️ ${grok.message}`);
344
356
  else if (!grok.ok) {
@@ -1,4 +1,5 @@
1
- import { chmodSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
2
3
  import { win32 } from "node:path";
3
4
  import { winswXmlPath } from "../lib/winsw";
4
5
  import { hardenSecretPath } from "../lib/windows-secret-acl";
@@ -9,7 +10,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmdirSync, unlinkSync } from "node:
9
10
  import { tmpdir } from "node:os";
10
11
  import { dirname, join } from "node:path";
11
12
  import { getConfigDir } from "../config";
12
- import { runWindowsElevatedScheduledTaskRegistration, WindowsSchtasksError } from "../lib/windows-elevation";
13
+ import { OCX_ELEVATED_STAGING_UNREADABLE, runWindowsElevatedScheduledTaskRegistration, WindowsSchtasksError, type StagedWindowsTaskXml } from "../lib/windows-elevation";
13
14
  import { defaultWinswEntry, installWinswService, statusWinswRaw, uninstallWinswService, WINSW_SERVICE_ID, type WinswStatus } from "../lib/winsw";
14
15
  import { forgetEphemeralSecretDir, forgetEphemeralSecretPath, hardenSecretDir } from "../lib/windows-secret-acl";
15
16
  import { recordOwnedConfigPath } from "../lib/config-ownership";
@@ -165,6 +166,197 @@ function cleanupWindowsSchedulerStage(
165
166
  if (cleanupError) throw cleanupError;
166
167
  }
167
168
 
169
+ /** A staged payload set for one elevated registration, plus the way to remove it. */
170
+ export interface StagedElevatedSchedulerRegistration {
171
+ readonly xml: StagedWindowsTaskXml;
172
+ readonly expectedExisting?: StagedWindowsTaskXml;
173
+ /** Remove every staged artifact. Idempotent, so a second call after success is a no-op. */
174
+ cleanup(): void;
175
+ }
176
+
177
+ export interface ElevatedSchedulerStagingDeps {
178
+ createStageDir?: () => string;
179
+ hardenDir?: (path: string) => void;
180
+ writePayload?: (path: string, bytes: Buffer) => void;
181
+ hardenPath?: (path: string) => void;
182
+ inspect?: (path: string) => { isSymbolicLink(): boolean; isFile(): boolean; isDirectory(): boolean };
183
+ removeStageDir?: (path: string) => void;
184
+ }
185
+
186
+ /**
187
+ * Stage the captured definitions an elevated registration needs, as files rather than
188
+ * as command-line payloads (#4692).
189
+ *
190
+ * A file that an administrator process will read is itself a privilege-escalation
191
+ * surface, so three properties have to hold together and none of them is sufficient
192
+ * alone:
193
+ *
194
+ * - **Access.** The directory is created fresh by `mkdtemp`, then ACL-hardened before
195
+ * anything is written into it, so another local account cannot read or replace the
196
+ * payload while the UAC prompt is open. Hardening the directory first is what makes
197
+ * the file private from the moment it exists.
198
+ * - **No reparse point.** Each artifact is inspected with `lstat` and rejected unless it
199
+ * is what it claims to be. `wx` already refuses to create over an existing name, which
200
+ * is the atomic step here — there is no replace path to race, because every path is
201
+ * inside a directory that did not exist a moment ago. The explicit check is what keeps
202
+ * that guarantee from depending on a reading of `O_EXCL` semantics.
203
+ * - **Tamper evidence.** The digest is taken over the exact bytes written, and the
204
+ * elevated script recomputes it over the bytes it reads. An ACL cannot cover this:
205
+ * a process running as the same user has the same SID and can rewrite the file, so
206
+ * the digest is the only thing that makes such a swap fail closed rather than
207
+ * silently register a different task definition.
208
+ *
209
+ * Payloads are UTF-16LE with no BOM, and the elevated process decodes them straight into
210
+ * `Register-ScheduledTask`. What is hashed is therefore exactly what is registered, with
211
+ * no trimming step in between that the two sides could disagree about.
212
+ */
213
+ export function stageElevatedSchedulerRegistration(
214
+ xml: string,
215
+ expectedExistingXml?: string,
216
+ deps: ElevatedSchedulerStagingDeps = {},
217
+ ): StagedElevatedSchedulerRegistration {
218
+ const createStageDir = deps.createStageDir
219
+ ?? (() => mkdtempSync(join(tmpdir(), WINDOWS_SCHEDULER_STAGE_PREFIX)));
220
+ const hardenDir = deps.hardenDir ?? ((path: string) => { hardenSecretDir(path, { required: true }); });
221
+ const writePayload = deps.writePayload ?? ((path: string, bytes: Buffer) => {
222
+ writeFileSync(path, bytes, { flag: "wx", mode: 0o600 });
223
+ });
224
+ const hardenPath = deps.hardenPath ?? ((path: string) => { hardenSecretPath(path, { required: true }); });
225
+ const inspect = deps.inspect ?? ((path: string) => lstatSync(path));
226
+ const removeStageDir = deps.removeStageDir ?? ((path: string) => { rmdirSync(path); });
227
+
228
+ const stageDir = createStageDir();
229
+ const files: string[] = [];
230
+ const cleanup = (): void => {
231
+ let failure: unknown;
232
+ for (const file of files.splice(0)) {
233
+ try {
234
+ unlinkSync(file);
235
+ forgetEphemeralSecretPath(file);
236
+ } catch (error) {
237
+ if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") forgetEphemeralSecretPath(file);
238
+ else if (failure === undefined) failure = error;
239
+ }
240
+ }
241
+ try {
242
+ removeStageDir(stageDir);
243
+ forgetEphemeralSecretDir(stageDir);
244
+ } catch (error) {
245
+ if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") forgetEphemeralSecretDir(stageDir);
246
+ else if (failure) throw new AggregateError([failure, error], "Elevated Task Scheduler staging cleanup failed.");
247
+ else failure = error;
248
+ }
249
+ if (failure) throw failure;
250
+ };
251
+
252
+ try {
253
+ try { chmodSync(stageDir, 0o700); } catch { /* required Windows ACL is authoritative */ }
254
+ const dirStats = inspect(stageDir);
255
+ if (dirStats.isSymbolicLink() || !dirStats.isDirectory()) {
256
+ throw new Error(`Refusing to stage an elevated Task Scheduler payload under a redirected path: ${stageDir}`);
257
+ }
258
+ hardenDir(stageDir);
259
+ const stage = (name: string, value: string): StagedWindowsTaskXml => {
260
+ const path = join(stageDir, name);
261
+ const bytes = Buffer.from(value, "utf16le");
262
+ writePayload(path, bytes);
263
+ files.push(path);
264
+ const stats = inspect(path);
265
+ if (stats.isSymbolicLink() || !stats.isFile()) {
266
+ throw new Error(`Refusing to stage an elevated Task Scheduler payload through a redirected path: ${path}`);
267
+ }
268
+ hardenPath(path);
269
+ return { path, sha256: createHash("sha256").update(bytes).digest("hex") };
270
+ };
271
+ return {
272
+ xml: stage("register.xml", xml),
273
+ ...(expectedExistingXml === undefined
274
+ ? {}
275
+ : { expectedExisting: stage("expected.xml", expectedExistingXml) }),
276
+ cleanup,
277
+ };
278
+ } catch (error) {
279
+ try {
280
+ cleanup();
281
+ } catch (cleanupError) {
282
+ throw new AggregateError(
283
+ [error, cleanupError],
284
+ "Elevated Task Scheduler staging failed and could not be cleaned up.",
285
+ );
286
+ }
287
+ throw error;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Turn an elevated registration exit code into something an operator can act on.
293
+ *
294
+ * The elevated process runs hidden, so nothing it writes survives; only the exit code
295
+ * crosses back. That makes an unexplained code the whole user-facing error, which is
296
+ * exactly what made the ENAMETOOLONG in #4692 expensive to diagnose. Staging introduces
297
+ * one new failure of its own — the payload is readable only by the account that created
298
+ * it, so an elevation answered with a different administrator's credentials cannot open
299
+ * it — and that one gets named along with its remedy rather than surfacing as a number.
300
+ */
301
+ export function describeElevatedRegistrationFailure(
302
+ failureLabel: string,
303
+ exitCode: number,
304
+ stageDir: string,
305
+ ): string {
306
+ if (exitCode === OCX_ELEVATED_STAGING_UNREADABLE) {
307
+ return `${failureLabel}: the elevated process could not read the staged task definition in `
308
+ + `${stageDir}. That directory is readable only by the account that staged it, so this `
309
+ + "happens when the UAC prompt was answered with a different administrator account. "
310
+ + "Approve the prompt as the signed-in user, or run the command again from a session "
311
+ + "already elevated as that user.";
312
+ }
313
+ return `${failureLabel} with exit code ${exitCode}.`;
314
+ }
315
+
316
+ /**
317
+ * Stage, elevate, and clean up — on every exit, including UAC cancellation and a
318
+ * synchronous spawn failure.
319
+ *
320
+ * A cleanup failure never replaces the registration failure it followed: an operator
321
+ * told only that a temp directory could not be removed would have no idea the task was
322
+ * never registered.
323
+ */
324
+ async function runStagedElevatedSchedulerRegistration(
325
+ taskName: string,
326
+ xml: string,
327
+ replace: boolean,
328
+ expectedExistingXml: string | undefined,
329
+ failureLabel: string,
330
+ ): Promise<void> {
331
+ const staged = stageElevatedSchedulerRegistration(xml, expectedExistingXml);
332
+ let failure: unknown;
333
+ try {
334
+ const exitCode = await runWindowsElevatedScheduledTaskRegistration(
335
+ taskName,
336
+ staged.xml,
337
+ replace,
338
+ staged.expectedExisting,
339
+ );
340
+ if (exitCode !== 0) {
341
+ failure = new Error(describeElevatedRegistrationFailure(failureLabel, exitCode, dirname(staged.xml.path)));
342
+ }
343
+ } catch (error) {
344
+ failure = error;
345
+ }
346
+ try {
347
+ staged.cleanup();
348
+ } catch (cleanupError) {
349
+ if (failure) {
350
+ throw new AggregateError(
351
+ [failure, cleanupError],
352
+ "Elevated Task Scheduler registration failed and its staging could not be cleaned up.",
353
+ );
354
+ }
355
+ throw cleanupError;
356
+ }
357
+ if (failure) throw failure;
358
+ }
359
+
168
360
  export function stageWindowsSchedulerRegistrationXml(
169
361
  attemptNonce: string,
170
362
  deps: WindowsSchedulerRegistrationStageDeps = {},
@@ -294,7 +486,8 @@ export async function registerFreshWindowsSchedulerTask(
294
486
  throw error;
295
487
  }
296
488
  // Register from the captured XML string inside the elevated process. Another
297
- // same-user process can mutate its own temp files, but cannot change this command.
489
+ // same-user process can mutate its own temp files, so the captured bytes are staged
490
+ // privately and the elevated script verifies their digest before registering them.
298
491
  // UAC can remain open for an arbitrary amount of time. Recheck the captured predecessor
299
492
  // before launch; the elevated helper repeats the same check after consent and before Force.
300
493
  assertReplacementPrecondition();
@@ -303,15 +496,13 @@ export async function registerFreshWindowsSchedulerTask(
303
496
  xml: string,
304
497
  replaceCurrent: boolean,
305
498
  previousXml?: string,
306
- ) => {
307
- const exitCode = await runWindowsElevatedScheduledTaskRegistration(
308
- taskName,
309
- xml,
310
- replaceCurrent,
311
- previousXml,
312
- );
313
- if (exitCode !== 0) throw new Error(`Background service install failed with exit code ${exitCode}.`);
314
- });
499
+ ) => runStagedElevatedSchedulerRegistration(
500
+ taskName,
501
+ xml,
502
+ replaceCurrent,
503
+ previousXml,
504
+ "Background service install failed",
505
+ ));
315
506
  await elevate(TASK, expectedXml, replace, expectedExistingXml);
316
507
  }
317
508
 
@@ -501,10 +692,13 @@ export async function restoreWindowsSchedulerTaskIfAbsent(registeredXml: string)
501
692
  ) {
502
693
  throw error;
503
694
  }
504
- const exitCode = await runWindowsElevatedScheduledTaskRegistration(TASK, registeredXml, false);
505
- if (exitCode !== 0) {
506
- throw new Error(`Task Scheduler rollback failed with exit code ${exitCode}.`);
507
- }
695
+ await runStagedElevatedSchedulerRegistration(
696
+ TASK,
697
+ registeredXml,
698
+ false,
699
+ undefined,
700
+ "Task Scheduler rollback failed",
701
+ );
508
702
  }
509
703
  const recoveredXml = statusWindowsXml();
510
704
  if (!windowsSchedulerRegistrationMatchesSnapshot(recoveredXml, registeredXml)) {
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { ELEVATION_REQUEST_TIMEOUT_MS, OCX_ELEVATED_PROTOCOL_FAILED, raceWithTimeout, resolveTrustedWindowsSchtasksExe, startElevatedSchtasksCreateAndRun, runWindowsElevated, toWindowsSchtasksError, WindowsElevationError, type ElevatedSchedulerOutcome, type ElevatedSchtasksCreateAndRunExecution, type ElevatedSchtasksCreateAndRunResult } from "../lib/windows-elevation";
7
7
  import { statusWinswRaw } from "../lib/winsw";
8
+ import { decodeWindowsTextBytes, type WindowsTextDecodeOptions } from "../lib/windows-text";
8
9
  import { isTestHomeGuardArmed } from "../lib/test-home-guard";
9
10
  import { TASK, windowsServiceScriptPath, windowsLauncherVbsPath, windowsTaskXmlPath, writeServiceInstallState } from "./state";
10
11
  import { buildWindowsSchtasksCreateArgs, windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy } from "./windows-taskxml";
@@ -16,28 +17,34 @@ import { WINSW_SERVICE_ID } from "../lib/winsw";
16
17
  * Decode schtasks stdout. `/query /xml` emits UTF-16LE (often with BOM) because the
17
18
  * registered task document is UTF-16; reading that as UTF-8 makes every health check
18
19
  * fail ("registration present but unhealthy") and rolls back a successful elevated create.
20
+ *
21
+ * Redirected output is NOT always UTF-16. Its encoding follows the console output code
22
+ * page of the spawning process tree rather than the XML declaration, so on a zh-CN host
23
+ * (ACP/OEMCP 936) the bytes are GBK — including inside a no-console background service.
24
+ * Decoding those as UTF-8 turned a CJK account name in
25
+ * `<SessionStateChangeTrigger><UserId>` into U+FFFD, the trigger scope then failed to
26
+ * match the correctly resolved `[SID, MACHINE\<name>]`, and `ocx service repair`
27
+ * aborted at its recognition gate on a registration OpenCodex had itself created. The
28
+ * same mojibake rolled back fresh installs at post-create verification (#4691).
29
+ *
30
+ * The fix is entirely in byte decoding, before any XML is parsed. The trigger scope stays
31
+ * an exact identity comparison: forgiving a replacement character there would let two
32
+ * different non-ASCII accounts collapse to the same value, which is a worse failure than
33
+ * the refusal it replaces.
34
+ *
35
+ * `decodeWindowsTextBytes` is the decoder this project already built for this class
36
+ * (UTF-16 -> strict UTF-8 -> the locale's legacy code page), and it already fixed the
37
+ * sibling `whoami`/PowerShell decode in `src/lib/windows-user-principal.ts` (#2914, and
38
+ * #722 for CP949). This call site was the last one still ending in a lossy UTF-8 decode.
19
39
  */
20
- export function decodeSchtasksOutput(buffer: Buffer): string {
21
- if (buffer.length === 0) return "";
22
- const bomUtf16Le = buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe;
23
- const bomUtf16Be = buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff;
24
- const looksUtf16Le = buffer.length >= 4
25
- && buffer[1] === 0x00
26
- && buffer[3] === 0x00
27
- && buffer[0] !== 0x00;
28
- if (bomUtf16Le || looksUtf16Le) {
29
- return buffer.toString("utf16le").replace(/^\uFEFF/, "").trim();
30
- }
31
- if (bomUtf16Be) {
32
- // Swap pairs then decode as utf16le.
33
- const swapped = Buffer.alloc(buffer.length - 2);
34
- for (let i = 2; i + 1 < buffer.length; i += 2) {
35
- swapped[i - 2] = buffer[i + 1]!;
36
- swapped[i - 1] = buffer[i]!;
37
- }
38
- return swapped.toString("utf16le").trim();
39
- }
40
- return buffer.toString("utf8").replace(/^\uFEFF/, "").trim();
40
+ export function decodeSchtasksOutput(
41
+ buffer: Buffer,
42
+ options: WindowsTextDecodeOptions = {},
43
+ ): string {
44
+ // `options` exists so a test can pin the code page; every production call passes the
45
+ // buffer alone and uses the active Intl locale, which is available to a service with no
46
+ // console because the selection reads the process locale rather than a console handle.
47
+ return decodeWindowsTextBytes(buffer, options);
41
48
  }
42
49
 
43
50
  function runFile(file: string, args: string[]): string {
package/src/service.ts CHANGED
@@ -19,7 +19,7 @@ export { decodeSchtasksOutput, setQuerySchtasksForTests, formatWindowsSchedulerS
19
19
  export type { WindowsSchedulerXmlState } from "./service/windows-taskxml";
20
20
  export { buildWindowsServiceScript, buildWindowsSchtasksCreateArgs, buildWindowsSchtasksCreateArgsForXml, buildWindowsLauncherVbs, buildWindowsTaskXml, buildWindowsTaskXmlDocument, windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy, readWindowsSchedulerXmlState } from "./service/windows-taskxml";
21
21
  export type { WindowsSchedulerRegistrationStageDeps, FreshWindowsSchedulerRegistrationDeps, RemoveNativeWindowsServiceDeps } from "./service/windows-ops";
22
- export { windowsListenPort, winswListenPort, writeServiceDefinitionFile, definitionCarriesCredential, stageWindowsSchedulerRegistrationXml, registerFreshWindowsSchedulerTask, removeNativeWindowsServiceForScheduler, assertWindowsNativeServiceAccountSupported, isWindowsSchedulerEndBenign, stopWindows, stopWindowsChecked, classifyWindowsServiceStop } from "./service/windows-ops";
22
+ export { windowsListenPort, winswListenPort, writeServiceDefinitionFile, definitionCarriesCredential, stageWindowsSchedulerRegistrationXml, stageElevatedSchedulerRegistration, describeElevatedRegistrationFailure, registerFreshWindowsSchedulerTask, removeNativeWindowsServiceForScheduler, assertWindowsNativeServiceAccountSupported, isWindowsSchedulerEndBenign, stopWindows, stopWindowsChecked, classifyWindowsServiceStop } from "./service/windows-ops";
23
23
  export type { ServiceRepairVerb, RepairServiceDeps } from "./service/repair";
24
24
  export { repairService } from "./service/repair";
25
25
  export type { ServiceInstallPreparationDeps, FreshWindowsSchedulerInstallDeps, ServiceStopOutcome, ServiceUninstallOutcome } from "./service/orchestration";