@bitkyc08/opencodex 2.83.0 → 2.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/ollama-native.ts +29 -1
  10. package/src/adapters/openai-chat/messages.ts +8 -4
  11. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  12. package/src/adapters/openai-responses/passthrough.ts +3 -0
  13. package/src/adapters/openai-responses/request-strips.ts +10 -2
  14. package/src/adapters/qoder/adapter.ts +16 -14
  15. package/src/bridge/errors.ts +19 -0
  16. package/src/bridge.ts +1 -1
  17. package/src/claude/context-windows.ts +54 -1
  18. package/src/claude/intercept/connect-proxy.ts +10 -3
  19. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  20. package/src/claude/intercept/picker-listener.ts +49 -1
  21. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  22. package/src/claude/intercept/settings.ts +101 -9
  23. package/src/cli/account-api.ts +65 -0
  24. package/src/cli/account-auth.ts +26 -2
  25. package/src/cli/account.ts +41 -5
  26. package/src/cli/capabilities-access-remote.ts +8 -0
  27. package/src/cli/capabilities-base.ts +13 -1
  28. package/src/cli/catalog-command-result.ts +2 -1
  29. package/src/cli/claude.ts +15 -6
  30. package/src/cli/config-command.ts +8 -2
  31. package/src/cli/debug.ts +8 -0
  32. package/src/cli/dispatch.ts +16 -3
  33. package/src/cli/export-command.ts +14 -1
  34. package/src/cli/hub-disable.ts +99 -0
  35. package/src/cli/hub.ts +4 -2
  36. package/src/cli/index.ts +13 -4
  37. package/src/cli/link-child.ts +104 -0
  38. package/src/cli/link-enrollment.ts +96 -0
  39. package/src/cli/link.ts +39 -24
  40. package/src/cli/observe-stream.ts +6 -1
  41. package/src/cli/observe.ts +10 -2
  42. package/src/cli/provider.ts +12 -0
  43. package/src/cli/registry.ts +5 -4
  44. package/src/cli/runtime-api.ts +27 -2
  45. package/src/cli/status.ts +104 -20
  46. package/src/client/connect.ts +39 -4
  47. package/src/client/hub-client.ts +14 -7
  48. package/src/client/hub-state.ts +25 -14
  49. package/src/client/link-home.ts +248 -0
  50. package/src/client/link-management-relay.ts +61 -0
  51. package/src/client/link-relay.ts +6 -1
  52. package/src/client/link-status.ts +14 -0
  53. package/src/client/machine-api.ts +14 -7
  54. package/src/client/machine-listener.ts +29 -4
  55. package/src/clients/config-export.ts +46 -16
  56. package/src/clients/omo-role-models.ts +6 -3
  57. package/src/codex/agent-role-models.ts +26 -8
  58. package/src/codex/auth-api/account-list.ts +17 -2
  59. package/src/codex/auth-api/account-selection.ts +17 -7
  60. package/src/codex/auth-api/routes.ts +5 -1
  61. package/src/codex/autostart-health.ts +3 -1
  62. package/src/codex/catalog/bundled.ts +5 -1
  63. package/src/codex/catalog/effort.ts +2 -2
  64. package/src/codex/catalog/metadata.ts +4 -2
  65. package/src/codex/catalog/native-models.ts +16 -2
  66. package/src/codex/native-profile-startup.ts +4 -1
  67. package/src/codex/prompt-journal.ts +20 -2
  68. package/src/codex/prompt-layers/encoding.ts +49 -0
  69. package/src/codex/prompt-layers/import-source.ts +30 -0
  70. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  71. package/src/codex/prompt-layers/toml-read.ts +52 -4
  72. package/src/codex/prompt-layers.ts +383 -20
  73. package/src/codex/prompt-lock-claim.ts +13 -3
  74. package/src/codex/prompt-lock-evidence.ts +3 -3
  75. package/src/codex/prompt-lock-owner.ts +14 -6
  76. package/src/codex/prompt-text-probe.ts +34 -5
  77. package/src/codex/quota.ts +13 -6
  78. package/src/codex/role-auto-assign.ts +2 -5
  79. package/src/codex/routing/active-account.ts +21 -0
  80. package/src/codex/routing.ts +1 -1
  81. package/src/codex/runtime.ts +196 -8
  82. package/src/codex/shim-overlay.ts +8 -4
  83. package/src/codex/shim.ts +1 -1
  84. package/src/combos/failover.ts +3 -0
  85. package/src/config/api-key-reconcile.ts +34 -0
  86. package/src/config/diagnostics.ts +16 -0
  87. package/src/config/feature-flags.ts +5 -0
  88. package/src/config/live-reconcile.ts +43 -29
  89. package/src/config/live-topology.ts +29 -0
  90. package/src/config/load-degrade.ts +2 -1
  91. package/src/config/paths.ts +3 -0
  92. package/src/config/salvage.ts +3 -1
  93. package/src/config/schema/config-schema.ts +2 -0
  94. package/src/generated/compatibility-version.json +311 -171
  95. package/src/grok/grpc-web.ts +23 -1
  96. package/src/grok/reset-coupon-ledger.ts +75 -5
  97. package/src/grok/reset-coupons.ts +4 -2
  98. package/src/integrations/mutation-plan.ts +13 -7
  99. package/src/integrations/registry.ts +45 -11
  100. package/src/integrations/state.ts +19 -7
  101. package/src/integrations/target.ts +42 -5
  102. package/src/integrations/writer.ts +14 -6
  103. package/src/lib/app-owned-memory-stores.ts +4 -0
  104. package/src/lib/app-owned-memory.ts +11 -0
  105. package/src/lib/json-byte-size.ts +31 -0
  106. package/src/lib/optional-shutdown-hooks.ts +35 -0
  107. package/src/lib/outbound-credential-registry.ts +210 -0
  108. package/src/lib/passive-read.ts +35 -0
  109. package/src/lib/shadow-call.ts +44 -5
  110. package/src/link/codex-settings.ts +155 -0
  111. package/src/link/management-routes.ts +89 -0
  112. package/src/link/remote-probe.ts +38 -0
  113. package/src/link/ssh-argv.ts +26 -5
  114. package/src/link/ssh-runner.ts +58 -27
  115. package/src/link/status-projection.ts +22 -4
  116. package/src/link/supervisor.ts +144 -49
  117. package/src/link/tunnel-state.ts +11 -4
  118. package/src/oauth/anthropic-continuity.ts +4 -3
  119. package/src/oauth/anthropic-identity.ts +13 -2
  120. package/src/oauth/anthropic-model-quota.ts +13 -1
  121. package/src/oauth/anthropic-routing.ts +230 -23
  122. package/src/oauth/anthropic.ts +18 -4
  123. package/src/oauth/google-antigravity.ts +24 -7
  124. package/src/oauth/index.ts +24 -15
  125. package/src/oauth/kiro-account-identity.ts +106 -0
  126. package/src/oauth/kiro.ts +48 -5
  127. package/src/oauth/plan.ts +29 -0
  128. package/src/oauth/store.ts +40 -3
  129. package/src/oauth/types.ts +2 -0
  130. package/src/plugins/loader.ts +11 -0
  131. package/src/providers/anthropic-billing-ownership.ts +74 -0
  132. package/src/providers/anthropic-billing-recovery.ts +31 -0
  133. package/src/providers/anthropic-billing-refusal.ts +55 -0
  134. package/src/providers/anthropic-reset-admission.ts +62 -0
  135. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  136. package/src/providers/api-key-resolve.ts +17 -2
  137. package/src/providers/key-failover.ts +52 -5
  138. package/src/providers/model-rename-startup.ts +5 -3
  139. package/src/providers/quota/account-cache.ts +11 -0
  140. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  141. package/src/providers/quota/report-cache.ts +14 -0
  142. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  143. package/src/providers/quota.ts +4 -1
  144. package/src/providers/registry/entries-core.ts +14 -14
  145. package/src/providers/registry/entries-extended.ts +0 -1
  146. package/src/providers/registry/model-seeds.ts +17 -0
  147. package/src/providers/request-pacing.ts +9 -0
  148. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  149. package/src/responses/compaction.ts +14 -0
  150. package/src/responses/parser.ts +7 -1
  151. package/src/server/auth-cors.ts +42 -0
  152. package/src/server/configured-credentials.ts +194 -0
  153. package/src/server/index/link-management.ts +58 -0
  154. package/src/server/index/optional-listeners.ts +5 -1
  155. package/src/server/index/serve-options.ts +13 -0
  156. package/src/server/lifecycle.ts +2 -1
  157. package/src/server/management/agent-settings-routes.ts +28 -10
  158. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  159. package/src/server/management/codex-agent-role-routes.ts +19 -6
  160. package/src/server/management/codex-prompt-routes.ts +136 -5
  161. package/src/server/management/codex-role-auto-assign.ts +1 -1
  162. package/src/server/management/grok-coupon-routes.ts +78 -27
  163. package/src/server/management/link-enrollment.ts +192 -0
  164. package/src/server/management/link-route-state.ts +185 -0
  165. package/src/server/management/link-routes.ts +82 -402
  166. package/src/server/management/model-rows.ts +3 -1
  167. package/src/server/management/provider-overwrite-carry.ts +10 -0
  168. package/src/server/management/provider-routes.ts +1 -0
  169. package/src/server/management/route-registry.ts +5 -3
  170. package/src/server/management/shared.ts +3 -0
  171. package/src/server/management-api.ts +3 -1
  172. package/src/server/messages-native.ts +68 -42
  173. package/src/server/relay-eager.ts +2 -0
  174. package/src/server/request-log-filter.ts +6 -1
  175. package/src/server/request-log-terminal-status.ts +63 -6
  176. package/src/server/responses/adapter-dispatch.ts +232 -79
  177. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  178. package/src/server/responses/codex-ws-exchange.ts +147 -6
  179. package/src/server/responses/codex-ws-wire.ts +203 -0
  180. package/src/server/responses/collaboration.ts +36 -7
  181. package/src/server/responses/compact.ts +126 -48
  182. package/src/server/responses/context-overflow.ts +13 -1
  183. package/src/server/responses/core-codex-account.ts +105 -1
  184. package/src/server/responses/core-combo-failure.ts +14 -0
  185. package/src/server/responses/core-combo.ts +7 -2
  186. package/src/server/responses/core-errors.ts +29 -1
  187. package/src/server/responses/core-options.ts +2 -0
  188. package/src/server/responses/deferred-collab-surface.ts +158 -0
  189. package/src/server/responses/deferred-send-reporter.ts +25 -0
  190. package/src/server/responses/fetch-helpers.ts +12 -1
  191. package/src/server/responses/passthrough-delivery.ts +4 -1
  192. package/src/server/responses/passthrough-dispatch.ts +15 -4
  193. package/src/server/responses/request-prepare.ts +6 -4
  194. package/src/server/responses/request-transport.ts +6 -8
  195. package/src/server/responses/sidecar-execution.ts +11 -2
  196. package/src/server/responses/ws-upstream.ts +9 -1
  197. package/src/server/startup-action-control.ts +22 -2
  198. package/src/server/startup-health-cache.ts +4 -0
  199. package/src/server/system-env-shell.ts +7 -2
  200. package/src/server/system-env.ts +11 -1
  201. package/src/service/cli.ts +5 -1
  202. package/src/service/desktop-startup.ts +147 -34
  203. package/src/service/desktop-supervision.d.mts +27 -1
  204. package/src/service/desktop-supervision.mjs +111 -5
  205. package/src/service/health-sentinel-launch.ts +176 -0
  206. package/src/service/health-sentinel-main.ts +447 -0
  207. package/src/service/health-sentinel-policy.ts +241 -0
  208. package/src/service/health-sentinel-probe.ts +54 -0
  209. package/src/service/health-sentinel-windows.ts +87 -0
  210. package/src/service/linux-linger.ts +20 -0
  211. package/src/types/config.ts +5 -0
  212. package/src/usage/expected-prices.ts +4 -0
  213. package/src/usage/log.ts +19 -1
  214. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  215. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  216. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Health-unresponsiveness recovery policy for the opt-in service health sentinel (#6671).
3
+ *
4
+ * Every supervisor this repository installs restarts the proxy only when it exits: the Windows
5
+ * Task Scheduler batch loop, launchd `KeepAlive`/`SuccessfulExit=false`, systemd
6
+ * `Restart=on-failure` and the desktop app's `on_exit`. A proxy that stays alive with its port
7
+ * listening while its event loop answers nothing is invisible to all of them. The sentinel turns
8
+ * that state into an exit, and this module is the whole of its judgement: pure, clock-injected,
9
+ * and free of I/O so every false-positive guard is testable on any platform.
10
+ *
11
+ * The guards, in the order they apply:
12
+ * - nothing counts until the target has answered once as itself and the startup grace is over;
13
+ * the grace is elapsed time, so setting the clock back neither restarts nor extends it;
14
+ * - only a probe that got no complete answer by its deadline counts. Any answer proves the event
15
+ * loop ran, and a refused or failed connection means the listener is closing, not hung;
16
+ * - an answer from another pid means the port is no longer the target's: stand down for good;
17
+ * - a probe that started far later than the cadence allows, that outlived its own deadline by
18
+ * far more than jitter, or during which the sentinel's own event loop ran late (sleep, or the
19
+ * sentinel itself paused or starved), discards the streak, because the timeout may belong to
20
+ * that pause rather than to the target;
21
+ * - once a probe times out the next ones follow back to back, so a run of timeouts is one
22
+ * continuous unresponsive stretch rather than separate stalls that line up with the cadence;
23
+ * - recovery needs both a run of consecutive timeouts and a minimum unhealthy span, which doubles
24
+ * for each recovery in the last hour, and a cap on recoveries per hour stands the sentinel down:
25
+ * a proxy that hangs again right after every restart is not fixed by killing it, and each kill
26
+ * loses the work it held. Nothing is ever replayed.
27
+ */
28
+
29
+ export type HealthProbeOutcome =
30
+ /** A complete status line and headers arrived before the deadline. */
31
+ | { kind: "answered"; pid: number | null; startedAt: number; endedAt: number }
32
+ /**
33
+ * No complete answer by the deadline, whether or not the connection was accepted.
34
+ * `sentinelLagMs` is how late the sentinel's own event loop ran during the probe: a sentinel
35
+ * that was itself paused or starved cannot tell a silent target from its own silence.
36
+ */
37
+ | { kind: "timeout"; startedAt: number; endedAt: number; sentinelLagMs?: number }
38
+ /** The connection was refused: nothing is listening. */
39
+ | { kind: "refused"; startedAt: number; endedAt: number }
40
+ /** Any other failure. */
41
+ | { kind: "error"; startedAt: number; endedAt: number };
42
+
43
+ export type HealthSentinelTiming = {
44
+ /** Time between the end of one answered probe and the start of the next. */
45
+ intervalMs: number;
46
+ /**
47
+ * Time between a timed-out probe and the next. Short on purpose: back-to-back probes make a run
48
+ * of timeouts mean one continuous unresponsive stretch, so recurring shorter stalls that happen
49
+ * to line up with the cadence cannot add up to a recovery.
50
+ */
51
+ failureIntervalMs: number;
52
+ /** A probe without a complete answer by this deadline is a timeout. */
53
+ probeTimeoutMs: number;
54
+ /** Counted from the sentinel's start; nothing counts before it ends. */
55
+ startupGraceMs: number;
56
+ /** Consecutive timeouts needed before a recovery. */
57
+ failureThreshold: number;
58
+ /** Minimum span from the first failing probe's start to the last one's end, before backoff. */
59
+ minUnhealthyMs: number;
60
+ /** Slack over the cadence (and over a probe's deadline) before a pause voids the streak. */
61
+ suspendSlackMs: number;
62
+ /** A timeout during which the sentinel's own loop ran later than this is not evidence. */
63
+ lagToleranceMs: number;
64
+ /** Window over which earlier recoveries raise the backoff and count toward the cap. */
65
+ recoveryWindowMs: number;
66
+ /** Recoveries within the window after which the sentinel stands down. */
67
+ maxRecoveriesPerWindow: number;
68
+ /** Ceiling on the backoff multiplier. */
69
+ maxBackoffMultiplier: number;
70
+ /**
71
+ * Oldest the final failing observation may be when the kill is issued. Lease, history and log
72
+ * work runs between the decision and the kill; evidence older than this is discarded.
73
+ */
74
+ maxEvidenceAgeMs: number;
75
+ };
76
+
77
+ export const DEFAULT_HEALTH_SENTINEL_TIMING: Readonly<HealthSentinelTiming> = Object.freeze({
78
+ intervalMs: 30_000,
79
+ failureIntervalMs: 1_000,
80
+ probeTimeoutMs: 20_000,
81
+ startupGraceMs: 120_000,
82
+ failureThreshold: 3,
83
+ minUnhealthyMs: 60_000,
84
+ suspendSlackMs: 5_000,
85
+ lagToleranceMs: 2_000,
86
+ recoveryWindowMs: 60 * 60_000,
87
+ maxRecoveriesPerWindow: 3,
88
+ maxBackoffMultiplier: 8,
89
+ maxEvidenceAgeMs: 5_000,
90
+ });
91
+
92
+ export type HealthSentinelDecision =
93
+ | { action: "continue"; note?: string }
94
+ | { action: "recover"; reason: string; consecutiveTimeouts: number; unhealthyMs: number }
95
+ /** Stop acting for this target; the reason is logged once. */
96
+ | { action: "stand_down"; reason: string };
97
+
98
+ export type HealthSentinelPolicyState = {
99
+ readonly armed: boolean;
100
+ readonly consecutiveTimeouts: number;
101
+ readonly firstFailureStartedAt: number | null;
102
+ readonly lastObservedAt: number | null;
103
+ readonly stoodDown: string | null;
104
+ };
105
+
106
+ export type HealthSentinelPolicy = {
107
+ observe(outcome: HealthProbeOutcome): HealthSentinelDecision;
108
+ state(): HealthSentinelPolicyState;
109
+ /** Minimum unhealthy span currently required, after backoff. */
110
+ requiredUnhealthyMs(): number;
111
+ /** How long to wait before the next probe: short while a failure streak is open. */
112
+ nextDelayMs(): number;
113
+ };
114
+
115
+ /**
116
+ * Recoveries recorded inside the window ending at `now`. An entry dated after `now` counts as
117
+ * recent: it means the wall clock was set back after it was written, and dropping it would let a
118
+ * clock rollback (or a restart after one) lift the hourly cap. It keeps counting until the clock
119
+ * passes it by a full window, which errs toward not killing.
120
+ */
121
+ export function recentRecoveryCount(history: readonly number[], now: number, windowMs: number): number {
122
+ return history.filter(at => Number.isFinite(at) && now - at < windowMs).length;
123
+ }
124
+
125
+ /** The unhealthy span required after `recent` recoveries: doubled per recovery, capped. */
126
+ export function requiredUnhealthySpan(timing: HealthSentinelTiming, recent: number): number {
127
+ const multiplier = Math.min(timing.maxBackoffMultiplier, 2 ** Math.max(0, recent));
128
+ return timing.minUnhealthyMs * multiplier;
129
+ }
130
+
131
+ export function createHealthSentinelPolicy(input: {
132
+ targetPid: number;
133
+ startedAt: number;
134
+ /** Epoch-ms of earlier recoveries for this installation, newest last. */
135
+ recoveryHistory: readonly number[];
136
+ timing?: Partial<HealthSentinelTiming>;
137
+ }): HealthSentinelPolicy {
138
+ const timing: HealthSentinelTiming = { ...DEFAULT_HEALTH_SENTINEL_TIMING, ...input.timing };
139
+ let armed = false;
140
+ let streak = 0;
141
+ let firstFailureStartedAt: number | null = null;
142
+ let lastObservedAt: number | null = null;
143
+ let stoodDown: string | null = null;
144
+ let required = timing.minUnhealthyMs;
145
+ // Startup grace is measured as elapsed time, accumulated from forward clock movement only, so a
146
+ // wall clock set back after (or during) the grace cannot reopen it and hold off recovery for as
147
+ // long as the clock was moved.
148
+ let graceElapsedMs = 0;
149
+ let graceClock = input.startedAt;
150
+ const noteGraceProgress = (at: number) => {
151
+ if (at > graceClock) graceElapsedMs += at - graceClock;
152
+ graceClock = at;
153
+ };
154
+
155
+ const resetStreak = () => {
156
+ streak = 0;
157
+ firstFailureStartedAt = null;
158
+ };
159
+
160
+ const standDown = (reason: string): HealthSentinelDecision => {
161
+ stoodDown = reason;
162
+ resetStreak();
163
+ return { action: "stand_down", reason };
164
+ };
165
+
166
+ const observe = (outcome: HealthProbeOutcome): HealthSentinelDecision => {
167
+ if (stoodDown !== null) return { action: "stand_down", reason: stoodDown };
168
+ noteGraceProgress(outcome.startedAt);
169
+ const previous = lastObservedAt;
170
+ lastObservedAt = outcome.endedAt;
171
+ let note: string | undefined;
172
+ if (previous !== null && streak > 0 && outcome.startedAt - previous > timing.failureIntervalMs + timing.suspendSlackMs) {
173
+ // The sentinel overslept its cadence (a suspended machine, or the sentinel itself starved),
174
+ // so the timeouts counted so far may belong to that pause rather than to the target.
175
+ resetStreak();
176
+ note = "observation gap; failure streak discarded";
177
+ }
178
+ switch (outcome.kind) {
179
+ case "answered": {
180
+ if (outcome.pid !== null && outcome.pid !== input.targetPid) {
181
+ return standDown("another process answers the health endpoint");
182
+ }
183
+ if (outcome.pid === input.targetPid) armed = true;
184
+ resetStreak();
185
+ return { action: "continue", note };
186
+ }
187
+ case "refused":
188
+ case "error":
189
+ resetStreak();
190
+ return { action: "continue", note };
191
+ case "timeout": {
192
+ if (!armed) return { action: "continue", note: note ?? "not armed: the target has not answered yet" };
193
+ if (graceElapsedMs < timing.startupGraceMs) {
194
+ return { action: "continue", note: note ?? "startup grace" };
195
+ }
196
+ if (
197
+ outcome.endedAt - outcome.startedAt > timing.probeTimeoutMs + timing.suspendSlackMs
198
+ || outcome.endedAt < outcome.startedAt
199
+ || (previous !== null && outcome.startedAt < previous)
200
+ ) {
201
+ // The probe outlived its own deadline by far more than scheduling jitter, or the wall
202
+ // clock ran backwards: its timing spanned a pause or a clock step, so this timeout says
203
+ // nothing about the target.
204
+ resetStreak();
205
+ return { action: "continue", note: "probe spanned a pause; failure streak discarded" };
206
+ }
207
+ if ((outcome.sentinelLagMs ?? 0) > timing.lagToleranceMs) {
208
+ // The sentinel itself stalled during the probe (paused, starved, or the machine slept),
209
+ // so the deadline may have expired while nobody was listening for the answer.
210
+ resetStreak();
211
+ return { action: "continue", note: "probe spanned a pause; failure streak discarded" };
212
+ }
213
+ streak += 1;
214
+ if (firstFailureStartedAt === null) firstFailureStartedAt = outcome.startedAt;
215
+ const unhealthyMs = outcome.endedAt - firstFailureStartedAt;
216
+ const recent = recentRecoveryCount(input.recoveryHistory, outcome.endedAt, timing.recoveryWindowMs);
217
+ required = requiredUnhealthySpan(timing, recent);
218
+ if (streak < timing.failureThreshold || unhealthyMs < required) return { action: "continue", note };
219
+ if (recent >= timing.maxRecoveriesPerWindow) {
220
+ return standDown(`${recent} recoveries in the last ${Math.round(timing.recoveryWindowMs / 60_000)} minutes; not recovering again`);
221
+ }
222
+ const decision: HealthSentinelDecision = {
223
+ action: "recover",
224
+ reason: `no health answer for ${streak} consecutive probes over ${Math.round(unhealthyMs / 1000)}s`,
225
+ consecutiveTimeouts: streak,
226
+ unhealthyMs,
227
+ };
228
+ resetStreak();
229
+ return decision;
230
+ }
231
+ }
232
+ };
233
+
234
+ return {
235
+ observe,
236
+ state: () => ({ armed, consecutiveTimeouts: streak, firstFailureStartedAt, lastObservedAt, stoodDown }),
237
+ requiredUnhealthyMs: () => required,
238
+ nextDelayMs: () => (streak > 0 ? timing.failureIntervalMs : timing.intervalMs),
239
+ };
240
+ }
241
+
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The service health sentinel's one question to the proxy it watches: does `GET /healthz`
3
+ * produce a complete answer before the deadline (#6671)?
4
+ *
5
+ * The request goes through {@link directLocalHttpFetch}: raw `node:net`, so neither proxy
6
+ * environment variables nor the bundled runtime's fetch stack (#6684) are involved, with capped
7
+ * response framing and an absolute deadline. Only the timing decides liveness; the body's `pid`
8
+ * lets the policy tell the target from another process that took the port.
9
+ */
10
+ import { directLocalHttpFetch } from "../server/direct-local-http";
11
+ import type { HealthProbeOutcome } from "./health-sentinel-policy";
12
+
13
+ export type HealthProbeTarget = { hostname: string; port: number };
14
+
15
+ export type HealthProbeIo = {
16
+ now?: () => number;
17
+ fetch?: (url: string, timeoutMs: number) => Promise<Response>;
18
+ };
19
+
20
+ /** The URL host to dial for a recorded listen hostname: wildcard binds answer on loopback. */
21
+ export function probeUrlHost(hostname: string | undefined): string {
22
+ const value = (hostname ?? "").trim().replace(/^\[|\]$/g, "");
23
+ if (!value || value === "0.0.0.0" || value === "localhost") return "127.0.0.1";
24
+ if (value === "::") return "[::1]";
25
+ return value.includes(":") ? `[${value}]` : value;
26
+ }
27
+
28
+ /** `pid` from a `/healthz` JSON body, or null when the body does not carry one. */
29
+ export function healthzPid(body: string): number | null {
30
+ try {
31
+ const parsed = JSON.parse(body) as { pid?: unknown };
32
+ return typeof parsed.pid === "number" && Number.isSafeInteger(parsed.pid) && parsed.pid > 0 ? parsed.pid : null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export async function probeHealth(target: HealthProbeTarget, timeoutMs: number, io: HealthProbeIo = {}): Promise<HealthProbeOutcome> {
39
+ const now = io.now ?? Date.now;
40
+ const fetchFn = io.fetch ?? ((url: string, deadlineMs: number) => directLocalHttpFetch(url, { headers: { accept: "application/json" } }, { timeoutMs: deadlineMs }));
41
+ const startedAt = now();
42
+ try {
43
+ const response = await fetchFn(`http://${probeUrlHost(target.hostname)}:${target.port}/healthz`, timeoutMs);
44
+ // The framed body is already in memory; reading it cannot wait on the target.
45
+ const body = await response.text().catch(() => "");
46
+ return { kind: "answered", pid: healthzPid(body), startedAt, endedAt: now() };
47
+ } catch (error) {
48
+ const endedAt = now();
49
+ const failure = error as { name?: unknown; code?: unknown };
50
+ if (failure?.name === "TimeoutError") return { kind: "timeout", startedAt, endedAt };
51
+ if (failure?.code === "ECONNREFUSED") return { kind: "refused", startedAt, endedAt };
52
+ return { kind: "error", startedAt, endedAt };
53
+ }
54
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * A Windows process handle on the health sentinel's parent, held for the sentinel's lifetime
3
+ * (#6671).
4
+ *
5
+ * Windows reuses process ids quickly, so "is pid N still my parent" checked just before a kill
6
+ * cannot bind the kill to the proxy: the proxy may exit and its id be handed to another process in
7
+ * between. An open handle can: while any handle to a process object is open, its id is not
8
+ * reused, and `TerminateProcess` acts on exactly that object. The handle is opened once at
9
+ * startup and accepted only if the process it names was created before this sentinel, which a
10
+ * replacement holding a recycled id cannot satisfy. libuv's kill-on-close Job would otherwise be
11
+ * the only tie between the two processes, and libuv ignores a failed Job assignment.
12
+ *
13
+ * Bun exposes no API for a handle on a process it did not spawn, so this calls kernel32 through
14
+ * `bun:ffi`. Nothing here runs on other platforms.
15
+ */
16
+ import { dlopen, FFIType } from "bun:ffi";
17
+
18
+ const PROCESS_TERMINATE = 0x0001;
19
+ const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
20
+ const SYNCHRONIZE = 0x0010_0000;
21
+ const WAIT_OBJECT_0 = 0;
22
+ const WAIT_TIMEOUT = 0x102;
23
+
24
+ export type ParentProcessHandle = {
25
+ /** TerminateProcess on the held handle; true when Windows accepted the request. */
26
+ terminate(exitCode: number): boolean;
27
+ /** True once the process has exited (non-blocking). */
28
+ exited(): boolean;
29
+ close(): void;
30
+ };
31
+
32
+ export type ParentHandleResult =
33
+ | { ok: true; handle: ParentProcessHandle }
34
+ | { ok: false; reason: string };
35
+
36
+ // HANDLEs travel as u64 (bigint), never as `ptr`: a JS number cannot carry every pointer-width
37
+ // value, and a truncated handle would name a different object.
38
+ function kernel32() {
39
+ return dlopen("kernel32.dll", {
40
+ OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.u64 },
41
+ GetProcessTimes: { args: [FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
42
+ TerminateProcess: { args: [FFIType.u64, FFIType.u32], returns: FFIType.i32 },
43
+ WaitForSingleObject: { args: [FFIType.u64, FFIType.u32], returns: FFIType.u32 },
44
+ CloseHandle: { args: [FFIType.u64], returns: FFIType.i32 },
45
+ }).symbols;
46
+ }
47
+
48
+ /** Open the parent and prove it is the process that created this one. */
49
+ export function openParentProcessHandle(parentPid: number): ParentHandleResult {
50
+ if (process.platform !== "win32") return { ok: false, reason: "not Windows" };
51
+ let k32: ReturnType<typeof kernel32>;
52
+ try {
53
+ k32 = kernel32();
54
+ } catch {
55
+ return { ok: false, reason: "kernel32 is unavailable" };
56
+ }
57
+ const handle = BigInt(k32.OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, parentPid));
58
+ if (handle === 0n) return { ok: false, reason: "the parent process cannot be opened" };
59
+ const close = () => { try { k32.CloseHandle(handle); } catch { /* already closed */ } };
60
+ const created = (target: bigint): bigint | null => {
61
+ const times = new BigUint64Array(4);
62
+ const ok = k32.GetProcessTimes(target, times.subarray(0, 1), times.subarray(1, 2), times.subarray(2, 3), times.subarray(3, 4));
63
+ return ok ? times[0]! : null;
64
+ };
65
+ // This process cannot be reused while it runs, so opening it by id is exact; the pseudo-handle
66
+ // from GetCurrentProcess is avoided because it is -1.
67
+ const self = BigInt(k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process.pid));
68
+ const parentCreated = created(handle);
69
+ const selfCreated = self === 0n ? null : created(self);
70
+ if (self !== 0n) { try { k32.CloseHandle(self); } catch { /* already closed */ } }
71
+ if (parentCreated === null || selfCreated === null || parentCreated === 0n || parentCreated >= selfCreated) {
72
+ close();
73
+ return { ok: false, reason: "the process holding the parent id was not created before this sentinel" };
74
+ }
75
+ if (k32.WaitForSingleObject(handle, 0) !== WAIT_TIMEOUT) {
76
+ close();
77
+ return { ok: false, reason: "the parent process has already exited" };
78
+ }
79
+ return {
80
+ ok: true,
81
+ handle: {
82
+ terminate: exitCode => k32.TerminateProcess(handle, exitCode) !== 0,
83
+ exited: () => k32.WaitForSingleObject(handle, 0) === WAIT_OBJECT_0,
84
+ close,
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,20 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { userInfo } from "node:os";
3
+
4
+ interface LinuxLingerDeps {
5
+ user?: () => string;
6
+ run?: typeof execFileSync;
7
+ }
8
+
9
+ /** Advisory only: never enables linger or changes the user's login settings. */
10
+ export function linuxServiceBootHint(deps: LinuxLingerDeps = {}): string {
11
+ try {
12
+ const user = (deps.user ?? (() => userInfo().username))();
13
+ const output = (deps.run ?? execFileSync)("loginctl", ["show-user", user, "-p", "Linger"], {
14
+ encoding: "utf8", timeout: 750, maxBuffer: 16 * 1024, stdio: ["ignore", "pipe", "pipe"],
15
+ }).trim();
16
+ if (output === "Linger=yes") return " Auto-start on boot: user lingering is already enabled.";
17
+ if (output === "Linger=no") return " For auto-start on boot: loginctl enable-linger $USER";
18
+ } catch { /* Missing loginctl or failed diagnosis is advisory, not an install failure. */ }
19
+ return " User lingering could not be verified. For auto-start on boot, check it and, if disabled, run: loginctl enable-linger $USER";
20
+ }
@@ -519,6 +519,11 @@ export interface OcxConfig {
519
519
  skills?: OcxSkillsConfig;
520
520
  /** Opt-in process-local aggregate request metrics on the authenticated management plane. */
521
521
  metricsExport?: { enabled?: boolean };
522
+ /**
523
+ * Opt-in recovery of a managed service proxy that stays alive but stops answering /healthz:
524
+ * a separate sentinel process terminates it so its supervisor restarts it (#6671).
525
+ */
526
+ serviceHealthRecovery?: { enabled?: boolean };
522
527
  /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
523
528
  emptyCompletionRetry?: boolean;
524
529
  /** Suppress allowlisted client-facing Codex transport hints; provider enforcement is unchanged. */
@@ -211,6 +211,8 @@ const META_MODEL_PRICING = "https://dev.meta.ai/docs/pricing-rate-limits";
211
211
  */
212
212
  const META_MUSE_SPARK_13: Cost4 = { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 };
213
213
  const META_MUSE_SPARK_13_CONTRIBUTOR: Cost4 = { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 };
214
+ // Muse Wonder (muse-wonder-1.0): Standard mode only, no long-context premium (verified 2026-10-13).
215
+ const META_MUSE_WONDER_10: Cost4 = { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 0 };
214
216
  const META_SPARK_SOURCE = `Meta Model API published price ${META_MODEL_PRICING}`;
215
217
  const META_SPARK_CONTRIBUTOR_SOURCE = `Meta Model API published Contributor-tier price ${META_MODEL_PRICING}; data-sharing discount tier`;
216
218
  const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after";
@@ -369,11 +371,13 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
369
371
  // cacheWrite=0: Meta publishes a cached-input price but no cache-write charge.
370
372
  { provider: "meta-model", modelId: "muse-spark-1.3", cost4: META_MUSE_SPARK_13, source: META_SPARK_SOURCE, verifiedAt: "2026-09-03", status: "verified" },
371
373
  { provider: "meta-model", modelId: "muse-spark-1.3-contributor", cost4: META_MUSE_SPARK_13_CONTRIBUTOR, source: META_SPARK_CONTRIBUTOR_SOURCE, verifiedAt: "2026-09-03", status: "verified" },
374
+ { provider: "meta-model", modelId: "muse-wonder-1.0", cost4: META_MUSE_WONDER_10, source: META_SPARK_SOURCE, verifiedAt: "2026-10-13", status: "verified" },
372
375
  // Same endpoint, same list price, different credential. Meta does not authorize this
373
376
  // reuse and settlement is not observable, so these are the public Model API rates as a
374
377
  // conservative estimate — not evidence of how the call is actually billed.
375
378
  { provider: "meta-muse", modelId: "muse-spark-1.3", cost4: META_MUSE_SPARK_13, source: META_SPARK_SOURCE, verifiedAt: "2026-09-03", status: "verified-derived" },
376
379
  { provider: "meta-muse", modelId: "muse-spark-1.3-contributor", cost4: META_MUSE_SPARK_13_CONTRIBUTOR, source: META_SPARK_CONTRIBUTOR_SOURCE, verifiedAt: "2026-09-03", status: "verified-derived" },
380
+ { provider: "meta-muse", modelId: "muse-wonder-1.0", cost4: META_MUSE_WONDER_10, source: META_SPARK_SOURCE, verifiedAt: "2026-10-13", status: "verified-derived" },
377
381
  // Daybreak aliases: priced as their current snapshots (red -> gpt-5.6-cyber,
378
382
  // blue -> gpt-5.6-sol). The alias ids carry no rows of their own upstream, hence
379
383
  // verified-derived. Blue deliberately reuses GPT56_SOL rather than duplicating the tuple.
package/src/usage/log.ts CHANGED
@@ -23,7 +23,7 @@ import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routi
23
23
  import { parseProtocolTraceV1, type ProtocolTraceV1 } from "../protocols/dto";
24
24
  import { ACCOUNT_LOG_LABEL_RE, CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
25
25
  import { claudeCompatibilityReason, normalizeClaudeFeatureCodes, type ClaudeFeatureCode } from "../claude/compatibility";
26
- import type { CodexWsStageRecord } from "../server/responses/codex-ws-wire";
26
+ import type { CodexWsStageRecord, CodexWsTerminalEvent } from "../server/responses/codex-ws-wire";
27
27
  import {
28
28
  ATTEMPT_RECOVERY_KIND_ROSTER,
29
29
  ATTEMPT_RECOVERY_WITHHELD_ROSTER,
@@ -805,6 +805,15 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
805
805
  * injected free-form field — drops the whole record rather than passing
806
806
  * attacker text into the DTO.
807
807
  */
808
+ // Total over the terminal union by `satisfies`; a type-only import keeps the WS transport out of
809
+ // this module's runtime graph.
810
+ const CODEX_WS_TERMINAL_EVENT_NAMES = {
811
+ "response.completed": true,
812
+ "response.failed": true,
813
+ "response.incomplete": true,
814
+ error: true,
815
+ } as const satisfies Record<CodexWsTerminalEvent, true>;
816
+
808
817
  function normalizeCodexWsStageRecord(value: unknown): CodexWsStageRecord | undefined {
809
818
  if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
810
819
  const stage = value as Record<string, unknown>;
@@ -839,6 +848,15 @@ function normalizeCodexWsStageRecord(value: unknown): CodexWsStageRecord | undef
839
848
  reused: stage.reused,
840
849
  ocxVersion: stage.ocxVersion,
841
850
  bunVersion: stage.bunVersion,
851
+ // #6740 fields are newer than the record: absent or malformed values read as unobserved
852
+ // rather than costing the row the rest of its stage evidence.
853
+ terminalEvent: typeof stage.terminalEvent === "string" && Object.hasOwn(CODEX_WS_TERMINAL_EVENT_NAMES, stage.terminalEvent)
854
+ ? stage.terminalEvent as CodexWsTerminalEvent
855
+ : null,
856
+ terminalErrorStatus: typeof stage.terminalErrorStatus === "number" && Number.isInteger(stage.terminalErrorStatus)
857
+ && stage.terminalErrorStatus >= 100 && stage.terminalErrorStatus <= 599
858
+ ? stage.terminalErrorStatus
859
+ : null,
842
860
  };
843
861
  }
844
862