@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/service.ts CHANGED
@@ -12,10 +12,11 @@ import { homedir } from "node:os";
12
12
  import { dirname, join, resolve } from "node:path";
13
13
  import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
14
14
  import { loadConfig } from "./config";
15
- import { restoreNativeCodex } from "./codex/inject";
15
+ import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
16
16
  import { stripGrokConfig } from "./grok/inject";
17
17
  import { isWslRuntime } from "./codex/home";
18
- import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
18
+ import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "./lib/bun-runtime";
19
+ import type { BunRuntimeSource } from "./lib/bun-runtime";
19
20
  import { isProcessAlive, stopProxy } from "./lib/process-control";
20
21
  import { serviceApiTokenFilePath } from "./lib/service-secrets";
21
22
  import { randomUUID } from "node:crypto";
@@ -43,11 +44,15 @@ const TASK = "opencodex-proxy";
43
44
 
44
45
  export type ServiceBackend = "scheduler" | "native";
45
46
 
46
- function cliEntry(): { bun: string; cli: string } {
47
+ function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
47
48
  // Bake the bundled Bun (npm global prefix, survives `ocx update`) rather than
48
49
  // a transient system Bun, so launchd/systemd/schtasks keep resolving even if a
49
50
  // standalone Bun is later removed. The CLI entry lives at src/cli/index.ts.
50
- return { bun: durableBunPath(), cli: join(import.meta.dir, "cli", "index.ts") };
51
+ //
52
+ // Path and provenance come from ONE resolution so the marker can never describe a
53
+ // different binary than the one actually baked.
54
+ const runtime = durableBunRuntime();
55
+ return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(import.meta.dir, "cli", "index.ts") };
51
56
  }
52
57
 
53
58
  function plistPath(): string {
@@ -169,13 +174,80 @@ function readServiceInstallState(): ServiceInstallState | null {
169
174
  return null;
170
175
  }
171
176
 
172
- /** Single accessor for update/reinstall code v1/legacy state maps to scheduler. */
177
+ /** What ONE state path said. Absent, unreadable and invalid are different answers. */
178
+ export type ServiceStateEvidence =
179
+ | { readonly path: string; readonly kind: "absent" }
180
+ | { readonly path: string; readonly kind: "unreadable"; readonly reason: string }
181
+ | { readonly path: string; readonly kind: "invalid" }
182
+ | { readonly path: string; readonly kind: "valid"; readonly state: ServiceInstallState };
183
+
184
+ /**
185
+ * Every state path, with what each one said.
186
+ *
187
+ * `readServiceInstallState` returns the FIRST path that parsed and discards the
188
+ * rest, so a valid mirror beside a corrupt one reads as clean. That is the right
189
+ * behavior for callers that just need the install state; it is the wrong input
190
+ * for deciding ownership, where a disagreement between mirrors is exactly the
191
+ * evidence that matters.
192
+ */
193
+ export function inspectServiceStateEvidence(
194
+ paths: readonly string[] = serviceStatePaths(),
195
+ ): readonly ServiceStateEvidence[] {
196
+ return paths.map((path): ServiceStateEvidence => {
197
+ let raw: string;
198
+ try {
199
+ raw = readFileSync(path, "utf8");
200
+ } catch (error) {
201
+ const code = error && typeof error === "object" && "code" in error
202
+ ? String((error as { code?: unknown }).code)
203
+ : "";
204
+ // ENOENT is an answer. EACCES, ENOTDIR and the rest are a failure to ask,
205
+ // and collapsing them into absence is how a locked-down state file would
206
+ // become permission to write.
207
+ if (code === "ENOENT") return { path, kind: "absent" };
208
+ return { path, kind: "unreadable", reason: code || String(error) };
209
+ }
210
+ let parsed: ServiceInstallState | null;
211
+ try {
212
+ parsed = parseServiceInstallState(JSON.parse(raw));
213
+ } catch {
214
+ return { path, kind: "invalid" };
215
+ }
216
+ return parsed ? { path, kind: "valid", state: parsed } : { path, kind: "invalid" };
217
+ });
218
+ }
219
+
220
+ /** The homes this process is actually using, for comparison against a claim. */
221
+ export function currentServiceHomes(): { codexHome: string; opencodexHome: string } {
222
+ return { codexHome: currentCodexHome(), opencodexHome: currentOpenCodexHome() };
223
+ }
224
+
225
+ export function serviceHomeMatches(a: string, b: string): boolean {
226
+ return normalizePathForCompare(a) === normalizePathForCompare(b);
227
+ }
228
+
229
+ /** Single accessor for backend-sensitive service code — v1/legacy state maps to scheduler. */
173
230
  export function readServiceBackend(): ServiceBackend {
174
231
  return readServiceInstallState()?.backend === "native" ? "native" : "scheduler";
175
232
  }
176
233
 
177
- /** The `ocx` argv that reinstalls the currently-chosen service backend (update paths). */
234
+ /**
235
+ * The `ocx` argv that refreshes an already-installed service after an update.
236
+ *
237
+ * `repair` discovers the installed backend itself and, on Windows scheduler installs,
238
+ * rewrites the wrapper assets and restarts the existing task WITHOUT `schtasks /create`
239
+ * (see repairService below). `install` always reaches `/create`, which requires
240
+ * elevation — so an ordinary non-elevated `ocx update` used to stop a working proxy and
241
+ * then fail to bring its service back.
242
+ *
243
+ * The historical export name is kept for callers outside this module.
244
+ */
178
245
  export function serviceReinstallArgs(): string[] {
246
+ return ["service", "repair"];
247
+ }
248
+
249
+ /** The `ocx` argv that registers a service from scratch, preserving the chosen backend. */
250
+ export function serviceInstallArgs(): string[] {
179
251
  return readServiceBackend() === "native" ? ["service", "install", "--native"] : ["service", "install"];
180
252
  }
181
253
 
@@ -229,6 +301,7 @@ export function assertServiceEnvironmentMatchesInstall(): void {
229
301
  }
230
302
  }
231
303
 
304
+
232
305
  function plistString(value: string): string {
233
306
  return value
234
307
  .replace(/&/g, "&")
@@ -243,13 +316,31 @@ function isLoopbackHostname(hostname: string | undefined): boolean {
243
316
  return normalized === "" || normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
244
317
  }
245
318
 
319
+ /**
320
+ * The `ocx` command a user should rerun for the service state they actually have.
321
+ *
322
+ * `installed` alone is not enough: `repairService()` refuses a Task-Scheduler-plus-WinSW
323
+ * conflict outright, so recommending repair there names a command guaranteed to fail.
324
+ * Install IS the valid conflict recovery, because `installWindows` removes the native
325
+ * backend first. Exported so the guard tests the real selector rather than a copy of it.
326
+ */
327
+ export function serviceRetryCommand(
328
+ diag: Pick<ServiceDiagnostic, "installed" | "conflict"> = diagnoseService(),
329
+ ): string {
330
+ return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
331
+ }
332
+
246
333
  export function assertServiceAuthEnvironment(): void {
247
334
  const config = loadConfig();
248
335
  if (isLoopbackHostname(config.hostname)) return;
249
336
  if (process.env.OPENCODEX_API_AUTH_TOKEN?.trim()) return;
337
+ // Reached from `service repair` as well as `install`, so name a command that can
338
+ // actually succeed (see serviceRetryCommand).
339
+ const diag = diagnoseService();
340
+ const retry = serviceRetryCommand(diag);
250
341
  throw new Error(
251
- "OPENCODEX_API_AUTH_TOKEN is required before installing a service for non-loopback hostname. " +
252
- "Set it in the same shell, then rerun `ocx service install`.",
342
+ `OPENCODEX_API_AUTH_TOKEN is required before ${diag.installed ? "refreshing" : "installing"} a service `
343
+ + `for non-loopback hostname. Set it in the same shell, then rerun \`${retry}\`.`,
253
344
  );
254
345
  }
255
346
 
@@ -268,13 +359,15 @@ function writeServiceApiTokenFile(): string | null {
268
359
  }
269
360
 
270
361
  export function buildPlist(): string {
271
- const { bun, cli } = cliEntry();
362
+ const { bun, bunRuntimeSource, cli } = cliEntry();
272
363
  const log = logPath();
273
364
  const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
274
365
  const codexHome = process.env.CODEX_HOME?.trim();
275
366
  const opencodexHome = process.env.OPENCODEX_HOME?.trim();
276
367
  const envLines = [
277
368
  ` <key>OCX_SERVICE</key><string>1</string>`,
369
+ ` <key>${BUN_RUNTIME_SOURCE_ENV}</key><string>${bunRuntimeSource}</string>`,
370
+ ` <key>${BUN_RUNTIME_PATH_ENV}</key><string>${plistString(bun)}</string>`,
278
371
  ` <key>PATH</key><string>${plistString(path)}</string>`,
279
372
  codexHome ? ` <key>CODEX_HOME</key><string>${plistString(codexHome)}</string>` : null,
280
373
  opencodexHome ? ` <key>OPENCODEX_HOME</key><string>${plistString(opencodexHome)}</string>` : null,
@@ -491,17 +584,14 @@ async function reportServiceServing(
491
584
  }
492
585
 
493
586
  /**
494
- * The reinstall command for the CURRENTLY INSTALLED backend.
587
+ * The command that repairs the CURRENTLY INSTALLED backend without re-registering it.
495
588
  *
496
- * Plain `ocx service install` on a native/WinSW install runs installWindows's
497
- * transactional backend switch, which tears down WinSW and replaces it with the Task
498
- * Scheduler backend. Advising it in a repair hint would silently change the user's
499
- * backend, so the hint has to carry `--native` when that is what is installed.
589
+ * `ocx service repair` reads the recorded backend itself, so it cannot silently switch a
590
+ * WinSW install to Task Scheduler the way a plain `ocx service install` would, and on
591
+ * Windows it needs no elevation because it never calls `schtasks /create`.
500
592
  */
501
593
  function serviceRepairCommand(): string {
502
- return process.platform === "win32" && readServiceBackend() === "native"
503
- ? "ocx service install --native"
504
- : "ocx service install";
594
+ return "ocx service repair";
505
595
  }
506
596
 
507
597
  function systemdQuote(value: string): string {
@@ -544,16 +634,28 @@ function sh(cmd: string): string {
544
634
  export function runLaunchctl(
545
635
  args: string[],
546
636
  deps: { run?: typeof spawnSync } = {},
547
- ): { ok: boolean; stdout: string; stderr: string } {
637
+ ): { ok: boolean; stdout: string; stderr: string; status: number | null } {
548
638
  const run = deps.run ?? spawnSync;
549
639
  const result = run("/bin/launchctl", args, { encoding: "utf8", windowsHide: true });
550
640
  // `error` is set when the spawn itself failed (ENOENT off macOS) and `status` is
551
641
  // null for a signalled child; neither may be reported as success.
552
- if (result.error) return { ok: false, stdout: "", stderr: String(result.error.message ?? "") };
642
+ if (result.error) {
643
+ return { ok: false, stdout: "", stderr: String(result.error.message ?? ""), status: null };
644
+ }
553
645
  return {
554
646
  ok: result.status === 0,
555
647
  stdout: String(result.stdout ?? "").trim(),
556
648
  stderr: String(result.stderr ?? "").trim(),
649
+ /*
650
+ * The NUMBER, not just its zero-ness.
651
+ *
652
+ * `launchctl print` distinguishes "that domain does not exist" (112) from
653
+ * "the domain answered and has no such service" (113), and an ownership
654
+ * probe needs that difference: the second proves absence, the first only
655
+ * proves we could not look. Collapsing both into `ok: false` forced callers
656
+ * to parse stderr, which Apple does not treat as a stable interface.
657
+ */
658
+ status: result.status ?? null,
557
659
  };
558
660
  }
559
661
 
@@ -788,6 +890,10 @@ export function windowsSchedulerTaskInstalled(taskName = TASK): boolean {
788
890
  export interface WindowsSchedulerInstallVerification {
789
891
  taskInstalled: boolean;
790
892
  registrationHealthy: boolean;
893
+ /** Well-formed XML that is PUBLISHED but policy-violating — permanent, never
894
+ * worth a settle retry (vs an empty/unreadable view, which is publication
895
+ * lag and transient). */
896
+ registrationInvalid: boolean;
791
897
  assetsHealthy: boolean;
792
898
  nativeServiceAbsent: boolean;
793
899
  /** True when SCM probe failed; not a proven WinSW presence. */
@@ -808,6 +914,10 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
808
914
  }): WindowsSchedulerInstallVerification {
809
915
  const registrationHealthy = inputs.xml.length > 0
810
916
  && windowsTaskRegistrationHealthy(inputs.xml, inputs.wscript, inputs.launcher);
917
+ // Permanent invalidity: the XML IS published but violates the registration
918
+ // contract — no amount of settling changes it. Empty/unreadable XML stays
919
+ // transient (publication lag).
920
+ const registrationInvalid = inputs.taskInstalled && inputs.xml.length > 0 && !registrationHealthy;
811
921
  const assetsHealthy = inputs.assetsExist;
812
922
  const nativeServiceAbsent = inputs.nativeStatus === "nonexistent";
813
923
  const nativeStatusUnknown = inputs.nativeStatus === "unknown";
@@ -831,6 +941,7 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
831
941
  return {
832
942
  taskInstalled: inputs.taskInstalled,
833
943
  registrationHealthy,
944
+ registrationInvalid,
834
945
  assetsHealthy,
835
946
  nativeServiceAbsent,
836
947
  nativeStatusUnknown,
@@ -909,6 +1020,8 @@ type FinalizeHooks = {
909
1020
  /** Defense-in-depth: late reconciliation must still own this attempt. */
910
1021
  stillOwnsAttempt?: (attemptId: string) => boolean;
911
1022
  requestTimeoutMs?: number;
1023
+ /** Test-only seam for the post-create settle backoff; real installs use a timer. */
1024
+ settleDelay?: (ms: number) => Promise<void>;
912
1025
  };
913
1026
 
914
1027
  let finalizeHooks: FinalizeHooks | null = null;
@@ -973,6 +1086,68 @@ function attemptStillOwned(options: ApplyElevatedOptions): boolean {
973
1086
  return !check || check(options.attemptId);
974
1087
  }
975
1088
 
1089
+ /**
1090
+ * Bounded post-create backoff, 1.1s total. Task Scheduler's non-elevated view can
1091
+ * lag an elevated `/create` by a few hundred milliseconds, so a single verification
1092
+ * would roll back a task that is merely not visible yet.
1093
+ */
1094
+ const SCHEDULER_SETTLE_DELAYS_MS = [50, 150, 300, 600] as const;
1095
+
1096
+ /**
1097
+ * Whether a failed verification is still worth re-checking after a short delay.
1098
+ *
1099
+ * Retrying is confined to states that a lagging scheduler view actually produces:
1100
+ * the task is not visible yet, or it is visible but its registration has not been
1101
+ * published in full. Everything else keeps its existing fail-closed meaning and is
1102
+ * rejected here so no delay can turn it into a pass:
1103
+ *
1104
+ * - a proven conflict (both backends present) is a real dual-backend install;
1105
+ * - missing assets are missing on disk, which no amount of waiting creates;
1106
+ * - a WinSW service that is proven present (`started`/`stopped`) is never absent
1107
+ * later. This is checked independently of `conflict`, which only becomes true
1108
+ * once the task itself is visible — while the task is still invisible the pair
1109
+ * is `conflict: false` with `nativeServiceAbsent: false`, and that must not retry;
1110
+ * - unknown SCM status is unproven rather than transient, and has its own
1111
+ * task-preserving branch below.
1112
+ */
1113
+ /** Exported for tests: the transient-vs-permanent settle decision. */
1114
+ export function schedulerVerificationMaySettle(v: WindowsSchedulerInstallVerification): boolean {
1115
+ if (v.ok) return false;
1116
+ if (v.conflict) return false;
1117
+ if (!v.assetsHealthy) return false;
1118
+ if (!v.nativeServiceAbsent) return false;
1119
+ // A published-but-invalid registration is permanent: no delay repairs it.
1120
+ if (v.registrationInvalid) return false;
1121
+ return !v.taskInstalled || !v.registrationHealthy;
1122
+ }
1123
+
1124
+ function settleDelay(ms: number): Promise<void> {
1125
+ const hook = finalizeHooks?.settleDelay;
1126
+ if (hook) return hook(ms);
1127
+ return new Promise(resolve => setTimeout(resolve, ms));
1128
+ }
1129
+
1130
+ /**
1131
+ * Verify the elevated install, re-checking only while the failure looks like a
1132
+ * scheduler view that has not caught up yet. Returns `null` when this attempt lost
1133
+ * ownership mid-settle: a newer attempt owns the task, so this one must neither
1134
+ * write install state nor roll anything back.
1135
+ */
1136
+ async function verifyWindowsSchedulerInstallAfterSettle(
1137
+ options: ApplyElevatedOptions,
1138
+ ): Promise<WindowsSchedulerInstallVerification | null> {
1139
+ const verify = finalizeHooks?.verify ?? verifyWindowsSchedulerInstall;
1140
+ let verification = verify();
1141
+ for (const delayMs of SCHEDULER_SETTLE_DELAYS_MS) {
1142
+ if (!schedulerVerificationMaySettle(verification)) break;
1143
+ if (!attemptStillOwned(options)) return null;
1144
+ await settleDelay(delayMs);
1145
+ if (!attemptStillOwned(options)) return null;
1146
+ verification = verify();
1147
+ }
1148
+ return verification;
1149
+ }
1150
+
976
1151
  async function applyElevatedSchedulerResult(
977
1152
  result: ElevatedSchtasksCreateAndRunResult,
978
1153
  options: ApplyElevatedOptions,
@@ -1002,7 +1177,9 @@ async function applyElevatedSchedulerResult(
1002
1177
  await reconcileUnknownElevatedOutcome(result.exitCode);
1003
1178
  }
1004
1179
 
1005
- const verification = (finalizeHooks?.verify ?? verifyWindowsSchedulerInstall)();
1180
+ const verification = await verifyWindowsSchedulerInstallAfterSettle(options);
1181
+ // Ownership moved to a newer attempt while settling; that attempt owns the outcome.
1182
+ if (!verification) return;
1006
1183
  if (!verification.ok) {
1007
1184
  // Preserve a healthy elevated task when WinSW absence cannot be proven (unknown SCM status).
1008
1185
  // Unknown is not a confirmed dual-backend conflict; install state is still withheld.
@@ -1019,6 +1196,9 @@ async function applyElevatedSchedulerResult(
1019
1196
  "Installation state was not written.",
1020
1197
  ]);
1021
1198
  }
1199
+ // Rollback deletes a real task, so it needs the same ownership fence as the
1200
+ // state write below: a stale attempt must never delete a newer attempt's task.
1201
+ if (!attemptStillOwned(options)) return;
1022
1202
  const rollbackError = await rollbackElevatedSchedulerTask();
1023
1203
  const parts = [
1024
1204
  "Elevated Task Scheduler registration did not produce a conflict-free install.",
@@ -1259,8 +1439,9 @@ function taskXmlRunLevelAcceptable(principal: string): boolean {
1259
1439
  }
1260
1440
 
1261
1441
  export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServiceListenPort()): string {
1262
- const { bun, cli } = entry;
1263
- const bunRuntime = durableBunRuntime();
1442
+ // Provenance rides along with the entry: a second durableBunRuntime() call here could
1443
+ // resolve differently from the binary the caller actually baked.
1444
+ const { bun, bunRuntimeSource, cli } = entry;
1264
1445
  const path = process.env.PATH ?? "";
1265
1446
  const lines = [
1266
1447
  "@echo off",
@@ -1269,6 +1450,8 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
1269
1450
  // it to UTF-8 is safe (no leak into user shells) and lets cmd parse UTF-8 remnants.
1270
1451
  "chcp 65001 >nul",
1271
1452
  windowsBatchSet("OCX_SERVICE", "1"),
1453
+ windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource),
1454
+ windowsBatchSet(BUN_RUNTIME_PATH_ENV, bun, "path"),
1272
1455
  windowsBatchSet("PATH", path, "pathList"),
1273
1456
  windowsBatchSet("CODEX_HOME", process.env.CODEX_HOME?.trim(), "path"),
1274
1457
  windowsBatchSet("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim(), "path"),
@@ -1282,7 +1465,7 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
1282
1465
  ":loop",
1283
1466
  '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] opencodex service wrapper start',
1284
1467
  '>>"%OCX_SERVICE_LOG%" echo bun="%OCX_BUN%"',
1285
- `>>"%OCX_SERVICE_LOG%" echo bun_source="${bunRuntime.source}"`,
1468
+ `>>"%OCX_SERVICE_LOG%" echo bun_source="${bunRuntimeSource}"`,
1286
1469
  '>>"%OCX_SERVICE_LOG%" echo cli="%OCX_CLI%"',
1287
1470
  '>>"%OCX_SERVICE_LOG%" echo opencodex_home="%OPENCODEX_HOME%"',
1288
1471
  '>>"%OCX_SERVICE_LOG%" echo codex_home="%CODEX_HOME%"',
@@ -1523,6 +1706,9 @@ function installLaunchd(): void {
1523
1706
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
1524
1707
  writeServiceApiTokenFile();
1525
1708
  const p = plistPath();
1709
+ // Capture this BEFORE writing: the write below makes the plist exist unconditionally,
1710
+ // so a post-write existsSync would call every fresh install an "installed" service.
1711
+ const wasInstalled = existsSync(p);
1526
1712
  writeFileSync(p, buildPlist(), "utf8");
1527
1713
  // Best-effort: an absent job is fine here, and a failed unload is caught by the
1528
1714
  // load verification below with a better message than a raw unload error.
@@ -1535,7 +1721,9 @@ function installLaunchd(): void {
1535
1721
  `launchctl could not load ${p}: ${loaded.stderr || "load reported failure"}\n`
1536
1722
  + "A previous job may still be bootstrapped. Try:\n"
1537
1723
  + ` launchctl bootout ${launchdGuiDomain()}/${LABEL}\n`
1538
- + "then re-run 'ocx service install'.",
1724
+ // macOS `service repair` delegates straight to installLaunchd, so this fires for
1725
+ // an already-installed service too; repair reloads it without re-registering.
1726
+ + `then re-run '${wasInstalled ? "ocx service repair" : "ocx service install"}'.`,
1539
1727
  );
1540
1728
  }
1541
1729
  writeServiceInstallState();
@@ -1572,8 +1760,8 @@ export function startLaunchd(deps: {
1572
1760
  throw new Error(
1573
1761
  `launchctl could not load ${p}: ${loaded.stderr || "load reported failure"}\n`
1574
1762
  + (live.loaded
1575
- ? `launchd is running an OLDER plist. Fix:\n launchctl bootout ${launchdGuiDomain()}/${LABEL}\n ocx service install`
1576
- : "The job is not loaded. Run 'ocx service install' to re-register it."),
1763
+ ? `launchd is running an OLDER plist. Fix:\n launchctl bootout ${launchdGuiDomain()}/${LABEL}\n ocx service repair`
1764
+ : "The job is not loaded. Run 'ocx service repair' to reload it."),
1577
1765
  );
1578
1766
  }
1579
1767
  function stopLaunchd(): void { try { sh(`launchctl unload "${plistPath()}"`); } catch { /* not loaded */ } }
@@ -1836,7 +2024,7 @@ export function bakedServicePathsDiagnostic(): string | null {
1836
2024
  if (!state?.bunPath || !state?.cliPath) return null;
1837
2025
  const missing = [state.bunPath, state.cliPath].filter(path => !existsSync(path));
1838
2026
  if (missing.length === 0) return null;
1839
- return `STALE baked paths (missing: ${missing.join(", ")}) — run 'ocx service install' to re-bake`;
2027
+ return `STALE baked paths (missing: ${missing.join(", ")}) — run 'ocx service repair' to re-bake`;
1840
2028
  }
1841
2029
 
1842
2030
  function serviceDiagnosticsSummary(): string {
@@ -1854,13 +2042,15 @@ function unitPath(): string {
1854
2042
  }
1855
2043
 
1856
2044
  export function buildUnit(): string {
1857
- const { bun, cli } = cliEntry();
2045
+ const { bun, bunRuntimeSource, cli } = cliEntry();
1858
2046
  const log = logPath();
1859
2047
  const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
1860
2048
  const codexHome = systemdEnvironmentAssignment("CODEX_HOME", process.env.CODEX_HOME?.trim());
1861
2049
  const opencodexHome = systemdEnvironmentAssignment("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim());
1862
2050
  const envLines = [
1863
2051
  systemdEnvironmentAssignment("OCX_SERVICE", "1"),
2052
+ systemdEnvironmentAssignment(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource),
2053
+ systemdEnvironmentAssignment(BUN_RUNTIME_PATH_ENV, bun),
1864
2054
  systemdEnvironmentAssignment("PATH", path),
1865
2055
  codexHome,
1866
2056
  opencodexHome,
@@ -2251,7 +2441,7 @@ export function deriveWindowsServiceDiagnostic(inputs: WindowsServiceDiagnosticI
2251
2441
  const detail = conflict
2252
2442
  ? "CONFLICT: Task Scheduler and native WinSW are both present — run 'ocx service uninstall' then reinstall one"
2253
2443
  : stale
2254
- ? "stale or missing service assets — run 'ocx service install' to repair"
2444
+ ? "stale or missing service assets — run 'ocx service repair'"
2255
2445
  : schedulerInstalled
2256
2446
  ? schedulerEnabled ? "Task Scheduler enabled" : "Task Scheduler disabled"
2257
2447
  : nativeInstalled
@@ -2373,7 +2563,7 @@ export async function serviceStatusReport(
2373
2563
  : null);
2374
2564
  const staleLine = stalePlist && stalePlist.loaded && !stalePlist.matchesPlist
2375
2565
  ? " launchd is running an OLDER plist than the one on disk.\n"
2376
- + ` Fix: launchctl bootout gui/$(id -u)/${LABEL} && ocx service install\n`
2566
+ + ` Fix: launchctl bootout gui/$(id -u)/${LABEL} && ocx service repair\n`
2377
2567
  : "";
2378
2568
 
2379
2569
  return `⚠️ ${diag.summary}\n`
@@ -2494,7 +2684,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
2494
2684
  process.exitCode = 1;
2495
2685
  break;
2496
2686
  }
2497
- const restore = restoreNativeCodex();
2687
+ const restore = await restoreNativeCodexAsync();
2498
2688
  if (restore.success) console.log("✅ service stopped + native Codex restored.");
2499
2689
  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.`);
2500
2690
  // The Grok fence is the other managed config this command owns. Leaving it behind
@@ -2532,7 +2722,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
2532
2722
  process.exit(1);
2533
2723
  }
2534
2724
  {
2535
- const restore = restoreNativeCodex();
2725
+ const restore = await restoreNativeCodexAsync();
2536
2726
  if (!restore.success) {
2537
2727
  console.error(`⚠️ native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` before using native Codex.`);
2538
2728
  }
@@ -3,6 +3,9 @@ param(
3
3
  [Parameter(Mandatory = $true)][string]$CliPath,
4
4
  [Parameter(Mandatory = $true)][string]$CodexHome,
5
5
  [Parameter(Mandatory = $true)][string]$OpenCodexHome,
6
+ # Provenance of $BunPath, chosen when the tray entry was built. Optional so an
7
+ # already-installed launcher command from an older version still starts.
8
+ [ValidateSet("", "override", "bundled", "process")][string]$BunRuntimeSource = "",
6
9
  [ValidateSet("Run", "Stop")][string]$Mode = "Run",
7
10
  [int]$HostPid = 0
8
11
  )
@@ -92,6 +95,12 @@ function Start-OcxCommand([string[]]$CommandArgs) {
92
95
  $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
93
96
  $psi.EnvironmentVariables["CODEX_HOME"] = $CodexHome
94
97
  $psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
98
+ if ($BunRuntimeSource) {
99
+ $psi.EnvironmentVariables["OCX_BUN_RUNTIME_SOURCE"] = $BunRuntimeSource
100
+ # Paired with the source so a later relaunch can tell the marker still describes
101
+ # this binary rather than one it merely inherited.
102
+ $psi.EnvironmentVariables["OCX_BUN_RUNTIME_PATH"] = $BunPath
103
+ }
95
104
  $process = [System.Diagnostics.Process]::Start($psi)
96
105
  if ($null -ne $process) { $process.Dispose() }
97
106
  Write-ActionLog "dispatched $($CommandArgs -join ' ')"
@@ -4,8 +4,9 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync,
4
4
  import { homedir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { expandUserPath, getConfigDir } from "../config";
7
- import { durableBunPath } from "../lib/bun-runtime";
8
- import { forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
7
+ import { durableBunRuntime } from "../lib/bun-runtime";
8
+ import type { BunRuntimeSource } from "../lib/bun-runtime";
9
+ import { forgetEphemeralSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
9
10
  import { recordOwnedConfigPath } from "../lib/config-ownership";
10
11
 
11
12
  const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
@@ -20,6 +21,8 @@ const TRAY_ICON_FILES = [
20
21
 
21
22
  export interface WindowsTrayEntry {
22
23
  bun: string;
24
+ /** Provenance of `bun`, resolved together with it. */
25
+ bunRuntimeSource: BunRuntimeSource;
23
26
  cli: string;
24
27
  script: string;
25
28
  codexHome: string;
@@ -81,8 +84,10 @@ function currentCodexHome(): string {
81
84
  }
82
85
 
83
86
  function currentEntry(): WindowsTrayEntry {
87
+ const runtime = durableBunRuntime();
84
88
  return {
85
- bun: durableBunPath(),
89
+ bun: runtime.path,
90
+ bunRuntimeSource: runtime.source,
86
91
  cli: join(import.meta.dir, "..", "cli", "index.ts"),
87
92
  script: installedTrayScriptPath(),
88
93
  codexHome: currentCodexHome(),
@@ -136,6 +141,7 @@ export function windowsTrayProcessArgs(entry: WindowsTrayEntry, mode: "Run" | "S
136
141
  "-WindowStyle", "Hidden",
137
142
  "-File", safePath(entry.script),
138
143
  "-BunPath", safePath(entry.bun),
144
+ "-BunRuntimeSource", entry.bunRuntimeSource,
139
145
  "-CliPath", safePath(entry.cli),
140
146
  "-CodexHome", safePath(entry.codexHome),
141
147
  "-OpenCodexHome", safePath(entry.opencodexHome),
@@ -170,6 +176,7 @@ export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, power
170
176
  "-WindowStyle", "Hidden",
171
177
  "-File", quoteRunValue(entry.script),
172
178
  "-BunPath", quoteRunValue(entry.bun),
179
+ "-BunRuntimeSource", entry.bunRuntimeSource,
173
180
  "-CliPath", quoteRunValue(entry.cli),
174
181
  "-CodexHome", quoteRunValue(entry.codexHome),
175
182
  "-OpenCodexHome", quoteRunValue(entry.opencodexHome),
@@ -233,7 +240,8 @@ export function replaceWindowsTrayOwnedFile(
233
240
  harden: target => {
234
241
  try { chmodSync(target, 0o600); } catch { /* best-effort */ }
235
242
  if (process.platform !== "win32") return;
236
- const hardened = hardenSecretPath(target, { required: true });
243
+ // Destination-keyed timeout memo: retries share one memo per final path.
244
+ const hardened = hardenSecretPath(target, { required: true, timeoutMemoKey: path });
237
245
  if (!hardened.ok) throw new Error("Windows tray ACL hardening did not complete; refusing to persist executable state.");
238
246
  },
239
247
  rename: renameSync,
@@ -247,14 +255,14 @@ export function replaceWindowsTrayOwnedFile(
247
255
  io.harden(temporary);
248
256
  io.rename(temporary, path);
249
257
  renamed = true;
250
- forgetHardenedSecretPath(temporary);
258
+ forgetEphemeralSecretPath(temporary);
251
259
  } finally {
252
260
  if (!renamed) {
253
261
  try {
254
262
  io.unlink(temporary);
255
- forgetHardenedSecretPath(temporary);
263
+ forgetEphemeralSecretPath(temporary);
256
264
  } catch (error) {
257
- if ((error as NodeJS.ErrnoException).code === "ENOENT") forgetHardenedSecretPath(temporary);
265
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") forgetEphemeralSecretPath(temporary);
258
266
  }
259
267
  }
260
268
  }