fix: fail closed across Surplus seller state

This commit is contained in:
david
2026-08-27 12:51:15 +02:00
parent 732de817aa
commit b714c09aca
6 changed files with 54 additions and 28 deletions

View File

@@ -156,4 +156,6 @@ SURPLUS_MAX_CANDIDATE_ORDER_BOOKS=10
SURPLUS_REPRICE_INTERVAL_MS=300000
SURPLUS_REQUEST_TIMEOUT_MS=120000
SURPLUS_STOP_AFTER_SETTLED_REQUESTS=1
# Leave empty until a live earnings record establishes the terminal status.
SURPLUS_SETTLED_STATUSES=
SURPLUS_STATE_PATH=/var/lib/surplus-manager/state.json

View File

@@ -311,9 +311,7 @@ export class SurplusClient {
const text = await readBoundedText(response, controller);
if (!response.ok) {
throw new SurplusHttpError(
`Surplus ${path} returned HTTP ${response.status}${
text ? `: ${sanitizedErrorSnippet(text)}` : ""
}`,
`Surplus ${path} returned HTTP ${response.status}`,
response.status,
response.headers.get("retry-after"),
);
@@ -500,9 +498,3 @@ function optionalMicroUsd(value: unknown, field: string): number | null {
return microUsd(value, field);
}
function sanitizedErrorSnippet(value: string): string {
return value
.slice(0, 300)
.replace(/(?:si_seller_|sk-or-v1-)[A-Za-z0-9_-]+/g, "[redacted]")
.replace(/[\r\n]+/g, " ");
}

View File

@@ -22,6 +22,7 @@ export interface SurplusManagerConfig {
pollIntervalMs: number;
requestTimeoutMs: number;
stopAfterTrades: number;
settledStatuses: string[];
statePath: string;
port: number;
}
@@ -124,6 +125,10 @@ export function loadSurplusManagerConfig(
1,
1,
),
settledStatuses: statusList(
env.SURPLUS_SETTLED_STATUSES ?? "",
"SURPLUS_SETTLED_STATUSES",
),
statePath: requiredWithDefault(
env.SURPLUS_STATE_PATH,
"/var/lib/surplus-manager/state.json",
@@ -188,6 +193,20 @@ function boundedInteger(
return parsed;
}
function statusList(value: string, name: string): string[] {
if (value.trim() === "") return [];
const statuses = value.split(",").map((status) => status.trim().toLowerCase());
const seen = new Set<string>();
for (const status of statuses) {
if (!/^[a-z][a-z0-9_-]*$/.test(status)) {
throw new Error(`${name} contains an invalid status`);
}
if (seen.has(status)) throw new Error(`${name} contains a duplicate status`);
seen.add(status);
}
return statuses;
}
function httpsUrl(value: string, name: string, hostname: string): string {
let parsed: URL;
try {

View File

@@ -70,11 +70,6 @@ interface SurplusControllerOptions {
log?: (message: string) => void;
}
const SETTLED_STATUSES: Record<string, true> = {
confirmed: true,
paid: true,
settled: true,
};
export class SurplusSellerController {
private readonly config: SurplusManagerConfig;
@@ -403,7 +398,6 @@ export class SurplusSellerController {
await this.pauseOffers(confirmedActive);
throw new Error("Adopted Surplus create did not reconcile to one active offer");
}
this.assertAggregateCap(confirmedOffers);
const adopted: SurplusControllerState = {
version: 1,
phase: "active",
@@ -412,9 +406,10 @@ export class SurplusSellerController {
highestTrades24h: pending.intent.baselineTrades24h,
};
try {
this.assertAggregateCap(confirmedOffers);
await this.stateStore.save(adopted);
} catch (error) {
await this.client.pauseOffer(primary.id, this.mutationId()).catch(() => undefined);
await this.pauseOffers(confirmedActive).catch(() => undefined);
throw error;
}
this.state = adopted;
@@ -430,10 +425,14 @@ export class SurplusSellerController {
if (this.state?.phase !== "active") return;
const currentState = this.state;
const offers = await this.client.listAllOffers();
const activeOffers = offers.filter((offer) => offer.status === "active");
const managed = offers.find((offer) => offer.id === currentState.offerId);
if (!managed) throw new Error(`Managed Surplus offer is missing: ${currentState.offerId}`);
const otherActive = offers.filter(
(offer) => offer.status === "active" && offer.id !== currentState.offerId,
if (!managed) {
await this.pauseOffers(activeOffers);
throw new Error(`Managed Surplus offer is missing: ${currentState.offerId}`);
}
const otherActive = activeOffers.filter(
(offer) => offer.id !== currentState.offerId,
);
if (otherActive.length > 0) {
await this.pauseOffers(otherActive);
@@ -600,11 +599,22 @@ export class SurplusSellerController {
): SurplusEarnings["recentSales"][number] | null {
const tradeObservedAt = Date.parse(state.tradeObservedAt);
return earnings.recentSales.find((sale) => {
if (!SETTLED_STATUSES[sale.settlementStatus.toLowerCase()]) return false;
if (sale.model !== state.intent.model || sale.sellerCostMicroUsd <= 0) return false;
if (sale.offerId !== null) return sale.offerId === state.offerId;
return sale.createdAt !== null &&
Date.parse(sale.createdAt) >= tradeObservedAt;
if (
!this.config.settledStatuses.includes(
sale.settlementStatus.toLowerCase(),
)
) {
return false;
}
if (
sale.offerId !== state.offerId ||
sale.createdAt === null ||
sale.model !== state.intent.model ||
sale.sellerCostMicroUsd <= 0
) {
return false;
}
return Date.parse(sale.createdAt) >= tradeObservedAt;
}) ?? null;
}
@@ -637,7 +647,11 @@ export class SurplusSellerController {
if (offer.capDailyUsd === null) {
throw new Error(`Active Surplus offer has no daily cap: ${offer.id}`);
}
activeCap += parseUsdDecimal(offer.capDailyUsd.toFixed(6), "ceil");
const capMicroUsd = offer.capDailyUsd * 1_000_000;
if (!Number.isSafeInteger(capMicroUsd)) {
throw new Error(`Active Surplus offer cap has excess precision: ${offer.id}`);
}
activeCap += BigInt(capMicroUsd) * 1_000n;
}
const aggregateCap = parseUsdDecimal(this.config.aggregateCapUsd, "floor");
if (activeCap > aggregateCap) {

View File

@@ -44,9 +44,7 @@ export class OpenRouterKeyClient {
const text = await readBoundedText(response, controller);
if (!response.ok) {
throw new Error(
`OpenRouter key endpoint returned HTTP ${response.status}${
text ? `: ${text.slice(0, 200).replace(/[\r\n]+/g, " ")}` : ""
}`,
`OpenRouter key endpoint returned HTTP ${response.status}`,
);
}
let body: unknown;