@bitkyc08/opencodex 2.58.0 → 2.60.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/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* check in `src/server/index.ts` and the pre-dispatch ceiling check in
|
|
6
6
|
* `src/server/responses/core.ts` -- had drifted into saying different things about the same
|
|
7
7
|
* refusal, and because the non-obvious part below has to be stated once and not twice.
|
|
8
|
+
*
|
|
9
|
+
* The HTTP admission DECISION lives here too, for the same reason: the lane derivation and the
|
|
10
|
+
* refusal it produces are two halves of one answer, and keeping them in one file is what stops
|
|
11
|
+
* a denial from losing the detail that makes it legible on the way out.
|
|
8
12
|
*/
|
|
9
13
|
import { formatErrorResponse } from "../bridge";
|
|
10
14
|
import {
|
|
@@ -14,9 +18,13 @@ import {
|
|
|
14
18
|
} from "./request-log";
|
|
15
19
|
import {
|
|
16
20
|
WORKFLOW_LOCAL_REFUSAL_HEADER,
|
|
21
|
+
admitWorkflowTurn,
|
|
17
22
|
workflowDenialSummary,
|
|
18
23
|
recordWorkflowRefusalEvent,
|
|
19
24
|
type WorkflowDenial,
|
|
25
|
+
type WorkflowDecision,
|
|
26
|
+
type WorkflowLane,
|
|
27
|
+
type WorkflowSpendDenialDetail,
|
|
20
28
|
} from "../lib/workflow-budget";
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -54,11 +62,16 @@ export function workflowRefusalResponse(
|
|
|
54
62
|
logCtx?: RequestLogContext,
|
|
55
63
|
refusalLog?: WorkflowRefusalLog,
|
|
56
64
|
rootId?: string,
|
|
65
|
+
spend?: WorkflowSpendDenialDetail,
|
|
57
66
|
): Response {
|
|
58
|
-
|
|
67
|
+
// The denial detail, when the caller has it, is what turns "a token ceiling refused this"
|
|
68
|
+
// into "the account ceiling of 100,000 is spent". It reaches the message, the event ring and
|
|
69
|
+
// the log row through this one function, so the three cannot drift into saying different
|
|
70
|
+
// things about the same refusal -- which is what put this module here in the first place.
|
|
71
|
+
const summary = workflowDenialSummary(reason, spend);
|
|
59
72
|
// Only a caller that decided the refusal ITSELF passes a root id. admitWorkflowTurn already
|
|
60
73
|
// records its own denials, so passing one there would double-count them.
|
|
61
|
-
if (rootId) recordWorkflowRefusalEvent(rootId, reason);
|
|
74
|
+
if (rootId) recordWorkflowRefusalEvent(rootId, reason, Date.now(), spend);
|
|
62
75
|
const recordOn = logCtx ?? refusalLog?.logCtx;
|
|
63
76
|
if (recordOn) {
|
|
64
77
|
markLocalRequestLogRefusal(recordOn, summary.code);
|
|
@@ -82,3 +95,44 @@ export function workflowRefusalResponse(
|
|
|
82
95
|
refusal.headers.set("Access-Control-Expose-Headers", WORKFLOW_LOCAL_REFUSAL_HEADER);
|
|
83
96
|
return refusal;
|
|
84
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Admit one HTTP turn against its root workflow budget.
|
|
101
|
+
*
|
|
102
|
+
* A fan-out shares the conversation it serves. Without a reserve, a worker burst takes every
|
|
103
|
+
* slot under its own root and the interactive turn that started it waits behind its own
|
|
104
|
+
* children. A request that names a parent is treated as that fan-out; a top-level request is
|
|
105
|
+
* the conversation and may use the reserved slots.
|
|
106
|
+
*/
|
|
107
|
+
export function admitHttpWorkflowTurn(headers: Headers): WorkflowDecision | undefined {
|
|
108
|
+
const rootId = headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
109
|
+
const threadId = headers.get("thread-id")?.trim() || undefined;
|
|
110
|
+
const lane: WorkflowLane = rootId !== undefined && threadId !== undefined && threadId !== rootId
|
|
111
|
+
? "worker"
|
|
112
|
+
: "interactive";
|
|
113
|
+
return admitWorkflowTurn(rootId, lane, undefined, threadId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The refusal for a decision {@link admitHttpWorkflowTurn} declined.
|
|
118
|
+
*
|
|
119
|
+
* It exists so the denial's own detail survives the trip to the wire. A token-ceiling denial
|
|
120
|
+
* arrives carrying the scope that refused and the number it refused against, and a caller that
|
|
121
|
+
* forwarded only `reason` would answer with a 429 that names no ceiling -- which is the
|
|
122
|
+
* illegible refusal this whole path is built to avoid. No root id is passed on: the admission
|
|
123
|
+
* check already recorded its own event, and passing one would count the refusal twice.
|
|
124
|
+
*/
|
|
125
|
+
export function workflowDecisionRefusalResponse(
|
|
126
|
+
decision: Extract<WorkflowDecision, { admitted: false }>,
|
|
127
|
+
logCtx?: RequestLogContext,
|
|
128
|
+
refusalLog?: WorkflowRefusalLog,
|
|
129
|
+
): Response {
|
|
130
|
+
const spend = decision.spendScope !== undefined && decision.spendLimit !== undefined
|
|
131
|
+
? {
|
|
132
|
+
scope: decision.spendScope,
|
|
133
|
+
limit: decision.spendLimit,
|
|
134
|
+
...(decision.spendProjected !== undefined ? { projected: decision.spendProjected } : {}),
|
|
135
|
+
}
|
|
136
|
+
: undefined;
|
|
137
|
+
return workflowRefusalResponse(decision.reason, logCtx, refusalLog, undefined, spend);
|
|
138
|
+
}
|
package/src/service/cli.ts
CHANGED
|
@@ -197,23 +197,28 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
197
197
|
assertServiceAuthEnvironment();
|
|
198
198
|
// A throw used to escape straight to the top level, so the one command that can
|
|
199
199
|
// leave a macOS hub evicted never reached its own serving check (#4236, defect 1f).
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
200
|
+
// Still ask whether anything is listening: on darwin the rollback inside installLaunchd
|
|
201
|
+
// may have brought the previous job back, and on Windows the preserve/restart protocol
|
|
202
|
+
// may have done the same. The operator needs both halves of that answer, and the exit
|
|
203
|
+
// code stays non-zero either way.
|
|
204
|
+
//
|
|
205
|
+
// The failure text travels INTO that report rather than being printed here. Printing it
|
|
206
|
+
// here and then letting the report reach its success line stated both outcomes for one
|
|
207
|
+
// run — "❌ Service repair failed: ... exit code 199" beside "✅ opencodex service
|
|
208
|
+
// repaired and serving on port 10100" — and the checkmark was the false half: the
|
|
209
|
+
// existing registration had been restarted, not repaired (#4914).
|
|
204
210
|
let repairError: unknown;
|
|
205
211
|
try {
|
|
206
212
|
await repairService({ verb });
|
|
207
213
|
} catch (error) {
|
|
208
214
|
repairError = error;
|
|
209
|
-
console.error(`❌ Service ${verb} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
210
215
|
process.exitCode = 1;
|
|
211
216
|
}
|
|
212
217
|
// All three platforms: a repair that reports success while nothing serves is the
|
|
213
218
|
// defect class this unit exists to close. Windows bakes its port into the
|
|
214
219
|
// scheduler wrapper or the WinSW XML, both of which installedServiceListenPort()
|
|
215
220
|
// now reads.
|
|
216
|
-
await reportServiceServing(verb === "restart" ? "restarted" : "repaired");
|
|
221
|
+
await reportServiceServing(verb === "restart" ? "restarted" : "repaired", {}, repairError);
|
|
217
222
|
if (repairError !== undefined) process.exitCode = 1;
|
|
218
223
|
return;
|
|
219
224
|
}
|
|
@@ -265,6 +270,11 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
265
270
|
await maybeShowStarPrompt();
|
|
266
271
|
break;
|
|
267
272
|
case "start":
|
|
273
|
+
// The installed launcher preserves the recorded CODEX_SQLITE_HOME: a
|
|
274
|
+
// changed sqlite_home/CODEX_SQLITE_HOME/CODEX_HOME would start the service
|
|
275
|
+
// on the recorded database while this shell resolves another, splitting
|
|
276
|
+
// native Codex history between databases. Same guard `stop` already runs.
|
|
277
|
+
assertServiceEnvironmentMatchesInstall();
|
|
268
278
|
ops.start();
|
|
269
279
|
await reportServiceServing("started");
|
|
270
280
|
break;
|
package/src/service/guards.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { isTestHomeGuardArmed } from "../lib/test-home-guard";
|
|
|
10
10
|
import { diagnoseService } from "./diagnostics";
|
|
11
11
|
import type { ServiceDiagnostic } from "./diagnostics";
|
|
12
12
|
import { currentCodexHome, currentOpenCodexHome, normalizePathForCompare, readServiceInstallState } from "./state";
|
|
13
|
+
import { resolveCodexSqliteHome } from "../codex/paths";
|
|
13
14
|
import { win32 } from "node:path";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -61,6 +62,15 @@ export function assertServiceEnvironmentMatchesInstall(): void {
|
|
|
61
62
|
"Run the service command from the same OpenCodex home so service state and secrets match.",
|
|
62
63
|
);
|
|
63
64
|
}
|
|
65
|
+
if (state.codexSqliteHome !== undefined) {
|
|
66
|
+
const actualCodexSqliteHome = resolveCodexSqliteHome({ codexHome: actualCodexHome });
|
|
67
|
+
if (normalizePathForCompare(state.codexSqliteHome) !== normalizePathForCompare(actualCodexSqliteHome)) {
|
|
68
|
+
throw new ServiceOwnershipError(
|
|
69
|
+
`Service was installed with Codex SQLite home=${state.codexSqliteHome}, but the current Codex SQLite home=${actualCodexSqliteHome}. ` +
|
|
70
|
+
"Run the service command with the same sqlite_home configuration and CODEX_SQLITE_HOME so native Codex history restore updates the correct database.",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
function isLoopbackHostname(hostname: string | undefined): boolean {
|
package/src/service/health.ts
CHANGED
|
@@ -148,6 +148,19 @@ export async function confirmServiceServing(
|
|
|
148
148
|
return { ok: false, port };
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The operation, as a noun, for a sentence that has to say it did not complete.
|
|
153
|
+
*
|
|
154
|
+
* The verb form reads as an accomplished fact ("service repaired"), which is exactly the
|
|
155
|
+
* claim that must not be made when the operation threw.
|
|
156
|
+
*/
|
|
157
|
+
const SERVICE_OPERATION_NOUN = {
|
|
158
|
+
installed: "install",
|
|
159
|
+
started: "start",
|
|
160
|
+
repaired: "repair",
|
|
161
|
+
restarted: "restart",
|
|
162
|
+
} as const;
|
|
163
|
+
|
|
151
164
|
/**
|
|
152
165
|
* Print the outcome of `install` / `start` / `repair` in terms of what the user cares
|
|
153
166
|
* about — is it serving? — instead of whether the manager accepted the registration.
|
|
@@ -156,10 +169,18 @@ export async function confirmServiceServing(
|
|
|
156
169
|
* worker reads the child's exit status, so a registered-but-silent service now makes it
|
|
157
170
|
* fall back to a direct proxy start rather than reporting a successful update over a
|
|
158
171
|
* dead port.
|
|
172
|
+
*
|
|
173
|
+
* `precedingFailure` is the error the operation itself threw, when it threw. The serving
|
|
174
|
+
* probe still runs — a rollback or a preserve/restart protocol may well have left the
|
|
175
|
+
* previous job answering, and the operator needs that half of the answer (#4236). But the
|
|
176
|
+
* two halves have to be ONE sentence. Printing the failure separately and then reaching
|
|
177
|
+
* the success line here reported both outcomes for the same run and credited work that did
|
|
178
|
+
* not happen: the registration was kept, not repaired (#4914).
|
|
159
179
|
*/
|
|
160
180
|
export async function reportServiceServing(
|
|
161
181
|
verb: "installed" | "started" | "repaired" | "restarted",
|
|
162
182
|
deps: Parameters<typeof confirmServiceServing>[0] = {},
|
|
183
|
+
precedingFailure?: unknown,
|
|
163
184
|
): Promise<void> {
|
|
164
185
|
const healthBudgetMs = deps.timeoutMs ?? serviceInstallHealthMs();
|
|
165
186
|
// Timed here rather than reported from the budget. confirmServiceServing knocks once
|
|
@@ -171,6 +192,28 @@ export async function reportServiceServing(
|
|
|
171
192
|
const startedAt = now();
|
|
172
193
|
const serving = await confirmServiceServing({ ...deps, timeoutMs: healthBudgetMs });
|
|
173
194
|
const waitedMs = Math.max(0, now() - startedAt);
|
|
195
|
+
const failureDetail = precedingFailure === undefined
|
|
196
|
+
? null
|
|
197
|
+
: precedingFailure instanceof Error ? precedingFailure.message : String(precedingFailure);
|
|
198
|
+
const operation = SERVICE_OPERATION_NOUN[verb];
|
|
199
|
+
if (failureDetail !== null) {
|
|
200
|
+
// Serving or not, the operation did not complete, so neither branch may print a
|
|
201
|
+
// checkmark. What differs is whether anything is answering, which is the fact the
|
|
202
|
+
// operator acts on next.
|
|
203
|
+
console.error(
|
|
204
|
+
serving.ok
|
|
205
|
+
? `⚠️ Service ${operation} did not complete: ${failureDetail}\n`
|
|
206
|
+
+ ` A proxy is answering on port ${serving.port}, so the existing registration was kept rather than replaced.\n`
|
|
207
|
+
+ ` The ${operation} did not take effect; rerun it once the reported cause is resolved.\n`
|
|
208
|
+
+ ` Log: ${serviceLogPath()}`
|
|
209
|
+
: `❌ Service ${operation} failed: ${failureDetail}\n`
|
|
210
|
+
+ ` No proxy answered on port ${serving.port} after ${Math.round(waitedMs / 1000)}s either.\n`
|
|
211
|
+
+ ` Log: ${serviceLogPath()}\n`
|
|
212
|
+
+ ` Meanwhile: ocx start (serves in the foreground)`,
|
|
213
|
+
);
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
174
217
|
if (serving.ok) {
|
|
175
218
|
console.log(`✅ opencodex service ${verb} and serving on port ${serving.port}.`);
|
|
176
219
|
return;
|
package/src/service/state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { delimiter, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
4
4
|
import { expandUserPath, getConfigDir } from "../config";
|
|
5
5
|
import { resolveCodexHomeDir, type CodexHomeDeps } from "../codex/home";
|
|
6
|
+
import { resolveCodexSqliteHome } from "../codex/paths";
|
|
6
7
|
import { durableBunRuntime, type BunRuntimeSource, type DurableBunRuntime } from "../lib/bun-runtime";
|
|
7
8
|
import { WINSW_SHA256, WINSW_VERSION } from "../lib/winsw";
|
|
8
9
|
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
@@ -218,6 +219,8 @@ export interface ServiceInstallState {
|
|
|
218
219
|
version: 1 | 2;
|
|
219
220
|
codexHome: string;
|
|
220
221
|
opencodexHome: string;
|
|
222
|
+
/** Effective Codex SQLite home used by this service's history integration. */
|
|
223
|
+
codexSqliteHome?: string;
|
|
221
224
|
/** Baked at install; lets status flag paths gone stale after npm prefix/nvm moves. */
|
|
222
225
|
bunPath?: string;
|
|
223
226
|
cliPath?: string;
|
|
@@ -241,7 +244,7 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
|
|
|
241
244
|
if (state.version !== 1 && state.version !== 2) return null;
|
|
242
245
|
if (typeof state.codexHome !== "string" || state.codexHome.length === 0) return null;
|
|
243
246
|
if (typeof state.opencodexHome !== "string" || state.opencodexHome.length === 0) return null;
|
|
244
|
-
for (const key of ["bunPath", "cliPath", "launcherPath", "winswVersion", "winswSha256"] as const) {
|
|
247
|
+
for (const key of ["codexSqliteHome", "bunPath", "cliPath", "launcherPath", "winswVersion", "winswSha256"] as const) {
|
|
245
248
|
if (state[key] !== undefined && (typeof state[key] !== "string" || state[key].length === 0)) return null;
|
|
246
249
|
}
|
|
247
250
|
if (state.version === 1) {
|
|
@@ -254,10 +257,12 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
|
|
|
254
257
|
|
|
255
258
|
export function writeServiceInstallState(backend: ServiceBackend = "scheduler", launcherPath?: string | null): void {
|
|
256
259
|
const { bun, cli } = cliEntry();
|
|
260
|
+
const codexHome = currentCodexHome();
|
|
257
261
|
const state: ServiceInstallState = {
|
|
258
262
|
version: 2,
|
|
259
|
-
codexHome
|
|
263
|
+
codexHome,
|
|
260
264
|
opencodexHome: currentOpenCodexHome(),
|
|
265
|
+
codexSqliteHome: resolveCodexSqliteHome({ codexHome }),
|
|
261
266
|
bunPath: bun,
|
|
262
267
|
cliPath: cli,
|
|
263
268
|
...(launcherPath ? { launcherPath } : {}),
|
|
@@ -116,6 +116,88 @@ function Start-OcxCommand([string[]]$CommandArgs, [switch]$TrackExit) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function Parse-StartupHealthText([string]$Text) {
|
|
120
|
+
foreach ($line in ($Text -split "\r?\n")) {
|
|
121
|
+
$trimmed = $line.Trim()
|
|
122
|
+
if (-not $trimmed) { continue }
|
|
123
|
+
try {
|
|
124
|
+
$parsed = $trimmed | ConvertFrom-Json
|
|
125
|
+
if ($parsed.status -in @("native", "protected", "at-risk") -and ($parsed.rebootSafe -is [bool])) {
|
|
126
|
+
return $parsed
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
# A non-JSON line from the CLI (for example a diagnostic banner) is not a
|
|
130
|
+
# startup-health payload; keep scanning for the actual JSON result.
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return $null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function Start-StartupHealthProbe {
|
|
138
|
+
# Startup safety is machine-local diagnostic state, so the tray asks the CLI to
|
|
139
|
+
# collect it directly (ocx __startup-health) instead of poking the management API,
|
|
140
|
+
# which is admin-token gated. The CLI collects the diagnostic straight from the OS,
|
|
141
|
+
# so it works without the proxy answering and without any credential.
|
|
142
|
+
#
|
|
143
|
+
# The child writes into redirected pipes that the tick reads asynchronously: only
|
|
144
|
+
# the completed Task<string> is ever touched, so a slow or hung diagnostic can never
|
|
145
|
+
# block the Windows Forms UI thread, and its lifetime is bounded by a kill in the
|
|
146
|
+
# tick that lets the next refresh replace it instead of stacking another process.
|
|
147
|
+
try {
|
|
148
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
149
|
+
$psi.FileName = $BunPath
|
|
150
|
+
$psi.Arguments = ((@($CliPath, "__startup-health") | ForEach-Object { ConvertTo-NativeArgument $_ }) -join " ")
|
|
151
|
+
$psi.UseShellExecute = $false
|
|
152
|
+
$psi.CreateNoWindow = $true
|
|
153
|
+
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
154
|
+
$psi.RedirectStandardOutput = $true
|
|
155
|
+
$psi.RedirectStandardError = $true
|
|
156
|
+
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
|
157
|
+
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
|
|
158
|
+
$psi.EnvironmentVariables["CODEX_HOME"] = $CodexHome
|
|
159
|
+
$psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
|
|
160
|
+
if ($BunRuntimeSource) {
|
|
161
|
+
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_SOURCE"] = $BunRuntimeSource
|
|
162
|
+
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_PATH"] = $BunPath
|
|
163
|
+
}
|
|
164
|
+
$process = [System.Diagnostics.Process]::Start($psi)
|
|
165
|
+
$script:startupProbeProcess = $process
|
|
166
|
+
# Drain both pipes asynchronously from the start so neither can fill and stall
|
|
167
|
+
# the child while the tick is only watching the output task's completion.
|
|
168
|
+
$script:startupProbeOutputTask = $process.StandardOutput.ReadToEndAsync()
|
|
169
|
+
$script:startupProbeErrorTask = $process.StandardError.ReadToEndAsync()
|
|
170
|
+
$script:startupProbeStarted = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
171
|
+
} catch {
|
|
172
|
+
Write-ActionLog "startup-health probe launch failed: $($_.Exception.GetType().Name)"
|
|
173
|
+
if ($null -ne $script:startupProbeProcess) {
|
|
174
|
+
# Process.Start succeeded but the async pipe setup failed: Dispose alone
|
|
175
|
+
# would leave the Bun child running, so terminate it before cleanup.
|
|
176
|
+
try {
|
|
177
|
+
$script:startupProbeProcess.Kill()
|
|
178
|
+
[void]$script:startupProbeProcess.WaitForExit(3000)
|
|
179
|
+
} catch {
|
|
180
|
+
Write-ActionLog "startup-health probe launch cleanup failed: $($_.Exception.GetType().Name)"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
Complete-StartupHealthProbe
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Complete-StartupHealthProbe {
|
|
188
|
+
$process = $script:startupProbeProcess
|
|
189
|
+
$script:startupProbeProcess = $null
|
|
190
|
+
$script:startupProbeOutputTask = $null
|
|
191
|
+
$script:startupProbeErrorTask = $null
|
|
192
|
+
if ($null -ne $process) {
|
|
193
|
+
try {
|
|
194
|
+
$process.Dispose()
|
|
195
|
+
} catch {
|
|
196
|
+
Write-ActionLog "startup-health probe dispose failed: $($_.Exception.GetType().Name)"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
119
201
|
function Read-ListenTarget {
|
|
120
202
|
foreach ($path in @((Join-Path $OpenCodexHome "runtime-port.json"), (Join-Path $OpenCodexHome "config.json"))) {
|
|
121
203
|
try {
|
|
@@ -172,6 +254,15 @@ $exitItem = $menu.Items.Add("Exit Tray")
|
|
|
172
254
|
$script:online = $false
|
|
173
255
|
$script:port = 10100
|
|
174
256
|
$script:proxyPid = $null
|
|
257
|
+
$script:wasOnline = $false
|
|
258
|
+
$script:startupHealth = $null
|
|
259
|
+
$script:startupHealthCheckedAt = 0L
|
|
260
|
+
$script:startupRefreshMs = 20000L
|
|
261
|
+
$script:startupProbeProcess = $null
|
|
262
|
+
$script:startupProbeOutputTask = $null
|
|
263
|
+
$script:startupProbeErrorTask = $null
|
|
264
|
+
$script:startupProbeStarted = 0L
|
|
265
|
+
$script:startupProbeTimeoutMs = 30000L
|
|
175
266
|
$script:pendingAction = $null
|
|
176
267
|
$script:pendingStarted = 0L
|
|
177
268
|
$script:pendingDeadline = 0L
|
|
@@ -228,18 +319,66 @@ function Update-TrayState {
|
|
|
228
319
|
$pidMatches = $null -eq $target.pid -or [int]$target.pid -eq [int]$health.pid
|
|
229
320
|
$script:online = $null -ne $health -and $health.status -eq "ok" -and $health.service -eq "opencodex" -and [int]$health.port -eq $script:port -and $pidMatches
|
|
230
321
|
$script:proxyPid = if ($script:online) { [int]$health.pid } else { $null }
|
|
322
|
+
$cameOnline = $script:online -and -not $script:wasOnline
|
|
323
|
+
$script:wasOnline = $script:online
|
|
324
|
+
$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
325
|
+
if ($null -ne $script:startupProbeProcess) {
|
|
326
|
+
$probeExited = $false
|
|
327
|
+
try {
|
|
328
|
+
$probeExited = $script:startupProbeProcess.HasExited
|
|
329
|
+
} catch {
|
|
330
|
+
$probeExited = $true
|
|
331
|
+
}
|
|
332
|
+
$probeTimedOut = ($now - $script:startupProbeStarted) -gt $script:startupProbeTimeoutMs
|
|
333
|
+
if ($probeExited -and $null -ne $script:startupProbeOutputTask -and $script:startupProbeOutputTask.IsCompleted) {
|
|
334
|
+
$probeStartup = $null
|
|
335
|
+
try {
|
|
336
|
+
$probeStartup = Parse-StartupHealthText $script:startupProbeOutputTask.Result
|
|
337
|
+
} catch {
|
|
338
|
+
Write-ActionLog "startup-health probe read failed: $($_.Exception.GetType().Name)"
|
|
339
|
+
}
|
|
340
|
+
$script:startupHealth = $probeStartup
|
|
341
|
+
Complete-StartupHealthProbe
|
|
342
|
+
$script:startupHealthCheckedAt = $now
|
|
343
|
+
} elseif ($probeTimedOut) {
|
|
344
|
+
# A hung diagnostic must not survive its refresh slot: terminate it so the
|
|
345
|
+
# next refresh cannot stack another orphaned Bun process behind it.
|
|
346
|
+
Write-ActionLog "startup-health probe timed out; terminating it"
|
|
347
|
+
try {
|
|
348
|
+
$script:startupProbeProcess.Kill()
|
|
349
|
+
[void]$script:startupProbeProcess.WaitForExit(3000)
|
|
350
|
+
} catch {
|
|
351
|
+
Write-ActionLog "startup-health probe kill failed: $($_.Exception.GetType().Name)"
|
|
352
|
+
}
|
|
353
|
+
$script:startupHealth = $null
|
|
354
|
+
Complete-StartupHealthProbe
|
|
355
|
+
$script:startupHealthCheckedAt = $now
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
$refreshDue =
|
|
359
|
+
$script:startupHealthCheckedAt -eq 0 -or
|
|
360
|
+
($now - $script:startupHealthCheckedAt -gt $script:startupRefreshMs)
|
|
361
|
+
if ($script:online -and ($cameOnline -or $refreshDue) -and $null -eq $script:startupProbeProcess) {
|
|
362
|
+
# Record the attempt so launch failures and invalid results remain throttled.
|
|
363
|
+
$script:startupHealthCheckedAt = $now
|
|
364
|
+
Start-StartupHealthProbe
|
|
365
|
+
}
|
|
231
366
|
if ($script:online) {
|
|
232
367
|
$statusItem.Text = "Proxy: Online (port $($script:port))"
|
|
233
368
|
$notify.Text = "opencodex: Online"
|
|
234
369
|
$startItem.Enabled = $false
|
|
235
370
|
$stopItem.Enabled = $true
|
|
236
371
|
$restartItem.Enabled = $true
|
|
237
|
-
|
|
238
|
-
|
|
372
|
+
# The management API is admin-token gated, so the tray must not poke
|
|
373
|
+
# /api/startup-health for the icon. The probe lifecycle maintenance and the
|
|
374
|
+
# refresh gate above run on every tick (even while offline) so a hung
|
|
375
|
+
# diagnostic is cleaned up outside the online-only UI branch.
|
|
376
|
+
$startup = $script:startupHealth
|
|
377
|
+
if ($null -ne $startup) {
|
|
239
378
|
$label = if ($startup.status -eq "at-risk") { "At risk" } elseif ($startup.status -eq "protected") { "Protected" } else { "Native routing" }
|
|
240
379
|
$safetyItem.Text = "Restart safety: $label"
|
|
241
380
|
$notify.Icon = if ($startup.status -eq "at-risk") { $warningIcon } else { $onlineIcon }
|
|
242
|
-
}
|
|
381
|
+
} else {
|
|
243
382
|
$safetyItem.Text = "Restart safety: unavailable"
|
|
244
383
|
$notify.Icon = $warningIcon
|
|
245
384
|
}
|
|
@@ -360,6 +499,19 @@ try {
|
|
|
360
499
|
if ($null -ne $script:pendingProcess) {
|
|
361
500
|
try { $script:pendingProcess.Dispose() } catch { $null = $_ }
|
|
362
501
|
}
|
|
502
|
+
if ($null -ne $script:startupProbeProcess) {
|
|
503
|
+
# A probe still running when the tray shuts down would outlive it: Dispose()
|
|
504
|
+
# releases the handle but does not terminate the child, so kill the active
|
|
505
|
+
# probe, wait briefly for exit, then release the probe state.
|
|
506
|
+
try {
|
|
507
|
+
Write-ActionLog "terminating active startup-health probe on tray shutdown"
|
|
508
|
+
$script:startupProbeProcess.Kill()
|
|
509
|
+
[void]$script:startupProbeProcess.WaitForExit(3000)
|
|
510
|
+
} catch {
|
|
511
|
+
Write-ActionLog "startup-health probe shutdown kill failed: $($_.Exception.GetType().Name)"
|
|
512
|
+
}
|
|
513
|
+
Complete-StartupHealthProbe
|
|
514
|
+
}
|
|
363
515
|
$notify.Dispose()
|
|
364
516
|
foreach ($icon in $script:ownedIcons) { $icon.Dispose() }
|
|
365
517
|
$menu.Dispose()
|
package/src/types/accounts.ts
CHANGED
|
@@ -19,6 +19,10 @@ export interface CodexAccount {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface CodexAccountCredentials {
|
|
22
|
+
/** Read-only Orca auth.json link; Orca exclusively owns OAuth refresh. */
|
|
23
|
+
sourceAuthPath?: string;
|
|
24
|
+
/** Bind the link to the originally imported OAuth subject. */
|
|
25
|
+
sourceSubject?: string;
|
|
22
26
|
accessToken: string;
|
|
23
27
|
refreshToken: string;
|
|
24
28
|
expiresAt: number;
|
package/src/types/config.ts
CHANGED
|
@@ -93,6 +93,31 @@ export interface OcxClaudeCodeConfig {
|
|
|
93
93
|
* forcing effort on every request can leak reasoning params to non-reasoning routes.
|
|
94
94
|
*/
|
|
95
95
|
alwaysEnableEffort?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Opt-in ENABLE_TOOL_SEARCH injection for launched Claude Code sessions (#4838).
|
|
98
|
+
*
|
|
99
|
+
* Claude Code turns MCP tool deferral off whenever ANTHROPIC_BASE_URL names a
|
|
100
|
+
* non-first-party host, so an `ocx claude` session ships every tool schema in
|
|
101
|
+
* full on every request. Its own diagnostic states the precondition for turning
|
|
102
|
+
* that back on: "Set ENABLE_TOOL_SEARCH=true (or auto / auto:N) if your proxy
|
|
103
|
+
* forwards tool_reference blocks."
|
|
104
|
+
*
|
|
105
|
+
* Default OFF, because opencodex only forwards them on the NATIVE ANTHROPIC
|
|
106
|
+
* PASSTHROUGH route, where the body reaches Anthropic untouched. On a translated
|
|
107
|
+
* route the deferral shape is not representable: compatibility.ts marks
|
|
108
|
+
* tool_search/tool_reference/deferred_tools unsupported, and toolsToResponses
|
|
109
|
+
* drops the tool_search server tool while ignoring defer_loading — deferred tools
|
|
110
|
+
* still carry input_schema on the wire, so the routed provider receives every
|
|
111
|
+
* schema anyway while Claude Code stops accounting for them and therefore stops
|
|
112
|
+
* compacting. Under `claudeCode.compatibility: "enforce"` the same request is
|
|
113
|
+
* rejected with 400 instead.
|
|
114
|
+
*
|
|
115
|
+
* `true` injects "true"; a string is passed through verbatim so Claude Code's own
|
|
116
|
+
* vocabulary (`auto`, `auto:N`, `force`) stays reachable. `false` and absent
|
|
117
|
+
* inject nothing — they do not force the variable off, because a value the
|
|
118
|
+
* operator exported themselves always wins.
|
|
119
|
+
*/
|
|
120
|
+
toolSearch?: boolean | string;
|
|
96
121
|
/**
|
|
97
122
|
* Subagent tier slots (devlog 260712 B2): injected as ANTHROPIC_DEFAULT_*_MODEL so
|
|
98
123
|
* Claude Code's Agent-tool aliases (opus/sonnet/haiku/fable + parent-inherit) route
|
|
@@ -378,6 +403,8 @@ export interface OcxConfig {
|
|
|
378
403
|
client?: OcxClientConnectionConfig;
|
|
379
404
|
/** Operator-facing redaction policy for management and CLI projections. */
|
|
380
405
|
privacy?: OcxPrivacyConfig;
|
|
406
|
+
/** Opt-in process-local aggregate request metrics on the authenticated management plane. */
|
|
407
|
+
metricsExport?: { enabled?: boolean };
|
|
381
408
|
/** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
|
|
382
409
|
emptyCompletionRetry?: boolean;
|
|
383
410
|
/** Suppress allowlisted client-facing Codex transport hints; provider enforcement is unchanged. */
|
|
@@ -591,6 +618,12 @@ export interface OcxConfig {
|
|
|
591
618
|
subagentEffortCap?: string;
|
|
592
619
|
/** Global model effort overrides, after provider model/wide pins; none means omission. */
|
|
593
620
|
modelPinnedEfforts?: Record<string, string>;
|
|
621
|
+
compactionRouting?: {
|
|
622
|
+
model: string;
|
|
623
|
+
reasoningEffort?: string;
|
|
624
|
+
/** Compaction triggers this override covers; omission means `["manual"]`. */
|
|
625
|
+
triggers?: ("manual" | "auto")[];
|
|
626
|
+
};
|
|
594
627
|
/**
|
|
595
628
|
* Models hidden from Codex discovery without blocking direct proxy calls. Routed provider ids
|
|
596
629
|
* are excluded from the catalog + /v1/models entirely. Account-qualified native ids hide only
|
|
@@ -722,13 +755,15 @@ export interface OcxConfig {
|
|
|
722
755
|
| { enabled: false }
|
|
723
756
|
| { enabled: true; port?: number };
|
|
724
757
|
/**
|
|
725
|
-
* Outbound
|
|
726
|
-
* "${HTTPS_PROXY}"
|
|
727
|
-
*
|
|
758
|
+
* Outbound proxy URL for provider requests. HTTP(S) example: "http://user:pass@proxy:8080"
|
|
759
|
+
* or "${HTTPS_PROXY}". SOCKS5 example: "socks5://127.0.0.1:10808" (`ocx start --socks5`).
|
|
760
|
+
* HTTP URLs are mirrored into HTTP_PROXY/HTTPS_PROXY when unset. SOCKS5 URLs are mirrored
|
|
761
|
+
* into ALL_PROXY, clear inherited HTTP(S)_PROXY, and use OpenCodex's SOCKS5 transport.
|
|
762
|
+
* Loopback stays in NO_PROXY.
|
|
728
763
|
* The literal `"auto"` reads the Windows WinINET static proxy (`ProxyEnable`/`ProxyServer`)
|
|
729
|
-
* once at process start
|
|
730
|
-
* or unreadable, it degrades to direct egress with
|
|
731
|
-
* changes are not followed.
|
|
764
|
+
* once at process start, preserving separate HTTP and HTTPS entries; on other platforms, or
|
|
765
|
+
* when the system proxy is off, SOCKS-only, or unreadable, it degrades to direct egress with
|
|
766
|
+
* one log line (#1525). PAC/WPAD and live changes are not followed.
|
|
732
767
|
*/
|
|
733
768
|
proxy?: string;
|
|
734
769
|
/**
|
|
@@ -778,6 +813,20 @@ export interface OcxConfig {
|
|
|
778
813
|
* built-in `openai` provider, so Codex does not select native remote compaction. Default off.
|
|
779
814
|
*/
|
|
780
815
|
codexClientCompaction?: boolean;
|
|
816
|
+
/**
|
|
817
|
+
* Label Codex shows for the injected `opencodex` provider. Defaults to `OpenCodex Proxy`.
|
|
818
|
+
*
|
|
819
|
+
* Presentation only. Routing is keyed on the provider id `opencodex` — the root
|
|
820
|
+
* `model_provider = "opencodex"` line and the `[model_providers.opencodex]` header — and this
|
|
821
|
+
* setting never touches either, so renaming the label cannot reroute or orphan a thread whose
|
|
822
|
+
* row already names that id.
|
|
823
|
+
*
|
|
824
|
+
* There is no way to emit an empty label: Codex rejects a provider with no name, so a blank,
|
|
825
|
+
* over-long, or control-character value falls back to the default rather than writing a config
|
|
826
|
+
* Codex would refuse to load. "Suppressing" the OpenCodex branding therefore means choosing a
|
|
827
|
+
* neutral label, not removing the field.
|
|
828
|
+
*/
|
|
829
|
+
codexProviderDisplayName?: string;
|
|
781
830
|
/**
|
|
782
831
|
* Compatibility mode: temporarily rewrite Codex resume-history metadata while the proxy is active
|
|
783
832
|
* so Codex App can show old OpenAI chats and opencodex-created exec chats under its default
|
|
@@ -811,6 +860,16 @@ export interface OcxConfig {
|
|
|
811
860
|
* absence is the only default state this policy has.
|
|
812
861
|
*/
|
|
813
862
|
codexPool?: OcxCodexPoolConfig;
|
|
863
|
+
/**
|
|
864
|
+
* Durable token ceilings for the spend-reservation ledger (#4546). Absent means the
|
|
865
|
+
* historical behaviour exactly: token spend is still accounted and journalled, and nothing
|
|
866
|
+
* is refused on it.
|
|
867
|
+
*
|
|
868
|
+
* Not in `getDefaultConfig()` on purpose, and deliberately shipped with no default figure.
|
|
869
|
+
* The ledger is on by default, so a default ceiling would start refusing real traffic on
|
|
870
|
+
* upgrade against a number nobody chose.
|
|
871
|
+
*/
|
|
872
|
+
spend?: OcxSpendConfig;
|
|
814
873
|
/** Opt-in per-account activation of newly reset Codex quota windows. */
|
|
815
874
|
codexQuotaAutoRefresh?: Record<string, {
|
|
816
875
|
fiveHour?: boolean;
|
|
@@ -1385,3 +1444,49 @@ export interface OcxCatalogAutoRefreshConfig {
|
|
|
1385
1444
|
*/
|
|
1386
1445
|
intervalMinutes?: number;
|
|
1387
1446
|
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* One scope's token ceiling.
|
|
1450
|
+
*
|
|
1451
|
+
* An object rather than a bare number because the ledger's scope limit is already a record in
|
|
1452
|
+
* `SpendReservationPolicy`, and a config shape that mirrors the runtime one cannot drift from
|
|
1453
|
+
* it silently. Absent `maxTokens` is the same as an absent scope: observe only.
|
|
1454
|
+
*/
|
|
1455
|
+
export interface OcxSpendScopeConfig {
|
|
1456
|
+
/**
|
|
1457
|
+
* Tokens the scope may hold at once, counting settled spend, open reservations and
|
|
1458
|
+
* unresolved spend. A reservation is the request's whole input plus its enforceable output
|
|
1459
|
+
* ceiling, so this is compared against a number that assumes every cached prefix misses.
|
|
1460
|
+
*
|
|
1461
|
+
* There is no default. A ceiling is a number only the operator knows -- it depends on the
|
|
1462
|
+
* plan, the account roster and what the install is for -- and the recorded lesson from the
|
|
1463
|
+
* observational phase of #4546 is that guessing one is worse than shipping none.
|
|
1464
|
+
*/
|
|
1465
|
+
maxTokens?: number;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Durable spend ceilings (#4546).
|
|
1470
|
+
*
|
|
1471
|
+
* The three scopes intersect: a request is admitted only when its own root workflow, the
|
|
1472
|
+
* identity that would serve it, and the pool it would draw from all have room. That is what
|
|
1473
|
+
* makes the ceiling hold against a caller that mints a fresh root id per request -- the root
|
|
1474
|
+
* is new, the identity and pool are not.
|
|
1475
|
+
*
|
|
1476
|
+
* Every field is optional and an empty section is the same as no section at all.
|
|
1477
|
+
*/
|
|
1478
|
+
export interface OcxSpendConfig {
|
|
1479
|
+
/** Ceiling for one root workflow -- the user-visible task, including its whole fan-out. */
|
|
1480
|
+
root?: OcxSpendScopeConfig;
|
|
1481
|
+
/** Ceiling for one authenticated identity, across every root it serves. */
|
|
1482
|
+
identity?: OcxSpendScopeConfig;
|
|
1483
|
+
/** Ceiling for one account pool, across every identity in it. */
|
|
1484
|
+
pool?: OcxSpendScopeConfig;
|
|
1485
|
+
/**
|
|
1486
|
+
* Days a dormant scope's accounting is retained. Default 7.
|
|
1487
|
+
*
|
|
1488
|
+
* A scope is dropped only when it is both idle and under its ceiling, so shortening this
|
|
1489
|
+
* cannot hand an exhausted scope a fresh allowance.
|
|
1490
|
+
*/
|
|
1491
|
+
retentionDays?: number;
|
|
1492
|
+
}
|