@bitkyc08/opencodex 2.90.0 → 2.92.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 +37 -3
- package/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +92 -0
- package/src/adapters/openai-responses/transport.ts +6 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +47 -8
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +53 -3
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +123 -35
- package/src/cli/provider.ts +46 -3
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +84 -9
- package/src/client/link-relay.ts +98 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +103 -3
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +155 -25
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/resolve.ts +12 -0
- package/src/combos/types.ts +19 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +375 -195
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/diagnostics.ts +193 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +63 -2
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +16 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +6 -2
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/mistral-transport.ts +37 -4
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +27 -22
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +7 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +22 -2
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +20 -7
- package/src/server/index/optional-listeners.ts +11 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +16 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +12 -4
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +125 -32
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +19 -6
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/definition.ts +20 -8
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +13 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/index-TmaHa54u.js +0 -94
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
|
@@ -203,6 +203,56 @@ export const AGENT_ROUTING_CAPABILITIES: readonly Capability[] = [
|
|
|
203
203
|
json: "payload",
|
|
204
204
|
details: ["Without --list at least one setting is required. Stored settings and Codex application outcomes remain distinct.","Stream-routed-output belongs only to web. Timeout changes preserve siblings; native apply and saved state are distinct."],
|
|
205
205
|
},
|
|
206
|
+
{
|
|
207
|
+
command: ["fallback"],
|
|
208
|
+
usage: "ocx fallback [status] [--json]",
|
|
209
|
+
summary: "Show which model and provider fallbacks are allowed, per-device local fallback, and the last fallback taken or refused.",
|
|
210
|
+
routes: [{"method": "GET", "path": "/api/fallback"}],
|
|
211
|
+
flags: [{"name": "--json", "value": "boolean", "summary": "Emit the ocx.fallback/1 state."}],
|
|
212
|
+
mutates: false,
|
|
213
|
+
json: "payload",
|
|
214
|
+
details: ["Reads the live proxy. On a Remote Link Child it reads the Home's state over the link (aliases omitted). The last event is in memory and resets on restart."],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
command: ["fallback", "off"],
|
|
218
|
+
usage: "ocx fallback off [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]",
|
|
219
|
+
summary: "Refuse model or provider fallback globally or for one provider, route or device.",
|
|
220
|
+
routes: [{"method": "PUT", "path": "/api/fallback"}],
|
|
221
|
+
flags: [{"name": "--kind", "value": "string", "summary": "model, provider or all (default)."}, {"name": "--provider", "value": "string", "summary": "Scope to the provider a request started on."}, {"name": "--route", "value": "string", "summary": "Scope to an exact selector such as combo/<id> or policy/<id>."}, {"name": "--device", "value": "string", "summary": "Scope to a Remote Link id or alias, or a Hub data key id; device off always refuses."}, {"name": "--json", "value": "boolean", "summary": "Emit the resulting state."}],
|
|
222
|
+
mutates: true,
|
|
223
|
+
json: "payload",
|
|
224
|
+
details: ["A refused hop answers fallback_refused with the triggering failure's status. Rotation among accounts or keys of one provider pool is not a fallback and stays on."],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
command: ["fallback", "on"],
|
|
228
|
+
usage: "ocx fallback on [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]",
|
|
229
|
+
summary: "Allow model or provider fallback at one scope, overriding a broader off.",
|
|
230
|
+
routes: [{"method": "PUT", "path": "/api/fallback"}],
|
|
231
|
+
flags: [{"name": "--kind", "value": "string", "summary": "model, provider or all (default)."}, {"name": "--provider", "value": "string", "summary": "Scope to a provider."}, {"name": "--route", "value": "string", "summary": "Scope to an exact selector."}, {"name": "--device", "value": "string", "summary": "Scope to a device."}, {"name": "--json", "value": "boolean", "summary": "Emit the resulting state."}],
|
|
232
|
+
mutates: true,
|
|
233
|
+
json: "payload",
|
|
234
|
+
details: ["Route beats provider beats global. A device setting can only refuse: on for a device records the value but never reopens a hop another scope refuses."],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
command: ["fallback", "clear"],
|
|
238
|
+
usage: "ocx fallback clear [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]",
|
|
239
|
+
summary: "Remove a fallback override so the scope inherits again.",
|
|
240
|
+
routes: [{"method": "PUT", "path": "/api/fallback"}],
|
|
241
|
+
flags: [{"name": "--kind", "value": "string", "summary": "model, provider or all (default)."}, {"name": "--provider", "value": "string", "summary": "Scope to a provider."}, {"name": "--route", "value": "string", "summary": "Scope to an exact selector."}, {"name": "--device", "value": "string", "summary": "Scope to a device."}, {"name": "--json", "value": "boolean", "summary": "Emit the resulting state."}],
|
|
242
|
+
mutates: true,
|
|
243
|
+
json: "payload",
|
|
244
|
+
details: ["Clearing every override restores today's behavior: every fallback allowed."],
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
command: ["fallback", "local"],
|
|
248
|
+
usage: "ocx fallback local <on|off> --device <id|alias> [--json]",
|
|
249
|
+
summary: "Record whether a device may use its own providers while the Home is unreachable.",
|
|
250
|
+
routes: [{"method": "PUT", "path": "/api/fallback"}],
|
|
251
|
+
flags: [{"name": "--device", "value": "string", "summary": "Remote Link id or alias, or Hub data key id."}, {"name": "--json", "value": "boolean", "summary": "Emit the resulting state."}],
|
|
252
|
+
mutates: true,
|
|
253
|
+
json: "payload",
|
|
254
|
+
details: ["Run on the Home. A permission GOAT reads; OpenCodex itself keeps answering home_unreachable on the device and never serves it from local providers."],
|
|
255
|
+
},
|
|
206
256
|
{
|
|
207
257
|
command: ["effort"],
|
|
208
258
|
usage: "ocx effort [status|low|medium|high|xhigh|max|ultra|->] [--json]",
|
|
@@ -335,13 +385,13 @@ export const AGENT_ROUTING_CAPABILITIES: readonly Capability[] = [
|
|
|
335
385
|
},
|
|
336
386
|
{
|
|
337
387
|
command: ["combo","set"],
|
|
338
|
-
usage: "ocx combo set <id> [--targets <provider/model[:weight],...> | --targets-file <FILE|->] [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->] [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->] [--alias <name|->] [--native-alias [on|off]] [--allow-metered-targets <on|off>] [--display-name <label|->] [--description <text|->] [--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->] [--decision-quota-signals <on|off|->] [--decision-quota-tiers <JSON|->] [--rename-from <id>] [--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>] [--json]",
|
|
388
|
+
usage: "ocx combo set <id> [--targets <provider/model[:weight],...> | --targets-file <FILE|->] [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->] [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->] [--alias <name|->] [--native-alias [on|off]] [--allow-metered-targets <on|off>] [--display-name <label|->] [--description <text|->] [--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->] [--decision-quota-signals <on|off|->] [--decision-quota-tiers <JSON|->] [--session-pin <duration|->] [--rename-from <id>] [--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>] [--json]",
|
|
339
389
|
summary: "Create, update or rename a combo.",
|
|
340
390
|
routes: [{"method":"GET","path":"/api/combos"},{"method":"PUT","path":"/api/combos"}],
|
|
341
|
-
flags: [{"name":"--allow-metered-targets","value":"string","summary":"on grants operator consent to metered targets only on a loopback-only proxy; off clears it."},{"name":"--targets","value":"string","summary":"Ordered provider/model[:weight] list; required for creation."},{"name":"--strategy","value":"string","summary":"Routing strategy."},{"name":"--sticky","value":"string","summary":"Round-robin limit 1-100; - resets."},{"name":"--effort","value":"string","summary":"Default effort; - clears."},{"name":"--effort-mode","value":"string","summary":"fallback or force; - resets to fallback."},{"name":"--alias","value":"string","summary":"Alias; - clears."},{"name":"--display-name","value":"string","summary":"Label; - clears."},{"name":"--description","value":"string","summary":"Catalog row description; - clears."},{"name":"--decision-provider","value":"string","summary":"JEV provider; - clears."},{"name":"--decision-model","value":"string","summary":"JEV route; - clears; exclusive with provider."},{"name":"--decision-timeout","value":"string","summary":"JEV deadline 1000-120000 ms; - clears."},{"name":"--decision-quota-signals","value":"string","summary":"JEV advisory quota on/off; - clears."},{"name":"--decision-quota-tiers","value":"string","summary":"Partial JSON thresholds; - clears."},{"name":"--rename-from","value":"string","summary":"Existing combo ID to rename."},{"name":"--json","value":"boolean","summary":"Emit structured JSON."},{"name":"--native-alias","value":"string","summary":"Bare legacy flag means on; explicit on/off preserves presence, including false."},{"name":"--targets-file","value":"string","summary":"Bounded ordered target JSON array, exclusive with --targets; preserves effort/profile/last-resort metadata."},{"name":"--image-input","value":"string","summary":"auto or disabled; explicit auto clears the previous disabled choice."},{"name":"--reasoning-effort-mode","value":"string","summary":"strict or adaptive, distinct from --effort-mode fallback/force."}],
|
|
391
|
+
flags: [{"name":"--allow-metered-targets","value":"string","summary":"on grants operator consent to metered targets only on a loopback-only proxy; off clears it."},{"name":"--targets","value":"string","summary":"Ordered provider/model[:weight] list; required for creation."},{"name":"--strategy","value":"string","summary":"Routing strategy."},{"name":"--sticky","value":"string","summary":"Round-robin limit 1-100; - resets."},{"name":"--effort","value":"string","summary":"Default effort; - clears."},{"name":"--effort-mode","value":"string","summary":"fallback or force; - resets to fallback."},{"name":"--alias","value":"string","summary":"Alias; - clears."},{"name":"--display-name","value":"string","summary":"Label; - clears."},{"name":"--description","value":"string","summary":"Catalog row description; - clears."},{"name":"--decision-provider","value":"string","summary":"JEV provider; - clears."},{"name":"--decision-model","value":"string","summary":"JEV route; - clears; exclusive with provider."},{"name":"--decision-timeout","value":"string","summary":"JEV deadline 1000-120000 ms; - clears."},{"name":"--decision-quota-signals","value":"string","summary":"JEV advisory quota on/off; - clears."},{"name":"--decision-quota-tiers","value":"string","summary":"Partial JSON thresholds; - clears."},{"name":"--session-pin","value":"string","summary":"JEV session pin idle window 1m-24h (90s, 20m, 1h); - clears."},{"name":"--rename-from","value":"string","summary":"Existing combo ID to rename."},{"name":"--json","value":"boolean","summary":"Emit structured JSON."},{"name":"--native-alias","value":"string","summary":"Bare legacy flag means on; explicit on/off preserves presence, including false."},{"name":"--targets-file","value":"string","summary":"Bounded ordered target JSON array, exclusive with --targets; preserves effort/profile/last-resort metadata."},{"name":"--image-input","value":"string","summary":"auto or disabled; explicit auto clears the previous disabled choice."},{"name":"--reasoning-effort-mode","value":"string","summary":"strict or adaptive, distinct from --effort-mode fallback/force."}],
|
|
342
392
|
mutates: true,
|
|
343
393
|
json: "payload",
|
|
344
|
-
details: ["create and update are aliases of set. Existing fields are read before writing; --targets replaces targets.","force overrides valid client effort and may increase cost and latency.","Only the implemented target grammar is supported; full GUI target metadata and routing-profile writes are separate tasks.","New file/flag conflicts refuse before transport. Partial edits preserve target metadata; explicit false/default values override server carry. Turning a native alias off may also require clearing its incompatible alias.","Read/merge/write uses one target and remains an upsert without revision CAS. Saved-but-incomplete catalog outcomes return nonzero with their receipt."],
|
|
394
|
+
details: ["create and update are aliases of set. Existing fields are read before writing; --targets replaces targets. Without --strategy an existing combo keeps its stored strategy and JEV policy; a new combo defaults to failover.","force overrides valid client effort and may increase cost and latency.","Only the implemented target grammar is supported; full GUI target metadata and routing-profile writes are separate tasks.","New file/flag conflicts refuse before transport. Partial edits preserve target metadata; explicit false/default values override server carry. Turning a native alias off may also require clearing its incompatible alias.","Read/merge/write uses one target and remains an upsert without revision CAS. Saved-but-incomplete catalog outcomes return nonzero with their receipt."],
|
|
345
395
|
},
|
|
346
396
|
{
|
|
347
397
|
command: ["combo", "remove"],
|
|
@@ -86,6 +86,16 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
86
86
|
usage: "ocx link reconnect --link-id <id> [--json]",
|
|
87
87
|
details: ["Requires a valid link ID and loopback admin authority. Stops the current ssh child, clears any backoff or failed state and starts a fresh attempt; read ocx link status for the outcome.","A Child-initiated link is refused with link_not_home_initiated: its own computer owns and retries that tunnel.","Always emits {linkId} JSON."],
|
|
88
88
|
},
|
|
89
|
+
{
|
|
90
|
+
command: ["link","cleanup"],
|
|
91
|
+
summary: "Find Windows Remote Link Children and explain how to move them to Hub mode.",
|
|
92
|
+
routes: [],
|
|
93
|
+
flags: [{"name":"--link-id","value":"string","summary":"Check only this link."},{"name":"--json","value":"boolean","summary":"Emit {links, changed:false} as JSON."}],
|
|
94
|
+
mutates: false,
|
|
95
|
+
json: "payload",
|
|
96
|
+
usage: "ocx link cleanup [--link-id <id>] [--json]",
|
|
97
|
+
details: ["Runs on the Home from its local link store. Asks each Home-initiated Child for uname -s over the link's pinned SSH trust; a Windows answer (Git Bash, MSYS, Cygwin, cmd or PowerShell) is reported with recovery steps.","Never revokes, disconnects or edits a link. A Windows Child needs ocx disconnect on itself, then ocx link revoke --force --yes on the Home, then ocx hub invite and ocx connect."],
|
|
98
|
+
},
|
|
89
99
|
{
|
|
90
100
|
command: ["remote-workspace","pair"],
|
|
91
101
|
summary: "Enroll this executor with one Hub using a one-time code from stdin and locally approved roots.",
|
|
@@ -156,7 +166,7 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
156
166
|
flags: [{ name: "--json", value: "boolean", summary: "Emit the status envelope as JSON." }],
|
|
157
167
|
mutates: false,
|
|
158
168
|
json: "envelope",
|
|
159
|
-
details: ["Reads /healthz plus local config; drives no management API route."],
|
|
169
|
+
details: ["Reads /healthz plus local config; drives no management API route.", "--json includes install: {method: bun|npm|pnpm|mise|source|standalone|unknown, evidence: {basis, mise?, error?}} from the updater's install detector."],
|
|
160
170
|
},
|
|
161
171
|
{
|
|
162
172
|
command: ["resolve"],
|
|
@@ -225,7 +235,10 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
225
235
|
flags: [{"name":"--json","value":"boolean","summary":"Emit the provider list as JSON."},{"name":"--jsonl","value":"boolean","summary":"Emit one configured provider per JSON line."}],
|
|
226
236
|
mutates: false,
|
|
227
237
|
json: "envelope",
|
|
228
|
-
details: [
|
|
238
|
+
details: [
|
|
239
|
+
"Local config and registry read, not GET /api/providers. --json and --jsonl are mutually exclusive.",
|
|
240
|
+
"Each configured row carries active: false while this device is linked to a Home (a Remote Link Child or a Hub-mode client), whose providers serve instead; true when the row serves. Credential values are never printed.",
|
|
241
|
+
],
|
|
229
242
|
usage: "ocx provider list [--json|--jsonl]",
|
|
230
243
|
},
|
|
231
244
|
{
|
|
@@ -621,11 +621,11 @@ export const OBSERVE_SYSTEM_CAPABILITIES: readonly Capability[] = [
|
|
|
621
621
|
},
|
|
622
622
|
{
|
|
623
623
|
command: ["service", "status"], summary: "Inspect local service-manager status.",
|
|
624
|
-
usage: "ocx service status",
|
|
624
|
+
usage: "ocx service status [--json]",
|
|
625
625
|
routes: [],
|
|
626
|
-
flags: [],
|
|
627
|
-
mutates: false, json: "
|
|
628
|
-
details: ["Local OS service manager: macOS launchd, Linux systemd, Windows Task Scheduler or WinSW. No management API
|
|
626
|
+
flags: [{"name": "--json", "value": "boolean", "summary": "Emit the passive service status document as JSON."}],
|
|
627
|
+
mutates: false, json: "payload",
|
|
628
|
+
details: ["Local OS service manager: macOS launchd, Linux systemd, Windows Task Scheduler or WinSW. No management API.", "--json is passive: schemaVersion 1 with installed, running, enabled, viable, stale, conflict, backend (launchd|systemd|schtasks|winsw|none), label, serving {probed, ok, port}, summary, logPath, ownershipLease and desktopLogin. It starts, stops, installs and repairs nothing.", "Backend options apply only to install; other verbs use the installed backend."],
|
|
629
629
|
},
|
|
630
630
|
{
|
|
631
631
|
command: ["service", "uninstall"], summary: "Uninstall the local service.",
|
|
@@ -221,11 +221,19 @@ function inspectionDeps(commandDeps: CodexCliUpdateCommandDeps): CodexCliInstall
|
|
|
221
221
|
return {
|
|
222
222
|
env: {
|
|
223
223
|
...trusted.managerRoots,
|
|
224
|
+
...trusted.transportEnv,
|
|
224
225
|
CODEX_CLI_PATH: trusted.codexCliPath ?? undefined,
|
|
225
226
|
PATH: trusted.path ?? undefined,
|
|
226
227
|
PATHEXT: trusted.pathExt ?? undefined,
|
|
228
|
+
// The lookup seeds the inspection models against the launch-time
|
|
229
|
+
// capture; ambient values are never consulted.
|
|
230
|
+
HOME: trusted.home ?? undefined,
|
|
231
|
+
NODE_PATH: trusted.nodePath ?? undefined,
|
|
227
232
|
},
|
|
228
233
|
configDir: trusted.configDir,
|
|
234
|
+
// Content observation picks the platform backing by the Node launcher's
|
|
235
|
+
// arch, which can differ from this Bun process's under Rosetta.
|
|
236
|
+
arch: trusted.arch ?? undefined,
|
|
229
237
|
// The snapshot is proof-bound to the launcher, but the report may only attest
|
|
230
238
|
// the environment candidate when the resolver picks the same canonical path —
|
|
231
239
|
// managed requires both.
|
|
@@ -26,6 +26,11 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
|
|
|
26
26
|
// `resolve` is read-only inspection for embedding shells: a lookup made to populate
|
|
27
27
|
// a consent surface must not trigger a shim repair side effect first.
|
|
28
28
|
if (command === "resolve") return true;
|
|
29
|
+
// `service status --json` is a passive inventory read for peers such as GOAT: a shim repair
|
|
30
|
+
// before dispatch would change the installation it was asked only to describe.
|
|
31
|
+
// Flags may precede the verb (`ocx service --json status`), exactly as parseServiceArgs reads it.
|
|
32
|
+
if (command === "service" && args.includes("--json")
|
|
33
|
+
&& args.slice(1).find(arg => !arg.startsWith("--")) === "status") return true;
|
|
29
34
|
// The desktop's approval-bound stop must validate its snapshot before any unrelated
|
|
30
35
|
// CLI repair changes the installation it was approved against.
|
|
31
36
|
if (command === "stop" && args.some(arg => arg.startsWith("--expect-"))) return true;
|
package/src/cli/combo-result.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeJevQuotaTiers } from "../combos/jev-quota-config";
|
|
2
|
+
import { normalizeJevSessionPin } from "../combos/jev-session-pin-config";
|
|
2
3
|
import { parseComboTargets } from "./combo-input";
|
|
3
4
|
import { printCatalogResult } from "./catalog-command-result";
|
|
4
5
|
|
|
@@ -39,6 +40,11 @@ function publicCombo(value: unknown): Record<string, unknown> {
|
|
|
39
40
|
if (!tiers) invalid();
|
|
40
41
|
result[key] = tiers;
|
|
41
42
|
continue;
|
|
43
|
+
} else if (key === "sessionPin") {
|
|
44
|
+
const pin = normalizeJevSessionPin(child);
|
|
45
|
+
if (!pin) invalid();
|
|
46
|
+
result[key] = pin;
|
|
47
|
+
continue;
|
|
42
48
|
} else if (key === "nativeAlias" || key === "allowMeteredTargets" || key === "decisionQuotaSignals") {
|
|
43
49
|
if (typeof child !== "boolean") invalid();
|
|
44
50
|
} else invalid();
|
package/src/cli/combo-stats.ts
CHANGED
|
@@ -53,6 +53,8 @@ function projectStats(value: unknown, id: string, range: string) {
|
|
|
53
53
|
const rawSummary = record(dto.summary);
|
|
54
54
|
const summary = {
|
|
55
55
|
...counts(rawSummary, SUMMARY_COUNTS),
|
|
56
|
+
// Absent means zero: a proxy started before session pins (#7525) does not report the field.
|
|
57
|
+
pinnedRequests: Object.hasOwn(rawSummary, "pinnedRequests") ? number(rawSummary.pinnedRequests) : 0,
|
|
56
58
|
averageLatencyMs: nullable(rawSummary.averageLatencyMs),
|
|
57
59
|
averageConfidence: nullable(rawSummary.averageConfidence, 1),
|
|
58
60
|
averageChosenProbability: nullable(rawSummary.averageChosenProbability, 1),
|
|
@@ -111,6 +113,7 @@ export async function handleComboStatsCommand(argv: string[], deps: RuntimeApiDe
|
|
|
111
113
|
`Combo ${id} — JEV observations (${range})`,
|
|
112
114
|
...(summary.decisions === 0 ? ["No recorded JEV decisions in this window."] : []),
|
|
113
115
|
`Decisions: ${summary.decisions}; applied: ${summary.appliedDecisions}; fail-open: ${summary.failOpenDecisions}`,
|
|
116
|
+
...(summary.pinnedRequests > 0 ? [`Session-pinned requests (no decision call): ${summary.pinnedRequests}`] : []),
|
|
114
117
|
`Successful requests: ${summary.successfulRequests}; requests with model fallback: ${summary.requestsWithModelFallback}`,
|
|
115
118
|
`Model tokens: ${summary.modelTotalTokens}; measured attempts: ${summary.measuredModelAttempts}/${summary.modelAttempts}`,
|
|
116
119
|
`Decision tokens: ${summary.decisionTotalTokens}; decisions reporting usage: ${summary.decisionUsageReported}/${summary.decisions}`,
|
package/src/cli/combo.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { normalizeJevQuotaTiers } from "../combos/jev-quota-config";
|
|
2
|
+
import { parseJevSessionPinDuration } from "../combos/jev-session-pin-config";
|
|
2
3
|
import { JEV_DECISION_TIMEOUT_MAX_MS, JEV_DECISION_TIMEOUT_MIN_MS } from "../combos/types";
|
|
3
4
|
import {
|
|
4
5
|
CliUsageError,
|
|
5
6
|
printData,
|
|
6
7
|
rejectArgs,
|
|
8
|
+
RuntimeApiError,
|
|
7
9
|
runCliAction,
|
|
8
10
|
runtimeRequest,
|
|
9
11
|
runtimeBaseUrl,
|
|
@@ -29,6 +31,7 @@ const USAGE = `Usage:
|
|
|
29
31
|
[--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>]
|
|
30
32
|
[--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->]
|
|
31
33
|
[--decision-quota-signals <on|off|->] [--decision-quota-tiers <JSON|->]
|
|
34
|
+
[--session-pin <duration|->] (e.g. 20m; keeps a session on JEV's target while active)
|
|
32
35
|
(jev only; the provider must be a configured jev-decision row)
|
|
33
36
|
[--rename-from <id>] [--json]
|
|
34
37
|
ocx combo stats <id> [--range <7d|30d|all>] [--json]
|
|
@@ -39,6 +42,15 @@ const USAGE = `Usage:
|
|
|
39
42
|
ocx combo discover [--query <text>] [--json]`;
|
|
40
43
|
|
|
41
44
|
type ComboRow = Record<string, unknown> & { id?: string; model?: string; strategy?: string };
|
|
45
|
+
const STRATEGIES = ["failover", "round-robin", "random", "least-used", "reset-window", "jev"] as const;
|
|
46
|
+
const isComboStrategy = (value: string): value is typeof STRATEGIES[number] => (STRATEGIES as readonly string[]).includes(value);
|
|
47
|
+
// Combo writes print fixed messages instead of server text. When --targets kept a stored JEV
|
|
48
|
+
// strategy the user did not name, a refusal of the targets or a JEV setting says so (#7579).
|
|
49
|
+
const KEPT_JEV_REJECTED = "combo_targets_rejected_by_kept_jev";
|
|
50
|
+
const KEPT_JEV_FIELDS = new Set(["targets", "decisionProvider", "decisionModel", "decisionTimeoutMs", "decisionQuotaSignals", "decisionQuotaTiers", "sessionPin"]);
|
|
51
|
+
const SET_ERRORS = {
|
|
52
|
+
[KEPT_JEV_REJECTED]: "The selected target rejected the new targets or the kept jev settings. Without --strategy, --targets keeps the stored jev strategy with its decision method, quota policy and session pin, and the save checks the new targets together with them. Inspect the combo with ocx combo show <id> --json, then fix the targets or those settings, or pass --strategy to change the strategy.",
|
|
53
|
+
};
|
|
42
54
|
|
|
43
55
|
function parseTargets(value: string): Array<{ provider: string; model: string; weight?: number }> {
|
|
44
56
|
const targets = value.split(",").map(part => part.trim()).filter(Boolean).map(part => {
|
|
@@ -91,29 +103,17 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
91
103
|
if (!id) throw new CliUsageError("combo id is required", USAGE);
|
|
92
104
|
const additional = await prepareComboInput(args, deps);
|
|
93
105
|
const targetsRaw = takeOption(args, "--targets");
|
|
106
|
+
const parsedTargets = targetsRaw === undefined ? undefined : parseTargets(targetsRaw);
|
|
94
107
|
const renameFrom = takeOption(args, "--rename-from");
|
|
95
|
-
const replacingTargets =
|
|
96
|
-
deps = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
97
|
-
// A structured target file replaces the target list, not unrelated combo settings.
|
|
98
|
-
const partialCurrent = targetsRaw === undefined
|
|
99
|
-
? await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", { redirect: "error" }, deps)
|
|
100
|
-
: undefined;
|
|
101
|
-
const partialExisting = partialCurrent?.combos?.find(row => row.id === (renameFrom ?? id));
|
|
102
|
-
if (!replacingTargets && !partialExisting) {
|
|
103
|
-
throw new CliUsageError("--targets is required when creating a combo", USAGE);
|
|
104
|
-
}
|
|
108
|
+
const replacingTargets = parsedTargets !== undefined || additional.targets !== undefined;
|
|
105
109
|
const strategyRaw = takeOption(args, "--strategy");
|
|
106
|
-
|
|
107
|
-
if (strategy !== "failover" && strategy !== "round-robin" && strategy !== "random" && strategy !== "least-used" && strategy !== "reset-window" && strategy !== "jev") throw new CliUsageError("--strategy must be failover, round-robin, random, least-used, reset-window, or jev", USAGE);
|
|
110
|
+
if (strategyRaw !== undefined && !isComboStrategy(strategyRaw)) throw new CliUsageError("--strategy must be failover, round-robin, random, least-used, reset-window, or jev", USAGE);
|
|
108
111
|
const stickyRaw = takeOption(args, "--sticky");
|
|
109
112
|
const stickyLimit = stickyRaw === undefined ? undefined : stickyRaw === "-" ? 1 : Number(stickyRaw);
|
|
110
113
|
if (stickyLimit !== undefined && (!Number.isInteger(stickyLimit) || stickyLimit < 1)) {
|
|
111
114
|
throw new CliUsageError("--sticky must be an integer from 1 to 100, or -", USAGE);
|
|
112
115
|
}
|
|
113
|
-
if (stickyLimit !== undefined)
|
|
114
|
-
if (stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE);
|
|
115
|
-
if (strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE);
|
|
116
|
-
}
|
|
116
|
+
if (stickyLimit !== undefined && stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE);
|
|
117
117
|
const effort = takeOption(args, "--effort");
|
|
118
118
|
const effortMode = takeOption(args, "--effort-mode");
|
|
119
119
|
if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force" && effortMode !== "-") {
|
|
@@ -128,19 +128,17 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
128
128
|
if (decisionModel !== undefined && decisionModel !== "-" && decisionProvider !== undefined && decisionProvider !== "-") {
|
|
129
129
|
throw new CliUsageError("--decision-model cannot be combined with --decision-provider", USAGE);
|
|
130
130
|
}
|
|
131
|
-
if (decisionModel !== undefined && decisionModel !== "-" && strategy !== "jev") {
|
|
132
|
-
throw new CliUsageError("--decision-model applies only to the jev strategy", USAGE);
|
|
133
|
-
}
|
|
134
|
-
if (decisionProvider !== undefined && decisionProvider !== "-" && strategy !== "jev") {
|
|
135
|
-
throw new CliUsageError("--decision-provider applies only to the jev strategy", USAGE);
|
|
136
|
-
}
|
|
137
131
|
const quotaSignals = takeOption(args, "--decision-quota-signals");
|
|
138
132
|
const quotaTiers = takeOption(args, "--decision-quota-tiers");
|
|
139
133
|
if (quotaSignals !== undefined && !["on", "off", "-"].includes(quotaSignals)) throw new CliUsageError("--decision-quota-signals must be on, off or -", USAGE);
|
|
140
134
|
let parsedQuotaTiers: ReturnType<typeof normalizeJevQuotaTiers> | null;
|
|
141
135
|
try { parsedQuotaTiers = quotaTiers === undefined || quotaTiers === "-" ? null : normalizeJevQuotaTiers(JSON.parse(quotaTiers)); } catch { throw new CliUsageError("invalid --decision-quota-tiers JSON", USAGE); }
|
|
142
136
|
if (quotaTiers !== undefined && quotaTiers !== "-" && !parsedQuotaTiers) throw new CliUsageError("quota tiers must be finite 0..100 and strictly ascending", USAGE);
|
|
143
|
-
|
|
137
|
+
const sessionPinRaw = takeOption(args, "--session-pin");
|
|
138
|
+
const sessionPinMs = sessionPinRaw === undefined || sessionPinRaw === "-" ? undefined : parseJevSessionPinDuration(sessionPinRaw);
|
|
139
|
+
if (sessionPinRaw !== undefined && sessionPinRaw !== "-" && sessionPinMs === undefined) {
|
|
140
|
+
throw new CliUsageError("--session-pin must be a duration from 1m to 24h (for example 90s, 20m, 1h, or milliseconds), or -", USAGE);
|
|
141
|
+
}
|
|
144
142
|
const decisionTimeout = takeOption(args, "--decision-timeout");
|
|
145
143
|
let decisionTimeoutMs: number | null | undefined;
|
|
146
144
|
if (decisionTimeout !== undefined) {
|
|
@@ -154,19 +152,38 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
154
152
|
USAGE,
|
|
155
153
|
);
|
|
156
154
|
}
|
|
157
|
-
if (decisionTimeoutMs !== null && strategy !== "jev") {
|
|
158
|
-
throw new CliUsageError("--decision-timeout applies only to the jev strategy", USAGE);
|
|
159
|
-
}
|
|
160
155
|
}
|
|
161
156
|
rejectArgs(args, USAGE);
|
|
157
|
+
// Flags that belong to one strategy. An explicit --strategy is checked before any request; an
|
|
158
|
+
// omitted one is the stored strategy, so it is checked once the row has been read.
|
|
159
|
+
const requireStrategyFlags = (strategy: string): void => {
|
|
160
|
+
if (stickyLimit !== undefined && strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE);
|
|
161
|
+
if (strategy === "jev") return;
|
|
162
|
+
if (decisionModel !== undefined && decisionModel !== "-") throw new CliUsageError("--decision-model applies only to the jev strategy", USAGE);
|
|
163
|
+
if (decisionProvider !== undefined && decisionProvider !== "-") throw new CliUsageError("--decision-provider applies only to the jev strategy", USAGE);
|
|
164
|
+
if (quotaSignals !== undefined && quotaSignals !== "-" || quotaTiers !== undefined && quotaTiers !== "-") throw new CliUsageError("quota settings apply only to jev", USAGE);
|
|
165
|
+
if (sessionPinMs !== undefined) throw new CliUsageError("--session-pin applies only to the jev strategy", USAGE);
|
|
166
|
+
if (decisionTimeoutMs != null) throw new CliUsageError("--decision-timeout applies only to the jev strategy", USAGE);
|
|
167
|
+
};
|
|
168
|
+
if (strategyRaw !== undefined) requireStrategyFlags(strategyRaw);
|
|
169
|
+
deps = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
170
|
+
// Replacing targets keeps every other stored setting, including the strategy and its policy (#7579).
|
|
171
|
+
const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", { redirect: "error" }, deps);
|
|
172
|
+
const existing = current.combos?.find(row => row.id === (renameFrom ?? id));
|
|
173
|
+
if (!replacingTargets && !existing) {
|
|
174
|
+
throw new CliUsageError("--targets is required when creating a combo", USAGE);
|
|
175
|
+
}
|
|
176
|
+
const strategy = strategyRaw ?? existing?.strategy ?? "failover";
|
|
177
|
+
if (!isComboStrategy(strategy)) throw new CliUsageError(`combo ${renameFrom ?? id} has an unsupported stored strategy; pass --strategy`, USAGE);
|
|
178
|
+
if (strategyRaw === undefined) requireStrategyFlags(strategy);
|
|
162
179
|
// Listing-only nulls must not be resent as explicit clears (alias rejects null).
|
|
163
|
-
const combo: Record<string, unknown> =
|
|
164
|
-
? Object.fromEntries(Object.entries(
|
|
165
|
-
: { strategy, stickyLimit: stickyLimit ?? 1
|
|
180
|
+
const combo: Record<string, unknown> = existing
|
|
181
|
+
? Object.fromEntries(Object.entries(existing).filter(([, value]) => value !== null))
|
|
182
|
+
: { strategy, stickyLimit: stickyLimit ?? 1 };
|
|
166
183
|
delete combo.id;
|
|
167
184
|
delete combo.model;
|
|
168
|
-
if (
|
|
169
|
-
|
|
185
|
+
if (replacingTargets) combo.targets = additional.targets ?? parsedTargets;
|
|
186
|
+
combo.strategy = strategy;
|
|
170
187
|
if (stickyLimit !== undefined) combo.stickyLimit = stickyLimit;
|
|
171
188
|
if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
|
|
172
189
|
if (effortMode !== undefined) combo.defaultEffortMode = effortMode === "-" ? "fallback" : effortMode;
|
|
@@ -184,26 +201,33 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
184
201
|
if (decisionTimeoutMs !== undefined) combo.decisionTimeoutMs = decisionTimeoutMs;
|
|
185
202
|
if (quotaSignals !== undefined) combo.decisionQuotaSignals = quotaSignals === "-" ? null : quotaSignals === "on";
|
|
186
203
|
if (quotaTiers !== undefined) combo.decisionQuotaTiers = parsedQuotaTiers;
|
|
204
|
+
if (sessionPinRaw !== undefined) combo.sessionPin = sessionPinMs === undefined ? null : { ttlMs: sessionPinMs };
|
|
187
205
|
if (strategy !== "jev") {
|
|
188
206
|
delete combo.decisionProvider;
|
|
189
207
|
delete combo.decisionModel;
|
|
190
208
|
delete combo.decisionTimeoutMs;
|
|
191
209
|
delete combo.decisionQuotaSignals;
|
|
192
210
|
delete combo.decisionQuotaTiers;
|
|
193
|
-
|
|
194
|
-
const current = partialCurrent ?? await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", { redirect: "error" }, deps);
|
|
195
|
-
const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id));
|
|
196
|
-
if (additional.imageInput === undefined && existing?.imageInput === "disabled") combo.imageInput = "disabled";
|
|
197
|
-
if (effort === undefined && existing?.defaultEffort != null && combo.defaultEffort === undefined) {
|
|
198
|
-
combo.defaultEffort = existing.defaultEffort;
|
|
211
|
+
delete combo.sessionPin;
|
|
199
212
|
}
|
|
200
213
|
if (effortMode === undefined && existing?.defaultEffortMode === "force") {
|
|
201
214
|
combo.defaultEffortMode = effort === "-" ? "fallback" : "force";
|
|
202
215
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
216
|
+
let result: unknown;
|
|
217
|
+
try {
|
|
218
|
+
result = await runtimeRequest("/api/combos", {
|
|
219
|
+
method: "PUT", redirect: "error",
|
|
220
|
+
body: serializeManagementJson({ id, combo, ...(renameFrom ? { renameFrom } : {}) }),
|
|
221
|
+
}, deps);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const field = error instanceof RuntimeApiError && error.body !== null && typeof error.body === "object"
|
|
224
|
+
? (error.body as Record<string, unknown>).field : undefined;
|
|
225
|
+
if (error instanceof RuntimeApiError && error.status === 400 && typeof field === "string" && KEPT_JEV_FIELDS.has(field)
|
|
226
|
+
&& replacingTargets && strategyRaw === undefined && strategy === "jev") {
|
|
227
|
+
throw new RuntimeApiError(error.message, error.status, { code: KEPT_JEV_REJECTED });
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
207
231
|
return printComboWriteResult(result, id, wantsJson);
|
|
208
232
|
}
|
|
209
233
|
|
|
@@ -286,7 +310,7 @@ async function discover(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
286
310
|
|
|
287
311
|
export async function handleComboCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
288
312
|
const [subcommand, ...writeArgs] = argv;
|
|
289
|
-
if (subcommand === "set" || subcommand === "create" || subcommand === "update") return runCatalogAction(() => set(writeArgs, deps));
|
|
313
|
+
if (subcommand === "set" || subcommand === "create" || subcommand === "update") return runCatalogAction(() => set(writeArgs, deps), SET_ERRORS);
|
|
290
314
|
if (subcommand === "stats") {
|
|
291
315
|
const { handleComboStatsCommand } = await import("./combo-stats");
|
|
292
316
|
return handleComboStatsCommand(writeArgs, deps);
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -437,6 +437,32 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
439
|
await handleConnectedSyncCatalogWrite(result, restartScope);
|
|
440
|
+
if (result.applyRecordFailed) {
|
|
441
|
+
console.error("The Home's Codex settings were applied, but the apply record could not be written; the client runtime was not restarted. Run 'ocx sync' again.");
|
|
442
|
+
process.exitCode = 1;
|
|
443
|
+
}
|
|
444
|
+
// OCX-6: a link Child's runtime must have started after the apply it serves under.
|
|
445
|
+
if (link && result.appliedRecord) {
|
|
446
|
+
const { applyRestartMessage, restartRuntimeAfterApply } = await import("./link-apply-restart");
|
|
447
|
+
const { sameClientConnectionOwner } = await import("../client/state");
|
|
448
|
+
const applied = clientState.value;
|
|
449
|
+
const outcome = await restartRuntimeAfterApply(result.appliedRecord, {
|
|
450
|
+
restart: startWhenStopped => deps.handleProxyRestart(startWhenStopped),
|
|
451
|
+
recoveryStart: recovering => deps.handleRestartStartWhenStopped(recovering),
|
|
452
|
+
connectionUnchanged: () => {
|
|
453
|
+
const current = readClientConnectionState();
|
|
454
|
+
return current.kind === "connected" && sameClientConnectionOwner(current.value, applied)
|
|
455
|
+
&& current.value.link?.linkId === applied.link?.linkId;
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
const message = applyRestartMessage(outcome);
|
|
459
|
+
if (outcome.kind === "failed" || outcome.kind === "connection-changed") {
|
|
460
|
+
console.error(message);
|
|
461
|
+
process.exitCode = 1;
|
|
462
|
+
} else {
|
|
463
|
+
console.log(message);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
440
466
|
// `process.exitCode` rather than a literal 0, for the same reason every other
|
|
441
467
|
// runner does it (tests/cli/cli-transport-honesty.test.ts): the catalog-write helper
|
|
442
468
|
// drives app-server restarts, and one of those recording a failure must not be
|
|
@@ -876,6 +902,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
876
902
|
const { handleEffortCommand } = await import("./effort");
|
|
877
903
|
return await handleEffortCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
|
|
878
904
|
},
|
|
905
|
+
fallback: async deps => {
|
|
906
|
+
const { handleFallbackCommand } = await import("./fallback");
|
|
907
|
+
return await handleFallbackCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
|
|
908
|
+
},
|
|
879
909
|
agent: async deps => {
|
|
880
910
|
const { handleAgentCommand } = await import("./agent");
|
|
881
911
|
return await handleAgentCommand(deps.args.slice(1));
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CliUsageError,
|
|
3
|
+
printData,
|
|
4
|
+
rejectArgs,
|
|
5
|
+
runCliAction,
|
|
6
|
+
runtimeRequest,
|
|
7
|
+
takeFlag,
|
|
8
|
+
takeOption,
|
|
9
|
+
terminalSafeText,
|
|
10
|
+
type RuntimeApiDeps,
|
|
11
|
+
} from "./runtime-api";
|
|
12
|
+
|
|
13
|
+
export const FALLBACK_USAGE = `Usage:
|
|
14
|
+
ocx fallback status [--json]
|
|
15
|
+
ocx fallback off [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]
|
|
16
|
+
ocx fallback on [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]
|
|
17
|
+
ocx fallback clear [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]
|
|
18
|
+
ocx fallback local <on|off> --device <id|alias> [--json]`;
|
|
19
|
+
|
|
20
|
+
type Switches = { model?: boolean; provider?: boolean };
|
|
21
|
+
interface DeviceRow { id?: string; alias?: string; localFallback?: boolean; model?: boolean | null; provider?: boolean | null }
|
|
22
|
+
interface EventRow { at?: string; site?: string; route?: string; kind?: string; from?: string; to?: string; trigger?: string; outcome?: string; scope?: string }
|
|
23
|
+
interface FallbackState {
|
|
24
|
+
global?: { model?: boolean; provider?: boolean };
|
|
25
|
+
providers?: Record<string, Switches>;
|
|
26
|
+
routes?: Record<string, Switches>;
|
|
27
|
+
devices?: DeviceRow[];
|
|
28
|
+
last?: EventRow | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const onOff = (value: boolean | null | undefined): string => value === false ? "off" : value === true ? "on" : "inherit";
|
|
32
|
+
|
|
33
|
+
function switchesLine(label: string, value: Switches): string {
|
|
34
|
+
const parts: string[] = [];
|
|
35
|
+
if (value.model !== undefined) parts.push("model " + onOff(value.model));
|
|
36
|
+
if (value.provider !== undefined) parts.push("provider " + onOff(value.provider));
|
|
37
|
+
return terminalSafeText(label) + ": " + parts.join(", ");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function fallbackStatusLines(state: FallbackState): string[] {
|
|
41
|
+
const lines = [
|
|
42
|
+
"model fallback: " + (state.global?.model === false ? "off" : "on") + " (global)",
|
|
43
|
+
"provider fallback: " + (state.global?.provider === false ? "off" : "on") + " (global)",
|
|
44
|
+
];
|
|
45
|
+
for (const [name, value] of Object.entries(state.providers ?? {})) lines.push(switchesLine("provider " + name, value));
|
|
46
|
+
for (const [name, value] of Object.entries(state.routes ?? {})) lines.push(switchesLine("route " + name, value));
|
|
47
|
+
for (const device of state.devices ?? []) {
|
|
48
|
+
const label = "device " + (device.id ?? "?") + (device.alias ? " (" + device.alias + ")" : "");
|
|
49
|
+
lines.push(terminalSafeText(label) + ": local fallback " + (device.localFallback ? "allowed" : "off")
|
|
50
|
+
+ ", model " + onOff(device.model) + ", provider " + onOff(device.provider));
|
|
51
|
+
}
|
|
52
|
+
const last = state.last;
|
|
53
|
+
lines.push(last
|
|
54
|
+
? terminalSafeText("last: " + last.at + " " + last.outcome + " " + last.kind + " fallback on " + last.route + ": "
|
|
55
|
+
+ last.from + " -> " + last.to + " (" + last.trigger + ", " + last.scope + ")")
|
|
56
|
+
: "last: no fallback since this proxy started");
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function takeScope(args: string[]): { scope: "global" | "provider" | "route" | "device"; key?: string } {
|
|
61
|
+
const provider = takeOption(args, "--provider");
|
|
62
|
+
const route = takeOption(args, "--route");
|
|
63
|
+
const device = takeOption(args, "--device");
|
|
64
|
+
const given = [provider, route, device].filter(value => value !== undefined);
|
|
65
|
+
if (given.length > 1) throw new CliUsageError("use one of --provider, --route or --device", FALLBACK_USAGE);
|
|
66
|
+
if (provider !== undefined) return { scope: "provider", key: provider };
|
|
67
|
+
if (route !== undefined) return { scope: "route", key: route };
|
|
68
|
+
if (device !== undefined) return { scope: "device", key: device };
|
|
69
|
+
return { scope: "global" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function takeKinds(args: string[]): Array<"model" | "provider"> {
|
|
73
|
+
const kind = takeOption(args, "--kind") ?? "all";
|
|
74
|
+
if (kind === "all") return ["model", "provider"];
|
|
75
|
+
if (kind === "model" || kind === "provider") return [kind];
|
|
76
|
+
throw new CliUsageError("--kind must be model, provider or all", FALLBACK_USAGE);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function put(body: Record<string, unknown>, deps: RuntimeApiDeps): Promise<FallbackState> {
|
|
80
|
+
return await runtimeRequest<FallbackState>("/api/fallback", {
|
|
81
|
+
method: "PUT",
|
|
82
|
+
headers: { "content-type": "application/json" },
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
}, deps);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
88
|
+
const args = [...argv];
|
|
89
|
+
const wantsJson = takeFlag(args, "--json");
|
|
90
|
+
rejectArgs(args, FALLBACK_USAGE);
|
|
91
|
+
const state = await runtimeRequest<FallbackState>("/api/fallback", {}, deps);
|
|
92
|
+
printData(state, wantsJson, fallbackStatusLines(state));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function setSwitch(argv: string[], value: boolean | null, deps: RuntimeApiDeps): Promise<void> {
|
|
96
|
+
const args = [...argv];
|
|
97
|
+
const wantsJson = takeFlag(args, "--json");
|
|
98
|
+
const kinds = takeKinds(args);
|
|
99
|
+
const scope = takeScope(args);
|
|
100
|
+
rejectArgs(args, FALLBACK_USAGE);
|
|
101
|
+
const state = await put({ writes: kinds.map(setting => ({ ...scope, setting, value })) }, deps);
|
|
102
|
+
printData(state, wantsJson, fallbackStatusLines(state));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function local(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
106
|
+
const args = [...argv];
|
|
107
|
+
const wantsJson = takeFlag(args, "--json");
|
|
108
|
+
const mode = args.shift();
|
|
109
|
+
if (mode !== "on" && mode !== "off") throw new CliUsageError("fallback local needs on or off", FALLBACK_USAGE);
|
|
110
|
+
const device = takeOption(args, "--device");
|
|
111
|
+
if (!device) throw new CliUsageError("fallback local needs --device <id|alias>", FALLBACK_USAGE);
|
|
112
|
+
rejectArgs(args, FALLBACK_USAGE);
|
|
113
|
+
const state = await put({ scope: "device", key: device, setting: "localFallback", value: mode === "on" }, deps);
|
|
114
|
+
printData(state, wantsJson, fallbackStatusLines(state));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function handleFallbackCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
118
|
+
return runCliAction(async () => {
|
|
119
|
+
const [sub, ...rest] = argv;
|
|
120
|
+
if (sub === undefined || sub === "status") await status(rest, deps);
|
|
121
|
+
else if (sub === "off") await setSwitch(rest, false, deps);
|
|
122
|
+
else if (sub === "on") await setSwitch(rest, true, deps);
|
|
123
|
+
else if (sub === "clear") await setSwitch(rest, null, deps);
|
|
124
|
+
else if (sub === "local") await local(rest, deps);
|
|
125
|
+
else throw new CliUsageError("unknown fallback command: " + sub, FALLBACK_USAGE);
|
|
126
|
+
});
|
|
127
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -81,6 +81,7 @@ Usage:
|
|
|
81
81
|
ocx agent <sub> Subagents, injection, effort caps, and sidecars
|
|
82
82
|
ocx message <sub> Codex queued messages, explicit SSH peers and foreground ownership
|
|
83
83
|
ocx effort [sub] Inspect and configure reasoning effort caps and defaults
|
|
84
|
+
ocx fallback <sub> Turn model or provider fallback off per scope; per-device local fallback
|
|
84
85
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
|
85
86
|
ocx inspect <sub> Effective config, catalog, analytics, pacing, client-config
|
|
86
87
|
ocx route <sub> Routing features (combo, policy)
|
package/src/cli/index.ts
CHANGED
|
@@ -167,7 +167,7 @@ import { startClientRuntimeUnderOwnershipLease } from "./client-start-fence";
|
|
|
167
167
|
import { recoverStartStateUnderOwnershipLease } from "./start-owner-fence";
|
|
168
168
|
import { handledSignalExitCode } from "../lib/handled-signal-exit";
|
|
169
169
|
import { createLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
170
|
-
import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
|
|
170
|
+
import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS, RESTART_REOBSERVE_RESERVE_MS } from "../lib/system-restart-contract";
|
|
171
171
|
import { startServiceLogging } from "../service/logging";
|
|
172
172
|
|
|
173
173
|
/**
|
|
@@ -941,7 +941,6 @@ async function handleTrayProxyStart(existingIsSuccess = true): Promise<boolean>
|
|
|
941
941
|
const PROXY_RESTART_OBSERVE_MS = MEMORY_DRAIN_RESTART_MS + REPLACEMENT_READY_TIMEOUT_MS + 15_000;
|
|
942
942
|
|
|
943
943
|
/** Reserve confirmation time within the shared restart deadline. */
|
|
944
|
-
const RESTART_REOBSERVE_RESERVE_MS = 10_000;
|
|
945
944
|
async function handleProxyRestart(
|
|
946
945
|
startWhenStopped: (recoveringLiveRestart: boolean) => Promise<ProxyRestartStartOutcome>,
|
|
947
946
|
): Promise<boolean> {
|