@bitkyc08/opencodex 2.67.0-preview.20260926 → 2.68.0-preview.20260927

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 (257) hide show
  1. package/bin/ocx.mjs +3 -0
  2. package/gui/dist/assets/App-B5-cAEd2.js +51 -0
  3. package/gui/dist/assets/App-DCBismRi.css +1 -0
  4. package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
  5. package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
  6. package/gui/dist/assets/index-BcXblvem.js +86 -0
  7. package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
  8. package/gui/dist/index.html +21 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-image-guard.ts +13 -1
  11. package/src/adapters/anthropic-image-normalize.ts +3 -2
  12. package/src/adapters/anthropic-output-schema.ts +36 -0
  13. package/src/adapters/anthropic.ts +10 -2
  14. package/src/adapters/base.ts +7 -0
  15. package/src/adapters/codebuddy/live-models.ts +180 -0
  16. package/src/adapters/coding-agent/protocol.ts +91 -12
  17. package/src/adapters/coding-agent/turn.ts +32 -27
  18. package/src/adapters/devin/cloud-direct/index.ts +1 -0
  19. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
  20. package/src/adapters/devin.ts +9 -6
  21. package/src/adapters/google-http.ts +4 -4
  22. package/src/adapters/google.ts +93 -3
  23. package/src/adapters/kiro/adapter.ts +6 -2
  24. package/src/adapters/kiro/stream.ts +12 -2
  25. package/src/adapters/kiro/usage.ts +3 -1
  26. package/src/adapters/kiro-errors.ts +11 -1
  27. package/src/adapters/kiro-events.ts +27 -1
  28. package/src/adapters/kiro-refusal.ts +30 -0
  29. package/src/adapters/kiro-retry.ts +71 -31
  30. package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
  31. package/src/adapters/openai-chat/messages.ts +31 -4
  32. package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
  33. package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
  34. package/src/adapters/openai-chat/tool-schema.ts +6 -2
  35. package/src/adapters/openai-chat.ts +2 -2
  36. package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
  37. package/src/adapters/openai-responses/passthrough.ts +9 -3
  38. package/src/adapters/opencode-go-additional-tools.ts +19 -10
  39. package/src/adapters/physical-send.ts +10 -3
  40. package/src/adapters/registry.ts +2 -1
  41. package/src/adapters/responses-tool-schema.ts +31 -3
  42. package/src/adapters/run-turn-queue.ts +63 -23
  43. package/src/adapters/unique-tool-call-ids.ts +63 -0
  44. package/src/adapters/xai-web-search.ts +32 -2
  45. package/src/bridge/sse.ts +14 -2
  46. package/src/chat/inbound.ts +20 -6
  47. package/src/claude/agents-inject.ts +4 -0
  48. package/src/claude/desktop-3p.ts +53 -13
  49. package/src/claude/desktop-profile.ts +41 -4
  50. package/src/claude/inbound-model-options.ts +14 -2
  51. package/src/claude/inbound.ts +1 -1
  52. package/src/claude/intercept/connect-proxy.ts +17 -1
  53. package/src/claude/intercept/local-ca.ts +7 -1
  54. package/src/claude/message-threads.ts +28 -0
  55. package/src/cli/account-api.ts +13 -0
  56. package/src/cli/account-auth.ts +49 -2
  57. package/src/cli/account-extended.ts +4 -2
  58. package/src/cli/account.ts +4 -2
  59. package/src/cli/capabilities.ts +17 -0
  60. package/src/cli/claude.ts +3 -1
  61. package/src/cli/dispatch.ts +62 -5
  62. package/src/cli/index.ts +84 -79
  63. package/src/cli/minimax.ts +4 -2
  64. package/src/cli/opencode.ts +6 -1
  65. package/src/cli/restart-handoff.ts +110 -0
  66. package/src/cli/status.ts +51 -0
  67. package/src/client/connect.ts +34 -15
  68. package/src/client/link-ingress.ts +102 -0
  69. package/src/client/link-join.ts +37 -16
  70. package/src/client/link-relay.ts +228 -45
  71. package/src/client/link-state.ts +54 -0
  72. package/src/client/link-status.ts +39 -0
  73. package/src/client/link-teardown.ts +2 -2
  74. package/src/client/link-tunnel.ts +615 -86
  75. package/src/client/machine-api.ts +10 -4
  76. package/src/client/machine-listener.ts +57 -10
  77. package/src/client/runtime.ts +210 -35
  78. package/src/clients/config-export/constants.ts +1 -12
  79. package/src/clients/config-export/contracts.ts +2 -0
  80. package/src/clients/config-export/model-metadata.ts +9 -3
  81. package/src/clients/config-export/omp.ts +1 -1
  82. package/src/clients/config-export/zcode-store.ts +2 -2
  83. package/src/clients/config-export.ts +36 -3
  84. package/src/codex/app-server-processes.ts +32 -0
  85. package/src/codex/app-server-restart-service.ts +20 -3
  86. package/src/codex/catalog/access-programs.ts +50 -0
  87. package/src/codex/catalog/build-entries.ts +13 -0
  88. package/src/codex/catalog/derive-entry.ts +1 -0
  89. package/src/codex/catalog/model-hints.ts +11 -0
  90. package/src/codex/catalog/parsing.ts +6 -2
  91. package/src/codex/catalog/provider-models.ts +81 -0
  92. package/src/codex/catalog/retained-sync.ts +3 -0
  93. package/src/codex/codex-write-lock.ts +2 -2
  94. package/src/codex/convergence.ts +2 -0
  95. package/src/codex/desired-state.ts +14 -4
  96. package/src/codex/home.ts +21 -3
  97. package/src/codex/inject/restore.ts +17 -0
  98. package/src/codex/inject/routing-classify.ts +3 -2
  99. package/src/codex/inject-coordination.ts +4 -1
  100. package/src/codex/inject.ts +8 -1
  101. package/src/codex/journal.ts +6 -2
  102. package/src/codex/management-convergence.ts +9 -0
  103. package/src/codex/model-entitlements.ts +28 -5
  104. package/src/codex/routing/idle-window.ts +58 -0
  105. package/src/codex/routing-drift.ts +134 -0
  106. package/src/codex/routing-healer.ts +419 -0
  107. package/src/codex/routing.ts +10 -0
  108. package/src/codex/runtime.ts +188 -57
  109. package/src/codex/sibling-handoff.ts +71 -0
  110. package/src/codex/sibling-start.ts +139 -0
  111. package/src/codex/sync.ts +5 -3
  112. package/src/combos/failover.ts +32 -5
  113. package/src/combos/request.ts +11 -3
  114. package/src/combos/reset-window.ts +10 -5
  115. package/src/combos/resolve.ts +36 -6
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/load-degrade.ts +2 -0
  118. package/src/config/paths.ts +7 -1
  119. package/src/config/process-state.ts +10 -1
  120. package/src/config/schema/compaction-recovery.ts +15 -0
  121. package/src/config/schema/config-schema.ts +2 -0
  122. package/src/config/schema/leaf-validators.ts +9 -3
  123. package/src/generated/compatibility-version.json +360 -204
  124. package/src/images/loop.ts +47 -32
  125. package/src/integrations/catalog-refresh.ts +4 -0
  126. package/src/integrations/omp-yaml-source.ts +1 -0
  127. package/src/lib/claude-request-projection.ts +101 -0
  128. package/src/lib/local-account-switch-capability.ts +48 -0
  129. package/src/lib/local-upstream.ts +146 -0
  130. package/src/lib/package-tree-integrity.ts +25 -3
  131. package/src/lib/package-tree-retarget.ts +130 -0
  132. package/src/lib/process-control.ts +4 -1
  133. package/src/lib/system-restart-contract.ts +106 -0
  134. package/src/lib/upstream-retry.ts +57 -1
  135. package/src/link/ports.ts +9 -0
  136. package/src/link/ssh-argv.ts +16 -0
  137. package/src/link/ssh-runner.ts +70 -2
  138. package/src/link/tunnel-state.ts +58 -9
  139. package/src/oauth/account-quota-rank.ts +29 -18
  140. package/src/oauth/generic-account-failover.ts +179 -22
  141. package/src/oauth/index.ts +17 -4
  142. package/src/oauth/kiro-account-load.ts +107 -0
  143. package/src/oauth/kiro-device-login.ts +312 -0
  144. package/src/oauth/kiro-terminal-failover.ts +28 -0
  145. package/src/oauth/login-flow-state.ts +7 -1
  146. package/src/oauth/pool-settings-capability.ts +23 -14
  147. package/src/oauth/store.ts +56 -3
  148. package/src/oauth/types.ts +4 -0
  149. package/src/plugins/loader.ts +350 -0
  150. package/src/plugins/upstream-hooks.ts +119 -0
  151. package/src/protocols/encoders/adapter-events.ts +9 -2
  152. package/src/providers/account-quota-disk.ts +42 -4
  153. package/src/providers/codebuddy-models.ts +6 -2
  154. package/src/providers/command-code-efforts.ts +33 -10
  155. package/src/providers/kiro-account-state-disk.ts +44 -0
  156. package/src/providers/kiro-model-catalog.ts +162 -0
  157. package/src/providers/kiro-models.ts +5 -4
  158. package/src/providers/kiro-quota-metrics.ts +35 -0
  159. package/src/providers/kiro-usage.ts +118 -14
  160. package/src/providers/quota/account-cache.ts +43 -7
  161. package/src/providers/quota/antigravity.ts +8 -3
  162. package/src/providers/quota/kiro-account-probe.ts +12 -0
  163. package/src/providers/quota/vendor-probes-key.ts +31 -26
  164. package/src/providers/quota/vendor-probes-oauth.ts +16 -7
  165. package/src/providers/quota-types.ts +14 -0
  166. package/src/providers/quota.ts +21 -21
  167. package/src/providers/registry/entries-extended.ts +5 -3
  168. package/src/providers/request-pacing.ts +410 -12
  169. package/src/remote-control/workspace-codex-runtime.ts +4 -3
  170. package/src/responses/citation-markers.ts +132 -65
  171. package/src/responses/hosted-tool-policy.ts +14 -3
  172. package/src/responses/parser-content.ts +3 -2
  173. package/src/responses/parser.ts +4 -1
  174. package/src/responses/schema.ts +4 -0
  175. package/src/responses/visualization-directives.ts +182 -0
  176. package/src/server/chat-native-sse.ts +24 -12
  177. package/src/server/chat-native.ts +137 -68
  178. package/src/server/claude-messages.ts +147 -12
  179. package/src/server/index/link-listener.ts +7 -0
  180. package/src/server/index/optional-listeners.ts +4 -1
  181. package/src/server/index/package-tree-guard.ts +35 -16
  182. package/src/server/index/serve-options.ts +4 -2
  183. package/src/server/index/startup-warnings.ts +6 -0
  184. package/src/server/index.ts +7 -7
  185. package/src/server/inference/client-encoder-delivery.ts +3 -0
  186. package/src/server/inference/context.ts +30 -2
  187. package/src/server/local-account-switch-auth.ts +79 -0
  188. package/src/server/management/agent-settings-routes.ts +11 -3
  189. package/src/server/management/config-routes.ts +28 -11
  190. package/src/server/management/context.ts +2 -0
  191. package/src/server/management/link-routes.ts +123 -39
  192. package/src/server/management/logs-usage-routes.ts +9 -42
  193. package/src/server/management/model-rows.ts +1 -0
  194. package/src/server/management/oauth-account-routes.ts +67 -12
  195. package/src/server/management/route-registry.ts +5 -5
  196. package/src/server/management/sibling-guard.ts +60 -0
  197. package/src/server/management/storage-log-guard-routes.ts +72 -6
  198. package/src/server/management/system-restart.ts +92 -135
  199. package/src/server/management/system-routes.ts +1 -1
  200. package/src/server/management-api.ts +31 -3
  201. package/src/server/management-auth.ts +3 -0
  202. package/src/server/port-reclaim.ts +111 -1
  203. package/src/server/proxy-liveness.ts +16 -0
  204. package/src/server/relay.ts +16 -15
  205. package/src/server/request-log-filter.ts +28 -27
  206. package/src/server/request-log.ts +7 -3
  207. package/src/server/request-metrics.ts +28 -0
  208. package/src/server/responses/adapter-continuation.ts +151 -21
  209. package/src/server/responses/adapter-delivery.ts +21 -7
  210. package/src/server/responses/adapter-dispatch.ts +239 -51
  211. package/src/server/responses/codex-ws-pool.ts +5 -4
  212. package/src/server/responses/codex-ws-request.ts +4 -1
  213. package/src/server/responses/compact.ts +8 -4
  214. package/src/server/responses/compaction-recovery-policy.ts +116 -0
  215. package/src/server/responses/compaction-recovery.ts +330 -0
  216. package/src/server/responses/core-combo-failure.ts +1 -0
  217. package/src/server/responses/core-combo.ts +5 -1
  218. package/src/server/responses/core-lifetime.ts +22 -0
  219. package/src/server/responses/core-options.ts +16 -1
  220. package/src/server/responses/core.ts +14 -4
  221. package/src/server/responses/empty-completion-guard.ts +2 -0
  222. package/src/server/responses/fetch-helpers.ts +82 -31
  223. package/src/server/responses/passthrough-delivery.ts +3 -2
  224. package/src/server/responses/passthrough-dispatch.ts +12 -2
  225. package/src/server/responses/passthrough-execution.ts +2 -2
  226. package/src/server/responses/request-send-budget.ts +21 -5
  227. package/src/server/responses/request-transport.ts +116 -18
  228. package/src/server/responses/reset-replay.ts +32 -0
  229. package/src/server/responses/run-turn-execution.ts +136 -19
  230. package/src/server/responses/sidecar-execution.ts +41 -8
  231. package/src/server/responses/terminal-guard.ts +2 -0
  232. package/src/server/responses/ws-upstream.ts +44 -4
  233. package/src/server/restart-replacement.ts +382 -0
  234. package/src/server/startup-health-cache.ts +23 -4
  235. package/src/server/stop-teardown.ts +35 -1
  236. package/src/server/system-env.ts +3 -0
  237. package/src/service/windows-taskxml.ts +11 -7
  238. package/src/service/windows-wrapper-exit.ts +8 -0
  239. package/src/stall-timeout.ts +35 -10
  240. package/src/storage/policy-job.ts +4 -0
  241. package/src/storage/scanner.ts +177 -29
  242. package/src/storage/storage-mutation-coordinator.ts +17 -0
  243. package/src/tray/windows-tray.ps1 +50 -23
  244. package/src/types/config.ts +4 -0
  245. package/src/types/provider.ts +5 -1
  246. package/src/types/request.ts +13 -2
  247. package/src/update/index.ts +3 -1
  248. package/src/update/job.ts +6 -2
  249. package/src/update/mise-launcher-target.ts +100 -0
  250. package/src/update/worker-launch.ts +46 -0
  251. package/src/usage/log.ts +2 -0
  252. package/src/web-search/loop.ts +5 -2
  253. package/gui/dist/assets/App-B3VRRYSj.js +0 -51
  254. package/gui/dist/assets/App-BJsT8Icc.css +0 -1
  255. package/gui/dist/assets/Tray-TYAA0EQY.js +0 -1
  256. package/gui/dist/assets/index-9Ju7nHsu.js +0 -86
  257. package/gui/dist/assets/tray-data-xKfS2gSG.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
+ import { readdir, stat as statAsync } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
5
  import { Database, constants } from "bun:sqlite";
@@ -110,6 +111,81 @@ function walkFiles(dir: string, relPrefix: string, out: FileEntry[]): void {
110
111
  }
111
112
  }
112
113
 
114
+ /** Upper bound on concurrent readdir/stat calls within one async scan. */
115
+ const SCAN_FS_CONCURRENCY = 64;
116
+
117
+ type FsLimit = <T>(op: () => Promise<T>) => Promise<T>;
118
+
119
+ /**
120
+ * FIFO limiter for filesystem calls. A finishing call hands its slot straight to the next
121
+ * waiter, so the bound holds exactly. Only leaf fs calls take a slot: a directory walk
122
+ * never holds one while awaiting its children, so recursion cannot deadlock the pool.
123
+ */
124
+ function createFsLimit(max: number): FsLimit {
125
+ let active = 0;
126
+ const waiting: Array<() => void> = [];
127
+ return async <T>(op: () => Promise<T>): Promise<T> => {
128
+ if (active < max) active += 1;
129
+ else await new Promise<void>(resolve => waiting.push(resolve));
130
+ try {
131
+ return await op();
132
+ } finally {
133
+ const next = waiting.shift();
134
+ if (next) next();
135
+ else active -= 1;
136
+ }
137
+ };
138
+ }
139
+
140
+ /** Entries started together at one directory level of an async scan. */
141
+ const SCAN_BATCH_SIZE = 256;
142
+
143
+ /**
144
+ * Map `items` through `fn` a batch at a time, keeping input order. A directory with tens of
145
+ * thousands of entries then has at most one batch of pending tasks at its level, instead of
146
+ * one promise per entry queued behind the fs limiter.
147
+ */
148
+ async function mapInBatches<T, R>(items: readonly T[], fn: (item: T) => Promise<R>): Promise<R[]> {
149
+ const results: R[] = [];
150
+ for (let start = 0; start < items.length; start += SCAN_BATCH_SIZE) {
151
+ const batch = await Promise.all(items.slice(start, start + SCAN_BATCH_SIZE).map(fn));
152
+ for (const result of batch) results.push(result);
153
+ }
154
+ return results;
155
+ }
156
+
157
+ /**
158
+ * Async twin of {@link walkFiles}: same skip rules, and entries come back in readdir
159
+ * order so the report (including `largest` tie order) matches the synchronous scan.
160
+ */
161
+ async function walkFilesAsync(dir: string, relPrefix: string, limit: FsLimit): Promise<FileEntry[]> {
162
+ let entries;
163
+ try {
164
+ entries = await limit(() => readdir(dir, { withFileTypes: true }));
165
+ } catch {
166
+ return [];
167
+ }
168
+ // Bun can settle small async fs calls on the microtask queue alone, so a walk of cached
169
+ // directories would otherwise finish without ever giving the event loop a turn. One timer
170
+ // turn per directory keeps request handling live during a large scan.
171
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
172
+ const parts = await mapInBatches(entries, async (entry): Promise<FileEntry[]> => {
173
+ const full = join(dir, entry.name);
174
+ const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
175
+ try {
176
+ if (entry.isDirectory()) return await walkFilesAsync(full, relPath, limit);
177
+ if (entry.isFile()) {
178
+ const stat = await limit(() => statAsync(full));
179
+ return [{ relPath, bytes: stat.size, mtimeMs: stat.mtimeMs }];
180
+ }
181
+ } catch {
182
+ /* entry vanished mid-scan — diagnostics tolerate racy trees */
183
+ }
184
+ return [];
185
+ });
186
+ return parts.flat();
187
+ }
188
+
113
189
  function buildBucket(key: StorageBucketKey, files: FileEntry[]): StorageBucket {
114
190
  const bucket: StorageBucket = {
115
191
  key,
@@ -169,8 +245,8 @@ function newestVersionedDb(names: string[], pattern: RegExp): string | null {
169
245
  return best;
170
246
  }
171
247
 
172
- export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageReport {
173
- const files: Record<StorageBucketKey, FileEntry[]> = {
248
+ function emptyFiles(): Record<StorageBucketKey, FileEntry[]> {
249
+ return {
174
250
  sessions: [],
175
251
  archived_sessions: [],
176
252
  logs_db: [],
@@ -179,36 +255,27 @@ export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageR
179
255
  deletion_manifests: [],
180
256
  other: [],
181
257
  };
258
+ }
182
259
 
183
- let rootNames: string[] = [];
184
- try {
185
- rootNames = readdirSync(codexHome);
186
- } catch (error) {
187
- // A missing home is a normal fresh-machine state — report zeros. Anything else
188
- // (e.g. ENOTDIR: CODEX_HOME points at a file) is a broken setup the caller
189
- // must surface as a scan failure, not silently render as an empty home.
190
- const code = (error as NodeJS.ErrnoException).code;
191
- if (code !== "ENOENT") throw error;
192
- }
260
+ /** A missing home is a normal fresh-machine state; any other readdir failure is rethrown. */
261
+ function rootReadFailure(error: unknown): string[] {
262
+ // A missing home is a normal fresh-machine state — report zeros. Anything else
263
+ // (e.g. ENOTDIR: CODEX_HOME points at a file) is a broken setup the caller
264
+ // must surface as a scan failure, not silently render as an empty home.
265
+ const code = (error as NodeJS.ErrnoException).code;
266
+ if (code !== "ENOENT") throw error;
267
+ return [];
268
+ }
193
269
 
194
- for (const name of rootNames) {
195
- const full = join(codexHome, name);
196
- let stat;
197
- try {
198
- stat = statSync(full);
199
- } catch {
200
- continue;
201
- }
202
- if (stat.isDirectory()) {
203
- // Quarantine trash (Phase 2) must not inflate "other" or totals.
204
- if (name === TRASH_DIR) continue;
205
- walkFiles(full, name, files[DIR_BUCKETS[name] ?? "other"]);
206
- } else if (stat.isFile()) {
207
- const key: StorageBucketKey = STATE_DB_FILE.test(name) ? "state_db" : LOGS_DB_FILE.test(name) ? "logs_db" : "other";
208
- files[key].push({ relPath: name, bytes: stat.size, mtimeMs: stat.mtimeMs });
209
- }
210
- }
270
+ function rootFileBucket(name: string): StorageBucketKey {
271
+ return STATE_DB_FILE.test(name) ? "state_db" : LOGS_DB_FILE.test(name) ? "logs_db" : "other";
272
+ }
211
273
 
274
+ function finishReport(
275
+ codexHome: string,
276
+ rootNames: string[],
277
+ files: Record<StorageBucketKey, FileEntry[]>,
278
+ ): StorageReport {
212
279
  const buckets = (Object.keys(files) as StorageBucketKey[]).map(key => buildBucket(key, files[key]));
213
280
 
214
281
  const stateDbName = newestVersionedDb(rootNames, STATE_DB_FILE);
@@ -236,3 +303,84 @@ export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageR
236
303
  buckets,
237
304
  };
238
305
  }
306
+
307
+ export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageReport {
308
+ const files = emptyFiles();
309
+
310
+ let rootNames: string[] = [];
311
+ try {
312
+ rootNames = readdirSync(codexHome);
313
+ } catch (error) {
314
+ rootNames = rootReadFailure(error);
315
+ }
316
+
317
+ for (const name of rootNames) {
318
+ const full = join(codexHome, name);
319
+ let stat;
320
+ try {
321
+ stat = statSync(full);
322
+ } catch {
323
+ continue;
324
+ }
325
+ if (stat.isDirectory()) {
326
+ // Quarantine trash (Phase 2) must not inflate "other" or totals.
327
+ if (name === TRASH_DIR) continue;
328
+ walkFiles(full, name, files[DIR_BUCKETS[name] ?? "other"]);
329
+ } else if (stat.isFile()) {
330
+ files[rootFileBucket(name)].push({ relPath: name, bytes: stat.size, mtimeMs: stat.mtimeMs });
331
+ }
332
+ }
333
+
334
+ return finishReport(codexHome, rootNames, files);
335
+ }
336
+
337
+ /**
338
+ * Same report as {@link scanStorage}, but the tree walk uses async fs calls so the server's
339
+ * event loop keeps serving while it runs. The synchronous walk measured ~3.7s for a
340
+ * 40k-file / 7.5GB CODEX_HOME on Windows, during which every listener (proxy traffic
341
+ * included) stalled each time the Storage page loaded. The two sqlite row counts stay
342
+ * synchronous: immutable readonly opens measured ~0.1s on the same home.
343
+ */
344
+ export async function scanStorageAsync(codexHome: string = resolveCodexHomeDir()): Promise<StorageReport> {
345
+ const files = emptyFiles();
346
+ const limit = createFsLimit(SCAN_FS_CONCURRENCY);
347
+
348
+ let rootNames: string[] = [];
349
+ try {
350
+ rootNames = await readdir(codexHome);
351
+ } catch (error) {
352
+ rootNames = rootReadFailure(error);
353
+ }
354
+
355
+ const roots = await mapInBatches(rootNames, async name => {
356
+ const full = join(codexHome, name);
357
+ try {
358
+ return { name, full, stat: await limit(() => statAsync(full)) };
359
+ } catch {
360
+ return null;
361
+ }
362
+ });
363
+ const walks = await mapInBatches(roots, async root => {
364
+ if (!root) return;
365
+ if (root.stat.isDirectory()) {
366
+ // Quarantine trash (Phase 2) must not inflate "other" or totals.
367
+ if (root.name === TRASH_DIR) return;
368
+ return { key: DIR_BUCKETS[root.name] ?? "other", entries: await walkFilesAsync(root.full, root.name, limit) };
369
+ }
370
+ if (root.stat.isFile()) {
371
+ return {
372
+ key: rootFileBucket(root.name),
373
+ entries: [{ relPath: root.name, bytes: root.stat.size, mtimeMs: root.stat.mtimeMs }],
374
+ };
375
+ }
376
+ });
377
+ // Appended in root order so bucket contents match the synchronous scan. One entry at a
378
+ // time: spreading a large bucket into push() can exceed the engine's argument limit.
379
+ for (const walk of walks) {
380
+ if (!walk) continue;
381
+ const bucket = files[walk.key];
382
+ for (const entry of walk.entries) bucket.push(entry);
383
+ }
384
+
385
+ return finishReport(codexHome, rootNames, files);
386
+ }
@@ -34,8 +34,24 @@ export const MAX_ACTIVE_STORAGE_HOME_SLOTS = 32;
34
34
  const slots = new Map<string, ActiveSlot>();
35
35
  const slotGate = createAdmissionGate("storage_home_slots", MAX_ACTIVE_STORAGE_HOME_SLOTS);
36
36
  let releaseMisses = 0;
37
+ let mutationEpoch = 0;
37
38
  let testHooks: StorageMutationCoordinatorTestHooks | null = null;
38
39
 
40
+ /**
41
+ * Advances whenever a CODEX_HOME storage mutation finishes (coordinated cleanup, restore
42
+ * and policy runs on lease release; Log Guard maintenance through
43
+ * {@link noteStorageMutationCompleted}). An in-flight storage scan that started under an
44
+ * older epoch describes the tree before that mutation and must not answer a later read.
45
+ */
46
+ export function storageMutationEpoch(): number {
47
+ return mutationEpoch;
48
+ }
49
+
50
+ /** Record a CODEX_HOME mutation that does not go through a coordinator slot. */
51
+ export function noteStorageMutationCompleted(): void {
52
+ mutationEpoch += 1;
53
+ }
54
+
39
55
  function slotKey(codexHome?: string): string {
40
56
  return resolve(codexHome ?? resolveCodexHomeDir());
41
57
  }
@@ -79,6 +95,7 @@ export function tryBeginStorageMutation(
79
95
  release() {
80
96
  if (!active) return;
81
97
  active = false;
98
+ mutationEpoch += 1;
82
99
  const owner = slots.get(key);
83
100
  if (owner?.lease === ownerLease) slots.delete(key);
84
101
  lease.release();
@@ -1,4 +1,4 @@
1
- param(
1
+ param(
2
2
  [Parameter(Mandatory = $true)][string]$BunPath,
3
3
  [Parameter(Mandatory = $true)][string]$CliPath,
4
4
  [Parameter(Mandatory = $true)][string]$CodexHome,
@@ -15,6 +15,19 @@ Add-Type -AssemblyName System.Windows.Forms
15
15
  Add-Type -AssemblyName System.Drawing
16
16
  try { [System.Windows.Forms.Application]::EnableVisualStyles() } catch { $null = $_ }
17
17
 
18
+ # Chinese UI cultures get Chinese tray text; every other locale keeps the English strings. The
19
+ # judgment is a named function so it can be exercised directly instead of only through a live
20
+ # desktop, where a selector that always answered English would still look correct.
21
+ function Test-TrayChineseCulture([string]$CultureName) {
22
+ return $CultureName -like "zh*"
23
+ }
24
+ $script:isZh = Test-TrayChineseCulture ([System.Globalization.CultureInfo]::CurrentUICulture.Name)
25
+ function Get-TrayText {
26
+ param([string]$En, [string]$Zh)
27
+ if ($script:isZh) { return $Zh }
28
+ return $En
29
+ }
30
+
18
31
  # Normalize aliases before deriving singleton/event names. Without this,
19
32
  # C:\path and C:\path\. create separate tray instances for the same home.
20
33
  function Normalize-HomePath([string]$Value) {
@@ -127,7 +140,7 @@ function Start-OcxCommand([string[]]$CommandArgs, [switch]$TrackExit) {
127
140
  return $true
128
141
  } catch {
129
142
  Write-ActionLog "launch failed: $($_.Exception.GetType().Name)"
130
- $notify.ShowBalloonTip(5000, "opencodex action failed", "The action could not start. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error)
143
+ $notify.ShowBalloonTip(5000, (Get-TrayText "opencodex action failed" "opencodex 操作失败"), (Get-TrayText "The action could not start. Open the logs folder or run ocx doctor." "操作无法启动。打开日志文件夹或运行 ocx doctor。"), [System.Windows.Forms.ToolTipIcon]::Error)
131
144
  return $false
132
145
  }
133
146
  }
@@ -396,19 +409,19 @@ $statusItem = New-Object System.Windows.Forms.ToolStripMenuItem
396
409
  $statusItem.Enabled = $false
397
410
  $safetyItem = New-Object System.Windows.Forms.ToolStripMenuItem
398
411
  $safetyItem.Enabled = $false
399
- $openItem = $menu.Items.Add("Open Dashboard")
400
- $updateItem = $menu.Items.Add("Update available")
412
+ $openItem = $menu.Items.Add((Get-TrayText "Open Dashboard" "打开面板"))
413
+ $updateItem = $menu.Items.Add((Get-TrayText "Update available" "有可用更新"))
401
414
  $updateItem.Visible = $false
402
415
  $updateItem.Enabled = $false
403
- $startItem = $menu.Items.Add("Start Proxy")
404
- $stopItem = $menu.Items.Add("Stop Proxy and Restore Native Routing")
405
- $restartItem = $menu.Items.Add("Restart Proxy")
416
+ $startItem = $menu.Items.Add((Get-TrayText "Start Proxy" "启动代理"))
417
+ $stopItem = $menu.Items.Add((Get-TrayText "Stop Proxy and Restore Native Routing" "停止代理并还原原生路由"))
418
+ $restartItem = $menu.Items.Add((Get-TrayText "Restart Proxy" "重启代理"))
406
419
  [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
407
420
  [void]$menu.Items.Add($statusItem)
408
421
  [void]$menu.Items.Add($safetyItem)
409
- $logsItem = $menu.Items.Add("Open Logs Folder")
422
+ $logsItem = $menu.Items.Add((Get-TrayText "Open Logs Folder" "打开日志文件夹"))
410
423
  [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
411
- $exitItem = $menu.Items.Add("Exit Tray")
424
+ $exitItem = $menu.Items.Add((Get-TrayText "Exit Tray" "退出托盘"))
412
425
 
413
426
  $script:online = $false
414
427
  $script:port = 10100
@@ -463,6 +476,14 @@ function Set-PendingAction([string]$Action, [int]$TimeoutSeconds) {
463
476
  function Complete-PendingAction([bool]$Success) {
464
477
  if ($null -eq $script:pendingAction) { return }
465
478
  $action = $script:pendingAction
479
+ # The pending value stays English because it is compared against the labels the click
480
+ # handlers set; only the text a user reads is localized.
481
+ $displayAction = switch ($action) {
482
+ "Start Proxy" { Get-TrayText "Start Proxy" "启动代理" }
483
+ "Stop Proxy" { Get-TrayText "Stop Proxy" "停止代理" }
484
+ "Restart Proxy" { Get-TrayText "Restart Proxy" "重启代理" }
485
+ default { $action }
486
+ }
466
487
  $script:pendingAction = $null
467
488
  if ($null -ne $script:pendingProcess) {
468
489
  try {
@@ -474,10 +495,10 @@ function Complete-PendingAction([bool]$Success) {
474
495
  }
475
496
  if ($Success) {
476
497
  Write-ActionLog "$action completed (port=$($script:port), pid=$($script:proxyPid))"
477
- $notify.ShowBalloonTip(2500, "opencodex", "$action completed.", [System.Windows.Forms.ToolTipIcon]::Info)
498
+ $notify.ShowBalloonTip(2500, "opencodex", (Get-TrayText "$displayAction completed." "$displayAction 已完成。"), [System.Windows.Forms.ToolTipIcon]::Info)
478
499
  } else {
479
500
  Write-ActionLog "$action failed to reach the expected state"
480
- $notify.ShowBalloonTip(5000, "opencodex action failed", "$action did not reach the expected state. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error)
501
+ $notify.ShowBalloonTip(5000, (Get-TrayText "opencodex action failed" "opencodex 操作失败"), (Get-TrayText "$displayAction did not reach the expected state. Open the logs folder or run ocx doctor." "$displayAction 未达到预期状态。打开日志文件夹或运行 ocx doctor。"), [System.Windows.Forms.ToolTipIcon]::Error)
481
502
  }
482
503
  }
483
504
 
@@ -536,8 +557,8 @@ function Update-TrayState {
536
557
  Start-StartupHealthProbe
537
558
  }
538
559
  if ($script:online) {
539
- $statusItem.Text = "Proxy: Online (port $($script:port))"
540
- $notify.Text = "opencodex: Online"
560
+ $statusItem.Text = (Get-TrayText "Proxy: Online (port $($script:port))" "代理: 在线 (端口 $($script:port))")
561
+ $notify.Text = (Get-TrayText "opencodex: Online" "opencodex: 在线")
541
562
  $startItem.Enabled = $false
542
563
  $stopItem.Enabled = $true
543
564
  $restartItem.Enabled = $true
@@ -547,21 +568,27 @@ function Update-TrayState {
547
568
  # diagnostic is cleaned up outside the online-only UI branch.
548
569
  $startup = $script:startupHealth
549
570
  if ($null -ne $startup) {
550
- $label = if ($startup.status -eq "at-risk") { "At risk" } elseif ($startup.status -eq "protected") { "Protected" } else { "Native routing" }
551
- $safetyItem.Text = "Restart safety: $label"
571
+ $label = if ($startup.status -eq "at-risk") {
572
+ Get-TrayText "At risk" "有风险"
573
+ } elseif ($startup.status -eq "protected") {
574
+ Get-TrayText "Protected" "已保护"
575
+ } else {
576
+ Get-TrayText "Native routing" "原生路由"
577
+ }
578
+ $safetyItem.Text = (Get-TrayText "Restart safety: $label" "重启保护: $label")
552
579
  $notify.Icon = if ($startup.status -eq "at-risk") {
553
580
  if ($script:updateAvailable) { $warningUpdateIcon } else { $warningIcon }
554
581
  } else {
555
582
  if ($script:updateAvailable) { $onlineUpdateIcon } else { $onlineIcon }
556
583
  }
557
584
  } else {
558
- $safetyItem.Text = "Restart safety: unavailable"
585
+ $safetyItem.Text = (Get-TrayText "Restart safety: unavailable" "重启保护: 不可用")
559
586
  $notify.Icon = if ($script:updateAvailable) { $warningUpdateIcon } else { $warningIcon }
560
587
  }
561
588
  } else {
562
- $statusItem.Text = "Proxy: Offline"
563
- $safetyItem.Text = "Restart safety: start the proxy to inspect"
564
- $notify.Text = "opencodex: Offline"
589
+ $statusItem.Text = (Get-TrayText "Proxy: Offline" "代理: 离线")
590
+ $safetyItem.Text = (Get-TrayText "Restart safety: start the proxy to inspect" "重启保护: 启动代理后查看")
591
+ $notify.Text = (Get-TrayText "opencodex: Offline" "opencodex: 离线")
565
592
  $notify.Icon = if ($script:updateAvailable) { $offlineUpdateIcon } else { $offlineIcon }
566
593
  $startItem.Enabled = $true
567
594
  $stopItem.Enabled = $false
@@ -607,7 +634,7 @@ $openItem.add_Click({ Start-OcxCommand @("gui") })
607
634
  $updateItem.add_Click({ Start-OcxCommand @("gui") })
608
635
  $startItem.add_Click({
609
636
  if (-not (Set-PendingAction "Start Proxy" 75)) { return }
610
- $statusItem.Text = "Proxy: Starting..."
637
+ $statusItem.Text = (Get-TrayText "Proxy: Starting..." "代理: 启动中...")
611
638
  # service start can spend 20s and the CLI then observes health for another 40s.
612
639
  $startProcess = Start-OcxCommand @("__tray-start") -TrackExit
613
640
  if ($startProcess -is [System.Diagnostics.Process]) {
@@ -618,7 +645,7 @@ $startItem.add_Click({
618
645
  })
619
646
  $stopItem.add_Click({
620
647
  if (-not (Set-PendingAction "Stop Proxy" 15)) { return }
621
- $statusItem.Text = "Proxy: Stopping..."
648
+ $statusItem.Text = (Get-TrayText "Proxy: Stopping..." "代理: 停止中...")
622
649
  $stopProcess = Start-OcxCommand @("stop") -TrackExit
623
650
  if ($stopProcess -is [System.Diagnostics.Process]) {
624
651
  $script:pendingProcess = $stopProcess
@@ -628,7 +655,7 @@ $stopItem.add_Click({
628
655
  })
629
656
  $restartItem.add_Click({
630
657
  if (-not (Set-PendingAction "Restart Proxy" 160)) { return }
631
- $statusItem.Text = "Proxy: Restarting..."
658
+ $statusItem.Text = (Get-TrayText "Proxy: Restarting..." "代理: 重启中...")
632
659
  # /api/system/restart may drain active work for 60s and then spend up to 70s
633
660
  # handing off to an identity-verified replacement. The tray observes health/PID
634
661
  # rather than the detached CLI exit, so keep a watchdog margin around that shared
@@ -665,7 +692,7 @@ $timer.add_Tick({
665
692
  $notify.ContextMenuStrip = $menu
666
693
  $notify.Icon = $offlineIcon
667
694
  $notify.Visible = $true
668
- $notify.Text = "opencodex: Checking..."
695
+ $notify.Text = (Get-TrayText "opencodex: Checking..." "opencodex: 检查中...")
669
696
 
670
697
  try {
671
698
  Initialize-UpdateBadgeReader
@@ -722,6 +722,8 @@ export interface OcxConfig {
722
722
  /** Compaction triggers this override covers; omission means `["manual"]`. */
723
723
  triggers?: ("manual" | "auto")[];
724
724
  };
725
+ /** Opt-in failure-only recovery; never replaces the initial compaction model. */
726
+ compactionRecovery?: { enabled: boolean; model: string; allowDevinInvalidArgument?: boolean };
725
727
  /**
726
728
  * Models hidden from Codex discovery without blocking direct proxy calls. Routed provider ids
727
729
  * are excluded from the catalog + /v1/models entirely. Account-qualified native ids hide only
@@ -1514,6 +1516,8 @@ export interface OcxWebSearchSidecarConfig {
1514
1516
  * explicit account selection. Only automatic rotation skips it.
1515
1517
  */
1516
1518
  export interface OcxCodexPoolConfig {
1519
+ /** Start idle Codex windows when the pool is initialized. */
1520
+ startIdleWindows?: boolean;
1517
1521
  /**
1518
1522
  * Plan keys ordinary rotation skips, matched case-insensitively against the plan stored on each
1519
1523
  * account. Absent or empty means no policy.
@@ -180,6 +180,8 @@ export interface RequestPacingRule {
180
180
  requestsPerMinute?: number;
181
181
  /** Minimum delay between request starts. The slower configured value wins. */
182
182
  minIntervalMs?: number;
183
+ /** Maximum number of requests concurrently in flight. */
184
+ maxConcurrentRequests?: number;
183
185
  }
184
186
 
185
187
  export interface ProviderRequestPacingConfig extends RequestPacingRule {
@@ -660,13 +662,15 @@ export interface OcxProviderConfig {
660
662
  * Reactive 429 rotation remains available even when proactive routing is disabled.
661
663
  */
662
664
  oauthAccountFailover?: {
665
+ /** Kiro OAuth only: active serving requests per account, 1..100. */
666
+ maxConcurrentPerAccount?: number;
663
667
  enabled?: boolean;
664
668
  /**
665
669
  * Generic OAuth pool selection strategy (#695). Persisted through the pool-settings
666
670
  * contract. Consumed by the selector only while `pool.kernel` is on; with the flag off
667
671
  * it is still merely persisted, so omitted and set behave the same.
668
672
  */
669
- strategy?: "quota" | "round-robin" | "fill-first";
673
+ strategy?: "quota" | "round-robin" | "fill-first" | "least-loaded";
670
674
  /**
671
675
  * 0-100 usage percent at which fill-first advances off the active account (#695).
672
676
  * Read only under `pool.kernel` with `strategy: "fill-first"`; 80 when unset, matching
@@ -221,8 +221,17 @@ export interface OcxImageContent {
221
221
 
222
222
  export interface OcxVideoContent {
223
223
  type: "video";
224
- /** A base64 `data:` URL from an OpenAI-compatible `video_url` part. */
224
+ /**
225
+ * A base64 `data:` URL from an OpenAI-compatible `video_url` part, or a URI
226
+ * the upstream can fetch itself (a YouTube watch URL, a Files API uri).
227
+ */
225
228
  videoUrl: string;
229
+ /**
230
+ * Gemini's agentic video mode, carried verbatim from the caller's
231
+ * `video_url.processing` (#3271). Absent for every request that does not ask
232
+ * for it, so no existing traffic gains a field.
233
+ */
234
+ processing?: string;
226
235
  }
227
236
 
228
237
  /**
@@ -361,7 +370,7 @@ export interface OcxProviderContinuationState {
361
370
  }
362
371
 
363
372
  export type AdapterEvent =
364
- | { type: "heartbeat"; replayUnsafe?: true }
373
+ | { type: "heartbeat"; replayUnsafe?: true; preflightReady?: true }
365
374
  | { type: "text_delta"; text: string; phase?: OcxMessagePhase }
366
375
  | { type: "thinking_delta"; thinking: string }
367
376
  // Anthropic extended-thinking round-trip: signature_delta for the current thinking block, and
@@ -438,6 +447,8 @@ export interface OcxUrlCitation {
438
447
  * - `totalTokens` = inputTokens + outputTokens. Never re-add cache detail on top.
439
448
  */
440
449
  export interface OcxUsage {
450
+ /** Provider-reported credit spend, independent of token estimates and USD pricing. */
451
+ providerCredits?: number;
441
452
  inputTokens: number;
442
453
  outputTokens: number;
443
454
  /**
@@ -38,6 +38,7 @@ import {
38
38
  } from "./npm-cache-preflight.mjs";
39
39
  import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
40
40
  import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
41
+ import { withoutSiblingMarker } from "../codex/sibling-start";
41
42
  import { packageVersion } from "../lib/package-version";
42
43
  import { selfLaunchArgv } from "../lib/self-launch-argv";
43
44
 
@@ -529,7 +530,8 @@ export async function runUpdate(): Promise<void> {
529
530
  };
530
531
  const startProxyDirectly = async (): Promise<boolean> => {
531
532
  if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) return false;
532
- const env = mutation.controlEnvironment();
533
+ // An ordinary owner: a stray sibling marker would otherwise mark it before any probe.
534
+ const env = mutation.controlEnvironment(withoutSiblingMarker(process.env));
533
535
  delete env.OCX_SERVICE;
534
536
  const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
535
537
  detached: true, stdio: "ignore", windowsHide: true, env: withProcessRuntimeProvenance(env),
package/src/update/job.ts CHANGED
@@ -60,6 +60,8 @@ import {
60
60
  runNpmCachePreflight,
61
61
  type NpmCachePreflightReason,
62
62
  } from "./npm-cache-preflight.mjs";
63
+ import { guiUpdateWorkerCommand } from "./worker-launch";
64
+ import { withoutSiblingMarker } from "../codex/sibling-start";
63
65
 
64
66
  const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
65
67
  const UPDATE_JOB_FILENAME = "update-job.json";
@@ -574,7 +576,8 @@ export function spawnGuiUpdateWorker(
574
576
  restart ? "restart" : "no-restart",
575
577
  ]);
576
578
  if (process.platform !== "win32") {
577
- return spawn(process.execPath, args, {
579
+ const launch = guiUpdateWorkerCommand(process.execPath, args);
580
+ return spawn(launch.command, launch.argv, {
578
581
  detached: true,
579
582
  stdio: "ignore",
580
583
  windowsHide: true,
@@ -932,7 +935,8 @@ function spawnDetachedStart(
932
935
  launcher = packageLauncherPath(),
933
936
  ): ChildProcess {
934
937
  const cmd = restartCommand(false, installer, launcher, port);
935
- const env = { ...process.env };
938
+ // An ordinary owner: a stray sibling marker would otherwise mark it before any probe.
939
+ const env: NodeJS.ProcessEnv = withoutSiblingMarker(process.env);
936
940
  delete env.OCX_SERVICE;
937
941
  updateJob(job, {}, `$ ${cmd.display}`);
938
942
  let stdio: "ignore" | [ "ignore", number, number ] = "ignore";
@@ -0,0 +1,100 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { dirname, isAbsolute, join } from "node:path";
3
+ import {
4
+ observePackageManifestAt,
5
+ packageManifestPathIn,
6
+ runningPackageRoot,
7
+ } from "../lib/package-tree-integrity";
8
+ import type { LauncherTarget, ResolveLauncherTarget } from "../lib/package-tree-retarget";
9
+ import { readServiceInstallState, SERVICE_MANAGED_ENV } from "../service/state";
10
+ import { detectInstallOwnershipFromPath, type InstallOwnership } from "./install-detection.mjs";
11
+
12
+ const PACKAGE_DIR = join("@bitkyc08", "opencodex");
13
+
14
+ /**
15
+ * The package launcher mise's npm backend creates in `<toolRoot>/<selector>/node_modules/.bin`,
16
+ * beside the package it starts. This is the layout `detectInstallOwnershipFromPath` verifies as
17
+ * mise-owned; a launcher in any other layout is not followed.
18
+ */
19
+ const LAUNCHER_BIN = join("node_modules", ".bin");
20
+ const LAUNCHER_PACKAGE_DIR = join("node_modules", PACKAGE_DIR);
21
+
22
+ export interface MiseLauncherTargetDeps {
23
+ platform?: NodeJS.Platform;
24
+ env?: NodeJS.ProcessEnv;
25
+ ownership?: () => InstallOwnership;
26
+ launcherPath?: () => string | undefined;
27
+ runningRoot?: () => string;
28
+ realpath?: (path: string) => string;
29
+ }
30
+
31
+ export interface MiseLauncherTargetWatchPlan {
32
+ runningRoot: string;
33
+ resolveTarget: ResolveLauncherTarget;
34
+ }
35
+
36
+ function canonical(path: string, realpath: (path: string) => string): string | null {
37
+ try {
38
+ return realpath(path);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Map a recorded service launcher to the package directory it starts, when it is one of mise's
46
+ * own package launchers for this tool. A mise shim (`<data>/shims/ocx`) resolves to the mise
47
+ * binary and names no package, and anything outside `<toolRoot>/<selector>/` is not a launcher
48
+ * this install owns, so both are unsupported and return null.
49
+ */
50
+ function launcherPackageEntry(launcherPath: string, toolRoot: string, realpath: (path: string) => string): string | null {
51
+ if (!isAbsolute(launcherPath)) return null;
52
+ const binDir = dirname(launcherPath);
53
+ const canonicalToolRoot = canonical(toolRoot, realpath);
54
+ if (canonicalToolRoot === null) return null;
55
+ if (!binDir.endsWith(`/${LAUNCHER_BIN}`)) return null;
56
+ const selectorDir = binDir.slice(0, binDir.length - LAUNCHER_BIN.length - 1);
57
+ if (canonical(dirname(selectorDir), realpath) !== canonicalToolRoot) return null;
58
+ return join(selectorDir, LAUNCHER_PACKAGE_DIR);
59
+ }
60
+
61
+ /**
62
+ * Decide whether this process should watch its service launcher for a mise upgrade, and build the
63
+ * resolver when it should.
64
+ *
65
+ * Eligible only when every one of these holds:
66
+ * - Linux. launchd services always run pinned package paths (see the stable-launcher contract),
67
+ * and Windows services have no launcher, so neither can follow a moved `latest` link.
68
+ * - The process is the managed service job itself (`OCX_SERVICE_MANAGED=1`). `OCX_SERVICE=1`
69
+ * alone is also set on proxies `ocx claude`/`ocx opencode` spawn, and a foreground proxy must
70
+ * never restart itself because a service record exists.
71
+ * - The running package has a verified mise owner, and the recorded launcher is one of that
72
+ * tool's package launchers.
73
+ * - At boot, the launcher resolves to exactly the running package. That proves the supervisor
74
+ * started this process through it, so a restart through it converges instead of looping.
75
+ */
76
+ export function planMiseLauncherTargetWatch(deps: MiseLauncherTargetDeps = {}): MiseLauncherTargetWatchPlan | null {
77
+ if ((deps.platform ?? process.platform) !== "linux") return null;
78
+ if ((deps.env ?? process.env)[SERVICE_MANAGED_ENV] !== "1") return null;
79
+ const realpath = deps.realpath ?? realpathSync;
80
+ const runningRoot = canonical((deps.runningRoot ?? runningPackageRoot)(), realpath);
81
+ if (runningRoot === null) return null;
82
+ const ownership = (deps.ownership ?? (() => detectInstallOwnershipFromPath(runningRoot)))();
83
+ if (ownership.installer !== "mise" || !ownership.owner) return null;
84
+ const owner = ownership.owner;
85
+ const launcherPath = (deps.launcherPath ?? (() => readServiceInstallState()?.launcherPath))();
86
+ if (!launcherPath) return null;
87
+ const entry = launcherPackageEntry(launcherPath, owner.toolRoot, realpath);
88
+ const toolRoot = canonical(owner.toolRoot, realpath);
89
+ if (entry === null || toolRoot === null) return null;
90
+
91
+ const resolveTarget = (): LauncherTarget | null => {
92
+ const root = canonical(entry, realpath);
93
+ // A floating link repointed outside this tool's installs is not a mise upgrade of it.
94
+ if (root === null || !root.startsWith(`${toolRoot}/`)) return null;
95
+ const manifest = observePackageManifestAt(packageManifestPathIn(root));
96
+ return manifest === null ? null : { root, manifest };
97
+ };
98
+ if (resolveTarget()?.root !== runningRoot) return null;
99
+ return { runningRoot, resolveTarget };
100
+ }