oasis_test_v2 2.2.72 → 2.2.73

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 (2) hide show
  1. package/dist/index.js +996 -38
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -845,6 +845,18 @@ var init_runtime = __esm({
845
845
  }
846
846
  });
847
847
 
848
+ // ../contract/src/runtime-allocation.ts
849
+ var RUNTIME_FAULT_PROBE_MAX_INTERVAL_MS, RUNTIME_FAULT_PROBE_MAX_TOTAL_MS, RUNTIME_QUOTA_DERATE_THRESHOLD_PERCENT, RUNTIME_ALLOCATION_MAX_ATTEMPTS;
850
+ var init_runtime_allocation = __esm({
851
+ "../contract/src/runtime-allocation.ts"() {
852
+ "use strict";
853
+ RUNTIME_FAULT_PROBE_MAX_INTERVAL_MS = 30 * 6e4;
854
+ RUNTIME_FAULT_PROBE_MAX_TOTAL_MS = 6 * 60 * 6e4;
855
+ RUNTIME_QUOTA_DERATE_THRESHOLD_PERCENT = 60;
856
+ RUNTIME_ALLOCATION_MAX_ATTEMPTS = 3;
857
+ }
858
+ });
859
+
848
860
  // ../contract/src/handle-passthrough.ts
849
861
  function makePassthroughProxy(spec) {
850
862
  const overrides = spec.overrides ?? {};
@@ -2260,6 +2272,7 @@ var init_src = __esm({
2260
2272
  init_storage();
2261
2273
  init_avatar();
2262
2274
  init_runtime();
2275
+ init_runtime_allocation();
2263
2276
  init_handle_passthrough();
2264
2277
  init_parse_extra_args();
2265
2278
  init_daemon_package();
@@ -136770,7 +136783,7 @@ var init_bus = __esm({
136770
136783
  notify: (ids2, payload2) => this.io.notify(ids2, payload2)
136771
136784
  };
136772
136785
  let ok3 = true;
136773
- let failureReason = null;
136786
+ let failureReason2 = null;
136774
136787
  for (const h of handlers) {
136775
136788
  const effectKey = `${h.name}#effect`;
136776
136789
  if (record9.appliedAt && await this.store.transaction((tx) => tx.isHandlerDone(record9.seq, effectKey))) {
@@ -136793,7 +136806,7 @@ var init_bus = __esm({
136793
136806
  await this.store.transaction((tx) => tx.deferEvent(record9.seq, "applied", lastErr, this.retryAt()));
136794
136807
  return false;
136795
136808
  }
136796
- failureReason = lastErr;
136809
+ failureReason2 = lastErr;
136797
136810
  } else {
136798
136811
  await this.store.transaction(async (tx) => {
136799
136812
  if (buffered.length) await tx.appendEvents(buffered.map((b2) => ({
@@ -136812,7 +136825,7 @@ var init_bus = __esm({
136812
136825
  });
136813
136826
  }
136814
136827
  }
136815
- await this.store.transaction((tx) => tx.markEvent(record9.seq, "done", failureReason, this.clock.now()));
136828
+ await this.store.transaction((tx) => tx.markEvent(record9.seq, "done", failureReason2, this.clock.now()));
136816
136829
  return ok3;
136817
136830
  }
136818
136831
  /* ═══════════════════════ scan:唯一的调度入口 ═══════════════════════ */
@@ -142340,7 +142353,10 @@ var init_dispatcher = __esm({
142340
142353
  * 画板只服务于「产界面」,评审/回复/收口回到能真正读写、能投票的通用运行时。
142341
142354
  */
142342
142355
  async resolveBindingFor(spec) {
142343
- const binding = await this.opts.resolveBinding?.(spec.actor) ?? void 0;
142356
+ const binding = await this.opts.resolveBinding?.(spec.actor, {
142357
+ ...spec.artifactId ? { unitOfWork: spec.artifactId } : {},
142358
+ action: spec.action
142359
+ }) ?? void 0;
142344
142360
  if (binding && spec.action !== "produce" && binding.runtimeKind === CANVAS_RUNTIME_KIND) {
142345
142361
  return { ...binding, runtimeKind: NON_PRODUCE_FALLBACK_RUNTIME_KIND };
142346
142362
  }
@@ -165172,9 +165188,24 @@ var init_live_chat = __esm({
165172
165188
  }
165173
165189
  if (turn.status === "running") {
165174
165190
  turn.cancelCause = "user";
165175
- try {
165176
- turn.handle.kill();
165177
- } catch {
165191
+ const sendKill = turn.handle.sendKill;
165192
+ if (typeof sendKill === "function") {
165193
+ let sent = false;
165194
+ try {
165195
+ sent = sendKill.call(turn.handle);
165196
+ } catch {
165197
+ }
165198
+ if (!sent) {
165199
+ try {
165200
+ turn.handle.cancelLocally?.();
165201
+ } catch {
165202
+ }
165203
+ }
165204
+ } else {
165205
+ try {
165206
+ turn.handle.kill();
165207
+ } catch {
165208
+ }
165178
165209
  }
165179
165210
  }
165180
165211
  return { stopped: true };
@@ -213451,6 +213482,9 @@ ${composed}`;
213451
213482
  kill: () => {
213452
213483
  void session.kill?.();
213453
213484
  },
213485
+ // Bug 0232:把「帧送没送出去 / 就地收尾」这对出口透到注册表,`/stop` 才判得了。
213486
+ // 条件透传不是省事:上层按「方法在不在」分流,本地 adapter 没有这条就该没有。
213487
+ ...typeof session.sendKill === "function" ? { sendKill: () => session.sendKill(), cancelLocally: () => session.cancelLocally?.() ?? false } : {},
213454
213488
  // ADR-0093 chat 道:把插入能力透到注册表,POST …/append 才有得调。
213455
213489
  ...typeof session.appendInput === "function" ? { appendInput: (input) => session.appendInput(input) } : {},
213456
213490
  get canAppendInput() {
@@ -215381,6 +215415,7 @@ var init_memory_registry_store = __esm({
215381
215415
  agentPrefs = /* @__PURE__ */ new Map();
215382
215416
  teams = /* @__PURE__ */ new Map();
215383
215417
  bindings = /* @__PURE__ */ new Map();
215418
+ runtimeCandidates = /* @__PURE__ */ new Map();
215384
215419
  runtimeConfigs = /* @__PURE__ */ new Map();
215385
215420
  runtimeModelOverrides = /* @__PURE__ */ new Map();
215386
215421
  // key: ${runtimeId}::${modelId}
@@ -215482,6 +215517,28 @@ var init_memory_registry_store = __esm({
215482
215517
  async listBindings() {
215483
215518
  return [...this.bindings.values()].map((b2) => ({ ...b2 }));
215484
215519
  }
215520
+ /* 候选 runtime(ADR 0616):与 PG 实现同一套语义——按 ord 升序读、整份替换、同 (node,kind) 去重。 */
215521
+ async listRuntimeCandidates(actorId) {
215522
+ return (this.runtimeCandidates.get(actorId) ?? []).map((c) => ({ ...c }));
215523
+ }
215524
+ async replaceRuntimeCandidates(actorId, candidates) {
215525
+ const seen = /* @__PURE__ */ new Set();
215526
+ const rows = [];
215527
+ for (const c of candidates) {
215528
+ const key2 = `${c.nodeId}::${c.runtimeKind}`;
215529
+ const model = typeof c.model === "string" && c.model.trim() ? c.model.trim() : null;
215530
+ const existing = rows.findIndex((r) => `${r.nodeId}::${r.runtimeKind}` === key2);
215531
+ if (existing >= 0) {
215532
+ rows[existing] = { ...rows[existing], model };
215533
+ continue;
215534
+ }
215535
+ seen.add(key2);
215536
+ rows.push({ actorId, nodeId: c.nodeId, runtimeKind: c.runtimeKind, model, ord: rows.length });
215537
+ }
215538
+ if (rows.length === 0) this.runtimeCandidates.delete(actorId);
215539
+ else this.runtimeCandidates.set(actorId, rows);
215540
+ return rows.map((c) => ({ ...c }));
215541
+ }
215485
215542
  async upsertRuntimeConfig(c) {
215486
215543
  this.runtimeConfigs.set(c.runtimeId, { ...c });
215487
215544
  }
@@ -222640,6 +222697,20 @@ var init_company_scoped_registry = __esm({
222640
222697
  async getBinding(actorId) {
222641
222698
  return await this.assertScoped(actorId, "getBinding") ? this.base.getBinding(actorId) : null;
222642
222699
  }
222700
+ /* 候选 runtime(ADR 0616):与 binding 同一道租户闸——越界读回空、越界写抛。
222701
+
222702
+ 刻意**不**按 `this.base` 有没有这两个方法来决定自己有没有(那样得写成类字段,而类字段的初始化
222703
+ 早于构造参数属性赋值,`this.base` 在那一刻还是 undefined)。底座没实现时:读回空、写抛一句
222704
+ 指名道姓的错——比静默丢掉一份候选清单强。 */
222705
+ async listRuntimeCandidates(actorId) {
222706
+ if (!this.base.listRuntimeCandidates) return [];
222707
+ return await this.assertScoped(actorId, "listRuntimeCandidates") ? this.base.listRuntimeCandidates(actorId) : [];
222708
+ }
222709
+ async replaceRuntimeCandidates(actorId, candidates) {
222710
+ await this.requireScoped(actorId, "replaceRuntimeCandidates");
222711
+ if (!this.base.replaceRuntimeCandidates) throw new Error("\u5F53\u524D\u767B\u8BB0\u5B58\u50A8\u5B9E\u73B0\u4E0D\u652F\u6301\u5019\u9009\u8FD0\u884C\u65F6\uFF08replaceRuntimeCandidates \u672A\u5B9E\u73B0\uFF09");
222712
+ return this.base.replaceRuntimeCandidates(actorId, candidates);
222713
+ }
222643
222714
  async listBindings() {
222644
222715
  const [all2, ids2] = await Promise.all([this.base.listBindings(), this.scopeIds()]);
222645
222716
  const actors = [];
@@ -225804,6 +225875,8 @@ function wireRecoveredChatTurn(deps) {
225804
225875
  kill: () => {
225805
225876
  void handle.kill();
225806
225877
  },
225878
+ // Bug 0232:重挂的轮一样会被人点停止、一样可能撞上节点够不着——同一对出口原样透传。
225879
+ ...typeof handle.sendKill === "function" ? { sendKill: () => handle.sendKill(), cancelLocally: () => handle.cancelLocally?.() ?? false } : {},
225807
225880
  // ADR-0093 chat 道:重挂的轮同样能被插入——否则「serve 重启过」会变成一条人看不见的能力差异。
225808
225881
  // **整份透传**(2026-09-17):此前只转 `text`,插话链路要的消费回执(`requireConsumptionReceipt`)、
225809
225882
  // 幂等键(`clientMessageKey`)与附件(`files` / `binaryFiles`)全被剥掉——runtime 照样读到了正文,
@@ -228400,6 +228473,32 @@ var init_service5 = __esm({
228400
228473
  listBindings() {
228401
228474
  return this.opts.store.listBindings();
228402
228475
  }
228476
+ /* ---------- 候选 runtime(ADR 0616) ---------- */
228477
+ /** 按 `ord` 升序。底座没实现(旧存储实现)→ 空清单,等于「只有主绑定」。 */
228478
+ listRuntimeCandidates(actorId) {
228479
+ return this.opts.store.listRuntimeCandidates?.(actorId) ?? Promise.resolve([]);
228480
+ }
228481
+ /**
228482
+ * 整份替换候选清单(加 / 删 / 排序在界面上是同一个动作)。返回写后的整份。
228483
+ *
228484
+ * 校验放在这里而不是路由:候选是「派到哪」的事实源,写进去的 `(nodeId, runtimeKind)`
228485
+ * 必须真的存在于这家公司的名册上——否则分配那一侧只会在几个月后的某次主机故障里,
228486
+ * 报一句「节点不存在」然后把活挂住,而错是在写入那一刻犯的。
228487
+ */
228488
+ async replaceRuntimeCandidates(actorId, candidates, by) {
228489
+ if (!this.opts.store.replaceRuntimeCandidates) {
228490
+ throw new Error("\u5F53\u524D\u767B\u8BB0\u5B58\u50A8\u5B9E\u73B0\u4E0D\u652F\u6301\u5019\u9009\u8FD0\u884C\u65F6");
228491
+ }
228492
+ const saved = await this.opts.store.replaceRuntimeCandidates(actorId, candidates);
228493
+ await this.audit({
228494
+ kind: "binding_change",
228495
+ actorId,
228496
+ by,
228497
+ at: this.now(),
228498
+ detail: { candidates: saved.map((c) => ({ nodeId: c.nodeId, runtimeKind: c.runtimeKind, model: c.model ?? null })) }
228499
+ });
228500
+ return saved;
228501
+ }
228403
228502
  /** 派单接缝的真实现:替换第零步假实现,第三部分代码零改动(决策 0001) */
228404
228503
  resolveBinding() {
228405
228504
  return async (actorId) => {
@@ -230849,6 +230948,42 @@ function actorsDomain(opts) {
230849
230948
  );
230850
230949
  return { status: 201, body: cfg };
230851
230950
  });
230951
+ router.get("/api/actors/:id/runtime-candidates", async (req) => {
230952
+ const { service } = await resolveCtx(req.auth.companyId);
230953
+ return { status: 200, body: { items: await service.listRuntimeCandidates(req.params.id) } };
230954
+ });
230955
+ router.put("/api/actors/:id/runtime-candidates", async (req) => {
230956
+ const { service } = await resolveCtx(req.auth.companyId);
230957
+ const actorId = req.params.id;
230958
+ if (!await service.getActor(actorId)) throw new ApiError(404, "NOT_FOUND", "\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5");
230959
+ const raw = req.body?.candidates;
230960
+ if (!Array.isArray(raw)) throw new ApiError(400, "BAD_REQUEST", "candidates \u5FC5\u987B\u662F\u6570\u7EC4");
230961
+ if (raw.length > 20) throw new ApiError(400, "BAD_REQUEST", "\u5019\u9009\u8FD0\u884C\u65F6\u6700\u591A 20 \u6761");
230962
+ const candidates = [];
230963
+ for (const item of raw) {
230964
+ const c = item;
230965
+ if (typeof c.nodeId !== "string" || !c.nodeId.trim()) throw new ApiError(400, "BAD_REQUEST", "\u6BCF\u6761\u5019\u9009\u90FD\u8981\u6709 nodeId");
230966
+ if (typeof c.runtimeKind !== "string" || !c.runtimeKind.trim()) throw new ApiError(400, "BAD_REQUEST", "\u6BCF\u6761\u5019\u9009\u90FD\u8981\u6709 runtimeKind");
230967
+ if (c.model !== void 0 && c.model !== null && typeof c.model !== "string") {
230968
+ throw new ApiError(400, "BAD_REQUEST", "model \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null\uFF08null = \u8DDF\u968F\u8BE5 runtime \u9ED8\u8BA4\uFF09");
230969
+ }
230970
+ candidates.push({
230971
+ nodeId: c.nodeId.trim(),
230972
+ runtimeKind: c.runtimeKind.trim(),
230973
+ ...c.model === void 0 ? {} : { model: typeof c.model === "string" ? c.model : null }
230974
+ });
230975
+ }
230976
+ if (opts.resolveNodeTenancy) {
230977
+ for (const c of candidates) {
230978
+ const facts = await opts.resolveNodeTenancy(c.nodeId).catch(() => null) ?? null;
230979
+ const foreign = foreignNodeBlock(facts, req.auth.companyId);
230980
+ if (foreign) throw new ApiError(400, "NODE_NOT_IN_COMPANY", foreign);
230981
+ if (!facts) throw new ApiError(400, "NODE_NOT_FOUND", `\u627E\u4E0D\u5230\u8282\u70B9 ${c.nodeId}`);
230982
+ }
230983
+ }
230984
+ const items = await service.replaceRuntimeCandidates(actorId, candidates, req.auth.actor);
230985
+ return { status: 200, body: { items } };
230986
+ });
230852
230987
  router.get("/api/actors/:id/config/versions", async (req) => {
230853
230988
  const { service } = await resolveCtx(req.auth.companyId);
230854
230989
  return { status: 200, body: { items: await service.listConfigVersions(req.params.id) } };
@@ -242898,7 +243033,7 @@ var init_runtime_activity = __esm({
242898
243033
 
242899
243034
  // ../server/src/node-health.ts
242900
243035
  function deriveNodeHealth(input) {
242901
- const staleMs = input.staleMs ?? 9e4;
243036
+ const staleMs = input.staleMs ?? NODE_HEARTBEAT_STALE_MS;
242902
243037
  const maxFail = input.maxConsecutiveFailures ?? 3;
242903
243038
  const cf = input.snapshot?.consecutiveFailures ?? 0;
242904
243039
  const heartbeatFresh = input.lastHeartbeatMs !== void 0 && input.now - input.lastHeartbeatMs < staleMs;
@@ -242957,7 +243092,7 @@ function describeNodeFault(f2, now = Date.now()) {
242957
243092
  `\u5728\u90A3\u4E4B\u524D\uFF0C\u91CD\u8BD5\u3001\u6362\u4EBA\u3001\u6539\u5185\u5BB9\u90FD\u4E0D\u4F1A\u8BA9\u5B83\u53D8\u597D\u3002`
242958
243093
  ].join("");
242959
243094
  }
242960
- var NodeHealthTracker;
243095
+ var NodeHealthTracker, NODE_HEARTBEAT_STALE_MS;
242961
243096
  var init_node_health = __esm({
242962
243097
  "../server/src/node-health.ts"() {
242963
243098
  "use strict";
@@ -242999,6 +243134,7 @@ var init_node_health = __esm({
242999
243134
  this.onChange?.();
243000
243135
  }
243001
243136
  };
243137
+ NODE_HEARTBEAT_STALE_MS = 9e4;
243002
243138
  }
243003
243139
  });
243004
243140
 
@@ -243034,6 +243170,15 @@ async function resolveRuntimeModels(deps, runtimeId) {
243034
243170
  }
243035
243171
  function nodesDomain(deps) {
243036
243172
  const quotaByNode = /* @__PURE__ */ new Map();
243173
+ void deps.nodeStore.listRuntimeQuotas().then((rows) => {
243174
+ for (const row of rows) {
243175
+ if (row.runtimeKind !== "codex" && row.runtimeKind !== "claude-code") continue;
243176
+ const list2 = quotaByNode.get(row.nodeId) ?? [];
243177
+ list2.push({ kind: row.runtimeKind, status: row.status, checkedAt: row.checkedAt, windows: row.windows, ...row.retryAt ? { retryAt: row.retryAt } : {} });
243178
+ quotaByNode.set(row.nodeId, list2);
243179
+ }
243180
+ if (rows.length > 0) console.log(`[nodes] \u989D\u5EA6\u6C34\u5408\uFF1A\u4ECE\u5E93\u91CC\u8BFB\u56DE ${rows.length} \u6761\u8FD0\u884C\u65F6\u989D\u5EA6`);
243181
+ }).catch((err) => console.warn(`[nodes] \u989D\u5EA6\u6C34\u5408\u5931\u8D25\uFF08\u7167\u65E7\u7B49\u4E0B\u4E00\u6B21\u91C7\u96C6\uFF09\uFF1A${err instanceof Error ? err.message : String(err)}`));
243037
243182
  const UPDATE_TIMEOUT_MS = 15e4;
243038
243183
  const nodeUpdates = /* @__PURE__ */ new Map();
243039
243184
  function runtimeInfoOf(r) {
@@ -243134,7 +243279,9 @@ function nodesDomain(deps) {
243134
243279
  if (msg.type !== "runtime_quotas") return;
243135
243280
  const daemon = hub.connectedDaemons().find((d) => d.daemonId === daemonId);
243136
243281
  if (!daemon) return;
243137
- quotaByNode.set(daemonId, sanitizeRuntimeQuotas(msg.quotas).filter((q2) => daemon.meta.adapters.includes(q2.kind)));
243282
+ const quotas = sanitizeRuntimeQuotas(msg.quotas).filter((q2) => daemon.meta.adapters.includes(q2.kind));
243283
+ quotaByNode.set(daemonId, quotas);
243284
+ void nodeStore.saveRuntimeQuotas(daemonId, quotas.map((q2) => ({ ...q2, nodeId: daemonId, runtimeKind: q2.kind }))).catch((err) => console.warn(`[nodes] \u989D\u5EA6\u843D\u5E93\u5931\u8D25(node=${daemonId})\uFF1A${err instanceof Error ? err.message : String(err)}`));
243138
243285
  });
243139
243286
  hub.addMessageListener(async (daemonId, msg) => {
243140
243287
  if (msg.type !== "hello") return;
@@ -243286,6 +243433,10 @@ function nodesDomain(deps) {
243286
243433
  ...sourceIpById.get(n.id) ? { sourceIp: sourceIpById.get(n.id) } : {},
243287
243434
  ...n.name ? { name: n.name } : {},
243288
243435
  ...updateStatus ? { updateStatus } : {},
243436
+ /* 并发上限与当前在跑数(ADR 0616 D2):`maxConcurrent` 只在真设过时带这一格——
243437
+ 缺省就是「未设置 = 不限制」,带一个 null 进去会让前端不得不再判一次。 */
243438
+ ...n.maxConcurrent != null ? { maxConcurrent: n.maxConcurrent } : {},
243439
+ activeRunCount,
243289
243440
  ...lastDisconnectedAt || lastReconnectedAt ? {
243290
243441
  connection: {
243291
243442
  ...lastDisconnectedAt ? { lastDisconnectedAt } : {},
@@ -243485,6 +243636,24 @@ function nodesDomain(deps) {
243485
243636
  deps.onStatusChange?.();
243486
243637
  return { status: 200, body: { ok: true, name } };
243487
243638
  });
243639
+ router.put("/api/nodes/:nodeId/max-concurrent", async (req) => {
243640
+ const nodeId = req.params["nodeId"];
243641
+ if (!nodeId) return { status: 400, body: { error: "missing nodeId" } };
243642
+ const forbidden = await forbidUnlessOrgManager(req, "\u8BBE\u7F6E\u8282\u70B9\u5E76\u53D1\u4E0A\u9650");
243643
+ if (forbidden) return forbidden;
243644
+ const scope2 = req.auth.companyId !== void 0 ? { companyId: req.auth.companyId } : void 0;
243645
+ const node2 = (await deps.nodeStore.listNodes(scope2)).find((n) => n.id === nodeId);
243646
+ if (!node2) return { status: 404, body: { error: "NODE_NOT_FOUND", message: "\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u4E0D\u5C5E\u4E8E\u5F53\u524D\u7EC4\u7EC7" } };
243647
+ const raw = req.body ?? {};
243648
+ const value2 = raw.maxConcurrent;
243649
+ if (value2 !== null && value2 !== void 0 && !(Number.isSafeInteger(value2) && value2 >= 0 && value2 <= 1e3)) {
243650
+ return { status: 400, body: { error: "BAD_REQUEST", message: "maxConcurrent \u5FC5\u987B\u662F 0\u20131000 \u7684\u6574\u6570\uFF0C\u6216 null\uFF08\u4E0D\u9650\u5236\uFF09" } };
243651
+ }
243652
+ const limit = value2 === void 0 || value2 === null ? null : value2;
243653
+ await deps.nodeStore.setNodeMaxConcurrent(nodeId, limit);
243654
+ deps.onStatusChange?.();
243655
+ return { status: 200, body: { ok: true, maxConcurrent: limit } };
243656
+ });
243488
243657
  router.delete("/api/nodes/:nodeId", async (req) => {
243489
243658
  const nodeId = req.params["nodeId"];
243490
243659
  if (!nodeId) return { status: 400, body: { error: "missing nodeId" } };
@@ -243706,6 +243875,353 @@ var init_runtime_drift = __esm({
243706
243875
  }
243707
243876
  });
243708
243877
 
243878
+ // ../server/src/domains/runtime-allocation/allocate.ts
243879
+ function runtimeTargetKey(target) {
243880
+ return `${target.nodeId}::${target.runtimeKind}`;
243881
+ }
243882
+ function idlenessOf(node2) {
243883
+ const limit = node2.maxConcurrent;
243884
+ if (limit === null || limit === void 0) return 1;
243885
+ if (limit <= 0) return 0;
243886
+ return Math.max(0, 1 - node2.activeRunCount / limit);
243887
+ }
243888
+ function atCapacity(node2) {
243889
+ const limit = node2.maxConcurrent;
243890
+ if (limit === null || limit === void 0) return false;
243891
+ return node2.activeRunCount >= limit;
243892
+ }
243893
+ function quotaDerateFactor(quota) {
243894
+ if (!quota || quota.status !== "ok" || quota.windows.length === 0) return { factor: 1 };
243895
+ const worst = Math.max(...quota.windows.map((w2) => w2.usedPercent));
243896
+ if (worst <= RUNTIME_QUOTA_DERATE_THRESHOLD_PERCENT) return { factor: 1, worstUsedPercent: worst };
243897
+ const remaining = Math.max(0, 100 - worst);
243898
+ return { factor: remaining / (100 - RUNTIME_QUOTA_DERATE_THRESHOLD_PERCENT), worstUsedPercent: worst };
243899
+ }
243900
+ function faultFromQuota(target, quota, now) {
243901
+ if (!quota) return null;
243902
+ const base = { nodeId: target.nodeId, runtimeKind: target.runtimeKind, markedAt: quota.checkedAt, attempts: 0 };
243903
+ if (quota.status === "unauthenticated") {
243904
+ return { ...base, reason: "unauthenticated", awaitingManual: true, detail: "\u8FD0\u884C\u65F6\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u9700\u8981\u5728\u8BE5\u673A\u5668\u4E0A\u91CD\u65B0\u767B\u5F55" };
243905
+ }
243906
+ if (quota.status === "rate_limited") {
243907
+ return {
243908
+ ...base,
243909
+ reason: "quota-exhausted",
243910
+ awaitingManual: false,
243911
+ ...quota.retryAt ? { nextProbeAt: quota.retryAt } : {},
243912
+ detail: "provider \u62A5\u9650\u6D41/\u989D\u5EA6\u8017\u5C3D"
243913
+ };
243914
+ }
243915
+ if (quota.status !== "ok") return null;
243916
+ const exhausted = quota.windows.filter((w2) => w2.usedPercent >= 100);
243917
+ if (exhausted.length === 0) return null;
243918
+ const resets = exhausted.map((w2) => w2.resetsAt).filter((v2) => Boolean(v2)).sort();
243919
+ const latest = resets.at(-1);
243920
+ return {
243921
+ ...base,
243922
+ reason: "quota-exhausted",
243923
+ awaitingManual: false,
243924
+ ...latest && Date.parse(latest) > now ? { nextProbeAt: latest } : {},
243925
+ detail: `\u989D\u5EA6\u5DF2\u7528\u6EE1\uFF08${exhausted.map((w2) => `${w2.bucket} ${Math.round(w2.usedPercent)}%`).join("\u3001")}\uFF09`
243926
+ };
243927
+ }
243928
+ function faultIsBlocking(fault, now) {
243929
+ if (fault.awaitingManual) return true;
243930
+ if (!fault.nextProbeAt) return true;
243931
+ return Date.parse(fault.nextProbeAt) > now;
243932
+ }
243933
+ function failureReason(rejections) {
243934
+ if (rejections.length === 0) return "no-candidates";
243935
+ const causes = new Set(rejections.map((r) => r.cause));
243936
+ if (causes.size === 1) {
243937
+ const only = [...causes][0];
243938
+ if (only === "at-capacity") return "all-at-capacity";
243939
+ if (only === "already-tried") return "all-tried";
243940
+ return "all-faulted";
243941
+ }
243942
+ const faultLike = /* @__PURE__ */ new Set(["node-unknown", "node-offline", "runtime-missing", "faulted"]);
243943
+ if ([...causes].every((c) => faultLike.has(c))) return "all-faulted";
243944
+ return "mixed";
243945
+ }
243946
+ function allocateRuntime(input, now = Date.now()) {
243947
+ const rejections = [];
243948
+ const eligible = [];
243949
+ for (const candidate of input.candidates) {
243950
+ const key2 = runtimeTargetKey(candidate);
243951
+ const node2 = input.nodes.get(candidate.nodeId);
243952
+ const nodeName = node2?.displayName?.trim() || candidate.nodeId;
243953
+ const reject = (cause, detail) => rejections.push({ nodeId: candidate.nodeId, runtimeKind: candidate.runtimeKind, nodeName, cause, ...detail ? { detail } : {} });
243954
+ if (input.alreadyTried?.has(key2)) {
243955
+ reject("already-tried", input.triedDetail?.get(key2) ?? "\u672C\u6B21\u5DF2\u8BD5\u8FC7\u5E76\u5931\u8D25");
243956
+ continue;
243957
+ }
243958
+ if (!node2) {
243959
+ reject("node-unknown", "\u8282\u70B9\u4E0D\u5B58\u5728\u3001\u5DF2\u5220\u9664\uFF0C\u6216\u4E0D\u5C5E\u4E8E\u5F53\u524D\u516C\u53F8");
243960
+ continue;
243961
+ }
243962
+ if (!node2.online) {
243963
+ reject("node-offline", "\u8282\u70B9\u5F53\u524D\u79BB\u7EBF");
243964
+ continue;
243965
+ }
243966
+ if (!node2.runtimeKinds.includes(candidate.runtimeKind)) {
243967
+ reject("runtime-missing", "\u8BE5\u673A\u5668\u4E0A\u6CA1\u6709\u8FD9\u4E2A\u8FD0\u884C\u65F6\uFF08\u672A\u88C5\u597D\u3001\u5DF2\u5378\u8F7D\u6216\u672A\u4E0A\u62A5\uFF09");
243968
+ continue;
243969
+ }
243970
+ const fault = input.faults?.get(key2) ?? faultFromQuota(candidate, input.quotas?.get(key2), now);
243971
+ if (fault && faultIsBlocking(fault, now)) {
243972
+ reject("faulted", `${fault.reason}${fault.detail ? `\uFF1A${fault.detail}` : ""}${fault.awaitingManual ? "\uFF08\u7B49\u4EBA\u5DE5\u6062\u590D\uFF09" : ""}`);
243973
+ continue;
243974
+ }
243975
+ if (atCapacity(node2)) {
243976
+ reject("at-capacity", `\u5728\u8DD1 ${node2.activeRunCount} / \u4E0A\u9650 ${node2.maxConcurrent}`);
243977
+ continue;
243978
+ }
243979
+ eligible.push(candidate);
243980
+ }
243981
+ if (eligible.length === 0) return { ok: false, reason: failureReason(rejections), rejections };
243982
+ const primary = eligible.find((c) => c.primary);
243983
+ const rank = (candidate) => {
243984
+ const node2 = input.nodes.get(candidate.nodeId);
243985
+ const idleness = idlenessOf(node2);
243986
+ const { factor, worstUsedPercent } = quotaDerateFactor(input.quotas?.get(runtimeTargetKey(candidate)));
243987
+ return {
243988
+ candidate,
243989
+ nodeName: node2.displayName?.trim() || candidate.nodeId,
243990
+ idleness,
243991
+ quotaFactor: factor,
243992
+ weight: idleness * factor,
243993
+ ...worstUsedPercent !== void 0 ? { worstQuotaUsedPercent: worstUsedPercent } : {}
243994
+ };
243995
+ };
243996
+ if (primary) {
243997
+ const ranked2 = [rank(primary)];
243998
+ return { ok: true, chosen: primary, nodeName: ranked2[0].nodeName, source: "primary", ranked: ranked2, rejections };
243999
+ }
244000
+ const ranked = eligible.map(rank).sort((a, b2) => b2.weight - a.weight || a.candidate.ord - b2.candidate.ord || a.candidate.nodeId.localeCompare(b2.candidate.nodeId) || a.candidate.runtimeKind.localeCompare(b2.candidate.runtimeKind));
244001
+ const top = ranked[0];
244002
+ return { ok: true, chosen: top.candidate, nodeName: top.nodeName, source: "candidate", ranked, rejections };
244003
+ }
244004
+ function canRetryAllocation(attempt) {
244005
+ return attempt < RUNTIME_ALLOCATION_MAX_ATTEMPTS;
244006
+ }
244007
+ function describeAllocationChoice(result) {
244008
+ const head = result.source === "primary" ? `\u9009\u4E2D\u4E3B\u8FD0\u884C\u65F6 ${result.nodeName} \xB7 ${result.chosen.runtimeKind}` : `\u4E3B\u8FD0\u884C\u65F6\u4E0D\u53EF\u7528\uFF0C\u6309\u6743\u91CD\u9009\u4E2D ${result.nodeName} \xB7 ${result.chosen.runtimeKind}`;
244009
+ const detail = result.ranked.map((r) => {
244010
+ const quota = r.worstQuotaUsedPercent === void 0 ? "\u65E0\u989D\u5EA6\u6570\u636E" : `\u989D\u5EA6\u5DF2\u7528 ${Math.round(r.worstQuotaUsedPercent)}%`;
244011
+ return `${r.nodeName}\xB7${r.candidate.runtimeKind} \u6743\u91CD ${r.weight.toFixed(2)}\uFF08\u7A7A\u95F2\u5EA6 ${r.idleness.toFixed(2)} \xD7 \u989D\u5EA6\u7CFB\u6570 ${r.quotaFactor.toFixed(2)}\uFF0C${quota}\uFF09`;
244012
+ }).join("\uFF1B");
244013
+ const skipped = result.rejections.length > 0 ? `\uFF1B\u6392\u9664 ${result.rejections.map((r) => `${r.nodeName}\xB7${r.runtimeKind}(${r.cause})`).join("\u3001")}` : "";
244014
+ return `${head}\u3002${detail}${skipped}`;
244015
+ }
244016
+ var init_allocate = __esm({
244017
+ "../server/src/domains/runtime-allocation/allocate.ts"() {
244018
+ "use strict";
244019
+ init_src();
244020
+ }
244021
+ });
244022
+
244023
+ // ../server/src/domains/runtime-allocation/errors.ts
244024
+ function describeRejections(rejections) {
244025
+ return rejections.map((r) => `${r.nodeName} \xB7 ${r.runtimeKind}\uFF08${CAUSE_TEXT[r.cause]}${r.detail ? `\uFF1A${r.detail}` : ""}\uFF09`).join("\uFF1B");
244026
+ }
244027
+ function requireAllocation(result, who) {
244028
+ if (result.ok) return result;
244029
+ const subject = who?.actorName?.trim() ? `\u300C${who.actorName.trim()}\u300D` : "\u8FD9\u4F4D Agent";
244030
+ const what = who?.what?.trim() ? `${who.what.trim()}\uFF1A` : "";
244031
+ const detail = describeRejections(result.rejections);
244032
+ if (result.reason === "no-candidates") {
244033
+ throw new RuntimeAllocationError(
244034
+ "RUNTIME_ALLOCATION_NO_CANDIDATES",
244035
+ `${what}${subject}\u6CA1\u6709\u53EF\u7528\u7684\u8FD0\u884C\u65F6\u76EE\u6807\uFF1A\u65E2\u6CA1\u6709\u4E3B\u8FD0\u884C\u65F6\u7ED1\u5B9A\uFF0C\u4E5F\u6CA1\u6709\u914D\u7F6E\u5019\u9009\u8FD0\u884C\u65F6\u3002\u8BF7\u5728\u5458\u5DE5\u8BE6\u60C5\u9875\u7684\u300C\u9ED8\u8BA4\u8FD0\u884C\u914D\u7F6E\u300D\u91CC\u7ED1\u5B9A\u4E00\u4E2A\u8FD0\u884C\u65F6\uFF0C\u6216\u52A0\u4E00\u6761\u5019\u9009\u8FD0\u884C\u65F6\u3002`,
244036
+ result.rejections
244037
+ );
244038
+ }
244039
+ if (result.reason === "all-at-capacity") {
244040
+ throw new RuntimeAllocationError(
244041
+ "RUNTIME_ALLOCATION_ALL_AT_CAPACITY",
244042
+ `${what}${subject}\u7684\u8FD0\u884C\u65F6\u5168\u90E8\u5DF2\u5230\u5E76\u53D1\u4E0A\u9650\uFF0C\u672C\u6B21\u6CA1\u6709\u6D3E\u51FA\uFF1A${detail}\u3002\u8BF7\u7B49\u5F53\u524D\u4F1A\u8BDD\u8DD1\u5B8C\uFF0C\u6216\u5728\u8FD0\u884C\u65F6\u7BA1\u7406\u9875\u628A\u8FD9\u4E9B\u8282\u70B9\u7684\u5E76\u53D1\u4E0A\u9650\u8C03\u9AD8\u3002`,
244043
+ result.rejections,
244044
+ 503
244045
+ );
244046
+ }
244047
+ throw new RuntimeAllocationError(
244048
+ "RUNTIME_ALLOCATION_ALL_UNAVAILABLE",
244049
+ `${what}${subject}\u7684\u8FD0\u884C\u65F6\u76EE\u524D\u90FD\u4E0D\u53EF\u7528\uFF0C\u672C\u6B21\u6CA1\u6709\u6D3E\u51FA\uFF1A${detail}\u3002\u8BF7\u786E\u8BA4\u90A3\u4E9B\u673A\u5668\u5F00\u7740\u3001\u8282\u70B9\u7A0B\u5E8F\u5728\u8FD0\u884C\u3001\u8FD0\u884C\u65F6\u5DF2\u767B\u5F55\u4E14\u989D\u5EA6\u672A\u8017\u5C3D\uFF1B\u6216\u5728\u5458\u5DE5\u8BE6\u60C5\u9875\u52A0\u4E00\u6761\u522B\u7684\u5019\u9009\u8FD0\u884C\u65F6\u3002`,
244050
+ result.rejections,
244051
+ result.reason === "all-tried" ? 409 : 503
244052
+ );
244053
+ }
244054
+ var RuntimeAllocationError, CAUSE_TEXT;
244055
+ var init_errors6 = __esm({
244056
+ "../server/src/domains/runtime-allocation/errors.ts"() {
244057
+ "use strict";
244058
+ RuntimeAllocationError = class extends Error {
244059
+ constructor(code2, message, rejections, status = 409) {
244060
+ super(message);
244061
+ this.code = code2;
244062
+ this.rejections = rejections;
244063
+ this.status = status;
244064
+ this.name = "RuntimeAllocationError";
244065
+ }
244066
+ };
244067
+ CAUSE_TEXT = {
244068
+ "node-unknown": "\u8282\u70B9\u4E0D\u53EF\u7528",
244069
+ "node-offline": "\u8282\u70B9\u79BB\u7EBF",
244070
+ "runtime-missing": "\u673A\u5668\u4E0A\u6CA1\u6709\u8FD9\u4E2A\u8FD0\u884C\u65F6",
244071
+ faulted: "\u6807\u8BB0\u4E3A\u6545\u969C",
244072
+ "at-capacity": "\u5DF2\u5230\u5E76\u53D1\u4E0A\u9650",
244073
+ "already-tried": "\u672C\u8F6E\u5DF2\u8BD5\u8FC7\u5E76\u5931\u8D25"
244074
+ };
244075
+ }
244076
+ });
244077
+
244078
+ // ../server/src/domains/runtime-allocation/service.ts
244079
+ function isRuntimeClassFailure(reason, errorMessage2) {
244080
+ if (reason === "server-unreachable" || reason === "rate-limit" || reason === "auth-error") return true;
244081
+ if (reason === "error" && errorMessage2?.startsWith("\u8FD9\u4E00\u8F6E\u6CA1\u80FD\u5728\u8282\u70B9\u4E0A\u542F\u52A8\uFF1A")) return true;
244082
+ return false;
244083
+ }
244084
+ function createRuntimeAllocator(ports) {
244085
+ const units = /* @__PURE__ */ new Map();
244086
+ const log3 = ports.log ?? ((line2) => console.log(line2));
244087
+ const sweep = (nowMs) => {
244088
+ for (const [key2, state] of units) if (nowMs - state.updatedAtMs > UNIT_STATE_TTL_MS) units.delete(key2);
244089
+ };
244090
+ const stateOf = (unitOfWork, nowMs) => {
244091
+ sweep(nowMs);
244092
+ const existing = units.get(unitOfWork);
244093
+ if (existing) return existing;
244094
+ const fresh = { tried: /* @__PURE__ */ new Map(), updatedAtMs: nowMs };
244095
+ units.set(unitOfWork, fresh);
244096
+ return fresh;
244097
+ };
244098
+ const flatten = (binding, candidates, primary) => {
244099
+ const out = [];
244100
+ const seen = /* @__PURE__ */ new Set();
244101
+ const head = primary ?? (binding?.nodeId && binding.runtimeKind && binding.status === "active" ? { nodeId: binding.nodeId, runtimeKind: binding.runtimeKind, model: null } : null);
244102
+ if (head) {
244103
+ out.push({ nodeId: head.nodeId, runtimeKind: head.runtimeKind, primary: true, ord: -1, model: head.model ?? null });
244104
+ seen.add(runtimeTargetKey(head));
244105
+ }
244106
+ for (const c of candidates) {
244107
+ const key2 = runtimeTargetKey(c);
244108
+ if (seen.has(key2)) continue;
244109
+ seen.add(key2);
244110
+ out.push({ nodeId: c.nodeId, runtimeKind: c.runtimeKind, primary: false, ord: c.ord, model: c.model ?? null });
244111
+ }
244112
+ return out;
244113
+ };
244114
+ const gather = async (actorId, primary) => {
244115
+ const [binding, candidateRows, nodeStates, faultRows, quotaRows] = await Promise.all([
244116
+ ports.primaryBinding(actorId).catch(() => null),
244117
+ ports.candidates(actorId).catch(() => []),
244118
+ ports.nodeStates(),
244119
+ ports.faults().catch(() => []),
244120
+ ports.quotas().catch(() => [])
244121
+ ]);
244122
+ return {
244123
+ candidates: flatten(binding, candidateRows, primary),
244124
+ nodes: new Map(nodeStates.map((n) => [n.nodeId, n])),
244125
+ faults: new Map(faultRows.map((f2) => [runtimeTargetKey(f2), f2])),
244126
+ quotas: new Map(quotaRows.map((q2) => [runtimeTargetKey(q2), q2]))
244127
+ };
244128
+ };
244129
+ return {
244130
+ /** 跑一次分配但**不抛错**(给需要自己决定怎么呈现失败的调用方,比如要落一条失败卡的那条路)。 */
244131
+ async tryAllocate(input) {
244132
+ const now = input.now ?? Date.now();
244133
+ const state = stateOf(input.unitOfWork, now);
244134
+ const gathered = await gather(input.actorId, input.primary);
244135
+ return allocateRuntime({
244136
+ candidates: gathered.candidates,
244137
+ nodes: gathered.nodes,
244138
+ faults: gathered.faults,
244139
+ quotas: gathered.quotas,
244140
+ alreadyTried: new Set(state.tried.keys()),
244141
+ triedDetail: state.tried
244142
+ }, now);
244143
+ },
244144
+ /**
244145
+ * 分配,选不出就抛 {@link RuntimeAllocationError}(经 `requireAllocation` 那一处——日后接排队改那里)。
244146
+ * 成功时记下「这件活这次派到哪」,供 {@link noteFailure} 回报。
244147
+ */
244148
+ async allocateFor(input) {
244149
+ const now = input.now ?? Date.now();
244150
+ const state = stateOf(input.unitOfWork, now);
244151
+ const actorName = await ports.actorName?.(input.actorId).catch(() => null) ?? null;
244152
+ if (!canRetryAllocation(state.tried.size)) {
244153
+ const tried = [...state.tried.entries()].map(([key2, detail]) => {
244154
+ const [nodeId, runtimeKind] = key2.split("::");
244155
+ return { nodeId, runtimeKind, nodeName: nodeId, cause: "already-tried", detail };
244156
+ });
244157
+ throw new RuntimeAllocationError(
244158
+ "RUNTIME_ALLOCATION_ALL_UNAVAILABLE",
244159
+ `${input.what ? `${input.what}\uFF1A` : ""}${actorName ? `\u300C${actorName}\u300D` : "\u8FD9\u4F4D Agent"}\u7684\u8FD0\u884C\u65F6\u5DF2\u8FDE\u7EED\u6362\u4E86 ${RUNTIME_ALLOCATION_MAX_ATTEMPTS} \u6B21\u90FD\u6CA1\u8DD1\u8D77\u6765\uFF0C\u4E0D\u518D\u81EA\u52A8\u91CD\u8BD5\uFF1A${tried.map((t) => `${t.nodeName} \xB7 ${t.runtimeKind}\uFF08${t.detail}\uFF09`).join("\uFF1B")}\u3002\u8BF7\u5148\u4FEE\u597D\u5176\u4E2D\u4E00\u53F0\u673A\u5668\uFF0C\u6216\u5728\u5458\u5DE5\u8BE6\u60C5\u9875\u52A0\u4E00\u6761\u522B\u7684\u5019\u9009\u8FD0\u884C\u65F6\u540E\u91CD\u65B0\u53D1\u8D77\u3002`,
244160
+ tried
244161
+ );
244162
+ }
244163
+ const result = requireAllocation(
244164
+ await this.tryAllocate({ actorId: input.actorId, unitOfWork: input.unitOfWork, ...input.primary !== void 0 ? { primary: input.primary } : {}, now }),
244165
+ { actorName, ...input.what ? { what: input.what } : {} }
244166
+ );
244167
+ const attempt = state.tried.size + 1;
244168
+ state.lastAssigned = { nodeId: result.chosen.nodeId, runtimeKind: result.chosen.runtimeKind };
244169
+ state.updatedAtMs = now;
244170
+ const explanation = describeAllocationChoice(result);
244171
+ log3(`[runtime-alloc] ${input.unitOfWork} \u7B2C ${attempt} \u6B21\uFF1A${explanation}`);
244172
+ return {
244173
+ binding: { actorId: input.actorId, nodeId: result.chosen.nodeId, runtimeKind: result.chosen.runtimeKind, status: "active" },
244174
+ model: result.chosen.model ?? null,
244175
+ source: result.source,
244176
+ attempt,
244177
+ explanation
244178
+ };
244179
+ },
244180
+ /**
244181
+ * 这件活在上次分给它的那个目标上**因 runtime/节点原因**失败了。下一次 `allocateFor` 会排除它。
244182
+ *
244183
+ * 返回 false = 这条失败不算 runtime/节点的错(agent 自己干砸了),什么都没记。
244184
+ */
244185
+ noteFailure(input) {
244186
+ if (!isRuntimeClassFailure(input.reason, input.errorMessage)) return false;
244187
+ const now = input.now ?? Date.now();
244188
+ const state = units.get(input.unitOfWork);
244189
+ const target = input.target ?? state?.lastAssigned;
244190
+ if (!state || !target) return false;
244191
+ const detail = input.errorMessage?.trim() || String(input.reason ?? "runtime \u5931\u8D25");
244192
+ state.tried.set(runtimeTargetKey(target), detail.slice(0, 200));
244193
+ state.updatedAtMs = now;
244194
+ log3(`[runtime-alloc] ${input.unitOfWork} \u5728 ${target.nodeId} \xB7 ${target.runtimeKind} \u4E0A\u5931\u8D25\uFF08${detail.slice(0, 80)}\uFF09\u2192 \u4E0B\u4E00\u8F6E\u6362\u5019\u9009`);
244195
+ return true;
244196
+ },
244197
+ /**
244198
+ * 这件活这一轮**不是因为 runtime/节点**结束的(跑成了、agent 自己干砸了、被停了)→ 丢掉整条失败链。
244199
+ *
244200
+ * 这是重试上限只对「连着换机器都没跑起来」生效的机制:正常派发一次就清一次,
244201
+ * 所以一个长命的工单节点不会因为派过很多次而撞上上限。
244202
+ */
244203
+ noteSettled(unitOfWork) {
244204
+ units.delete(unitOfWork);
244205
+ },
244206
+ /** 只给测试与诊断用:这件活当前失败链的账。 */
244207
+ inspect(unitOfWork) {
244208
+ const s2 = units.get(unitOfWork);
244209
+ if (!s2) return null;
244210
+ return { failures: s2.tried.size, tried: [...s2.tried.keys()], ...s2.lastAssigned ? { lastAssigned: s2.lastAssigned } : {} };
244211
+ }
244212
+ };
244213
+ }
244214
+ var UNIT_STATE_TTL_MS;
244215
+ var init_service7 = __esm({
244216
+ "../server/src/domains/runtime-allocation/service.ts"() {
244217
+ "use strict";
244218
+ init_src();
244219
+ init_allocate();
244220
+ init_errors6();
244221
+ UNIT_STATE_TTL_MS = 2 * 60 * 6e4;
244222
+ }
244223
+ });
244224
+
243709
244225
  // ../server/src/domains/types/index.ts
243710
244226
  function typesDomain(deps) {
243711
244227
  const adminOf = async (companyId) => {
@@ -243829,6 +244345,10 @@ var init_node_store = __esm({
243829
244345
  MemoryNodeStore = class {
243830
244346
  nodes = /* @__PURE__ */ new Map();
243831
244347
  runtimes = /* @__PURE__ */ new Map();
244348
+ /** 故障标记,键 = `${nodeId}::${runtimeKind}`(与 `runtimeTargetKey` 同一个键形状)。 */
244349
+ faults = /* @__PURE__ */ new Map();
244350
+ /** 落库额度,键同上。 */
244351
+ quotas = /* @__PURE__ */ new Map();
243832
244352
  async upsertNode(node2) {
243833
244353
  this.nodes.set(node2.id, { ...this.nodes.get(node2.id), ...node2 });
243834
244354
  }
@@ -243941,6 +244461,27 @@ var init_node_store = __esm({
243941
244461
  }
243942
244462
  return { nodeDeleted: false };
243943
244463
  }
244464
+ /* ---------- 运行时分配(ADR 0616) ---------- */
244465
+ async setNodeMaxConcurrent(nodeId, limit) {
244466
+ const n = this.nodes.get(nodeId);
244467
+ if (n) n.maxConcurrent = limit;
244468
+ }
244469
+ async upsertRuntimeFault(fault) {
244470
+ this.faults.set(`${fault.nodeId}::${fault.runtimeKind}`, { ...fault });
244471
+ }
244472
+ async clearRuntimeFault(nodeId, runtimeKind) {
244473
+ return this.faults.delete(`${nodeId}::${runtimeKind}`);
244474
+ }
244475
+ async listRuntimeFaults(filter) {
244476
+ return [...this.faults.values()].filter((f2) => filter?.nodeId ? f2.nodeId === filter.nodeId : true).filter((f2) => filter?.companyId ? this.nodes.get(f2.nodeId)?.companyId === filter.companyId : true).map((f2) => ({ ...f2 }));
244477
+ }
244478
+ async saveRuntimeQuotas(nodeId, quotas) {
244479
+ for (const key2 of [...this.quotas.keys()]) if (key2.startsWith(`${nodeId}::`)) this.quotas.delete(key2);
244480
+ for (const q2 of quotas) this.quotas.set(`${q2.nodeId}::${q2.runtimeKind}`, { ...q2 });
244481
+ }
244482
+ async listRuntimeQuotas(filter) {
244483
+ return [...this.quotas.values()].filter((q2) => filter?.nodeId ? q2.nodeId === filter.nodeId : true).filter((q2) => filter?.companyId ? this.nodes.get(q2.nodeId)?.companyId === filter.companyId : true).map((q2) => ({ ...q2 }));
244484
+ }
243944
244485
  };
243945
244486
  FileNodeStore = class _FileNodeStore extends MemoryNodeStore {
243946
244487
  constructor(file) {
@@ -243953,15 +244494,19 @@ var init_node_store = __esm({
243953
244494
  const snap = JSON.parse(fs26.readFileSync(file, "utf8"));
243954
244495
  for (const n of snap.nodes ?? []) s2.nodes.set(n.id, n);
243955
244496
  for (const r of snap.runtimes ?? []) s2.runtimes.set(r.id, r);
244497
+ for (const f2 of snap.faults ?? []) s2.faults.set(`${f2.nodeId}::${f2.runtimeKind}`, f2);
244498
+ for (const q2 of snap.quotas ?? []) s2.quotas.set(`${q2.nodeId}::${q2.runtimeKind}`, q2);
243956
244499
  } catch {
243957
- fs26.writeFileSync(file, JSON.stringify({ nodes: [], runtimes: [] }, null, 2));
244500
+ fs26.writeFileSync(file, JSON.stringify({ nodes: [], runtimes: [], faults: [], quotas: [] }, null, 2));
243958
244501
  }
243959
244502
  return s2;
243960
244503
  }
243961
244504
  flush() {
243962
244505
  fs26.writeFileSync(this.file, JSON.stringify({
243963
244506
  nodes: [...this.nodes.values()],
243964
- runtimes: [...this.runtimes.values()]
244507
+ runtimes: [...this.runtimes.values()],
244508
+ faults: [...this.faults.values()],
244509
+ quotas: [...this.quotas.values()]
243965
244510
  }, null, 2));
243966
244511
  }
243967
244512
  async upsertNode(node2) {
@@ -244025,6 +244570,25 @@ var init_node_store = __esm({
244025
244570
  this.flush();
244026
244571
  return r;
244027
244572
  }
244573
+ // ⚠ 同上:并发上限、故障标记、额度三类写入漏了 override 就只活在内存,重启回到「没上限、没故障、
244574
+ // 没额度数据」——而那三件事恰恰都是「重启不能丢」才有意义。
244575
+ async setNodeMaxConcurrent(nodeId, limit) {
244576
+ await super.setNodeMaxConcurrent(nodeId, limit);
244577
+ this.flush();
244578
+ }
244579
+ async upsertRuntimeFault(fault) {
244580
+ await super.upsertRuntimeFault(fault);
244581
+ this.flush();
244582
+ }
244583
+ async clearRuntimeFault(nodeId, runtimeKind) {
244584
+ const r = await super.clearRuntimeFault(nodeId, runtimeKind);
244585
+ if (r) this.flush();
244586
+ return r;
244587
+ }
244588
+ async saveRuntimeQuotas(nodeId, quotas) {
244589
+ await super.saveRuntimeQuotas(nodeId, quotas);
244590
+ this.flush();
244591
+ }
244028
244592
  };
244029
244593
  }
244030
244594
  });
@@ -245298,7 +245862,7 @@ function compactModelChunks(rows) {
245298
245862
  return out;
245299
245863
  }
245300
245864
  var EVENT_PAGE_LIMIT, MODEL_CHUNK_COMPACT_WINDOW_MS, MODEL_CHUNK_RAW_MULTIPLIER, TraceService;
245301
- var init_service7 = __esm({
245865
+ var init_service8 = __esm({
245302
245866
  "../server/src/domains/trace/service.ts"() {
245303
245867
  "use strict";
245304
245868
  init_sink();
@@ -246074,9 +246638,9 @@ function createTraceDomain(opts) {
246074
246638
  var init_trace2 = __esm({
246075
246639
  "../server/src/domains/trace/index.ts"() {
246076
246640
  "use strict";
246077
- init_service7();
246641
+ init_service8();
246078
246642
  init_routes9();
246079
- init_service7();
246643
+ init_service8();
246080
246644
  init_routes9();
246081
246645
  init_read_scope();
246082
246646
  init_sink();
@@ -246609,7 +247173,7 @@ function stateFromAttempt(attempt, handoff, checkpoint, now, stalledAfterMs, ope
246609
247173
  return "failed";
246610
247174
  }
246611
247175
  var import_node_crypto64, BLOCKING_STATES, HANDOFF_OUTCOMES2, RESUME_EXTERNAL_EFFECT_LIMIT, RESUME_SNAPSHOT_PREFIX, RESUME_SNAPSHOT_SUFFIX, ExecutionContinuityService;
246612
- var init_service8 = __esm({
247176
+ var init_service9 = __esm({
246613
247177
  "../server/src/domains/execution-continuity/service.ts"() {
246614
247178
  "use strict";
246615
247179
  init_src();
@@ -247429,8 +247993,8 @@ var init_execution_continuity2 = __esm({
247429
247993
  "../server/src/domains/execution-continuity/index.ts"() {
247430
247994
  "use strict";
247431
247995
  init_routes10();
247432
- init_service8();
247433
- init_service8();
247996
+ init_service9();
247997
+ init_service9();
247434
247998
  init_routes10();
247435
247999
  init_handoff_work_bridge();
247436
248000
  init_upstream_handoff_input();
@@ -250517,7 +251081,7 @@ function appendSourceNote(brief, automation, source, atIso) {
250517
251081
  \u672C\u5DE5\u5355\u7531\u81EA\u52A8\u5316\u300C${automation.name}\u300D\u4E8E ${atIso} \u89E6\u53D1\uFF08\u6765\u6E90\uFF1A${source}\uFF09\u3002\u5F00\u5DE5\u540E\u8BF7\u628A\u5DE5\u5355\u6807\u9898\u6539\u6210\u51C6\u786E\u53CD\u6620\u5B9E\u9645\u5DE5\u4F5C\u7684\u63CF\u8FF0\u3002`;
250518
251082
  }
250519
251083
  var import_node_crypto66, DATE_TOKEN, AutomationsService, EVENT_RETRY_BASE_MS, EVENT_RETRY_MAX_MS, EVENT_RETRY_MAX_ATTEMPTS, eventAttempts, eventLastAttemptAt, eventLastError, CHAT_REPLY_MAX;
250520
- var init_service9 = __esm({
251084
+ var init_service10 = __esm({
250521
251085
  "../server/src/domains/automations/service.ts"() {
250522
251086
  "use strict";
250523
251087
  import_node_crypto66 = require("node:crypto");
@@ -252252,7 +252816,7 @@ var init_routes11 = __esm({
252252
252816
  init_src();
252253
252817
  init_templates();
252254
252818
  init_router();
252255
- init_service9();
252819
+ init_service10();
252256
252820
  init_projection2();
252257
252821
  init_scheduler();
252258
252822
  init_registry3();
@@ -252367,10 +252931,10 @@ function createAutomationsDomain(opts) {
252367
252931
  var init_automations = __esm({
252368
252932
  "../server/src/domains/automations/index.ts"() {
252369
252933
  "use strict";
252370
- init_service9();
252934
+ init_service10();
252371
252935
  init_webhook();
252372
252936
  init_routes11();
252373
- init_service9();
252937
+ init_service10();
252374
252938
  init_routes11();
252375
252939
  init_projection2();
252376
252940
  init_webhook();
@@ -253186,6 +253750,32 @@ var init_daemon_adapter = __esm({
253186
253750
  }
253187
253751
  }
253188
253752
  }
253753
+ /**
253754
+ * Bug 0232:**就地**收尾一次派发——用户点了停止、而 kill 帧送不出去(节点 socket 非 OPEN)时用。
253755
+ *
253756
+ * 走的是和「服务端自判死」完全同一个 {@link settle},所以三件必须做的事一次做齐:
253757
+ * 兑现在途的插话等待者、触发 exitCbs(本轮收口、释放 `chat_session_turns` 的槽、输入框解冻)、
253758
+ * 把 dispatchId 记进 `settledDispatchIds`。
253759
+ *
253760
+ * 为什么这样不会造出两个 agent 一起跑(防双跑的三道防线都是现成的):
253761
+ * 1. 节点上的会话进程自己有看门狗——连不上服务端超过 `SESSION_LIMITS.suicideAfterUnreachableMs`
253762
+ * (5 分钟)就自尽(`packages/cli/src/daemon/session-process.ts`);
253763
+ * 2. 契约上钉着不变式 `suicideAfterUnreachableMs ≤ reapGraceMs`,服务端判死必然发生在它自尽之后;
253764
+ * 3. 它要是在这之前连回来了,hub 的三级认领(`daemon-hub.ts` 的 `judgeSession`)第一级就命中
253765
+ * `settledDispatchIds` → 判 `settled` → 下发 `evict` → 会话 `selfTerminate("evicted")`。
253766
+ * 所以僵尸的存活时间是 `min(网络恢复时间, 5 分钟)`,这个窗口自己会关。
253767
+ *
253768
+ * 归因不在这里写:`cancelCause` 在点停止那一刻就由 `LiveChatRegistry.stop` 写成了 `user`,
253769
+ * 收尾时读出来,页面照旧显示「用户停止了输出」。
253770
+ *
253771
+ * @returns true = 确由这次调用收的尾;false = 这次派发不在在途表里(已收尾 / 不归本 adapter 管)。
253772
+ */
253773
+ cancelLocally(dispatchId, errorMessage2 = "\u7528\u6237\u505C\u6B62\uFF08\u8282\u70B9\u79BB\u7EBF\uFF0C\u672A\u7B49\u5230\u9000\u51FA\u5E27\uFF09") {
253774
+ const entry = this.pending.get(dispatchId);
253775
+ if (!entry || entry.exited) return false;
253776
+ this.settle(dispatchId, { code: null, reason: "cancelled", errorMessage: errorMessage2 });
253777
+ return true;
253778
+ }
253189
253779
  /** 当前在途(未退出)会话数;传 runtimeKind 时收窄到单个 runtime 实例。 */
253190
253780
  activeRunCount(nodeId, runtimeKind) {
253191
253781
  let n = 0;
@@ -253435,14 +254025,18 @@ var init_daemon_adapter = __esm({
253435
254025
  const appendTimeoutMs = this.appendTimeoutMs;
253436
254026
  const sessionReadyWaitMs = this.sessionReadyWaitMs;
253437
254027
  const confirm = (key2) => this.confirmInputReceipt(nodeId, dispatchId, key2);
254028
+ const sendKill = () => routeToSession(hub, nodeId, dispatchId, { type: "kill", dispatchId });
254029
+ const cancelLocally = () => this.cancelLocally(dispatchId);
253438
254030
  return {
253439
254031
  id: dispatchId,
253440
254032
  get supportsLiveProtocol() {
253441
254033
  return entry.supportsLiveProtocol;
253442
254034
  },
253443
254035
  async kill() {
253444
- routeToSession(hub, nodeId, dispatchId, { type: "kill", dispatchId });
254036
+ sendKill();
253445
254037
  },
254038
+ sendKill,
254039
+ cancelLocally,
253446
254040
  onExit(cb) {
253447
254041
  if (entry.exited) cb(entry.exited);
253448
254042
  else entry.exitCbs.push(cb);
@@ -253527,6 +254121,8 @@ var init_daemon_adapter = __esm({
253527
254121
  const appendTimeoutMs = this.appendTimeoutMs;
253528
254122
  const sessionReadyWaitMs = this.sessionReadyWaitMs;
253529
254123
  const confirm = (key2) => this.confirmInputReceipt(nodeId, dispatchId, key2);
254124
+ const sendKill = () => routeToSession(hub, nodeId, dispatchId, { type: "kill", dispatchId });
254125
+ const cancelLocally = () => this.cancelLocally(dispatchId);
253530
254126
  return {
253531
254127
  id: dispatchId,
253532
254128
  stashDroppedFrames: stashed.droppedFrames ?? 0,
@@ -253534,8 +254130,10 @@ var init_daemon_adapter = __esm({
253534
254130
  return entry.supportsLiveProtocol;
253535
254131
  },
253536
254132
  async kill() {
253537
- routeToSession(hub, nodeId, dispatchId, { type: "kill", dispatchId });
254133
+ sendKill();
253538
254134
  },
254135
+ sendKill,
254136
+ cancelLocally,
253539
254137
  onExit(cb) {
253540
254138
  if (entry.exited) cb(entry.exited);
253541
254139
  else entry.exitCbs.push(cb);
@@ -255705,6 +256303,18 @@ var init_postgres_registry = __esm({
255705
256303
  status text NOT NULL -- active | paused
255706
256304
  )`);
255707
256305
  await pool.query(`
256306
+ CREATE TABLE IF NOT EXISTS "${s2}".actor_runtime_candidates (
256307
+ actor_id text NOT NULL,
256308
+ node_id text NOT NULL,
256309
+ runtime_kind text NOT NULL,
256310
+ model text,
256311
+ ord integer NOT NULL,
256312
+ updated_at timestamptz NOT NULL,
256313
+ updated_by text NOT NULL,
256314
+ PRIMARY KEY (actor_id, node_id, runtime_kind)
256315
+ )`);
256316
+ await pool.query(`CREATE INDEX IF NOT EXISTS actor_runtime_candidates_actor_ord_idx ON "${s2}".actor_runtime_candidates (actor_id, ord)`);
256317
+ await pool.query(`
255708
256318
  CREATE TABLE IF NOT EXISTS "${s2}".connectors (
255709
256319
  id text PRIMARY KEY,
255710
256320
  name text NOT NULL,
@@ -256062,6 +256672,49 @@ var init_postgres_registry = __esm({
256062
256672
  const r = await this.pool.query(`SELECT * FROM ${this.s}.actor_bindings ORDER BY actor_id`);
256063
256673
  return r.rows.map((row) => ({ actorId: row.actor_id, nodeId: row.node_id, runtimeKind: row.runtime_kind, status: row.status }));
256064
256674
  }
256675
+ /* ---------- 候选 runtime(ADR 0616) ---------- */
256676
+ async listRuntimeCandidates(actorId) {
256677
+ const r = await this.pool.query(
256678
+ // 第二、三排序键让同 ord 的存量脏数据也有**确定**次序——分配算法的定序是全序的,
256679
+ // 读侧若随机返回,那句「为什么选了它」下一次就解释不了同一件事。
256680
+ `SELECT actor_id, node_id, runtime_kind, model, ord FROM ${this.s}.actor_runtime_candidates
256681
+ WHERE actor_id = $1 ORDER BY ord, node_id, runtime_kind`,
256682
+ [actorId]
256683
+ );
256684
+ return r.rows.map((row) => ({
256685
+ actorId: row.actor_id,
256686
+ nodeId: row.node_id,
256687
+ runtimeKind: row.runtime_kind,
256688
+ model: row.model ?? null,
256689
+ ord: Number(row.ord)
256690
+ }));
256691
+ }
256692
+ async replaceRuntimeCandidates(actorId, candidates, by) {
256693
+ const now = (/* @__PURE__ */ new Date()).toISOString();
256694
+ const updatedBy = by ?? "actor:system:registry";
256695
+ const client = await this.pool.connect();
256696
+ try {
256697
+ await client.query("BEGIN");
256698
+ await client.query(`DELETE FROM ${this.s}.actor_runtime_candidates WHERE actor_id = $1`, [actorId]);
256699
+ for (const [i, c] of candidates.entries()) {
256700
+ const model = typeof c.model === "string" && c.model.trim() ? c.model.trim() : null;
256701
+ await client.query(
256702
+ `INSERT INTO ${this.s}.actor_runtime_candidates (actor_id, node_id, runtime_kind, model, ord, updated_at, updated_by)
256703
+ VALUES ($1,$2,$3,$4,$5,$6,$7)
256704
+ ON CONFLICT (actor_id, node_id, runtime_kind) DO UPDATE SET model=$4, ord=$5, updated_at=$6, updated_by=$7`,
256705
+ [actorId, c.nodeId, c.runtimeKind, model, i, now, updatedBy]
256706
+ );
256707
+ }
256708
+ await client.query("COMMIT");
256709
+ } catch (err) {
256710
+ await client.query("ROLLBACK").catch(() => {
256711
+ });
256712
+ throw err;
256713
+ } finally {
256714
+ client.release();
256715
+ }
256716
+ return this.listRuntimeCandidates(actorId);
256717
+ }
256065
256718
  /* ---------- Runtime 默认配置 ---------- */
256066
256719
  async upsertRuntimeConfig(c) {
256067
256720
  await this.pool.query(
@@ -265205,7 +265858,7 @@ var init_postgres_delegations = __esm({
265205
265858
  });
265206
265859
 
265207
265860
  // ../storage/src/postgres-nodes.ts
265208
- var import_node_crypto75, ident13, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToRuntime;
265861
+ var import_node_crypto75, ident13, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToFault, rowToQuota, rowToRuntime;
265209
265862
  var init_postgres_nodes = __esm({
265210
265863
  "../storage/src/postgres-nodes.ts"() {
265211
265864
  "use strict";
@@ -265255,6 +265908,33 @@ var init_postgres_nodes = __esm({
265255
265908
  await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS node_runtimes_company_id_uq ON "${s2}".node_runtimes (company_id, id)`);
265256
265909
  await pool.query(`CREATE INDEX IF NOT EXISTS node_runtimes_company_node_idx ON "${s2}".node_runtimes (company_id, node_id)`);
265257
265910
  await pool.query(`CREATE INDEX IF NOT EXISTS node_runtimes_company_status_idx ON "${s2}".node_runtimes (company_id, status)`);
265911
+ await pool.query(`ALTER TABLE "${s2}".nodes ADD COLUMN IF NOT EXISTS max_concurrent integer`);
265912
+ await pool.query(`
265913
+ CREATE TABLE IF NOT EXISTS "${s2}".node_runtime_faults (
265914
+ node_id text NOT NULL REFERENCES "${s2}".nodes(id) ON DELETE CASCADE,
265915
+ runtime_kind text NOT NULL,
265916
+ company_id text,
265917
+ reason text NOT NULL,
265918
+ detail text,
265919
+ marked_at timestamptz NOT NULL,
265920
+ attempts integer NOT NULL DEFAULT 0,
265921
+ next_probe_at timestamptz,
265922
+ awaiting_manual boolean NOT NULL DEFAULT false,
265923
+ PRIMARY KEY (node_id, runtime_kind)
265924
+ )`);
265925
+ await pool.query(`CREATE INDEX IF NOT EXISTS node_runtime_faults_company_idx ON "${s2}".node_runtime_faults (company_id)`);
265926
+ await pool.query(`
265927
+ CREATE TABLE IF NOT EXISTS "${s2}".node_runtime_quotas (
265928
+ node_id text NOT NULL REFERENCES "${s2}".nodes(id) ON DELETE CASCADE,
265929
+ runtime_kind text NOT NULL,
265930
+ company_id text,
265931
+ status text NOT NULL,
265932
+ checked_at timestamptz NOT NULL,
265933
+ retry_at timestamptz,
265934
+ windows jsonb NOT NULL DEFAULT '[]'::jsonb,
265935
+ PRIMARY KEY (node_id, runtime_kind)
265936
+ )`);
265937
+ await pool.query(`CREATE INDEX IF NOT EXISTS node_runtime_quotas_company_idx ON "${s2}".node_runtime_quotas (company_id)`);
265258
265938
  return new _PostgresNodeStore(pool, schema);
265259
265939
  }
265260
265940
  async upsertNode(n) {
@@ -265408,6 +266088,95 @@ var init_postgres_nodes = __esm({
265408
266088
  }
265409
266089
  return { nodeDeleted: false };
265410
266090
  }
266091
+ /* ---------- 运行时分配(ADR 0616):并发上限 / 故障标记 / 落库额度 ---------- */
266092
+ async setNodeMaxConcurrent(nodeId, limit) {
266093
+ await this.pool.query(`UPDATE ${this.s}.nodes SET max_concurrent=$2 WHERE id=$1`, [nodeId, limit]);
266094
+ }
266095
+ async upsertRuntimeFault(fault) {
266096
+ await this.pool.query(
266097
+ // company_id 从 node 行继承(node 是唯一事实源,同 upsertRuntime 的兜底),任何调用方都绕不过去。
266098
+ `INSERT INTO ${this.s}.node_runtime_faults
266099
+ (node_id, runtime_kind, company_id, reason, detail, marked_at, attempts, next_probe_at, awaiting_manual)
266100
+ VALUES ($1,$2,(SELECT company_id FROM ${this.s}.nodes WHERE id=$1),$3,$4,$5,$6,$7,$8)
266101
+ ON CONFLICT (node_id, runtime_kind) DO UPDATE SET
266102
+ company_id = COALESCE(EXCLUDED.company_id, ${this.s}.node_runtime_faults.company_id),
266103
+ reason=EXCLUDED.reason, detail=EXCLUDED.detail, marked_at=EXCLUDED.marked_at,
266104
+ attempts=EXCLUDED.attempts, next_probe_at=EXCLUDED.next_probe_at, awaiting_manual=EXCLUDED.awaiting_manual`,
266105
+ [
266106
+ fault.nodeId,
266107
+ fault.runtimeKind,
266108
+ fault.reason,
266109
+ fault.detail ?? null,
266110
+ fault.markedAt,
266111
+ fault.attempts,
266112
+ fault.nextProbeAt ?? null,
266113
+ fault.awaitingManual
266114
+ ]
266115
+ );
266116
+ }
266117
+ async clearRuntimeFault(nodeId, runtimeKind) {
266118
+ const r = await this.pool.query(
266119
+ `DELETE FROM ${this.s}.node_runtime_faults WHERE node_id=$1 AND runtime_kind=$2`,
266120
+ [nodeId, runtimeKind]
266121
+ );
266122
+ return (r.rowCount ?? 0) > 0;
266123
+ }
266124
+ async listRuntimeFaults(filter) {
266125
+ const where = [];
266126
+ const args = [];
266127
+ if (filter?.nodeId) {
266128
+ args.push(filter.nodeId);
266129
+ where.push(`node_id = $${args.length}`);
266130
+ }
266131
+ if (filter?.companyId) {
266132
+ args.push(filter.companyId);
266133
+ where.push(`company_id = $${args.length}`);
266134
+ }
266135
+ const r = await this.pool.query(
266136
+ `SELECT * FROM ${this.s}.node_runtime_faults${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY node_id, runtime_kind`,
266137
+ args
266138
+ );
266139
+ return r.rows.map(rowToFault);
266140
+ }
266141
+ async saveRuntimeQuotas(nodeId, quotas) {
266142
+ const client = await this.pool.connect();
266143
+ try {
266144
+ await client.query("BEGIN");
266145
+ await client.query(`DELETE FROM ${this.s}.node_runtime_quotas WHERE node_id=$1`, [nodeId]);
266146
+ for (const q2 of quotas) {
266147
+ await client.query(
266148
+ `INSERT INTO ${this.s}.node_runtime_quotas
266149
+ (node_id, runtime_kind, company_id, status, checked_at, retry_at, windows)
266150
+ VALUES ($1,$2,(SELECT company_id FROM ${this.s}.nodes WHERE id=$1),$3,$4,$5,$6::jsonb)`,
266151
+ [nodeId, q2.runtimeKind, q2.status, q2.checkedAt, q2.retryAt ?? null, JSON.stringify(q2.windows ?? [])]
266152
+ );
266153
+ }
266154
+ await client.query("COMMIT");
266155
+ } catch (err) {
266156
+ await client.query("ROLLBACK").catch(() => {
266157
+ });
266158
+ throw err;
266159
+ } finally {
266160
+ client.release();
266161
+ }
266162
+ }
266163
+ async listRuntimeQuotas(filter) {
266164
+ const where = [];
266165
+ const args = [];
266166
+ if (filter?.nodeId) {
266167
+ args.push(filter.nodeId);
266168
+ where.push(`node_id = $${args.length}`);
266169
+ }
266170
+ if (filter?.companyId) {
266171
+ args.push(filter.companyId);
266172
+ where.push(`company_id = $${args.length}`);
266173
+ }
266174
+ const r = await this.pool.query(
266175
+ `SELECT * FROM ${this.s}.node_runtime_quotas${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY node_id, runtime_kind`,
266176
+ args
266177
+ );
266178
+ return r.rows.map(rowToQuota);
266179
+ }
265411
266180
  };
265412
266181
  PostgresNodeTokenStore = class _PostgresNodeTokenStore {
265413
266182
  constructor(pool, schema) {
@@ -265450,7 +266219,28 @@ var init_postgres_nodes = __esm({
265450
266219
  nodeVersion: row.node_version,
265451
266220
  status: row.status,
265452
266221
  lastSeenAt: new Date(row.last_seen_at).toISOString(),
265453
- enrolledAt: new Date(row.enrolled_at).toISOString()
266222
+ enrolledAt: new Date(row.enrolled_at).toISOString(),
266223
+ // 只在库里真有值时带上这一格:`undefined` 与 `null` 在 `exactOptionalPropertyTypes` 下不等价,
266224
+ // 而判据只认「有没有上限」——带一个 null 进去会让下游不得不再判一次。
266225
+ ...row.max_concurrent != null ? { maxConcurrent: Number(row.max_concurrent) } : {}
266226
+ });
266227
+ rowToFault = (row) => ({
266228
+ nodeId: row.node_id,
266229
+ runtimeKind: row.runtime_kind,
266230
+ reason: row.reason,
266231
+ ...row.detail ? { detail: row.detail } : {},
266232
+ markedAt: new Date(row.marked_at).toISOString(),
266233
+ attempts: Number(row.attempts ?? 0),
266234
+ ...row.next_probe_at ? { nextProbeAt: new Date(row.next_probe_at).toISOString() } : {},
266235
+ awaitingManual: Boolean(row.awaiting_manual)
266236
+ });
266237
+ rowToQuota = (row) => ({
266238
+ nodeId: row.node_id,
266239
+ runtimeKind: row.runtime_kind,
266240
+ status: row.status,
266241
+ checkedAt: new Date(row.checked_at).toISOString(),
266242
+ ...row.retry_at ? { retryAt: new Date(row.retry_at).toISOString() } : {},
266243
+ windows: Array.isArray(row.windows) ? row.windows : []
265454
266244
  });
265455
266245
  rowToRuntime = (row) => ({
265456
266246
  id: row.id,
@@ -268652,6 +269442,16 @@ var init_cutover_aware_registry = __esm({
268652
269442
  async listBindings() {
268653
269443
  return (await this.orgStore()).listBindings();
268654
269444
  }
269445
+ // 候选 runtime(ADR 0616):与 binding 同一侧(组织面);底座没实现就退化成「没有候选」。
269446
+ async listRuntimeCandidates(actorId) {
269447
+ const store = await this.orgStore();
269448
+ return store.listRuntimeCandidates ? store.listRuntimeCandidates(actorId) : [];
269449
+ }
269450
+ async replaceRuntimeCandidates(actorId, candidates) {
269451
+ const store = await this.orgStoreForWrite();
269452
+ if (!store.replaceRuntimeCandidates) throw new Error("\u5F53\u524D\u767B\u8BB0\u5B58\u50A8\u5B9E\u73B0\u4E0D\u652F\u6301\u5019\u9009\u8FD0\u884C\u65F6\uFF08replaceRuntimeCandidates \u672A\u5B9E\u73B0\uFF09");
269453
+ return store.replaceRuntimeCandidates(actorId, candidates);
269454
+ }
268655
269455
  async upsertRuntimeConfig(...args) {
268656
269456
  return (await this.orgStoreForWrite()).upsertRuntimeConfig(...args);
268657
269457
  }
@@ -274375,6 +275175,7 @@ __export(src_exports2, {
274375
275175
  MemoryWriteConflictError: () => MemoryWriteConflictError,
274376
275176
  MirrorEngine: () => MirrorEngine,
274377
275177
  MissingCompanyContextError: () => MissingCompanyContextError,
275178
+ NODE_HEARTBEAT_STALE_MS: () => NODE_HEARTBEAT_STALE_MS,
274378
275179
  NPM_PROBE_TIMEOUT_MS: () => NPM_PROBE_TIMEOUT_MS,
274379
275180
  NdjsonOplogStore: () => NdjsonOplogStore,
274380
275181
  NodeHealthTracker: () => NodeHealthTracker,
@@ -274404,6 +275205,7 @@ __export(src_exports2, {
274404
275205
  RolesAdminService: () => RolesAdminService,
274405
275206
  Router: () => Router,
274406
275207
  RunSettlementFunnel: () => RunSettlementFunnel,
275208
+ RuntimeAllocationError: () => RuntimeAllocationError,
274407
275209
  RuntimeStartMarker: () => RuntimeStartMarker,
274408
275210
  SERVER_PING: () => SERVER_PING,
274409
275211
  SHARED_GRAPH_BODY: () => SHARED_GRAPH_BODY,
@@ -274427,6 +275229,7 @@ __export(src_exports2, {
274427
275229
  actorStats: () => actorStats,
274428
275230
  actorsDomain: () => actorsDomain,
274429
275231
  agentDefaultRuntimeForNewSession: () => agentDefaultRuntimeForNewSession,
275232
+ allocateRuntime: () => allocateRuntime,
274430
275233
  answerFallbackReason: () => answerFallbackReason,
274431
275234
  appendChatMessageOnce: () => appendChatMessageOnce,
274432
275235
  applyBuiltinSkillVariants: () => applyBuiltinSkillVariants,
@@ -274437,6 +275240,7 @@ __export(src_exports2, {
274437
275240
  assertCreatableVariable: () => assertCreatableVariable,
274438
275241
  assistantRowRecovery: () => assistantRowRecovery,
274439
275242
  assistantsDomain: () => assistantsDomain,
275243
+ atCapacity: () => atCapacity,
274440
275244
  automationsDomain: () => automationsDomain,
274441
275245
  backfillOrgModel: () => backfillOrgModel,
274442
275246
  backfillOrgModelOnStartup: () => backfillOrgModelOnStartup,
@@ -274469,6 +275273,7 @@ __export(src_exports2, {
274469
275273
  buildWorkorderSummaries: () => buildWorkorderSummaries,
274470
275274
  builtinSkillToFiles: () => builtinSkillToFiles,
274471
275275
  canAnnounceRuntimeSwitch: () => canAnnounceRuntimeSwitch,
275276
+ canRetryAllocation: () => canRetryAllocation,
274472
275277
  chatBroadcastEnabledFromEnv: () => chatBroadcastEnabledFromEnv,
274473
275278
  chatDelegationEnabledFromEnv: () => chatDelegationEnabledFromEnv,
274474
275279
  chatDispatchScope: () => chatDispatchScope,
@@ -274536,6 +275341,7 @@ __export(src_exports2, {
274536
275341
  createRegistryAuditSink: () => createRegistryAuditSink,
274537
275342
  createReworkHandoffInputResolver: () => createReworkHandoffInputResolver,
274538
275343
  createRunSettlementWiring: () => createRunSettlementWiring,
275344
+ createRuntimeAllocator: () => createRuntimeAllocator,
274539
275345
  createSkillMarketClientFromEnv: () => createSkillMarketClientFromEnv,
274540
275346
  createSpawnMutex: () => createSpawnMutex,
274541
275347
  createTokenIssuer: () => createTokenIssuer,
@@ -274560,8 +275366,10 @@ __export(src_exports2, {
274560
275366
  deliverDelegationReturn: () => deliverDelegationReturn,
274561
275367
  deliverWorkorderBroadcast: () => deliverWorkorderBroadcast,
274562
275368
  deriveNodeHealth: () => deriveNodeHealth,
275369
+ describeAllocationChoice: () => describeAllocationChoice,
274563
275370
  describeAttachmentsForAppend: () => describeAttachmentsForAppend,
274564
275371
  describeNodeFault: () => describeNodeFault,
275372
+ describeRejections: () => describeRejections,
274565
275373
  deterministicKnowledgePlan: () => deterministicKnowledgePlan,
274566
275374
  deterministicUuid: () => deterministicUuid,
274567
275375
  digestOf: () => digestOf2,
@@ -274588,6 +275396,8 @@ __export(src_exports2, {
274588
275396
  fallbackTurnId: () => fallbackTurnId,
274589
275397
  fastPathFallbackReason: () => fastPathFallbackReason,
274590
275398
  fastPathQualityReason: () => fastPathQualityReason,
275399
+ faultFromQuota: () => faultFromQuota,
275400
+ faultIsBlocking: () => faultIsBlocking,
274591
275401
  fenceDispatch: () => fenceDispatch,
274592
275402
  fetchLatestNpmVersion: () => fetchLatestNpmVersion,
274593
275403
  fetchSkillFromSource: () => fetchSkillFromSource,
@@ -274603,6 +275413,7 @@ __export(src_exports2, {
274603
275413
  hashPassword: () => hashPassword,
274604
275414
  hideDeleted: () => hideDeleted,
274605
275415
  humanSubmitSourceRunId: () => humanSubmitSourceRunId,
275416
+ idlenessOf: () => idlenessOf,
274606
275417
  isActiveCoordinatorAgent: () => isActiveCoordinatorAgent,
274607
275418
  isCompanyCrossTenantPath: () => isCompanyCrossTenantPath,
274608
275419
  isCoordinatorAgentName: () => isCoordinatorAgentName,
@@ -274621,6 +275432,7 @@ __export(src_exports2, {
274621
275432
  isNodeHealthy: () => isNodeHealthy,
274622
275433
  isOperatorRequest: () => isOperatorRequest,
274623
275434
  isRunTerminalStatus: () => isRunTerminalStatus,
275435
+ isRuntimeClassFailure: () => isRuntimeClassFailure,
274624
275436
  isRuntimeDrifted: () => isRuntimeDrifted,
274625
275437
  isRuntimeRunReader: () => isRuntimeRunReader,
274626
275438
  isSealedKnowledgeSnapshotContent: () => isSealedKnowledgeSnapshotContent,
@@ -274717,6 +275529,7 @@ __export(src_exports2, {
274717
275529
  providerKnowledgeHash: () => providerKnowledgeHash,
274718
275530
  providerRoundTripKnowledgeHash: () => providerRoundTripKnowledgeHash,
274719
275531
  publishMarker: () => publishMarker,
275532
+ quotaDerateFactor: () => quotaDerateFactor,
274720
275533
  readCredentialAuditLog: () => readCredentialAuditLog,
274721
275534
  readLocalPack: () => readLocalPack,
274722
275535
  readRegisteredToolVersions: () => readRegisteredToolVersions,
@@ -274754,6 +275567,7 @@ __export(src_exports2, {
274754
275567
  repairSubmissionDelivery: () => repairSubmissionDelivery,
274755
275568
  repairSubmissionDeliveryAcrossCompanies: () => repairSubmissionDeliveryAcrossCompanies,
274756
275569
  replaceModelPriceCacheForTest: () => replaceModelPriceCacheForTest,
275570
+ requireAllocation: () => requireAllocation,
274757
275571
  requiresKnowledgePlanner: () => requiresKnowledgePlanner,
274758
275572
  resendSender: () => resendSender,
274759
275573
  resetBuildInfoCacheForTest: () => resetBuildInfoCacheForTest,
@@ -274777,6 +275591,7 @@ __export(src_exports2, {
274777
275591
  runtimeConnectionStatus: () => runtimeConnectionStatus,
274778
275592
  runtimeDriftBlock: () => runtimeDriftBlock,
274779
275593
  runtimeSwitchedNotice: () => runtimeSwitchedNotice,
275594
+ runtimeTargetKey: () => runtimeTargetKey,
274780
275595
  safeAttachmentName: () => safeAttachmentName,
274781
275596
  sanitizeSkillDir: () => sanitizeSkillDir,
274782
275597
  scanFrontmatterEntries: () => scanFrontmatterEntries,
@@ -274912,6 +275727,9 @@ var init_src11 = __esm({
274912
275727
  init_coordinator_runs();
274913
275728
  init_usage_read();
274914
275729
  init_runtime_drift();
275730
+ init_allocate();
275731
+ init_errors6();
275732
+ init_service7();
274915
275733
  init_runtime_status();
274916
275734
  init_runtime_tenancy();
274917
275735
  init_types4();
@@ -275287,6 +276105,7 @@ var import_node_crypto91 = require("node:crypto");
275287
276105
 
275288
276106
  // ../cli/src/serve.ts
275289
276107
  init_src11();
276108
+ init_src11();
275290
276109
  init_chat_cancel_attribution();
275291
276110
  init_src();
275292
276111
  init_src11();
@@ -280271,6 +281090,59 @@ async function startServe(opts) {
280271
281090
  return (await ctx.service.latestConfig(actorId))?.model ?? null;
280272
281091
  };
280273
281092
  agentDefaultRuntimeResolvers = { resolveBinding: resolveCompanyBinding, resolveActorModel: resolveAgentConfiguredModel };
281093
+ const runtimeAllocators = /* @__PURE__ */ new Map();
281094
+ const runtimeAllocatorFor = (companyId) => {
281095
+ const hit = runtimeAllocators.get(companyId);
281096
+ if (hit) return hit;
281097
+ const allocator = createRuntimeAllocator({
281098
+ // 主绑定与候选清单都**按公司解析**:全局 registry 对已切 `actor-config` 的公司是一份冻结旧镜像
281099
+ //(理由与 `resolveCompanyBinding` 文件头同源)。
281100
+ primaryBinding: (actorId) => resolveCompanyBinding(companyId, actorId),
281101
+ candidates: async (actorId) => {
281102
+ const registry2 = (await getEngine(companyId)).registry;
281103
+ return registry2.listRuntimeCandidates ? registry2.listRuntimeCandidates(actorId) : [];
281104
+ },
281105
+ nodeStates: async () => {
281106
+ const [nodes, runtimes] = await Promise.all([
281107
+ nodeStore.listNodes({ companyId }),
281108
+ nodeStore.listRuntimes(void 0, { companyId })
281109
+ ]);
281110
+ return nodes.filter((n) => n.status !== "deleted").map((n) => ({
281111
+ nodeId: n.id,
281112
+ // 报名称不报 id(ADR 0612):节点名 → 主机名 → 万不得已才是 id。
281113
+ displayName: n.name?.trim() || n.hostname || n.id,
281114
+ /**
281115
+ * 「在线」只取**可达性**:长连接在 + 心跳新鲜。刻意**不用** `healthOfNode(...).healthy`
281116
+ * ——那个判据还叠了「连败 ≥ 3 次」这一档软不健康。
281117
+ *
281118
+ * 为什么不叠:委派子会话此前走的是 `bindingRuntimeState`(在线 + 上报了这个 adapter),
281119
+ * 连败次数根本不参与。把它叠进来,一台**还连着、还能跑**但最近连败过三次的机器会被分配
281120
+ * 排除掉,而这个员工要是没配候选,那一轮就从「照旧派出去」变成当场 503 —— 拿一个新功能
281121
+ * 去掐掉一条本来能走的路。
281122
+ *
281123
+ * 连败这类「机器看着在、但活总是挂」的判断归**故障标记**(ADR 0616 D8,第 2 批):
281124
+ * 它会写进 `node_runtime_faults`,那时分配读的是故障行,粒度是 (节点, runtime) 而不是整台机器,
281125
+ * 也带得上「下次什么时候再试」。工单派发那条路的 `checkDispatchable` 仍按原样拦连败节点,
281126
+ * 与改动前逐字相同。
281127
+ */
281128
+ online: (hub?.connectedDaemons() ?? []).some((d) => d.daemonId === n.id) && (() => {
281129
+ const seen = hub?.lastSeenAt(n.id);
281130
+ return seen !== void 0 && Date.now() - seen < NODE_HEARTBEAT_STALE_MS;
281131
+ })(),
281132
+ /* 机器上有什么 runtime:漂移行(登记表有、机器探测不到)不算有——判据同 `runtimeDriftBlock`。
281133
+ `presentOnMachine` 缺省(还没对账过的旧行)一律当作正常,与那道闸逐字一致。 */
281134
+ runtimeKinds: runtimes.filter((r) => r.nodeId === n.id && r.presentOnMachine !== false).map((r) => r.kind),
281135
+ maxConcurrent: n.maxConcurrent ?? null,
281136
+ activeRunCount: activeRunCountOf(n.id)
281137
+ }));
281138
+ },
281139
+ faults: () => nodeStore.listRuntimeFaults({ companyId }),
281140
+ quotas: () => nodeStore.listRuntimeQuotas({ companyId }),
281141
+ actorName: async (actorId) => (await (await actors.resolveCtx(companyId)).service.getActor(actorId).catch(() => null))?.name ?? null
281142
+ });
281143
+ runtimeAllocators.set(companyId, allocator);
281144
+ return allocator;
281145
+ };
280274
281146
  if (pgPool) {
280275
281147
  const pinPool = pgPool;
280276
281148
  void (async () => {
@@ -281520,6 +282392,7 @@ async function startServe(opts) {
281520
282392
  const priorChatSession = chatSessionId && turnChatStore ? await turnChatStore.getSession(chatSessionId) : null;
281521
282393
  let binding;
281522
282394
  let runtimeTargetSource = "actor_binding";
282395
+ let allocatedModelOverride;
281523
282396
  if (stableDispatch) {
281524
282397
  binding = { actorId, ...stableDispatch.target, status: "active" };
281525
282398
  } else {
@@ -281546,14 +282419,42 @@ async function startServe(opts) {
281546
282419
  }
281547
282420
  });
281548
282421
  } catch (e) {
281549
- if (e instanceof SessionRuntimeOverrideError) throw new ApiError(e.status, e.code, e.message);
281550
- throw e;
282422
+ if (e instanceof SessionRuntimeOverrideError) {
282423
+ if (!delegatedChildTurn) throw new ApiError(e.status, e.code, e.message);
282424
+ console.log(`[runtime-alloc] \u59D4\u6D3E\u5B50\u4F1A\u8BDD\u9489\u4F4F\u7684\u76EE\u6807\u4E0D\u53EF\u7528\uFF08${e.code}\uFF09\uFF0C\u4EA4\u7ED9\u5206\u914D\u6311\u5019\u9009`);
282425
+ resolved = null;
282426
+ } else throw e;
281551
282427
  }
281552
- if (!resolved) {
282428
+ if (!resolved && !delegatedChildTurn) {
281553
282429
  throw new ApiError(409, "ACTOR_BINDING_MISSING", `\u8FD9\u4F4D Agent \u8FD8\u6CA1\u6709\u7ED1\u5B9A\u8FD0\u884C\u65F6\uFF0C\u8BF7${chatRebindHint("\u7ED1\u5B9A\u4E00\u4E2A\u53EF\u7528\u7684\u8FD0\u884C\u65F6")}\u540E\u518D\u53D1`);
281554
282430
  }
281555
- binding = resolved.binding;
281556
- runtimeTargetSource = resolved.source;
282431
+ binding = resolved?.binding ?? { actorId, nodeId: "", runtimeKind: "", status: "active" };
282432
+ runtimeTargetSource = resolved?.source ?? "actor_binding";
282433
+ if (delegatedChildTurn) {
282434
+ const pinned = sessionRuntimeOverride(priorChatSession);
282435
+ const primary = resolved ? { nodeId: resolved.binding.nodeId, runtimeKind: resolved.binding.runtimeKind } : pinned ? { nodeId: pinned.nodeId, runtimeKind: pinned.runtimeKind } : null;
282436
+ try {
282437
+ const allocated = await runtimeAllocatorFor(runCompanyId).allocateFor({
282438
+ actorId,
282439
+ // 「同一件活」= 这条子会话。重试上限算在它头上,不与同一位专家的别条委派互相扣次数。
282440
+ unitOfWork: `chat-session:${chatSessionId ?? artifactId}`,
282441
+ what: "\u59D4\u6D3E\u5B50\u4F1A\u8BDD",
282442
+ primary
282443
+ });
282444
+ if (allocated.binding.nodeId !== binding.nodeId || allocated.binding.runtimeKind !== binding.runtimeKind) {
282445
+ binding = allocated.binding;
282446
+ runtimeTargetSource = "session_override";
282447
+ allocatedModelOverride = allocated.model ?? void 0;
282448
+ }
282449
+ } catch (e) {
282450
+ if (e instanceof RuntimeAllocationError) throw new ApiError(e.status === 503 ? 503 : 409, e.code, e.message);
282451
+ console.warn(`[runtime-alloc] \u59D4\u6D3E\u5B50\u4F1A\u8BDD\u5206\u914D\u5931\u8D25\uFF1A${String(e)}`);
282452
+ if (!resolved) throw e;
282453
+ }
282454
+ }
282455
+ if (!binding.nodeId || !binding.runtimeKind) {
282456
+ throw new ApiError(409, "ACTOR_BINDING_MISSING", `\u8FD9\u4F4D Agent \u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u8FD0\u884C\u65F6\uFF0C\u8BF7${chatRebindHint("\u7ED1\u5B9A\u4E00\u4E2A\u53EF\u7528\u7684\u8FD0\u884C\u65F6")}\u540E\u518D\u53D1`);
282457
+ }
281557
282458
  }
281558
282459
  const requireDispatchNode = async (target) => {
281559
282460
  const targetNode = await nodeStore.getNode(target.nodeId);
@@ -281777,7 +282678,7 @@ async function startServe(opts) {
281777
282678
  };
281778
282679
  const provision = isolatedKnowledgeTurn ? { env: {}, wrapperPaths: [], requiredTools: [], requiredToolVersions: {}, connectorCreds: [], cleanup: async () => void 0 } : await buildActorProvision(actorService, actorId, chatDispatchScope(priorChatSession));
281779
282680
  const actorCtx = await buildActorContext(actorService, actorId).catch(() => null);
281780
- const sessionModel = configuredModel(sessionModelOverride);
282681
+ const sessionModel = configuredModel(allocatedModelOverride !== void 0 ? allocatedModelOverride : sessionModelOverride);
281781
282682
  const actorConfiguredModel = isolatedBackground ? configuredModel(actorCtx?.config?.model) : void 0;
281782
282683
  const chatResolvedModel = sessionModel ?? actorConfiguredModel;
281783
282684
  const chatModel = chatResolvedModel ?? await registryStore.getRuntimeConfig(`runtime:${binding.nodeId}:${binding.runtimeKind}`).then((c) => configuredModel(c?.model)).catch(() => void 0);
@@ -281982,6 +282883,24 @@ async function startServe(opts) {
281982
282883
  const done = new Promise((resolve11, reject) => {
281983
282884
  handle.onExit((info) => {
281984
282885
  chatLiveSessions.delete(chatJobKey);
282886
+ if (delegatedChildTurn) {
282887
+ try {
282888
+ const unitOfWork = `chat-session:${chatSessionId ?? artifactId}`;
282889
+ const allocator = runtimeAllocatorFor(runCompanyId);
282890
+ if (isRuntimeClassFailure(info.reason, info.errorMessage)) {
282891
+ allocator.noteFailure({
282892
+ unitOfWork,
282893
+ target: { nodeId: binding.nodeId, runtimeKind: binding.runtimeKind },
282894
+ ...info.reason ? { reason: info.reason } : {},
282895
+ ...info.errorMessage ? { errorMessage: info.errorMessage } : {}
282896
+ });
282897
+ } else {
282898
+ allocator.noteSettled(unitOfWork);
282899
+ }
282900
+ } catch (err) {
282901
+ console.warn(`[runtime-alloc] \u59D4\u6D3E\u5B50\u4F1A\u8BDD\u5931\u8D25\u56DE\u62A5\u51FA\u9519\uFF08\u4E0D\u5F71\u54CD\u6536\u5C3E\uFF09\uFF1A${String(err)}`);
282902
+ }
282903
+ }
281985
282904
  setTimeout(() => {
281986
282905
  chatRunByDispatch.delete(handle.id);
281987
282906
  }, 6e4).unref?.();
@@ -282052,6 +282971,9 @@ async function startServe(opts) {
282052
282971
  },
282053
282972
  done,
282054
282973
  kill: () => handle.kill(),
282974
+ // Bug 0232:`kill()` 报不了帧的去向(契约钉死 `Promise<void>`),这一对能。
282975
+ // 条件透传:本地 adapter 的句柄没有这条,上层按「方法在不在」分流回老行为。
282976
+ ...typeof handle.sendKill === "function" ? { sendKill: () => handle.sendKill(), cancelLocally: () => handle.cancelLocally?.() ?? false } : {},
282055
282977
  // ADR-0093 对话追加(chat 道):透传底层句柄的 appendInput。runtime 不原生支持时 handle 上
282056
282978
  // 压根没这个方法,这里也就不给——server 据此让插入入口置灰,而不是让人按下去才发现不行。
282057
282979
  // 不带 interrupt:人插一句话走回合边界排队,不作废在飞回合(那是 produce 道 ADR-0094 的语义)。
@@ -282425,6 +283347,18 @@ async function startServe(opts) {
282425
283347
  const mapping = dispatchToWork.get(msg.dispatchId);
282426
283348
  const exitBus = mapping ? busForCompany(mapping.companyId) ?? busForWorkorder(mapping.workorderId) : null;
282427
283349
  if (mapping && exitBus) {
283350
+ if (mapping.artifactId && mapping.companyId) {
283351
+ const allocator = runtimeAllocatorFor(mapping.companyId);
283352
+ if (isRuntimeClassFailure(msg.info.reason, msg.info.errorMessage)) {
283353
+ allocator.noteFailure({
283354
+ unitOfWork: mapping.artifactId,
283355
+ ...msg.info.reason ? { reason: msg.info.reason } : {},
283356
+ ...msg.info.errorMessage ? { errorMessage: msg.info.errorMessage } : {}
283357
+ });
283358
+ } else {
283359
+ allocator.noteSettled(mapping.artifactId);
283360
+ }
283361
+ }
282428
283362
  await exitBus.submit({
282429
283363
  companyId: "",
282430
283364
  workorderId: mapping.workorderId,
@@ -282884,7 +283818,8 @@ async function startServe(opts) {
282884
283818
  try {
282885
283819
  result = replyIssueId ? await bridge.replyWork(rev.artifactId, rev.author, replyIssueId, ledgerId) : await bridge.produce(rev.artifactId, rev.author, workId, ledgerId, opts2?.resumeEligible, opts2?.protocolRecovery?.round);
282886
283820
  } catch (err) {
282887
- void dispatchLedgerFor(companyId).then((l) => l.close(ledgerId, { outcome: "failed", reason: `dispatch error: ${err instanceof Error ? err.message : String(err)}` })).catch(ledgerWarn("close(error)"));
283821
+ const reason = err instanceof RuntimeAllocationError ? err.message : `dispatch error: ${err instanceof Error ? err.message : String(err)}`;
283822
+ void dispatchLedgerFor(companyId).then((l) => l.close(ledgerId, { outcome: "failed", reason })).catch(ledgerWarn("close(error)"));
282888
283823
  console.log(`[new-engine] dispatchWork ${workId} \u6D3E\u53D1\u5F02\u5E38\uFF1A${String(err)} \u2192 \u7F6E work failed`);
282889
283824
  const failBus = busForCompany(companyId);
282890
283825
  if (failBus) {
@@ -282899,7 +283834,7 @@ async function startServe(opts) {
282899
283834
  semanticsVersion: 3,
282900
283835
  dispatchId: ledgerId,
282901
283836
  outcome: "failed",
282902
- detail: { reason: `dispatch error: ${err instanceof Error ? err.message : String(err)}` }
283837
+ detail: { reason }
282903
283838
  }
282904
283839
  });
282905
283840
  }
@@ -282910,6 +283845,9 @@ async function startServe(opts) {
282910
283845
  dispatchToWork.set(result.dispatchId, {
282911
283846
  workId,
282912
283847
  workorderId: wid,
283848
+ // ★ ADR 0616:运行时分配按「同一件活」记失败链,工单这条路的单位就是这个节点(artifactId)。
283849
+ // 退出帧上只有 dispatchId,所以必须在这里记下来——否则失败时不知道该把哪条链标上。
283850
+ artifactId: rev.artifactId,
282913
283851
  // ★ 记下归属公司:session_started / session_exited / 收口漏斗都据它把事件投回**这一家**的 bus。
282914
283852
  companyId,
282915
283853
  ...delivery ? { nodeId: delivery.nodeId } : {}
@@ -283183,7 +284121,27 @@ async function startServe(opts) {
283183
284121
  }),
283184
284122
  serverUrl: localUrl,
283185
284123
  serverUrlFor: (ctx) => serverUrlForBinding(ctx.binding),
283186
- resolveBinding: async (actorId) => (await runActors()).service.resolveBinding()(actorId),
284124
+ /**
284125
+ * ★ ADR 0616:工单节点派活(produce / review / integrate / reply)的目标由**运行时分配**决定,
284126
+ * 不再直接读那条主绑定。两个触发时机都落在这里:
284127
+ *
284128
+ * ① 开一个新会话时 —— 每次 spawn 前跑一次分配(排除故障与满并发、主可用就选主)。
284129
+ * ② 上一轮因 runtime/节点原因失败时 —— `session_exited` 那边已经把失败目标记进这件活的
284130
+ * 失败链,这一次分配会自动换到另一个候选;连着换满 `RUNTIME_ALLOCATION_MAX_ATTEMPTS` 次
284131
+ * 仍起不来就抛错(由 `dispatchWork` 的 catch 落成 work failed,原文带机器名与原因)。
284132
+ *
284133
+ * 没带 `ctx.unitOfWork` 的调用(`executorSignature` 只要一个指纹)退回旧路:那条路不该
284134
+ * 消耗一次分配尝试,更不该因为「所有候选都试过了」而抛错——指纹算不出来只会退化,不该阻断。
284135
+ */
284136
+ resolveBinding: async (actorId, ctx) => {
284137
+ if (!ctx?.unitOfWork) return (await runActors()).service.resolveBinding()(actorId);
284138
+ const resolved = await runtimeAllocatorFor(companyId).allocateFor({
284139
+ actorId,
284140
+ unitOfWork: ctx.unitOfWork,
284141
+ what: "\u5DE5\u5355\u8282\u70B9\u6D3E\u6D3B"
284142
+ });
284143
+ return resolved.binding;
284144
+ },
283187
284145
  prepareReadonlyInputs: async ({ actor, artifactType, binding, codeRepo, pinnedRevisions }) => {
283188
284146
  let prepared = null;
283189
284147
  try {
@@ -284589,7 +285547,7 @@ ${nodeFault}` : "");
284589
285547
  const zbEnabled = opts.zeroOutputBreaker?.enabled ?? !zbDisabledByEnv;
284590
285548
  const zbIntervalMs = opts.zeroOutputBreaker?.intervalMs ?? 3e4;
284591
285549
  const zbWindowMs = opts.zeroOutputBreaker?.windowMs ?? (envInt("OASIS_ZERO_OUTPUT_BREAKER_WINDOW_MIN") ?? 30) * 6e4;
284592
- const zbThreshold = opts.zeroOutputBreaker?.threshold ?? envInt("OASIS_ZERO_OUTPUT_BREAKER_N") ?? 5;
285550
+ const zbThreshold = opts.zeroOutputBreaker?.threshold ?? envInt("OASIS_ZERO_OUTPUT_BREAKER_N") ?? 20;
284593
285551
  const zbCostThresholdMicros = opts.zeroOutputBreaker?.costThresholdMicros ?? (envInt("OASIS_ZERO_OUTPUT_BREAKER_COST_USD") ?? 0) * 1e6;
284594
285552
  if (zbEnabled && zbIntervalMs > 0 && (zbThreshold > 0 || zbCostThresholdMicros > 0)) {
284595
285553
  const zbActions = /* @__PURE__ */ new Set(["produce", "review"]);
@@ -289795,7 +290753,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
289795
290753
  }
289796
290754
  }
289797
290755
  }
289798
- var CLI_VERSION = "2.2.72" ? "2.2.72" : "dev";
290756
+ var CLI_VERSION = "2.2.73" ? "2.2.73" : "dev";
289799
290757
  function apiHeaders(token, company) {
289800
290758
  const headers = {
289801
290759
  authorization: `Bearer ${token}`,
@@ -293601,7 +294559,7 @@ function shimScript() {
293601
294559
  }
293602
294560
 
293603
294561
  // src/index.ts
293604
- var PKG_VERSION = true ? "2.2.72" : "dev";
294562
+ var PKG_VERSION = true ? "2.2.73" : "dev";
293605
294563
  var LOCAL_BIN = localBin();
293606
294564
  var NPM_PREFIX = npmPrefix();
293607
294565
  var INSTANCE = DEFAULT_INSTANCE;