@bitkyc08/opencodex 2.69.0 → 2.70.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.
- package/gui/dist/assets/App-_MrbTrBe.js +51 -0
- package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
- package/gui/dist/assets/index-DKrO2eF3.js +86 -0
- package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-model-contract.ts +119 -0
- package/src/adapters/anthropic.ts +14 -83
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +542 -0
- package/src/adapters/command-code-tool-text.ts +99 -67
- package/src/adapters/command-code.ts +5 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
- package/src/adapters/devin/cloud-direct/chat.ts +78 -48
- package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
- package/src/adapters/devin/context-overflow.ts +75 -0
- package/src/adapters/devin/live-models.ts +174 -13
- package/src/adapters/devin/reasoning-signature.ts +100 -0
- package/src/adapters/devin.ts +310 -117
- package/src/adapters/kiro/payload.ts +10 -10
- package/src/adapters/kiro-images.ts +58 -7
- package/src/adapters/openai-chat/passthrough.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/bridge/response-json.ts +15 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/cli/account-extended.ts +31 -1
- package/src/cli/account.ts +3 -0
- package/src/cli/capabilities.ts +13 -0
- package/src/cli/client-start-fence.ts +36 -0
- package/src/cli/dispatch.ts +3 -2
- package/src/cli/index.ts +98 -117
- package/src/cli/provider-runtime.ts +23 -2
- package/src/cli/provider.ts +3 -0
- package/src/cli/resolve.ts +18 -0
- package/src/cli/start-owner-fence.ts +30 -0
- package/src/cli/stop-approval.ts +13 -1
- package/src/cli/stop-restore.ts +73 -0
- package/src/cli/tray-proxy.ts +221 -18
- package/src/cli/version-skew.ts +25 -23
- package/src/client/runtime.ts +8 -4
- package/src/codex/auth-api/account-list.ts +11 -5
- package/src/codex/auth-api/login-flow.ts +5 -3
- package/src/codex/auth-api/main-account-probe.ts +186 -110
- package/src/codex/auth-api/pool-mode-gate.ts +61 -7
- package/src/codex/auth-api/pool-quota-probe.ts +142 -90
- package/src/codex/auth-api/reset-credit-service.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +3 -3
- package/src/codex/catalog/provider-models.ts +2 -0
- package/src/codex/low-quota-events.ts +28 -0
- package/src/codex/low-quota-observer.ts +23 -0
- package/src/codex/low-quota-protection.ts +272 -0
- package/src/codex/quota-query-backoff.ts +148 -0
- package/src/codex/quota.ts +7 -2
- package/src/config/diagnostics.ts +10 -1
- package/src/config/load-degrade.ts +17 -0
- package/src/config/mutation-lock.ts +3 -4
- package/src/config/process-state.ts +1 -1
- package/src/config/provider-validation.ts +22 -0
- package/src/config/schema/blocked-model-redirects.ts +17 -0
- package/src/config/schema/config-schema.ts +4 -1
- package/src/config/schema/leaf-validators.ts +33 -2
- package/src/config/serving-runtimes.ts +412 -0
- package/src/generated/compatibility-version.json +211 -115
- package/src/generated/model-metadata.ts +3 -3
- package/src/images/loop.ts +3 -0
- package/src/lib/errors.ts +2 -1
- package/src/lib/handled-signal-exit.ts +23 -0
- package/src/lib/request-failure-model.ts +1 -0
- package/src/lib/self-launch-argv.ts +9 -0
- package/src/lib/shadow-call.ts +5 -2
- package/src/lib/strict-semver.ts +26 -0
- package/src/lib/transient-service-path.ts +16 -0
- package/src/lib/windows-service-wrappers.ts +10 -6
- package/src/lib/winsw.ts +3 -1
- package/src/oauth/anthropic-model-routes.ts +105 -0
- package/src/oauth/anthropic-routing.ts +85 -48
- package/src/oauth/devin.ts +135 -5
- package/src/oauth/index.ts +20 -3
- package/src/oauth/kiro-terminal-failover.ts +23 -9
- package/src/oauth/pool-settings-capability.ts +10 -2
- package/src/oauth/store.ts +1 -1
- package/src/protocols/encoders/adapter-events.ts +3 -1
- package/src/providers/github-copilot-context.ts +27 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/quota/account-cache.ts +8 -3
- package/src/providers/quota/devin.ts +286 -0
- package/src/providers/quota/vendor-probes-oauth.ts +6 -2
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +1 -1
- package/src/providers/registry/model-seeds.ts +5 -2
- package/src/responses/compaction-images.ts +27 -0
- package/src/router.ts +104 -16
- package/src/routing/analytics.ts +1 -0
- package/src/server/auth-cors.ts +9 -1
- package/src/server/background-lifecycle.ts +21 -10
- package/src/server/chat-completions.ts +2 -1
- package/src/server/chat-native.ts +1 -0
- package/src/server/index.ts +4 -8
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/management/context.ts +3 -0
- package/src/server/management/low-quota-routes.ts +14 -0
- package/src/server/management/oauth-account-routes.ts +21 -1
- package/src/server/management/provider-overwrite-carry.ts +79 -6
- package/src/server/management/provider-routes.ts +41 -28
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management-api.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +3 -0
- package/src/server/responses/adapter-delivery.ts +3 -0
- package/src/server/responses/adapter-dispatch.ts +20 -4
- package/src/server/responses/antigravity-validation-refusal.ts +20 -0
- package/src/server/responses/core-auth.ts +1 -0
- package/src/server/responses/core-codex-account.ts +1 -0
- package/src/server/responses/core-normalize.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +8 -1
- package/src/server/responses/request-sidecar-auth.ts +2 -0
- package/src/server/responses/request-transport.ts +27 -10
- package/src/server/responses/response-effects.ts +4 -1
- package/src/server/responses/run-turn-execution.ts +126 -42
- package/src/server/responses/sidecar-execution.ts +4 -1
- package/src/service/diagnostics.ts +3 -1
- package/src/service/guarded-manager-target.ts +39 -12
- package/src/service/launchd.ts +14 -6
- package/src/service/repair.ts +6 -1
- package/src/service/service-child-ownership.ts +168 -0
- package/src/service/state.ts +22 -4
- package/src/service/systemd.ts +2 -2
- package/src/service/windows-guarded-manager.ts +530 -0
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service.ts +2 -0
- package/src/types/config.ts +26 -0
- package/src/types/provider.ts +12 -0
- package/src/types/request.ts +2 -0
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +13 -0
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/vision/anthropic-describe.ts +2 -1
- package/src/web-search/anthropic-executor.ts +2 -1
- package/src/web-search/loop.ts +1 -0
- package/gui/dist/assets/App-DDbgOTh6.js +0 -51
- package/gui/dist/assets/index-CuaBJkG4.js +0 -86
- package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
package/src/cli/tray-proxy.ts
CHANGED
|
@@ -40,6 +40,25 @@ export type ProxyRestartDiscovery =
|
|
|
40
40
|
| { status: "absent" }
|
|
41
41
|
| { status: "uncertain"; error?: unknown };
|
|
42
42
|
|
|
43
|
+
export type ProxyRestartStartOutcome =
|
|
44
|
+
| { status: "started" }
|
|
45
|
+
| { status: "skipped" }
|
|
46
|
+
| { status: "failed"; launch: "never" | "exited" | "unknown"; error?: unknown };
|
|
47
|
+
|
|
48
|
+
/** Only the launched child's observed exit (or no launch) permits another attempt. */
|
|
49
|
+
export function restartStartOutcome(
|
|
50
|
+
started: boolean,
|
|
51
|
+
child: { exitCode: number | null; signalCode: string | null } | null,
|
|
52
|
+
error?: unknown,
|
|
53
|
+
): ProxyRestartStartOutcome {
|
|
54
|
+
if (started) return { status: "started" };
|
|
55
|
+
return {
|
|
56
|
+
status: "failed",
|
|
57
|
+
launch: child === null ? "never" : child.exitCode !== null || child.signalCode !== null ? "exited" : "unknown",
|
|
58
|
+
error,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
export interface ProxyRestartDiscoveryIo {
|
|
44
63
|
findLive: () => Promise<ProxyRestartLive | null>;
|
|
45
64
|
waitBetweenChecks?: () => Promise<void>;
|
|
@@ -48,11 +67,81 @@ export interface ProxyRestartDiscoveryIo {
|
|
|
48
67
|
|
|
49
68
|
export interface ProxyRestartIo {
|
|
50
69
|
findLive: () => Promise<ProxyRestartDiscovery>;
|
|
51
|
-
startWhenStopped: () =>
|
|
70
|
+
startWhenStopped: (recoveringLiveRestart: boolean) => ProxyRestartStartOutcome | Promise<ProxyRestartStartOutcome>;
|
|
52
71
|
requestInPlaceRestart: (
|
|
53
72
|
previous: ProxyRestartLive,
|
|
54
73
|
) => ProxyRestartRequestOutcome | Promise<ProxyRestartRequestOutcome>;
|
|
55
74
|
waitForReplacement: (previous: ProxyRestartLive) => Promise<ProxyRestartLive | null>;
|
|
75
|
+
/** Pause between start attempts; defaults to a short sleep. Tests pass a recorder. */
|
|
76
|
+
waitBetweenAttempts?: () => Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Strong re-observation after a missed replacement. Defaults to `findLive`, but the
|
|
79
|
+
* production wiring passes a fresh bounded window: by the time the replacement wait
|
|
80
|
+
* expires, the shared observe deadline has expired too, so reusing it would answer
|
|
81
|
+
* `uncertain` forever and the crash-recovery path could never run.
|
|
82
|
+
*/
|
|
83
|
+
reobserveAfterReplacement?: (previous: ProxyRestartLive) => Promise<ProxyRestartDiscovery>;
|
|
84
|
+
/** A failed recovery start must recheck under a fresh window after the shared deadline. */
|
|
85
|
+
recheckAfterFailedStart?: () => Promise<ProxyRestartDiscovery>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Poll for a departed predecessor inside a bounded confirmation window. Each round is
|
|
90
|
+
* already a strong observation (the caller supplies it); this only decides whether
|
|
91
|
+
* another round still fits: absent and valid-replacement verdicts return immediately,
|
|
92
|
+
* while the still-live old PID (or another uncertain round) keeps polling until the
|
|
93
|
+
* budget check refuses. A replacement that lands late in the window is still attested
|
|
94
|
+
* instead of being cut off by the wait that reserved the window.
|
|
95
|
+
*/
|
|
96
|
+
export async function pollReplacementDeparture(
|
|
97
|
+
observe: () => Promise<ProxyRestartDiscovery>,
|
|
98
|
+
previous: ProxyRestartLive,
|
|
99
|
+
shouldContinue: () => boolean,
|
|
100
|
+
wait: () => Promise<void>,
|
|
101
|
+
): Promise<ProxyRestartDiscovery> {
|
|
102
|
+
for (;;) {
|
|
103
|
+
const round = await observe();
|
|
104
|
+
if (round.status === "absent") return round;
|
|
105
|
+
if (round.status === "live" && isProxyReplacement(previous, round.live)) return round;
|
|
106
|
+
if (!shouldContinue()) return round;
|
|
107
|
+
await wait();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function waitForProxyReplacement(
|
|
112
|
+
previous: ProxyRestartLive,
|
|
113
|
+
deadlineAt: number,
|
|
114
|
+
observe: (deadlineAt: number) => Promise<ProxyRestartLive | null>,
|
|
115
|
+
): Promise<ProxyRestartLive | null> {
|
|
116
|
+
while (Date.now() < deadlineAt) {
|
|
117
|
+
const live = await observe(deadlineAt);
|
|
118
|
+
if (Date.now() >= deadlineAt) return null;
|
|
119
|
+
if (isProxyReplacement(previous, live)) return live;
|
|
120
|
+
const remainingMs = deadlineAt - Date.now();
|
|
121
|
+
if (remainingMs > 0) await Bun.sleep(Math.min(250, remainingMs));
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function reobserveRestartReplacement(
|
|
127
|
+
previous: ProxyRestartLive,
|
|
128
|
+
deadlineAt: number,
|
|
129
|
+
observe: (end: number) => Promise<ProxyRestartDiscovery>,
|
|
130
|
+
): Promise<ProxyRestartDiscovery> {
|
|
131
|
+
const windowMs = Math.min(5_000, deadlineAt - Date.now());
|
|
132
|
+
if (windowMs < 1_500) return Promise.resolve({
|
|
133
|
+
status: "uncertain", error: new Error("restart_reobserve_window_exhausted"),
|
|
134
|
+
});
|
|
135
|
+
const end = Date.now() + windowMs;
|
|
136
|
+
return pollReplacementDeparture(() => observe(end), previous, () => Date.now() + 750 < end, () => Bun.sleep(250));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Production recovery recheck: the original restart deadline may already be spent. */
|
|
140
|
+
export function recheckRestartFailedStart(
|
|
141
|
+
observe: (end: number) => Promise<ProxyRestartDiscovery>,
|
|
142
|
+
): Promise<ProxyRestartDiscovery> {
|
|
143
|
+
const end = Date.now() + 5_000;
|
|
144
|
+
return observe(end);
|
|
56
145
|
}
|
|
57
146
|
|
|
58
147
|
/**
|
|
@@ -145,13 +234,110 @@ export async function runTrayProxyStart(io: TrayProxyStartIo): Promise<boolean>
|
|
|
145
234
|
* child and lets ordinary /api/stop restore native routing between the two halves.
|
|
146
235
|
* When no proxy is live there is nothing to recycle, so restart degrades to the
|
|
147
236
|
* caller's normal start path.
|
|
237
|
+
*
|
|
238
|
+
* An uncertain discovery round is re-observed. A failed start is retried only after
|
|
239
|
+
* proof that it never launched or its child exited; a health miss is insufficient.
|
|
240
|
+
* A missed replacement gets a bounded re-observation. A live target is never stopped
|
|
241
|
+
* to make room, and absence is confirmed twice before any fresh start.
|
|
242
|
+
*/
|
|
243
|
+
/**
|
|
244
|
+
* A proven pre-launch refusal or observed child exit can be retried within this bound.
|
|
245
|
+
* A deliberate operator refusal (`"skipped"`) never retries.
|
|
148
246
|
*/
|
|
247
|
+
const RESTART_START_ATTEMPTS = 3;
|
|
248
|
+
|
|
249
|
+
/** Discovery rounds per restart transaction; an uncertain round is a transient race. */
|
|
250
|
+
const RESTART_DISCOVERY_ATTEMPTS = 3;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Start leg with phase evidence. A bare boolean cannot tell a pre-launch refusal from
|
|
254
|
+
* a child that was launched but is not healthy yet, or from post-health steps that
|
|
255
|
+
* threw on an already-live proxy — and blindly respawning on any of those risks a
|
|
256
|
+
* second proxy racing the first for the port. So every failed attempt is followed by a
|
|
257
|
+
* beat and a strong re-observation: a live proxy attests success (no second start),
|
|
258
|
+
* and only confirmed absence plus launch proof earns another attempt.
|
|
259
|
+
*/
|
|
260
|
+
async function startRestartedProxy(
|
|
261
|
+
io: ProxyRestartIo,
|
|
262
|
+
waitBetweenAttempts: () => Promise<void>,
|
|
263
|
+
recoveringLiveRestart: boolean,
|
|
264
|
+
/** The restarted proxy's identity; recovery success must be a different process. */
|
|
265
|
+
previous?: ProxyRestartLive,
|
|
266
|
+
): Promise<ProxyRestartResult> {
|
|
267
|
+
let originalError: unknown;
|
|
268
|
+
for (let attempt = 0; attempt < RESTART_START_ATTEMPTS; attempt++) {
|
|
269
|
+
let outcome: ProxyRestartStartOutcome;
|
|
270
|
+
try {
|
|
271
|
+
outcome = await io.startWhenStopped(recoveringLiveRestart);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// An unstructured exception can have happened after launch.
|
|
274
|
+
outcome = { status: "failed", launch: "unknown", error };
|
|
275
|
+
}
|
|
276
|
+
if (outcome.status === "skipped") return recoveringLiveRestart
|
|
277
|
+
? { ok: false, phase: "replacement" } : { ok: true, mode: "skipped" };
|
|
278
|
+
if (outcome.status === "started") {
|
|
279
|
+
if (!previous) return { ok: true, mode: "started" };
|
|
280
|
+
// Recovery after a live restart: the start path counts any healthy proxy it finds as
|
|
281
|
+
// started, and the ORIGINAL process reappearing would satisfy it. Success needs an
|
|
282
|
+
// identity-verified replacement — a different runtime PID on the original port —
|
|
283
|
+
// observed in a fresh window; anything else fails closed as a missed replacement.
|
|
284
|
+
let confirm: ProxyRestartDiscovery;
|
|
285
|
+
try {
|
|
286
|
+
confirm = await (io.recheckAfterFailedStart?.() ?? io.findLive());
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return { ok: false, phase: "replacement", error };
|
|
289
|
+
}
|
|
290
|
+
return confirm.status === "live" && isProxyReplacement(previous, confirm.live)
|
|
291
|
+
? { ok: true, mode: "started" }
|
|
292
|
+
: { ok: false, phase: "replacement" };
|
|
293
|
+
}
|
|
294
|
+
if (attempt === 0) originalError = outcome.error;
|
|
295
|
+
// Beat first: a child that was just launched may still be binding, and post-health
|
|
296
|
+
// steps may have thrown on an already-live proxy. Re-observe before deciding: live
|
|
297
|
+
// attests success (never a second start), only confirmed absence earns a retry.
|
|
298
|
+
await waitBetweenAttempts();
|
|
299
|
+
let recheck: ProxyRestartDiscovery;
|
|
300
|
+
try {
|
|
301
|
+
recheck = await (io.recheckAfterFailedStart?.() ?? io.findLive());
|
|
302
|
+
} catch {
|
|
303
|
+
return { ok: false, phase: "start", error: originalError };
|
|
304
|
+
}
|
|
305
|
+
// Runtime-attested only: a config-sourced observation is not proof a proxy serves,
|
|
306
|
+
// so it keeps the start failure instead of reporting a success nobody earned.
|
|
307
|
+
// A throw on an attested-live proxy is different from a clean refusal: post-health
|
|
308
|
+
// work failed on a serving process, so the error propagates instead of converting
|
|
309
|
+
// to success — still without spawning again.
|
|
310
|
+
const attested = recheck.status === "live"
|
|
311
|
+
&& recheck.live.pid !== null
|
|
312
|
+
&& recheck.live.source === "runtime";
|
|
313
|
+
// Recovery after a live restart: the old PID coming back is not a replacement this
|
|
314
|
+
// command started. It is serving, so never start another over it, and never call it
|
|
315
|
+
// success either.
|
|
316
|
+
if (attested && previous && recheck.status === "live" && !isProxyReplacement(previous, recheck.live)) {
|
|
317
|
+
return { ok: false, phase: "replacement" };
|
|
318
|
+
}
|
|
319
|
+
if (attested && outcome.error !== undefined) return { ok: false, phase: "start", error: originalError };
|
|
320
|
+
if (attested) return { ok: true, mode: "started" };
|
|
321
|
+
if (outcome.launch === "unknown" || recheck.status !== "absent") {
|
|
322
|
+
return { ok: false, phase: "start", error: originalError };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { ok: false, phase: "start", error: originalError };
|
|
326
|
+
}
|
|
327
|
+
|
|
149
328
|
export async function runProxyRestart(io: ProxyRestartIo): Promise<ProxyRestartResult> {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
329
|
+
const waitBetweenAttempts = io.waitBetweenAttempts ?? (() => Bun.sleep(500));
|
|
330
|
+
// An uncertain round is a transient appear/vanish race, not a verdict: re-observe a
|
|
331
|
+
// few times before failing, so one invocation survives a supervisor mid-handoff.
|
|
332
|
+
let discovery: ProxyRestartDiscovery = { status: "uncertain", error: new Error("restart_discovery_no_attempt") };
|
|
333
|
+
for (let attempt = 0; attempt < RESTART_DISCOVERY_ATTEMPTS; attempt++) {
|
|
334
|
+
if (attempt > 0) await waitBetweenAttempts();
|
|
335
|
+
try {
|
|
336
|
+
discovery = await io.findLive();
|
|
337
|
+
} catch (error) {
|
|
338
|
+
discovery = { status: "uncertain", error };
|
|
339
|
+
}
|
|
340
|
+
if (discovery.status !== "uncertain") break;
|
|
155
341
|
}
|
|
156
342
|
|
|
157
343
|
if (discovery.status === "uncertain") {
|
|
@@ -159,13 +345,7 @@ export async function runProxyRestart(io: ProxyRestartIo): Promise<ProxyRestartR
|
|
|
159
345
|
}
|
|
160
346
|
|
|
161
347
|
if (discovery.status === "absent") {
|
|
162
|
-
|
|
163
|
-
const started = await io.startWhenStopped();
|
|
164
|
-
if (started === "skipped") return { ok: true, mode: "skipped" };
|
|
165
|
-
return started ? { ok: true, mode: "started" } : { ok: false, phase: "start" };
|
|
166
|
-
} catch (error) {
|
|
167
|
-
return { ok: false, phase: "start", error };
|
|
168
|
-
}
|
|
348
|
+
return startRestartedProxy(io, waitBetweenAttempts, false);
|
|
169
349
|
}
|
|
170
350
|
|
|
171
351
|
const previous = discovery.live;
|
|
@@ -187,13 +367,36 @@ export async function runProxyRestart(io: ProxyRestartIo): Promise<ProxyRestartR
|
|
|
187
367
|
return { ok: false, phase: "request", error: request.error };
|
|
188
368
|
}
|
|
189
369
|
|
|
370
|
+
let replacement: ProxyRestartLive | null;
|
|
190
371
|
try {
|
|
191
|
-
|
|
192
|
-
if (replacement) return { ok: true, mode: "restarted", live: replacement };
|
|
193
|
-
return request.accepted
|
|
194
|
-
? { ok: false, phase: "replacement" }
|
|
195
|
-
: { ok: false, phase: "request", error: request.error };
|
|
372
|
+
replacement = await io.waitForReplacement(previous);
|
|
196
373
|
} catch (error) {
|
|
197
374
|
return { ok: false, phase: "replacement", error };
|
|
198
375
|
}
|
|
376
|
+
if (replacement) return { ok: true, mode: "restarted", live: replacement };
|
|
377
|
+
// The replacement never arrived: re-observe once instead of failing blind. A proxy
|
|
378
|
+
// that crashed mid-restart reads absent (safe to start fresh — nothing live can race
|
|
379
|
+
// the bind); a replacement that landed just past the deadline still proves success;
|
|
380
|
+
// the same PID or another uncertain round fails closed exactly as before. A live
|
|
381
|
+
// target is never stopped to make room: the no-stop/start-fallback invariant holds.
|
|
382
|
+
const reobserveHook = io.reobserveAfterReplacement;
|
|
383
|
+
const reobserve = reobserveHook ? () => reobserveHook(previous) : io.findLive;
|
|
384
|
+
let again: ProxyRestartDiscovery;
|
|
385
|
+
try {
|
|
386
|
+
again = await reobserve();
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return { ok: false, phase: "replacement", error };
|
|
389
|
+
}
|
|
390
|
+
if (again.status === "absent") {
|
|
391
|
+
// Only an ACCEPTED restart earns a recovery start. After an uncertain request the old
|
|
392
|
+
// proxy may merely be mid-restart and reappear; starting one here could race it.
|
|
393
|
+
if (!request.accepted) return { ok: false, phase: "request", error: request.error };
|
|
394
|
+
return startRestartedProxy(io, waitBetweenAttempts, true, previous);
|
|
395
|
+
}
|
|
396
|
+
if (again.status === "live" && isProxyReplacement(previous, again.live)) {
|
|
397
|
+
return { ok: true, mode: "restarted", live: again.live };
|
|
398
|
+
}
|
|
399
|
+
return request.accepted
|
|
400
|
+
? { ok: false, phase: "replacement" }
|
|
401
|
+
: { ok: false, phase: "request", error: request.error };
|
|
199
402
|
}
|
package/src/cli/version-skew.ts
CHANGED
|
@@ -9,16 +9,31 @@
|
|
|
9
9
|
* comparison instead of reimplementing it -- two diagnostics disagreeing about whether an
|
|
10
10
|
* install is stale would be worse than neither reporting it.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { compareStrictSemver, parseStrictSemver } from "../lib/strict-semver";
|
|
13
13
|
|
|
14
14
|
/** Placeholder versions that mean "unknown", not "different". */
|
|
15
15
|
const PLACEHOLDERS = new Set(["unknown", "0.0.0"]);
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Which side of the comparison runs newer, so a consumer does not reparse the warning to
|
|
19
|
+
* find the direction. `"match"` is a real, non-placeholder equality; placeholders and a
|
|
20
|
+
* missing proxy version are `"unknown"`, and `"incomparable"` covers versions that
|
|
21
|
+
* differ but do not order under strict semver. The desktop shell reads this field to
|
|
22
|
+
* keep a takeover from silently downgrading a newer runtime.
|
|
23
|
+
*/
|
|
24
|
+
export type VersionRelation =
|
|
25
|
+
| "match"
|
|
26
|
+
| "cli-newer"
|
|
27
|
+
| "proxy-newer"
|
|
28
|
+
| "incomparable"
|
|
29
|
+
| "unknown";
|
|
30
|
+
|
|
17
31
|
export interface VersionSkew {
|
|
18
32
|
readonly cliVersion: string;
|
|
19
33
|
/** Version the live proxy reported, or null when nothing is live or it reported none. */
|
|
20
34
|
readonly proxyVersion: string | null;
|
|
21
35
|
readonly skewed: boolean;
|
|
36
|
+
readonly relation: VersionRelation;
|
|
22
37
|
/** Operator-facing explanation; null when there is nothing to report. */
|
|
23
38
|
readonly warning: string | null;
|
|
24
39
|
}
|
|
@@ -28,25 +43,6 @@ export function isConfirmedVersionMatch(skew: VersionSkew): boolean {
|
|
|
28
43
|
return skew.proxyVersion === skew.cliVersion && !PLACEHOLDERS.has(skew.cliVersion);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
|
-
/** SemVer precedence ignores build metadata; raw equality is handled separately. */
|
|
32
|
-
function compareVersions(cli: StrictSemver, proxy: StrictSemver): number {
|
|
33
|
-
for (let i = 0; i < cli.core.length; i++) {
|
|
34
|
-
if (cli.core[i]! !== proxy.core[i]!) return cli.core[i]! > proxy.core[i]! ? 1 : -1;
|
|
35
|
-
}
|
|
36
|
-
if (cli.prerelease.length === 0) return proxy.prerelease.length === 0 ? 0 : 1;
|
|
37
|
-
if (proxy.prerelease.length === 0) return -1;
|
|
38
|
-
for (let i = 0; i < Math.max(cli.prerelease.length, proxy.prerelease.length); i++) {
|
|
39
|
-
const left = cli.prerelease[i];
|
|
40
|
-
const right = proxy.prerelease[i];
|
|
41
|
-
if (left === right) continue;
|
|
42
|
-
if (left === undefined) return -1;
|
|
43
|
-
if (right === undefined) return 1;
|
|
44
|
-
if (typeof left !== typeof right) return typeof left === "bigint" ? -1 : 1;
|
|
45
|
-
return left > right ? 1 : -1;
|
|
46
|
-
}
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
/**
|
|
51
47
|
* Compare the running CLI against the live proxy.
|
|
52
48
|
*
|
|
@@ -58,12 +54,17 @@ function compareVersions(cli: StrictSemver, proxy: StrictSemver): number {
|
|
|
58
54
|
*/
|
|
59
55
|
export function computeVersionSkew(cliVersion: string, proxyVersion: string | undefined): VersionSkew {
|
|
60
56
|
const proxy = proxyVersion ?? null;
|
|
61
|
-
if (proxy === null || PLACEHOLDERS.has(proxy) || PLACEHOLDERS.has(cliVersion)
|
|
62
|
-
return { cliVersion, proxyVersion: proxy, skewed: false, warning: null };
|
|
57
|
+
if (proxy === null || PLACEHOLDERS.has(proxy) || PLACEHOLDERS.has(cliVersion)) {
|
|
58
|
+
return { cliVersion, proxyVersion: proxy, skewed: false, relation: "unknown", warning: null };
|
|
59
|
+
}
|
|
60
|
+
if (proxy === cliVersion) {
|
|
61
|
+
return { cliVersion, proxyVersion: proxy, skewed: false, relation: "match", warning: null };
|
|
63
62
|
}
|
|
64
63
|
const cliSemver = parseStrictSemver(cliVersion);
|
|
65
64
|
const proxySemver = parseStrictSemver(proxy);
|
|
66
|
-
const order = cliSemver && proxySemver ?
|
|
65
|
+
const order = cliSemver && proxySemver ? compareStrictSemver(cliSemver, proxySemver) : 0;
|
|
66
|
+
const relation: VersionRelation =
|
|
67
|
+
order > 0 ? "cli-newer" : order < 0 ? "proxy-newer" : "incomparable";
|
|
67
68
|
const advice = order > 0
|
|
68
69
|
? "the running proxy is older than this CLI. Restart the proxy using the intended current installation. "
|
|
69
70
|
// `restart`, not `repair`: a version skew leaves the service DEFINITION unchanged, and
|
|
@@ -77,6 +78,7 @@ export function computeVersionSkew(cliVersion: string, proxyVersion: string | un
|
|
|
77
78
|
cliVersion,
|
|
78
79
|
proxyVersion: proxy,
|
|
79
80
|
skewed: true,
|
|
81
|
+
relation,
|
|
80
82
|
warning: `CLI ${cliVersion} does not match the running proxy ${proxy} — ${advice}`,
|
|
81
83
|
};
|
|
82
84
|
}
|
package/src/client/runtime.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { removePid, removeRuntimePort, writePid, writeRuntimePort, type RuntimeP
|
|
|
7
7
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
8
8
|
import { createLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
9
9
|
import { loadServiceTokenFromFile, serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
10
|
+
import { handledSignalExitCode } from "../lib/handled-signal-exit";
|
|
10
11
|
import {
|
|
11
12
|
DESKTOP_RESTART_EXIT_CODE,
|
|
12
13
|
DESKTOP_SUPERVISED_ENV,
|
|
@@ -169,7 +170,8 @@ export async function recycleStandalone(
|
|
|
169
170
|
// operator noticed. Exit 1 is what those configs are watching for, and it is the same
|
|
170
171
|
// policy the dashboard recycle already uses (src/server/management/system-restart.ts).
|
|
171
172
|
//
|
|
172
|
-
// launchd's KeepAlive
|
|
173
|
+
// launchd's KeepAlive is failure-only too (`SuccessfulExit` false), so exit 1 is what
|
|
174
|
+
// relaunches it as well; an exit 0 would leave the job stopped.
|
|
173
175
|
if (process.env.OCX_SERVICE === "1") {
|
|
174
176
|
exit(1);
|
|
175
177
|
return;
|
|
@@ -266,7 +268,7 @@ export async function bindClientListener(
|
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
export async function startClientRuntime(
|
|
269
|
-
options: { port?: number; block?: boolean } = {},
|
|
271
|
+
options: { port?: number; block?: boolean; afterPublish?: () => void } = {},
|
|
270
272
|
io: ClientRuntimeIo = {},
|
|
271
273
|
): Promise<void> {
|
|
272
274
|
const state = readClientConnectionState();
|
|
@@ -310,9 +312,10 @@ export async function startClientRuntime(
|
|
|
310
312
|
installCrashGuards();
|
|
311
313
|
writePid(process.pid);
|
|
312
314
|
writeRuntimePort(clientRuntimeRecord(process.pid, boundPort));
|
|
315
|
+
options.afterPublish?.();
|
|
313
316
|
|
|
314
317
|
let shuttingDown = false;
|
|
315
|
-
const shutdown = () => {
|
|
318
|
+
const shutdown = (signal?: NodeJS.Signals) => {
|
|
316
319
|
if (shuttingDown) return;
|
|
317
320
|
shuttingDown = true;
|
|
318
321
|
void (async () => {
|
|
@@ -328,7 +331,8 @@ export async function startClientRuntime(
|
|
|
328
331
|
console.warn(`[client] listener stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
329
332
|
} finally {
|
|
330
333
|
cleanup();
|
|
331
|
-
|
|
334
|
+
// launchd relaunches only unsuccessful exits; an external signal must still count.
|
|
335
|
+
process.exit(handledSignalExitCode(signal));
|
|
332
336
|
}
|
|
333
337
|
})();
|
|
334
338
|
};
|
|
@@ -12,7 +12,7 @@ import { codexPlanValue, isThirtyDayOnlyCodexPlan } from "../plan";
|
|
|
12
12
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "../account-runtime-state";
|
|
13
13
|
import { getValidMainAccountToken, MainAccountTokenRefreshError, MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
14
14
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
15
|
-
import { captureMainAccountIdentityGeneration, getMainAccountCredentialPresence, isMainAccountIdentityGenerationLive } from "../main-account-cache";
|
|
15
|
+
import { captureMainAccountIdentityGeneration, getMainAccountCredentialPresence, getMainAccountInfoCache, isMainAccountIdentityGenerationLive } from "../main-account-cache";
|
|
16
16
|
import type { CodexQuotaRefreshOutcome } from "../quota-refresh-outcome";
|
|
17
17
|
import { getMainAccountHardLockStatus } from "../main-account-hard-lock";
|
|
18
18
|
import type { MainAccountHardLockStatus } from "../main-account-hard-lock";
|
|
@@ -256,7 +256,8 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
256
256
|
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
257
257
|
// One redaction decision for the whole snapshot, read once from the operator's config (#3859).
|
|
258
258
|
const maskEmails = emailMaskingEnabled(runtimeConfig);
|
|
259
|
-
const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1
|
|
259
|
+
const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1, undefined, false,
|
|
260
|
+
forceRefresh, false, runtimeConfig);
|
|
260
261
|
const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
261
262
|
const cred = getCodexAccountCredential(account.id);
|
|
262
263
|
let quotaResult: PoolQuotaResult;
|
|
@@ -327,7 +328,11 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
327
328
|
});
|
|
328
329
|
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
329
330
|
const mainSnapshotLive = isMainAccountIdentityGenerationLive(fetchedMainGeneration);
|
|
330
|
-
|
|
331
|
+
// An ordinary same-account return can be parsed after its credential was replaced.
|
|
332
|
+
// The card and hard-lock status must describe the same published quota snapshot.
|
|
333
|
+
const mainInfo = mainSnapshotLive
|
|
334
|
+
? mainResult.infoUnpublished ? getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO : mainResult.info
|
|
335
|
+
: EMPTY_MAIN_ACCOUNT_INFO;
|
|
331
336
|
const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
|
|
332
337
|
? mainResult.hasCredential
|
|
333
338
|
: getMainAccountCredentialPresence() ?? false;
|
|
@@ -395,7 +400,7 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
|
|
|
395
400
|
return;
|
|
396
401
|
}
|
|
397
402
|
if (isAccountNeedsReauth(accountId)) return;
|
|
398
|
-
await fetchMainAccountInfoAttempt(true, 1, lease, false, false);
|
|
403
|
+
await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config);
|
|
399
404
|
} finally {
|
|
400
405
|
lease.release();
|
|
401
406
|
}
|
|
@@ -442,7 +447,8 @@ export async function pauseExhaustedCodexAccounts(
|
|
|
442
447
|
failedAccountCount: number;
|
|
443
448
|
}> => {
|
|
444
449
|
if (!mainLease) return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
445
|
-
const mainResult = await fetchMainAccountInfoAttempt(true, 1, mainLease, true
|
|
450
|
+
const mainResult = await fetchMainAccountInfoAttempt(true, 1, mainLease, true,
|
|
451
|
+
true, false, config);
|
|
446
452
|
if (!mainResult.credentialChecked || !mainResult.hasCredential) {
|
|
447
453
|
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 0 };
|
|
448
454
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { withCodexAccountLogLabel } from "../account-label";
|
|
2
2
|
import { getCodexAccountCredential, markCodexAccountValidated, readCodexAccountRecord, saveCodexAccountCredential, CodexCredentialRefreshLockTimeoutError, CodexCredentialRefreshBusyError, CodexCredentialRefreshStaleError } from "../account-store";
|
|
3
|
-
import { clearAccountQuota, isCodexQuotaExhausted, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
|
|
3
|
+
import { clearAccountQuota, isCodexQuotaExhausted, isValidWhamHistoryObservation, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
|
|
4
4
|
import type { StoredAccountQuota, WhamUsageResponse } from "../quota";
|
|
5
5
|
import { ConfigMutationLockError, withConfigMutationLockSync } from "../../config";
|
|
6
6
|
import { appendDefaultCodexAccountNamespace, codexAccountPickerEnabled } from "../account-namespaces";
|
|
@@ -265,6 +265,7 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
|
|
|
265
265
|
let email = cred.email || accountId;
|
|
266
266
|
let plan: string | undefined;
|
|
267
267
|
let quota: Omit<StoredAccountQuota, "updatedAt"> | null = null;
|
|
268
|
+
let policyQuota: Omit<StoredAccountQuota, "updatedAt"> | null = null;
|
|
268
269
|
try {
|
|
269
270
|
const tokens = { access_token: cred.access, account_id: oauthAccountId };
|
|
270
271
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
@@ -276,6 +277,7 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
|
|
|
276
277
|
email = data.email ?? email;
|
|
277
278
|
plan = nonEmptyPlan(data.plan_type) ?? undefined;
|
|
278
279
|
quota = parseUsageQuota(data);
|
|
280
|
+
policyQuota = isValidWhamHistoryObservation(data) ? quota : null;
|
|
279
281
|
}
|
|
280
282
|
} catch { /* wham fetch is non-blocking */ }
|
|
281
283
|
// Reauth must refresh the same ChatGPT identity already bound to this pool slot.
|
|
@@ -390,7 +392,7 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
|
|
|
390
392
|
clearCodexPoolRefreshFailure(accountId);
|
|
391
393
|
if (warmup.validatedAt !== undefined) markCodexAccountValidated(accountId, warmup.validatedAt, generation);
|
|
392
394
|
clearAccountNeedsReauth(accountId);
|
|
393
|
-
if (quota) setAccountQuotaFromParsed(accountId, quota);
|
|
395
|
+
if (quota) setAccountQuotaFromParsed(accountId, quota, undefined, undefined, policyQuota);
|
|
394
396
|
// Keep the pool id stable; refresh display metadata after a successful login/reauth.
|
|
395
397
|
accounts[existingIdx] = withCodexAccountLogLabel({
|
|
396
398
|
...accounts[existingIdx],
|
|
@@ -420,7 +422,7 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
|
|
|
420
422
|
// A new quota row is generation-gated by live account ownership. Reconcile the
|
|
421
423
|
// durable config owner first so a partial prior sweep cannot reject this write.
|
|
422
424
|
if (newAccountPersistence?.status === "committed" && quota) {
|
|
423
|
-
setAccountQuotaFromParsed(accountId, quota);
|
|
425
|
+
setAccountQuotaFromParsed(accountId, quota, undefined, undefined, policyQuota);
|
|
424
426
|
}
|
|
425
427
|
const { catalogRefreshPending } = await convergeAccountNamespaceCatalog(
|
|
426
428
|
latestConfig,
|