@bitkyc08/opencodex 2.87.0 → 2.89.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 +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-BnRSujOq.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/index-uchD-ngf.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +86 -48
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +52 -30
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/passthrough.ts +17 -12
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +17 -3
- package/src/bridge/response-json.ts +4 -4
- package/src/bridge/sse.ts +17 -10
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +18 -5
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/cross-home-owner.ts +16 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +30 -3
- package/src/codex/auth-api/account-list.ts +3 -0
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-context.ts +9 -4
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-gather.ts +3 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/combos/failover.ts +2 -1
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/opencode-zen-upgrade.ts +9 -8
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +411 -175
- package/src/images/loop.ts +101 -27
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/request-failure-model.ts +2 -0
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/generic-account-failover.ts +7 -4
- package/src/oauth/index.ts +5 -35
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +5 -0
- package/src/providers/key-failover.ts +11 -11
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +12 -73
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +39 -6
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +3 -0
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +3 -2
- package/src/providers/registry/entries-extended.ts +8 -52
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/responses/web-search-replay.ts +138 -0
- package/src/routing/analytics.ts +7 -15
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +28 -13
- package/src/server/claude-messages.ts +4 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +10 -1
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +34 -7
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +7 -2
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +104 -29
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +6 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +9 -3
- package/src/server/responses/core-combo.ts +34 -7
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +30 -33
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/request-transport.ts +32 -3
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +59 -10
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/definition.ts +9 -1
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +30 -4
- package/src/types/wire.ts +4 -2
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +16 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +212 -73
- package/src/web-search/progress-stream.ts +14 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/index-CfYDMj34.js +0 -94
|
@@ -2,13 +2,16 @@ import { MessageBudget } from "../messaging/budget";
|
|
|
2
2
|
import { readMessageInput } from "../messaging/input";
|
|
3
3
|
import { localSessions, messageFailure, sendLocalMessage } from "../messaging/send";
|
|
4
4
|
import { parseMessageArgs } from "./message-args";
|
|
5
|
+
import { parseRemoteMessageArgs } from "./message-remote-args";
|
|
5
6
|
import { terminalSafeText } from "./runtime-api";
|
|
6
7
|
|
|
7
8
|
/** Run one local messaging operation after pure syntax validation, then release owned resources. */
|
|
8
9
|
export async function runMessageCommand(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
|
10
|
+
const remote = parseRemoteMessageArgs(argv);
|
|
11
|
+
if (remote) return (await import("./message-remote-command")).runRemoteMessageCommand(remote, env);
|
|
9
12
|
const args = parseMessageArgs(argv);
|
|
10
13
|
if (!args) {
|
|
11
|
-
console.error("Usage: ocx message sessions [--json] | send (--thread <uuid> | --name <exact-name>) --stdin [--kind request|response|notification] [--in-reply-to <uuid>] [--json]");
|
|
14
|
+
console.error("Usage: ocx message sessions [--host <peer>] [--json] | send (--thread <uuid> | --name <exact-name>) --stdin [--host <peer>] [--kind request|response|notification] [--in-reply-to <uuid>] [--json] | enable [--port <port>] | disable | status | hosts <probe|add|list|remove|abandon|allow-inbound|deny-inbound> | serve [--host <peer>]...");
|
|
12
15
|
return 64;
|
|
13
16
|
}
|
|
14
17
|
const controller = new AbortController();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { validAlias, validPort, REMOTE_LIMITS } from "../messaging/remote-contract";
|
|
2
|
+
import { isSshAlias } from "../link/ssh-argv";
|
|
3
|
+
import { parseMessageArgs, type MessageArgs } from "./message-args";
|
|
4
|
+
import { isThreadId } from "../messaging/types";
|
|
5
|
+
|
|
6
|
+
export type RemoteMessageArgs = { action: "enable"; port?: number; json: boolean }
|
|
7
|
+
| { action: "disable" | "status" | "hosts-list" | "_control" | "_port"; json: boolean }
|
|
8
|
+
| { action: "serve"; hosts: string[]; json: boolean }
|
|
9
|
+
| { action: "hosts-probe"; ssh: string; json: boolean }
|
|
10
|
+
| { action: "hosts-add"; alias: string; ssh: string; fingerprint: string; json: boolean }
|
|
11
|
+
| { action: "hosts-remove"; host: string; transaction?: string; json: boolean }
|
|
12
|
+
| { action: "hosts-allow-inbound" | "hosts-deny-inbound"; host: string; json: boolean }
|
|
13
|
+
| { action: "hosts-abandon"; transaction: string; json: boolean }
|
|
14
|
+
| { action: "remote-operation"; host: string; local: MessageArgs; json: boolean };
|
|
15
|
+
|
|
16
|
+
/** Pure remote syntax validation precedes state, home selection, listeners or SSH allocation. */
|
|
17
|
+
export function parseRemoteMessageArgs(argv: readonly string[]): RemoteMessageArgs | null {
|
|
18
|
+
let action = argv[0], start = 1, positional: string | undefined;
|
|
19
|
+
if (action === "sessions" || action === "send") {
|
|
20
|
+
const hostIndex = argv.indexOf("--host");
|
|
21
|
+
if (hostIndex < 0 || argv.lastIndexOf("--host") !== hostIndex || !validAlias(argv[hostIndex + 1])) return null;
|
|
22
|
+
const local = parseMessageArgs([...argv.slice(0, hostIndex), ...argv.slice(hostIndex + 2)]);
|
|
23
|
+
return local ? { action: "remote-operation", host: argv[hostIndex + 1]!, local, json: local.json } : null;
|
|
24
|
+
}
|
|
25
|
+
if (action === "hosts") {
|
|
26
|
+
if (!["list", "probe", "add", "remove", "abandon", "allow-inbound", "deny-inbound"].includes(argv[1] ?? "")) return null;
|
|
27
|
+
action = `hosts-${argv[1]}`; start = 2;
|
|
28
|
+
if (["hosts-add", "hosts-remove", "hosts-allow-inbound", "hosts-deny-inbound"].includes(action)) {
|
|
29
|
+
positional = argv[start++]; if (!validAlias(positional)) return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const seen = new Set<string>(), flags: Record<string, string> = {}, hosts: string[] = [];
|
|
33
|
+
for (let index = start; index < argv.length; index++) {
|
|
34
|
+
const flag = argv[index]!;
|
|
35
|
+
if (seen.has(flag) && !(action === "serve" && flag === "--host")) return null;
|
|
36
|
+
seen.add(flag);
|
|
37
|
+
if (flag === "--json") continue;
|
|
38
|
+
const allowed = action === "enable" ? ["--port"] : action === "serve" ? ["--host"]
|
|
39
|
+
: action === "hosts-probe" ? ["--ssh"] : action === "hosts-add" ? ["--ssh", "--fingerprint"]
|
|
40
|
+
: action === "hosts-abandon" || action === "hosts-remove" ? ["--transaction"] : [];
|
|
41
|
+
if (!allowed.includes(flag)) return null;
|
|
42
|
+
const value = argv[++index]; if (!value || value.startsWith("--")) return null;
|
|
43
|
+
flags[flag] = value; if (flag === "--host") hosts.push(value);
|
|
44
|
+
}
|
|
45
|
+
const json = seen.has("--json");
|
|
46
|
+
if (action === "enable") {
|
|
47
|
+
if (flags["--port"] !== undefined && (!/^\d{4,5}$/.test(flags["--port"]) || !validPort(Number(flags["--port"])))) return null;
|
|
48
|
+
return { action, json, ...(flags["--port"] ? { port: Number(flags["--port"]) } : {}) };
|
|
49
|
+
}
|
|
50
|
+
if (action === "serve") return hosts.length <= REMOTE_LIMITS.peers && hosts.every(validAlias)
|
|
51
|
+
&& new Set(hosts).size === hosts.length ? { action, hosts, json } : null;
|
|
52
|
+
if (["disable", "status", "hosts-list", "_control", "_port"].includes(action ?? "")) {
|
|
53
|
+
return { action: action as "disable", json };
|
|
54
|
+
}
|
|
55
|
+
if (action === "hosts-remove") {
|
|
56
|
+
const transaction = flags["--transaction"];
|
|
57
|
+
if (transaction !== undefined && !isThreadId(transaction)) return null;
|
|
58
|
+
return { action, host: positional!, json, ...(transaction !== undefined ? { transaction } : {}) };
|
|
59
|
+
}
|
|
60
|
+
if (action === "hosts-allow-inbound" || action === "hosts-deny-inbound") return { action, host: positional!, json };
|
|
61
|
+
if (action === "hosts-abandon" && isThreadId(flags["--transaction"])) return { action, transaction: flags["--transaction"]!, json };
|
|
62
|
+
if ((action === "hosts-probe" || action === "hosts-add") && flags["--ssh"] && isSshAlias(flags["--ssh"])) {
|
|
63
|
+
if (action === "hosts-probe") return { action, ssh: flags["--ssh"], json };
|
|
64
|
+
if (/^SHA256:[A-Za-z0-9+/=]{1,64}$/.test(flags["--fingerprint"] ?? "")) return {
|
|
65
|
+
action, alias: positional!, ssh: flags["--ssh"], fingerprint: flags["--fingerprint"], json,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getConfigDir } from "../config/paths";
|
|
2
|
+
import { MessageBudget } from "../messaging/budget";
|
|
3
|
+
import { readMessageInput } from "../messaging/input";
|
|
4
|
+
import { RemoteCapacity } from "../messaging/remote-contract";
|
|
5
|
+
import { enrollRemoteHost, handleEnrollmentControl, probeRemoteHost, removeRemoteHost } from "../messaging/remote-enrollment";
|
|
6
|
+
import { abandonRemoteEnrollment, pendingRemoteEnrollment } from "../messaging/remote-enrollment-recovery";
|
|
7
|
+
import { remoteControl } from "../messaging/remote-auth";
|
|
8
|
+
import { ownerEndpoint, startRemoteOwner } from "../messaging/remote-owner";
|
|
9
|
+
import { remotePortCandidate } from "../messaging/remote-ports";
|
|
10
|
+
import { remoteSessions, sendRemoteMessage } from "../messaging/remote-send";
|
|
11
|
+
import { RemoteMessageStore } from "../messaging/remote-store";
|
|
12
|
+
import { messageFailure } from "../messaging/send";
|
|
13
|
+
import { messageCodexHome } from "./message-runtime";
|
|
14
|
+
import { terminalSafeText } from "./runtime-api";
|
|
15
|
+
import type { RemoteMessageArgs } from "./message-remote-args";
|
|
16
|
+
|
|
17
|
+
/** Activate remote resources only after an explicit remote command's pure parser succeeds. */
|
|
18
|
+
export async function runRemoteMessageCommand(args: RemoteMessageArgs, env: NodeJS.ProcessEnv): Promise<number> {
|
|
19
|
+
const store = new RemoteMessageStore(getConfigDir()), controller = new AbortController(), capacity = new RemoteCapacity();
|
|
20
|
+
const cancel = () => controller.abort();
|
|
21
|
+
process.once("SIGINT", cancel); process.once("SIGTERM", cancel);
|
|
22
|
+
const emit = (value: unknown) => console.log(args.json || args.action.startsWith("_")
|
|
23
|
+
? JSON.stringify(value) : terminalSafeText(JSON.stringify(value)));
|
|
24
|
+
try {
|
|
25
|
+
if (args.action === "serve") {
|
|
26
|
+
const owner = await startRemoteOwner(store, messageCodexHome(env), args.hosts, controller.signal);
|
|
27
|
+
try {
|
|
28
|
+
emit({ protocol: "ocx-message-remote/1", running: true, port: owner.port });
|
|
29
|
+
await Promise.race([owner.finished, new Promise<void>(resolve => {
|
|
30
|
+
controller.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
31
|
+
if (controller.signal.aborted) resolve();
|
|
32
|
+
})]);
|
|
33
|
+
if (!controller.signal.aborted) { emit({ running: false, error: { code: "owner_retired", message: "Foreground messaging owner retired; restart explicitly after resolving its route/configuration failure." } }); return 1; }
|
|
34
|
+
} finally { await owner.close(); }
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const budget = new MessageBudget(30000, controller.signal);
|
|
38
|
+
try {
|
|
39
|
+
if (args.action === "enable") { store.enable(args.port ?? store.read()?.port ?? 39176); emit(store.publicState()); }
|
|
40
|
+
else if (args.action === "disable") { store.disable(); emit(store.publicState()); }
|
|
41
|
+
else if (args.action === "hosts-list") emit({ ...store.publicState(), pendingEnrollment: pendingRemoteEnrollment(store) });
|
|
42
|
+
else if (args.action === "hosts-abandon") { emit(abandonRemoteEnrollment(store, args.transaction)); return 3; }
|
|
43
|
+
else if (args.action === "status") {
|
|
44
|
+
const state = store.read(); let running = false, routes: unknown = [];
|
|
45
|
+
if (state?.enabled) {
|
|
46
|
+
try { const result = await remoteControl(state, ownerEndpoint(state), "message/routes", {}, budget);
|
|
47
|
+
running = true; routes = result; } catch { /* Disabled/offline is not activation or repair. */ }
|
|
48
|
+
}
|
|
49
|
+
emit({ ...store.publicState(), running, routes, pendingEnrollment: pendingRemoteEnrollment(store) });
|
|
50
|
+
} else if (args.action === "hosts-probe") emit({ ssh: args.ssh, fingerprint: (await probeRemoteHost(args.ssh, budget, capacity)).fingerprint });
|
|
51
|
+
else if (args.action === "hosts-add") emit(await enrollRemoteHost(store, args.alias, args.ssh, args.fingerprint, budget, capacity));
|
|
52
|
+
else if (args.action === "hosts-allow-inbound" || args.action === "hosts-deny-inbound") {
|
|
53
|
+
emit(store.setInbound(args.host, args.action === "hosts-allow-inbound"));
|
|
54
|
+
}
|
|
55
|
+
else if (args.action === "hosts-remove") {
|
|
56
|
+
const receipt = await removeRemoteHost(store, args.host, budget, capacity, undefined, undefined, args.transaction); emit(receipt);
|
|
57
|
+
return receipt.remote === "removed" ? 0 : 3;
|
|
58
|
+
} else if (args.action === "_control") {
|
|
59
|
+
emit(handleEnrollmentControl(store, JSON.parse(await readMessageInput(Bun.stdin.stream(), budget))));
|
|
60
|
+
} else if (args.action === "_port") { store.requireEnabled(); emit({ port: remotePortCandidate() }); }
|
|
61
|
+
else if (args.action === "remote-operation") {
|
|
62
|
+
if (args.local.action === "sessions") emit(await remoteSessions(store, args.host, budget));
|
|
63
|
+
else {
|
|
64
|
+
const body = await readMessageInput(Bun.stdin.stream(), budget);
|
|
65
|
+
const receipt = await sendRemoteMessage(store, { ...args.local, host: args.host, body },
|
|
66
|
+
{ home: messageCodexHome(env), senderId: env.CODEX_THREAD_ID }, budget);
|
|
67
|
+
emit(receipt); return receipt.status === "queued" ? 0 : receipt.status === "unknown" ? 3 : 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
} finally { budget.dispose(); }
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Control errors never echo stdin, SSH output, keys or paths; a failed enrollment keeps its transaction.
|
|
74
|
+
const failure = messageFailure(error), unknown = ["enrollment_unknown", "remote_control_unknown", "remote_storage_unknown", "cleanup_incomplete"].includes(failure.code);
|
|
75
|
+
emit({ schema: "ocx-message-error/1", ...(unknown ? { status: "unknown" } : args.action === "remote-operation" && args.local.action === "send" ? { status: "not_sent" } : {}), error: failure }); return unknown ? 3 : 1;
|
|
76
|
+
} finally { process.removeListener("SIGINT", cancel); process.removeListener("SIGTERM", cancel); }
|
|
77
|
+
}
|
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
takeBooleanOption,
|
|
11
11
|
takeFlag,
|
|
12
12
|
takeOption,
|
|
13
|
+
terminalSafeText,
|
|
13
14
|
type RuntimeApiDeps,
|
|
14
15
|
} from "./runtime-api";
|
|
16
|
+
import { interactiveConfirm } from "./interactive-confirm";
|
|
15
17
|
import { takeProviderEditSettings } from "./provider-settings";
|
|
16
18
|
import { printProviderReceipt, runProviderAction } from "./provider-result";
|
|
17
19
|
import { providerQuotaLine } from "./account-extended";
|
|
@@ -46,7 +48,7 @@ const USAGE = `Usage:
|
|
|
46
48
|
[--retain-models <id,id|->] [--model <id> --text-only]
|
|
47
49
|
[--xai-chat <on|off>] [--upstream-http-version <http1.1|->] [--fast <on|off>] [--context-window <tokens|->]
|
|
48
50
|
[--allow-private-network <on|off>] [--model-context-tier <model=default|long_context>] [--json]
|
|
49
|
-
ocx provider test <name> [--json]
|
|
51
|
+
ocx provider test <name> [--model <id> [--yes]] [--json]
|
|
50
52
|
ocx provider quota [--refresh] [--json]
|
|
51
53
|
ocx provider resets [--limit <n>] [--json]
|
|
52
54
|
ocx provider presets [--json]
|
|
@@ -147,12 +149,78 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
147
149
|
return printProviderReceipt(result, wantsJson, "Provider edit");
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
interface CompletionTestDto {
|
|
153
|
+
applicable: boolean;
|
|
154
|
+
reason?: string;
|
|
155
|
+
detail?: string;
|
|
156
|
+
adapter?: string;
|
|
157
|
+
ok?: boolean;
|
|
158
|
+
wireModel?: string;
|
|
159
|
+
upstreamModel?: string | null;
|
|
160
|
+
modelComparison?: "match" | "mismatch" | "unknown";
|
|
161
|
+
comparisonNote?: "missing_metadata" | "conflicting_metadata";
|
|
162
|
+
ttftMs?: number | null;
|
|
163
|
+
totalLatencyMs?: number;
|
|
164
|
+
error?: { code: string; message: string; status?: number } | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function testCompletion(name: string, model: string, yes: boolean, wantsJson: boolean, deps: RuntimeApiDeps): Promise<number> {
|
|
168
|
+
const input = deps.stdinImpl ?? process.stdin;
|
|
169
|
+
if (!yes && (wantsJson || !input.isTTY)) {
|
|
170
|
+
throw new CliUsageError("completion tests consume quota and may incur cost; --yes is required with --json or non-interactive input", USAGE);
|
|
171
|
+
}
|
|
172
|
+
const warning = terminalSafeText(`${name}: completion test for model ${model}. Warning: this sends an upstream request, consumes quota and may incur cost.`);
|
|
173
|
+
if (wantsJson) console.error(warning);
|
|
174
|
+
else console.log(warning);
|
|
175
|
+
if (!yes && !await interactiveConfirm({
|
|
176
|
+
question: "Run this completion test?", defaultYes: false, input: input as NodeJS.ReadStream,
|
|
177
|
+
})) {
|
|
178
|
+
console.log("cancelled");
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
const result = await runtimeRequest<CompletionTestDto>(`/api/providers/test-completion?name=${encodeURIComponent(name)}`, {
|
|
182
|
+
method: "POST", redirect: "error", body: JSON.stringify({ model, confirm: true }),
|
|
183
|
+
}, deps);
|
|
184
|
+
if (result.applicable === false) {
|
|
185
|
+
printData(result, wantsJson, [
|
|
186
|
+
`${name}: not applicable — ${result.reason}`,
|
|
187
|
+
...(result.detail ? [`Detail: ${result.detail}`] : []),
|
|
188
|
+
...(result.adapter ? [`Adapter: ${result.adapter}`] : []),
|
|
189
|
+
]);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
if (result.ok === true) {
|
|
193
|
+
printData(result, wantsJson, [
|
|
194
|
+
`${name}: connected`,
|
|
195
|
+
`TTFT: ${result.ttftMs ?? "unknown"} ms`,
|
|
196
|
+
`Total latency: ${result.totalLatencyMs ?? "unknown"} ms`,
|
|
197
|
+
`Upstream reported ${result.upstreamModel ?? "unknown"} for ${result.wireModel} — ${result.modelComparison}${result.comparisonNote ? ` (${result.comparisonNote})` : ""}`,
|
|
198
|
+
"metadata only — not proof of model identity",
|
|
199
|
+
]);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
printData(result, wantsJson, [
|
|
203
|
+
`${name}: failed`,
|
|
204
|
+
`${result.error?.code ?? "unknown"}: ${result.error?.message ?? "Completion test failed"}${result.error?.status === undefined ? "" : ` (HTTP ${result.error.status})`}`,
|
|
205
|
+
]);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
150
209
|
async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
151
210
|
const args = [...argv];
|
|
152
211
|
const name = args.shift()?.trim();
|
|
153
212
|
const wantsJson = takeFlag(args, "--json");
|
|
154
213
|
if (!name) throw new CliUsageError("provider name is required", USAGE);
|
|
214
|
+
const model = takeOption(args, "--model")?.trim();
|
|
215
|
+
const yes = takeFlag(args, "--yes");
|
|
155
216
|
rejectArgs(args, USAGE);
|
|
217
|
+
if (yes && model === undefined) throw new CliUsageError("--yes requires --model", USAGE);
|
|
218
|
+
if (model !== undefined) {
|
|
219
|
+
if (!model || model.length > 256 || /[\x00-\x1f\x7f-\x9f]/.test(model)) {
|
|
220
|
+
throw new CliUsageError("--model must contain 1..256 characters without control characters", USAGE);
|
|
221
|
+
}
|
|
222
|
+
return testCompletion(name, model, yes, wantsJson, deps);
|
|
223
|
+
}
|
|
156
224
|
const result = await runtimeRequest<Record<string, unknown>>(`/api/providers/test?name=${encodeURIComponent(name)}`, {
|
|
157
225
|
method: "POST",
|
|
158
226
|
}, deps);
|
package/src/cli/registry.ts
CHANGED
|
@@ -16,9 +16,9 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: "message",
|
|
19
|
-
usage: "ocx message <sessions|send> [options]",
|
|
20
|
-
summary: "Discover loaded
|
|
21
|
-
details: ["
|
|
19
|
+
usage: "ocx message <sessions|send|enable|disable|status|hosts|serve> [options]",
|
|
20
|
+
summary: "Discover loaded Codex sessions and queue messages locally or through explicit SSH peers.",
|
|
21
|
+
details: ["Remote listeners/tunnels require explicit foreground serve; no daemon start. queued is submission, not processing; never replay an unknown send."],
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
name: "chatgpt",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AccountPauseDetails } from "../types/account-auto-pause";
|
|
1
3
|
import type { OcxConfig } from "../types";
|
|
2
4
|
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
3
5
|
|
|
@@ -7,7 +9,9 @@ export function isCodexAccountPaused(config: OcxConfig, accountId: string): bool
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/** Persist the account's pool eligibility without changing credentials or runtime health. */
|
|
10
|
-
export function setCodexAccountPaused(config: OcxConfig, accountId: string, paused: boolean): void {
|
|
12
|
+
export function setCodexAccountPaused(config: OcxConfig, accountId: string, paused: boolean, details?: AccountPauseDetails): void {
|
|
13
|
+
config.codexAccountPauseState = { ...config.codexAccountPauseState,
|
|
14
|
+
[accountId]: { revision: randomUUID(), ...(paused && details ? { details } : {}) } };
|
|
11
15
|
const pausedIds = new Set(config.pausedCodexAccountIds ?? []);
|
|
12
16
|
if (paused) pausedIds.add(accountId);
|
|
13
17
|
else pausedIds.delete(accountId);
|
|
@@ -18,4 +22,5 @@ export function setCodexAccountPaused(config: OcxConfig, accountId: string, paus
|
|
|
18
22
|
|
|
19
23
|
export function forgetCodexAccountPause(config: OcxConfig, accountId: string): void {
|
|
20
24
|
setCodexAccountPaused(config, accountId, false);
|
|
25
|
+
if (config.codexAccountPauseState) delete config.codexAccountPauseState[accountId];
|
|
21
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setDecisionAccountUsable } from "../lib/decision-quota-sink";
|
|
1
2
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
2
3
|
import { isCodexAccountGenerationLive } from "./account-store";
|
|
3
4
|
|
|
@@ -39,12 +40,14 @@ export function clearMainRefreshGrantRejection(key: string): void {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
/** Quarantine a Codex account (or one credential generation) after an auth failure, and mark its advisory decision-quota row unusable so routing evidence cannot treat it as healthy. */
|
|
42
44
|
export function markAccountNeedsReauth(
|
|
43
45
|
id: string,
|
|
44
46
|
writerGeneration = captureConfigGeneration(),
|
|
45
47
|
credentialGeneration?: number,
|
|
46
48
|
): void {
|
|
47
49
|
if (writerGeneration < lastReconciledGeneration && !liveAccountIds.has(id)) return;
|
|
50
|
+
setDecisionAccountUsable(id === "__main__" ? "codex-main" : "codex", id, false, credentialGeneration);
|
|
48
51
|
// An account-wide mark supersedes a generation-scoped one: it is the stronger claim.
|
|
49
52
|
if (credentialGeneration === undefined || !reauthAccounts.has(id)) {
|
|
50
53
|
reauthAccounts.set(id, credentialGeneration);
|
|
@@ -81,6 +84,7 @@ export function isAccountNeedsReauth(id: string): boolean {
|
|
|
81
84
|
return true;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/** Lift a reauth quarantine only when the proof matches the quarantined credential generation, and restore the advisory decision-quota row's usability. */
|
|
84
88
|
export function clearAccountNeedsReauth(id: string, credentialGeneration?: number): void {
|
|
85
89
|
// A model response proves only the credential it used. Keep account-wide
|
|
86
90
|
// quarantine and evidence from another generation intact.
|
|
@@ -88,4 +92,5 @@ export function clearAccountNeedsReauth(id: string, credentialGeneration?: numbe
|
|
|
88
92
|
&& (reauthAccounts.get(id) !== credentialGeneration
|
|
89
93
|
|| !isCodexAccountGenerationLive(id, credentialGeneration))) return;
|
|
90
94
|
reauthAccounts.delete(id);
|
|
95
|
+
setDecisionAccountUsable(id === "__main__" ? "codex-main" : "codex", id, true, credentialGeneration);
|
|
91
96
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
hardenExistingSecret,
|
|
15
15
|
withConfigMutationLockSync,
|
|
16
16
|
} from "../config";
|
|
17
|
+
import { notifyCodexStoreLoaded } from "../lib/decision-quota-sink";
|
|
17
18
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
18
19
|
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
19
20
|
import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
@@ -113,30 +114,56 @@ function normalizeRecord(value: CodexAccountCredentials | CodexAccountCredential
|
|
|
113
114
|
return undefined;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Observe-only read for the JEV quota activation owner: the same normalization as the loader,
|
|
119
|
+
* without hardening, invalid-file backup or slot notification. Absent or invalid stores read as empty.
|
|
120
|
+
*/
|
|
121
|
+
export function peekCodexAccountRecordStore(): Record<string, CodexAccountCredentialRecord> {
|
|
122
|
+
const path = codexAccountsPath();
|
|
123
|
+
if (!existsSync(path)) return {};
|
|
124
|
+
const normalized: Record<string, CodexAccountCredentialRecord> = {};
|
|
125
|
+
try {
|
|
126
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as RawCodexAccountStore;
|
|
127
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
128
|
+
const record = normalizeRecord(value);
|
|
129
|
+
if (record) normalized[id] = record;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Read the Codex account record store from disk with hardened permissions, and hand it to the opt-in decision-quota slot (a no-op unless JEV quota signals are active). */
|
|
116
138
|
function loadCodexAccountRecordStore(): CodexAccountStore {
|
|
117
139
|
const path = codexAccountsPath();
|
|
118
140
|
hardenConfigDir();
|
|
119
141
|
hardenExistingSecret(path);
|
|
120
|
-
if (!existsSync(path)) return {};
|
|
142
|
+
if (!existsSync(path)) { notifyCodexStoreLoaded({}); return {}; }
|
|
143
|
+
const normalized: CodexAccountStore = {};
|
|
121
144
|
try {
|
|
122
145
|
const raw = JSON.parse(readFileSync(path, "utf-8")) as RawCodexAccountStore;
|
|
123
|
-
const normalized: CodexAccountStore = {};
|
|
124
146
|
for (const [id, value] of Object.entries(raw)) {
|
|
125
147
|
const record = normalizeRecord(value);
|
|
126
148
|
if (record) normalized[id] = record;
|
|
127
149
|
}
|
|
128
|
-
return normalized;
|
|
129
150
|
} catch {
|
|
130
151
|
backupInvalidConfig(path);
|
|
152
|
+
notifyCodexStoreLoaded({});
|
|
131
153
|
return {};
|
|
132
154
|
}
|
|
155
|
+
// Outside the parse try: roster publication is advisory and must never route a loaded store into the invalid-config backup path.
|
|
156
|
+
notifyCodexStoreLoaded(normalized);
|
|
157
|
+
return normalized;
|
|
133
158
|
}
|
|
134
159
|
|
|
160
|
+
/** Atomically write the Codex account store, then hand it to the opt-in decision-quota slot so active advisory evidence tracks the persisted credentials. */
|
|
135
161
|
function persist(store: CodexAccountStore): void {
|
|
136
162
|
const dir = getConfigDir();
|
|
137
163
|
assertNotRealHomeUnderTest(dir);
|
|
138
164
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
139
165
|
atomicWriteFile(codexAccountsPath(), JSON.stringify(store, null, 2) + "\n");
|
|
166
|
+
notifyCodexStoreLoaded(store);
|
|
140
167
|
}
|
|
141
168
|
|
|
142
169
|
function persistCredentialMutation(store: CodexAccountStore): void {
|
|
@@ -141,6 +141,7 @@ export function poolAccountDto(
|
|
|
141
141
|
logLabel: codexAccountLogLabel(account),
|
|
142
142
|
isMain: false,
|
|
143
143
|
paused,
|
|
144
|
+
...(config.codexAccountPauseState?.[account.id]?.details ? { pauseDetails: config.codexAccountPauseState[account.id]!.details } : {}),
|
|
144
145
|
priority,
|
|
145
146
|
autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(config, account.id),
|
|
146
147
|
creditsAfterLimit: codexAccountUsesCreditsAfterLimit(config, account.id),
|
|
@@ -167,6 +168,7 @@ export interface CodexAuthAccountDto {
|
|
|
167
168
|
logLabel?: string;
|
|
168
169
|
isMain: boolean;
|
|
169
170
|
paused: boolean;
|
|
171
|
+
pauseDetails?: import("../../types/account-auto-pause").AccountPauseDetails;
|
|
170
172
|
/** Selection order; higher is used earlier. Always present, 0 when unset. */
|
|
171
173
|
priority: number;
|
|
172
174
|
/** Null inherits the global usage-switch threshold; 0 disables it for this account. */
|
|
@@ -403,6 +405,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
403
405
|
logLabel: "main",
|
|
404
406
|
isMain: true,
|
|
405
407
|
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
408
|
+
...(runtimeConfig.codexAccountPauseState?.[MAIN_CODEX_ACCOUNT_ID]?.details ? { pauseDetails: runtimeConfig.codexAccountPauseState[MAIN_CODEX_ACCOUNT_ID]!.details } : {}),
|
|
406
409
|
mainAccountHardLock: { ...getMainAccountHardLockStatus(runtimeConfig),
|
|
407
410
|
externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
|
|
408
411
|
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
@@ -6,7 +6,7 @@ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../.
|
|
|
6
6
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
7
7
|
import { isSelectableCodexPoolAccount } from "../account-id";
|
|
8
8
|
import type { OcxConfig } from "../../types";
|
|
9
|
-
import { parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
|
|
9
|
+
import { isValidWhamHistoryObservation, parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
|
|
10
10
|
import type { StoredAccountQuota, WhamUsageResponse } from "../quota";
|
|
11
11
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
12
12
|
import { getMainChatgptAccountId, readCodexTokensResult } from "../auth-collision";
|
|
@@ -278,6 +278,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
278
278
|
let quotaPhase: "request" | "body" | "decode" | "publish" = "request";
|
|
279
279
|
let quotaRefreshGeneration = captureMainAccountIdentityGeneration();
|
|
280
280
|
const readState: { owner?: CodexUsageOwner<MainAccountInfoFetchResult>; usable: boolean } = { usable: false };
|
|
281
|
+
/** Perform one single-flight main-account usage read: classify auth failures and publish parsed quota and decision evidence only while the dispatch and credential are still current. */
|
|
281
282
|
const read = async (): Promise<MainAccountInfoFetchResult> => {
|
|
282
283
|
try {
|
|
283
284
|
let dispatchSequence = 0;
|
|
@@ -398,7 +399,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
398
399
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
399
400
|
setMainAccountPlan(result.plan);
|
|
400
401
|
if (result.quota) {
|
|
401
|
-
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota);
|
|
402
|
+
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota, undefined, false, false, isValidWhamHistoryObservation(usage) ? quota : null);
|
|
402
403
|
}
|
|
403
404
|
publishQuotaDispatch(dispatchSequence);
|
|
404
405
|
readState.usable = quota !== null;
|
|
@@ -69,7 +69,7 @@ import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
|
69
69
|
import { maskAccountId } from "../lib/privacy";
|
|
70
70
|
import { formatErrorResponse, formatUsageLimitReachedResponse } from "../bridge";
|
|
71
71
|
import { nextCodexPoolRecoveryAt } from "./routing/pool-recovery";
|
|
72
|
-
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
|
|
72
|
+
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, isValidWhamHistoryObservation, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
|
|
73
73
|
import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "./account-credit-use";
|
|
74
74
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
75
75
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
@@ -665,6 +665,7 @@ function assertReserveAdmission(config: CodexAuthPolicyConfig): void {
|
|
|
665
665
|
}
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
/** Authorize a reserve request for the main credential, re-checking admission and writer liveness, and publish the ordinary quota observed during the capability read. */
|
|
668
669
|
async function authorizeReserveCredential(
|
|
669
670
|
token: { accessToken: string; chatgptAccountId: string },
|
|
670
671
|
writer: MainQuotaWriter | undefined,
|
|
@@ -681,9 +682,11 @@ async function authorizeReserveCredential(
|
|
|
681
682
|
const authorization = isMainReserveAuthorizationLive(existing, token) ? existing
|
|
682
683
|
: await getMainReserveAuthorization({
|
|
683
684
|
token, writer, signal,
|
|
685
|
+
/** Publish the ordinary usage observed during the reserve capability read: parse it once and reuse that observation as the advisory decision evidence when it is a valid history observation. */
|
|
684
686
|
observeOrdinaryQuota(data, capturedWriter) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
+
const parsed = parseUsageQuota(data);
|
|
688
|
+
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, parsed, writerGeneration,
|
|
689
|
+
capturedWriter, parseMainPolicyUsageQuota(data), undefined, false, false, isValidWhamHistoryObservation(data) ? parsed : null);
|
|
687
690
|
},
|
|
688
691
|
});
|
|
689
692
|
// The capability read also publishes ordinary quota. A new 99% reading or cooldown wins.
|
|
@@ -769,8 +772,10 @@ export function createCodexAuthDispatchGuard(
|
|
|
769
772
|
terminalHelper = false,
|
|
770
773
|
): ((headers: Headers) => void) | undefined {
|
|
771
774
|
const reserveGuard = createCodexReserveDispatchGuard(ctx, config, modelId, admission, terminalHelper);
|
|
772
|
-
if (ctx.kind !== "pool") return reserveGuard;
|
|
775
|
+
if (ctx.kind !== "pool" && ctx.kind !== "main-pool") return reserveGuard;
|
|
773
776
|
return headers => {
|
|
777
|
+
if (config.pausedCodexAccountIds?.includes(ctx.accountId)) throw new CodexPoolAuthenticationError("Selected Codex account is paused; resume it in account settings");
|
|
778
|
+
if (ctx.kind === "main-pool") { reserveGuard?.(headers); return; }
|
|
774
779
|
assertPoolAccountCredits(ctx.accountId, poolContextCreditHoldResetAt(ctx, config));
|
|
775
780
|
reserveGuard?.(headers);
|
|
776
781
|
};
|
|
@@ -195,6 +195,7 @@ export function deriveComboCatalogModel(
|
|
|
195
195
|
...(combo.alias ? { alias: combo.alias } : {}),
|
|
196
196
|
...(combo.nativeAlias ? { nativeAlias: true } : {}),
|
|
197
197
|
...(combo.displayName ? { displayName: combo.displayName } : {}),
|
|
198
|
+
...(combo.description ? { description: combo.description } : {}),
|
|
198
199
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
199
200
|
...(members.every(member => member.parallelToolCalls === true)
|
|
200
201
|
? { parallelToolCalls: true }
|
|
@@ -401,6 +402,20 @@ export function exactComboCatalogSlugs(
|
|
|
401
402
|
}));
|
|
402
403
|
}
|
|
403
404
|
|
|
405
|
+
/** Current combo row descriptions by public catalog slug, for retained-row re-projection (#7229). */
|
|
406
|
+
export function comboCatalogDescriptions(
|
|
407
|
+
config: Pick<OcxConfig, "combos" | "disabledModels">,
|
|
408
|
+
): Map<string, string> {
|
|
409
|
+
const current = exactComboCatalogSlugs(config);
|
|
410
|
+
return new Map(listComboIds(config).flatMap(id => {
|
|
411
|
+
const raw = config.combos?.[id];
|
|
412
|
+
const alias = typeof raw?.alias === "string" ? raw.alias.trim() : "";
|
|
413
|
+
const publicSlug = alias || comboModelId(id);
|
|
414
|
+
const description = typeof raw?.description === "string" ? raw.description.trim() : "";
|
|
415
|
+
return description && current.has(publicSlug) ? [[publicSlug, description] as const] : [];
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
|
|
404
419
|
export function normalizedOpenAiApiSignature(model: CatalogModel): string {
|
|
405
420
|
const normalized = {
|
|
406
421
|
provider: model.provider,
|
|
@@ -5,6 +5,7 @@ import { encodeRoutedModelId } from "../../providers/slug-codec";
|
|
|
5
5
|
import { canonicalAutoReviewModelKey, isValidAutoReviewModel as isValidAutoReviewTarget } from "../../config/provider-validation";
|
|
6
6
|
import { readConfiguredAutoReviewModel } from "./parsing";
|
|
7
7
|
import type { RawEntry } from "./parsing";
|
|
8
|
+
import { hasOcxRoutedAuthorship } from "./ownership";
|
|
8
9
|
import { configuredCatalogEntry } from "./subagent-roster";
|
|
9
10
|
|
|
10
11
|
const AUTO_REVIEW_ROOT_MARKER = "opencodex_auto_review_root";
|
|
@@ -38,7 +39,7 @@ function isRoutedCatalogEntry(entry: RawEntry): boolean {
|
|
|
38
39
|
if (isCodexControlPlaneModel(entry.slug)) return false;
|
|
39
40
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
40
41
|
return slug.includes("/")
|
|
41
|
-
|| (
|
|
42
|
+
|| hasOcxRoutedAuthorship(entry);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/** Restore an owned native value, retaining provenance to avoid legacy reclassification. */
|
|
@@ -4,6 +4,7 @@ import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasonin
|
|
|
4
4
|
import { clearModelCache } from "../model-cache";
|
|
5
5
|
import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
|
|
6
6
|
import { COMBO_NAMESPACE } from "../../combos";
|
|
7
|
+
import { hasOcxComboAuthorship, hasOcxRoutedAuthorship, reapplyRetainedComboDescription, ROUTED_DESCRIPTION_PREFIX } from "./ownership";
|
|
7
8
|
import {
|
|
8
9
|
CODEX_CUSTOM_MODEL_CATALOG_KIND,
|
|
9
10
|
CODEX_PROVIDER_MODEL_CATALOG_KIND,
|
|
@@ -218,7 +219,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
218
219
|
const routed = deriveEntry(
|
|
219
220
|
template,
|
|
220
221
|
slug,
|
|
221
|
-
|
|
222
|
+
`${ROUTED_DESCRIPTION_PREFIX}${nativeAlias.provider} (${nativeAlias.owned_by ?? nativeAlias.provider}).`,
|
|
222
223
|
5,
|
|
223
224
|
nativeAlias,
|
|
224
225
|
exactComboSlugs,
|
|
@@ -276,7 +277,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
276
277
|
const e = deriveEntry(
|
|
277
278
|
template,
|
|
278
279
|
slug,
|
|
279
|
-
|
|
280
|
+
`${ROUTED_DESCRIPTION_PREFIX}${m.provider} (${m.owned_by ?? m.provider}).`,
|
|
280
281
|
5,
|
|
281
282
|
m,
|
|
282
283
|
exactComboSlugs,
|
|
@@ -380,15 +381,15 @@ export function orderForModelPicker(
|
|
|
380
381
|
* True when an existing catalog row was authored by OpenCodex routing (#855).
|
|
381
382
|
* Every generated routed row — current full-slug form, the June–July 2026
|
|
382
383
|
* provider-name form, and legacy combo aliases — carries the stable
|
|
383
|
-
* description prefix `Routed via opencodex →
|
|
384
|
+
* description prefix `Routed via opencodex → `, or — when a combo overrides its description
|
|
385
|
+
* (#7229) — the `opencodex_routed_origin` marker (./ownership); foreign rows from Cursor or
|
|
384
386
|
* user tooling do not. `owned_by` describes upstream ownership, and `comp_hash`
|
|
385
387
|
* describes history compatibility; neither is an authorship signal.
|
|
386
388
|
*/
|
|
387
389
|
export function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
388
390
|
if (isNativeAliasCatalogEntry(entry)) return true;
|
|
389
|
-
const desc = typeof entry.description === "string" ? entry.description : "";
|
|
390
391
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
391
|
-
return slug.includes("/") &&
|
|
392
|
+
return slug.includes("/") && hasOcxRoutedAuthorship(entry);
|
|
392
393
|
}
|
|
393
394
|
|
|
394
395
|
function recoverableNativeSlug(entry: RawEntry): string | null {
|
|
@@ -503,6 +504,11 @@ export interface ObservedCatalogMergeInput {
|
|
|
503
504
|
readonly multiAgentV2Enabled: boolean;
|
|
504
505
|
readonly keepNativeChatGptOnV1?: boolean;
|
|
505
506
|
readonly exactComboSlugs: ReadonlySet<string>;
|
|
507
|
+
/**
|
|
508
|
+
* Current combo row descriptions by public slug (#7229). When supplied, retained combo rows
|
|
509
|
+
* are re-projected onto it; absent means the caller has no config and leaves them unchanged.
|
|
510
|
+
*/
|
|
511
|
+
readonly comboDescriptions?: ReadonlyMap<string, string>;
|
|
506
512
|
readonly hasPhysicalComboProvider: boolean;
|
|
507
513
|
readonly includeNativeOpenAi: boolean;
|
|
508
514
|
readonly accountBoundEntries: readonly RawEntry[];
|
|
@@ -545,6 +551,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
545
551
|
multiAgentV2Enabled,
|
|
546
552
|
keepNativeChatGptOnV1,
|
|
547
553
|
exactComboSlugs,
|
|
554
|
+
comboDescriptions,
|
|
548
555
|
hasPhysicalComboProvider,
|
|
549
556
|
includeNativeOpenAi,
|
|
550
557
|
accountBoundEntries,
|
|
@@ -639,8 +646,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
639
646
|
}
|
|
640
647
|
const freshExactComboEntries = new Set(admittedRoutedEntries.filter(entry => (
|
|
641
648
|
isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
642
|
-
&&
|
|
643
|
-
&& entry.description.startsWith(`Routed via opencodex → ${COMBO_NAMESPACE} (`)
|
|
649
|
+
&& hasOcxComboAuthorship(entry)
|
|
644
650
|
)));
|
|
645
651
|
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
|
|
646
652
|
const freshEquivalentKeys = new Set(admittedRoutedEntries.flatMap(entry => (
|
|
@@ -802,6 +808,10 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
802
808
|
delete entry[SPAWN_PRIORITY_FIELD];
|
|
803
809
|
}
|
|
804
810
|
const slug = String(entry.slug);
|
|
811
|
+
if (comboDescriptions !== undefined && exactComboSlugs.has(slug)
|
|
812
|
+
&& (isNativeAliasCatalogEntry(entry) || (entry.owned_by === COMBO_NAMESPACE && hasOcxComboAuthorship(entry)))) {
|
|
813
|
+
reapplyRetainedComboDescription(entry, comboDescriptions.get(slug));
|
|
814
|
+
}
|
|
805
815
|
if (!isOcxAuthoredRoutedEntry(entry)) continue;
|
|
806
816
|
// The builder no longer copies a template's comp_hash onto routed rows (#5796), but a row
|
|
807
817
|
// kept from disk may still carry one. Custom rows, Codex-forward aliases included, never
|