feat: add sub2api sentinel trust cadence

This commit is contained in:
Codex
2026-06-11 13:55:25 +00:00
parent ea92eed148
commit d475cf2a9e
4 changed files with 159 additions and 15 deletions
@@ -32,6 +32,8 @@ export interface CodexPoolSentinelConfig {
cadence: {
successInitialIntervalMinutes: number;
successMaxIntervalMinutes: number;
trustedSuccessMaxIntervalMinutes: number;
untrustedSuccessMaxIntervalMinutes: number;
successBackoffMultiplier: number;
jitterPercent: number;
};
@@ -61,6 +63,7 @@ export interface CodexPoolSentinelProfileSecret {
baseUrl: string;
apiKey: string;
upstreamUserAgent: string | null;
trustUpstream: boolean;
}
export interface CodexPoolSentinelManifestOptions {
@@ -103,6 +106,8 @@ export function defaultCodexPoolSentinelConfig(): CodexPoolSentinelConfig {
cadence: {
successInitialIntervalMinutes: 1,
successMaxIntervalMinutes: 20,
trustedSuccessMaxIntervalMinutes: 20,
untrustedSuccessMaxIntervalMinutes: 2,
successBackoffMultiplier: 2,
jitterPercent: 10,
},
@@ -145,6 +150,9 @@ export function readCodexPoolSentinelConfig(value: unknown, defaults: CodexPoolS
const cadence = isRecord(value.cadence) ? value.cadence : {};
const freeze = isRecord(value.freeze) ? value.freeze : {};
const pricing = isRecord(value.pricing) ? value.pricing : {};
const legacySuccessMax = readInt(valueAt(cadence, "successMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.successMaxIntervalMinutes`, defaults.cadence.successMaxIntervalMinutes, 1, 1440);
const trustedSuccessMax = readInt(valueAt(cadence, "trustedSuccessMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.trustedSuccessMaxIntervalMinutes`, legacySuccessMax, 1, 1440);
const untrustedSuccessMax = readInt(valueAt(cadence, "untrustedSuccessMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.untrustedSuccessMaxIntervalMinutes`, legacySuccessMax, 1, 1440);
const config: CodexPoolSentinelConfig = {
monitor: {
enabled: readBoolean(valueAt(monitor, "enabled"), `${sourcePath}.sentinel.monitor.enabled`, defaults.monitor.enabled),
@@ -176,7 +184,9 @@ export function readCodexPoolSentinelConfig(value: unknown, defaults: CodexPoolS
},
cadence: {
successInitialIntervalMinutes: readInt(valueAt(cadence, "successInitialIntervalMinutes"), `${sourcePath}.sentinel.cadence.successInitialIntervalMinutes`, defaults.cadence.successInitialIntervalMinutes, 1, 1440),
successMaxIntervalMinutes: readInt(valueAt(cadence, "successMaxIntervalMinutes"), `${sourcePath}.sentinel.cadence.successMaxIntervalMinutes`, defaults.cadence.successMaxIntervalMinutes, 1, 1440),
successMaxIntervalMinutes: legacySuccessMax,
trustedSuccessMaxIntervalMinutes: trustedSuccessMax,
untrustedSuccessMaxIntervalMinutes: untrustedSuccessMax,
successBackoffMultiplier: readInt(valueAt(cadence, "successBackoffMultiplier"), `${sourcePath}.sentinel.cadence.successBackoffMultiplier`, defaults.cadence.successBackoffMultiplier, 1, 10),
jitterPercent: readInt(valueAt(cadence, "jitterPercent"), `${sourcePath}.sentinel.cadence.jitterPercent`, defaults.cadence.jitterPercent, 0, 50),
},
@@ -198,6 +208,12 @@ export function readCodexPoolSentinelConfig(value: unknown, defaults: CodexPoolS
if (config.cadence.successMaxIntervalMinutes < config.cadence.successInitialIntervalMinutes) {
throw new Error(`${sourcePath}.sentinel.cadence.successMaxIntervalMinutes must be >= successInitialIntervalMinutes`);
}
if (config.cadence.trustedSuccessMaxIntervalMinutes < config.cadence.successInitialIntervalMinutes) {
throw new Error(`${sourcePath}.sentinel.cadence.trustedSuccessMaxIntervalMinutes must be >= successInitialIntervalMinutes`);
}
if (config.cadence.untrustedSuccessMaxIntervalMinutes < config.cadence.successInitialIntervalMinutes) {
throw new Error(`${sourcePath}.sentinel.cadence.untrustedSuccessMaxIntervalMinutes must be >= successInitialIntervalMinutes`);
}
if (config.freeze.maxTtlMinutes < config.freeze.initialTtlMinutes) {
throw new Error(`${sourcePath}.sentinel.freeze.maxTtlMinutes must be >= initialTtlMinutes`);
}
@@ -1032,6 +1048,7 @@ def probe_account(profile, config, purpose):
return {
"accountName": profile["accountName"],
"profile": profile.get("profile"),
"trustUpstream": profile.get("trustUpstream") is True,
"purpose": purpose,
"ok": ok,
"markerMatched": marker_matched,
@@ -1115,11 +1132,17 @@ def choose_forced_profiles(profiles, state, config, now, names):
missing = sorted(name for name in names if name not in set(found))
return due, {"selected": len(due), "due": len(due), "limit": "forced-accounts", "budgetMode": "record-only", "ledger": ledger_for(state, now)[1], "requestedAccounts": sorted(names), "missingAccounts": missing}
def next_success_interval(account_state, config):
def success_max_interval(profile, config):
cadence = config["cadence"]
if profile.get("trustUpstream") is True:
return int(cadence.get("trustedSuccessMaxIntervalMinutes") or cadence.get("successMaxIntervalMinutes"))
return int(cadence.get("untrustedSuccessMaxIntervalMinutes") or cadence.get("successMaxIntervalMinutes"))
def next_success_interval(account_state, config, profile):
streak = int(account_state.get("successStreak") or 0)
previous = int(account_state.get("successIntervalMinutes") or 0)
initial = int(config["cadence"]["successInitialIntervalMinutes"])
maximum = int(config["cadence"]["successMaxIntervalMinutes"])
maximum = success_max_interval(profile, config)
multiplier = int(config["cadence"]["successBackoffMultiplier"])
return initial if streak <= 0 or previous <= 0 else min(maximum, max(initial, previous * multiplier))
@@ -1133,7 +1156,7 @@ def next_freeze_interval(account_state, config, was_recovery):
return min(maximum, max(initial, previous * multiplier))
return initial
def apply_result(result, state, config, now, admin):
def apply_result(result, state, config, now, admin, profile):
name = result["accountName"]
account_state = state.setdefault("accounts", {}).setdefault(name, {})
add_usage(state, account_state, now, result.get("usage") or {})
@@ -1154,9 +1177,10 @@ def apply_result(result, state, config, now, admin):
account_state["qualityGate"] = {**quality_gate, "pending": False, "clearedAt": iso(now)}
account_state["successStreak"] = 0
account_state["successIntervalMinutes"] = 0
interval = next_success_interval(account_state, config)
interval = next_success_interval(account_state, config, profile)
account_state["successStreak"] = int(account_state.get("successStreak") or 0) + 1
account_state["successIntervalMinutes"] = interval
account_state["successMaxIntervalMinutes"] = success_max_interval(profile, config)
account_state["nextProbeAfter"] = iso(add_minutes(now, interval, int(config["cadence"]["jitterPercent"])))
account_state["lastOkAt"] = iso(now)
account_state["lastStatus"] = "ok"
@@ -1190,6 +1214,7 @@ def apply_result(result, state, config, now, admin):
account_state["nextProbeAfter"] = iso(until)
account_state["successStreak"] = 0
account_state["successIntervalMinutes"] = 0
account_state["successMaxIntervalMinutes"] = success_max_interval(profile, config)
account_state["lastStatus"] = "quarantined"
else:
retry = int(config["probe"]["transportRetryMinutes"])
@@ -1197,9 +1222,12 @@ def apply_result(result, state, config, now, admin):
account_state["lastStatus"] = "marker-not-matched-no-freeze"
account_state["lastFailureAt"] = iso(now)
account_state["lastProbeAt"] = iso(now)
account_state["trustUpstream"] = profile.get("trustUpstream") is True
account_state["lastProbe"] = {
"ok": result.get("ok"),
"purpose": result.get("purpose"),
"trustUpstream": result.get("trustUpstream"),
"successMaxIntervalMinutes": success_max_interval(profile, config),
"httpStatus": result.get("httpStatus"),
"durationMs": result.get("durationMs"),
"markerMatched": result.get("markerMatched"),
@@ -1263,11 +1291,12 @@ def main():
actions = []
if (config["monitor"]["enabled"] or forced_names) and due:
with ThreadPoolExecutor(max_workers=max(1, len(due))) as executor:
futures = [executor.submit(probe_account, item["profile"], config, item["purpose"]) for item in due]
futures = {executor.submit(probe_account, item["profile"], config, item["purpose"]): item["profile"] for item in due}
for future in as_completed(futures):
result = future.result()
results.append(result)
actions.append({"accountName": result["accountName"], **apply_result(result, state, config, now, admin)})
profile = futures[future]
actions.append({"accountName": result["accountName"], **apply_result(result, state, config, now, admin, profile)})
history = state.setdefault("history", [])
run_summary = {
"at": iso(now),
@@ -1293,6 +1322,7 @@ def main():
"results": [{
"accountName": item.get("accountName"),
"purpose": item.get("purpose"),
"trustUpstream": item.get("trustUpstream"),
"ok": item.get("ok"),
"markerMatched": item.get("markerMatched"),
"httpStatus": item.get("httpStatus"),