@bitkyc08/opencodex 2.79.0 → 2.81.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 +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -4
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +10 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +6 -3
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +347 -199
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +16 -16
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-scope.ts +4 -1
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +21 -18
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/desktop-startup.ts +100 -3
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -10
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-BOUa-ek3.js +0 -54
- package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
- package/gui/dist/assets/index-DwHNDF4s.js +0 -94
- package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { MessageBudget } from "./budget";
|
|
2
|
+
import { localSocket, type LocalSocket } from "./socket";
|
|
3
|
+
import { isRecord, isThreadId, LocalMessagingError, type LocalMetadataClient, type LocalThread } from "./types";
|
|
4
|
+
|
|
5
|
+
type Method = "initialize" | "thread/loaded/list" | "thread/read" | "thread/queue/add";
|
|
6
|
+
interface QueueResult {
|
|
7
|
+
status: "not_sent" | "queued" | "unknown";
|
|
8
|
+
error?: { code: string; message: string };
|
|
9
|
+
}
|
|
10
|
+
/** Only a validated server rejection can establish non-submission after a write. */
|
|
11
|
+
class QueueRejection extends LocalMessagingError {}
|
|
12
|
+
interface Pending {
|
|
13
|
+
resolve(value: unknown): void;
|
|
14
|
+
reject(error: Error): void;
|
|
15
|
+
timer: ReturnType<typeof setTimeout>;
|
|
16
|
+
method: Method;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Metadata discovery plus one text-only queue method; no lifecycle/turn/permission mutation. */
|
|
20
|
+
export class LocalMessageRpc implements LocalMetadataClient {
|
|
21
|
+
private nextId = 0;
|
|
22
|
+
private readonly pending = new Map<number, Pending>();
|
|
23
|
+
private closed = false;
|
|
24
|
+
private readonly abort: () => void;
|
|
25
|
+
|
|
26
|
+
/** Bind connection failure and operation cancellation to all pending requests. */
|
|
27
|
+
private constructor(private readonly socket: LocalSocket, private readonly budget: MessageBudget,
|
|
28
|
+
private readonly rpcTimeoutMs: number) {
|
|
29
|
+
this.abort = () => this.close(budget.signal.reason);
|
|
30
|
+
socket.onmessage = event => this.receive(event.data);
|
|
31
|
+
socket.onerror = socket.onclose = () => this.close();
|
|
32
|
+
budget.signal.addEventListener("abort", this.abort, { once: true });
|
|
33
|
+
if (budget.signal.aborted) this.abort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Connect and initialize an existing local daemon within budget; never start or repair one. */
|
|
37
|
+
static async connect(url: string, budget: MessageBudget, rpcTimeoutMs = 10_000): Promise<LocalMessageRpc> {
|
|
38
|
+
budget.throwIfEnded();
|
|
39
|
+
if (!Number.isInteger(rpcTimeoutMs) || rpcTimeoutMs <= 0 || rpcTimeoutMs > 10_000) {
|
|
40
|
+
throw new LocalMessagingError("invalid_budget", "RPC timeout must be between 1 and 10000 milliseconds.");
|
|
41
|
+
}
|
|
42
|
+
const socket = localSocket(url);
|
|
43
|
+
try {
|
|
44
|
+
await new Promise<void>((resolve, reject) => {
|
|
45
|
+
const cleanup = () => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
budget.signal.removeEventListener("abort", abort);
|
|
48
|
+
socket.onopen = socket.onerror = socket.onclose = null;
|
|
49
|
+
};
|
|
50
|
+
const abort = () => { cleanup(); reject(budget.signal.reason); };
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
cleanup(); reject(new LocalMessagingError("daemon_unavailable", "Local Codex connection timed out; no daemon was started."));
|
|
53
|
+
}, budget.remainingMs(rpcTimeoutMs));
|
|
54
|
+
budget.signal.addEventListener("abort", abort, { once: true });
|
|
55
|
+
socket.onopen = () => { cleanup(); resolve(); };
|
|
56
|
+
socket.onerror = socket.onclose = () => {
|
|
57
|
+
cleanup(); reject(new LocalMessagingError("daemon_unavailable", "Cannot connect to the existing local Codex daemon."));
|
|
58
|
+
};
|
|
59
|
+
if (budget.signal.aborted) abort();
|
|
60
|
+
});
|
|
61
|
+
} catch (error) { socket.terminate(); throw error; }
|
|
62
|
+
const rpc = new LocalMessageRpc(socket, budget, rpcTimeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
const result = await rpc.request("initialize", {
|
|
65
|
+
clientInfo: { name: "opencodex_message", version: "1.0.0" },
|
|
66
|
+
capabilities: { experimentalApi: true },
|
|
67
|
+
});
|
|
68
|
+
if (!isRecord(result)) throw rpc.invalidMetadata();
|
|
69
|
+
budget.throwIfEnded();
|
|
70
|
+
socket.send(JSON.stringify({ method: "initialized", params: {} }));
|
|
71
|
+
return rpc;
|
|
72
|
+
} catch (error) { rpc.close(); throw error; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Read up to 50 loaded UUIDs, rejecting malformed IDs and unbounded pagination cursors. */
|
|
76
|
+
async loadedPage(cursor?: string): Promise<{ data: string[]; nextCursor: string | null }> {
|
|
77
|
+
const raw = await this.request("thread/loaded/list", { limit: 50, ...(cursor ? { cursor } : {}) });
|
|
78
|
+
if (!isRecord(raw) || !Array.isArray(raw.data) || raw.data.length > 50 || !raw.data.every(isThreadId)
|
|
79
|
+
|| (raw.nextCursor !== undefined && raw.nextCursor !== null
|
|
80
|
+
&& (typeof raw.nextCursor !== "string" || !raw.nextCursor || raw.nextCursor.length > 4096))) {
|
|
81
|
+
throw this.invalidMetadata();
|
|
82
|
+
}
|
|
83
|
+
return { data: raw.data, nextCursor: typeof raw.nextCursor === "string" ? raw.nextCursor : null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Validate one exact thread and project ID/name/status only, without requesting turns. */
|
|
87
|
+
async readThread(id: string): Promise<LocalThread> {
|
|
88
|
+
if (!isThreadId(id)) throw new LocalMessagingError("invalid_selector", "A valid Codex thread ID is required.");
|
|
89
|
+
const raw = await this.request("thread/read", { threadId: id, includeTurns: false });
|
|
90
|
+
const thread = isRecord(raw) ? raw.thread : null;
|
|
91
|
+
if (!isRecord(thread) || thread.id !== id || (thread.name !== undefined && thread.name !== null
|
|
92
|
+
&& (typeof thread.name !== "string" || thread.name.length > 4096)) || !isRecord(thread.status)
|
|
93
|
+
|| !["idle", "active", "systemError", "notLoaded"].includes(String(thread.status.type))) {
|
|
94
|
+
throw this.invalidMetadata();
|
|
95
|
+
}
|
|
96
|
+
return { id, name: typeof thread.name === "string" ? thread.name : null, status: thread.status.type as LocalThread["status"] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Submit once on this connection; after an accepted write only a correlated reply is definitive. */
|
|
100
|
+
async queueMessage(threadId: string, text: string, messageId: string): Promise<QueueResult> {
|
|
101
|
+
let written = false;
|
|
102
|
+
try {
|
|
103
|
+
if (!isThreadId(threadId) || !isThreadId(messageId) || !text || text.includes("\0")
|
|
104
|
+
|| Buffer.byteLength(text) > 32 * 1024) {
|
|
105
|
+
throw new LocalMessagingError("invalid_message", "A bounded text message and UUID correlation are required.");
|
|
106
|
+
}
|
|
107
|
+
const raw = await this.request("thread/queue/add", {
|
|
108
|
+
threadId, input: [{ type: "text", text }], clientUserMessageId: messageId,
|
|
109
|
+
}, () => { written = true; });
|
|
110
|
+
const queued = isRecord(raw) ? raw.queuedSubmission : null;
|
|
111
|
+
if (!isRecord(queued) || typeof queued.id !== "string" || !queued.id || queued.id.length > 4096
|
|
112
|
+
|| queued.clientUserMessageId !== messageId || !Array.isArray(queued.input) || queued.input.length !== 1
|
|
113
|
+
|| !isRecord(queued.input[0]) || queued.input[0].type !== "text" || queued.input[0].text !== text
|
|
114
|
+
|| (queued.input[0].text_elements !== undefined
|
|
115
|
+
&& (!Array.isArray(queued.input[0].text_elements) || queued.input[0].text_elements.length !== 0))) {
|
|
116
|
+
throw this.invalidMetadata();
|
|
117
|
+
}
|
|
118
|
+
return { status: "queued" };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof QueueRejection || !written) {
|
|
121
|
+
return { status: "not_sent", error: error instanceof LocalMessagingError
|
|
122
|
+
? { code: error.code, message: error.message }
|
|
123
|
+
: { code: "messaging_failed", message: "Local queue submission could not be started." } };
|
|
124
|
+
}
|
|
125
|
+
return { status: "unknown", error: { code: "submission_unknown",
|
|
126
|
+
message: "Local queue submission may have occurred. Do not replay; recipient processing is unknown." } };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Idempotently reject pending work, remove handlers and terminate only this connection. */
|
|
131
|
+
close(error: Error = new LocalMessagingError("daemon_unavailable", "Local Codex connection closed.")): void {
|
|
132
|
+
if (this.closed) return;
|
|
133
|
+
this.closed = true;
|
|
134
|
+
this.budget.signal.removeEventListener("abort", this.abort);
|
|
135
|
+
this.socket.onopen = this.socket.onmessage = this.socket.onerror = this.socket.onclose = null;
|
|
136
|
+
for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); }
|
|
137
|
+
this.pending.clear();
|
|
138
|
+
this.socket.terminate();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Fail closed on malformed metadata without including the daemon's response in the error. */
|
|
142
|
+
private invalidMetadata(): LocalMessagingError {
|
|
143
|
+
const error = new LocalMessagingError("invalid_metadata", "Codex returned invalid local session metadata.");
|
|
144
|
+
this.close(error);
|
|
145
|
+
return error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Issue one whitelisted RPC with bounded concurrency and timeout; record accepted writes. */
|
|
149
|
+
private request(method: Method, params: Record<string, unknown>, onWritten?: () => void): Promise<unknown> {
|
|
150
|
+
this.budget.throwIfEnded();
|
|
151
|
+
if (this.closed || this.socket.readyState !== 1) throw new LocalMessagingError("daemon_unavailable", "Local Codex connection is closed.");
|
|
152
|
+
if (this.pending.size >= 4) throw new LocalMessagingError("rpc_limit", "Local Codex metadata concurrency limit exceeded.");
|
|
153
|
+
const id = ++this.nextId;
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const timer = setTimeout(() => this.close(new LocalMessagingError("rpc_timeout", "Local Codex request timed out.")),
|
|
156
|
+
this.budget.remainingMs(this.rpcTimeoutMs));
|
|
157
|
+
this.pending.set(id, { resolve, reject, timer, method });
|
|
158
|
+
try { this.socket.send(JSON.stringify({ id, method, params })); onWritten?.(); } catch { this.close(); }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Settle matching bounded RPC frames; ignore notifications and sanitize remote errors. */
|
|
163
|
+
private receive(data: unknown): void {
|
|
164
|
+
try {
|
|
165
|
+
if (typeof data !== "string" || Buffer.byteLength(data) > 1024 * 1024) throw new Error();
|
|
166
|
+
const raw: unknown = JSON.parse(data);
|
|
167
|
+
if (!isRecord(raw)) throw new Error();
|
|
168
|
+
if (typeof raw.method === "string" && raw.id === undefined) return;
|
|
169
|
+
if (typeof raw.id !== "number" || !Number.isSafeInteger(raw.id)
|
|
170
|
+
|| Object.hasOwn(raw, "result") === Object.hasOwn(raw, "error")) throw new Error();
|
|
171
|
+
const pending = this.pending.get(raw.id);
|
|
172
|
+
if (!pending) {
|
|
173
|
+
// During a one-shot submission an uncorrelated response cannot acknowledge our write.
|
|
174
|
+
if ([...this.pending.values()].some(request => request.method === "thread/queue/add")) throw new Error();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let rejection: LocalMessagingError | undefined;
|
|
178
|
+
if (Object.hasOwn(raw, "error")) {
|
|
179
|
+
const error = raw.error;
|
|
180
|
+
if (!isRecord(error) || !Number.isSafeInteger(error.code) || typeof error.message !== "string") throw new Error();
|
|
181
|
+
if (pending.method === "thread/queue/add") {
|
|
182
|
+
const unsupported = error.code === -32601 || (error.code === -32600
|
|
183
|
+
&& (error.message === "thread/queue/add requires experimentalApi capability"
|
|
184
|
+
|| error.message.startsWith("Invalid request: unknown variant `thread/queue/add`")));
|
|
185
|
+
rejection = unsupported
|
|
186
|
+
? new QueueRejection("unsupported_queue", "Your Codex daemon does not support local queueing (experimental thread/queue/add).")
|
|
187
|
+
: new QueueRejection("queue_rejected", "Codex rejected the local queue submission.");
|
|
188
|
+
} else rejection = new LocalMessagingError("rpc_rejected", "Codex rejected the local metadata request.");
|
|
189
|
+
}
|
|
190
|
+
this.pending.delete(raw.id);
|
|
191
|
+
clearTimeout(pending.timer);
|
|
192
|
+
if (rejection) pending.reject(rejection);
|
|
193
|
+
else pending.resolve(raw.result);
|
|
194
|
+
} catch { this.close(new LocalMessagingError("invalid_metadata", "Codex returned an invalid or oversized RPC frame.")); }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { MessageBudget } from "./budget";
|
|
2
|
+
import { discoverLoaded, resolveLoaded } from "./discovery";
|
|
3
|
+
import { messageEnvelope, validateMessage, type MessageOptions } from "./envelope";
|
|
4
|
+
import { LocalMessageRpc } from "./rpc";
|
|
5
|
+
import { localDaemonEndpoint } from "./socket";
|
|
6
|
+
import { isThreadId, LocalMessagingError, type LocalThread } from "./types";
|
|
7
|
+
|
|
8
|
+
export interface MessageReceipt {
|
|
9
|
+
schema: "ocx-message/1";
|
|
10
|
+
messageId: string;
|
|
11
|
+
kind: MessageOptions["kind"];
|
|
12
|
+
inReplyTo: string | null;
|
|
13
|
+
status: "not_sent" | "queued" | "unknown";
|
|
14
|
+
sender: { threadId: string; name: string | null; identitySource: "CODEX_THREAD_ID" } | null;
|
|
15
|
+
target: { threadId: string; name: string | null } | null;
|
|
16
|
+
error?: { code: string; message: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Expose caller-safe messaging errors without serializing arbitrary daemon output. */
|
|
20
|
+
export function messageFailure(error: unknown) {
|
|
21
|
+
return error instanceof LocalMessagingError ? { code: error.code, message: error.message }
|
|
22
|
+
: { code: "messaging_failed", message: "Local messaging failed; private daemon output is not included." };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Return a complete loaded-session snapshot and close the command-owned RPC connection. */
|
|
26
|
+
export async function localSessions(home: string, budget: MessageBudget): Promise<LocalThread[]> {
|
|
27
|
+
const rpc = await LocalMessageRpc.connect(localDaemonEndpoint(home).url, budget);
|
|
28
|
+
try { return await discoverLoaded(rpc, budget); } finally { rpc.close(); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Revalidate and submit once on the discovery connection; never reconnect, resume or replay. */
|
|
32
|
+
export async function sendLocalMessage(options: MessageOptions & { thread?: string; name?: string; body: string },
|
|
33
|
+
context: { home: string; senderId?: string }, budget: MessageBudget): Promise<MessageReceipt> {
|
|
34
|
+
const receipt: MessageReceipt = { schema: "ocx-message/1", messageId: crypto.randomUUID(), kind: options.kind,
|
|
35
|
+
inReplyTo: options.inReplyTo ?? null, status: "not_sent", sender: null, target: null };
|
|
36
|
+
let rpc: LocalMessageRpc | undefined;
|
|
37
|
+
try {
|
|
38
|
+
validateMessage(options, options.body);
|
|
39
|
+
if (context.senderId !== undefined && !isThreadId(context.senderId)) {
|
|
40
|
+
throw new LocalMessagingError("invalid_sender", "CODEX_THREAD_ID is not a UUID; sender identity cannot be inferred.");
|
|
41
|
+
}
|
|
42
|
+
const endpoint = localDaemonEndpoint(context.home);
|
|
43
|
+
rpc = await LocalMessageRpc.connect(endpoint.url, budget);
|
|
44
|
+
const threads = await discoverLoaded(rpc, budget);
|
|
45
|
+
const target = resolveLoaded(threads, options);
|
|
46
|
+
receipt.target = { threadId: target.id, name: target.name };
|
|
47
|
+
const sender = context.senderId ? resolveLoaded(threads, { thread: context.senderId }) : null;
|
|
48
|
+
receipt.sender = sender ? { threadId: sender.id, name: sender.name, identitySource: "CODEX_THREAD_ID" } : null;
|
|
49
|
+
const envelope = messageEnvelope(receipt.messageId, options, options.body, sender);
|
|
50
|
+
// Recheck the exact resolved ID, never re-resolve a name or reconnect by path.
|
|
51
|
+
const fresh = await rpc.readThread(target.id);
|
|
52
|
+
if (fresh.status === "notLoaded") throw new LocalMessagingError("target_not_loaded", "The destination unloaded before submission.");
|
|
53
|
+
budget.throwIfEnded();
|
|
54
|
+
Object.assign(receipt, await rpc.queueMessage(target.id, envelope.text, receipt.messageId));
|
|
55
|
+
} catch (error) { receipt.error = messageFailure(error); }
|
|
56
|
+
finally { rpc?.close(); }
|
|
57
|
+
return receipt;
|
|
58
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { LocalMessagingError } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface LocalSocket extends WebSocket { terminate(): void }
|
|
6
|
+
interface SocketStat {
|
|
7
|
+
uid: number;
|
|
8
|
+
mode: number;
|
|
9
|
+
isSocket(): boolean;
|
|
10
|
+
isDirectory(): boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface SocketFilesystem {
|
|
13
|
+
realpath(path: string): string;
|
|
14
|
+
lstat(path: string): SocketStat;
|
|
15
|
+
}
|
|
16
|
+
/** Bun on macOS cannot realpath a socket inode; resolve its parent and terminal symlinks. */
|
|
17
|
+
function socketRealpath(path: string): string {
|
|
18
|
+
for (let links = 0; links < 40; links++) {
|
|
19
|
+
const parent = realpathSync(dirname(path));
|
|
20
|
+
const candidate = join(parent, basename(path));
|
|
21
|
+
if (!lstatSync(candidate).isSymbolicLink()) return candidate;
|
|
22
|
+
path = resolve(parent, readlinkSync(candidate));
|
|
23
|
+
}
|
|
24
|
+
throw new Error("Too many socket symlinks");
|
|
25
|
+
}
|
|
26
|
+
const filesystem: SocketFilesystem = { realpath: socketRealpath, lstat: lstatSync };
|
|
27
|
+
|
|
28
|
+
/** Explicit home only: resolving or starting the user's daemon is not transport work. */
|
|
29
|
+
export function localDaemonEndpoint(codexHome: string, platform = process.platform) {
|
|
30
|
+
const path = join(codexHome, "app-server-control", "app-server-control.sock");
|
|
31
|
+
if (!["linux", "darwin"].includes(platform) || !isAbsolute(codexHome)
|
|
32
|
+
|| Buffer.byteLength(path) > 103 || /[:?#%\\\x00-\x1f]/.test(path)) {
|
|
33
|
+
throw new LocalMessagingError("unsupported_socket", "This local Codex control socket cannot be addressed on this platform.");
|
|
34
|
+
}
|
|
35
|
+
return { url: `ws+unix://${path}:/` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** StrictModes on the resolved endpoint and every ancestor; symlinked homes are permitted. */
|
|
39
|
+
export function trustedSocketPath(path: string, fs: SocketFilesystem = filesystem,
|
|
40
|
+
uid = process.getuid?.()): string {
|
|
41
|
+
try {
|
|
42
|
+
if (uid === undefined) throw new Error();
|
|
43
|
+
const real = fs.realpath(path);
|
|
44
|
+
if (!isAbsolute(real)) throw new Error();
|
|
45
|
+
const socket = fs.lstat(real);
|
|
46
|
+
if (!socket.isSocket() || socket.uid !== uid) throw new Error();
|
|
47
|
+
for (let dir = dirname(real); ; dir = dirname(dir)) {
|
|
48
|
+
const stat = fs.lstat(dir);
|
|
49
|
+
if (!stat.isDirectory() || (stat.uid !== uid && stat.uid !== 0)
|
|
50
|
+
|| ((stat.mode & 0o022) !== 0 && !(stat.uid === 0 && (stat.mode & 0o1000) !== 0))) throw new Error();
|
|
51
|
+
if (dir === "/") break;
|
|
52
|
+
}
|
|
53
|
+
return real;
|
|
54
|
+
} catch {
|
|
55
|
+
throw new LocalMessagingError("untrusted_socket", "The local Codex control socket or its parent directories are not trusted.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Validate trust before opening the resolved Unix endpoint; never accept network transports. */
|
|
60
|
+
export function localSocket(url: string): LocalSocket {
|
|
61
|
+
if (!["linux", "darwin"].includes(process.platform)
|
|
62
|
+
|| !/^ws\+unix:\/\/\/[^:?#%\\\x00-\x1f]+:\/$/.test(url) || Buffer.byteLength(url.slice(10, -2)) > 103) {
|
|
63
|
+
throw new LocalMessagingError("unsupported_socket", "Messaging accepts only the local Unix control socket.");
|
|
64
|
+
}
|
|
65
|
+
const path = trustedSocketPath(url.slice(10, -2));
|
|
66
|
+
// A short alias does not bypass the actual Unix address budget or URL grammar.
|
|
67
|
+
if (Buffer.byteLength(path) > 103 || /[:?#%\\\x00-\x1f]/.test(path)) {
|
|
68
|
+
throw new LocalMessagingError("unsupported_socket", "Messaging accepts only the local Unix control socket.");
|
|
69
|
+
}
|
|
70
|
+
const Constructor = WebSocket as unknown as { new(url: string): LocalSocket };
|
|
71
|
+
return new Constructor(`ws+unix://${path}:/`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Typed failure whose message is safe for CLI receipts, never raw daemon or helper output. */
|
|
2
|
+
export class LocalMessagingError extends Error {
|
|
3
|
+
/** Attach a stable failure code to caller-authored, sanitized explanatory text. */
|
|
4
|
+
constructor(readonly code: string, message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "LocalMessagingError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Narrow an untrusted RPC value to a non-null, non-array object before field validation. */
|
|
11
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Accept UUID-shaped thread/correlation IDs, not names or command fragments. */
|
|
16
|
+
export function isThreadId(value: unknown): value is string {
|
|
17
|
+
return typeof value === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LocalThread {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string | null;
|
|
23
|
+
status: "idle" | "active" | "systemError" | "notLoaded";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LocalMetadataClient {
|
|
27
|
+
loadedPage(cursor?: string): Promise<{ data: string[]; nextCursor: string | null }>;
|
|
28
|
+
readThread(id: string): Promise<LocalThread>;
|
|
29
|
+
}
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { anthropicModelFamily } from "./anthropic-model-quota";
|
|
2
2
|
/** Narrow pre-output account recovery, fenced to the bearer that physically sent the turn. */
|
|
3
3
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
4
|
-
import { classifyAnthropic429, anthropicRetryAfterMs,
|
|
4
|
+
import { classifyAnthropic429, anthropicRetryAfterMs, anthropicRatePolicyFor, ANTHROPIC_SHORT_RETRY_MS, ANTHROPIC_MAX_INLINE_THROTTLE_MS } from "./anthropic-rate-limit-policy";
|
|
5
5
|
import { isNonReplayableResponse, sleepWithAbort } from "../lib/upstream-retry";
|
|
6
6
|
import { credentialGeneration, getAccountCredentialWithStatus } from "./store";
|
|
7
7
|
import type { OAuthAccessSnapshot } from "./index";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
9
9
|
import type { AnthropicRouteDecision } from "./anthropic-model-routes";
|
|
10
|
-
import {
|
|
10
|
+
import { anthropicRoutingFor } from "./anthropic-routing";
|
|
11
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
12
|
+
import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
13
|
+
import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIsCurrent,
|
|
14
|
+
type AnthropicPhysicalSendOwnership } from "./anthropic-send-ownership";
|
|
11
15
|
|
|
12
|
-
const responseCredentials = new WeakMap<Response,
|
|
13
|
-
|
|
16
|
+
const responseCredentials = new WeakMap<Response, AnthropicPhysicalSendOwnership & { providerAccountUuid?: string; checkProviderUuid: boolean }>();
|
|
17
|
+
type RetryState = { firstAccountId: string; sameAccount: boolean; detour: boolean };
|
|
18
|
+
const retryStatesByInstance = new Map<AnthropicInstanceId, WeakMap<object, RetryState>>();
|
|
14
19
|
const verdicts = new WeakMap<Response, Promise<boolean>>();
|
|
15
20
|
|
|
16
|
-
/**
|
|
21
|
+
/** Legacy compatibility only; physical callers must capture ownership before fetch and use ForSend. */
|
|
17
22
|
export function bindAnthropicRefusalCredential(response: Response, snapshot: OAuthAccessSnapshot, providerAccountUuid?: string): void {
|
|
18
|
-
|
|
23
|
+
const owner = captureAnthropicPhysicalSendOwnership(snapshot);
|
|
24
|
+
if (!owner) { responseCredentials.delete(response); return; }
|
|
25
|
+
if (arguments.length >= 3) bindAnthropicRefusalCredentialForSend(response, owner, providerAccountUuid);
|
|
26
|
+
else bindAnthropicRefusalCredentialForSend(response, owner);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Retain the pre-send owner even when stale; binding a returned response cannot renew its authority. */
|
|
30
|
+
export function bindAnthropicRefusalCredentialForSend(response: Response, owner: AnthropicPhysicalSendOwnership, providerAccountUuid?: string): void {
|
|
31
|
+
responseCredentials.set(response, Object.freeze({ ...owner, providerAccountUuid, checkProviderUuid: arguments.length >= 3 }));
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
async function isAccountRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
|
|
@@ -41,7 +54,8 @@ async function isAccountRefusal(response: Response, signal?: AbortSignal): Promi
|
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
export async function
|
|
57
|
+
export async function rotateAnthropicAccountOnResponseForInstance(
|
|
58
|
+
instance: AnthropicInstanceId,
|
|
45
59
|
response: Response,
|
|
46
60
|
options: {
|
|
47
61
|
config: OcxConfig;
|
|
@@ -61,13 +75,25 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
61
75
|
currentDecision?: () => AnthropicRouteDecision | null;
|
|
62
76
|
},
|
|
63
77
|
): Promise<string | null> {
|
|
64
|
-
if (options.signal?.aborted || isNonReplayableResponse(response)
|
|
78
|
+
if (options.signal?.aborted || isNonReplayableResponse(response)
|
|
79
|
+
|| configuredAnthropicInstance(options.config, instance) !== instance) return null;
|
|
80
|
+
const sent = responseCredentials.get(response);
|
|
81
|
+
if (!sent || sent.provider !== instance || sent.accountId !== options.accountId) return null;
|
|
82
|
+
const routing = anthropicRoutingFor(instance);
|
|
83
|
+
const { pauseAnthropicRateAdmission, anthropicRatePauseUntil } = anthropicRatePolicyFor(instance);
|
|
84
|
+
const { recordAnthropicAccountRefusal, rotateAnthropicAccountOnRefusal,
|
|
85
|
+
hasAnthropicFailoverQuorum, isAnthropicAccountPoolEnabled, pickAlternateAnthropicAccount } = routing;
|
|
86
|
+
const ownedCurrent = () => {
|
|
87
|
+
if (configuredAnthropicInstance(options.config, instance) !== instance
|
|
88
|
+
|| !anthropicPhysicalSendOwnershipIsCurrent(sent)) return undefined;
|
|
89
|
+
const row = getAccountCredentialWithStatus(instance, sent.accountId);
|
|
90
|
+
return row && !row.needsReauth && row.credential.access === sent.accessToken
|
|
91
|
+
&& credentialGeneration(row.credential) === sent.generation
|
|
92
|
+
&& (!sent.checkProviderUuid || row.credential.accountId === sent.providerAccountUuid) ? row : undefined;
|
|
93
|
+
};
|
|
65
94
|
if (response.status === 429) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (!sent || sent.accountId !== options.accountId || !current || current.needsReauth
|
|
69
|
-
|| credentialGeneration(current.credential) !== sent.generation
|
|
70
|
-
|| sent.checkProviderUuid && current.credential.accountId !== sent.providerAccountUuid) return null;
|
|
95
|
+
const current = ownedCurrent();
|
|
96
|
+
if (!current) return null;
|
|
71
97
|
options.currentDecision?.();
|
|
72
98
|
const kind = classifyAnthropic429(response.headers);
|
|
73
99
|
if (kind === "family-quota") {
|
|
@@ -80,6 +106,8 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
80
106
|
const delay = anthropicRetryAfterMs(response.headers.get("retry-after"), now) ?? ANTHROPIC_SHORT_RETRY_MS;
|
|
81
107
|
if (kind === "transient-rate") pauseAnthropicRateAdmission(sent.accountId, now + delay);
|
|
82
108
|
if (!options.canRetry || options.allow429Recovery === false || !options.requestKey) return null;
|
|
109
|
+
let retryStates = retryStatesByInstance.get(instance);
|
|
110
|
+
if (!retryStates) { retryStates = new WeakMap(); retryStatesByInstance.set(instance, retryStates); }
|
|
83
111
|
let state = retryStates.get(options.requestKey);
|
|
84
112
|
if (!state) { state = { firstAccountId: sent.accountId, sameAccount: false, detour: false }; retryStates.set(options.requestKey, state); }
|
|
85
113
|
// A concurrent committed selection may have moved the same-account proposal.
|
|
@@ -90,7 +118,7 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
90
118
|
// Millisecond rounding can wake just before this deadline; later extensions still bind.
|
|
91
119
|
const retryAt = now + wait;
|
|
92
120
|
try { await sleepWithAbort(wait, options.signal); } catch { return null; }
|
|
93
|
-
const live =
|
|
121
|
+
const live = ownedCurrent();
|
|
94
122
|
if (!live || live.paused || live.needsReauth || options.signal?.aborted
|
|
95
123
|
|| credentialGeneration(live.credential) !== sent.generation
|
|
96
124
|
|| sent.checkProviderUuid && live.credential.accountId !== sent.providerAccountUuid
|
|
@@ -111,8 +139,6 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
111
139
|
}
|
|
112
140
|
} else {
|
|
113
141
|
if (response.status !== 403 || options.allowAccountRefusal === false) return null;
|
|
114
|
-
const sent = responseCredentials.get(response);
|
|
115
|
-
if (!sent || sent.accountId !== options.accountId) return null;
|
|
116
142
|
let verdict = verdicts.get(response);
|
|
117
143
|
if (!verdict) {
|
|
118
144
|
verdict = isAccountRefusal(response, options.signal);
|
|
@@ -120,7 +146,7 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
120
146
|
}
|
|
121
147
|
if (!await verdict || options.signal?.aborted) return null;
|
|
122
148
|
// A late refusal must not cool a new credential stored while its body was being read.
|
|
123
|
-
const current =
|
|
149
|
+
const current = ownedCurrent();
|
|
124
150
|
if (!current || current.paused || current.needsReauth || credentialGeneration(current.credential) !== sent.generation
|
|
125
151
|
|| sent.checkProviderUuid && current.credential.accountId !== sent.providerAccountUuid) return null;
|
|
126
152
|
}
|
|
@@ -134,3 +160,11 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
134
160
|
return rotateAnthropicAccountOnRefusal(options.config, options.accountId, status,
|
|
135
161
|
response.headers.get("retry-after"), options.sessionKey, Date.now(), response.headers, decision, options.model, options.excludedAccountIds);
|
|
136
162
|
}
|
|
163
|
+
|
|
164
|
+
/** Compatibility entrypoint: legacy consumers always recover inside the primary pool. */
|
|
165
|
+
export function rotateAnthropicAccountOnResponse(
|
|
166
|
+
response: Response,
|
|
167
|
+
options: Parameters<typeof rotateAnthropicAccountOnResponseForInstance>[2],
|
|
168
|
+
): Promise<string | null> {
|
|
169
|
+
return rotateAnthropicAccountOnResponseForInstance("anthropic", response, options);
|
|
170
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
2
|
+
import { resolveAnthropicAccountPoolConfig } from "./anthropic-pool-config";
|
|
1
3
|
import type { OcxConfig } from "../types";
|
|
2
4
|
import type { ProviderAccount } from "./types";
|
|
3
5
|
|
|
@@ -7,7 +9,11 @@ export function parseAnthropicAccountThreshold(value: unknown): number | null {
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/** Missing or malformed legacy metadata inherits; concrete zero must survive nullish fallback. */
|
|
10
|
-
export function
|
|
12
|
+
export function effectiveAnthropicAccountThresholdForInstance(instance: AnthropicInstanceId, config: OcxConfig, account?: Pick<ProviderAccount, "autoSwitchThresholdOverride">): number {
|
|
11
13
|
return parseAnthropicAccountThreshold(account?.autoSwitchThresholdOverride)
|
|
12
|
-
?? parseAnthropicAccountThreshold(config.
|
|
14
|
+
?? parseAnthropicAccountThreshold(resolveAnthropicAccountPoolConfig(config, instance).autoSwitchThreshold) ?? 80;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function effectiveAnthropicAccountThreshold(config: OcxConfig, account?: Pick<ProviderAccount, "autoSwitchThresholdOverride">): number {
|
|
18
|
+
return effectiveAnthropicAccountThresholdForInstance("anthropic", config, account);
|
|
13
19
|
}
|
|
@@ -3,6 +3,7 @@ import { normalizeAnthropicIdentity, resolveAnthropicAccountIdentity, type Anthr
|
|
|
3
3
|
import { credentialGeneration, type AuthStore } from "./store";
|
|
4
4
|
import { REFRESH_SKEW_MS } from "./refresh-policy";
|
|
5
5
|
import type { OAuthCredentials, ProviderAccountSet } from "./types";
|
|
6
|
+
import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
6
7
|
|
|
7
8
|
export type ClaudeCredentialObservation =
|
|
8
9
|
| { kind: "absent" }
|
|
@@ -13,6 +14,14 @@ export async function newerClaudeCredential(
|
|
|
13
14
|
stored: OAuthCredentials, now: number, signal?: AbortSignal,
|
|
14
15
|
resolveIdentity: AnthropicIdentityResolver = resolveAnthropicAccountIdentity,
|
|
15
16
|
): Promise<ClaudeCredentialObservation> {
|
|
17
|
+
return newerClaudeCredentialForInstance("anthropic", stored, now, signal, resolveIdentity);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function newerClaudeCredentialForInstance(
|
|
21
|
+
instance: AnthropicInstanceId, stored: OAuthCredentials, now: number, signal?: AbortSignal,
|
|
22
|
+
resolveIdentity: AnthropicIdentityResolver = resolveAnthropicAccountIdentity,
|
|
23
|
+
): Promise<ClaudeCredentialObservation> {
|
|
24
|
+
if (instance === "anthropic2") return { kind: "absent" };
|
|
16
25
|
if (stored.source !== "local-cli") return { kind: "absent" };
|
|
17
26
|
const disk = detectClaudeCodeToken();
|
|
18
27
|
if (!disk || !Number.isFinite(disk.expires) || disk.expires <= now + REFRESH_SKEW_MS
|
|
@@ -39,6 +48,10 @@ export async function newerClaudeCredential(
|
|
|
39
48
|
|
|
40
49
|
/** Capture primitives before network awaits; check them again inside the serialized writer. */
|
|
41
50
|
export function captureAnthropicCredentialOwner(set: ProviderAccountSet, accountId: string) {
|
|
51
|
+
return captureAnthropicCredentialOwnerForInstance("anthropic", set, accountId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function captureAnthropicCredentialOwnerForInstance(instance: AnthropicInstanceId, set: ProviderAccountSet, accountId: string) {
|
|
42
55
|
const row = set.accounts.find(account => account.id === accountId)!;
|
|
43
56
|
const loginId = row.loginId;
|
|
44
57
|
const generation = credentialGeneration(row.credential);
|
|
@@ -46,12 +59,13 @@ export function captureAnthropicCredentialOwner(set: ProviderAccountSet, account
|
|
|
46
59
|
const needsReauth = row.needsReauth;
|
|
47
60
|
const selection = { id: set.activeAccountId, revision: set.selectionRevision };
|
|
48
61
|
return (store: AuthStore, diskGeneration?: string, allowPaused = false): boolean => {
|
|
49
|
-
const current = store[
|
|
62
|
+
const current = store[instance];
|
|
50
63
|
const account = current?.accounts.find(candidate => candidate.id === accountId);
|
|
51
64
|
if (!account || (!allowPaused && account.paused) || account.loginId !== loginId
|
|
52
65
|
|| account.needsReauth !== needsReauth || credentialGeneration(account.credential) !== generation
|
|
53
66
|
|| identityMetadata(account.credential) !== metadata) return false;
|
|
54
67
|
if (diskGeneration !== undefined) {
|
|
68
|
+
if (instance === "anthropic2") return false;
|
|
55
69
|
if (current?.activeAccountId !== selection.id || current?.selectionRevision !== selection.revision) return false;
|
|
56
70
|
const disk = detectClaudeCodeToken();
|
|
57
71
|
if (!disk || disk.expires <= Date.now() + REFRESH_SKEW_MS || credentialGeneration(disk) !== diskGeneration) return false;
|