@bitkyc08/opencodex 2.81.0 → 2.83.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -48,6 +48,7 @@ import { normalizeLogConversationId } from "../request-log-conversation";
|
|
|
48
48
|
import { rememberResponseState } from "../../responses/state";
|
|
49
49
|
import { trackStreamLifetime } from "../lifecycle";
|
|
50
50
|
import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
|
|
51
|
+
import { createSidecarSendBudget } from "./sidecar-send-budget";
|
|
51
52
|
|
|
52
53
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
53
54
|
export async function executeResponsesSidecars(
|
|
@@ -78,6 +79,7 @@ export async function executeResponsesSidecars(
|
|
|
78
79
|
| "anthropicSessionKey"
|
|
79
80
|
| "commitResolvedOAuthSelection"
|
|
80
81
|
| "resolveSelectionAdapter"
|
|
82
|
+
| "resolveCopilotSelection"
|
|
81
83
|
| "oauthDispatch"
|
|
82
84
|
| "noteRoutedAttemptSend"
|
|
83
85
|
| "bindKeyUsageFromBridge"
|
|
@@ -90,7 +92,7 @@ export async function executeResponsesSidecars(
|
|
|
90
92
|
| "notifyResponseComplete"
|
|
91
93
|
| "cancelResponseCompletion"
|
|
92
94
|
>,
|
|
93
|
-
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "
|
|
95
|
+
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
|
|
94
96
|
) {
|
|
95
97
|
const { config, options, logCtx } = requestContext;
|
|
96
98
|
const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
|
|
@@ -102,6 +104,7 @@ export async function executeResponsesSidecars(
|
|
|
102
104
|
anthropicSessionKey,
|
|
103
105
|
commitResolvedOAuthSelection,
|
|
104
106
|
resolveSelectionAdapter,
|
|
107
|
+
resolveCopilotSelection,
|
|
105
108
|
oauthDispatch,
|
|
106
109
|
} = transportState;
|
|
107
110
|
const {
|
|
@@ -114,6 +117,7 @@ export async function executeResponsesSidecars(
|
|
|
114
117
|
} = requestState;
|
|
115
118
|
const { routedCompaction, openAiSidecar } = sidecarState;
|
|
116
119
|
const { reserveCredentialHop } = sendBudgetState;
|
|
120
|
+
const sidecarBudget = createSidecarSendBudget(options, sendBudgetState, () => logCtx.usageLogInputTokens);
|
|
117
121
|
const {
|
|
118
122
|
commitReasoningReplayServingRoute,
|
|
119
123
|
continuationStateForResponse,
|
|
@@ -194,6 +198,7 @@ export async function executeResponsesSidecars(
|
|
|
194
198
|
// A single request may rotate once for a verification refusal. Keep this separate from the
|
|
195
199
|
// broader OAuth failover count: a sidecar turn can also spend that count on rate-limit recovery.
|
|
196
200
|
let antigravityValidationRotationAttempted = false;
|
|
201
|
+
/** Admit a replacement credential, or leave the loop's original refusal authoritative. */
|
|
197
202
|
const rotateSidecarProviderOn429 = async (
|
|
198
203
|
retryAfter: string | null,
|
|
199
204
|
responseHeaders?: Headers,
|
|
@@ -202,7 +207,7 @@ export async function executeResponsesSidecars(
|
|
|
202
207
|
): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
|
|
203
208
|
const antigravityValidationResponse = canRunWebSearch && route.providerName === "google-antigravity"
|
|
204
209
|
&& route.provider.authMode === "oauth" && originalResponse?.status === 403;
|
|
205
|
-
if (route.providerName !== "kiro" && !(anthropicInstance && originalResponse?.status === 403)
|
|
210
|
+
if (route.providerName !== "kiro" && !(anthropicInstance && (originalResponse?.status === 403 || originalResponse?.status === 401))
|
|
206
211
|
&& !antigravityValidationResponse && originalResponse && originalResponse.status !== 429) return null;
|
|
207
212
|
let antigravityVerification = false;
|
|
208
213
|
if (antigravityValidationResponse) {
|
|
@@ -264,7 +269,8 @@ export async function executeResponsesSidecars(
|
|
|
264
269
|
return null;
|
|
265
270
|
}
|
|
266
271
|
recoveryKind = "oauth-account-403";
|
|
267
|
-
sendBudgetState.pendingHopPermit = hop.permit;
|
|
272
|
+
if (!options.comboInitialSend) sendBudgetState.pendingHopPermit = hop.permit;
|
|
273
|
+
else sidecarBudget.ownCredentialHop(hop.permit);
|
|
268
274
|
}
|
|
269
275
|
const rotated = !originalResponse || originalResponse.status === 429
|
|
270
276
|
? rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
@@ -277,6 +283,8 @@ export async function executeResponsesSidecars(
|
|
|
277
283
|
// `applyFailoverSnapshot` has already rebound the route to the sibling identity/project.
|
|
278
284
|
} else if (rotated) {
|
|
279
285
|
route.provider = rotated;
|
|
286
|
+
try { await resolveCopilotSelection(retryParsed ?? parsed); }
|
|
287
|
+
catch { return null; } // Keep the sidecar's original refusal when replacement negotiation fails.
|
|
280
288
|
} else if (
|
|
281
289
|
// A POSITIVE gate, not an early return. An early `return null` here made every later arm
|
|
282
290
|
// unreachable: Anthropic never has a genericFailoverAccountId (isGenericFailoverProvider
|
|
@@ -323,8 +331,9 @@ export async function executeResponsesSidecars(
|
|
|
323
331
|
return null;
|
|
324
332
|
}
|
|
325
333
|
recoveryKind = "oauth-account-429";
|
|
326
|
-
if (canRunWebSearch && route.providerName === "google-antigravity")
|
|
327
|
-
|
|
334
|
+
if (!options.comboInitialSend && canRunWebSearch && route.providerName === "google-antigravity")
|
|
335
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
336
|
+
else sidecarBudget.ownCredentialHop(hop.permit);
|
|
328
337
|
} else if (
|
|
329
338
|
// Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
|
|
330
339
|
// web-search or image-bridge turn was terminal even with the pool fully enabled -- while
|
|
@@ -364,8 +373,8 @@ export async function executeResponsesSidecars(
|
|
|
364
373
|
hop.permit?.release();
|
|
365
374
|
return null;
|
|
366
375
|
}
|
|
367
|
-
recoveryKind = "anthropic-oauth-429";
|
|
368
|
-
hop.permit
|
|
376
|
+
recoveryKind = originalResponse?.status === 401 ? "oauth-401" : "anthropic-oauth-429";
|
|
377
|
+
sidecarBudget.ownCredentialHop(hop.permit);
|
|
369
378
|
} else {
|
|
370
379
|
// No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
|
|
371
380
|
// credential. The 429 is terminal for this sidecar turn.
|
|
@@ -451,11 +460,14 @@ export async function executeResponsesSidecars(
|
|
|
451
460
|
const imageProviderFetch = providerFetch(
|
|
452
461
|
route.provider,
|
|
453
462
|
options.codexWsRuntimeIdentity,
|
|
454
|
-
{ providerName: route.providerName, modelId: route.modelId },
|
|
463
|
+
{ providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(transportState.adapter) },
|
|
455
464
|
);
|
|
456
465
|
const imgResponse = await runWithImageBridge({
|
|
457
466
|
parsed, adapter: transportState.adapter,
|
|
458
|
-
incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget },
|
|
467
|
+
incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
|
|
468
|
+
onProducerStart: sidecarBudget.takeProducerOwnership,
|
|
469
|
+
onProducerEnd: sidecarBudget.release,
|
|
470
|
+
onIterationEnd: sidecarBudget.releaseUnsentHop,
|
|
459
471
|
...(imgPlan ? { plan: imgPlan } : {}),
|
|
460
472
|
...(vidPlan ? { videoPlan: vidPlan } : {}),
|
|
461
473
|
forwardHeaders: requestState.selectedForwardHeaders,
|
|
@@ -471,10 +483,10 @@ export async function executeResponsesSidecars(
|
|
|
471
483
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
472
484
|
waitForRequestSlot: imageProviderFetch.waitForPacing,
|
|
473
485
|
fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
|
|
474
|
-
fetchForRequest: (request, iterParsed) => {
|
|
486
|
+
fetchForRequest: (request, iterParsed, adapter) => {
|
|
475
487
|
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
476
488
|
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
477
|
-
providerName: route.providerName, modelId: route.modelId,
|
|
489
|
+
providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
|
|
478
490
|
});
|
|
479
491
|
return fetch.unpacedFetch ?? fetch;
|
|
480
492
|
},
|
|
@@ -541,12 +553,14 @@ export async function executeResponsesSidecars(
|
|
|
541
553
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
542
554
|
providerName: route.providerName,
|
|
543
555
|
modelId: route.modelId,
|
|
556
|
+
...sidecarBudget.fetchOptions(transportState.adapter),
|
|
544
557
|
})(input, init)) as typeof globalThis.fetch;
|
|
545
558
|
const wsResponse = await runWithWebSearch({
|
|
546
559
|
parsed, adapter: transportState.adapter,
|
|
547
|
-
|
|
560
|
+
onIterationEnd: sidecarBudget.releaseUnsentHop,
|
|
561
|
+
fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
548
562
|
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
549
|
-
providerName: route.providerName, modelId: route.modelId,
|
|
563
|
+
providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
|
|
550
564
|
}),
|
|
551
565
|
incomingMeta: {
|
|
552
566
|
headers: requestState.selectedForwardHeaders,
|
|
@@ -554,11 +568,11 @@ export async function executeResponsesSidecars(
|
|
|
554
568
|
abortSignal: options.abortSignal,
|
|
555
569
|
translatorBudget,
|
|
556
570
|
providerFetch: routedProviderFetch,
|
|
557
|
-
...(route.providerName === "google-antigravity" ? {
|
|
571
|
+
...(!options.comboInitialSend && route.providerName === "google-antigravity" ? {
|
|
558
572
|
sendBudget: sendBudgetState.refundableAdapterDispatchBudget,
|
|
559
573
|
onPhysicalSend: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) =>
|
|
560
574
|
sendBudgetState.noteAdapterPhysicalSend(logCtx.usageLogInputTokens, send),
|
|
561
|
-
} :
|
|
575
|
+
} : sidecarBudget.incomingMeta),
|
|
562
576
|
},
|
|
563
577
|
backend: wsPlan.backend,
|
|
564
578
|
forwardProvider: wsPlan.forwardSidecar?.provider,
|
|
@@ -591,7 +605,7 @@ export async function executeResponsesSidecars(
|
|
|
591
605
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
592
606
|
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
593
607
|
on429: rotateSidecarProviderOn429,
|
|
594
|
-
...(route.providerName === "google-antigravity" ? { onIterationEnd: () => {
|
|
608
|
+
...(!options.comboInitialSend && route.providerName === "google-antigravity" ? { onIterationEnd: () => {
|
|
595
609
|
// The next Antigravity adapter dispatch claims this permit. If request construction,
|
|
596
610
|
// admission or cancellation stopped it first, return the unused send allowance.
|
|
597
611
|
const permit = sendBudgetState.pendingHopPermit;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { IncomingMeta, ProviderAdapter } from "../../adapters/base";
|
|
2
|
+
import type { HandleResponsesOptions } from "./core-options";
|
|
3
|
+
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
4
|
+
import type { ProviderFetchOptions } from "./fetch-helpers";
|
|
5
|
+
import type { SingleUseDispatchPermit } from "../../lib/request-execution-budget";
|
|
6
|
+
|
|
7
|
+
/** Hosted inference consumes the same child-owned booking as ordinary dispatch. */
|
|
8
|
+
export function createSidecarSendBudget(
|
|
9
|
+
options: HandleResponsesOptions,
|
|
10
|
+
budget: Pick<ResponsesSendBudget, "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
|
|
11
|
+
inputTokens: () => number | undefined,
|
|
12
|
+
) {
|
|
13
|
+
const initial = options.comboInitialSend;
|
|
14
|
+
let hopPermit: SingleUseDispatchPermit | undefined;
|
|
15
|
+
let producerActive = false;
|
|
16
|
+
/** Detach this owner’s hop before refunding its still-unused reservation. */
|
|
17
|
+
const releaseHop = (): void => {
|
|
18
|
+
const permit = hopPermit;
|
|
19
|
+
hopPermit = undefined;
|
|
20
|
+
if (budget.pendingHopPermit === permit) budget.pendingHopPermit = undefined;
|
|
21
|
+
permit?.release();
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
/** Retain a Combo hop until dispatch; direct requests keep eager settlement. */
|
|
25
|
+
ownCredentialHop(permit?: SingleUseDispatchPermit): void {
|
|
26
|
+
if (!initial) { permit?.use(); return; } // Preserve direct callers' reporting contract.
|
|
27
|
+
releaseHop();
|
|
28
|
+
hopPermit = permit;
|
|
29
|
+
budget.pendingHopPermit = permit;
|
|
30
|
+
},
|
|
31
|
+
// Adapter-owned transports settle through the live dispatch view, not an HTTP receipt too.
|
|
32
|
+
incomingMeta: initial ? {
|
|
33
|
+
comboAttempt: options.comboAttempt === true,
|
|
34
|
+
sendBudget: budget.adapterDispatchBudget,
|
|
35
|
+
onPhysicalSend: send => budget.noteAdapterPhysicalSend(inputTokens(), send),
|
|
36
|
+
onRecoveryWithheld: budget.noteAdapterRecoveryWithheld,
|
|
37
|
+
} satisfies Partial<IncomingMeta> : {},
|
|
38
|
+
/** Attach HTTP receipts only when the adapter does not own physical dispatch. */
|
|
39
|
+
fetchOptions(adapter: ProviderAdapter): Pick<ProviderFetchOptions, "onPhysicalDispatch"> {
|
|
40
|
+
return initial && !adapter.fetchResponse && !adapter.runTurn
|
|
41
|
+
? { onPhysicalDispatch: () => {
|
|
42
|
+
const prepaid = budget.pendingHopPermit;
|
|
43
|
+
budget.pendingHopPermit = undefined;
|
|
44
|
+
budget.noteInitialDispatch(prepaid);
|
|
45
|
+
} } : {};
|
|
46
|
+
},
|
|
47
|
+
/** Transfer cleanup to the asynchronous producer before it can outlive the response. */
|
|
48
|
+
takeProducerOwnership(): void {
|
|
49
|
+
if (initial) { initial.producerOwned = true; producerActive = true; }
|
|
50
|
+
},
|
|
51
|
+
/** Release unused bookings only after the owning producer has settled. */
|
|
52
|
+
release(): void {
|
|
53
|
+
initial?.permit.release(); producerActive = false; releaseHop();
|
|
54
|
+
},
|
|
55
|
+
/** An iteration may finish before runTurn; defer its refund while that producer is active. */
|
|
56
|
+
releaseUnsentHop(): void { if (!producerActive) releaseHop(); },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { formatErrorResponse } from "../../bridge";
|
|
2
|
+
import { SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE } from "../../lib/errors";
|
|
3
|
+
import { markLocalRequestLogRefusal, type RequestLogContext } from "../request-log";
|
|
4
|
+
import { SpendLedgerFileRefusedError } from "../../lib/spend-reservation-ledger";
|
|
5
|
+
|
|
6
|
+
type StorageRefusalLog = Pick<RequestLogContext, "localTerminalReason" | "terminalSource" | "errorCode">;
|
|
7
|
+
|
|
8
|
+
/** Local storage refusal, including retry wrappers: never provider-health evidence. */
|
|
9
|
+
export function mapSpendLedgerStorageError(error: unknown, logCtx?: StorageRefusalLog) {
|
|
10
|
+
const seen = new Set<unknown>();
|
|
11
|
+
while (error instanceof Error && !seen.has(error)) {
|
|
12
|
+
if (error instanceof SpendLedgerFileRefusedError) {
|
|
13
|
+
if (logCtx) {
|
|
14
|
+
markLocalRequestLogRefusal(logCtx, SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE);
|
|
15
|
+
logCtx.errorCode = SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
status: 503,
|
|
19
|
+
errorType: "server_error",
|
|
20
|
+
code: SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE,
|
|
21
|
+
message: error.message,
|
|
22
|
+
} as const;
|
|
23
|
+
}
|
|
24
|
+
seen.add(error);
|
|
25
|
+
error = error.cause;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function spendLedgerStorageErrorResponse(error: unknown, logCtx?: StorageRefusalLog): Response | undefined {
|
|
31
|
+
const refusal = mapSpendLedgerStorageError(error, logCtx);
|
|
32
|
+
return refusal && formatErrorResponse(refusal.status, refusal.code, refusal.message);
|
|
33
|
+
}
|
|
@@ -15,6 +15,7 @@ import type { NativeResponseControl } from "./native-response-control";
|
|
|
15
15
|
// returned event frames as an SSE byte stream, so every downstream consumer
|
|
16
16
|
// (passthrough relay, adapter parsers, usage sniffing) is unchanged.
|
|
17
17
|
|
|
18
|
+
import { codexWsReuseAcrossTurnsEnabled } from "../../config/codex-ws-reuse-setting";
|
|
18
19
|
import { compareBunVersions } from "../../lib/bun-stream-caps";
|
|
19
20
|
import { resolveProxyRoute, socks5ProxyFromEnv } from "../../lib/proxy-env";
|
|
20
21
|
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
@@ -26,7 +27,7 @@ import { codexWsCreateFrameExceedsLimit } from "./codex-ws-wire";
|
|
|
26
27
|
import { isLoopbackUrl, rewriteWebSocketDial } from "../../plugins/upstream-hooks";
|
|
27
28
|
export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES, MAX_CODEX_WS_QUEUE_BYTES,
|
|
28
29
|
MAX_CODEX_WS_CREATE_FRAME_BYTES, CODEX_WS_CREATE_FRAME_LIMIT_BYTES, codexWsCreateFrameExceedsLimit,
|
|
29
|
-
isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./codex-ws-wire";
|
|
30
|
+
isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse, isCodexWsPreludeProjection } from "./codex-ws-wire";
|
|
30
31
|
export const MIN_BOUNDED_CODEX_WS_BUN_VERSION = "1.4.0";
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -99,8 +100,10 @@ export function planCodexWsDial(
|
|
|
99
100
|
// and is authoritative; a pooled socket's upgrade copy is intentionally ignored. A rewriter
|
|
100
101
|
// therefore cannot change them here, and the upgrade keeps the values the frame carries.
|
|
101
102
|
const dialHeaders = { ...rewritten.headers };
|
|
103
|
+
const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
|
|
102
104
|
for (const name of CODEX_WS_FRAME_HEADERS) {
|
|
103
|
-
if (
|
|
105
|
+
if (reuseAcrossTurns) delete dialHeaders[name];
|
|
106
|
+
else if (Object.hasOwn(headers, name)) dialHeaders[name] = headers[name]!;
|
|
104
107
|
else delete dialHeaders[name];
|
|
105
108
|
}
|
|
106
109
|
const dial = { ...rewritten, headers: dialHeaders };
|
|
@@ -148,7 +151,8 @@ export function shouldUseCodexWsUpstream(
|
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
|
|
154
|
+
/** Select the bounded WS lane or HTTP fallback, forwarding receipts at physical dispatch. */
|
|
155
|
+
export async function codexWsUpstreamFetch(
|
|
152
156
|
url: string,
|
|
153
157
|
init: RequestInit,
|
|
154
158
|
sseFallback: typeof globalThis.fetch,
|
|
@@ -157,6 +161,7 @@ export function codexWsUpstreamFetch(
|
|
|
157
161
|
beforeDispatch?: (headers: Headers) => void,
|
|
158
162
|
nativeControl?: NativeResponseControl,
|
|
159
163
|
beforeContinuation?: () => Promise<void>,
|
|
164
|
+
onPhysicalDispatch?: () => void,
|
|
160
165
|
): Promise<Response> {
|
|
161
166
|
const prepared = prepareCodexWsRequest(url, init);
|
|
162
167
|
if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
|
|
@@ -217,8 +222,16 @@ export function codexWsUpstreamFetch(
|
|
|
217
222
|
const dial = planCodexWsDial(wsUrl, headers, proxy);
|
|
218
223
|
if (!dial) return sseFallback(url, init);
|
|
219
224
|
const identity = control ? null : codexWsReuseIdentity(url, dial.headers, frameText, dial.proxy, dial.url);
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
const reuseAcrossTurns = identity !== null && codexWsReuseAcrossTurnsEnabled();
|
|
226
|
+
const pooled = identity
|
|
227
|
+
? reuseAcrossTurns
|
|
228
|
+
? await codexWsPool.acquireWaiting(identity, dial.url, dial.headers, dial.proxy, signal)
|
|
229
|
+
: codexWsPool.acquire(identity, dial.url, dial.headers, dial.proxy)
|
|
230
|
+
: null;
|
|
231
|
+
if (reuseAcrossTurns && signal?.aborted) {
|
|
232
|
+
return Promise.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
233
|
+
}
|
|
234
|
+
session = pooled ?? new CodexWsSession(dial.url, dial.headers, false, undefined, dial.proxy);
|
|
222
235
|
if (!session.busy && !session.reserve()) {
|
|
223
236
|
session.dispose();
|
|
224
237
|
return sseFallback(url, init);
|
|
@@ -229,7 +242,7 @@ export function codexWsUpstreamFetch(
|
|
|
229
242
|
return codexWsExchange({
|
|
230
243
|
session, url, init, prepared, sseFallback, onQuota, beforeDispatch,
|
|
231
244
|
nativeControl: control,
|
|
232
|
-
beforeContinuation,
|
|
245
|
+
beforeContinuation, onPhysicalDispatch,
|
|
233
246
|
bunVersion: typeof runtime === "string" ? runtime : runtime.version,
|
|
234
247
|
});
|
|
235
248
|
}
|
|
@@ -88,7 +88,8 @@ export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupH
|
|
|
88
88
|
// Mirror deriveStartupHealth's choice: an already-registered service is refreshed in
|
|
89
89
|
// place. Hardcoding installService here silently undid that for every stale-cache
|
|
90
90
|
// read, which is the path the dashboard hits while a probe is revalidating.
|
|
91
|
-
|
|
91
|
+
recommendedAction: value.desktop ? "Reopen OpenCodex and check Start at Login." : value.recommendedAction,
|
|
92
|
+
recommendedCommand: value.routingKind === "opencodex-local" && value.desktop !== undefined
|
|
92
93
|
? null
|
|
93
94
|
: value.routingKind === "custom-local" || value.routingKind === "unknown"
|
|
94
95
|
? value.commands.restoreNative
|
|
@@ -41,6 +41,8 @@ export interface WorkflowRefusalLog {
|
|
|
41
41
|
readonly requestId: string;
|
|
42
42
|
readonly start: number;
|
|
43
43
|
readonly logCtx: RequestLogContext;
|
|
44
|
+
/** Receiving HTTP route's notification after this refusal's final-log call. */
|
|
45
|
+
readonly onLogged?: () => void;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -84,6 +86,7 @@ export function workflowRefusalResponse(
|
|
|
84
86
|
addFinalRequestLog(refusalLog.requestId, refusalLog.start, refusalLog.logCtx, 429, {
|
|
85
87
|
closeReason: "terminal",
|
|
86
88
|
});
|
|
89
|
+
refusalLog.onLogged?.();
|
|
87
90
|
}
|
|
88
91
|
const refusal = formatErrorResponse(
|
|
89
92
|
429,
|
package/src/service/cli.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { WindowsSchedulerTaskProbe } from "./windows-scheduler";
|
|
|
26
26
|
import { win32 } from "node:path";
|
|
27
27
|
import { serviceDiagnosticsSummary } from "./diagnostics";
|
|
28
28
|
import { runServiceClaim } from "./claim";
|
|
29
|
+
import { createSupervisionLatch, desktopServiceCommandRefusal } from "./desktop-command-guard";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* `restart` is NO LONGER folded into `repair`.
|
|
@@ -214,6 +215,7 @@ export function removeServiceTokenAfterUninstall(
|
|
|
214
215
|
/** Execute a service verb while preserving client-owned credentials during uninstall. */
|
|
215
216
|
export async function serviceCommand(...args: (string | undefined)[]): Promise<void> {
|
|
216
217
|
const filteredArgs = args.filter((a): a is string => Boolean(a));
|
|
218
|
+
const supervisionLatch = createSupervisionLatch();
|
|
217
219
|
const execute = async (): Promise<void> => {
|
|
218
220
|
// `claim` is not an install verb: it is deliberately outside planServiceCommand (whose
|
|
219
221
|
// backend/installation checks do not apply to an ownership write) and outside
|
|
@@ -231,6 +233,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
231
233
|
process.exit(1);
|
|
232
234
|
}
|
|
233
235
|
const { parsed, command } = plan;
|
|
236
|
+
const refusal = desktopServiceCommandRefusal(command, undefined, supervisionLatch);
|
|
237
|
+
if (refusal) {
|
|
238
|
+
console.error(refusal);
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
234
242
|
if (command === "repair" || command === "restart") {
|
|
235
243
|
const verb: ServiceRepairVerb = command === "restart" ? "restart" : "repair";
|
|
236
244
|
assertServiceEnvironmentMatchesInstall();
|
|
@@ -249,7 +257,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
249
257
|
// existing registration had been restarted, not repaired (#4914).
|
|
250
258
|
let repairError: unknown;
|
|
251
259
|
try {
|
|
252
|
-
await repairService({ verb });
|
|
260
|
+
await repairService({ verb, supervisionLatch });
|
|
253
261
|
} catch (error) {
|
|
254
262
|
repairError = error;
|
|
255
263
|
process.exitCode = 1;
|
|
@@ -295,12 +303,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
295
303
|
throw new Error(`Task Scheduler state could not be verified before install: ${scheduler.detail}`);
|
|
296
304
|
}
|
|
297
305
|
if (scheduler.status === "absent") {
|
|
298
|
-
await installFreshWindowsSchedulerSafely();
|
|
306
|
+
await installFreshWindowsSchedulerSafely({ supervisionLatch });
|
|
299
307
|
} else {
|
|
300
|
-
await installServiceSafely(backend, ops.install);
|
|
308
|
+
await installServiceSafely(backend, ops.install, { supervisionLatch });
|
|
301
309
|
}
|
|
302
310
|
} else {
|
|
303
|
-
await installServiceSafely(backend, ops.install);
|
|
311
|
+
await installServiceSafely(backend, ops.install, { supervisionLatch });
|
|
304
312
|
}
|
|
305
313
|
} catch (error) {
|
|
306
314
|
console.error(`❌ Service install cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -474,6 +482,14 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
474
482
|
};
|
|
475
483
|
|
|
476
484
|
const preliminary = parseServiceArgs(filteredArgs);
|
|
485
|
+
if (preliminary.invalid.length === 0) {
|
|
486
|
+
const refusal = desktopServiceCommandRefusal(preliminary.sub, undefined, supervisionLatch);
|
|
487
|
+
if (refusal) {
|
|
488
|
+
console.error(refusal);
|
|
489
|
+
process.exitCode = 1;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
477
493
|
const windowsMutation = process.platform === "win32"
|
|
478
494
|
&& preliminary.invalid.length === 0
|
|
479
495
|
&& preliminary.sub !== "status";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createSupervisionLatch, inspectDesktopSupervision } from "./desktop-supervision.mjs";
|
|
2
|
+
|
|
3
|
+
export { createSupervisionLatch };
|
|
4
|
+
export type DesktopSupervision = ReturnType<typeof inspectDesktopSupervision>;
|
|
5
|
+
export type SupervisionInspector = (deps?: Parameters<typeof inspectDesktopSupervision>[0]) => DesktopSupervision;
|
|
6
|
+
export type SupervisionLatch = ReturnType<typeof createSupervisionLatch>;
|
|
7
|
+
|
|
8
|
+
export function desktopServiceRefusal(evidence: DesktopSupervision, blocked: boolean): string | null {
|
|
9
|
+
if (!blocked) return null;
|
|
10
|
+
const runtime = evidence.kind === "desktop" ? ` (pid ${evidence.runtimePid}, ${evidence.app})` : "";
|
|
11
|
+
return `OpenCodex Desktop supervises the running proxy${runtime}. `
|
|
12
|
+
+ "Quit OpenCodex, then rerun 'ocx service install' to move startup management to this CLI.";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertNoDesktopSupervision(
|
|
16
|
+
inspect: SupervisionInspector = inspectDesktopSupervision,
|
|
17
|
+
latch: SupervisionLatch = createSupervisionLatch(),
|
|
18
|
+
): void {
|
|
19
|
+
const evidence = inspect();
|
|
20
|
+
const refusal = desktopServiceRefusal(evidence, latch.observe(evidence));
|
|
21
|
+
if (refusal) throw new Error(refusal);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function desktopServiceCommandRefusal(
|
|
25
|
+
command: string,
|
|
26
|
+
inspect: SupervisionInspector = inspectDesktopSupervision,
|
|
27
|
+
latch: SupervisionLatch = createSupervisionLatch(),
|
|
28
|
+
): string | null {
|
|
29
|
+
if (!["install", "repair", "start", "restart"].includes(command)) return null;
|
|
30
|
+
const evidence = inspect();
|
|
31
|
+
return desktopServiceRefusal(evidence, latch.observe(evidence));
|
|
32
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, readFileSync,
|
|
2
|
+
import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { readPid } from "../config/process-state";
|
|
6
6
|
import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
|
|
7
|
+
import { inspectDesktopSupervision, processIdentity, procfs, type ProcReader } from "./desktop-supervision.mjs";
|
|
7
8
|
|
|
8
9
|
export interface DesktopStartupDiagnostic {
|
|
9
10
|
/** Durable desktop claim; failed supervision does not release ownership. */
|
|
@@ -11,11 +12,13 @@ export interface DesktopStartupDiagnostic {
|
|
|
11
12
|
loginEnabled: boolean;
|
|
12
13
|
running: boolean;
|
|
13
14
|
viable: boolean;
|
|
15
|
+
/** Live parentage when no durable claim exists; never converts supervision into ownership. */
|
|
16
|
+
supervisor?: { supervisorPid: number; runtimePid: number; app: string };
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
/** A login item alone is insufficient: the same app must
|
|
19
|
+
/** A login item alone is insufficient: the same app must supervise this proxy. */
|
|
17
20
|
export function deriveDesktopStartup(facts: Omit<DesktopStartupDiagnostic, "viable">): DesktopStartupDiagnostic {
|
|
18
|
-
return { ...facts, viable: facts.owned && facts.loginEnabled && facts.running };
|
|
21
|
+
return { ...facts, viable: (facts.owned || facts.supervisor !== undefined) && facts.loginEnabled && facts.running };
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
function run(command: string, args: string[]): string {
|
|
@@ -28,25 +31,12 @@ interface DesktopStartupDeps {
|
|
|
28
31
|
uid?: number;
|
|
29
32
|
ownership?: () => ServiceOwnershipResolution;
|
|
30
33
|
readPid?: () => number | null;
|
|
34
|
+
readRuntimePortPid?: () => number | null;
|
|
31
35
|
run?: typeof run;
|
|
32
36
|
env?: NodeJS.ProcessEnv;
|
|
33
37
|
proc?: ProcReader;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
/** Linux process identity from procfs: executable target and parent pid. */
|
|
37
|
-
interface ProcReader {
|
|
38
|
-
exe(pid: number): string;
|
|
39
|
-
parent(pid: number): number;
|
|
40
|
-
}
|
|
41
|
-
const procfs: ProcReader = {
|
|
42
|
-
exe: pid => readlinkSync(`/proc/${pid}/exe`),
|
|
43
|
-
// comm may contain spaces or parentheses, so fields are counted after the last ')'.
|
|
44
|
-
parent: pid => {
|
|
45
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
46
|
-
return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
40
|
/** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
|
|
51
41
|
export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
52
42
|
const platform = deps.platform ?? process.platform;
|
|
@@ -56,9 +46,47 @@ export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopS
|
|
|
56
46
|
? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
|
|
57
47
|
}
|
|
58
48
|
|
|
59
|
-
function
|
|
60
|
-
const
|
|
61
|
-
|
|
49
|
+
function macLoginRegistration(deps: DesktopStartupDeps) {
|
|
50
|
+
const execute = deps.run ?? run;
|
|
51
|
+
const path = join(deps.home ?? homedir(), "Library", "LaunchAgents", "OpenCodex.plist");
|
|
52
|
+
const plist = JSON.parse(execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]));
|
|
53
|
+
const args = plist.ProgramArguments;
|
|
54
|
+
if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
|
|
55
|
+
|| args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
|
|
56
|
+
|| !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
|
|
57
|
+
|| (plist.Program !== undefined && plist.Program !== args[0])) return null;
|
|
58
|
+
const app = realpathSync(args[0]);
|
|
59
|
+
const proxy = realpathSync(join(dirname(app), "ocx"));
|
|
60
|
+
accessSync(app, constants.X_OK);
|
|
61
|
+
accessSync(proxy, constants.X_OK);
|
|
62
|
+
const domain = `gui/${deps.uid ?? process.getuid!()}`;
|
|
63
|
+
const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
|
|
64
|
+
const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
|
|
65
|
+
const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
|
|
66
|
+
const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
|
|
67
|
+
const loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
|
|
68
|
+
&& !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
|
|
69
|
+
&& program !== undefined && realpathSync(program) === app
|
|
70
|
+
&& loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
|
|
71
|
+
return { app, proxy, loginEnabled };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: number, loginFor: (app: string) => boolean): DesktopStartupDiagnostic | undefined {
|
|
75
|
+
const evidence = inspectDesktopSupervision(deps);
|
|
76
|
+
if (evidence.kind !== "desktop") return undefined;
|
|
77
|
+
let loginEnabled = false;
|
|
78
|
+
try { loginEnabled = loginFor(evidence.app); }
|
|
79
|
+
catch { /* Login failure revokes restart viability, not the observed live supervisor. */ }
|
|
80
|
+
if ((deps.ownership ?? resolveServiceOwnership)().kind !== "none") return undefined;
|
|
81
|
+
// Login probes can outlive the runtime or its parent; only the same live chain gets credit.
|
|
82
|
+
const final = inspectDesktopSupervision({ ...deps, targetPid: evidence.runtimePid });
|
|
83
|
+
if (final.kind !== "desktop" || final.runtimePid !== evidence.runtimePid
|
|
84
|
+
|| final.supervisorPid !== evidence.supervisorPid || final.app !== evidence.app) return undefined;
|
|
85
|
+
// A new claim or revision during the last process probe invalidates the unowned projection.
|
|
86
|
+
const finalOwner = (deps.ownership ?? resolveServiceOwnership)();
|
|
87
|
+
if (finalOwner.kind !== "none" || finalOwner.revision !== ownerRevision) return undefined;
|
|
88
|
+
const { supervisorPid, runtimePid, app } = evidence;
|
|
89
|
+
return deriveDesktopStartup({ owned: false, loginEnabled, running: true, supervisor: { supervisorPid, runtimePid, app } });
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
/** Read-only macOS desktop ownership, login registration and live parent/child checks. */
|
|
@@ -66,6 +94,10 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
|
|
|
66
94
|
if ((deps.platform ?? process.platform) !== "darwin") return undefined;
|
|
67
95
|
const ownership = deps.ownership ?? resolveServiceOwnership;
|
|
68
96
|
const owner = ownership();
|
|
97
|
+
if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
|
|
98
|
+
const login = macLoginRegistration(deps);
|
|
99
|
+
return login !== null && login.app === app && login.loginEnabled;
|
|
100
|
+
});
|
|
69
101
|
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
70
102
|
const facts = { owned: true, loginEnabled: false, running: false };
|
|
71
103
|
const execute = deps.run ?? run;
|
|
@@ -74,26 +106,10 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
|
|
|
74
106
|
const home = deps.home ?? homedir();
|
|
75
107
|
const id = readFileSync(join(home, "Library", "Application Support", "com.opencodex.desktop", "install-id"), "utf8").trim();
|
|
76
108
|
if (id !== owner.ownership.installId) return deriveDesktopStartup(facts);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|| args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
|
|
82
|
-
|| !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
|
|
83
|
-
|| (plist.Program !== undefined && plist.Program !== args[0])) return deriveDesktopStartup(facts);
|
|
84
|
-
const app = realpathSync(args[0]);
|
|
85
|
-
const proxy = realpathSync(join(dirname(app), "ocx"));
|
|
86
|
-
accessSync(app, constants.X_OK);
|
|
87
|
-
accessSync(proxy, constants.X_OK);
|
|
88
|
-
const domain = `gui/${deps.uid ?? process.getuid!()}`;
|
|
89
|
-
const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
|
|
90
|
-
const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
|
|
91
|
-
const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
|
|
92
|
-
const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
|
|
93
|
-
facts.loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
|
|
94
|
-
&& !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
|
|
95
|
-
&& program !== undefined && realpathSync(program) === app
|
|
96
|
-
&& loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
|
|
109
|
+
const login = macLoginRegistration(deps);
|
|
110
|
+
if (!login) return deriveDesktopStartup(facts);
|
|
111
|
+
const { app, proxy } = login;
|
|
112
|
+
facts.loginEnabled = login.loginEnabled;
|
|
97
113
|
const pid = pidReader();
|
|
98
114
|
if (pid !== null) {
|
|
99
115
|
const child = processIdentity(pid, execute);
|
|
@@ -135,6 +151,16 @@ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): Desk
|
|
|
135
151
|
if ((deps.platform ?? process.platform) !== "linux") return undefined;
|
|
136
152
|
const ownership = deps.ownership ?? resolveServiceOwnership;
|
|
137
153
|
const owner = ownership();
|
|
154
|
+
if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
|
|
155
|
+
const home = deps.home ?? homedir();
|
|
156
|
+
const configured = (deps.env ?? process.env).XDG_CONFIG_HOME;
|
|
157
|
+
const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
|
|
158
|
+
// auto-launch's HOME entry must remain inside the session's XDG autostart search path.
|
|
159
|
+
if (resolve(config) !== resolve(home, ".config")) return false;
|
|
160
|
+
const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
|
|
161
|
+
const login = linuxLoginApp(entry);
|
|
162
|
+
return login !== null && realpathSync(login) === app;
|
|
163
|
+
});
|
|
138
164
|
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
139
165
|
const facts = { owned: true, loginEnabled: false, running: false };
|
|
140
166
|
const proc = deps.proc ?? procfs;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SupervisionEvidence =
|
|
2
|
+
| { kind: "desktop"; runtimePid: number; supervisorPid: number; app: string; proxy: string }
|
|
3
|
+
| { kind: "none" }
|
|
4
|
+
| { kind: "unknown"; reason: string; desktopSeen: boolean }
|
|
5
|
+
| { kind: "unsupported" };
|
|
6
|
+
|
|
7
|
+
export interface ProcReader {
|
|
8
|
+
exe(pid: number): string;
|
|
9
|
+
parent(pid: number): number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DesktopSupervisionDeps {
|
|
13
|
+
targetPid?: number;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
run?: (command: string, args: string[]) => string;
|
|
16
|
+
proc?: ProcReader;
|
|
17
|
+
readPid?: () => number | null;
|
|
18
|
+
readRuntimePortPid?: () => number | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function desktopSupervisionPaths(): { pid: string; runtimePort: string };
|
|
22
|
+
export const procfs: ProcReader;
|
|
23
|
+
export function processIdentity(pid: number, execute?: DesktopSupervisionDeps["run"], onExecutable?: (path: string) => void):
|
|
24
|
+
{ parent: number; executable: string } | null;
|
|
25
|
+
export function inspectDesktopSupervision(deps?: DesktopSupervisionDeps): SupervisionEvidence;
|
|
26
|
+
export function createSupervisionLatch(): { observe(evidence: SupervisionEvidence): boolean };
|