@tokenbuddy/tokenbuddy 1.0.35 → 1.0.37
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/dist/src/buyer-store.d.ts +6 -1
- package/dist/src/buyer-store.js +43 -4
- package/dist/src/cli.js +2 -2
- package/dist/src/daemon.d.ts +12 -0
- package/dist/src/daemon.js +791 -61
- package/dist/src/doctor-diagnostics.js +1 -6
- package/dist/src/provider-install.d.ts +2 -2
- package/dist/src/provider-install.js +248 -2
- package/dist/src/seller-catalog.d.ts +21 -0
- package/dist/src/seller-catalog.js +17 -0
- package/dist/src/seller-route-planner.d.ts +4 -1
- package/dist/src/seller-route-planner.js +3 -0
- package/dist/src/seller-routing-strategy.d.ts +3 -0
- package/dist/src/terminal-detect.d.ts +1 -1
- package/dist/src/terminal-detect.js +3 -2
- package/package.json +15 -2
- package/static/ui/assets/index-Djfl9tw5.js +271 -0
- package/static/ui/assets/index-DkfztCkn.css +1 -0
- package/static/ui/index.html +2 -2
- package/dist/src/buyer-store.d.ts.map +0 -1
- package/dist/src/buyer-store.js.map +0 -1
- package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
- package/dist/src/clawtip-bootstrap.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/credit-tracker.d.ts.map +0 -1
- package/dist/src/credit-tracker.js.map +0 -1
- package/dist/src/daemon.d.ts.map +0 -1
- package/dist/src/daemon.js.map +0 -1
- package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
- package/dist/src/doctor-clawtip-wallet.js.map +0 -1
- package/dist/src/doctor-diagnostics.d.ts.map +0 -1
- package/dist/src/doctor-diagnostics.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/init-clawtip-activation.d.ts.map +0 -1
- package/dist/src/init-clawtip-activation.js.map +0 -1
- package/dist/src/init-payment-options.d.ts.map +0 -1
- package/dist/src/init-payment-options.js.map +0 -1
- package/dist/src/init-setup.d.ts.map +0 -1
- package/dist/src/init-setup.js.map +0 -1
- package/dist/src/model-index.d.ts.map +0 -1
- package/dist/src/model-index.js.map +0 -1
- package/dist/src/package-update.d.ts.map +0 -1
- package/dist/src/package-update.js.map +0 -1
- package/dist/src/prewarm-cache.d.ts.map +0 -1
- package/dist/src/prewarm-cache.js.map +0 -1
- package/dist/src/prewarm-scheduler.d.ts.map +0 -1
- package/dist/src/prewarm-scheduler.js.map +0 -1
- package/dist/src/provider-install.d.ts.map +0 -1
- package/dist/src/provider-install.js.map +0 -1
- package/dist/src/provider-routing-config.d.ts.map +0 -1
- package/dist/src/provider-routing-config.js.map +0 -1
- package/dist/src/registry-trust.d.ts.map +0 -1
- package/dist/src/registry-trust.js.map +0 -1
- package/dist/src/route-failover.d.ts.map +0 -1
- package/dist/src/route-failover.js.map +0 -1
- package/dist/src/seller-catalog.d.ts.map +0 -1
- package/dist/src/seller-catalog.js.map +0 -1
- package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
- package/dist/src/seller-concurrency-limiter.js.map +0 -1
- package/dist/src/seller-metadata-cache.d.ts.map +0 -1
- package/dist/src/seller-metadata-cache.js.map +0 -1
- package/dist/src/seller-pool.d.ts.map +0 -1
- package/dist/src/seller-pool.js.map +0 -1
- package/dist/src/seller-route-planner.d.ts.map +0 -1
- package/dist/src/seller-route-planner.js.map +0 -1
- package/dist/src/seller-routing-config.d.ts.map +0 -1
- package/dist/src/seller-routing-config.js.map +0 -1
- package/dist/src/seller-routing-strategy.d.ts.map +0 -1
- package/dist/src/seller-routing-strategy.js.map +0 -1
- package/dist/src/stream-failover.d.ts.map +0 -1
- package/dist/src/stream-failover.js.map +0 -1
- package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
- package/dist/src/tb-clawtip-proof.js.map +0 -1
- package/dist/src/tb-proxyd.d.ts.map +0 -1
- package/dist/src/tb-proxyd.js.map +0 -1
- package/dist/src/terminal-detect.d.ts.map +0 -1
- package/dist/src/terminal-detect.js.map +0 -1
- package/dist/src/terminal-image.d.ts.map +0 -1
- package/dist/src/terminal-image.js.map +0 -1
- package/src/buyer-store.ts +0 -1090
- package/src/clawtip-bootstrap.ts +0 -65
- package/src/cli.ts +0 -2243
- package/src/credit-tracker.ts +0 -295
- package/src/daemon.ts +0 -5475
- package/src/doctor-clawtip-wallet.ts +0 -95
- package/src/doctor-diagnostics.ts +0 -1026
- package/src/index.ts +0 -16
- package/src/init-clawtip-activation.ts +0 -695
- package/src/init-payment-options.ts +0 -373
- package/src/init-setup.ts +0 -165
- package/src/model-index.ts +0 -278
- package/src/package-update.ts +0 -311
- package/src/prewarm-cache.ts +0 -485
- package/src/prewarm-scheduler.ts +0 -675
- package/src/provider-install.ts +0 -1006
- package/src/provider-routing-config.ts +0 -410
- package/src/registry-trust.ts +0 -51
- package/src/route-failover.ts +0 -304
- package/src/seller-catalog.ts +0 -505
- package/src/seller-concurrency-limiter.ts +0 -161
- package/src/seller-metadata-cache.ts +0 -91
- package/src/seller-pool.ts +0 -557
- package/src/seller-route-planner.ts +0 -513
- package/src/seller-routing-config.ts +0 -211
- package/src/seller-routing-strategy.ts +0 -362
- package/src/stream-failover.ts +0 -152
- package/src/tb-clawtip-proof.ts +0 -28
- package/src/tb-proxyd.ts +0 -101
- package/src/terminal-detect.ts +0 -333
- package/src/terminal-image.ts +0 -228
- package/static/ui/assets/index-0MVXD7bH.css +0 -1
- package/static/ui/assets/index-BVbeDEwq.js +0 -271
- package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
- package/tests/cli-routing.test.ts +0 -363
- package/tests/control-plane-ui-endpoints.test.ts +0 -1630
- package/tests/credit-tracker.test.ts +0 -165
- package/tests/daemon-413-fallback.test.ts +0 -92
- package/tests/daemon-classify.test.ts +0 -452
- package/tests/daemon-roles.test.ts +0 -92
- package/tests/daemon-trusted-registry-cache.test.ts +0 -132
- package/tests/e2e.test.ts +0 -366
- package/tests/image-generation-e2e.test.ts +0 -230
- package/tests/model-index.test.ts +0 -198
- package/tests/package-update.test.ts +0 -147
- package/tests/prewarm-cache.test.ts +0 -296
- package/tests/prewarm-scheduler.test.ts +0 -367
- package/tests/provider-routing-config.test.ts +0 -150
- package/tests/registry-trust.test.ts +0 -28
- package/tests/route-failover.test.ts +0 -222
- package/tests/seller-catalog-413.test.ts +0 -120
- package/tests/seller-catalog-utilities.test.ts +0 -124
- package/tests/seller-concurrency-limiter.test.ts +0 -83
- package/tests/seller-metadata-cache.test.ts +0 -89
- package/tests/seller-pool.test.ts +0 -365
- package/tests/seller-route-planner.test.ts +0 -312
- package/tests/seller-routing-config.test.ts +0 -124
- package/tests/seller-routing-strategy.test.ts +0 -167
- package/tests/stream-failover.test.ts +0 -52
- package/tests/thousand-seller.test.ts +0 -151
- package/tests/tokenbuddy.test.ts +0 -4043
- package/tsconfig.json +0 -8
package/src/route-failover.ts
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { createModuleLogger } from "@tokenbuddy/logging";
|
|
2
|
-
import type { RegistrySeller } from "./seller-catalog.js";
|
|
3
|
-
import type { SellerPool, FailureKind, PoolEntry } from "./seller-pool.js";
|
|
4
|
-
import type { CreditTracker } from "./credit-tracker.js";
|
|
5
|
-
|
|
6
|
-
const logger = createModuleLogger("tb-proxyd:route-failover");
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 路由失败后的下一步动作(控制器收到 `FailoverDecision` 后据此执行)。
|
|
10
|
-
* - `retry_same_seller`:在同 seller 上做 jittered backoff 重试
|
|
11
|
-
* - `failover_next`:从池里取出下一个候选 seller
|
|
12
|
-
* - `fail_fast`:立刻向上抛错,不再尝试其它候选
|
|
13
|
-
* - `abort`:候选已耗尽,放弃本轮
|
|
14
|
-
*/
|
|
15
|
-
export type RouteAction = "retry_same_seller" | "failover_next" | "fail_fast" | "abort";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 一次失败后 `RouteFailover.decide()` 返回的结构化决策。
|
|
19
|
-
* 控制器根据 `action` 决定是否重试/切换/终止,并把 `wastedCreditMicros`
|
|
20
|
-
* 上报到 doctor 面板。
|
|
21
|
-
*/
|
|
22
|
-
export interface FailoverDecision {
|
|
23
|
-
/** 控制器要执行的下一步动作 */
|
|
24
|
-
action: RouteAction;
|
|
25
|
-
/** 仅当 `action === "retry_same_seller"` 时使用,表示 backoff 后多久重试(毫秒) */
|
|
26
|
-
retryDelayMs?: number;
|
|
27
|
-
/** 决策理由,结构化但可读,供日志 / doctor 渲染 */
|
|
28
|
-
reason: string;
|
|
29
|
-
/** 本次失败后被废弃的 credit(USD micros),供 doctor 统计 */
|
|
30
|
-
wastedCreditMicros?: number;
|
|
31
|
-
/** 失败发生在 fresh-purchase 窗口内(刚买不到 N 秒),用于触发软重试保护 */
|
|
32
|
-
freshPurchase: boolean;
|
|
33
|
-
/** 在切走这个 seller 之前的最后一次尝试索引(0-based,含本次失败) */
|
|
34
|
-
retryAttemptsBeforeFailover: number;
|
|
35
|
-
/** 当次会话是否已超出 auto-purchase 预算;true 时不再触发新一轮 buy */
|
|
36
|
-
budgetExceeded: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 一次路由选择的最小可执行单元:池里的某个 entry + 关联的 registry 描述。
|
|
41
|
-
* `routeIndex` 仅用于日志排版,控制器自己维护尝试序号。
|
|
42
|
-
*/
|
|
43
|
-
export interface RouteCandidate {
|
|
44
|
-
/** 在本批候选中的序号(0-based),仅作日志可读性使用 */
|
|
45
|
-
routeIndex: number;
|
|
46
|
-
/** 池里的 entry(包含 circuit / healthScore 等运行时状态) */
|
|
47
|
-
entry: PoolEntry;
|
|
48
|
-
/** registry 里的原始 seller 描述 */
|
|
49
|
-
registrySeller: RegistrySeller;
|
|
50
|
-
/** seller 全局 ID(同时也是 token class) */
|
|
51
|
-
sellerId: string;
|
|
52
|
-
/** 去掉尾部斜杠后的 seller URL */
|
|
53
|
-
url: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 控制器调用 `decide()` 时传入的失败上下文。
|
|
58
|
-
* 描述"这次失败发生在谁身上、是什么错误、第几次尝试"。
|
|
59
|
-
*/
|
|
60
|
-
export interface DecideContext {
|
|
61
|
-
/** 失败时正在调用的 seller ID */
|
|
62
|
-
sellerId: string;
|
|
63
|
-
/** HTTP status(如有),用于区分 4xx/5xx */
|
|
64
|
-
status?: number;
|
|
65
|
-
/** 错误分类,由调用方在边界层归一化为 `FailureKind` */
|
|
66
|
-
errorKind: FailureKind;
|
|
67
|
-
/** 人类可读错误描述(不携带敏感字段),用于日志/doctor */
|
|
68
|
-
errorMessage?: string;
|
|
69
|
-
/** 当前是该 seller 的第几次尝试(0-based;日志里同名字段保持该语义) */
|
|
70
|
-
attempt: number;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 构造 `RouteFailover` 所需的依赖与可调参数。
|
|
75
|
-
* 默认值见 `DEFAULTS`:软重试 2 次,jitter 100-400ms。
|
|
76
|
-
*/
|
|
77
|
-
export interface RouteFailoverOptions {
|
|
78
|
-
/** 共享的 seller pool,控制器只读 + 委托 recordSuccess/recordFailure */
|
|
79
|
-
pool: SellerPool;
|
|
80
|
-
/** 共享的 credit tracker,用于 wasted 计算与 auto-purchase 预算 */
|
|
81
|
-
creditTracker: CreditTracker;
|
|
82
|
-
// v1.1 / v1.2 design defaults; exposed for tests.
|
|
83
|
-
/** 软重试次数上限,默认 2 */
|
|
84
|
-
softRetryAttempts?: number; // default 2
|
|
85
|
-
/** 软重试 backoff jitter 下界(毫秒),默认 100 */
|
|
86
|
-
softRetryJitterMinMs?: number; // default 100
|
|
87
|
-
/** 软重试 backoff jitter 上界(毫秒),默认 400 */
|
|
88
|
-
softRetryJitterMaxMs?: number; // default 400
|
|
89
|
-
/** 注入的随机源(测试用),默认 `Math.random` */
|
|
90
|
-
random?: () => number;
|
|
91
|
-
/** 注入的时钟(测试用),默认 `Date.now` */
|
|
92
|
-
now?: () => number;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const DEFAULTS = {
|
|
96
|
-
softRetryAttempts: 2,
|
|
97
|
-
softRetryJitterMinMs: 100,
|
|
98
|
-
softRetryJitterMaxMs: 400
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Decision engine that wraps the v2 `SellerPool` and the
|
|
103
|
-
* `CreditTracker`. v1.2 §17.3 - §17.8 defines the rules: the controller
|
|
104
|
-
* calls `decide()` after a failure to learn whether to retry the same
|
|
105
|
-
* seller, fail over to the next candidate, fail fast, or give up.
|
|
106
|
-
*
|
|
107
|
-
* The controller is intentionally side-effect-light: it delegates
|
|
108
|
-
* bookkeeping (circuit transitions, wasted-credit transfer) to the pool
|
|
109
|
-
* and tracker via `recordFailure`, and only returns a structured
|
|
110
|
-
* decision. This makes the rules unit-testable without a live HTTP server.
|
|
111
|
-
*/
|
|
112
|
-
export class RouteFailover {
|
|
113
|
-
private readonly pool: SellerPool;
|
|
114
|
-
private readonly creditTracker: CreditTracker;
|
|
115
|
-
private readonly softRetryAttempts: number;
|
|
116
|
-
private readonly softRetryJitterMinMs: number;
|
|
117
|
-
private readonly softRetryJitterMaxMs: number;
|
|
118
|
-
private readonly random: () => number;
|
|
119
|
-
private readonly now: () => number;
|
|
120
|
-
|
|
121
|
-
constructor(options: RouteFailoverOptions) {
|
|
122
|
-
this.pool = options.pool;
|
|
123
|
-
this.creditTracker = options.creditTracker;
|
|
124
|
-
this.softRetryAttempts = options.softRetryAttempts ?? DEFAULTS.softRetryAttempts;
|
|
125
|
-
this.softRetryJitterMinMs = options.softRetryJitterMinMs ?? DEFAULTS.softRetryJitterMinMs;
|
|
126
|
-
this.softRetryJitterMaxMs = options.softRetryJitterMaxMs ?? DEFAULTS.softRetryJitterMaxMs;
|
|
127
|
-
this.random = options.random ?? Math.random;
|
|
128
|
-
this.now = options.now ?? Date.now;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Pick the next candidate from the pool. Returns `undefined` when the
|
|
133
|
-
* pool is exhausted, which the caller treats as `abort` (no more routes
|
|
134
|
-
* to try). The returned `routeIndex` is informational (used for log
|
|
135
|
-
* lines) and tracks the absolute ordering across attempts; the controller
|
|
136
|
-
* maintains its own counter.
|
|
137
|
-
*/
|
|
138
|
-
pickNext(modelId: string, protocol: string, paymentMethod: string, limit: number = 4): RouteCandidate | undefined {
|
|
139
|
-
const result = this.pool.pick({ modelId, protocol, paymentMethod, limit });
|
|
140
|
-
if (result.candidates.length === 0) {
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
const first = result.candidates[0];
|
|
144
|
-
return {
|
|
145
|
-
routeIndex: 0,
|
|
146
|
-
entry: first.entry,
|
|
147
|
-
registrySeller: first.registrySeller,
|
|
148
|
-
sellerId: first.entry.sellerId,
|
|
149
|
-
url: first.entry.url
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Mark a successful inference against `sellerId` with the latest known
|
|
155
|
-
* balance. Mirrors `SellerPool.recordSuccess` semantics: resets the
|
|
156
|
-
* failure counter, closes the circuit, and reports the spend to the
|
|
157
|
-
* credit tracker.
|
|
158
|
-
*/
|
|
159
|
-
recordSuccess(sellerId: string, balanceMicros: number): void {
|
|
160
|
-
this.pool.recordSuccess(sellerId, balanceMicros, this.now());
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Decide what to do after a failure. Returns a structured
|
|
165
|
-
* `FailoverDecision`; the controller (daemon.ts) executes the action.
|
|
166
|
-
* The decision also drives accounting: on a failover, the wasted
|
|
167
|
-
* balance is reported in `wastedCreditMicros` for the doctor's summary.
|
|
168
|
-
*/
|
|
169
|
-
decide(context: DecideContext, totalCandidates: number): FailoverDecision {
|
|
170
|
-
const isHard = context.errorKind === "hard_4xx" || context.errorKind === "auth_invalid" || context.errorKind === "no_compatible";
|
|
171
|
-
const isSoft = context.errorKind === "soft_5xx" || context.errorKind === "deadline";
|
|
172
|
-
const isBusyCapacity = context.errorKind === "busy_capacity";
|
|
173
|
-
const isPurchaseFailure = context.errorKind === "purchase_failed";
|
|
174
|
-
const info = this.pool.inspect(context.sellerId);
|
|
175
|
-
const freshPurchase = info.freshPurchase;
|
|
176
|
-
const budgetExceeded = !this.creditTracker.canAutoPurchase(this.now());
|
|
177
|
-
|
|
178
|
-
// Always mark the failure on the pool so circuit thresholds advance.
|
|
179
|
-
const updated = this.pool.recordFailure(context.sellerId, context.errorKind, {
|
|
180
|
-
reason: context.errorMessage,
|
|
181
|
-
now: this.now()
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
if (isBusyCapacity) {
|
|
185
|
-
return {
|
|
186
|
-
action: "failover_next",
|
|
187
|
-
reason: "busy_capacity",
|
|
188
|
-
freshPurchase,
|
|
189
|
-
retryAttemptsBeforeFailover: context.attempt,
|
|
190
|
-
budgetExceeded
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (isPurchaseFailure) {
|
|
195
|
-
return {
|
|
196
|
-
action: "failover_next",
|
|
197
|
-
reason: "purchase_failed",
|
|
198
|
-
wastedCreditMicros: this.creditTracker.getEntry(context.sellerId)?.currentBalanceMicros,
|
|
199
|
-
freshPurchase,
|
|
200
|
-
retryAttemptsBeforeFailover: context.attempt,
|
|
201
|
-
budgetExceeded
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (isHard) {
|
|
206
|
-
// Hard failures are not eligible for retry; the seller is wrong
|
|
207
|
-
// for this request. The pool has already transferred leftover
|
|
208
|
-
// credit to the wasted bucket.
|
|
209
|
-
return {
|
|
210
|
-
action: "failover_next",
|
|
211
|
-
reason: `hard_failure:${context.errorKind}`,
|
|
212
|
-
wastedCreditMicros: this.creditTracker.getEntry(context.sellerId)?.leftoverCreditMicros,
|
|
213
|
-
freshPurchase,
|
|
214
|
-
retryAttemptsBeforeFailover: context.attempt,
|
|
215
|
-
budgetExceeded
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (isSoft && freshPurchase && context.attempt < this.softRetryAttempts) {
|
|
220
|
-
// Soft failure inside the fresh-purchase window: retry the same
|
|
221
|
-
// seller with jittered backoff. This is the v1.1 protection against
|
|
222
|
-
// throwing freshly bought credit onto a flaky upstream.
|
|
223
|
-
const span = Math.max(0, this.softRetryJitterMaxMs - this.softRetryJitterMinMs);
|
|
224
|
-
const delay = this.softRetryJitterMinMs + Math.floor(this.random() * span);
|
|
225
|
-
logger.info("route.retry_same_seller.soft", "soft failure in fresh-purchase window; retrying same seller", {
|
|
226
|
-
sellerId: context.sellerId,
|
|
227
|
-
attempt: context.attempt,
|
|
228
|
-
delayMs: delay
|
|
229
|
-
});
|
|
230
|
-
return {
|
|
231
|
-
action: "retry_same_seller",
|
|
232
|
-
retryDelayMs: delay,
|
|
233
|
-
reason: "soft_failure_fresh_purchase_window",
|
|
234
|
-
freshPurchase: true,
|
|
235
|
-
retryAttemptsBeforeFailover: context.attempt,
|
|
236
|
-
budgetExceeded
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (isSoft && context.attempt < this.softRetryAttempts) {
|
|
241
|
-
// Soft failure but not in the fresh-purchase window: still retry
|
|
242
|
-
// once (caller asked for `attempt < softRetryAttempts`) but log the
|
|
243
|
-
// reason. This keeps the "give the seller a second chance" window
|
|
244
|
-
// available for cold sellers without burning any credit.
|
|
245
|
-
const span = Math.max(0, this.softRetryJitterMaxMs - this.softRetryJitterMinMs);
|
|
246
|
-
const delay = this.softRetryJitterMinMs + Math.floor(this.random() * span);
|
|
247
|
-
logger.info("route.retry_same_seller.soft", "soft failure retry outside fresh-purchase window", {
|
|
248
|
-
sellerId: context.sellerId,
|
|
249
|
-
attempt: context.attempt,
|
|
250
|
-
delayMs: delay
|
|
251
|
-
});
|
|
252
|
-
return {
|
|
253
|
-
action: "retry_same_seller",
|
|
254
|
-
retryDelayMs: delay,
|
|
255
|
-
reason: "soft_failure_retry",
|
|
256
|
-
freshPurchase: false,
|
|
257
|
-
retryAttemptsBeforeFailover: context.attempt,
|
|
258
|
-
budgetExceeded
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Default: fail over. The wasted credit is whatever current balance
|
|
263
|
-
// remained on the seller after the failure was recorded (the pool
|
|
264
|
-
// transfers it to the leftover bucket for hard failures; for soft
|
|
265
|
-
// failures we surface the *pre-failure* balance so the user sees the
|
|
266
|
-
// impact of the cut-over).
|
|
267
|
-
const wasted = updated?.lastFailAt
|
|
268
|
-
? this.creditTracker.getEntry(context.sellerId)?.currentBalanceMicros
|
|
269
|
-
: undefined;
|
|
270
|
-
return {
|
|
271
|
-
action: "failover_next",
|
|
272
|
-
reason: isSoft ? "soft_failure_exhausted_retries" : `${context.errorKind}`,
|
|
273
|
-
wastedCreditMicros: wasted,
|
|
274
|
-
freshPurchase,
|
|
275
|
-
retryAttemptsBeforeFailover: context.attempt,
|
|
276
|
-
budgetExceeded
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Convenience: returns true when the next request should be aborted
|
|
282
|
-
* outright (no more candidates).
|
|
283
|
-
*/
|
|
284
|
-
shouldAbort(totalCandidates: number, hasMoreCandidates: boolean): boolean {
|
|
285
|
-
return !hasMoreCandidates;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Inspect the auto-purchase budget. The controller calls this before
|
|
290
|
-
* triggering a fresh `purchase/create` round-trip so it can refuse to
|
|
291
|
-
* auto-purchase once the session is at risk.
|
|
292
|
-
*/
|
|
293
|
-
canAutoPurchase(): boolean {
|
|
294
|
-
return this.creditTracker.canAutoPurchase(this.now());
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Surface the wasted credit so far for the route currently failing over.
|
|
299
|
-
* Returned in micros.
|
|
300
|
-
*/
|
|
301
|
-
wastedMicrosFor(sellerId: string): number {
|
|
302
|
-
return this.creditTracker.getEntry(sellerId)?.currentBalanceMicros ?? 0;
|
|
303
|
-
}
|
|
304
|
-
}
|