@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
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isGpt56NativeSlug,
|
|
27
27
|
} from "./effort";
|
|
28
28
|
import { CATALOG_INACTIVE_REASON_FIELD, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
|
|
29
|
+
import { applyRoutedDescription } from "./ownership";
|
|
29
30
|
|
|
30
31
|
export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: NativeContextLimitsInput): RawEntry {
|
|
31
32
|
if (priority !== 9) clone.priority = priority;
|
|
@@ -128,7 +129,7 @@ export function deriveEntry(
|
|
|
128
129
|
delete e[SPAWN_PRIORITY_FIELD];
|
|
129
130
|
e.slug = slug;
|
|
130
131
|
e.display_name = routedDisplayName(slug, model);
|
|
131
|
-
e
|
|
132
|
+
applyRoutedDescription(e, desc, model);
|
|
132
133
|
e.priority = priority;
|
|
133
134
|
e.visibility = "list";
|
|
134
135
|
if ("upgrade" in e) e.upgrade = null;
|
|
@@ -212,6 +213,7 @@ export function deriveEntry(
|
|
|
212
213
|
: { web_search_tool_type: "text_and_image", supports_search_tool: true }
|
|
213
214
|
: {}),
|
|
214
215
|
};
|
|
216
|
+
applyRoutedDescription(entry, desc, model);
|
|
215
217
|
if (isRouted) {
|
|
216
218
|
applyRoutedCodexToolMode(entry, model?.codexToolMode);
|
|
217
219
|
applyReasoningLevels(
|
|
@@ -38,6 +38,7 @@ import type { RawEntry } from "./parsing";
|
|
|
38
38
|
import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexModelsCache, unique } from "./bundled";
|
|
39
39
|
import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
40
40
|
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
41
|
+
import { ROUTED_ORIGIN_FIELD } from "./ownership";
|
|
41
42
|
import { RESERVE_METADATA_SOURCE_FIELD } from "./reserve";
|
|
42
43
|
import {
|
|
43
44
|
ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
|
|
@@ -813,6 +814,7 @@ export function observedReserveCatalogSource(
|
|
|
813
814
|
}
|
|
814
815
|
source.slug = NATIVE_RESERVE_MODEL;
|
|
815
816
|
delete source.opencodex_catalog_kind;
|
|
817
|
+
delete source[ROUTED_ORIGIN_FIELD];
|
|
816
818
|
delete source[RESERVE_METADATA_SOURCE_FIELD];
|
|
817
819
|
delete source[ACCOUNT_BOUND_OBSERVED_NATIVE_MARKER];
|
|
818
820
|
delete source[ACCOUNT_BOUND_OBSERVED_SELECTORS_MARKER];
|
|
@@ -189,7 +189,6 @@ export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds:
|
|
|
189
189
|
);
|
|
190
190
|
}
|
|
191
191
|
export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
|
|
192
|
-
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
193
192
|
// xAI /models advertises both the dated deployment and this floating alias.
|
|
194
193
|
// Keep only grok-4.20-multi-agent-0309; the alias is the same server-side id.
|
|
195
194
|
if (providerName === "xai" && modelId === "grok-4.20-multi-agent-beta-latest") return false;
|
|
@@ -205,7 +204,6 @@ export function shouldRetainConfiguredProviderModel(
|
|
|
205
204
|
prov?: OcxProviderConfig,
|
|
206
205
|
): boolean {
|
|
207
206
|
if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true;
|
|
208
|
-
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
209
207
|
if (modelInList(prov?.retainModels, modelId)) return true;
|
|
210
208
|
return false;
|
|
211
209
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { COMBO_NAMESPACE } from "../../combos/identifiers";
|
|
2
|
+
import type { CatalogModel, RawEntry } from "./parsing";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authorship of OpenCodex-generated routed catalog rows (#855, #7229).
|
|
6
|
+
*
|
|
7
|
+
* A generated routed row normally carries the description prefix below, and that prefix is what
|
|
8
|
+
* removal, template selection, auto-review and residue detection have always recognized. A combo
|
|
9
|
+
* may replace its row description (#7229), so such a row carries a stable marker instead. Rows
|
|
10
|
+
* with the generated description stay byte-identical and keep prefix recognition; only an exact
|
|
11
|
+
* known marker value confers authorship.
|
|
12
|
+
*/
|
|
13
|
+
export const ROUTED_DESCRIPTION_PREFIX = "Routed via opencodex → ";
|
|
14
|
+
export const ROUTED_ORIGIN_FIELD = "opencodex_routed_origin";
|
|
15
|
+
export const COMBO_ROUTED_ORIGIN = "combo-v1";
|
|
16
|
+
const COMBO_DESCRIPTION_PREFIX = `${ROUTED_DESCRIPTION_PREFIX}${COMBO_NAMESPACE} (`;
|
|
17
|
+
|
|
18
|
+
function descriptionStartsWith(entry: RawEntry, prefix: string): boolean {
|
|
19
|
+
return typeof entry.description === "string" && entry.description.startsWith(prefix);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True for a row whose description was overridden by a combo and stamped by OpenCodex. */
|
|
23
|
+
export function hasComboRoutedOriginMarker(entry: RawEntry): boolean {
|
|
24
|
+
return entry[ROUTED_ORIGIN_FIELD] === COMBO_ROUTED_ORIGIN;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Generated by OpenCodex routing: the stable marker, or the legacy generated description. */
|
|
28
|
+
export function hasOcxRoutedAuthorship(entry: RawEntry): boolean {
|
|
29
|
+
return hasComboRoutedOriginMarker(entry) || descriptionStartsWith(entry, ROUTED_DESCRIPTION_PREFIX);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Generated for a combo: the stable marker, or the legacy generated combo description. */
|
|
33
|
+
export function hasOcxComboAuthorship(entry: RawEntry): boolean {
|
|
34
|
+
return hasComboRoutedOriginMarker(entry) || descriptionStartsWith(entry, COMBO_DESCRIPTION_PREFIX);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The operator's combo description override, if this routed model has one. */
|
|
38
|
+
export function comboDescriptionOverride(model: CatalogModel | undefined): string | undefined {
|
|
39
|
+
if (model?.provider !== COMBO_NAMESPACE) return undefined;
|
|
40
|
+
const description = typeof model.description === "string" ? model.description.trim() : "";
|
|
41
|
+
return description || undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set a routed row's description and its authorship marker together. A template clone may carry
|
|
46
|
+
* another row's marker, so the marker is always rewritten from this model's own override.
|
|
47
|
+
*/
|
|
48
|
+
export function applyRoutedDescription(entry: RawEntry, generated: string, model: CatalogModel | undefined): void {
|
|
49
|
+
const override = comboDescriptionOverride(model);
|
|
50
|
+
entry.description = override ?? generated;
|
|
51
|
+
if (override !== undefined) entry[ROUTED_ORIGIN_FIELD] = COMBO_ROUTED_ORIGIN;
|
|
52
|
+
else delete entry[ROUTED_ORIGIN_FIELD];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A combo row retained from the previous catalog (its fresh derivation failed, e.g. a member
|
|
57
|
+
* provider is temporarily unavailable) bypasses deriveEntry. Re-project the combo's CURRENT
|
|
58
|
+
* description onto it so clearing or changing the description in config still reaches the
|
|
59
|
+
* picker. A cleared override returns the row to the generated combo text without the marker;
|
|
60
|
+
* an unmarked row without a current override is left exactly as it was.
|
|
61
|
+
*/
|
|
62
|
+
export function reapplyRetainedComboDescription(entry: RawEntry, current: string | undefined): void {
|
|
63
|
+
const description = current?.trim();
|
|
64
|
+
if (description) {
|
|
65
|
+
entry.description = description;
|
|
66
|
+
entry[ROUTED_ORIGIN_FIELD] = COMBO_ROUTED_ORIGIN;
|
|
67
|
+
} else if (hasComboRoutedOriginMarker(entry)) {
|
|
68
|
+
entry.description = `${ROUTED_DESCRIPTION_PREFIX}${COMBO_NAMESPACE} (${COMBO_NAMESPACE}).`;
|
|
69
|
+
delete entry[ROUTED_ORIGIN_FIELD];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -43,6 +43,7 @@ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREA
|
|
|
43
43
|
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
44
44
|
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
45
45
|
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
46
|
+
import { hasComboRoutedOriginMarker, hasOcxRoutedAuthorship } from "./ownership";
|
|
46
47
|
import { NATIVE_GPT6_ASTRA_MODEL } from "./native-models";
|
|
47
48
|
import { recordOwnedConfigPath } from "../../lib/config-ownership";
|
|
48
49
|
|
|
@@ -95,6 +96,12 @@ export interface CatalogModel {
|
|
|
95
96
|
* which carries no CatalogModel, so a configured displayName can never override a native name.
|
|
96
97
|
*/
|
|
97
98
|
displayName?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Display-only Codex catalog `description` override, honored for combo rows only (#7229).
|
|
101
|
+
* Unset keeps the generated "Routed via opencodex → …" text; a set value stamps the row with
|
|
102
|
+
* the ownership marker in ./ownership so removal and template selection still recognize it.
|
|
103
|
+
*/
|
|
104
|
+
description?: string;
|
|
98
105
|
owned_by?: string;
|
|
99
106
|
reasoningEfforts?: string[];
|
|
100
107
|
/** Exact CCA family evidence, retained through the in-memory discovery cache. */
|
|
@@ -297,7 +304,7 @@ export function findNativeTemplate(catalog: RawCatalog | null): RawEntry | null
|
|
|
297
304
|
&& "base_instructions" in m
|
|
298
305
|
&& m.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
|
|
299
306
|
&& m.owned_by !== COMBO_NAMESPACE
|
|
300
|
-
&& !(
|
|
307
|
+
&& !hasOcxRoutedAuthorship(m),
|
|
301
308
|
) ?? null;
|
|
302
309
|
}
|
|
303
310
|
|
|
@@ -325,7 +332,7 @@ export function findSupportedNativeTemplate(catalog: RawCatalog | null): RawEntr
|
|
|
325
332
|
&& "base_instructions" in m
|
|
326
333
|
&& m.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
|
|
327
334
|
&& m.owned_by !== COMBO_NAMESPACE
|
|
328
|
-
&& !(
|
|
335
|
+
&& !hasOcxRoutedAuthorship(m),
|
|
329
336
|
) ?? null;
|
|
330
337
|
}
|
|
331
338
|
|
|
@@ -925,7 +932,9 @@ export function readCatalogBackup(catalogPath: string): RawCatalog | null {
|
|
|
925
932
|
|
|
926
933
|
export function catalogHasRoutedEntries(catalog: RawCatalog | null): boolean {
|
|
927
934
|
return (catalog?.models ?? []).some(m => typeof m.slug === "string"
|
|
928
|
-
&& (m.slug.includes("/")
|
|
935
|
+
&& (m.slug.includes("/")
|
|
936
|
+
|| m.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND
|
|
937
|
+
|| hasComboRoutedOriginMarker(m)));
|
|
929
938
|
}
|
|
930
939
|
|
|
931
940
|
export function writePristineCatalogBackup(backupPath: string, catalogPath: string, catalog: RawCatalog): void {
|
|
@@ -60,7 +60,7 @@ import { isMultiAgentV2Enabled } from "../features";
|
|
|
60
60
|
import { clampCatalogModelsToCodexSupport } from "./effort";
|
|
61
61
|
import { suppressedSyntheticMaxCatalogSlugs } from "./model-hints";
|
|
62
62
|
import { filterCatalogVisibleModels, gatherRoutedModels, type CatalogGatherProviderModelOutcome } from "./provider-fetch";
|
|
63
|
-
import { dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
|
|
63
|
+
import { comboCatalogDescriptions, dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
|
|
64
64
|
import {
|
|
65
65
|
withCatalogWriteSerialization,
|
|
66
66
|
type CatalogWritePermit,
|
|
@@ -530,6 +530,7 @@ function writeRetainedCatalogSync({
|
|
|
530
530
|
multiAgentV2Enabled,
|
|
531
531
|
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
532
532
|
exactComboSlugs,
|
|
533
|
+
comboDescriptions: comboCatalogDescriptions(config),
|
|
533
534
|
hasPhysicalComboProvider,
|
|
534
535
|
includeNativeOpenAi,
|
|
535
536
|
accountBoundEntries,
|
|
@@ -102,7 +102,7 @@ import type {
|
|
|
102
102
|
CatalogTrustedOpenAiApiPolicySnapshot,
|
|
103
103
|
} from "../convergence-types";
|
|
104
104
|
import type { CatalogGatherProviderAuthOutcome, CatalogGatherProviderModelOutcome, GatherFlightCapture, ModelsAuthResolverFactory } from "./gather-capture";
|
|
105
|
-
import { applyProviderConfigHints, configuredAutoCompactTokenLimit, configuredMaxInputTokens, configuredReasoningSummarySupport, modelInputModalities, routedMaxOutputTokens, withVisionSidecarInput } from "./model-hints";
|
|
105
|
+
import { applyProviderConfigHints, configuredAutoCompactTokenLimit, configuredMaxInputTokens, configuredModelDisplayName, configuredReasoningSummarySupport, modelInputModalities, routedMaxOutputTokens, withVisionSidecarInput } from "./model-hints";
|
|
106
106
|
import { resolveComboCatalogMember } from "./combo-member";
|
|
107
107
|
import { captureGatherFlight, captureTrustedOpenAiApiPolicy, gatherFlightKey, keyedGatherBytesIdentity, withCanonicalOpenAiForwardAuthDefault } from "./gather-capture";
|
|
108
108
|
import { fetchProviderModelsWithAuth, observedModelsAuthResolver, refreshingModelsAuthResolver } from "./provider-models";
|
|
@@ -886,10 +886,12 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
|
886
886
|
undefined,
|
|
887
887
|
metadataConfigDir,
|
|
888
888
|
);
|
|
889
|
+
const displayName = configuredModelDisplayName(configured, id);
|
|
889
890
|
return {
|
|
890
891
|
provider: OPENAI_API_PROVIDER_ID,
|
|
891
892
|
id,
|
|
892
893
|
owned_by: OPENAI_API_PROVIDER_ID,
|
|
894
|
+
...(displayName ? { displayName } : {}),
|
|
893
895
|
...(contextWindow ? { contextWindow } : {}),
|
|
894
896
|
...(maxInputTokens ? { maxInputTokens } : {}),
|
|
895
897
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
@@ -4,6 +4,7 @@ import type { OcxConfig } from "../../types";
|
|
|
4
4
|
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
5
5
|
import { isOcxAuthoredRoutedEntry } from "./build-entries";
|
|
6
6
|
import { isNativeAliasCatalogEntry } from "./metadata";
|
|
7
|
+
import { hasOcxRoutedAuthorship } from "./ownership";
|
|
7
8
|
import type { RawCatalog, RawEntry } from "./parsing";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -25,10 +26,9 @@ function routedNamespace(entry: RawEntry): string | null {
|
|
|
25
26
|
if (trustedAccountBoundNativeCatalogSlug(entry) !== undefined) return null;
|
|
26
27
|
if (isNativeAliasCatalogEntry(entry)) return COMBO_NAMESPACE;
|
|
27
28
|
// Combo aliases may be bare or use an unrelated provider namespace. Ownership selects the
|
|
28
|
-
// combo namespace only with the same
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
&& entry.description.startsWith("Routed via opencodex → ")) return COMBO_NAMESPACE;
|
|
29
|
+
// combo namespace only with the same authorship signal as routed rows (generated description or
|
|
30
|
+
// the #7229 marker a custom combo description carries).
|
|
31
|
+
if (entry.owned_by === COMBO_NAMESPACE && hasOcxRoutedAuthorship(entry)) return COMBO_NAMESPACE;
|
|
32
32
|
// Foreign rows (Cursor, user tooling) are never removed by the provider rule.
|
|
33
33
|
if (!isOcxAuthoredRoutedEntry(entry)) return null;
|
|
34
34
|
return entry.slug.slice(0, entry.slug.indexOf("/"));
|
package/src/codex/convergence.ts
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
orderForSubagents,
|
|
54
54
|
} from "./catalog/sync";
|
|
55
55
|
import { multiAgentV2EnabledFromConfigText } from "./features";
|
|
56
|
-
import { enforceCatalogSlugUniqueness, exactComboCatalogSlugs } from "./catalog/aggregation";
|
|
56
|
+
import { comboCatalogDescriptions, enforceCatalogSlugUniqueness, exactComboCatalogSlugs } from "./catalog/aggregation";
|
|
57
57
|
import {
|
|
58
58
|
isNativeAliasCatalogEntry,
|
|
59
59
|
accountBoundNativeOpenAiSlugs,
|
|
@@ -392,6 +392,7 @@ function prepareCatalog(
|
|
|
392
392
|
multiAgentV2Enabled,
|
|
393
393
|
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
394
394
|
exactComboSlugs,
|
|
395
|
+
comboDescriptions: comboCatalogDescriptions(config),
|
|
395
396
|
hasPhysicalComboProvider,
|
|
396
397
|
includeNativeOpenAi,
|
|
397
398
|
accountBoundEntries,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { isTestHomeGuardArmed } from "../lib/test-home-guard";
|
|
3
|
+
|
|
4
|
+
/** What a low-quota notice may tell the operating system. Never an email, raw id or credential. */
|
|
5
|
+
export type LowQuotaNotice = {
|
|
6
|
+
/** Pseudonymous `p<hex6>` log label of the pool account. */
|
|
7
|
+
accountLabel: string;
|
|
8
|
+
window: "short" | "weekly";
|
|
9
|
+
percentUsed: number;
|
|
10
|
+
threshold: number;
|
|
11
|
+
/** Epoch milliseconds of the window reset, when known. */
|
|
12
|
+
resetAt: number | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Resolves when the OS notifier accepted the notice; rejects on failure, timeout or abort. */
|
|
16
|
+
export type LowQuotaNoticeSink = (notice: LowQuotaNotice, signal: AbortSignal) => Promise<void>;
|
|
17
|
+
|
|
18
|
+
type SpawnedNotifier = { exited: Promise<number | null>; kill(): void };
|
|
19
|
+
export type LowQuotaNotifierIo = {
|
|
20
|
+
platform?: NodeJS.Platform;
|
|
21
|
+
/** Returns true when the absolute path is an existing executable candidate. */
|
|
22
|
+
exists?: (path: string) => boolean;
|
|
23
|
+
spawn?: (argv: string[]) => SpawnedNotifier;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Formats the reset time for display; defaults to the host locale. */
|
|
26
|
+
formatTime?: (epochMs: number) => string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const OSASCRIPT = "/usr/bin/osascript";
|
|
30
|
+
/** Literal system locations only: a PATH lookup would let any earlier PATH entry run as the proxy. */
|
|
31
|
+
const NOTIFY_SEND_CANDIDATES = ["/usr/bin/notify-send", "/usr/local/bin/notify-send", "/bin/notify-send"];
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
33
|
+
const TITLE = "OpenCodex quota";
|
|
34
|
+
|
|
35
|
+
export function formatLowQuotaNotice(notice: LowQuotaNotice, formatTime: (epochMs: number) => string = defaultFormatTime): { title: string; body: string } {
|
|
36
|
+
const window = notice.window === "short" ? "5-hour" : "weekly";
|
|
37
|
+
const reset = notice.resetAt === null ? "reset time unknown" : `resets ${formatTime(notice.resetAt)}`;
|
|
38
|
+
return {
|
|
39
|
+
title: TITLE,
|
|
40
|
+
body: `Account ${notice.accountLabel}: ${window} quota at ${Math.round(notice.percentUsed)}% (threshold ${notice.threshold}%), ${reset}.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function defaultFormatTime(epochMs: number): string {
|
|
45
|
+
return new Date(epochMs).toLocaleString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** notify-send bodies may be parsed as markup by the notification server. */
|
|
49
|
+
function escapeMarkup(text: string): string {
|
|
50
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds the fixed argv for the host notifier. The notice text is always a separate argv item:
|
|
55
|
+
* the AppleScript source is constant and reads its arguments, and notify-send stops option
|
|
56
|
+
* parsing at `--`, so no account or quota text is ever interpreted as script or flags.
|
|
57
|
+
*/
|
|
58
|
+
export function lowQuotaNotifierArgv(
|
|
59
|
+
platform: NodeJS.Platform,
|
|
60
|
+
exists: (path: string) => boolean,
|
|
61
|
+
title: string,
|
|
62
|
+
body: string,
|
|
63
|
+
): string[] | undefined {
|
|
64
|
+
if (platform === "darwin") {
|
|
65
|
+
if (!exists(OSASCRIPT)) return undefined;
|
|
66
|
+
return [OSASCRIPT, "-e", "on run argv", "-e", "display notification (item 2 of argv) with title (item 1 of argv)", "-e", "end run", title, body];
|
|
67
|
+
}
|
|
68
|
+
if (platform === "linux") {
|
|
69
|
+
const notifySend = NOTIFY_SEND_CANDIDATES.find(exists);
|
|
70
|
+
if (!notifySend) return undefined;
|
|
71
|
+
return [notifySend, "--app-name=OpenCodex", "--", title, escapeMarkup(body)];
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function defaultSpawn(argv: string[]): SpawnedNotifier {
|
|
77
|
+
const proc = Bun.spawn(argv, { stdin: "ignore", stdout: "ignore", stderr: "ignore", windowsHide: true });
|
|
78
|
+
return { exited: proc.exited, kill: () => { try { proc.kill(); } catch { /* already gone */ } } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns a sink that hands notices to this host's notification service, or undefined when the
|
|
83
|
+
* platform has none we support (Windows and others stay log-only). An armed test process never
|
|
84
|
+
* reaches a real notifier unless a spawn is injected.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveLowQuotaDesktopSink(io: LowQuotaNotifierIo = {}): LowQuotaNoticeSink | undefined {
|
|
87
|
+
if (!io.spawn && isTestHomeGuardArmed()) return undefined;
|
|
88
|
+
const platform = io.platform ?? process.platform;
|
|
89
|
+
const exists = io.exists ?? existsSync;
|
|
90
|
+
const spawn = io.spawn ?? defaultSpawn;
|
|
91
|
+
const timeoutMs = io.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
92
|
+
const probe = lowQuotaNotifierArgv(platform, exists, TITLE, "");
|
|
93
|
+
if (!probe) return undefined;
|
|
94
|
+
return (notice, signal) => {
|
|
95
|
+
if (signal.aborted) return Promise.reject(new Error("low-quota notice cancelled"));
|
|
96
|
+
const { title, body } = formatLowQuotaNotice(notice, io.formatTime);
|
|
97
|
+
const argv = lowQuotaNotifierArgv(platform, exists, title, body);
|
|
98
|
+
if (!argv) return Promise.reject(new Error("low-quota notifier unavailable"));
|
|
99
|
+
let child: SpawnedNotifier;
|
|
100
|
+
try {
|
|
101
|
+
child = spawn(argv);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return Promise.reject(error);
|
|
104
|
+
}
|
|
105
|
+
return new Promise<void>((resolve, reject) => {
|
|
106
|
+
let settled = false;
|
|
107
|
+
const finish = (error?: Error) => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
signal.removeEventListener("abort", onAbort);
|
|
112
|
+
if (error) reject(error);
|
|
113
|
+
else resolve();
|
|
114
|
+
};
|
|
115
|
+
const onAbort = () => { child.kill(); finish(new Error("low-quota notice cancelled")); };
|
|
116
|
+
const timer = setTimeout(() => { child.kill(); finish(new Error("low-quota notifier timed out")); }, timeoutMs);
|
|
117
|
+
(timer as { unref?: () => void }).unref?.();
|
|
118
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
119
|
+
child.exited.then(
|
|
120
|
+
code => finish(code === 0 ? undefined : new Error(`low-quota notifier exited ${code}`)),
|
|
121
|
+
error => finish(error instanceof Error ? error : new Error("low-quota notifier failed")),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -4,15 +4,28 @@ import { isCodexAccountPaused, setCodexAccountPaused } from "./account-pause";
|
|
|
4
4
|
import { createLowQuotaEventLedger, type LowQuotaEvent } from "./low-quota-events";
|
|
5
5
|
import { registerLowQuotaObserver } from "./low-quota-observer";
|
|
6
6
|
import { resetAtToMs } from "./quota-types";
|
|
7
|
+
import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "./account-label";
|
|
8
|
+
import type { LowQuotaNotice, LowQuotaNoticeSink } from "./low-quota-desktop-notifier";
|
|
7
9
|
|
|
8
|
-
type Window = "short" | "weekly";
|
|
9
|
-
type Notice = { window: Window; percentUsed: number; threshold: number };
|
|
10
10
|
type Dependencies = {
|
|
11
11
|
persist?: (config: OcxConfig) => void | Promise<void>;
|
|
12
|
-
|
|
12
|
+
/** Injected sink; takes precedence over desktop delivery. */
|
|
13
|
+
notify?: (notice: LowQuotaNotice, signal: AbortSignal) => void | Promise<void>;
|
|
14
|
+
/** Resolved per notice, and only while the live policy opts into actions.desktop. */
|
|
15
|
+
desktopSink?: () => LowQuotaNoticeSink | undefined;
|
|
13
16
|
};
|
|
14
17
|
type EventBase = Omit<LowQuotaEvent, "timestamp" | "status" | "delivery">;
|
|
15
|
-
type Episode = {
|
|
18
|
+
type Episode = {
|
|
19
|
+
reset: string;
|
|
20
|
+
notice: "in-flight" | "logged" | "delivered" | "failed" | undefined;
|
|
21
|
+
pausedByUs: boolean;
|
|
22
|
+
noticeBase?: EventBase;
|
|
23
|
+
noticeAbort?: AbortController;
|
|
24
|
+
/** A failed notice may retry only after this time, so a broken notifier is not respawned per request. */
|
|
25
|
+
noticeRetryAt?: number;
|
|
26
|
+
/** The episode's log line was written; retries do not repeat it. */
|
|
27
|
+
warned?: boolean;
|
|
28
|
+
};
|
|
16
29
|
export type LowQuotaRegistration = (() => void) & {
|
|
17
30
|
hasPendingSave(): boolean;
|
|
18
31
|
flush(): Promise<void>;
|
|
@@ -20,6 +33,7 @@ export type LowQuotaRegistration = (() => void) & {
|
|
|
20
33
|
};
|
|
21
34
|
|
|
22
35
|
const RETRY_DELAYS_MS = [100, 250];
|
|
36
|
+
const NOTICE_RETRY_MS = 5 * 60_000;
|
|
23
37
|
const FLUSH_DEADLINE_MS = 500;
|
|
24
38
|
|
|
25
39
|
/** Each server owns its own policy, episode state and deferred writer. */
|
|
@@ -44,6 +58,12 @@ export function registerCodexLowQuotaProtection(config: OcxConfig, deps: Depende
|
|
|
44
58
|
if (delivery === "pause-save") pauseStatus.set(base, status);
|
|
45
59
|
ledger.publish({ ...base, delivery, status, timestamp: Date.now() });
|
|
46
60
|
}
|
|
61
|
+
/** Publishes one cancellation for an in-flight notice and stops its delivery. */
|
|
62
|
+
function cancelNotice(episode: Episode): void {
|
|
63
|
+
if (episode.notice === "in-flight" && episode.noticeBase) event(episode.noticeBase, "notice", "cancelled");
|
|
64
|
+
episode.noticeAbort?.abort();
|
|
65
|
+
episode.noticeAbort = undefined;
|
|
66
|
+
}
|
|
47
67
|
function cancelTimer(): void {
|
|
48
68
|
if (saveTimer) clearTimeout(saveTimer);
|
|
49
69
|
saveTimer = null;
|
|
@@ -132,9 +152,7 @@ export function registerCodexLowQuotaProtection(config: OcxConfig, deps: Depende
|
|
|
132
152
|
if (pauseStatus.get(base) === "pending") event(base, "pause-save", "cancelled");
|
|
133
153
|
pendingPauseEvents.delete(key);
|
|
134
154
|
}
|
|
135
|
-
for (const episode of episodes.values())
|
|
136
|
-
if (episode.notice === "in-flight" && episode.noticeBase) event(episode.noticeBase, "notice", "cancelled");
|
|
137
|
-
}
|
|
155
|
+
for (const episode of episodes.values()) cancelNotice(episode);
|
|
138
156
|
dirty = false;
|
|
139
157
|
autoPausedAccounts.clear();
|
|
140
158
|
unregister();
|
|
@@ -175,9 +193,7 @@ export function registerCodexLowQuotaProtection(config: OcxConfig, deps: Depende
|
|
|
175
193
|
const policy = config.codexPool?.lowQuotaProtection;
|
|
176
194
|
const nextKey = JSON.stringify(policy);
|
|
177
195
|
if (policyKey !== nextKey) {
|
|
178
|
-
for (const episode of episodes.values())
|
|
179
|
-
if (episode.notice === "in-flight" && episode.noticeBase) event(episode.noticeBase, "notice", "cancelled");
|
|
180
|
-
}
|
|
196
|
+
for (const episode of episodes.values()) cancelNotice(episode);
|
|
181
197
|
episodes.clear();
|
|
182
198
|
autoPausedAccounts.clear();
|
|
183
199
|
policyKey = nextKey;
|
|
@@ -187,7 +203,7 @@ export function registerCodexLowQuotaProtection(config: OcxConfig, deps: Depende
|
|
|
187
203
|
for (const id of autoPausedAccounts) if (!liveIds.has(id)) autoPausedAccounts.delete(id);
|
|
188
204
|
for (const [key, episode] of episodes) {
|
|
189
205
|
if (liveIds.has(key.split("\u0000")[0]!)) continue;
|
|
190
|
-
|
|
206
|
+
cancelNotice(episode);
|
|
191
207
|
episodes.delete(key);
|
|
192
208
|
}
|
|
193
209
|
if (!liveIds.has(accountId)) return;
|
|
@@ -203,7 +219,7 @@ export function registerCodexLowQuotaProtection(config: OcxConfig, deps: Depende
|
|
|
203
219
|
const key = `${accountId}\u0000${window}`;
|
|
204
220
|
if (percentUsed < policy.threshold) {
|
|
205
221
|
const prior = episodes.get(key);
|
|
206
|
-
if (prior
|
|
222
|
+
if (prior) cancelNotice(prior);
|
|
207
223
|
episodes.delete(key);
|
|
208
224
|
continue;
|
|
209
225
|
}
|
|
@@ -211,6 +227,7 @@ export function registerCodexLowQuotaProtection(config: OcxConfig, deps: Depende
|
|
|
211
227
|
const reset = resetAt === null ? "unknown" : String(resetAt);
|
|
212
228
|
let episode = episodes.get(key);
|
|
213
229
|
if (!episode || episode.reset !== reset) {
|
|
230
|
+
if (episode) cancelNotice(episode);
|
|
214
231
|
episode = { reset, notice: undefined, pausedByUs: false };
|
|
215
232
|
episodes.set(key, episode);
|
|
216
233
|
}
|
|
@@ -234,32 +251,41 @@ export function registerCodexLowQuotaProtection(config: OcxConfig, deps: Depende
|
|
|
234
251
|
scheduleSave();
|
|
235
252
|
}
|
|
236
253
|
// A manual resume leaves pausedByUs set until recovery or a new reset episode.
|
|
237
|
-
if (policy.actions.notify && episode.notice !== "in-flight" && episode.notice !== "logged" && episode.notice !== "delivered"
|
|
238
|
-
|
|
239
|
-
if (!
|
|
254
|
+
if (policy.actions.notify && episode.notice !== "in-flight" && episode.notice !== "logged" && episode.notice !== "delivered"
|
|
255
|
+
&& !(episode.notice === "failed" && (episode.noticeRetryAt ?? 0) > Date.now())) {
|
|
256
|
+
if (!episode.warned) {
|
|
257
|
+
episode.warned = true;
|
|
258
|
+
console.warn(`[codex-low-quota] ${window === "short" ? "5-hour" : "weekly"} quota reached ${percentUsed}% used (threshold ${policy.threshold}%)`);
|
|
259
|
+
}
|
|
260
|
+
const sink = deps.notify ?? (policy.actions.desktop === true ? deps.desktopSink?.() : undefined);
|
|
261
|
+
if (!sink) {
|
|
240
262
|
event(base, "notice", "logged");
|
|
241
263
|
episode.notice = "logged";
|
|
242
264
|
continue;
|
|
243
265
|
}
|
|
244
|
-
|
|
245
|
-
|
|
266
|
+
const account = config.codexAccounts?.find(candidate => candidate.id === accountId);
|
|
267
|
+
const notice: LowQuotaNotice = {
|
|
268
|
+
accountLabel: account ? codexAccountLogLabel(account) : fallbackCodexAccountLogLabel(accountId),
|
|
269
|
+
window, percentUsed, threshold: policy.threshold, resetAt,
|
|
270
|
+
};
|
|
271
|
+
const abort = new AbortController();
|
|
272
|
+
const current = episode;
|
|
273
|
+
current.notice = "in-flight";
|
|
274
|
+
current.noticeBase = base;
|
|
275
|
+
current.noticeAbort = abort;
|
|
246
276
|
event(base, "notice", "pending");
|
|
277
|
+
const settle = (status: "delivered" | "failed") => {
|
|
278
|
+
if (closed || abort.signal.aborted || episodes.get(key) !== current) return;
|
|
279
|
+
event(base, "notice", status);
|
|
280
|
+
current.notice = status;
|
|
281
|
+
current.noticeRetryAt = status === "failed" ? Date.now() + NOTICE_RETRY_MS : undefined;
|
|
282
|
+
current.noticeBase = undefined;
|
|
283
|
+
current.noticeAbort = undefined;
|
|
284
|
+
};
|
|
247
285
|
try {
|
|
248
|
-
void Promise.resolve(
|
|
249
|
-
if (closed || episodes.get(key) !== episode) return;
|
|
250
|
-
event(base, "notice", "delivered");
|
|
251
|
-
episode.notice = "delivered";
|
|
252
|
-
episode.noticeBase = undefined;
|
|
253
|
-
}, () => {
|
|
254
|
-
if (closed || episodes.get(key) !== episode) return;
|
|
255
|
-
event(base, "notice", "failed");
|
|
256
|
-
episode.notice = "failed";
|
|
257
|
-
episode.noticeBase = undefined;
|
|
258
|
-
});
|
|
286
|
+
void Promise.resolve(sink(notice, abort.signal)).then(() => settle("delivered"), () => settle("failed"));
|
|
259
287
|
} catch {
|
|
260
|
-
|
|
261
|
-
episode.notice = "failed";
|
|
262
|
-
episode.noticeBase = undefined;
|
|
288
|
+
settle("failed");
|
|
263
289
|
}
|
|
264
290
|
}
|
|
265
291
|
}
|