@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -1,13 +1,14 @@
1
1
  import { isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
2
+ import { sseDataPayload } from "./sse-payload-rewrite";
2
3
 
3
4
  export const MAX_CLIENT_SSE_FRAME_BYTES = 4 * 1024 * 1024;
4
5
 
5
6
  export const EMPTY_BYTES = new Uint8Array(0);
6
7
 
7
- const LF_LF = Uint8Array.of(10, 10);
8
- const LF_CR_LF = Uint8Array.of(10, 13, 10);
9
- const CR_LF_LF = Uint8Array.of(13, 10, 10);
10
- const CR_LF_CR_LF = Uint8Array.of(13, 10, 13, 10);
8
+ // Intern every legal pair; a split CRLF may leave a framing-only LF continuation.
9
+ const DELIMITERS = new Map([
10
+ "\n", "\n\n", "\n\r", "\n\r\n", "\r\r", "\r\r\n", "\r\n\n", "\r\n\r", "\r\n\r\n",
11
+ ].map(text => [text, Uint8Array.from(text, char => char.charCodeAt(0))]));
11
12
 
12
13
  export class SseFrameTooLargeError extends Error {
13
14
  readonly maxBytes: number;
@@ -32,6 +33,8 @@ export class SseFrameCountLimitError extends Error {
32
33
  export type BoundedSseFrame = {
33
34
  block: Uint8Array;
34
35
  delimiter: Uint8Array;
36
+ /** A late LF extending a previously returned CR; no new event. */
37
+ continuation?: true;
35
38
  };
36
39
 
37
40
  /**
@@ -40,29 +43,22 @@ export type BoundedSseFrame = {
40
43
  * Returns the delimiter length in bytes, `0` when `index` does not start a
41
44
  * delimiter, and `undefined` when more bytes are required to decide.
42
45
  */
43
- function delimiterLengthAt(
46
+ export function sseDelimiterLengthAt(
44
47
  index: number,
45
48
  length: number,
46
49
  byteAt: (index: number) => number,
47
50
  ): number | undefined {
48
51
  const first = byteAt(index);
49
- if (first === 10) {
50
- if (index + 1 >= length) return undefined;
51
- const second = byteAt(index + 1);
52
- if (second === 10) return 2;
53
- if (second !== 13) return 0;
54
- if (index + 2 >= length) return undefined;
55
- return byteAt(index + 2) === 10 ? 3 : 0;
56
- }
57
- if (first !== 13) return 0;
58
- if (index + 1 >= length) return undefined;
59
- if (byteAt(index + 1) !== 10) return 0;
60
- if (index + 2 >= length) return undefined;
61
- const third = byteAt(index + 2);
62
- if (third === 10) return 3;
63
- if (third !== 13) return 0;
64
- if (index + 3 >= length) return undefined;
65
- return byteAt(index + 3) === 10 ? 4 : 0;
52
+ if (first !== 10 && first !== 13) return 0;
53
+ let next = index + 1;
54
+ if (next >= length) return undefined;
55
+ if (first === 13 && byteAt(next) === 10) next += 1;
56
+ if (next >= length) return undefined;
57
+ const second = byteAt(next);
58
+ if (second !== 10 && second !== 13) return 0;
59
+ next += 1;
60
+ if (second === 13 && next < length && byteAt(next) === 10) next += 1;
61
+ return next - index;
66
62
  }
67
63
 
68
64
  function delimiterBytesAt(
@@ -70,9 +66,9 @@ function delimiterBytesAt(
70
66
  delimiterLength: number,
71
67
  byteAt: (index: number) => number,
72
68
  ): Uint8Array {
73
- if (delimiterLength === 2) return LF_LF;
74
- if (delimiterLength === 4) return CR_LF_CR_LF;
75
- return byteAt(index) === 10 ? LF_CR_LF : CR_LF_LF;
69
+ let text = "";
70
+ for (let offset = 0; offset < delimiterLength; offset++) text += String.fromCharCode(byteAt(index + offset));
71
+ return DELIMITERS.get(text)!;
76
72
  }
77
73
 
78
74
  function copyRange(
@@ -100,14 +96,8 @@ function copyRange(
100
96
  * cost on ordinary framing.
101
97
  */
102
98
  function isResponsesTerminalFrame(block: Uint8Array): boolean {
103
- const data: string[] = [];
104
- for (const line of new TextDecoder().decode(block).split(/\r?\n/)) {
105
- if (!line.startsWith("data:")) continue;
106
- const value = line.slice(5);
107
- data.push(value.startsWith(" ") ? value.slice(1) : value);
108
- }
109
- if (data.length === 0) return false;
110
- const payload = data.join("\n");
99
+ const payload = sseDataPayload(new TextDecoder().decode(block));
100
+ if (payload === null) return false;
111
101
  if (payload === "[DONE]") return false;
112
102
  try {
113
103
  const parsed = JSON.parse(payload) as {
@@ -161,6 +151,7 @@ export class BoundedSseFrameBuffer {
161
151
  private candidate: Uint8Array = EMPTY_BYTES;
162
152
  private candidateBytes = 0;
163
153
  private disposed = false;
154
+ private pendingLineFeed = false;
164
155
 
165
156
  constructor(maxFrameBytes = MAX_CLIENT_SSE_FRAME_BYTES) {
166
157
  if (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes <= 0) {
@@ -177,6 +168,7 @@ export class BoundedSseFrameBuffer {
177
168
  this.delimiterTail = EMPTY_BYTES;
178
169
  this.candidate = EMPTY_BYTES;
179
170
  this.candidateBytes = 0;
171
+ this.pendingLineFeed = false;
180
172
  }
181
173
 
182
174
  private ensureCapacity(requiredBytes: number): void {
@@ -226,6 +218,7 @@ export class BoundedSseFrameBuffer {
226
218
  if (chunk.byteLength === 0) return [];
227
219
 
228
220
  const frames: BoundedSseFrame[] = [];
221
+ let completedFrames = 0;
229
222
  const previousTail = this.delimiterTail;
230
223
  this.delimiterTail = EMPTY_BYTES;
231
224
  const tailLength = previousTail.byteLength;
@@ -245,12 +238,19 @@ export class BoundedSseFrameBuffer {
245
238
 
246
239
  try {
247
240
  let index = 0;
248
- let retainedThrough = 0;
241
+ if (this.pendingLineFeed && byteAt(0) === 10) {
242
+ // The prior event was already dispatched on CR. Preserve its later LF without
243
+ // charging it to the next event or letting it pair with another line ending.
244
+ frames.push({ block: EMPTY_BYTES, delimiter: DELIMITERS.get("\n")!, continuation: true });
245
+ index = 1;
246
+ }
247
+ this.pendingLineFeed = false;
248
+ let retainedThrough = index;
249
249
  while (index < totalLength) {
250
- const delimiterLength = delimiterLengthAt(index, totalLength, byteAt);
250
+ const delimiterLength = sseDelimiterLengthAt(index, totalLength, byteAt);
251
251
  if (delimiterLength === undefined) break;
252
252
  if (delimiterLength > 0) {
253
- if (frames.length >= this.maxFramesPerFeed) {
253
+ if (completedFrames >= this.maxFramesPerFeed) {
254
254
  this.clear();
255
255
  this.disposed = true;
256
256
  throw new SseFrameCountLimitError(this.maxFramesPerFeed);
@@ -259,7 +259,9 @@ export class BoundedSseFrameBuffer {
259
259
  const block = this.takeCandidate();
260
260
  const delimiter = delimiterBytesAt(index, delimiterLength, byteAt);
261
261
  frames.push({ block, delimiter });
262
+ completedFrames += 1;
262
263
  index += delimiterLength;
264
+ this.pendingLineFeed = index === totalLength && byteAt(index - 1) === 13;
263
265
  retainedThrough = index;
264
266
  continue;
265
267
  }
@@ -65,7 +65,8 @@ export function composeSseBlockRewrites(...rewrites: SseBlockRewrite[]): SseBloc
65
65
 
66
66
  /** Split one complete SSE event block while retaining its original blank-line delimiter. */
67
67
  export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null {
68
- const match = buffer.match(/\r?\n\r?\n/);
68
+ // The negative lookahead prevents backtracking from splitting one CRLF into two endings.
69
+ const match = buffer.match(/(?:\r\n|\r(?!\n)|\n){2}/);
69
70
  if (!match || match.index === undefined) return null;
70
71
  return {
71
72
  block: buffer.slice(0, match.index),
@@ -87,6 +88,7 @@ export function createSseBlockBuffer(
87
88
  append(fragment: string): void;
88
89
  next(): { block: string; delimiter: string } | null;
89
90
  tail(): string;
91
+ isEmpty(): boolean;
90
92
  compact(): void;
91
93
  clear(): void;
92
94
  } {
@@ -95,6 +97,7 @@ export function createSseBlockBuffer(
95
97
  let offset = 0;
96
98
  let scanOffset = 0;
97
99
  let bufferBytes = 0;
100
+ const delimiterPattern = /(?:\r\n|\r(?!\n)|\n){2}/g;
98
101
 
99
102
  const compact = (): void => {
100
103
  if (offset === 0) return;
@@ -128,43 +131,33 @@ export function createSseBlockBuffer(
128
131
  }
129
132
  },
130
133
  next() {
131
- for (;;) {
132
- const newline = buffer.indexOf("\n", scanOffset);
133
- if (newline < 0) {
134
- scanOffset = buffer.length;
135
- return null;
136
- }
137
- let end = newline + 1;
138
- if (buffer[end] === "\r") end += 1;
139
- if (end === buffer.length) {
140
- // Keep the candidate first newline until its possible blank-line delimiter arrives.
141
- scanOffset = newline;
142
- return null;
143
- }
144
- if (buffer[end] !== "\n") {
145
- scanOffset = newline + 1;
146
- continue;
147
- }
148
- end += 1;
149
- const start = newline > offset && buffer[newline - 1] === "\r" ? newline - 1 : newline;
150
- const block = buffer.slice(offset, start);
151
- const delimiter = buffer.slice(start, end);
152
- const nextBytes = bufferBytes - Buffer.byteLength(block, "utf8") - delimiter.length;
153
- const reservation = budget.reserveTransient(nextBytes, scope);
154
- reservation.commitRetained();
155
- budget.releaseRetained(bufferBytes, scope);
156
- bufferBytes = nextBytes;
157
- offset = end;
158
- scanOffset = end;
159
- if (offset === buffer.length) {
160
- buffer = "";
161
- offset = 0;
162
- scanOffset = 0;
163
- }
164
- return { block, delimiter };
134
+ delimiterPattern.lastIndex = scanOffset;
135
+ const match = delimiterPattern.exec(buffer);
136
+ if (!match) {
137
+ // A delimiter is at most four code units. Revisit only its possible split prefix.
138
+ scanOffset = Math.max(offset, buffer.length - 3);
139
+ return null;
165
140
  }
141
+ const start = match.index;
142
+ const end = start + match[0].length;
143
+ const block = buffer.slice(offset, start);
144
+ const delimiter = buffer.slice(start, end);
145
+ const nextBytes = bufferBytes - Buffer.byteLength(block, "utf8") - delimiter.length;
146
+ const reservation = budget.reserveTransient(nextBytes, scope);
147
+ reservation.commitRetained();
148
+ budget.releaseRetained(bufferBytes, scope);
149
+ bufferBytes = nextBytes;
150
+ offset = end;
151
+ scanOffset = end;
152
+ if (offset === buffer.length) {
153
+ buffer = "";
154
+ offset = 0;
155
+ scanOffset = 0;
156
+ }
157
+ return { block, delimiter };
166
158
  },
167
159
  tail: () => buffer.slice(offset),
160
+ isEmpty: () => offset === buffer.length,
168
161
  compact,
169
162
  clear() {
170
163
  budget.releaseRetained(bufferBytes, scope);
@@ -184,11 +177,11 @@ export function sseDataPayload(block: string): string | null {
184
177
  const len = block.length;
185
178
 
186
179
  while (lineStart < len) {
187
- const nextNewline = block.indexOf("\n", lineStart);
188
- let lineEnd = nextNewline === -1 ? len : nextNewline;
189
- const nextStart = nextNewline === -1 ? len : nextNewline + 1;
190
- if (lineEnd > lineStart && block.charCodeAt(lineEnd - 1) === 13) {
191
- lineEnd -= 1;
180
+ let lineEnd = lineStart;
181
+ while (lineEnd < len && block.charCodeAt(lineEnd) !== 13 && block.charCodeAt(lineEnd) !== 10) lineEnd += 1;
182
+ let nextStart = lineEnd;
183
+ if (nextStart < len) {
184
+ nextStart += block.charCodeAt(lineEnd) === 13 && block.charCodeAt(lineEnd + 1) === 10 ? 2 : 1;
192
185
  }
193
186
 
194
187
  const lineLen = lineEnd - lineStart;
@@ -218,10 +211,19 @@ export function sseDataPayload(block: string): string | null {
218
211
  return found ? result : null;
219
212
  }
220
213
 
214
+ /** Shared line rules for field-mutating repairs; identity paths keep the original block. */
215
+ export function sseLineEnding(block: string): "\r\n" | "\r" | "\n" {
216
+ return block.match(/\r\n|\r|\n/)?.[0] as "\r\n" | "\r" | "\n" ?? "\n";
217
+ }
218
+
219
+ export function splitSseBlock(block: string): { newline: string; lines: string[] } {
220
+ return { newline: sseLineEnding(block), lines: block.split(/\r\n|\r|\n/) };
221
+ }
222
+
221
223
  /** Replace an SSE event's data field while preserving non-data fields and newline style. */
222
224
  export function replaceSseDataPayload(block: string, payload: string): string {
223
- const newline = block.includes("\r\n") ? "\r\n" : "\n";
224
- const lines = block.split(/\r?\n/);
225
+ if (sseDataPayload(block) === payload) return block;
226
+ const { newline, lines } = splitSseBlock(block);
225
227
  const rewritten: string[] = [];
226
228
  let replaced = false;
227
229
  for (const line of lines) {
@@ -230,7 +232,7 @@ export function replaceSseDataPayload(block: string, payload: string): string {
230
232
  continue;
231
233
  }
232
234
  if (!replaced) {
233
- rewritten.push(...payload.split(/\r?\n/).map(line => `data: ${line}`));
235
+ rewritten.push(...payload.split(/\r\n|\r|\n/).map(line => `data: ${line}`));
234
236
  replaced = true;
235
237
  }
236
238
  }
@@ -278,6 +280,8 @@ export function relaySseWithBlockRewrite(
278
280
  // Relays have several independent teardown paths; disposal is exactly once.
279
281
  let disposed = false;
280
282
  let cancelled = false;
283
+ let pendingLineFeed = false;
284
+ let pendingLineFeedVisible = false;
281
285
  const disposeRewrite = (): void => {
282
286
  if (disposed) return;
283
287
  disposed = true;
@@ -308,10 +312,18 @@ export function relaySseWithBlockRewrite(
308
312
  let emitted = 0;
309
313
  let next: { block: string; delimiter: string } | null;
310
314
  while (!cancelled && (next = buffer.next())) {
311
- const outBlocks = rewrite(next.block);
315
+ const { block, delimiter } = next;
316
+ // A CR followed by buffered text is already settled; only an end-of-buffer CR
317
+ // can still receive the LF that extends its delimiter in the next fragment.
318
+ pendingLineFeed = delimiter.endsWith("\r") && buffer.isEmpty();
319
+ const outBlocks = rewrite(block);
320
+ pendingLineFeedVisible = outBlocks.length > 0;
312
321
  if (cancelled) return emitted;
313
- for (const outBlock of outBlocks) {
314
- enqueueText(controller, outBlock + next.delimiter);
322
+ for (let index = 0; index < outBlocks.length; index++) {
323
+ // Synthetic earlier blocks need a settled delimiter; only the final one can
324
+ // inherit an upstream LF in a later chunk. Completing CR as CRLF is equivalent.
325
+ const settled = index < outBlocks.length - 1 && delimiter.endsWith("\r") ? delimiter + "\n" : delimiter;
326
+ enqueueText(controller, outBlocks[index]! + settled);
315
327
  emitted += 1;
316
328
  }
317
329
  }
@@ -332,6 +344,23 @@ export function relaySseWithBlockRewrite(
332
344
  return emitted;
333
345
  };
334
346
 
347
+ const appendFragment = (controller: ReadableStreamDefaultController<Uint8Array>, fragment: string): number => {
348
+ let continuation = false;
349
+ if (pendingLineFeed && fragment.length > 0) {
350
+ continuation = fragment.startsWith("\n");
351
+ pendingLineFeed = false;
352
+ if (continuation) fragment = fragment.slice(1);
353
+ }
354
+ // Consume a late LF before scanning, so it cannot pair with a new LF to create
355
+ // a phantom empty callback. Any following line ending stays in the input buffer.
356
+ buffer.append(fragment);
357
+ if (continuation && pendingLineFeedVisible) {
358
+ enqueueText(controller, "\n");
359
+ return 1;
360
+ }
361
+ return 0;
362
+ };
363
+
335
364
  return new ReadableStream<Uint8Array>({
336
365
  async pull(controller) {
337
366
  try {
@@ -346,7 +375,7 @@ export function relaySseWithBlockRewrite(
346
375
  // after its disposal (#893 review).
347
376
  if (cancelled) return;
348
377
  if (done) {
349
- buffer.append(decoder.decode());
378
+ appendFragment(controller, decoder.decode());
350
379
  emitProcessedBlocks(controller, true);
351
380
  if (cancelled) return;
352
381
  buffer.clear();
@@ -354,8 +383,8 @@ export function relaySseWithBlockRewrite(
354
383
  controller.close();
355
384
  return;
356
385
  }
357
- buffer.append(decoder.decode(value, { stream: true }));
358
- const emitted = emitProcessedBlocks(controller);
386
+ const continuation = appendFragment(controller, decoder.decode(value, { stream: true }));
387
+ const emitted = continuation + emitProcessedBlocks(controller);
359
388
  if (cancelled || emitted > 0) return;
360
389
  }
361
390
  } catch (error) {
@@ -8,6 +8,7 @@ import type { ResponsesTerminalStatus } from "../bridge";
8
8
  import type { DataPlaneAdmission } from "./auth-cors";
9
9
  import type { AdmissionLease, AdmissionReservation } from "../lib/admission";
10
10
  import { BoundedSseFrameBuffer } from "./sse-frame-buffer";
11
+ import { sseDataPayload as parseSseBlock } from "./sse-payload-rewrite";
11
12
  import { safeResponseHeaders } from "./safe-response-headers";
12
13
  import type { AudioSocketTarget } from "./audio-dictation";
13
14
 
@@ -183,17 +184,6 @@ export function buildWsErrorFrame(
183
184
  };
184
185
  }
185
186
 
186
- function parseSseBlock(block: string): string | null {
187
- const data: string[] = [];
188
- for (const line of block.split(/\r?\n/)) {
189
- if (line.startsWith("data:")) {
190
- const value = line.slice(5);
191
- data.push(value.startsWith(" ") ? value.slice(1) : value);
192
- }
193
- }
194
- return data.length > 0 ? data.join("\n") : null;
195
- }
196
-
197
187
  function payloadType(payload: string): string | null {
198
188
  try {
199
189
  const json = JSON.parse(payload) as { type?: unknown };
@@ -16,7 +16,8 @@ import { resolveServiceListenPort, reportServiceServing } from "./health";
16
16
  import { platformOps, proxyStillLiveAfterStop, stopTrackedProxyForServiceCommand, installServiceSafely, installFreshWindowsSchedulerSafely, removeServiceInstallState, isServiceInstalled } from "./orchestration";
17
17
  import { repairService } from "./repair";
18
18
  import type { ServiceRepairVerb } from "./repair";
19
- import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership } from "./state";
19
+ import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership, serviceStatePaths } from "./state";
20
+ import { ownershipMutationLeaseStatusLine } from "./ownership-mutation-lease.mjs";
20
21
  import { foreignServiceOwnerRefusal, unknownServiceOwnerRefusal } from "./repair";
21
22
  import type { ServiceBackend } from "./state";
22
23
  import { unitPath } from "./systemd";
@@ -419,6 +420,10 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
419
420
  // subsumes the not-installed case and adds the serving / stale-plist split.
420
421
  console.log(await serviceStatusReport());
421
422
  }
423
+ // A busy lease is why a supervised start can keep failing while the line above only
424
+ // says "not running"; report its recorded PID with unverified identity and lease age here.
425
+ const lease = ownershipMutationLeaseStatusLine(serviceStatePaths());
426
+ if (lease) console.log(lease);
422
427
  console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
423
428
  break;
424
429
  }
@@ -5,6 +5,22 @@ export interface OwnershipMutationLeaseOptions {
5
5
  readonly processAlive?: (pid: number) => boolean;
6
6
  readonly beforeRelease?: (lockPath: string) => void;
7
7
  readonly joinToken?: string;
8
+ /** Names the current executable at the recorded PID, identity unverified; defaults to `tasklist` or `ps`. */
9
+ readonly processImage?: (pid: number) => string | null;
10
+ }
11
+
12
+ /** The recorded lease owner, identity unverified, read for diagnostics; never used to reclaim it. */
13
+ export interface OwnershipMutationLeaseHolder {
14
+ readonly path: string;
15
+ /** The recorded PID; null when the directory holds no parseable owner. */
16
+ readonly pid: number | null;
17
+ /** Liveness of the current process at the recorded PID; identity is not verified. */
18
+ readonly alive: boolean | null;
19
+ /** The current executable at the recorded PID when alive and readable; identity is not verified. */
20
+ readonly image: string | null;
21
+ /** Milliseconds since the owner was written, on the clock stale recovery uses. */
22
+ readonly ageMs: number | null;
23
+ readonly record: "complete" | "incomplete" | "empty" | "unreadable";
8
24
  }
9
25
 
10
26
  export interface OwnershipMutationLease { readonly token: string; release(): void }
@@ -30,3 +46,13 @@ export declare function withOwnershipMutationLease<T>(
30
46
  run: () => T,
31
47
  options?: OwnershipMutationLeaseOptions,
32
48
  ): T;
49
+
50
+ export declare function inspectOwnershipMutationLease(
51
+ statePaths: readonly string[],
52
+ options?: Pick<OwnershipMutationLeaseOptions, "now" | "processAlive" | "processImage">,
53
+ ): OwnershipMutationLeaseHolder | null;
54
+
55
+ export declare function ownershipMutationLeaseStatusLine(
56
+ statePaths: readonly string[],
57
+ options?: Pick<OwnershipMutationLeaseOptions, "now" | "processAlive" | "processImage">,
58
+ ): string | null;
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import {
3
4
  closeSync,
@@ -108,6 +109,73 @@ function readIncompleteOwner(path) {
108
109
  } catch { return null; }
109
110
  }
110
111
 
112
+ /** The current executable at the recorded PID, identity unverified; null if unreadable within a second. */
113
+ function processImage(pid) {
114
+ try {
115
+ if (process.platform === "win32") {
116
+ const listed = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
117
+ encoding: "utf8", timeout: 1_000, windowsHide: true,
118
+ });
119
+ const match = /^"([^"]+)","(\d+)"/.exec((listed.stdout ?? "").trim());
120
+ return listed.status === 0 && match && Number(match[2]) === pid ? match[1] : null;
121
+ }
122
+ const listed = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], { encoding: "utf8", timeout: 1_000 });
123
+ const command = (listed.stdout ?? "").trim();
124
+ return listed.status === 0 && command ? basename(command) : null;
125
+ } catch { return null; }
126
+ }
127
+
128
+ /**
129
+ * The recorded owner of the lease directory at `path`, read as stale recovery reads it, or null
130
+ * when no lock directory exists. `pid` is null when the directory has no parseable owner; `age`
131
+ * uses the same clock as reclamation (the later of the record's creation and its file mtime).
132
+ * Liveness and image describe the current process at the recorded PID; its identity is not verified.
133
+ */
134
+ function leaseHolder(path, now, alive, image) {
135
+ if (!existsSync(path)) return null;
136
+ const observed = readOwner(path);
137
+ const incomplete = observed ? null : readIncompleteOwner(path);
138
+ // Neither reader matched: the holder may have released between these reads. Report a free
139
+ // lease then, not an unreadable one.
140
+ if (!observed && !incomplete && !existsSync(path)) return null;
141
+ const pid = observed?.record.pid ?? incomplete?.pid ?? null;
142
+ const since = observed ? Math.max(observed.record.createdAt, observed.mtimeMs) : incomplete?.mtimeMs ?? null;
143
+ const live = pid === null ? null : alive(pid);
144
+ return {
145
+ path,
146
+ pid,
147
+ alive: live,
148
+ image: live ? image(pid) : null,
149
+ ageMs: since === null ? null : Math.max(0, now() - since),
150
+ record: observed ? "complete" : incomplete?.ownerPath ? "incomplete" : incomplete ? "empty" : "unreadable",
151
+ };
152
+ }
153
+
154
+ function describeHolder(holder) {
155
+ const age = holder.ageMs === null ? "" : `, lease age ${Math.round(holder.ageMs / 1_000)}s`;
156
+ if (holder.record === "unreadable") return "holder unknown: the lock directory does not hold exactly one owner file";
157
+ if (holder.record === "empty") return `no owner file written yet${age}`;
158
+ const state = holder.alive ? ["alive", ...(holder.image ? [holder.image] : [])].join(", ") : "not alive";
159
+ const partial = holder.record === "incomplete" ? ", owner record incomplete" : "";
160
+ return `recorded PID ${holder.pid} [${state}, identity unverified${partial}]${age}`;
161
+ }
162
+
163
+ const RECLAIM_HINT = `stale leases are reclaimed after ${STALE_MS / 1_000}s once the recorded PID is no longer alive`;
164
+
165
+ /** The recorded lease owner for `statePaths`, identity unverified, or null if free. Reads only; never reclaims. */
166
+ export function inspectOwnershipMutationLease(statePaths, options = {}) {
167
+ return leaseHolder(leasePath(statePaths), options.now ?? Date.now, options.processAlive ?? processAlive,
168
+ options.processImage ?? processImage);
169
+ }
170
+
171
+ /** One status line naming the recorded PID, identity unverified, or null when the lease is free. */
172
+ export function ownershipMutationLeaseStatusLine(statePaths, options = {}) {
173
+ const holder = inspectOwnershipMutationLease(statePaths, options);
174
+ if (!holder) return null;
175
+ return `Runtime mutation lease busy at ${holder.path} (${describeHolder(holder)}); `
176
+ + `opencodex waits for it to start or change service state, and ${RECLAIM_HINT}.`;
177
+ }
178
+
111
179
  function reclaim(path, now, alive) {
112
180
  const observed = readOwner(path);
113
181
  const incomplete = observed ? null : readIncompleteOwner(path);
@@ -182,7 +250,14 @@ export function acquireOwnershipMutationLease(
182
250
  }
183
251
  if (error?.code !== "EEXIST") throw error;
184
252
  if (reclaim(path, now, alive)) continue;
185
- if (now() >= deadline) throw new Error(`another process owns the runtime mutation lease at ${path}`);
253
+ if (now() >= deadline) {
254
+ const holder = leaseHolder(path, now, alive, options.processImage ?? processImage);
255
+ const error = new Error(`another process owns the runtime mutation lease at ${path}`
256
+ + (holder ? ` (${describeHolder(holder)}; ${RECLAIM_HINT})` : ""));
257
+ error.code = "OWNERSHIP_MUTATION_LEASE_BUSY";
258
+ error.holder = holder;
259
+ throw error;
260
+ }
186
261
  (options.sleep ?? sleep)(POLL_MS);
187
262
  }
188
263
  }
@@ -154,7 +154,6 @@ export function buildWindowsServiceScript(
154
154
  ' if exist "%OCX_PKG_DIR%" rmdir /s /q "%OCX_PKG_DIR%" 2>nul',
155
155
  ' move "%OCX_PKG_DIR%\\..\\%%B\\opencodex" "%OCX_PKG_DIR%" >nul 2>&1',
156
156
  ' if exist "%OCX_PKG_DIR%\\package.json" (',
157
- ' set "OCX_RESTORED_BACKUP=%%B"',
158
157
  " goto backup_restored",
159
158
  " )",
160
159
  " )",
@@ -162,7 +161,7 @@ export function buildWindowsServiceScript(
162
161
  '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] no restorable backup found',
163
162
  "goto :eof",
164
163
  ":backup_restored",
165
- '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from %OCX_RESTORED_BACKUP%',
164
+ '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from transactional-update backup',
166
165
  "goto :eof",
167
166
  ].filter((line): line is string => Boolean(line));
168
167
  return `${lines.join("\r\n")}\r\n`;
@@ -29,6 +29,7 @@ import {
29
29
  import { decodeWindowsTextBytes } from "./lib/windows-text";
30
30
  import { WINSW_SERVICE_ID } from "./lib/winsw";
31
31
  import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV } from "./lib/bun-runtime";
32
+ import { REAL_BUN_MIN_BYTES } from "./lib/bun-binary-validator.mjs";
32
33
  import { WINDOWS_WRAPPER_PROTOCOL_ENV, WINDOWS_WRAPPER_STAY_OUT_EXIT_CODE } from "./service/windows-wrapper-exit";
33
34
  import { buildWindowsServiceScript, windowsTaskActionMatches } from "./service/windows-taskxml";
34
35
  import { inspectServiceStateEvidence, serviceStatePathsForOpenCodexHome } from "./service/state";
@@ -613,8 +614,41 @@ function matchesGeneratedStandaloneControlFlow(body: string, port: number): bool
613
614
  scriptLines.filter((line, index) => index >= end || line === 'set "ERRORLEVEL="' || !line.startsWith('set "'));
614
615
  const actualFlow = withoutPrefixSets(lines, boundary);
615
616
  const generatedFlow = withoutPrefixSets(expected, expectedBoundary);
616
- return actualFlow.length === generatedFlow.length
617
- && actualFlow.every((line, index) => line === generatedFlow[index]);
617
+ // Read-only recognition of the exact previous generator output keeps installed
618
+ // standalone services identifiable across the backup-log hardening update.
619
+ // Never emit or execute this legacy variant; every other control line still matches.
620
+ const legacyFlow = generatedFlow.flatMap(line => {
621
+ if (line === " goto backup_restored") return [' set "OCX_RESTORED_BACKUP=%%B"', line];
622
+ if (line === '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from transactional-update backup') {
623
+ return ['>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from %OCX_RESTORED_BACKUP%'];
624
+ }
625
+ return [line];
626
+ });
627
+ // Also recognize the exact pre-placeholder version, not a partial size-gate
628
+ // hybrid. Only remove these complete known blocks from trusted generator output.
629
+ const beforePlaceholderGate = (flow: string[]): string[] | null => {
630
+ const blocks = [
631
+ ['set "OCX_BUN_BYTES="', 'for %%F in ("%OCX_BUN%") do set "OCX_BUN_BYTES=%%~zF"',
632
+ 'if not defined OCX_BUN_BYTES goto bun_not_ready', `if %OCX_BUN_BYTES% LSS ${REAL_BUN_MIN_BYTES} goto bun_not_ready`],
633
+ [":bun_not_ready",
634
+ '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] bundled Bun is not ready (%OCX_BUN_BYTES% bytes, npm placeholder or mid-install); waiting for its postinstall, retrying in 5s - if this persists, reinstall opencodex with bun scripts allowed',
635
+ "ping -n 6 127.0.0.1 >nul", "goto loop"],
636
+ ];
637
+ let earlier = flow;
638
+ for (const block of blocks) {
639
+ const at = earlier.indexOf(block[0]!);
640
+ if (at < 0 || !block.every((line, offset) => earlier[at + offset] === line)) return null;
641
+ earlier = [...earlier.slice(0, at), ...earlier.slice(at + block.length)];
642
+ }
643
+ return earlier;
644
+ };
645
+ const recognized = [generatedFlow, legacyFlow];
646
+ for (const flow of [...recognized]) {
647
+ const earlier = beforePlaceholderGate(flow);
648
+ if (earlier) recognized.push(earlier);
649
+ }
650
+ return recognized.some(expectedFlow => actualFlow.length === expectedFlow.length
651
+ && actualFlow.every((line, index) => line === expectedFlow[index]));
618
652
  }
619
653
 
620
654
  /** Validate the generated launch shape before interpreting omitted optional homes. */
@@ -219,6 +219,8 @@ export interface OcxClaudeCodeConfig {
219
219
  * definition. Unset inherits the parent session effort.
220
220
  */
221
221
  subagentEffort?: "low" | "medium" | "high" | "xhigh" | "max";
222
+ /** Optional roster-style model forced on subagents at the next routed Claude Code launch. */
223
+ subagentModelForce?: string;
222
224
  /** Claude-originated web-search override. Unset fields inherit the global sidecar settings. */
223
225
  webSearchSidecar?: { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa"; model?: string };
224
226
  /** Claude-originated vision override. Unset fields inherit the global sidecar settings. */
@@ -750,6 +752,8 @@ export interface OcxConfig {
750
752
  modelPinnedEfforts?: Record<string, string>;
751
753
  compactionRouting?: {
752
754
  model: string;
755
+ /** Incoming model allowlist: exact selectors or provider/*; omitted means all models. */
756
+ sourceModels?: string[];
753
757
  reasoningEffort?: string;
754
758
  /** Compaction triggers this override covers; omission means `["manual"]`. */
755
759
  triggers?: ("manual" | "auto")[];
@@ -1214,6 +1218,8 @@ export interface OcxConfig {
1214
1218
  */
1215
1219
  anthropicAccountPool?: {
1216
1220
  enabled?: boolean;
1221
+ /** Preserve native Claude Messages while the pool is enabled. Default true; false selects legacy translation. */
1222
+ nativeMessages?: boolean;
1217
1223
  /** Usage % threshold for new-session auto-pick. Default 80. 0 = disabled (affinity/active only). */
1218
1224
  autoSwitchThreshold?: number;
1219
1225
  /** New-session rotation strategy. Default quota (today's behaviour). */