@bitkyc08/opencodex 2.8.2-preview.20260731 → 2.9.1
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 +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-CuVjugeE.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +86 -5
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +89 -14
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +7 -1
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +8 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro.ts +10 -2
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +239 -97
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +75 -32
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +37 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +173 -53
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +51 -12
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +23 -3
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +45 -18
- package/src/types.ts +34 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
- package/gui/dist/assets/index-GC0Vlu1Z.js +0 -67
package/src/codex/shim.ts
CHANGED
|
@@ -25,12 +25,14 @@ import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
|
25
25
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
26
26
|
import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
27
27
|
import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
28
|
+
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
28
29
|
|
|
29
30
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
30
31
|
const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
|
|
31
32
|
export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
|
|
32
33
|
export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
|
|
33
34
|
const CODEX_SHIM_RESTORE_LOCK_STALE_MS = 30_000;
|
|
35
|
+
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
34
36
|
let lastShimDiscoveryError: string | null = null;
|
|
35
37
|
/** Last human-readable reason discovery returned null (exposed for doctor/tests). */
|
|
36
38
|
export function lastCodexDiscoveryError(): string | null {
|
|
@@ -309,10 +311,12 @@ export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
|
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
if (skippedInterop) {
|
|
312
|
-
lastShimDiscoveryError =
|
|
314
|
+
lastShimDiscoveryError = truncateRetainedUtf8(
|
|
313
315
|
`Found a Windows codex at ${skippedInterop} via WSL PATH interop, but no Linux-side codex. ` +
|
|
314
316
|
"Refusing to shim a Windows launcher from WSL (a WSL shim breaks Windows invocations). " +
|
|
315
|
-
"Install codex inside WSL (npm i -g @openai/codex), or run 'ocx ensure' from Windows to shim the Windows side."
|
|
317
|
+
"Install codex inside WSL (npm i -g @openai/codex), or run 'ocx ensure' from Windows to shim the Windows side.",
|
|
318
|
+
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
319
|
+
);
|
|
316
320
|
}
|
|
317
321
|
return null;
|
|
318
322
|
}
|
|
@@ -324,9 +328,11 @@ function findWindowsCodexTargets(): ShimFileState[] | null {
|
|
|
324
328
|
if (existsSync(exe) && !isShim(exe)) {
|
|
325
329
|
try {
|
|
326
330
|
if (!lstatSync(exe).isDirectory()) {
|
|
327
|
-
lastShimDiscoveryError =
|
|
331
|
+
lastShimDiscoveryError = truncateRetainedUtf8(
|
|
328
332
|
`Found codex.exe at ${exe}. Refusing to rename a real .exe because exact codex.exe invocations would break; ` +
|
|
329
|
-
"install a codex.cmd/codex.ps1 launcher or use `ocx service install` for autostart."
|
|
333
|
+
"install a codex.cmd/codex.ps1 launcher or use `ocx service install` for autostart.",
|
|
334
|
+
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
335
|
+
);
|
|
330
336
|
return null;
|
|
331
337
|
}
|
|
332
338
|
} catch { /* keep scanning */ }
|
|
@@ -26,6 +26,7 @@ import { isThreadSpawnRequest } from "../server/effort-policy";
|
|
|
26
26
|
import { PROVIDER_REGISTRY } from "../providers/registry";
|
|
27
27
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
28
28
|
import { routeModel, type RouteResult } from "../router";
|
|
29
|
+
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
29
30
|
export const DEFAULT_SUBAGENT_MODEL_FALLBACK_POLL_MS = 60_000;
|
|
30
31
|
|
|
31
32
|
type SubagentQuotaPrimeFn = (config: OcxConfig, reason: string) => Promise<void>;
|
|
@@ -245,6 +246,17 @@ export function noteSubagentModelFailure(
|
|
|
245
246
|
reason: "quota_exhausted",
|
|
246
247
|
},
|
|
247
248
|
);
|
|
249
|
+
sweepExpiredOnWrite(now);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function sweepExpiredSubagentModelHealth(now = Date.now()): number {
|
|
253
|
+
let removed = 0;
|
|
254
|
+
for (const [key, health] of modelHealth) {
|
|
255
|
+
if (health.unavailableUntil > now) continue;
|
|
256
|
+
modelHealth.delete(key);
|
|
257
|
+
removed += 1;
|
|
258
|
+
}
|
|
259
|
+
return removed;
|
|
248
260
|
}
|
|
249
261
|
|
|
250
262
|
export function resetSubagentModelFallbackStateForTests(): void {
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import type { ServerWebSocket } from "bun";
|
|
2
2
|
import type { WsData } from "../server/ws-bridge";
|
|
3
|
+
import { createAdmissionGate, type AdmissionMetrics, type AdmissionReservation } from "../lib/admission";
|
|
4
|
+
|
|
5
|
+
export const MAX_TRACKED_CODEX_WEBSOCKETS = 128;
|
|
6
|
+
const websocketGate = createAdmissionGate("codex_websockets", MAX_TRACKED_CODEX_WEBSOCKETS);
|
|
7
|
+
|
|
8
|
+
export function tryReserveCodexWebSocket(): AdmissionReservation<ServerWebSocket<WsData>> | null {
|
|
9
|
+
const gateLease = websocketGate.tryAcquire();
|
|
10
|
+
if (!gateLease) return null;
|
|
11
|
+
let socket: ServerWebSocket<WsData> | undefined;
|
|
12
|
+
let active = true;
|
|
13
|
+
return {
|
|
14
|
+
bind(value) {
|
|
15
|
+
if (!active) return;
|
|
16
|
+
socket = value;
|
|
17
|
+
},
|
|
18
|
+
release() {
|
|
19
|
+
if (!active) return;
|
|
20
|
+
active = false;
|
|
21
|
+
socket = undefined;
|
|
22
|
+
gateLease.release();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function codexWebSocketAdmissionMetrics(): AdmissionMetrics {
|
|
28
|
+
return websocketGate.metrics();
|
|
29
|
+
}
|
|
3
30
|
|
|
4
31
|
const socketsByAccount = new Map<string, Set<ServerWebSocket<WsData>>>();
|
|
5
32
|
|
package/src/combos/failover.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { classifyError, isCyberPolicyCode } from "../lib/errors";
|
|
2
2
|
import type { OcxComboTarget } from "../types";
|
|
3
3
|
import { targetKey } from "./types";
|
|
4
|
+
import {
|
|
5
|
+
captureConfigGeneration,
|
|
6
|
+
sweepExpiredOnWrite,
|
|
7
|
+
type GenerationContext,
|
|
8
|
+
} from "../lib/state-store-sweeper";
|
|
4
9
|
|
|
5
10
|
interface TargetCooldown {
|
|
6
11
|
cooldownUntil: number;
|
|
@@ -11,6 +16,8 @@ const MAX_COOLDOWN_MS = 10 * 60_000;
|
|
|
11
16
|
|
|
12
17
|
/** Map<`${comboId}\0${provider/model}`, TargetCooldown> */
|
|
13
18
|
const targetCooldowns = new Map<string, TargetCooldown>();
|
|
19
|
+
let lastReconciledGeneration = 0;
|
|
20
|
+
let liveComboTargets = new Set<string>();
|
|
14
21
|
|
|
15
22
|
function cooldownMapKey(
|
|
16
23
|
comboId: string,
|
|
@@ -55,20 +62,43 @@ export function isComboTargetInCooldown(
|
|
|
55
62
|
export function coolComboTarget(
|
|
56
63
|
comboId: string,
|
|
57
64
|
target: Pick<OcxComboTarget, "provider" | "model">,
|
|
58
|
-
options?: { retryAfter?: string | null; now?: number; cooldownMs?: number },
|
|
65
|
+
options?: { retryAfter?: string | null; now?: number; cooldownMs?: number; writerGeneration?: number },
|
|
59
66
|
): void {
|
|
60
67
|
const now = options?.now ?? Date.now();
|
|
68
|
+
const writerGeneration = options?.writerGeneration ?? captureConfigGeneration();
|
|
69
|
+
const ownerKey = `${comboId}::${targetKey(target)}`;
|
|
70
|
+
if (writerGeneration < lastReconciledGeneration && !liveComboTargets.has(ownerKey)) return;
|
|
61
71
|
const cooldownMs = options?.cooldownMs
|
|
62
72
|
?? parseRetryAfterMs(options?.retryAfter, now)
|
|
63
73
|
?? DEFAULT_COOLDOWN_MS;
|
|
64
74
|
targetCooldowns.set(cooldownMapKey(comboId, target), {
|
|
65
75
|
cooldownUntil: now + Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS),
|
|
66
76
|
});
|
|
77
|
+
sweepExpiredOnWrite(now);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function reconcileComboTargetCooldowns(context: GenerationContext): number {
|
|
81
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
82
|
+
liveComboTargets = new Set(context.comboTargets);
|
|
83
|
+
lastReconciledGeneration = context.generation;
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function sweepExpiredComboTargetCooldowns(now = Date.now()): number {
|
|
88
|
+
let removed = 0;
|
|
89
|
+
for (const [key, cooldown] of targetCooldowns) {
|
|
90
|
+
if (cooldown.cooldownUntil > now) continue;
|
|
91
|
+
targetCooldowns.delete(key);
|
|
92
|
+
removed += 1;
|
|
93
|
+
}
|
|
94
|
+
return removed;
|
|
67
95
|
}
|
|
68
96
|
|
|
69
97
|
export function clearComboTargetCooldowns(comboId?: string): void {
|
|
70
98
|
if (comboId === undefined) {
|
|
71
99
|
targetCooldowns.clear();
|
|
100
|
+
liveComboTargets.clear();
|
|
101
|
+
lastReconciledGeneration = 0;
|
|
72
102
|
return;
|
|
73
103
|
}
|
|
74
104
|
const prefix = `${comboId}\0`;
|
package/src/combos/request.ts
CHANGED
|
@@ -2,6 +2,15 @@ import type { OcxComboDefaultEffort, OcxComboTarget, OcxConfig } from "../types"
|
|
|
2
2
|
import { resolveComboId } from "./types";
|
|
3
3
|
|
|
4
4
|
const warnedUnsupportedDefaults = new Set<string>();
|
|
5
|
+
let lastWarningReconciledGeneration = 0;
|
|
6
|
+
|
|
7
|
+
export function reconcileComboWarningMemos(generation: number): number {
|
|
8
|
+
if (generation <= lastWarningReconciledGeneration) return 0;
|
|
9
|
+
const removed = warnedUnsupportedDefaults.size;
|
|
10
|
+
warnedUnsupportedDefaults.clear();
|
|
11
|
+
lastWarningReconciledGeneration = generation;
|
|
12
|
+
return removed;
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
export function resetComboEffortWarningStateForTests(): void {
|
|
7
16
|
warnedUnsupportedDefaults.clear();
|
package/src/combos/resolve.ts
CHANGED
|
@@ -2,12 +2,17 @@ import type { OcxComboTarget, OcxConfig } from "../types";
|
|
|
2
2
|
import { coolComboTarget, isComboTargetInCooldown } from "./failover";
|
|
3
3
|
import { getCombo, resolveComboId, targetKey } from "./types";
|
|
4
4
|
import type { NormalizedComboConfig } from "./types";
|
|
5
|
+
import {
|
|
6
|
+
captureConfigGeneration,
|
|
7
|
+
type GenerationContext,
|
|
8
|
+
} from "../lib/state-store-sweeper";
|
|
5
9
|
|
|
6
10
|
export interface ComboPick {
|
|
7
11
|
comboId: string;
|
|
8
12
|
target: Required<OcxComboTarget>;
|
|
9
13
|
targetIndex: number;
|
|
10
14
|
attempted: string[];
|
|
15
|
+
writerGeneration: number;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
interface SelectionState {
|
|
@@ -17,6 +22,17 @@ interface SelectionState {
|
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
const selectionState = new Map<string, SelectionState>();
|
|
25
|
+
let lastReconciledGeneration = 0;
|
|
26
|
+
let liveComboTargets = new Set<string>();
|
|
27
|
+
|
|
28
|
+
function comboTargetOwnerKey(comboId: string, key: string): string {
|
|
29
|
+
return `${comboId}::${key}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mayCommitComboState(comboId: string, key: string, writerGeneration: number): boolean {
|
|
33
|
+
return writerGeneration >= lastReconciledGeneration
|
|
34
|
+
|| liveComboTargets.has(comboTargetOwnerKey(comboId, key));
|
|
35
|
+
}
|
|
20
36
|
|
|
21
37
|
export class UnknownComboError extends Error {
|
|
22
38
|
constructor(readonly comboId: string) {
|
|
@@ -74,6 +90,7 @@ export function pickComboTarget(
|
|
|
74
90
|
eligible?: (target: Required<OcxComboTarget>) => boolean;
|
|
75
91
|
} = {},
|
|
76
92
|
): ComboPick | null {
|
|
93
|
+
const writerGeneration = captureConfigGeneration();
|
|
77
94
|
const combo = getCombo(config, comboId);
|
|
78
95
|
if (!combo) throw new UnknownComboError(comboId);
|
|
79
96
|
const excluded = new Set(options.exclude ?? []);
|
|
@@ -114,6 +131,7 @@ export function pickComboTarget(
|
|
|
114
131
|
target,
|
|
115
132
|
targetIndex,
|
|
116
133
|
attempted: [...excluded, targetKey(target)],
|
|
134
|
+
writerGeneration,
|
|
117
135
|
};
|
|
118
136
|
}
|
|
119
137
|
|
|
@@ -121,10 +139,13 @@ export function noteComboSuccess(
|
|
|
121
139
|
comboId: string,
|
|
122
140
|
combo: NormalizedComboConfig,
|
|
123
141
|
target: Required<OcxComboTarget>,
|
|
142
|
+
writerGeneration = captureConfigGeneration(),
|
|
124
143
|
): void {
|
|
125
144
|
if (combo.strategy !== "round-robin") return;
|
|
145
|
+
const key = targetKey(target);
|
|
146
|
+
if (!mayCommitComboState(comboId, key, writerGeneration)) return;
|
|
126
147
|
const state = selectionState.get(comboId);
|
|
127
|
-
if (!state || state.activeKey !==
|
|
148
|
+
if (!state || state.activeKey !== key) return;
|
|
128
149
|
state.successes += 1;
|
|
129
150
|
if (state.successes >= combo.stickyLimit) {
|
|
130
151
|
delete state.activeKey;
|
|
@@ -132,7 +153,12 @@ export function noteComboSuccess(
|
|
|
132
153
|
}
|
|
133
154
|
}
|
|
134
155
|
|
|
135
|
-
export function noteComboFailure(
|
|
156
|
+
export function noteComboFailure(
|
|
157
|
+
comboId: string,
|
|
158
|
+
target: OcxComboTarget,
|
|
159
|
+
writerGeneration = captureConfigGeneration(),
|
|
160
|
+
): void {
|
|
161
|
+
if (!mayCommitComboState(comboId, targetKey(target), writerGeneration)) return;
|
|
136
162
|
const state = selectionState.get(comboId);
|
|
137
163
|
if (state?.activeKey === targetKey(target)) {
|
|
138
164
|
delete state.activeKey;
|
|
@@ -149,8 +175,11 @@ export function advanceComboAfterFailure(
|
|
|
149
175
|
eligible?: (target: Required<OcxComboTarget>) => boolean;
|
|
150
176
|
} = {},
|
|
151
177
|
): ComboPick | null {
|
|
152
|
-
noteComboFailure(pick.comboId, pick.target);
|
|
153
|
-
coolComboTarget(pick.comboId, pick.target,
|
|
178
|
+
noteComboFailure(pick.comboId, pick.target, pick.writerGeneration);
|
|
179
|
+
coolComboTarget(pick.comboId, pick.target, {
|
|
180
|
+
...options,
|
|
181
|
+
writerGeneration: pick.writerGeneration,
|
|
182
|
+
});
|
|
154
183
|
return pickComboTarget(config, pick.comboId, {
|
|
155
184
|
exclude: pick.attempted,
|
|
156
185
|
eligible: target => !isComboTargetInCooldown(pick.comboId, target, options.now)
|
|
@@ -158,9 +187,36 @@ export function advanceComboAfterFailure(
|
|
|
158
187
|
});
|
|
159
188
|
}
|
|
160
189
|
|
|
190
|
+
export function reconcileComboRotationState(context: GenerationContext): number {
|
|
191
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
192
|
+
let removed = 0;
|
|
193
|
+
for (const [comboId, state] of selectionState) {
|
|
194
|
+
if (!context.comboIds.has(comboId)) {
|
|
195
|
+
selectionState.delete(comboId);
|
|
196
|
+
removed += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (state.activeKey && !context.comboTargets.has(comboTargetOwnerKey(comboId, state.activeKey))) {
|
|
200
|
+
delete state.activeKey;
|
|
201
|
+
state.successes = 0;
|
|
202
|
+
removed += 1;
|
|
203
|
+
}
|
|
204
|
+
for (const key of state.currentWeights.keys()) {
|
|
205
|
+
if (context.comboTargets.has(comboTargetOwnerKey(comboId, key))) continue;
|
|
206
|
+
state.currentWeights.delete(key);
|
|
207
|
+
removed += 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
liveComboTargets = new Set(context.comboTargets);
|
|
211
|
+
lastReconciledGeneration = context.generation;
|
|
212
|
+
return removed;
|
|
213
|
+
}
|
|
214
|
+
|
|
161
215
|
export function clearComboSelectionState(comboId?: string): void {
|
|
162
216
|
if (comboId === undefined) {
|
|
163
217
|
selectionState.clear();
|
|
218
|
+
liveComboTargets.clear();
|
|
219
|
+
lastReconciledGeneration = 0;
|
|
164
220
|
return;
|
|
165
221
|
}
|
|
166
222
|
selectionState.delete(comboId);
|
package/src/combos/types.ts
CHANGED
|
@@ -304,6 +304,18 @@ export function listComboIds(config: { combos?: Record<string, OcxComboConfig> }
|
|
|
304
304
|
return Object.keys(config.combos ?? {}).sort((a, b) => a.localeCompare(b));
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
export function listLiveComboTargetKeys(
|
|
308
|
+
config: { combos?: Record<string, OcxComboConfig> },
|
|
309
|
+
): ReadonlySet<string> {
|
|
310
|
+
const keys = new Set<string>();
|
|
311
|
+
for (const id of listComboIds(config)) {
|
|
312
|
+
const combo = getCombo(config, id);
|
|
313
|
+
if (!combo) continue;
|
|
314
|
+
for (const target of combo.targets) keys.add(`${id}::${targetKey(target)}`);
|
|
315
|
+
}
|
|
316
|
+
return keys;
|
|
317
|
+
}
|
|
318
|
+
|
|
307
319
|
export function getCombo(
|
|
308
320
|
config: { combos?: Record<string, OcxComboConfig> },
|
|
309
321
|
id: string,
|