@bolloon/bolloon-agent 0.5.5 → 0.6.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.
Files changed (109) hide show
  1. package/dist/agents/parse-tool-call.js +6 -1
  2. package/dist/agents/pi-sdk-session-factory.js +36 -3
  3. package/dist/agents/pi-sdk.js +349 -199
  4. package/dist/agents/run-context.js +20 -0
  5. package/dist/agents/runner-resolver.js +2 -1
  6. package/dist/electron-build/agents/parse-tool-call.js +6 -1
  7. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  8. package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
  9. package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
  10. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  11. package/dist/electron-build/agents/pi-sdk.js +349 -199
  12. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  13. package/dist/electron-build/agents/run-context.js +24 -0
  14. package/dist/electron-build/agents/run-context.js.map +1 -0
  15. package/dist/electron-build/agents/runner-resolver.js +2 -1
  16. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  17. package/dist/electron-build/kernel/channel-actor.js +314 -0
  18. package/dist/electron-build/kernel/channel-actor.js.map +1 -0
  19. package/dist/index.js +24 -19
  20. package/dist/kernel/channel-actor.js +297 -0
  21. package/dist/kernel/control.js +95 -0
  22. package/dist/kernel/gate-scan.js +1036 -0
  23. package/dist/kernel/model-runtime.js +472 -0
  24. package/dist/kernel/plan-channel-actor.js +298 -0
  25. package/dist/kernel/plan-constraint.js +165 -0
  26. package/dist/kernel/plan-deletion.js +32 -0
  27. package/dist/kernel/plan-modelruntime.js +60 -0
  28. package/dist/kernel/plan-runcontext.js +163 -0
  29. package/dist/kernel/plan.js +153 -0
  30. package/dist/kernel/roster.js +242 -0
  31. package/dist/web/bolloon-web.json +3 -3
  32. package/dist/web/client.js +3 -2
  33. package/dist/web/routes-tasks.js +5 -1
  34. package/dist/web/server.js +58 -18
  35. package/dist/web/sw.js +1 -1
  36. package/dist/web/ui/message-renderer.js +473 -29
  37. package/dist/web/ui/step-timeline.js +13 -12
  38. package/package.json +1 -1
  39. package/scripts/build-web.ts +74 -1
  40. package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
  41. package/dist/constraint-runtime/_archive_helper.js +0 -9
  42. package/dist/constraint-runtime/assistant/index.d.ts +0 -11
  43. package/dist/constraint-runtime/assistant/index.js +0 -12
  44. package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
  45. package/dist/constraint-runtime/bootstrap/index.js +0 -12
  46. package/dist/constraint-runtime/bridge/index.d.ts +0 -11
  47. package/dist/constraint-runtime/bridge/index.js +0 -12
  48. package/dist/constraint-runtime/buddy/index.d.ts +0 -11
  49. package/dist/constraint-runtime/buddy/index.js +0 -12
  50. package/dist/constraint-runtime/cli/index.d.ts +0 -11
  51. package/dist/constraint-runtime/cli/index.js +0 -12
  52. package/dist/constraint-runtime/components/index.d.ts +0 -11
  53. package/dist/constraint-runtime/components/index.js +0 -12
  54. package/dist/constraint-runtime/constants/index.d.ts +0 -11
  55. package/dist/constraint-runtime/constants/index.js +0 -12
  56. package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
  57. package/dist/constraint-runtime/coordinator/index.js +0 -12
  58. package/dist/constraint-runtime/cost_hook.d.ts +0 -2
  59. package/dist/constraint-runtime/cost_hook.js +0 -4
  60. package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
  61. package/dist/constraint-runtime/entrypoints/index.js +0 -12
  62. package/dist/constraint-runtime/execution_registry.d.ts +0 -20
  63. package/dist/constraint-runtime/execution_registry.js +0 -44
  64. package/dist/constraint-runtime/hooks/index.d.ts +0 -11
  65. package/dist/constraint-runtime/hooks/index.js +0 -12
  66. package/dist/constraint-runtime/ink.d.ts +0 -1
  67. package/dist/constraint-runtime/ink.js +0 -4
  68. package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
  69. package/dist/constraint-runtime/keybindings/index.js +0 -12
  70. package/dist/constraint-runtime/memdir/index.d.ts +0 -11
  71. package/dist/constraint-runtime/memdir/index.js +0 -12
  72. package/dist/constraint-runtime/migrations/index.d.ts +0 -11
  73. package/dist/constraint-runtime/migrations/index.js +0 -12
  74. package/dist/constraint-runtime/moreright/index.d.ts +0 -11
  75. package/dist/constraint-runtime/moreright/index.js +0 -12
  76. package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
  77. package/dist/constraint-runtime/native_ts/index.js +0 -12
  78. package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
  79. package/dist/constraint-runtime/output_styles/index.js +0 -12
  80. package/dist/constraint-runtime/plugins/index.d.ts +0 -11
  81. package/dist/constraint-runtime/plugins/index.js +0 -12
  82. package/dist/constraint-runtime/port_manifest.d.ts +0 -7
  83. package/dist/constraint-runtime/port_manifest.js +0 -11
  84. package/dist/constraint-runtime/query.d.ts +0 -6
  85. package/dist/constraint-runtime/query.js +0 -1
  86. package/dist/constraint-runtime/remote/index.d.ts +0 -11
  87. package/dist/constraint-runtime/remote/index.js +0 -12
  88. package/dist/constraint-runtime/schemas/index.d.ts +0 -11
  89. package/dist/constraint-runtime/schemas/index.js +0 -12
  90. package/dist/constraint-runtime/screens/index.d.ts +0 -11
  91. package/dist/constraint-runtime/screens/index.js +0 -12
  92. package/dist/constraint-runtime/server/index.d.ts +0 -11
  93. package/dist/constraint-runtime/server/index.js +0 -12
  94. package/dist/constraint-runtime/services/index.d.ts +0 -11
  95. package/dist/constraint-runtime/services/index.js +0 -12
  96. package/dist/constraint-runtime/state/index.d.ts +0 -11
  97. package/dist/constraint-runtime/state/index.js +0 -12
  98. package/dist/constraint-runtime/system_init.d.ts +0 -1
  99. package/dist/constraint-runtime/system_init.js +0 -20
  100. package/dist/constraint-runtime/types/index.d.ts +0 -11
  101. package/dist/constraint-runtime/types/index.js +0 -12
  102. package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
  103. package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
  104. package/dist/constraint-runtime/utils/index.d.ts +0 -11
  105. package/dist/constraint-runtime/utils/index.js +0 -12
  106. package/dist/constraint-runtime/vim/index.d.ts +0 -11
  107. package/dist/constraint-runtime/vim/index.js +0 -12
  108. package/dist/constraint-runtime/voice/index.d.ts +0 -11
  109. package/dist/constraint-runtime/voice/index.js +0 -12
@@ -0,0 +1,472 @@
1
+ /**
2
+ * **K6: ModelRuntime 骨架** —— `acquire(modelSnapshot)` **只读**入手。
3
+ *
4
+ * 本步只做三件事 (其余能力在 `plan-modelruntime.ts` 里仍是 `not-started`):
5
+ * ① **连接复用** (按 snapshot 派生 key 池化, 同一 key 只开一次连接)
6
+ * ② **timeout** (按 snapshot 的预算; 超时即中止底层调用, 不是"放弃等待")
7
+ * ③ **cancellation** (外部 AbortSignal 透传到连接, 取消真发生)
8
+ *
9
+ * 边界 (K6 红线):
10
+ * · **只读**: 绝不改 provider 配置 / API key / 默认 URL / Global model / Run snapshot;
11
+ * · 不重做既有 `selectModel` / registry / catalog / Run snapshot —— 真正的 provider 调用由 **ports 注入**
12
+ * (内核不许 import 业务模块: `KERNEL_ALLOWED_IMPORT_PREFIXES = ['kernel/']`)。
13
+ *
14
+ * 通用判据 (K2 实证): **本文件出现后, 旧写口的调用点数不许增加** —— 由 `scanModelRuntimeFile` 机械核对。
15
+ */
16
+ /** 429 识别 (端口可以把限流表达成返回值 status=429, 或抛一个带 status 的错) */
17
+ export function isRateLimited(x) {
18
+ if (!x || typeof x !== 'object')
19
+ return false;
20
+ const o = x;
21
+ if (o.status === 429 || o.status === '429')
22
+ return true;
23
+ if (typeof o.error === 'string' && /\b429\b|rate ?limit/i.test(o.error))
24
+ return true;
25
+ return false;
26
+ }
27
+ /** 退避参数 (写成数据; 实现与测试共用) */
28
+ export const BACKOFF_POLICY = {
29
+ baseMs: 200,
30
+ factor: 2,
31
+ maxMs: 5_000,
32
+ /** 抖动比例 (±) —— 防止多个调用方同拍重试 */
33
+ jitter: 0.25,
34
+ maxRetries: 3,
35
+ };
36
+ /**
37
+ * 计算第 n 次重试的等待时长 (n 从 0 起): `min(base * factor^n, max)` 再乘 ±jitter。
38
+ * `retryAfterMs` (供应商给的 Retry-After) 存在时**取最大值** —— 尊重上游而不只是听自己的退避曲线。
39
+ */
40
+ export function backoffDelayMs(n, opts) {
41
+ const exp = Math.min(BACKOFF_POLICY.baseMs * BACKOFF_POLICY.factor ** n, BACKOFF_POLICY.maxMs);
42
+ const r = opts?.random ?? Math.random;
43
+ const jittered = exp * (1 + BACKOFF_POLICY.jitter * (2 * r() - 1));
44
+ return Math.max(0, Math.round(Math.max(jittered, opts?.retryAfterMs ?? 0)));
45
+ }
46
+ /**
47
+ * **熔断策略** (数据; 实现与测试共用)。按 pool key 记连续失败 ⇒ 开路 ⇒ 冷却后半开探测。
48
+ * **什么算失败**: 网络/供应商故障类; **不算**: 取消 (调用方的选择) 与 429 (那是退避的活), 见 `countsTowardBreaker`。
49
+ */
50
+ export const BREAKER_POLICY = {
51
+ failureThreshold: 3,
52
+ cooldownMs: 30_000,
53
+ /** 半开时允许几个探测 */
54
+ halfOpenProbes: 1,
55
+ why: '连续失败 ⇒ 开路 (fail fast, 不把故障放大) · 冷却后放一个探测 ⇒ 成功闭合, 失败重新开路',
56
+ };
57
+ /** 这次失败该不该计入熔断 */
58
+ export function countsTowardBreaker(err) {
59
+ if (err instanceof ModelAbortError)
60
+ return false; // 调用方取消 ⇒ 不是供应商的错
61
+ if (err instanceof ModelTimeoutError)
62
+ return true; // 超时是供应商侧症状
63
+ if (isRateLimited(err))
64
+ return false; // 限流交给退避, 不熔断
65
+ return true;
66
+ }
67
+ export class ModelCircuitOpenError extends Error {
68
+ retryAtMs;
69
+ constructor(retryAtMs) {
70
+ super(`熔断中 (冷却至 ${retryAtMs})`);
71
+ this.retryAtMs = retryAtMs;
72
+ this.name = 'ModelCircuitOpenError';
73
+ }
74
+ }
75
+ export class ModelCapabilityError extends Error {
76
+ missing;
77
+ unknown;
78
+ constructor(missing, unknown = false) {
79
+ super(unknown ? `未知能力 (snapshot 未声明 capabilities, 无法确认 ${missing.join(', ')})` : `缺能力: ${missing.join(', ')}`);
80
+ this.missing = missing;
81
+ this.unknown = unknown;
82
+ this.name = 'ModelCapabilityError';
83
+ }
84
+ }
85
+ export class ModelTimeoutError extends Error {
86
+ budgetMs;
87
+ constructor(budgetMs) {
88
+ super(`模型调用超时 (${budgetMs}ms)`);
89
+ this.budgetMs = budgetMs;
90
+ this.name = 'ModelTimeoutError';
91
+ }
92
+ }
93
+ export class ModelAbortError extends Error {
94
+ constructor() { super('模型调用被取消'); this.name = 'ModelAbortError'; }
95
+ }
96
+ /** snapshot → 池键 (只用**只读**字段; 与 provider/model/端点相关, 与调用内容无关) */
97
+ export function modelPoolKey(s) {
98
+ return `${s.provider}::${s.model}::${s.baseUrl ?? ''}`;
99
+ }
100
+ export class ModelRuntime {
101
+ ports;
102
+ defaultTimeoutMs;
103
+ maxConcurrency;
104
+ pool = new Map();
105
+ stats = { reused: 0, opened: 0, timeouts: 0, aborts: 0, concurrencyWaits: 0, maxObservedConcurrency: 0, rateLimited: 0, retries: 0, lastBackoffMs: 0, circuitOpened: 0, failFast: 0, capabilityRejects: 0, halfOpenProbes: 0, breakerClosed: 0, fallbacks: 0, usageRecorded: 0, usageDropped: 0 };
106
+ /** 逐 key 熔断状态 */
107
+ breakers = new Map();
108
+ /** 每 key 当前在飞的调用数 + 等待队列 (多供应商并发: 各 key 各自算) */
109
+ active = new Map();
110
+ /** 等待者 → 它的 granted 标记 (转让路径用; 见 acquireSlot) */
111
+ grantedProbe = new WeakMap();
112
+ waiters = new Map();
113
+ constructor(ports, defaultTimeoutMs = 30_000,
114
+ /** 每个 key 的最大并发 (多供应商并发: 逐 key 计数, 互不阻塞) */
115
+ maxConcurrency = 4) {
116
+ this.ports = ports;
117
+ this.defaultTimeoutMs = defaultTimeoutMs;
118
+ this.maxConcurrency = maxConcurrency;
119
+ if (typeof ports?.openConnection !== 'function')
120
+ throw new Error('ModelRuntime 需要 openConnection 端口');
121
+ }
122
+ /** **只读入口**: 取一个租约 (可能需要先开连接; 同一 pool key 复用) */
123
+ async acquire(snapshot, opts) {
124
+ if (!snapshot?.provider || !snapshot?.model)
125
+ throw new Error('acquire 需要 provider + model (缺了不许猜)');
126
+ // **只读**: 只读这些字段, 一个都不写回 (snapshot 常被上层冻结; 写它会当场抛)
127
+ const frozen = snapshot;
128
+ // ① 能力检查 (**拒在开连接之前** ⇒ 不浪费一次连接; 只读 snapshot.capabilities, 不改任何东西)
129
+ const need = opts?.require ?? [];
130
+ if (need.length > 0) {
131
+ if (!frozen.capabilities) {
132
+ this.stats.capabilityRejects += 1;
133
+ throw new ModelCapabilityError(need, true);
134
+ }
135
+ const have = new Set(frozen.capabilities);
136
+ const missing = need.filter((c) => !have.has(c));
137
+ if (missing.length > 0) {
138
+ this.stats.capabilityRejects += 1;
139
+ throw new ModelCapabilityError(missing);
140
+ }
141
+ }
142
+ const key = modelPoolKey(frozen);
143
+ const { conn } = await this.ensureConnection(frozen);
144
+ const snapshotReadonly = frozen;
145
+ let released = false;
146
+ const self = this;
147
+ return {
148
+ snapshot: snapshotReadonly,
149
+ release: () => {
150
+ if (released)
151
+ return;
152
+ released = true;
153
+ const cur = self.pool.get(key);
154
+ if (cur)
155
+ cur.leases = Math.max(0, cur.leases - 1);
156
+ },
157
+ call: async (req, opts) => {
158
+ if (released)
159
+ return { ok: false, error: '租约已归还' };
160
+ const budget = snapshotReadonly.timeoutMs ?? this.defaultTimeoutMs;
161
+ const external = opts?.signal;
162
+ if (external?.aborted) {
163
+ self.stats.aborts += 1;
164
+ throw new ModelAbortError();
165
+ }
166
+ const started = (self.ports.now ?? Date.now)();
167
+ // **候选清单 = 主 provider + 备用 (来自 snapshot, 只读)** —— 运行时绝不改全局配置
168
+ const candidates = [snapshotReadonly];
169
+ for (const p of snapshotReadonly.fallbackProviders ?? []) {
170
+ if (p && p !== snapshotReadonly.provider)
171
+ candidates.push({ ...snapshotReadonly, provider: p });
172
+ }
173
+ let last = { ok: false, error: '没有可用的 provider' };
174
+ let totalAttempts = 0;
175
+ for (let ci = 0; ci < candidates.length; ci += 1) {
176
+ const snapI = candidates[ci];
177
+ const keyI = modelPoolKey(snapI);
178
+ const isFallback = ci > 0;
179
+ if (isFallback)
180
+ self.stats.fallbacks += 1;
181
+ try {
182
+ const gate = self.breakerGate(keyI);
183
+ if (gate) {
184
+ self.stats.failFast += 1;
185
+ // 熔断开路: **有下一个候选才回退**; 否则保持"抛出"的契约 (调用方能按类型区分快速失败)
186
+ if (ci + 1 < candidates.length) {
187
+ last = { ok: false, provider: snapI.provider, fallback: isFallback, error: `熔断中 (冷却至 ${gate})` };
188
+ continue;
189
+ }
190
+ throw new ModelCircuitOpenError(gate);
191
+ }
192
+ const connI = keyI === key ? conn : (await self.ensureConnection(snapI)).conn;
193
+ await self.acquireSlot(keyI, external);
194
+ try {
195
+ for (let attempt = 0;; attempt += 1) {
196
+ totalAttempts += 1;
197
+ let result;
198
+ try {
199
+ result = await self.callOnce(connI, req, external, budget);
200
+ }
201
+ catch (err) {
202
+ if (countsTowardBreaker(err))
203
+ self.onFailure(keyI);
204
+ else
205
+ self.onSuccess(keyI);
206
+ throw err; // 取消/超时: 取消要立刻冒出去 (不回退), 超时由外层 catch 处理
207
+ }
208
+ if (result.ok) {
209
+ self.onSuccess(keyI);
210
+ const ms = (self.ports.now ?? Date.now)() - started;
211
+ const out = { ...result, provider: snapI.provider, fallback: isFallback, reused: (self.pool.get(keyI)?.leases ?? 0) > 1, ms };
212
+ await self.recordUsage(snapI, { provider: snapI.provider, model: snapI.model, ms, attempts: totalAttempts, fallback: isFallback, usage: result.usage });
213
+ return out;
214
+ }
215
+ if (!isRateLimited(result)) {
216
+ if (countsTowardBreaker(result))
217
+ self.onFailure(keyI);
218
+ else
219
+ self.onSuccess(keyI);
220
+ last = { ...result, provider: snapI.provider, fallback: isFallback, reused: (self.pool.get(keyI)?.leases ?? 0) > 1, ms: (self.ports.now ?? Date.now)() - started };
221
+ break; // 非限流失败 ⇒ 交给下一个候选
222
+ }
223
+ self.stats.rateLimited += 1;
224
+ if (attempt >= BACKOFF_POLICY.maxRetries) {
225
+ last = { ...result, provider: snapI.provider, fallback: isFallback, ms: (self.ports.now ?? Date.now)() - started, error: `限流重试用尽 (${attempt + 1} 次): ${String(result.error ?? '')}` };
226
+ break; // 限流用尽 ⇒ 也可回退 (容量问题不是 bug)
227
+ }
228
+ const wait = backoffDelayMs(attempt, { retryAfterMs: Number(result.retryAfterMs ?? 0) || undefined, random: self.ports.random });
229
+ self.stats.retries += 1;
230
+ self.stats.lastBackoffMs = wait;
231
+ await self.sleepOrAbort(wait, external); // 退避期间取消 ⇒ 立刻抛
232
+ }
233
+ }
234
+ finally {
235
+ self.releaseSlot(keyI);
236
+ }
237
+ }
238
+ catch (err) {
239
+ if (err instanceof ModelAbortError)
240
+ throw err; // 调用方取消 ⇒ 不回退, 直接冒出去
241
+ if (err instanceof ModelTimeoutError) {
242
+ // 超时: **有下一个候选才回退**; 最后一个候选的超时保持原有契约 (抛出, 调用方才能区分"超时"与"软失败")
243
+ if (ci + 1 < candidates.length) {
244
+ last = { ok: false, provider: snapI.provider, fallback: isFallback, error: err.message };
245
+ continue;
246
+ }
247
+ throw err;
248
+ }
249
+ throw err;
250
+ }
251
+ }
252
+ const tried = candidates.map((c) => c.provider).join(' → ');
253
+ last = { ...last, ms: (self.ports.now ?? Date.now)() - started, error: `全部候选失败 (${tried}): ${String(last.error ?? '')}` };
254
+ await self.recordUsage(candidates[candidates.length - 1], { provider: last.provider ?? '', model: candidates[candidates.length - 1].model, ms: last.ms ?? 0, attempts: totalAttempts, fallback: candidates.length > 1, usage: undefined });
255
+ return last;
256
+ },
257
+ };
258
+ }
259
+ /** 连接池: 同一 pool key 复用; 开不起来不留脏槽 */
260
+ async ensureConnection(snapshot) {
261
+ const key = modelPoolKey(snapshot);
262
+ let slot = this.pool.get(key);
263
+ if (slot)
264
+ this.stats.reused += 1;
265
+ if (!slot) {
266
+ const controller = new AbortController(); // 连接级取消 (租约级用各自的 signal)
267
+ const opening = this.ports.openConnection(snapshot, controller.signal);
268
+ this.stats.opened += 1;
269
+ slot = { conn: undefined, opening, leases: 0 };
270
+ this.pool.set(key, slot);
271
+ try {
272
+ slot.conn = await opening;
273
+ }
274
+ catch (err) {
275
+ this.pool.delete(key); // 开不起来就不留脏槽 (否则后续全被判"复用")
276
+ throw err;
277
+ }
278
+ }
279
+ else if (!slot.conn) {
280
+ slot.conn = await slot.opening;
281
+ }
282
+ return { conn: slot.conn, key };
283
+ }
284
+ /** usage 记录: 端口抛错**不许**影响调用结果 (只记 usageDropped) */
285
+ async recordUsage(snapshot, entry) {
286
+ if (typeof this.ports.recordUsage !== 'function')
287
+ return;
288
+ try {
289
+ await this.ports.recordUsage(entry);
290
+ this.stats.usageRecorded += 1;
291
+ }
292
+ catch {
293
+ this.stats.usageDropped += 1;
294
+ }
295
+ }
296
+ /** 一次尝试 (超时与取消都走受控 controller; 底层只认一个 signal) */
297
+ async callOnce(conn, req, external, budget) {
298
+ const ctl = new AbortController();
299
+ const onExternalAbort = () => ctl.abort();
300
+ external?.addEventListener?.('abort', onExternalAbort, { once: true });
301
+ let timer;
302
+ const timeoutPromise = new Promise((_, reject) => {
303
+ timer = setTimeout(() => { this.stats.timeouts += 1; ctl.abort(); reject(new ModelTimeoutError(budget)); }, budget);
304
+ });
305
+ try {
306
+ const res = await Promise.race([conn.call(req, ctl.signal), timeoutPromise]);
307
+ return (res ?? { ok: true });
308
+ }
309
+ catch (err) {
310
+ if (err instanceof ModelTimeoutError)
311
+ throw err;
312
+ if (ctl.signal.aborted) {
313
+ this.stats.aborts += 1;
314
+ throw new ModelAbortError();
315
+ }
316
+ throw err;
317
+ }
318
+ finally {
319
+ if (timer)
320
+ clearTimeout(timer);
321
+ external?.removeEventListener?.('abort', onExternalAbort);
322
+ }
323
+ }
324
+ /** 退避睡眠 (可取消; 不真等领域: 端口注入 sleep) */
325
+ async sleepOrAbort(ms, signal) {
326
+ if (signal?.aborted) {
327
+ this.stats.aborts += 1;
328
+ throw new ModelAbortError();
329
+ }
330
+ const sleep = this.ports.sleep ?? ((d) => new Promise((r) => setTimeout(r, d)));
331
+ let onAbort;
332
+ const aborted = new Promise((_, reject) => {
333
+ if (!signal)
334
+ return;
335
+ onAbort = () => { this.stats.aborts += 1; reject(new ModelAbortError()); };
336
+ signal.addEventListener('abort', onAbort, { once: true });
337
+ });
338
+ try {
339
+ await Promise.race([sleep(ms, signal), aborted]);
340
+ }
341
+ finally {
342
+ if (onAbort)
343
+ signal?.removeEventListener?.('abort', onAbort);
344
+ }
345
+ }
346
+ /** 熔断门: 开路且未到冷却 ⇒ 返回"何时可再试" (调用方快速失败); 冷却已过 ⇒ 转半开并放行探测 */
347
+ breakerGate(key) {
348
+ const b = this.breakers.get(key);
349
+ if (!b || b.state === 'closed')
350
+ return null;
351
+ const nowMs = (this.ports.now ?? Date.now)();
352
+ if (b.state === 'open' && nowMs >= b.openUntil) {
353
+ b.state = 'half-open';
354
+ b.probes = 0;
355
+ }
356
+ if (b.state === 'open')
357
+ return b.openUntil;
358
+ if (b.probes >= BREAKER_POLICY.halfOpenProbes)
359
+ return b.openUntil; // 半开探测名额用完 ⇒ 仍然快速失败
360
+ b.probes += 1;
361
+ this.stats.halfOpenProbes += 1;
362
+ return null;
363
+ }
364
+ /** 成功: 半开 ⇒ 闭合 (清零); 闭合态失败计数清零 */
365
+ onSuccess(key) {
366
+ const b = this.breakers.get(key);
367
+ if (!b)
368
+ return;
369
+ if (b.state === 'half-open') {
370
+ b.state = 'closed';
371
+ b.failures = 0;
372
+ b.probes = 0;
373
+ this.stats.breakerClosed += 1;
374
+ return;
375
+ }
376
+ b.failures = 0;
377
+ }
378
+ /** 失败: 累计; 达阈值 ⇒ 开路; 半开里失败 ⇒ 立刻重新开路 (冷却重新计时) */
379
+ onFailure(key) {
380
+ const b = this.breakers.get(key) ?? { failures: 0, state: 'closed', openUntil: 0, probes: 0 };
381
+ b.failures += 1;
382
+ const halfOpenFail = b.state === 'half-open';
383
+ if (halfOpenFail || b.failures >= BREAKER_POLICY.failureThreshold) {
384
+ b.state = 'open';
385
+ b.openUntil = (this.ports.now ?? Date.now)() + BREAKER_POLICY.cooldownMs;
386
+ b.probes = 0;
387
+ this.stats.circuitOpened += 1;
388
+ }
389
+ this.breakers.set(key, b);
390
+ }
391
+ /** 只读: 逐 key 熔断状态 (诊断/测试) */
392
+ breakerStates() {
393
+ const out = {};
394
+ for (const [k, v] of this.breakers)
395
+ out[k] = { failures: v.failures, state: v.state, openUntil: v.openUntil };
396
+ return out;
397
+ }
398
+ /** 取并发槽 (逐 key; 排队可取消) */
399
+ async acquireSlot(key, signal) {
400
+ const cur = this.active.get(key) ?? 0;
401
+ if (cur < this.maxConcurrency) {
402
+ this.active.set(key, cur + 1);
403
+ this.stats.maxObservedConcurrency = Math.max(this.stats.maxObservedConcurrency, cur + 1);
404
+ return;
405
+ }
406
+ if (signal?.aborted) {
407
+ this.stats.aborts += 1;
408
+ throw new ModelAbortError();
409
+ }
410
+ this.stats.concurrencyWaits += 1;
411
+ const granted = { value: false };
412
+ await new Promise((resolve, reject) => {
413
+ const list = this.waiters.get(key) ?? [];
414
+ const entry = { resolve, reject, signal };
415
+ const onAbort = () => {
416
+ const i = list.indexOf(entry);
417
+ if (i >= 0)
418
+ list.splice(i, 1);
419
+ this.stats.aborts += 1;
420
+ reject(new ModelAbortError());
421
+ };
422
+ if (signal)
423
+ signal.addEventListener('abort', onAbort, { once: true });
424
+ list.push(entry);
425
+ this.waiters.set(key, list);
426
+ const wrappedResolve = () => { if (signal)
427
+ signal.removeEventListener('abort', onAbort); resolve(); };
428
+ entry.resolve = wrappedResolve;
429
+ // 槽是**转让**来的 (releaseSlot 直接 resolve 并置 granted) ⇒ 不再自增, 否则并发账虚高 (实测踩到)
430
+ this.grantedProbe.set(entry, granted);
431
+ });
432
+ if (!granted.value) {
433
+ this.active.set(key, (this.active.get(key) ?? 0) + 1);
434
+ this.stats.maxObservedConcurrency = Math.max(this.stats.maxObservedConcurrency, this.active.get(key));
435
+ }
436
+ }
437
+ /** 还槽: 有排队者就交给它 (不释放额度, 直接转让) */
438
+ releaseSlot(key) {
439
+ const list = this.waiters.get(key) ?? [];
440
+ const next = list.shift();
441
+ if (next) {
442
+ if (!this.waiters.get(key)?.length)
443
+ this.waiters.delete(key);
444
+ const probe = this.grantedProbe.get(next);
445
+ if (probe)
446
+ probe.value = true;
447
+ next.resolve();
448
+ return;
449
+ }
450
+ const cur = this.active.get(key) ?? 0;
451
+ if (cur <= 1)
452
+ this.active.delete(key);
453
+ else
454
+ this.active.set(key, cur - 1);
455
+ }
456
+ /** 只读统计 (诊断/门用) */
457
+ snapshotStats() {
458
+ return { connections: this.pool.size, defaultTimeoutMs: this.defaultTimeoutMs, ...this.stats };
459
+ }
460
+ /** 关闭全部连接 (进程收尾用) */
461
+ async closeAll() {
462
+ const all = [...this.pool.values()];
463
+ this.pool.clear();
464
+ await Promise.all(all.map(async (s) => {
465
+ try {
466
+ const c = s.conn ?? (await s.opening);
467
+ await c.close();
468
+ }
469
+ catch { /* 关不掉的连接不阻塞收尾 */ }
470
+ }));
471
+ }
472
+ }