oasis_test_v2 2.2.58 → 2.2.60

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 +667 -136
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -835,9 +835,11 @@ var init_avatar = __esm({
835
835
  });
836
836
 
837
837
  // ../contract/src/runtime.ts
838
+ var SESSION_STARTUP_FAILED_PREFIX;
838
839
  var init_runtime = __esm({
839
840
  "../contract/src/runtime.ts"() {
840
841
  "use strict";
842
+ SESSION_STARTUP_FAILED_PREFIX = "\u8FD9\u4E00\u8F6E\u6CA1\u80FD\u5728\u8282\u70B9\u4E0A\u542F\u52A8\uFF1A";
841
843
  }
842
844
  });
843
845
 
@@ -10569,6 +10571,8 @@ function estimateUsageCostBreakdown(model, usage) {
10569
10571
  cacheMicros: Math.round(
10570
10572
  (cacheCreationTokens * price.inputPerMillion + cacheReadTokens * cacheReadPrice) / 1e6 * 1e6
10571
10573
  ),
10574
+ cacheReadMicros: Math.round(cacheReadTokens * cacheReadPrice / 1e6 * 1e6),
10575
+ cacheCreationMicros: Math.round(cacheCreationTokens * price.inputPerMillion / 1e6 * 1e6),
10572
10576
  outputMicros: Math.round(outputTokens * price.outputPerMillion / 1e6 * 1e6)
10573
10577
  };
10574
10578
  }
@@ -11281,8 +11285,8 @@ var init_chat_item_ledger = __esm({
11281
11285
  note: text6,
11282
11286
  ...extra?.reason ? { reason: extra.reason } : {},
11283
11287
  ...extra?.detail ? { detail: extra.detail } : {},
11284
- /* 「这一轮是用户点停止的」这一格(ADR 0600):**持久的那一份归因就在这里**——
11285
- 刷新 / 换设备读的是这一行,前端据它决定写「用户停止了输出」还是「对话已中断」。 */
11288
+ /* 「这一轮是谁停的」这一格(ADR 0600):**持久的那一份归因就在这里**——
11289
+ 刷新 / 换设备读的是这一行,前端据它决定提示条上那句话;缺省 = 认不出 = 「对话已中断」。 */
11286
11290
  ...extra?.cancelledBy ? { cancelledBy: extra.cancelledBy } : {}
11287
11291
  }
11288
11292
  });
@@ -11876,15 +11880,39 @@ var init_chat_item_ledger = __esm({
11876
11880
  });
11877
11881
 
11878
11882
  // ../server/src/chat-cancel-attribution.ts
11879
- function attributeChatExit(text6, extra, stoppedByUser) {
11880
- if (!stoppedByUser || extra.reason !== "cancelled") return { text: text6, extra };
11881
- return { text: USER_STOPPED_OUTPUT_NOTE, extra: { ...extra, cancelledBy: "user" } };
11883
+ function hasCancelNote(by) {
11884
+ return by !== void 0 && by in CHAT_CANCEL_NOTE;
11882
11885
  }
11883
- var USER_STOPPED_OUTPUT_NOTE;
11886
+ function chatCancelNote(by) {
11887
+ return hasCancelNote(by) ? CHAT_CANCEL_NOTE[by] : CHAT_CANCELLED_NOTE;
11888
+ }
11889
+ function cancelCauseFromExitMessage(errorMessage2) {
11890
+ const line2 = errorMessage2?.trim();
11891
+ if (!line2) return void 0;
11892
+ return NODE_SHUTDOWN_EXIT_MARKERS.some((m2) => line2.includes(m2)) ? "system-restart" : void 0;
11893
+ }
11894
+ function attributeChatExit(text6, extra, cause) {
11895
+ if (extra.reason !== "cancelled") return { text: text6, extra };
11896
+ const by = cause ?? extra.cancelledBy;
11897
+ if (!by) return { text: text6, extra };
11898
+ return { text: chatCancelNote(by), extra: { ...extra, cancelledBy: by } };
11899
+ }
11900
+ var CHAT_CANCEL_NOTE, CHAT_CANCELLED_NOTE, USER_STOPPED_OUTPUT_NOTE, NODE_SHUTDOWN_EXIT_MARKERS;
11884
11901
  var init_chat_cancel_attribution = __esm({
11885
11902
  "../server/src/chat-cancel-attribution.ts"() {
11886
11903
  "use strict";
11887
- USER_STOPPED_OUTPUT_NOTE = "\u7528\u6237\u505C\u6B62\u4E86\u8F93\u51FA";
11904
+ CHAT_CANCEL_NOTE = {
11905
+ user: "\u7528\u6237\u505C\u6B62\u4E86\u8F93\u51FA",
11906
+ "system-restart": "\u5BF9\u8BDD\u5DF2\u4E2D\u65AD\uFF1A\u7CFB\u7EDF\u66F4\u65B0\u91CD\u542F",
11907
+ "node-disconnected": "\u5BF9\u8BDD\u5DF2\u4E2D\u65AD\uFF1A\u6267\u884C\u8FD9\u4E00\u8F6E\u7684\u673A\u5668\u65AD\u5F00\u4E86\u8FDE\u63A5",
11908
+ runtime: "\u5BF9\u8BDD\u5DF2\u4E2D\u65AD\uFF1A\u6267\u884C\u7A0B\u5E8F\u4E2D\u9014\u53D6\u6D88\u4E86\u8FD9\u4E00\u8F6E"
11909
+ };
11910
+ CHAT_CANCELLED_NOTE = "\u5BF9\u8BDD\u5DF2\u4E2D\u65AD";
11911
+ USER_STOPPED_OUTPUT_NOTE = CHAT_CANCEL_NOTE.user;
11912
+ NODE_SHUTDOWN_EXIT_MARKERS = [
11913
+ "\u8282\u70B9 daemon \u505C\u673A\uFF08",
11914
+ "\u8282\u70B9\u6B63\u5728\u505C\u673A\u6392\u7A7A"
11915
+ ];
11888
11916
  }
11889
11917
  });
11890
11918
 
@@ -12322,9 +12350,10 @@ function runBackgroundAssistantChatTurn(deps) {
12322
12350
  chatExit?.summary ?? (err instanceof Error ? err.message : String(err)),
12323
12351
  {
12324
12352
  ...chatExit?.reason ? { reason: chatExit.reason } : {},
12325
- ...chatExit?.detail ? { detail: chatExit.detail } : {}
12353
+ ...chatExit?.detail ? { detail: chatExit.detail } : {},
12354
+ ...chatExit?.cancelledBy ? { cancelledBy: chatExit.cancelledBy } : {}
12326
12355
  },
12327
- liveTurn?.stoppedByUser() ?? false
12356
+ liveTurn?.cancelCause()
12328
12357
  );
12329
12358
  const part = { type: "error", seq: ++localSeq, text: text6, ...extra };
12330
12359
  localParts.push(part);
@@ -12385,6 +12414,73 @@ var init_stable_chat_dispatch_ids = __esm({
12385
12414
  }
12386
12415
  });
12387
12416
 
12417
+ // ../server/src/chat-interrupt-notice.ts
12418
+ function chatInterruptCauseOf(exitReason) {
12419
+ if (exitReason === "sweep-orphan" || exitReason === "sweep-absolute-cap") return exitReason;
12420
+ return "run-failed";
12421
+ }
12422
+ async function recordChatTurnInterruption(deps) {
12423
+ const { items, sessionId, turnId } = deps;
12424
+ if (!items || !chatItemsDoubleWriteEnabled()) return [];
12425
+ const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
12426
+ const log3 = deps.log ?? (() => {
12427
+ });
12428
+ try {
12429
+ const rows = await items.listItemsByTurn(sessionId, turnId);
12430
+ const noticeId = chatInterruptItemId(turnId);
12431
+ const existing = rows.find((r) => r.id === noticeId);
12432
+ let version2 = await items.sessionVersionCursor(sessionId).catch(() => 0);
12433
+ const touched = [];
12434
+ const at = now();
12435
+ for (const row of rows) {
12436
+ if (row.status !== "streaming") continue;
12437
+ const updated = await items.applyOp(row.id, { op: "set_status", status: "interrupted" }, { at, version: ++version2, sessionId });
12438
+ if (updated) touched.push(updated);
12439
+ }
12440
+ const payload2 = deps.cause === "sweep-orphan" ? { code: "error", note: CHAT_INTERRUPT_NOTICE[deps.cause], reason: "cancelled", closedBy: deps.reason } : { code: "error", note: CHAT_INTERRUPT_NOTICE[deps.cause], reason: deps.reason };
12441
+ if (existing) {
12442
+ const updated = await items.applyOp(noticeId, { op: "set_payload", payload: payload2 }, { at, version: ++version2, sessionId });
12443
+ if (updated) touched.push(updated);
12444
+ return touched;
12445
+ }
12446
+ const created = await items.createItem({
12447
+ id: noticeId,
12448
+ sessionId,
12449
+ turnId,
12450
+ ...deps.messageId ? { messageId: deps.messageId } : {},
12451
+ ...deps.runId ? { runId: deps.runId } : {},
12452
+ kind: "control",
12453
+ role: "assistant",
12454
+ status: "failed",
12455
+ payload: payload2,
12456
+ // 这句话不进 `chat_messages.content`(那边由各自的收口路自己写),对账按这一位分流。
12457
+ metadata: { [CHAT_ITEM_META_LEGACY_CONTENT]: false },
12458
+ createdAt: at,
12459
+ version: ++version2
12460
+ });
12461
+ touched.push(created);
12462
+ return touched;
12463
+ } catch (err) {
12464
+ log3(`[chat-interrupt] items \u7ED3\u5C40\u9879\u5199\u5165\u5931\u8D25\uFF1Asession=${sessionId} turn=${turnId} ${String(err)}`);
12465
+ return [];
12466
+ }
12467
+ }
12468
+ var chatInterruptItemId, CHAT_INTERRUPT_NOTICE;
12469
+ var init_chat_interrupt_notice = __esm({
12470
+ "../server/src/chat-interrupt-notice.ts"() {
12471
+ "use strict";
12472
+ init_chat_item();
12473
+ init_chat_cancel_attribution();
12474
+ init_chat_item_ledger();
12475
+ chatInterruptItemId = (turnId) => `chat-interrupt:${turnId}`;
12476
+ CHAT_INTERRUPT_NOTICE = {
12477
+ "sweep-orphan": CHAT_CANCELLED_NOTE,
12478
+ "sweep-absolute-cap": "\u8FD9\u4E00\u8F6E\u8FD0\u884C\u65F6\u95F4\u8D85\u8FC7\u4E86\u4E0A\u9650\uFF0C\u5DF2\u88AB\u7CFB\u7EDF\u7EC8\u6B62\u3002\u4E0A\u9762\u662F\u7EC8\u6B62\u524D\u5DF2\u7ECF\u751F\u6210\u7684\u5185\u5BB9\uFF0C\u53EF\u4EE5\u628A\u4EFB\u52A1\u62C6\u5C0F\u4E00\u4E9B\u518D\u53D1\u4E00\u6B21\u3002",
12479
+ "run-failed": "\u8FD9\u4E00\u8F6E\u6CA1\u6709\u6B63\u5E38\u7ED3\u675F\uFF08\u8FD0\u884C\u88AB\u4E2D\u65AD\uFF09\uFF0C\u6CA1\u80FD\u8DD1\u5B8C\u3002\u4E0A\u9762\u662F\u4E2D\u65AD\u524D\u5DF2\u7ECF\u751F\u6210\u7684\u5185\u5BB9\uFF0C\u4F60\u53EF\u4EE5\u518D\u53D1\u4E00\u6B21\u3002"
12480
+ };
12481
+ }
12482
+ });
12483
+
12388
12484
  // ../server/src/chat-turn-gate.ts
12389
12485
  function localTurnSourceRunId(source, turnId) {
12390
12486
  return `${source}:${turnId}`;
@@ -12540,6 +12636,19 @@ function assistantRowRecovery(chatStore, opts = {}) {
12540
12636
  now: () => patch.completedAt,
12541
12637
  ...outputsPart ? { outputsPart } : {}
12542
12638
  });
12639
+ if (patch.status !== "error") return;
12640
+ const items = opts.itemsFor?.(turn.chatSessionId);
12641
+ if (!items) return;
12642
+ await recordChatTurnInterruption({
12643
+ items,
12644
+ sessionId: turn.chatSessionId,
12645
+ turnId: await resolveChatItemsTurnId(items, row.id, fallbackTurnId(turn.assistantRunId ?? void 0)),
12646
+ runId: turn.assistantRunId ?? null,
12647
+ messageId: row.id,
12648
+ cause: patch.cause ?? "run-failed",
12649
+ reason: patch.cause ?? "run-failed",
12650
+ now: () => patch.completedAt
12651
+ });
12543
12652
  }
12544
12653
  };
12545
12654
  }
@@ -12570,7 +12679,9 @@ async function reconcileChatTurn(turn, deps) {
12570
12679
  if (deps.settleAssistantRow) {
12571
12680
  await deps.settleAssistantRow(turn, {
12572
12681
  status: patch.status === "succeeded" ? "done" : "error",
12573
- completedAt: patch.completedAt
12682
+ completedAt: patch.completedAt,
12683
+ // 由头只在判死那一档有意义;`orphaned` 是本拍自己判的,其余按 run 的 exit_reason 走。
12684
+ ...patch.status === "succeeded" ? {} : { cause: patch.cause ?? "run-failed" }
12574
12685
  }).catch((e) => deps.log?.(`[chat-turn] \u8F6E\u6B21 ${turn.id} \u7684 assistant \u884C\u6536\u53E3\u5931\u8D25\uFF08\u8F6E\u6B21\u7167\u5E38\u6536\u53E3\uFF09: ${String(e)}`));
12575
12686
  }
12576
12687
  return writeTurn(patch);
@@ -12616,7 +12727,9 @@ async function reconcileChatTurn(turn, deps) {
12616
12727
  const ok3 = await settleTurnAndRow({
12617
12728
  status,
12618
12729
  completedAt: run.endedAt ?? new Date(deps.now).toISOString(),
12619
- ...status === "succeeded" ? {} : { lastError: `run ${runId} \u7EC8\u6001 ${run.status}\uFF08\u7531\u6062\u590D\u5668\u6536\u53E3\uFF09` }
12730
+ ...status === "succeeded" ? {} : { lastError: `run ${runId} \u7EC8\u6001 ${run.status}\uFF08\u7531\u6062\u590D\u5668\u6536\u53E3\uFF09` },
12731
+ // Bug #0185:孤儿扫描 / 绝对上限关掉的轮,对话里那句话要说得出是哪一种。
12732
+ ...status === "succeeded" ? {} : { cause: chatInterruptCauseOf(run.exitReason) }
12620
12733
  });
12621
12734
  if (!ok3) return raced;
12622
12735
  return { settled: true, turnId: turn.id, status, reason: `run \u7EC8\u6001 ${run.status}` };
@@ -12678,6 +12791,8 @@ var init_chat_turn_gate = __esm({
12678
12791
  import_node_crypto16 = require("node:crypto");
12679
12792
  init_chat_assistant_turn();
12680
12793
  init_stable_chat_dispatch_ids();
12794
+ init_chat_item_ledger();
12795
+ init_chat_interrupt_notice();
12681
12796
  DISPATCH_INTENT_PREFIX = "dispatch-intent:";
12682
12797
  DISPATCH_CLAIMED_PREFIX = "dispatch-claimed:";
12683
12798
  CHAT_TURN_ORPHAN_GRACE_MS = 10 * 6e4;
@@ -163715,7 +163830,8 @@ var init_live_chat = __esm({
163715
163830
  turn.v3Subscribers.delete(sub);
163716
163831
  };
163717
163832
  },
163718
- stoppedByUser: () => turn.stoppedByUser === true,
163833
+ cancelCause: () => turn.cancelCause,
163834
+ stoppedByUser: () => turn.cancelCause === "user",
163719
163835
  finish: (status) => {
163720
163836
  stopUserItems?.();
163721
163837
  for (const receipt of turn.inputReceipts.values()) receipt.abandon();
@@ -164290,7 +164406,7 @@ var init_live_chat = __esm({
164290
164406
  return { stopped: false, reason: "turn-changed" };
164291
164407
  }
164292
164408
  if (turn.status === "running") {
164293
- turn.stoppedByUser = true;
164409
+ turn.cancelCause = "user";
164294
164410
  try {
164295
164411
  turn.handle.kill();
164296
164412
  } catch {
@@ -164298,6 +164414,17 @@ var init_live_chat = __esm({
164298
164414
  }
164299
164415
  return { stopped: true };
164300
164416
  }
164417
+ /**
164418
+ * 平台自己要停这一轮时的留痕(ADR 0600 扩档):liveness 静默保险丝、hub 驱逐会话都经这里。
164419
+ * 与 {@link stop} 同一条纪律——**写在真去 kill 之前**,且只写还在跑的那一轮;已经收尾的不改。
164420
+ * 先到先赢:`/stop` 已经写过 `user` 的轮不被后来的系统原因盖掉。
164421
+ */
164422
+ markCancelCause(chatSessionId, cause) {
164423
+ const turn = this.turns.get(chatSessionId);
164424
+ if (!turn || turn.status !== "running" || turn.cancelCause) return false;
164425
+ turn.cancelCause = cause;
164426
+ return true;
164427
+ }
164301
164428
  /** 该会话当前是否有一轮 running(供上层判断是否要挡并发发送)。 */
164302
164429
  isRunning(chatSessionId) {
164303
164430
  return this.turns.get(chatSessionId)?.status === "running";
@@ -166577,7 +166704,7 @@ async function validateExplicitOwner(registry2, owner, artifactType) {
166577
166704
  if (!b2 || b2.status !== "active") {
166578
166705
  return {
166579
166706
  actor: null,
166580
- issue: { code: "missing_binding", severity: "warning", artifactType, message: `\u6307\u5B9A\u7684\u8D1F\u8D23\u4EBA ${a.name}\uFF08${owner}\uFF09\u7F3A\u5C11 active runtime \u7ED1\u5B9A\uFF0C${artifactType} \u8282\u70B9\u9000\u56DE\u6309\u5C97\u4F4D\u89E3\u6790\u3002` }
166707
+ issue: { code: "missing_binding", severity: "warning", artifactType, message: `\u6307\u5B9A\u7684\u8D1F\u8D23\u4EBA ${a.name}\uFF08${owner}\uFF09\u7F3A\u5C11\u53EF\u7528\u7684\u8FD0\u884C\u65F6\u7ED1\u5B9A\uFF0C${artifactType} \u8282\u70B9\u9000\u56DE\u6309\u5C97\u4F4D\u89E3\u6790\u3002` }
166581
166708
  };
166582
166709
  }
166583
166710
  }
@@ -166626,7 +166753,7 @@ async function resolveActiveRole(registry2, role, artifactType, roles) {
166626
166753
  severity: "warning",
166627
166754
  role,
166628
166755
  ...artifactType ? { artifactType } : {},
166629
- message: `\u5C97\u4F4D ${role} \u6307\u5B9A\u7684\u9ED8\u8BA4\u627F\u63A5\u4EBA ${declared} \u5F53\u524D\u4E0D\u53EF\u63A5\u5355\uFF08\u79BB\u5C97 / \u4E0D\u5728\u8BE5\u5C97\u4F4D / \u7F3A runtime \u7ED1\u5B9A\uFF09\uFF0C\u5DF2\u9000\u56DE\u6309\u89C4\u5219\u6311\u4EBA\u3002`
166756
+ message: `\u5C97\u4F4D ${role} \u6307\u5B9A\u7684\u9ED8\u8BA4\u627F\u63A5\u4EBA ${declared} \u5F53\u524D\u4E0D\u53EF\u63A5\u5355\uFF08\u79BB\u5C97 / \u4E0D\u5728\u8BE5\u5C97\u4F4D / \u7F3A\u8FD0\u884C\u65F6\u7ED1\u5B9A\uFF09\uFF0C\u5DF2\u9000\u56DE\u6309\u89C4\u5219\u6311\u4EBA\u3002`
166630
166757
  }
166631
166758
  };
166632
166759
  }
@@ -166659,7 +166786,7 @@ async function resolveActiveRole(registry2, role, artifactType, roles) {
166659
166786
  ...artifactType ? { artifactType } : {},
166660
166787
  // 措辞对两条路都成立:规则路会丢节点、agent/剧本路保留为待岗——**都是「暂不自动派发」**,
166661
166788
  // 而非「暂不创建」(后者在保留待岗后就是假话)。
166662
- message: `${preferred.name}\uFF08${preferred.id}\uFF09\u7F3A\u5C11 active runtime \u7ED1\u5B9A\uFF0C${nodeWord}\u6682\u4E0D\u81EA\u52A8\u6D3E\u53D1\uFF08\u5F85\u8865\u9F50\u540E\u81EA\u52A8\u63A5\u624B\uFF09\u3002`
166789
+ message: `${preferred.name}\uFF08${preferred.id}\uFF09\u7F3A\u5C11\u53EF\u7528\u7684\u8FD0\u884C\u65F6\u7ED1\u5B9A\uFF0C${nodeWord}\u6682\u4E0D\u81EA\u52A8\u6D3E\u53D1\uFF08\u5F85\u8865\u9F50\u540E\u81EA\u52A8\u63A5\u624B\uFF09\u3002`
166663
166790
  }
166664
166791
  };
166665
166792
  }
@@ -168631,7 +168758,19 @@ async function planChatContinuation(input) {
168631
168758
  fallbackInjectedMessageIds: fallbackMessage ? fallbackHistory.map((t) => t.id) : []
168632
168759
  };
168633
168760
  }
168634
- var HISTORY_FETCH_SLACK, UNFINISHED_OPEN, UNFINISHED_EMPTY;
168761
+ function placeholderRuntimeSessionFlag(isPlaceholder) {
168762
+ return isPlaceholder ? { [RUNTIME_SESSION_ID_PLACEHOLDER_KEY]: true } : {};
168763
+ }
168764
+ function nativeRuntimeSessionMetadata(metadata, reported) {
168765
+ if (!reported) return void 0;
168766
+ const base = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : void 0;
168767
+ if (!base) return void 0;
168768
+ if (base[RUNTIME_SESSION_ID_PLACEHOLDER_KEY] !== true) return void 0;
168769
+ if (typeof base["runtimeSessionId"] !== "string" || base["runtimeSessionId"] === reported) return void 0;
168770
+ const { [RUNTIME_SESSION_ID_PLACEHOLDER_KEY]: _wasPlaceholder, ...rest } = base;
168771
+ return { ...rest, runtimeSessionId: reported };
168772
+ }
168773
+ var HISTORY_FETCH_SLACK, UNFINISHED_OPEN, UNFINISHED_EMPTY, RUNTIME_SESSION_ID_PLACEHOLDER_KEY;
168635
168774
  var init_continuation3 = __esm({
168636
168775
  "../server/src/domains/chat-sessions/continuation.ts"() {
168637
168776
  "use strict";
@@ -168641,6 +168780,7 @@ var init_continuation3 = __esm({
168641
168780
  HISTORY_FETCH_SLACK = 2;
168642
168781
  UNFINISHED_OPEN = "\u3010\u4E0A\u4E00\u8F6E\u672A\u5B8C\u6210\u2014\u2014\u4EE5\u4E0B\u662F\u4E2D\u65AD\u524D\u5DF2\u4EA7\u51FA\u7684\u90E8\u5206\uFF0C\u4E0D\u662F\u5B8C\u6574\u4EA4\u4ED8\u3011";
168643
168782
  UNFINISHED_EMPTY = "\u3010\u4E0A\u4E00\u8F6E\u672A\u5B8C\u6210\u2014\u2014\u88AB\u4E2D\u65AD\u65F6\u8FD8\u6CA1\u6709\u4EA7\u51FA\u4EFB\u4F55\u6587\u5B57\u3011";
168783
+ RUNTIME_SESSION_ID_PLACEHOLDER_KEY = "runtimeSessionIdIsPlaceholder";
168644
168784
  }
168645
168785
  });
168646
168786
 
@@ -170890,7 +171030,7 @@ var init_service2 = __esm({
170890
171030
  try {
170891
171031
  await this.live.get(chatSessionId)?.kill?.();
170892
171032
  } catch (error2) {
170893
- const warning = `\u53D6\u6D88\u4FE1\u53F7\u5DF2\u8BB0\u5F55\uFF0C\u4F46 Runtime \u7EC8\u6B62\u8C03\u7528\u5931\u8D25\uFF1A${error2 instanceof Error ? error2.message : String(error2)}`.slice(0, 1e3);
171033
+ const warning = `\u53D6\u6D88\u4FE1\u53F7\u5DF2\u8BB0\u5F55\uFF0C\u4F46\u8FD0\u884C\u65F6\u7EC8\u6B62\u8C03\u7528\u5931\u8D25\uFF1A${error2 instanceof Error ? error2.message : String(error2)}`.slice(0, 1e3);
170894
171034
  await this.runStore.patchIfStatus(chatSessionId, ["interrupted"], {
170895
171035
  warnings: [...transitioned.warnings ?? [], warning],
170896
171036
  updatedAt: this.now()
@@ -171709,7 +171849,7 @@ var init_service2 = __esm({
171709
171849
  const executionHealth = runtimeError ? "degraded" : "healthy";
171710
171850
  const contentUsability = complete ? "usable" : "partial";
171711
171851
  const warnings = [
171712
- ...runtimeMessage ? [`Runtime \u5F02\u5E38\u9000\u51FA\uFF0C\u4F46\u5DF2\u4FDD\u7559\u6709\u5F15\u7528\u7684\u56DE\u7B54\uFF1A${runtimeMessage}`] : [],
171852
+ ...runtimeMessage ? [`\u8FD0\u884C\u65F6\u5F02\u5E38\u9000\u51FA\uFF0C\u4F46\u5DF2\u4FDD\u7559\u6709\u5F15\u7528\u7684\u56DE\u7B54\uFF1A${runtimeMessage}`] : [],
171713
171853
  ...!complete ? ["\u77E5\u8BC6\u7BA1\u7406\u5458\u56DE\u7B54\u7F3A\u5C11\u5B8C\u6574\u5B8C\u6210\u6807\u8BB0\uFF0C\u6309\u90E8\u5206\u5185\u5BB9\u8FD4\u56DE\u3002"] : []
171714
171854
  ];
171715
171855
  if (answerAttempt) {
@@ -199493,7 +199633,7 @@ var init_curator_skill = __esm({
199493
199633
  throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u5FC5\u987B\u662F\u5DF2\u6FC0\u6D3B\u7684 AI \u5458\u5DE5\uFF1A${config2.curatorActorId}`);
199494
199634
  }
199495
199635
  const binding = (await this.actors.listBindings()).find((item) => item.actorId === config2.curatorActorId && item.status === "active");
199496
- if (!binding) throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u5C1A\u672A\u7ED1\u5B9A\u53EF\u7528 Runtime\uFF1A${config2.curatorActorId}`);
199636
+ if (!binding) throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u5C1A\u672A\u7ED1\u5B9A\u53EF\u7528\u8FD0\u884C\u65F6\uFF1A${config2.curatorActorId}`);
199497
199637
  const currentConfig = await this.actors.latestConfig(config2.curatorActorId);
199498
199638
  const prompt2 = currentConfig?.prompt ?? "";
199499
199639
  const model = currentConfig?.model ?? "";
@@ -202956,9 +203096,10 @@ function registerDelegatedChildTurn(liveChat, childSessionId, session, deps = {}
202956
203096
  chatExit?.summary ?? (err instanceof Error ? err.message : String(err)),
202957
203097
  {
202958
203098
  ...chatExit?.reason ? { reason: chatExit.reason } : {},
202959
- ...chatExit?.detail ? { detail: chatExit.detail } : {}
203099
+ ...chatExit?.detail ? { detail: chatExit.detail } : {},
203100
+ ...chatExit?.cancelledBy ? { cancelledBy: chatExit.cancelledBy } : {}
202960
203101
  },
202961
- ctrl.stoppedByUser()
203102
+ ctrl.cancelCause()
202962
203103
  );
202963
203104
  try {
202964
203105
  ctrl.emit({ type: "error", text: text6, ...extra });
@@ -212311,9 +212452,10 @@ ${composed}`;
212311
212452
  chatExit?.summary ?? (err instanceof Error ? err.message : String(err)),
212312
212453
  {
212313
212454
  ...chatExit?.reason ? { reason: chatExit.reason } : {},
212314
- ...chatExit?.detail ? { detail: chatExit.detail } : {}
212455
+ ...chatExit?.detail ? { detail: chatExit.detail } : {},
212456
+ ...chatExit?.cancelledBy ? { cancelledBy: chatExit.cancelledBy } : {}
212315
212457
  },
212316
- live?.stoppedByUser() ?? false
212458
+ live?.cancelCause()
212317
212459
  );
212318
212460
  emit({ type: "error", text: errText2, ...errExtra });
212319
212461
  const errorOutcome = itemLedger.recordError(errText2, errExtra);
@@ -212385,7 +212527,7 @@ ${composed}`;
212385
212527
  return;
212386
212528
  }
212387
212529
  if (!opts.claudeBin) {
212388
- throw new ApiError(501, "CHAT_RUNTIME_NOT_CONFIGURED", "\u5373\u65F6\u5BF9\u8BDD\u672A\u63A5\u5165 actor runtime \u6D3E\u53D1\u94FE\u8DEF");
212530
+ throw new ApiError(501, "CHAT_RUNTIME_NOT_CONFIGURED", "\u5373\u65F6\u5BF9\u8BDD\u672A\u63A5\u5165 actor \u8FD0\u884C\u65F6\u6D3E\u53D1\u94FE\u8DEF");
212389
212531
  }
212390
212532
  const env = body2.actorId && opts.resolveActorEnv ? await opts.resolveActorEnv(body2.actorId) : {};
212391
212533
  let spawnCwd;
@@ -224483,13 +224625,21 @@ function wireRecoveredChatTurn(deps) {
224483
224625
  log3(`[chat-recovery] \u4F1A\u8BDD ${plan.chatSessionId} \u91CD\u6302\uFF1A\u8865\u53D1\u6709 ${droppedFrames} \u5E27\u6CA1\u63A5\u4F4F\uFF08run ${plan.runId}\uFF09\uFF0C\u672C\u8F6E\u6309\u300C\u8F93\u51FA\u4E0D\u5B8C\u6574\u300D\u6536\u5C3E${exitIsFailure ? "\uFF08\u9000\u51FA\u5E27\u672C\u8EAB\u4E5F\u662F\u5931\u8D25\uFF09" : "\uFF0C\u4E0D\u8BB0\u6210\u6210\u529F"}`);
224484
224626
  }
224485
224627
  if (failed) {
224628
+ const exitAttribution = exitIsFailure ? attributeChatExit(exitErrorText(info), {
224629
+ ...info.reason ? { reason: info.reason } : {},
224630
+ ...cancelCauseFromExitMessage(info.errorMessage) ? { cancelledBy: cancelCauseFromExitMessage(info.errorMessage) } : {}
224631
+ }, live.cancelCause()) : void 0;
224486
224632
  const errText2 = [
224487
- exitIsFailure ? exitErrorText(info) : void 0,
224633
+ exitAttribution?.text,
224488
224634
  droppedFrames > 0 ? outputGapNotice(droppedFrames) : void 0
224489
224635
  ].filter((t) => Boolean(t)).join("\n\n");
224490
- live.emit({ type: "error", text: errText2 });
224636
+ live.emit({ type: "error", text: errText2, ...exitAttribution?.extra.cancelledBy ? { cancelledBy: exitAttribution.extra.cancelledBy } : {} });
224491
224637
  const errReason = exitIsFailure ? info.reason : OUTPUT_GAP_ERROR_CODE;
224492
- const errorOutcome = itemLedger.recordError(errText2, { ...errReason ? { reason: errReason } : {} });
224638
+ const errExtra = {
224639
+ ...errReason ? { reason: errReason } : {},
224640
+ ...exitAttribution?.extra.cancelledBy ? { cancelledBy: exitAttribution.extra.cancelledBy } : {}
224641
+ };
224642
+ const errorOutcome = itemLedger.recordError(errText2, errExtra);
224493
224643
  live.publishServerItem({
224494
224644
  itemId: errorOutcome.itemId,
224495
224645
  itemType: "control",
@@ -224497,7 +224647,7 @@ function wireRecoveredChatTurn(deps) {
224497
224647
  version: errorOutcome.version,
224498
224648
  ord: errorOutcome.ord,
224499
224649
  status: "failed",
224500
- payload: { code: "error", note: errText2, ...errReason ? { reason: errReason } : {} }
224650
+ payload: { code: "error", note: errText2, ...errExtra }
224501
224651
  });
224502
224652
  }
224503
224653
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -224545,6 +224695,10 @@ function wireRecoveredChatTurn(deps) {
224545
224695
  live.emit({ type: "done", ...plan.runtimeSessionId ? { sessionId: plan.runtimeSessionId } : {} });
224546
224696
  live.finish(failed ? "error" : "done");
224547
224697
  deps.unregister();
224698
+ const nativeSessionMetadata = info.runtimeSessionId && info.runtimeSessionId !== plan.runtimeSessionId ? nativeRuntimeSessionMetadata(
224699
+ (await trace.getRun(plan.runId).catch(() => null))?.metadata,
224700
+ info.runtimeSessionId
224701
+ ) : void 0;
224548
224702
  enqueueTrace(async () => {
224549
224703
  await appender.append([{
224550
224704
  eventType: "run.finished",
@@ -224556,7 +224710,9 @@ function wireRecoveredChatTurn(deps) {
224556
224710
  ...runTerminalPatch(info, plan.startedAt, finishedAt),
224557
224711
  // 运行时自己退干净了,但平台没把输出接全——运行记录不许写「成功」(Bug 0123)。
224558
224712
  // 退出帧本身已经是失败时保持它的归因,只补一句缺口说明。
224559
- ...outputGapRunPatch(droppedFrames, exitIsFailure, info)
224713
+ ...outputGapRunPatch(droppedFrames, exitIsFailure, info),
224714
+ // 首轮占位会话号 → runtime 报回的原生号(Bug 0171)。
224715
+ ...nativeSessionMetadata ? { metadata: nativeSessionMetadata } : {}
224560
224716
  });
224561
224717
  if (info.usage) await settleRunUsage(trace, plan.runId);
224562
224718
  });
@@ -224573,12 +224729,14 @@ async function sweepDanglingChatTurns(deps) {
224573
224729
  let status;
224574
224730
  let completedAt;
224575
224731
  let reason;
224732
+ let exitReason = null;
224576
224733
  if (row.runId) {
224577
224734
  const run = await deps.getRun(row.runId).catch(() => null);
224578
224735
  if (run && run.status === "running") continue;
224579
224736
  status = run?.status === "succeeded" ? "done" : "error";
224580
224737
  completedAt = run?.endedAt ?? new Date(deps.now).toISOString();
224581
224738
  reason = run ? `run \u7EC8\u6001 ${run.status}` : "run \u7F3A\u5931";
224739
+ exitReason = run?.exitReason ?? null;
224582
224740
  } else {
224583
224741
  if (deps.now - Date.parse(row.createdAt) < (deps.noRunGraceMs ?? 10 * 6e4)) continue;
224584
224742
  status = "error";
@@ -224601,6 +224759,22 @@ async function sweepDanglingChatTurns(deps) {
224601
224759
  completedAt,
224602
224760
  ...finalParts.length ? { parts: finalParts } : {}
224603
224761
  });
224762
+ if (status === "error") {
224763
+ const items = deps.itemsFor?.(row.sessionId);
224764
+ if (items) {
224765
+ await recordChatTurnInterruption({
224766
+ items,
224767
+ sessionId: row.sessionId,
224768
+ turnId: await resolveChatItemsTurnId(items, row.id, fallbackTurnId(row.runId)),
224769
+ runId: row.runId,
224770
+ messageId: row.id,
224771
+ cause: chatInterruptCauseOf(exitReason),
224772
+ reason: exitReason ?? reason,
224773
+ now: () => completedAt,
224774
+ ...deps.log ? { log: deps.log } : {}
224775
+ });
224776
+ }
224777
+ }
224604
224778
  closed.push(row.id);
224605
224779
  deps.log?.(`[chat-sweep] \u884C ${row.id}\uFF08\u4F1A\u8BDD ${row.sessionId}\uFF09\u6536\u655B\u4E3A ${status}\uFF08${reason}\uFF09`);
224606
224780
  }
@@ -224617,6 +224791,8 @@ async function reconcileChatExitFrame(deps) {
224617
224791
  const meta = asObj3(run.metadata);
224618
224792
  if (meta?.dispatchId !== deps.dispatchId) continue;
224619
224793
  if (deps.ownedRunId?.(row.sessionId) === row.runId) return false;
224794
+ const metaRsid = typeof meta?.runtimeSessionId === "string" ? meta.runtimeSessionId : void 0;
224795
+ const nativeSessionMetadata = deps.info.runtimeSessionId && deps.info.runtimeSessionId !== metaRsid ? nativeRuntimeSessionMetadata(run.metadata, deps.info.runtimeSessionId) : void 0;
224620
224796
  const turn = await deps.getTurnForRun?.(row.sessionId, run.id);
224621
224797
  const outputsPart = deps.info.fileRestoreError ? void 0 : deps.outputsPartFor?.(row.sessionId);
224622
224798
  if (turn && turn.status !== "reserved" && turn.status !== "running") {
@@ -224638,6 +224814,9 @@ async function reconcileChatExitFrame(deps) {
224638
224814
  trace: deps.trace,
224639
224815
  ...deps.items ? { items: deps.items } : {},
224640
224816
  ...outputsPart ? { outputsPart } : {},
224817
+ // 轮已被清扫拍/用户 stop 收掉、退出帧才补发上来:run 若还挂 running,这就是把原生号
224818
+ // 写进运行记录的最后一次机会(这条路自己没有退出帧,所以由这里算好递进去)。
224819
+ ...nativeSessionMetadata ? { runMetadata: nativeSessionMetadata } : {},
224641
224820
  ...deps.log ? { log: deps.log } : {}
224642
224821
  });
224643
224822
  return true;
@@ -224717,7 +224896,6 @@ async function reconcileChatExitFrame(deps) {
224717
224896
  completedAt: finishedAt,
224718
224897
  ...finalParts.length ? { parts: finalParts } : {}
224719
224898
  });
224720
- const metaRsid = typeof meta?.runtimeSessionId === "string" ? meta.runtimeSessionId : void 0;
224721
224899
  const nextRuntimeSessionId = info.runtimeSessionId ?? metaRsid;
224722
224900
  await deps.chatStore.updateSession(row.sessionId, {
224723
224901
  ...nextRuntimeSessionId ? { runtimeSessionId: nextRuntimeSessionId } : {},
@@ -224738,7 +224916,9 @@ async function reconcileChatExitFrame(deps) {
224738
224916
  await deps.trace.appendEvents(row.runId, seqless.map((e, i) => ({ ...e, seq: baseSeq + i + 1 })));
224739
224917
  await deps.trace.updateRun(row.runId, {
224740
224918
  ...runTerminalPatch(info, run.startedAt, finishedAt),
224741
- ...outputGapRunPatch(droppedFrames, exitIsFailure, info)
224919
+ ...outputGapRunPatch(droppedFrames, exitIsFailure, info),
224920
+ // 首轮占位会话号 → runtime 报回的原生号(Bug 0171,判据见本函数上方那一段)。
224921
+ ...nativeSessionMetadata ? { metadata: nativeSessionMetadata } : {}
224742
224922
  });
224743
224923
  if (info.usage) await settleRunUsage(deps.trace, row.runId);
224744
224924
  } catch (err) {
@@ -224808,7 +224988,9 @@ async function sweepDanglingChatTurnsAcrossCompanies(deps) {
224808
224988
  listMessages: (sid) => stores.chatStore.listMessages(sid),
224809
224989
  updateMessage: (id, patch) => stores.chatStore.updateMessage(id, patch),
224810
224990
  ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {},
224811
- ...sessionOutputsScan ? { outputsPartFor: (sid) => outputsPartProvider(sessionOutputsScan, sid, companyId) } : {}
224991
+ ...sessionOutputsScan ? { outputsPartFor: (sid) => outputsPartProvider(sessionOutputsScan, sid, companyId) } : {},
224992
+ // Bug #0185:结局项要写进**这一家**的 `chat_items`(公司分 schema,写错家等于没写)。
224993
+ ...stores.items ? { itemsFor: () => stores.items } : {}
224812
224994
  });
224813
224995
  closed.push(...ids2);
224814
224996
  } catch (err) {
@@ -224863,7 +225045,8 @@ async function reconcileSettledChatTurn(deps) {
224863
225045
  if (run?.status === "running") {
224864
225046
  await deps.trace.updateRun(plan.runId, {
224865
225047
  status: turn.status,
224866
- endedAt: completedAt
225048
+ endedAt: completedAt,
225049
+ ...deps.runMetadata ? { metadata: deps.runMetadata } : {}
224867
225050
  });
224868
225051
  }
224869
225052
  }
@@ -224958,14 +225141,17 @@ var init_chat_recovery = __esm({
224958
225141
  "../server/src/chat-recovery.ts"() {
224959
225142
  "use strict";
224960
225143
  init_file_restore();
225144
+ init_continuation3();
224961
225145
  init_chat_parts();
224962
225146
  init_sink();
224963
225147
  init_resilient_appender();
224964
225148
  init_usage_ledger();
224965
225149
  init_chat_item_ledger();
225150
+ init_chat_interrupt_notice();
224966
225151
  init_chat_assistant_turn();
224967
225152
  init_src2();
224968
225153
  init_session_exit_outcome();
225154
+ init_chat_cancel_attribution();
224969
225155
  asObj3 = (v2) => v2 && typeof v2 === "object" && !Array.isArray(v2) ? v2 : void 0;
224970
225156
  partsOf = (parts) => Array.isArray(parts) ? parts.filter((p2) => p2 && typeof p2 === "object" && typeof p2.type === "string") : [];
224971
225157
  maxPartSeq = (parts) => parts.reduce((n, p2) => Math.max(n, typeof p2.seq === "number" ? p2.seq : 0), 0);
@@ -225259,6 +225445,7 @@ var init_daemon_hub = __esm({
225259
225445
  getNodeStatus;
225260
225446
  /** 握手被拒审计钩子。 */
225261
225447
  onHandshakeRejected;
225448
+ onSessionEvicted;
225262
225449
  /** 握手失败限流器(改动 3)。 */
225263
225450
  throttle;
225264
225451
  /** 外部可订阅的事件回调 */
@@ -225291,6 +225478,7 @@ var init_daemon_hub = __esm({
225291
225478
  this.resolveDataplane = opts.resolveDataplane;
225292
225479
  this.coldStartGraceMs = opts.coldStartGraceMs ?? LINK_WINDOWS.hubColdStartGraceMs;
225293
225480
  this.onHandshakeRejected = opts.onHandshakeRejected;
225481
+ this.onSessionEvicted = opts.onSessionEvicted;
225294
225482
  this.throttle = new HandshakeThrottle(opts.throttle);
225295
225483
  this.wss = new import_websocket_server.default({
225296
225484
  server,
@@ -225666,6 +225854,31 @@ var init_daemon_hub = __esm({
225666
225854
  }
225667
225855
  return ids2;
225668
225856
  }
225857
+ /**
225858
+ * 当前持有活数据面连接的会话**自报的 `jobArtifactId`**(Bug #0185)。
225859
+ *
225860
+ * 为什么 {@link liveSessionDispatchIds} 不够:那条线按 `dispatchId` 认领,而 **chat 道的运行账本
225861
+ * 以 artifactId 为键**——`run.id = chat-run:*` ≠ dispatchId(见本文件顶上那段与 `orphan-sweep.ts`
225862
+ * 的注释)。D2′ 之后 chat 会话搬进了独立会话进程:它不在 daemon 的 `activeSessions` 里
225863
+ * (那份只列 daemon 自己那张表),也不在 serve 的在途表里(重启即空),于是 serve 重启后
225864
+ * **四条认领线一条都不命中**,一条跑得好好的对话被 orphan-sweep 判孤儿闭合。
225865
+ *
225866
+ * 2026-09-21 生产(node-76fd271b454a,期间连续发版三次):三轮 chat run 全部
225867
+ * `status=orphaned / exitReason=sweep-orphan`,节点在线、会话进程还连着 hub。
225868
+ *
225869
+ * 会话在 `session_hello` 里已经把 `jobArtifactId` 报上来了(`session-process.ts` 那段注释写的
225870
+ * 就是这件事),这里只是把它取出来。与节点 hello 那条线同形、同语义,**只增不减**:
225871
+ * 命中只能阻止误闭合,永不导致误闭合。
225872
+ */
225873
+ liveSessionArtifactIds() {
225874
+ const ids2 = /* @__PURE__ */ new Set();
225875
+ for (const [dispatchId, ws] of this.sessionSockets) {
225876
+ if (ws.readyState !== import_websocket.default.OPEN) continue;
225877
+ const artifactId = this.sessionMeta.get(dispatchId)?.jobArtifactId;
225878
+ if (artifactId) ids2.add(artifactId);
225879
+ }
225880
+ return ids2;
225881
+ }
225669
225882
  /** 往会话进程直投一帧(kill / append_input / ping)。返回是否写进了 OPEN 的 socket。 */
225670
225883
  sendToSession(dispatchId, msg) {
225671
225884
  const ws = this.sessionSockets.get(dispatchId);
@@ -225680,7 +225893,13 @@ var init_daemon_hub = __esm({
225680
225893
  evictSession(dispatchId, reason) {
225681
225894
  const meta = this.sessionMeta.get(dispatchId);
225682
225895
  const sent = this.sendToSession(dispatchId, { type: "evict", sessionId: meta?.sessionId ?? dispatchId, reason });
225683
- if (sent) console.warn(`[hub] evict \u4F1A\u8BDD dispatch=${dispatchId} reason=${reason}`);
225896
+ if (sent) {
225897
+ console.warn(`[hub] evict \u4F1A\u8BDD dispatch=${dispatchId} reason=${reason}`);
225898
+ try {
225899
+ this.onSessionEvicted?.(dispatchId, reason);
225900
+ } catch {
225901
+ }
225902
+ }
225684
225903
  return sent;
225685
225904
  }
225686
225905
  /**
@@ -225829,16 +226048,60 @@ var init_daemon_hub = __esm({
225829
226048
  });
225830
226049
 
225831
226050
  // ../server/src/domains/actors/skill-fetcher.ts
226051
+ function indentWidth(line2) {
226052
+ return line2.length - line2.replace(/^[ \t]+/, "").length;
226053
+ }
226054
+ function scanFrontmatterEntries(fm) {
226055
+ const lines = fm.split("\n");
226056
+ const out = [];
226057
+ for (let i = 0; i < lines.length; i += 1) {
226058
+ const raw = lines[i];
226059
+ const text6 = raw.trim();
226060
+ if (!text6 || text6.startsWith("#")) continue;
226061
+ if (indentWidth(raw) > 0) continue;
226062
+ const colon = text6.indexOf(":");
226063
+ if (colon < 0) continue;
226064
+ const key2 = text6.slice(0, colon).trim();
226065
+ const rest = text6.slice(colon + 1).trim();
226066
+ if (BLOCK_SCALAR_HEAD.test(rest)) {
226067
+ const body2 = [];
226068
+ let j = i + 1;
226069
+ for (; j < lines.length; j += 1) {
226070
+ const next = lines[j];
226071
+ if (next.trim() === "") {
226072
+ body2.push("");
226073
+ continue;
226074
+ }
226075
+ if (indentWidth(next) === 0) break;
226076
+ body2.push(next);
226077
+ }
226078
+ i = j - 1;
226079
+ out.push({ key: key2, value: foldBlockScalar(body2), line: raw, block: true });
226080
+ continue;
226081
+ }
226082
+ out.push({ key: key2, value: unquoteScalar(rest), line: raw, block: false });
226083
+ }
226084
+ return out;
226085
+ }
226086
+ function unquoteScalar(value2) {
226087
+ const q2 = value2[0];
226088
+ if ((q2 === '"' || q2 === "'") && value2.length > 1 && value2.endsWith(q2)) return value2.slice(1, -1);
226089
+ return value2;
226090
+ }
226091
+ function foldBlockScalar(body2) {
226092
+ const widths = body2.filter((l) => l.trim()).map(indentWidth);
226093
+ const base = widths.length ? Math.min(...widths) : 0;
226094
+ return body2.map((l) => l.slice(base)).join(" ").replace(/[ \t\r\n]+/g, " ").trim();
226095
+ }
225832
226096
  function parseSkillFrontmatter(content3) {
225833
226097
  if (!content3.startsWith("---")) return { name: "", description: "" };
225834
226098
  const end = content3.indexOf("---", 3);
225835
226099
  if (end < 0) return { name: "", description: "" };
225836
226100
  let name = "";
225837
226101
  let description = "";
225838
- for (const line2 of content3.slice(3, end).split("\n")) {
225839
- const t = line2.trim();
225840
- if (t.startsWith("name:")) name = t.slice(5).trim().replace(/^["']|["']$/g, "");
225841
- else if (t.startsWith("description:")) description = t.slice(12).trim().replace(/^["']|["']$/g, "");
226102
+ for (const entry of scanFrontmatterEntries(content3.slice(3, end))) {
226103
+ if (entry.key === "name") name = entry.value;
226104
+ else if (entry.key === "description") description = entry.value;
225842
226105
  }
225843
226106
  return { name, description };
225844
226107
  }
@@ -225851,15 +226114,13 @@ function validateSkillMd(content3) {
225851
226114
  return "frontmatter \u7684\u67D0\u4E2A\u503C\u91CC\u542B `---`\uFF0C\u4F1A\u88AB\u89E3\u6790\u5668\u8BEF\u5F53\u6210\u7ED3\u675F\u6807\u8BB0\uFF08\u591A\u534A\u662F\u628A\u6574\u4EFD\u6587\u4EF6\u5185\u5BB9\u5F53\u6210\u4E86\u5B57\u6BB5\u503C\uFF09";
225852
226115
  }
225853
226116
  const fm = content3.slice(3, strictEnd);
225854
- for (const raw of fm.split("\n")) {
225855
- const line2 = raw.trim();
225856
- if (!line2 || line2.startsWith("#")) continue;
225857
- const colon = line2.indexOf(":");
225858
- if (colon < 0) continue;
225859
- const value2 = line2.slice(colon + 1).trim();
225860
- const q2 = value2[0];
225861
- if ((q2 === '"' || q2 === "'") && !(value2.length > 1 && value2.endsWith(q2))) {
225862
- return `frontmatter \u7684 \`${line2.slice(0, colon).trim()}\` \u503C\u6709\u672A\u95ED\u5408\u7684\u5F15\u53F7\u2014\u2014\u591A\u534A\u662F\u628A\u6574\u4EFD\u6587\u4EF6\u5185\u5BB9\u5F53\u6210\u4E86\u5B57\u6BB5\u503C\uFF08description \u53EA\u5199\u4E00\u53E5\u8BDD\uFF0C\u6B63\u6587\u5199\u5728 frontmatter \u4E4B\u540E\uFF09`;
226117
+ for (const entry of scanFrontmatterEntries(fm)) {
226118
+ if (entry.block) continue;
226119
+ const text6 = entry.line.trim();
226120
+ const rawValue = text6.slice(text6.indexOf(":") + 1).trim();
226121
+ const q2 = rawValue[0];
226122
+ if ((q2 === '"' || q2 === "'") && !(rawValue.length > 1 && rawValue.endsWith(q2))) {
226123
+ return `frontmatter \u7684 \`${entry.key}\` \u503C\u6709\u672A\u95ED\u5408\u7684\u5F15\u53F7\u2014\u2014\u591A\u534A\u662F\u628A\u6574\u4EFD\u6587\u4EF6\u5185\u5BB9\u5F53\u6210\u4E86\u5B57\u6BB5\u503C\uFF08description \u53EA\u5199\u4E00\u53E5\u8BDD\uFF0C\u6B63\u6587\u5199\u5728 frontmatter \u4E4B\u540E\uFF09`;
225863
226124
  }
225864
226125
  }
225865
226126
  const { name, description } = parseSkillFrontmatter(content3);
@@ -225876,7 +226137,7 @@ function toYamlDoubleQuoted(value2) {
225876
226137
  }
225877
226138
  async function fetchText(url, headers = {}) {
225878
226139
  const res = await fetch(url, { headers });
225879
- if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
226140
+ if (!res.ok) throw new SkillFetchHttpError(res.status, url);
225880
226141
  const buf = await res.arrayBuffer();
225881
226142
  if (buf.byteLength > currentFileLimits().skillFileBytes) throw new Error(`File too large (>${currentFileLimits().skillFileBytes} bytes): ${url}`);
225882
226143
  return new TextDecoder().decode(buf);
@@ -225931,6 +226192,21 @@ async function fetchFromClawHub(sourceUrl) {
225931
226192
  files: bundle.files
225932
226193
  };
225933
226194
  }
226195
+ async function repoIsReadable(owner, repo) {
226196
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
226197
+ try {
226198
+ const res = await fetch(url, { headers: { ...githubHeaders(), Accept: "application/vnd.github+json" } });
226199
+ return res.ok;
226200
+ } catch {
226201
+ return true;
226202
+ }
226203
+ }
226204
+ async function skillMdMissingMessage(owner, repo, skillDir) {
226205
+ if (await repoIsReadable(owner, repo)) {
226206
+ return `SKILL.md not found in ${owner}/${repo} at ${skillDir || "root"}`;
226207
+ }
226208
+ return process.env["GITHUB_TOKEN"]?.trim() ? `\u65E0\u6743\u8BBF\u95EE\u4ED3\u5E93 ${owner}/${repo}\uFF1A\u670D\u52A1\u7AEF\u914D\u7F6E\u7684 GitHub \u51ED\u636E\u8BFB\u4E0D\u5230\u5B83\uFF08\u79C1\u6709\u4ED3\u5E93\u9700\u8981\u628A\u8BE5\u51ED\u636E\u52A0\u8FDB\u4ED3\u5E93\u534F\u4F5C\u8005\uFF0C\u6216\u4ED3\u5E93\u540D\u5199\u9519\u4E86\uFF09` : `\u65E0\u6743\u8BBF\u95EE\u4ED3\u5E93 ${owner}/${repo}\uFF1A\u5B83\u4E0D\u662F\u516C\u5F00\u4ED3\u5E93\uFF0C\u800C\u670D\u52A1\u7AEF\u6CA1\u6709\u914D\u7F6E GitHub \u8BBF\u95EE\u51ED\u636E\uFF08GITHUB_TOKEN\uFF09`;
226209
+ }
225934
226210
  async function getDefaultBranch(owner, repo) {
225935
226211
  const info = await fetchGitHubApi(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`).catch(() => null);
225936
226212
  return info?.default_branch ?? "main";
@@ -225951,7 +226227,7 @@ async function fetchSkillDir(owner, repo, dir, branch) {
225951
226227
  const skillMdUrl = `${rawPrefix}/${dir ? dir + "/" : ""}SKILL.md`;
225952
226228
  let skillMd;
225953
226229
  try {
225954
- skillMd = await fetchText(skillMdUrl);
226230
+ skillMd = await fetchText(skillMdUrl, githubHeaders());
225955
226231
  } catch {
225956
226232
  return null;
225957
226233
  }
@@ -226033,7 +226309,7 @@ async function fetchFromGitHub(sourceUrl) {
226033
226309
  skillDir = segs.slice(2).join("/");
226034
226310
  }
226035
226311
  const result = await fetchSkillDir(owner, repo, skillDir, branch);
226036
- if (!result) throw new Error(`SKILL.md not found in ${owner}/${repo} at ${skillDir || "root"}`);
226312
+ if (!result) throw new Error(await skillMdMissingMessage(owner, repo, skillDir));
226037
226313
  const fm = parseSkillFrontmatter(result.files["SKILL.md"] ?? "");
226038
226314
  const slugBase = skillDir ? skillDir.split("/").pop().replace(/[^a-z0-9-]/g, "-") : repo.replace(/[^a-z0-9-]/g, "-");
226039
226315
  return {
@@ -226055,12 +226331,20 @@ async function fetchSkillFromSource(sourceUrl, kind) {
226055
226331
  throw new Error(`Unknown import kind: ${kind}`);
226056
226332
  }
226057
226333
  }
226058
- var SKILL_DESCRIPTION_MAX, Bundle;
226334
+ var BLOCK_SCALAR_HEAD, SKILL_DESCRIPTION_MAX, SkillFetchHttpError, Bundle;
226059
226335
  var init_skill_fetcher = __esm({
226060
226336
  "../server/src/domains/actors/skill-fetcher.ts"() {
226061
226337
  "use strict";
226062
226338
  init_file_limits2();
226339
+ BLOCK_SCALAR_HEAD = /^[|>][0-9+-]{0,2}[ \t]*(?:#.*)?$/;
226063
226340
  SKILL_DESCRIPTION_MAX = 600;
226341
+ SkillFetchHttpError = class extends Error {
226342
+ constructor(status, url) {
226343
+ super(`HTTP ${status} fetching ${url}`);
226344
+ this.status = status;
226345
+ this.name = "SkillFetchHttpError";
226346
+ }
226347
+ };
226064
226348
  Bundle = class {
226065
226349
  files = {};
226066
226350
  add(path42, content3) {
@@ -226703,6 +226987,37 @@ var init_service5 = __esm({
226703
226987
  await this.opts.store.removeBinding(actorId);
226704
226988
  await this.audit({ kind: "binding_change", actorId, by, at: this.now(), detail: { removed: true } });
226705
226989
  }
226990
+ /**
226991
+ * 运行时被删除时,把指向它的绑定一并撤掉(与 {@link dropRuntimeBindingOfDeletedAgent} 对称的另一头)。
226992
+ *
226993
+ * 为什么撤绑而不是把绑定留着标成失效(缺陷 0124,2026-09-21 R05 第 4 步实测):
226994
+ * 管理员删掉 `node-abe354908271` 上的 codex 后,绑在它上面的两个 Agent 的绑定行仍是
226995
+ * `codex / active`,指着一条已经不存在的运行时——运行时页要靠「这行还在不在」去过滤它,
226996
+ * 派单要靠 resolveBinding 去挡它,组织页要自己推「绑的那个还在不在」,**每个读者各挡一遍,
226997
+ * 漏一处就复发**。绑定表是「此刻该派到哪」的唯一事实源,不是历史账本:历史在 agent_runs /
226998
+ * traces / 审计里,撤绑一条都不动,所以 R05 要的「已有运行记录仍查得到」不受影响。
226999
+ * `BindingStatus` 只有 `active | paused` 两个值,拿 `paused`(人工暂停、可恢复)表示
227000
+ * 「目标没了」会让后来的读者误判,而加新值是 `packages/contract` 变更(红线 4)。
227001
+ *
227002
+ * 幂等:没有绑定就是空数组;`removeBinding` 对不存在的 actor 是 no-op。
227003
+ * @returns 被撤绑的 actorId 列表(调用方用来回给界面:删完影响了谁)。
227004
+ */
227005
+ async releaseBindingsOnRuntime(target, by) {
227006
+ const affected = (await this.opts.store.listBindings()).filter(
227007
+ (b2) => b2.nodeId === target.nodeId && b2.runtimeKind === target.runtimeKind
227008
+ );
227009
+ for (const b2 of affected) {
227010
+ await this.opts.store.removeBinding(b2.actorId);
227011
+ await this.audit({
227012
+ kind: "binding_change",
227013
+ actorId: b2.actorId,
227014
+ by,
227015
+ at: this.now(),
227016
+ detail: { removed: true, reason: "runtime_deleted", nodeId: b2.nodeId, runtimeKind: b2.runtimeKind }
227017
+ });
227018
+ }
227019
+ return affected.map((b2) => b2.actorId);
227020
+ }
226706
227021
  listBindings() {
226707
227022
  return this.opts.store.listBindings();
226708
227023
  }
@@ -227743,13 +228058,14 @@ ${input.description}
227743
228058
  }
227744
228059
  /**
227745
228060
  * 按绑定选出这个执行上下文里每个变量名用哪一条(替代旧路径的三层 for,规则逐条对齐):
227746
- * · 项目 > 这位 Agent > 全员;
228061
+ * · **这位 Agent > 项目 > 全员**(发起人 2026-09-21:项目是「小范围的组织默认值」,Agent 自己的配置最优先;
228062
+ * 2026-09-16 的 #1820 曾把项目排在最前,这次改回来,旧路径与这里一起改);
227747
228063
  * · 连接器没启用的那一条不算,由下一层顶上(同旧路径 `put` 的早退);
227748
228064
  * · 放不进进程环境的名字跳过;
227749
228065
  * · 选出的那条到期就整个不提供,**不回退**到下一层(PRD §7,同旧路径)。
227750
228066
  */
227751
228067
  async boundWinners(ctx, enabledConnectors) {
227752
- const rank = { all: 1, agent: 2, project: 3 };
228068
+ const rank = { all: 1, project: 2, agent: 3 };
227753
228069
  const best = /* @__PURE__ */ new Map();
227754
228070
  for (const row of await this.opts.store.resolveBoundVariables(ctx)) {
227755
228071
  if (row.connectorId && !enabledConnectors.has(row.connectorId)) continue;
@@ -227908,11 +228224,11 @@ ${input.description}
227908
228224
  /**
227909
228225
  * ADR 凭据保险库 §4.3 / ADR 0125 / **决策 0565 D4(2026-09-16 修订)**:派单/reveal 用的**合并后**
227910
228226
  * 变量条目(明文 + deliveryMode)。
227911
- * 优先级低→高:**global < personal < project**(同 key 时高层覆盖低层,**项目变量最优先**)。
227912
- * 即发起人 2026-09-16 定的口径:**当前项目变量 > Agent 个人变量 > 对该 Agent 生效的组织变量**。
227913
- * - 2026-09-16 之前是 `global < project < personal`(个人最优先)。改的是这三行 for 的先后,
227914
- * 不是 `put` 的条件——**显式空值照旧算「有值」**:高层写了空串就覆盖成空串,不回落低层
227915
- * (`put` 无条件 `merged.set`,别顺手加 `if (value)`,那会把「这个项目刻意置空」变成「读到组织值」)。
228227
+ * 优先级低→高:**global < project < personal**(同 key 时高层覆盖低层,**Agent 自己的那份最优先**)。
228228
+ * 即发起人 2026-09-21 定的口径:**Agent 个人变量 > 当前项目变量 > 对该 Agent 生效的组织变量**
228229
+ * (项目视为「一个小范围的组织」的默认值;2026-09-16 的 #1820 曾改成项目最优先,2026-09-21 改回来)。
228230
+ * - 改的是这三行 for 的先后,不是 `put` 的条件——**显式空值照旧算「有值」**:高层写了空串就覆盖成空串,
228231
+ * 不回落低层(`put` 无条件 `merged.set`,别顺手加 `if (value)`,那会把「刻意置空」变成「读到下层值」)。
227916
228232
  * - `opts.projectId` 给出时才叠加 project 层;缺省 = 只有 personal+global(存量行为)——
227917
228233
  * 没有项目上下文时这次改动不改变任何取值。
227918
228234
  * 连接器门禁与 resolveActorEnv 同源(未启用的连接器变量不注入)。
@@ -227960,12 +228276,12 @@ ${input.description}
227960
228276
  for (const row of rows) {
227961
228277
  if (row.scope === "global" && !row.actorId) put3(row);
227962
228278
  }
227963
- for (const row of rows) {
227964
- if (row.scope === "personal" && (!row.actorId || row.actorId === actorId)) put3(row);
227965
- }
227966
228279
  for (const row of projectRows) {
227967
228280
  if (row.scope === "project") put3(row);
227968
228281
  }
228282
+ for (const row of rows) {
228283
+ if (row.scope === "personal" && (!row.actorId || row.actorId === actorId)) put3(row);
228284
+ }
227969
228285
  return [...merged.values()].filter((e) => !expiredWinner.get(e.key));
227970
228286
  }
227971
228287
  /**
@@ -233378,7 +233694,7 @@ var init_activity2 = __esm({
233378
233694
  也是引擎做的(`activate.ts` 的 `tryToRun`)——管理者根本不知道这两件事,此前却写成
233379
233695
  「{管理者}已主动介入」「{管理者}正在重试」,把系统干的事指名道姓按到人头上。
233380
233696
  「后来谁介入了」由 `plan.node_retry` 那张卡说,那里的主语是**真按下重试的人**,说的是事实。 */
233381
- timedOut: (executor) => `${executor} Runtime \u8D85\u65F6\u3002`,
233697
+ timedOut: (executor) => `${executor} \u8FD0\u884C\u65F6\u8D85\u65F6\u3002`,
233382
233698
  failed: (executor) => `${executor} \u6267\u884C\u5931\u8D25\u3002`,
233383
233699
  /**
233384
233700
  * **零产出轮**:会话正常退出,一件东西都没交(引擎判卷不是 success)。与 {@link failed} 分开说——
@@ -233386,7 +233702,7 @@ var init_activity2 = __esm({
233386
233702
  */
233387
233703
  endedWithoutOutput: (executor, node2) => `${executor} \u672C\u8F6E\u7ED3\u675F\uFF0C\u6CA1\u6709\u4EA4\u51FA\u300A${node2}\u300B\u7684\u4EFB\u4F55\u6210\u679C\u3002`,
233388
233704
  /** 人点了重试——只在 `plan.node_retry` 用,`by` 是真按下重试的那个人。 */
233389
- retriedAfterTimeout: (executor, by) => `${executor} Runtime \u8D85\u65F6\uFF0C${by}\u5DF2\u4ECB\u5165\u91CD\u8BD5\u3002`,
233705
+ retriedAfterTimeout: (executor, by) => `${executor} \u8FD0\u884C\u65F6\u8D85\u65F6\uFF0C${by}\u5DF2\u4ECB\u5165\u91CD\u8BD5\u3002`,
233390
233706
  retriedAfterFailure: (executor, by) => `${executor} \u6267\u884C\u5931\u8D25\uFF0C${by}\u5DF2\u4ECB\u5165\u91CD\u8BD5\u3002`,
233391
233707
  resumed: (executor, node2) => `${executor} \u5DF2\u6062\u590D\uFF0C\u7EE7\u7EED\u5904\u7406\u300A${node2}\u300B\u3002`,
233392
233708
  killed: (node2) => `\u300A${node2}\u300B\u672C\u8F6E\u6267\u884C\u5DF2\u7EC8\u6B62\u3002`,
@@ -235790,6 +236106,22 @@ function distribute(total, weights) {
235790
236106
  return share;
235791
236107
  });
235792
236108
  }
236109
+ function splitSegmentCost(cost, model, d) {
236110
+ if (cost === null) return null;
236111
+ const zero = { input: 0, cacheRead: 0, cacheCreation: 0, output: 0, unsplit: 0 };
236112
+ if (cost === 0) return zero;
236113
+ const resolved = resolvedModel(model);
236114
+ const est = resolved ? estimateUsageCostBreakdown(resolved, {
236115
+ inputTokens: d.input,
236116
+ outputTokens: d.output,
236117
+ cacheReadTokens: d.cacheRead,
236118
+ cacheCreationTokens: d.cacheCreation
236119
+ }) : null;
236120
+ const weights = est ? [est.inputMicros, est.cacheReadMicros, est.cacheCreationMicros, est.outputMicros] : [0, 0, 0, 0];
236121
+ if (!weights.some((w2) => w2 > 0)) return { ...zero, unsplit: cost };
236122
+ const [input, cacheRead, cacheCreation, output] = distribute(cost, weights);
236123
+ return { input, cacheRead, cacheCreation, output, unsplit: 0 };
236124
+ }
235793
236125
  function unpricedSegmentCost(model) {
235794
236126
  return resolvedModel(model) === null ? 0 : null;
235795
236127
  }
@@ -235845,7 +236177,8 @@ function deriveRunSegments(run) {
235845
236177
  outputTokens: d.output,
235846
236178
  cacheReadTokens: d.cacheRead,
235847
236179
  cacheCreationTokens: d.cacheCreation,
235848
- costUsdMicros: costs[i] ?? null
236180
+ costUsdMicros: costs[i] ?? null,
236181
+ costParts: splitSegmentCost(costs[i] ?? null, point3.model ?? run.effectiveModel, d)
235849
236182
  }));
235850
236183
  }
235851
236184
  function deriveRounds(runs) {
@@ -236256,14 +236589,16 @@ function attributed(facts, relations) {
236256
236589
  const draftOf = (key2) => {
236257
236590
  let d = projectDrafts.get(key2);
236258
236591
  if (!d) {
236259
- d = { conv: new Cell(), tasks: [], unlinked: new Cell(), group: new Cell() };
236592
+ d = { convs: [], tasks: [], unlinked: new Cell(), group: new Cell() };
236260
236593
  projectDrafts.set(key2, d);
236261
236594
  }
236262
236595
  return d;
236263
236596
  };
236264
236597
  for (const [rootId, bySession] of convSessionCells) {
236265
- const d = draftOf(projectKey(relations.sessions.get(rootId).projectId));
236266
- for (const cell of bySession.values()) d.conv.addCell(cell);
236598
+ const rootCell = new Cell();
236599
+ for (const cell of bySession.values()) rootCell.addCell(cell);
236600
+ if (!rootCell.active) continue;
236601
+ draftOf(projectKey(relations.sessions.get(rootId).projectId)).convs.push({ rootId, cell: rootCell });
236267
236602
  }
236268
236603
  for (const workorderId of taskAmount.keys()) {
236269
236604
  draftOf(projectKey(relations.workorders.get(workorderId).projectId)).tasks.push(workorderId);
@@ -236272,15 +236607,26 @@ function attributed(facts, relations) {
236272
236607
  const projects = [];
236273
236608
  for (const [projectId2, d] of projectDrafts) {
236274
236609
  const rows = [];
236275
- if (d.conv.active) {
236276
- d.group.addCell(d.conv);
236277
- rows.push({ kind: "conversations", workorderId: null, title: null, createdAt: null, amount: d.conv.amount.toAmount(), flags: d.conv.flags.toFlags() });
236278
- }
236610
+ const convRows = d.convs.map(({ rootId, cell }) => {
236611
+ d.group.addCell(cell);
236612
+ const s2 = relations.sessions.get(rootId);
236613
+ return {
236614
+ kind: "conversation",
236615
+ sessionId: rootId,
236616
+ workorderId: null,
236617
+ title: s2.title || UNTITLED_CONVERSATION,
236618
+ createdAt: s2.createdAt,
236619
+ amount: cell.amount.toAmount(),
236620
+ flags: cell.flags.toFlags()
236621
+ };
236622
+ }).sort((a, b2) => byCreatedAt(a.createdAt, b2.createdAt) || (a.sessionId < b2.sessionId ? -1 : 1));
236623
+ rows.push(...convRows);
236279
236624
  const taskRows = d.tasks.map((workorderId) => {
236280
236625
  d.group.addCell(taskCells.get(workorderId));
236281
236626
  const wo = relations.workorders.get(workorderId);
236282
236627
  return {
236283
236628
  kind: "task",
236629
+ sessionId: null,
236284
236630
  workorderId,
236285
236631
  title: wo.title,
236286
236632
  createdAt: wo.createdAt,
@@ -236291,7 +236637,7 @@ function attributed(facts, relations) {
236291
236637
  rows.push(...taskRows);
236292
236638
  if (d.unlinked.active) {
236293
236639
  d.group.addCell(d.unlinked);
236294
- rows.push({ kind: "unlinked", workorderId: null, title: UNLINKED_ROW_TITLE, createdAt: null, amount: d.unlinked.amount.toAmount(), flags: d.unlinked.flags.toFlags() });
236640
+ rows.push({ kind: "unlinked", sessionId: null, workorderId: null, title: UNLINKED_ROW_TITLE, createdAt: null, amount: d.unlinked.amount.toAmount(), flags: d.unlinked.flags.toFlags() });
236295
236641
  }
236296
236642
  if (!d.group.active) continue;
236297
236643
  projects.push({
@@ -236314,6 +236660,7 @@ var init_statistics = __esm({
236314
236660
  "use strict";
236315
236661
  init_src();
236316
236662
  init_uncategorized_project();
236663
+ init_model_pricing();
236317
236664
  init_accounting();
236318
236665
  UNKNOWN_AGENT_NAME = "\u672A\u77E5 Agent";
236319
236666
  UNREGISTERED_RUNTIME_NAME = "\u672A\u767B\u8BB0\u8FD0\u884C\u65F6";
@@ -236323,38 +236670,60 @@ var init_statistics = __esm({
236323
236670
  BUCKET_KEYS = ["input", "output", "cacheRead", "cacheCreation"];
236324
236671
  AmountAcc = class {
236325
236672
  input = 0;
236326
- cache = 0;
236673
+ cacheRead = 0;
236674
+ cacheCreation = 0;
236327
236675
  output = 0;
236328
236676
  knownCost = 0;
236677
+ costParts = { input: 0, cacheRead: 0, cacheCreation: 0, output: 0, unsplit: 0 };
236329
236678
  unknownTokens = 0;
236330
236679
  knownSegments = 0;
236331
236680
  unknownSegments = 0;
236332
236681
  addSegment(s2) {
236333
- const cache = s2.cacheReadTokens + s2.cacheCreationTokens;
236334
236682
  this.input += s2.inputTokens;
236335
- this.cache += cache;
236683
+ this.cacheRead += s2.cacheReadTokens;
236684
+ this.cacheCreation += s2.cacheCreationTokens;
236336
236685
  this.output += s2.outputTokens;
236337
236686
  if (s2.costUsdMicros === null) {
236338
- this.unknownTokens += s2.inputTokens + cache + s2.outputTokens;
236687
+ this.unknownTokens += s2.inputTokens + s2.cacheReadTokens + s2.cacheCreationTokens + s2.outputTokens;
236339
236688
  this.unknownSegments += 1;
236340
236689
  } else {
236341
236690
  this.knownCost += s2.costUsdMicros;
236691
+ if (s2.costParts) this.addParts(s2.costParts);
236692
+ else this.costParts.unsplit += s2.costUsdMicros;
236342
236693
  this.knownSegments += 1;
236343
236694
  }
236344
236695
  }
236345
236696
  addAcc(o) {
236346
236697
  this.input += o.input;
236347
- this.cache += o.cache;
236698
+ this.cacheRead += o.cacheRead;
236699
+ this.cacheCreation += o.cacheCreation;
236348
236700
  this.output += o.output;
236349
236701
  this.knownCost += o.knownCost;
236702
+ this.addParts(o.costParts);
236350
236703
  this.unknownTokens += o.unknownTokens;
236351
236704
  this.knownSegments += o.knownSegments;
236352
236705
  this.unknownSegments += o.unknownSegments;
236353
236706
  }
236707
+ addParts(p2) {
236708
+ this.costParts.input += p2.input;
236709
+ this.costParts.cacheRead += p2.cacheRead;
236710
+ this.costParts.cacheCreation += p2.cacheCreation;
236711
+ this.costParts.output += p2.output;
236712
+ this.costParts.unsplit += p2.unsplit;
236713
+ }
236354
236714
  toAmount() {
236715
+ const cache = this.cacheRead + this.cacheCreation;
236355
236716
  return {
236356
- tokens: { input: this.input, cache: this.cache, output: this.output, total: this.input + this.cache + this.output },
236717
+ tokens: {
236718
+ input: this.input,
236719
+ cache,
236720
+ cacheRead: this.cacheRead,
236721
+ cacheCreation: this.cacheCreation,
236722
+ output: this.output,
236723
+ total: this.input + cache + this.output
236724
+ },
236357
236725
  costUsdMicros: this.knownCost,
236726
+ costParts: { ...this.costParts },
236358
236727
  costKnown: this.unknownSegments === 0 ? "full" : this.knownSegments === 0 ? "none" : "partial",
236359
236728
  costUnknownTokens: this.unknownTokens
236360
236729
  };
@@ -241008,8 +241377,9 @@ function nodesDomain(deps) {
241008
241377
  const runtime = (await deps.nodeStore.listRuntimes()).find((r) => r.id === runtimeId);
241009
241378
  const { nodeDeleted } = await deps.nodeStore.deleteRuntime(runtimeId);
241010
241379
  if (nodeDeleted && runtime) revokeNodeAccess(runtime.nodeId);
241380
+ const releasedActorIds = runtime ? await deps.releaseBindingsOnRuntime?.(req.auth.companyId, { nodeId: runtime.nodeId, runtimeKind: runtime.kind }, req.auth.actor) ?? [] : [];
241011
241381
  deps.onStatusChange?.();
241012
- return { status: 200, body: { ok: true } };
241382
+ return { status: 200, body: { ok: true, releasedActorIds } };
241013
241383
  });
241014
241384
  router.get("/api/runtimes/:runtimeId/models", async (req) => {
241015
241385
  const runtimeId = req.params["runtimeId"];
@@ -245325,7 +245695,7 @@ function throwWorkdirError(code2) {
245325
245695
  case "NODE_OFFLINE":
245326
245696
  throw new ApiError(503, "NODE_OFFLINE", "\u5DE5\u4F5C\u533A\u6240\u5728\u8282\u70B9\u5F53\u524D\u79BB\u7EBF\uFF0C\u65E0\u6CD5\u67E5\u770B\u6587\u4EF6");
245327
245697
  case "SESSION_NOT_BOUND":
245328
- throw new ApiError(503, "SESSION_NOT_BOUND", "\u8BE5 agent \u5F53\u524D\u672A\u7ED1\u5B9A runtime\uFF0C\u65E0\u5DE5\u4F5C\u533A\u53EF\u8BFB");
245698
+ throw new ApiError(503, "SESSION_NOT_BOUND", "\u8BE5 agent \u5F53\u524D\u672A\u7ED1\u5B9A\u8FD0\u884C\u65F6\uFF0C\u65E0\u5DE5\u4F5C\u533A\u53EF\u8BFB");
245329
245699
  case "TIMEOUT":
245330
245700
  throw new ApiError(504, "TIMEOUT", "\u8BF7\u6C42\u8282\u70B9\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5");
245331
245701
  case "NOT_FOUND":
@@ -245732,7 +246102,7 @@ function createChatSessionsDomain(opts) {
245732
246102
  }
245733
246103
  const startedAt = Date.parse(activeTurnRow.startedAt ?? activeTurnRow.reservedAt);
245734
246104
  const stale = Number.isFinite(startedAt) && Date.now() - startedAt >= LINK_WINDOWS.chatTurnPhaseStaleAfterMs;
245735
- return { turnId: activeTurnRow.id, phase: stale ? "interrupted" : "running" };
246105
+ return { turnId: activeTurnRow.id, phase: stale ? "recovering" : "running" };
245736
246106
  })();
245737
246107
  const snapshotEntries = [];
245738
246108
  let nextSinceVersion = sinceVersion;
@@ -250901,7 +251271,7 @@ var init_daemon_adapter = __esm({
250901
251271
  }
250902
251272
  async spawn(job) {
250903
251273
  if (job.binding?.nodeId) return this.opts.remote.spawn(job);
250904
- const msg = `\u62D2\u7EDD\u6D3E\u53D1\uFF1A${job.actor} \u65E0 active runtime \u7ED1\u5B9A\u2014\u2014\u4E0D\u56DE\u843D\u672C\u5730\uFF08\u5148\u5728\u5E73\u53F0\u4E3A\u8BE5\u5458\u5DE5\u914D\u7F6E runtime\uFF09`;
251274
+ const msg = `\u62D2\u7EDD\u6D3E\u53D1\uFF1A${job.actor} \u65E0\u53EF\u7528\u7684\u8FD0\u884C\u65F6\u7ED1\u5B9A\u2014\u2014\u4E0D\u56DE\u843D\u672C\u5730\uFF08\u5148\u5728\u5E73\u53F0\u4E3A\u8BE5\u5458\u5DE5\u914D\u7F6E\u8FD0\u884C\u65F6\uFF09`;
250905
251275
  console.warn(`[dispatch] ${msg}\uFF08artifact=${job.artifactId}\uFF09`);
250906
251276
  throw new Error(msg);
250907
251277
  }
@@ -271144,6 +271514,7 @@ __export(src_exports2, {
271144
271514
  BindingRouterAdapter: () => BindingRouterAdapter,
271145
271515
  BoardWatcher: () => BoardWatcher,
271146
271516
  CHAT_FOLDER_TASK_LABEL: () => CHAT_FOLDER_TASK_LABEL,
271517
+ CHAT_INTERRUPT_NOTICE: () => CHAT_INTERRUPT_NOTICE,
271147
271518
  CHAT_ITEMS_DOUBLE_WRITE_ENV: () => CHAT_ITEMS_DOUBLE_WRITE_ENV,
271148
271519
  CHAT_TURN_ORPHAN_GRACE_MS: () => CHAT_TURN_ORPHAN_GRACE_MS,
271149
271520
  COMPANY_CROSS_TENANT_PREFIXES: () => COMPANY_CROSS_TENANT_PREFIXES,
@@ -271277,6 +271648,7 @@ __export(src_exports2, {
271277
271648
  REJECTED_PROPOSALS_IN_TASK: () => REJECTED_PROPOSALS_IN_TASK,
271278
271649
  RETIRED_CD_BOT_ACTOR: () => RETIRED_CD_BOT_ACTOR,
271279
271650
  RETIRED_CD_ROLE: () => RETIRED_CD_ROLE,
271651
+ RUNTIME_SESSION_ID_PLACEHOLDER_KEY: () => RUNTIME_SESSION_ID_PLACEHOLDER_KEY,
271280
271652
  RUNTIME_START_FRAME_TYPES: () => RUNTIME_START_FRAME_TYPES,
271281
271653
  RUN_DEATH_PATHS: () => RUN_DEATH_PATHS,
271282
271654
  RUN_SETTLEMENT_CONVERGENCE_BUDGET_MS: () => RUN_SETTLEMENT_CONVERGENCE_BUDGET_MS,
@@ -271290,6 +271662,7 @@ __export(src_exports2, {
271290
271662
  SHARED_GRAPH_BODY: () => SHARED_GRAPH_BODY,
271291
271663
  SKILL_DESCRIPTION_MAX: () => SKILL_DESCRIPTION_MAX,
271292
271664
  STATUS_MARKER: () => STATUS_MARKER,
271665
+ SkillFetchHttpError: () => SkillFetchHttpError,
271293
271666
  TenancyVisibilityInputError: () => TenancyVisibilityInputError,
271294
271667
  TraceService: () => TraceService,
271295
271668
  TraceStoreSink: () => TraceStoreSink,
@@ -271349,6 +271722,8 @@ __export(src_exports2, {
271349
271722
  chatBroadcastEnabledFromEnv: () => chatBroadcastEnabledFromEnv,
271350
271723
  chatDelegationEnabledFromEnv: () => chatDelegationEnabledFromEnv,
271351
271724
  chatDispatchScope: () => chatDispatchScope,
271725
+ chatInterruptCauseOf: () => chatInterruptCauseOf,
271726
+ chatInterruptItemId: () => chatInterruptItemId,
271352
271727
  chatItemsDoubleWriteEnabled: () => chatItemsDoubleWriteEnabled,
271353
271728
  chatNodeChanged: () => chatNodeChanged,
271354
271729
  chatRunLedgerFields: () => chatRunLedgerFields,
@@ -271539,6 +271914,7 @@ __export(src_exports2, {
271539
271914
  memberNameResolver: () => memberNameResolver,
271540
271915
  migrateDefaultCompany: () => migrateDefaultCompany,
271541
271916
  mirrorDirFor: () => mirrorDirFor,
271917
+ nativeRuntimeSessionMetadata: () => nativeRuntimeSessionMetadata,
271542
271918
  needsClosureReport: () => needsClosureReport,
271543
271919
  nodeConclusionsFromSnapshot: () => nodeConclusionsFromSnapshot,
271544
271920
  nodeCoordinatorFactsFromSnapshot: () => nodeCoordinatorFactsFromSnapshot,
@@ -271565,6 +271941,7 @@ __export(src_exports2, {
271565
271941
  parseZhizengzengMarkdown: () => parseZhizengzengMarkdown,
271566
271942
  participatingTaskCount: () => participatingTaskCount,
271567
271943
  pickToolVersions: () => pickToolVersions,
271944
+ placeholderRuntimeSessionFlag: () => placeholderRuntimeSessionFlag,
271568
271945
  planChatContinuation: () => planChatContinuation,
271569
271946
  planChatTurnRecovery: () => planChatTurnRecovery,
271570
271947
  planChatTurnRecoveryAcrossCompanies: () => planChatTurnRecoveryAcrossCompanies,
@@ -271601,6 +271978,7 @@ __export(src_exports2, {
271601
271978
  reconcileTerminalRuns: () => reconcileTerminalRuns,
271602
271979
  recordChatRuntimeIdentity: () => recordChatRuntimeIdentity,
271603
271980
  recordChatSessionExit: () => recordChatSessionExit,
271981
+ recordChatTurnInterruption: () => recordChatTurnInterruption,
271604
271982
  recordDeliveredOutputHandoff: () => recordDeliveredOutputHandoff,
271605
271983
  recordFinalUsagePoint: () => recordFinalUsagePoint,
271606
271984
  recordOtlpUsageCheckpoint: () => recordOtlpUsageCheckpoint,
@@ -271645,6 +272023,7 @@ __export(src_exports2, {
271645
272023
  runtimeDriftBlock: () => runtimeDriftBlock,
271646
272024
  safeAttachmentName: () => safeAttachmentName,
271647
272025
  sanitizeSkillDir: () => sanitizeSkillDir,
272026
+ scanFrontmatterEntries: () => scanFrontmatterEntries,
271648
272027
  scanSessionOutputs: () => scanSessionOutputs,
271649
272028
  scanZeroOutputBreaker: () => scanZeroOutputBreaker,
271650
272029
  scoreKnowledgeChunk: () => scoreKnowledgeChunk,
@@ -271741,6 +272120,7 @@ var init_src11 = __esm({
271741
272120
  init_recovery_plan();
271742
272121
  init_chat_attachment_files();
271743
272122
  init_live_chat();
272123
+ init_chat_interrupt_notice();
271744
272124
  init_chat_recovery();
271745
272125
  init_chat_system_message();
271746
272126
  init_stable_chat_dispatch_ids();
@@ -272263,12 +272643,14 @@ var DiscussionService = class {
272263
272643
  decisions: []
272264
272644
  }));
272265
272645
  }
272266
- /** Chat title is a retryable projection of committed content, never of a model draft. */
272646
+ /** Chat title is a retryable projection of committed content, never of a model draft.
272647
+ * Projected once per conversation: later revisions (regeneration or page edits) keep the chat
272648
+ * title stable so users can find the conversation again, and never overwrite a manual rename. */
272267
272649
  async syncDiscussionTitle(sessionId) {
272268
272650
  await this.store.transaction(async (tx) => {
272269
272651
  const discussion = await tx.get("discussion", sessionId);
272270
272652
  const job = await tx.get("job", sessionId);
272271
- if (!discussion || !job || job.value.titleSyncedRevision === discussion.revision) return;
272653
+ if (!discussion || !job || job.value.titleSyncedRevision !== void 0) return;
272272
272654
  const session = await this.chat.getSession(sessionId);
272273
272655
  if (!session || projectScope(session) !== discussion.value.record.project_id || session.humanActorId !== discussion.value.userId || session.aiActorId !== discussion.value.agentId) return;
272274
272656
  if (session.title !== discussion.value.record.title) await this.chat.updateSession(sessionId, { title: discussion.value.record.title });
@@ -273401,7 +273783,9 @@ init_src4();
273401
273783
 
273402
273784
  // ../cli/src/chat-exit.ts
273403
273785
  var fs31 = __toESM(require("node:fs"), 1);
273786
+ init_src();
273404
273787
  init_src7();
273788
+ init_chat_cancel_attribution();
273405
273789
  function transcriptTail(ref2) {
273406
273790
  try {
273407
273791
  const stat4 = fs31.statSync(ref2);
@@ -273434,6 +273818,7 @@ var AUTH_RE2 = /\b401\b|\b403\b|unauthori[sz]ed|authenticat|oauth|invalid[_ -]?(
273434
273818
  var MODEL_RE = /\bmodel\b[^\n]{0,120}?\b(?:not found|not exist|does not exist|not available|not supported|unavailable|unknown)\b|\b(?:unknown|invalid|unsupported) model\b|model_not_found/i;
273435
273819
  var UNREACHABLE_RE2 = /不在线|socket 非 OPEN|帧未送达|失联/;
273436
273820
  function failureKind(reason, line2) {
273821
+ if (line2.startsWith(SESSION_STARTUP_FAILED_PREFIX)) return "startup-failed";
273437
273822
  switch (reason) {
273438
273823
  case "auth-error":
273439
273824
  return "auth";
@@ -273476,6 +273861,8 @@ function failureSummary(kind, ctx) {
273476
273861
  return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD9\u4E2A\u8FD0\u884C\u65F6\u7528\u4E0D\u4E86\u5F53\u524D\u914D\u7F6E\u7684\u6A21\u578B\u3002\u8BF7${chatRebindHint("\u6362\u4E00\u4E2A\u53EF\u7528\u7684\u6A21\u578B")}\u540E\u518D\u53D1\u4E00\u6B21\u3002`;
273477
273862
  case "timeout":
273478
273863
  return "\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD9\u4E00\u8F6E\u8FD0\u884C\u8D85\u8FC7\u4E86\u65F6\u95F4\u4E0A\u9650\uFF0C\u5DF2\u88AB\u7EC8\u6B62\u3002\u53EF\u4EE5\u628A\u4EFB\u52A1\u62C6\u5C0F\u4E00\u4E9B\u518D\u53D1\u3002";
273864
+ case "startup-failed":
273865
+ return `\u5BF9\u8BDD\u5931\u8D25\uFF1A${ctx.readable || SESSION_STARTUP_FAILED_PREFIX}\u3002\u8FD9\u4E00\u8F6E\u6CA1\u6709\u5F00\u59CB\u8FD0\u884C\uFF0C\u53EF\u4EE5\u76F4\u63A5\u518D\u53D1\u4E00\u6B21\uFF1B\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\uFF0C\u591A\u534A\u662F\u90A3\u53F0\u8282\u70B9\u8D1F\u8F7D\u8FC7\u9AD8\uFF0C\u8BF7\u7BA1\u7406\u5458\u7ED9\u5B83\u51CF\u8D1F\u6216${chatRebindHint("\u6362\u4E00\u4E2A\u8FD0\u884C\u65F6")}\u3002`;
273479
273866
  case "output-limit":
273480
273867
  return "\u5BF9\u8BDD\u5931\u8D25\uFF1A\u6A21\u578B\u8F93\u51FA\u8D85\u51FA\u957F\u5EA6\u4E0A\u9650\uFF0C\u56DE\u590D\u88AB\u622A\u65AD\u4E86\u3002\u53EF\u4EE5\u8BA9\u5B83\u5206\u51E0\u6B21\u8F93\u51FA\uFF0C\u6216\u628A\u4EFB\u52A1\u62C6\u5C0F\u4E00\u4E9B\u518D\u53D1\u3002";
273481
273868
  case "unknown":
@@ -273493,8 +273880,10 @@ function buildChatExitError(info) {
273493
273880
  if (!detail && info.transcriptRef) detail = transcriptTail(info.transcriptRef);
273494
273881
  }
273495
273882
  let summary;
273883
+ let cancelledBy;
273496
273884
  if (info.reason === "cancelled") {
273497
- summary = "\u5BF9\u8BDD\u5DF2\u4E2D\u65AD";
273885
+ cancelledBy = cancelCauseFromExitMessage(info.errorMessage);
273886
+ summary = chatCancelNote(cancelledBy);
273498
273887
  } else {
273499
273888
  const firstErrorLine = (info.errorMessage?.split("\n").map((line2) => line2.trim()).find(Boolean) ?? "").replace(INTERNAL_PREFIX_RE, "");
273500
273889
  const readable = firstErrorLine ? clipLine(providerMessage(firstErrorLine) ?? firstErrorLine) : "";
@@ -273508,6 +273897,7 @@ function buildChatExitError(info) {
273508
273897
  const chatExit = {
273509
273898
  ...info.reason ? { reason: info.reason } : {},
273510
273899
  summary,
273900
+ ...cancelledBy ? { cancelledBy } : {},
273511
273901
  ...detail ? { detail: `${genericLine}
273512
273902
 
273513
273903
  ${detail}`.slice(0, 4200) } : {}
@@ -275525,7 +275915,7 @@ async function startServe(opts) {
275525
275915
  }
275526
275916
  const binding = await registryStore.getBinding(agentId);
275527
275917
  if (!binding || binding.status !== "active") {
275528
- return { ok: false, reason: "agent_runtime_required", error: `\u6267\u884C\u4EBA\u672A\u7ED1\u5B9A runtime\uFF1A${agentId}` };
275918
+ return { ok: false, reason: "agent_runtime_required", error: `\u6267\u884C\u4EBA\u672A\u7ED1\u5B9A\u8FD0\u884C\u65F6\uFF1A${agentId}` };
275529
275919
  }
275530
275920
  return { ok: true };
275531
275921
  }
@@ -276354,10 +276744,10 @@ async function startServe(opts) {
276354
276744
  const preflightKnowledgeConnector = async (connectorSlug, actorId) => {
276355
276745
  const label = connectorDisplayName(connectorSlug);
276356
276746
  const binding = await actors.service.resolveBinding()(actorId);
276357
- if (!binding) throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u5C1A\u672A\u7ED1\u5B9A\u53EF\u7528 Runtime\uFF1A${actorId}`);
276747
+ if (!binding) throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u5C1A\u672A\u7ED1\u5B9A\u53EF\u7528\u8FD0\u884C\u65F6\uFF1A${actorId}`);
276358
276748
  const node2 = hub?.connectedDaemons().find((item) => item.daemonId === binding.nodeId);
276359
276749
  if (!node2) throw new Error(`${label} Connector \u672A\u5C31\u7EEA\uFF1A\u77E5\u8BC6\u7BA1\u7406\u5458\u7ED1\u5B9A\u8282\u70B9 ${binding.nodeId} \u5F53\u524D\u4E0D\u5728\u7EBF`);
276360
- if (!node2.meta.adapters.includes(binding.runtimeKind)) throw new Error(`${label} Connector \u672A\u5C31\u7EEA\uFF1A\u8282\u70B9\u672A\u4E0A\u62A5 Runtime ${binding.runtimeKind}`);
276750
+ if (!node2.meta.adapters.includes(binding.runtimeKind)) throw new Error(`${label} Connector \u672A\u5C31\u7EEA\uFF1A\u8282\u70B9\u672A\u4E0A\u62A5\u8FD0\u884C\u65F6 ${binding.runtimeKind}`);
276361
276751
  const readiness = node2.meta.connectorReadiness?.find((item) => item.slug === connectorSlug);
276362
276752
  if (!readiness?.ready) {
276363
276753
  throw new Error(`${label} Connector \u672A\u5C31\u7EEA\uFF1A${readiness?.error ?? "\u8282\u70B9\u672A\u4E0A\u62A5 wrapper/\u968F\u5305\u547D\u4EE4\u8D44\u4EA7\u81EA\u68C0\uFF0C\u8BF7\u5347\u7EA7\u5E76\u91CD\u542F node-daemon"}`);
@@ -276370,10 +276760,10 @@ async function startServe(opts) {
276370
276760
  throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u5FC5\u987B\u662F\u5DF2\u6FC0\u6D3B\u7684 AI \u5458\u5DE5\uFF1A${actorId}`);
276371
276761
  }
276372
276762
  const binding = await actors.service.resolveBinding()(actorId);
276373
- if (!binding) throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u5C1A\u672A\u7ED1\u5B9A\u53EF\u7528 Runtime\uFF1A${actorId}`);
276763
+ if (!binding) throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u5C1A\u672A\u7ED1\u5B9A\u53EF\u7528\u8FD0\u884C\u65F6\uFF1A${actorId}`);
276374
276764
  const node2 = hub?.connectedDaemons().find((item) => item.daemonId === binding.nodeId);
276375
276765
  if (!node2) throw new Error(`\u77E5\u8BC6\u7BA1\u7406\u5458\u7ED1\u5B9A\u8282\u70B9 ${binding.nodeId} \u5F53\u524D\u4E0D\u5728\u7EBF`);
276376
- if (!node2.meta.adapters.includes(binding.runtimeKind)) throw new Error(`\u8282\u70B9\u672A\u4E0A\u62A5 Runtime ${binding.runtimeKind}`);
276766
+ if (!node2.meta.adapters.includes(binding.runtimeKind)) throw new Error(`\u8282\u70B9\u672A\u4E0A\u62A5\u8FD0\u884C\u65F6 ${binding.runtimeKind}`);
276377
276767
  if (!chatRemoteAdapter) throw new Error("node-gateway \u5C1A\u672A\u5C31\u7EEA");
276378
276768
  };
276379
276769
  const knowledgeProvisioner = new KnowledgeCuratorProvisioner(actors.service, (connectorSlug, skillId) => createConnectorBySlug(connectorSlug)?.embeddedSkills?.find((skill) => skill.id === skillId) ?? null);
@@ -276843,6 +277233,12 @@ async function startServe(opts) {
276843
277233
  const [roster, bindings] = await Promise.all([service.listActors({ kind: "agent" }), service.listBindings()]);
276844
277234
  return { actors: roster, bindings };
276845
277235
  },
277236
+ // 缺陷 0124:删掉运行时后,绑在它上面的 Agent 不该继续挂在一条已经不存在的运行时上。
277237
+ // 与 actorBindings 同一条解析路径(按请求公司拿这家的员工域),写侧落审计。
277238
+ releaseBindingsOnRuntime: async (companyId, target, by) => {
277239
+ const { service } = await actors.resolveCtx(companyId ?? defaultCompanyId);
277240
+ return service.releaseBindingsOnRuntime(target, by);
277241
+ },
276846
277242
  nodeTokens,
276847
277243
  enrollTokens,
276848
277244
  nodeStore,
@@ -277736,6 +278132,11 @@ async function startServe(opts) {
277736
278132
  ...chatRunLedgerFields({ executionProfile, chatSessionId, chatTurnId }),
277737
278133
  metadata: {
277738
278134
  runtimeSessionId,
278135
+ /* Bug 0102 / 0171:这个会话号是平台现编的占位 id 吗(首轮手里没有 runtime 的号)。
278136
+ 收尾时据它决定要不要换成 runtime 报回来的原生号——**正常收尾路与恢复路读的是同一位**:
278137
+ 恢复路由另一个进程收尾,`resumeRuntimeSession` 这个闭包变量随旧 serve 一起没了,
278138
+ 判据只能落在持久事实上(见 `placeholderRuntimeSessionFlag`)。 */
278139
+ ...placeholderRuntimeSessionFlag(!resumeRuntimeSession),
277739
278140
  nodeId: target.nodeId,
277740
278141
  runtimeKind: target.runtimeKind,
277741
278142
  companyId: runCompanyId,
@@ -278001,7 +278402,15 @@ async function startServe(opts) {
278001
278402
  ...requiredConnectors.length > 0 ? { requiredConnectorSlugs: requiredConnectors } : {}
278002
278403
  });
278003
278404
  const chatJobKey = `chat::${artifactId}`;
278004
- chatLiveSessions.set(chatJobKey, { artifactId, sessionId: traceRunId, dispatchId: handle.id, kill: () => handle.kill() });
278405
+ chatLiveSessions.set(chatJobKey, {
278406
+ artifactId,
278407
+ sessionId: traceRunId,
278408
+ dispatchId: handle.id,
278409
+ kill: () => handle.kill(),
278410
+ ...chatSessionId ? { markCancelCause: (cause) => {
278411
+ liveChat.markCancelCause(chatSessionId, cause);
278412
+ } } : {}
278413
+ });
278005
278414
  chatRunByDispatch.set(handle.id, { runId: traceRunId, companyId: runCompanyId });
278006
278415
  enqueueTrace(async () => {
278007
278416
  const ts3 = await traceStoreFor(runCompanyId);
@@ -278067,12 +278476,9 @@ async function startServe(opts) {
278067
278476
  const effectiveModel = info.model ?? chatModel ?? null;
278068
278477
  const modelSource = info.model ? "runtime" : chatModelSource;
278069
278478
  let nativeSessionMetadata;
278070
- if (!resumeRuntimeSession && info.runtimeSessionId && info.runtimeSessionId !== runtimeSessionId) {
278479
+ if (info.runtimeSessionId && info.runtimeSessionId !== runtimeSessionId) {
278071
278480
  const run = await (await traceStoreFor(runCompanyId)).getRun(traceRunId).catch(() => null);
278072
- const base = run?.metadata && typeof run.metadata === "object" && !Array.isArray(run.metadata) ? run.metadata : void 0;
278073
- if (base?.["runtimeSessionId"] === runtimeSessionId) {
278074
- nativeSessionMetadata = { ...base, runtimeSessionId: info.runtimeSessionId };
278075
- }
278481
+ nativeSessionMetadata = nativeRuntimeSessionMetadata(run?.metadata, info.runtimeSessionId);
278076
278482
  }
278077
278483
  await traceAppender.update({
278078
278484
  status: classifySessionExitStatus(info),
@@ -278226,6 +278632,18 @@ async function startServe(opts) {
278226
278632
  // getNode 无 status 过滤,返回 deleted 行;查询失败时 verifyClient 侧默认拒绝(绝不放行)。
278227
278633
  getNodeStatus: async (nodeId) => (await nodeStore.getNode(nodeId))?.status ?? null,
278228
278634
  throttle: handshakeThrottleOpts,
278635
+ /* 驱逐留痕(ADR 0600 扩档):会话自尽后报回来的只是个光秃秃的 `cancelled`,
278636
+ 「这台机器上的这个会话被换下了」只有这一帧知道。chat 轮据此写「执行这一轮的机器断开了连接」。 */
278637
+ onSessionEvicted: (dispatchId) => {
278638
+ for (const c of chatLiveSessions.values()) {
278639
+ if (c.dispatchId !== dispatchId) continue;
278640
+ try {
278641
+ c.markCancelCause?.("node-disconnected");
278642
+ } catch {
278643
+ }
278644
+ break;
278645
+ }
278646
+ },
278229
278647
  // 改动 2/3 观测:deleted / unknown / status-error / rate-limited / invalid-token 分类可见,供事后归因。
278230
278648
  onHandshakeRejected: (rej) => {
278231
278649
  console.warn(
@@ -278551,10 +278969,12 @@ async function startServe(opts) {
278551
278969
  storesFor: async (companyId) => {
278552
278970
  const turns = await chatTurnsFor(companyId);
278553
278971
  const messages = await chatStoreFor(companyId);
278972
+ const items = await chatItemsFor(companyId).catch(() => void 0);
278554
278973
  return {
278555
278974
  turns,
278556
278975
  ...assistantRowRecovery(messages, {
278557
- outputsPartFor: (sid) => outputsPartProvider(sessionOutputsScan, sid, companyId)
278976
+ outputsPartFor: (sid) => outputsPartProvider(sessionOutputsScan, sid, companyId),
278977
+ ...items ? { itemsFor: () => items } : {}
278558
278978
  })
278559
278979
  };
278560
278980
  },
@@ -279857,7 +280277,7 @@ async function startServe(opts) {
279857
280277
  const quote = errorMessage2?.trim().slice(0, 300);
279858
280278
  if (reason === "rate-limit") {
279859
280279
  const hint = extractResetHint(errorMessage2);
279860
- return `**\u5B83\u7684\u989D\u5EA6\u88AB\u6253\u6EE1\u4E86**${hint ? `\uFF08runtime \u539F\u8BDD\uFF1A\`${hint}\`\uFF09` : ""}\u2014\u2014\u989D\u5EA6\u5230\u70B9\u4F1A\u81EA\u5DF1\u6062\u590D\uFF0C**\u4E0D\u9700\u8981\u4F60\u4FEE\u4EFB\u4F55\u4E1C\u897F**\uFF1B\u4E0D\u60F3\u7B49\u5C31\u7ED9\u8FD9\u4E2A\u5458\u5DE5\u6362\u4E2A runtime\u3002` + (quote ? `
280280
+ return `**\u5B83\u7684\u989D\u5EA6\u88AB\u6253\u6EE1\u4E86**${hint ? `\uFF08\u8FD0\u884C\u65F6\u539F\u8BDD\uFF1A\`${hint}\`\uFF09` : ""}\u2014\u2014\u989D\u5EA6\u5230\u70B9\u4F1A\u81EA\u5DF1\u6062\u590D\uFF0C**\u4E0D\u9700\u8981\u4F60\u4FEE\u4EFB\u4F55\u4E1C\u897F**\uFF1B\u4E0D\u60F3\u7B49\u5C31\u7ED9\u8FD9\u4E2A\u5458\u5DE5\u6362\u4E2A\u8FD0\u884C\u65F6\u3002` + (quote ? `
279861
280281
  > ${quote}` : "");
279862
280282
  }
279863
280283
  const label = {
@@ -280226,7 +280646,7 @@ ${nodeFault}` : "");
280226
280646
  if (horizon === "short" && s2.sinceMs < quotaStarvationMs * 2) continue;
280227
280647
  const hours = Math.round(s2.sinceMs / 36e5);
280228
280648
  const hint = extractResetHint(s2.lastErrorMessage);
280229
- const when = hint ? `runtime \u539F\u8BDD\uFF1A\`${hint}\`` : "runtime \u6CA1\u8BF4\u4EC0\u4E48\u65F6\u5019\u6062\u590D";
280649
+ const when = hint ? `\u8FD0\u884C\u65F6\u539F\u8BDD\uFF1A\`${hint}\`` : "\u8FD0\u884C\u65F6\u6CA1\u8BF4\u4EC0\u4E48\u65F6\u5019\u6062\u590D";
280230
280650
  const longNote = horizon === "long" ? "**\u770B\u65F6\u95F4\u662F\u8DE8\u5929\u7684\u989D\u5EA6\u7A97\u53E3\uFF0C\u7B49\u4E0D\u8D77**\u2014\u2014" : "";
280231
280651
  await escalateHold(
280232
280652
  s2.artifactId,
@@ -280471,7 +280891,13 @@ ${nodeFault}` : "");
280471
280891
  liveness: (sessionId) => probe.liveness(sessionId),
280472
280892
  kill: (jobKey) => {
280473
280893
  const c = chatLiveSessions.get(jobKey);
280474
- if (c) return Promise.resolve(c.kill()).then(() => true).catch(() => false);
280894
+ if (c) {
280895
+ try {
280896
+ c.markCancelCause?.("no-output");
280897
+ } catch {
280898
+ }
280899
+ return Promise.resolve(c.kill()).then(() => true).catch(() => false);
280900
+ }
280475
280901
  return dispatcherByJob.get(jobKey)?.killSession(jobKey) ?? Promise.resolve(false);
280476
280902
  },
280477
280903
  // trace 写不进去的 run:冻结的事件钟是"观测降级"不是"真静默",按"信息不足→不判"跳过、交 wallClock
@@ -280501,6 +280927,7 @@ ${nodeFault}` : "");
280501
280927
  for (const d of hub?.connectedDaemons() ?? []) {
280502
280928
  for (const a of d.meta.activeSessions ?? []) if (a.jobArtifactId) ids2.add(a.jobArtifactId);
280503
280929
  }
280930
+ for (const a of hub?.liveSessionArtifactIds() ?? []) ids2.add(a);
280504
280931
  return ids2;
280505
280932
  };
280506
280933
  const apiSilenceMs = opts.apiSilenceMs ?? 6e5;
@@ -280940,6 +281367,10 @@ function log2(tag, msg, extra) {
280940
281367
  var import_node_child_process18 = require("node:child_process");
280941
281368
  var fs35 = __toESM(require("node:fs"), 1);
280942
281369
  var path29 = __toESM(require("node:path"), 1);
281370
+ var SESSION_LOCK_WAIT_MS = 1e4;
281371
+ var POLL_MS = 20;
281372
+ var PROBE_TIMES = 5;
281373
+ var PROBE_GAP_MS = 100;
280943
281374
  var flockAvailable = null;
280944
281375
  function hasFlock() {
280945
281376
  if (flockAvailable !== null) return flockAvailable;
@@ -280952,48 +281383,88 @@ function hasFlock() {
280952
281383
  }
280953
281384
  return flockAvailable;
280954
281385
  }
280955
- function acquireSessionLock(lockPath) {
280956
- if (!hasFlock()) return null;
281386
+ function sleepSync(ms) {
281387
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
281388
+ }
281389
+ function acquireSessionLockAttempt(lockPath, opts = {}) {
281390
+ if (!hasFlock()) return { outcome: "no-flock" };
281391
+ const waitMs = opts.waitMs ?? SESSION_LOCK_WAIT_MS;
280957
281392
  fs35.mkdirSync(path29.dirname(lockPath), { recursive: true });
280958
281393
  const readyFile = `${lockPath}.ready.${process.pid}`;
280959
- try {
280960
- fs35.unlinkSync(readyFile);
280961
- } catch {
281394
+ const heldFile = `${lockPath}.held.${process.pid}`;
281395
+ for (const f2 of [readyFile, heldFile]) {
281396
+ try {
281397
+ fs35.unlinkSync(f2);
281398
+ } catch {
281399
+ }
280962
281400
  }
280963
- const child = (0, import_node_child_process18.spawn)("flock", ["-n", lockPath, "-c", `printf ok > '${readyFile}'; exec cat`], {
280964
- stdio: ["pipe", "ignore", "ignore"],
280965
- detached: true
280966
- });
280967
- const deadline = Date.now() + 2e3;
281401
+ const child = (0, import_node_child_process18.spawn)(
281402
+ "sh",
281403
+ ["-c", 'flock -n "$1" -c "printf ok > \\"$2\\"; exec cat" || printf held > "$3"', "sh", lockPath, readyFile, heldFile],
281404
+ { stdio: ["pipe", "ignore", "ignore"], detached: true }
281405
+ );
281406
+ const startedAt = Date.now();
281407
+ const deadline = startedAt + waitMs;
280968
281408
  let ready = false;
280969
- while (Date.now() < deadline) {
281409
+ let held = false;
281410
+ for (; ; ) {
280970
281411
  if (fs35.existsSync(readyFile)) {
280971
281412
  ready = true;
280972
281413
  break;
280973
281414
  }
280974
- (0, import_node_child_process18.spawnSync)("sleep", ["0.02"], { stdio: "ignore" });
281415
+ if (fs35.existsSync(heldFile)) {
281416
+ held = true;
281417
+ break;
281418
+ }
281419
+ if (Date.now() >= deadline) break;
281420
+ sleepSync(POLL_MS);
280975
281421
  }
280976
- try {
280977
- fs35.unlinkSync(readyFile);
280978
- } catch {
281422
+ for (const f2 of [readyFile, heldFile]) {
281423
+ try {
281424
+ fs35.unlinkSync(f2);
281425
+ } catch {
281426
+ }
280979
281427
  }
280980
- if (!ready) {
281428
+ const killChild = () => {
281429
+ try {
281430
+ if (child.pid) process.kill(-child.pid, "SIGKILL");
281431
+ } catch {
281432
+ }
280981
281433
  try {
280982
281434
  child.kill("SIGKILL");
280983
281435
  } catch {
280984
281436
  }
280985
- return null;
281437
+ };
281438
+ if (held) {
281439
+ killChild();
281440
+ return { outcome: "held" };
280986
281441
  }
280987
- return {
280988
- path: lockPath,
280989
- release() {
280990
- try {
280991
- if (child.pid) process.kill(-child.pid, "SIGKILL");
280992
- } catch {
281442
+ if (!ready) {
281443
+ killChild();
281444
+ let heldEverywhere = true;
281445
+ for (let i = 0; i < PROBE_TIMES; i += 1) {
281446
+ if (!isSessionLocked(lockPath)) {
281447
+ heldEverywhere = false;
281448
+ break;
280993
281449
  }
280994
- try {
280995
- child.stdin?.destroy();
280996
- } catch {
281450
+ if (i + 1 < PROBE_TIMES) sleepSync(PROBE_GAP_MS);
281451
+ }
281452
+ if (heldEverywhere) return { outcome: "held" };
281453
+ return { outcome: "undetermined", waitedMs: Date.now() - startedAt };
281454
+ }
281455
+ return {
281456
+ outcome: "acquired",
281457
+ lock: {
281458
+ path: lockPath,
281459
+ release() {
281460
+ try {
281461
+ if (child.pid) process.kill(-child.pid, "SIGKILL");
281462
+ } catch {
281463
+ }
281464
+ try {
281465
+ child.stdin?.destroy();
281466
+ } catch {
281467
+ }
280997
281468
  }
280998
281469
  }
280999
281470
  };
@@ -281322,14 +281793,21 @@ async function runSessionProcess(specFile, adapter) {
281322
281793
  process.env["OASIS_BIND_RUNTIME_PDEATHSIG"] = "1";
281323
281794
  const spec = await readSpec(specFile);
281324
281795
  const { dispatchId, job, auth, sessionDir } = spec;
281325
- const lock = acquireSessionLock(path31.join(sessionDir, "lock"));
281326
- if (!lock && hasFlock()) {
281796
+ const attempt = acquireSessionLockAttempt(path31.join(sessionDir, "lock"));
281797
+ if (attempt.outcome === "held") {
281327
281798
  log2("[session]", `${dispatchId}: \u4F1A\u8BDD\u9501\u5DF2\u88AB\u5360\u7528\uFF0C\u672C\u8FDB\u7A0B\u9000\u51FA\uFF08\u9632\u53CC\u8DD1\uFF09`);
281328
281799
  return;
281329
281800
  }
281330
- if (!hasFlock()) {
281801
+ if (attempt.outcome === "no-flock") {
281331
281802
  log2("[session]", `${dispatchId}: \u672C\u673A\u65E0 flock(1)\uFF0C\u5B64\u513F\u5DE1\u68C0\u90A3\u9053\u9632\u7EBF\u4E0D\u53EF\u7528\uFF08\u53EA\u5269 evict + watchdog\uFF09`);
281332
281803
  }
281804
+ const lock = attempt.outcome === "acquired" ? attempt.lock : null;
281805
+ if (attempt.outcome === "undetermined") {
281806
+ const detail = `\u4F1A\u8BDD\u9501\u5728 ${Math.round(attempt.waitedMs / 1e3)} \u79D2\u5185\u65E2\u6CA1\u62FF\u5230\u3001\u4E5F\u5224\u4E0D\u51FA\u88AB\u8C01\u5360\u7740\uFF08\u8282\u70B9\u8D1F\u8F7D\u8FC7\u9AD8\uFF0C\u7A97\u53E3 ${Math.round(SESSION_LOCK_WAIT_MS / 1e3)}s\uFF09`;
281807
+ log2("[session]", `${dispatchId}: ${detail} \u2192 \u4E0D\u5F00\u5DE5\uFF0C\u628A\u5931\u8D25\u62A5\u7ED9\u670D\u52A1\u7AEF`);
281808
+ await new SessionProcessState(spec, adapter, null).reportStartupFailure(`${SESSION_STARTUP_FAILED_PREFIX}${detail}`);
281809
+ return;
281810
+ }
281333
281811
  const state = new SessionProcessState(spec, adapter, lock);
281334
281812
  await state.run();
281335
281813
  }
@@ -281438,6 +281916,27 @@ var SessionProcessState = class {
281438
281916
  await done;
281439
281917
  this.teardown();
281440
281918
  }
281919
+ /**
281920
+ * 开工前就退出:连上服务端、把 `session_exited` 报出去、等一下 ack,然后收摊(Bug #0184 第 2 条)。
281921
+ *
281922
+ * 它和 {@link run} 的区别只有「不起 runtime」:帧走同一条必达路径(带 seq、进 outbox、重连重放),
281923
+ * 所以新节点 + 老服务端也不会出错——老服务端收到的就是一条普通的 `session_exited`。
281924
+ *
281925
+ * `reason` 用既有的 `error` 而不是新增枚举值:退出归因是 contract 上的封闭值域,
281926
+ * 加一个值会让**老服务端**读到不认识的 reason;真实原因放 `errorMessage`(那是自由文本,
281927
+ * 两侧都已按"给人看的一句话"对待)。
281928
+ */
281929
+ async reportStartupFailure(errorMessage2) {
281930
+ this.connect();
281931
+ this.sendKeyFrame({
281932
+ type: "session_exited",
281933
+ dispatchId: this.spec.dispatchId,
281934
+ sessionId: this.spec.dispatchId,
281935
+ info: { code: null, reason: "error", errorMessage: errorMessage2 }
281936
+ });
281937
+ await this.waitExitAck();
281938
+ this.teardown();
281939
+ }
281441
281940
  /** 会话已退出:等 exit 帧的 ack(等不到也走,服务端有时钟扫描兜底),然后放行主流程。 */
281442
281941
  async afterSessionExit() {
281443
281942
  await this.waitExitAck();
@@ -285224,8 +285723,9 @@ function safeFileName(raw, fallback) {
285224
285723
  const cleaned = displayName2(raw).replace(/[\\/:*?"<>|-]+/g, "_").replace(/\s+/g, "_").replace(/^\.+/, "").replace(/_+/g, "_").replace(/^_|_$/g, "");
285225
285724
  return cleaned || fallback;
285226
285725
  }
285227
- function resolveProjectFileOutPath(rawOut, fileName) {
285228
- const fallback = `${PROJECT_FILES_DIR}/${safeFileName(fileName, "file")}`;
285726
+ function resolveProjectFileOutPath(rawOut, fileName, contentType) {
285727
+ const ext = sourceExtension(fileName, contentType);
285728
+ const fallback = keepExtension(`${PROJECT_FILES_DIR}/${safeFileName(fileName, "file")}`, ext);
285229
285729
  if (rawOut === void 0 || rawOut.trim() === "") return fallback;
285230
285730
  const raw = rawOut.trim();
285231
285731
  if (path35.isAbsolute(raw) || /^[a-zA-Z]:[\\/]/.test(raw)) {
@@ -285244,7 +285744,38 @@ function resolveProjectFileOutPath(rawOut, fileName) {
285244
285744
  if (segments.length < 2 || segments[0] !== PROJECT_FILES_DIR) {
285245
285745
  throw new Error(`--out \u5FC5\u987B\u843D\u5728 ${PROJECT_FILES_DIR}/ \u4E0B\uFF08\u4F8B\uFF1A--out ${PROJECT_FILES_DIR}/${safeFileName(fileName, "file")}\uFF09`);
285246
285746
  }
285247
- return segments.join("/");
285747
+ return keepExtension(segments.join("/"), ext);
285748
+ }
285749
+ var EXT_BY_MIME = {
285750
+ "text/markdown": ".md",
285751
+ "text/x-markdown": ".md",
285752
+ "text/plain": ".txt",
285753
+ "text/html": ".html",
285754
+ "text/csv": ".csv",
285755
+ "application/json": ".json",
285756
+ "application/pdf": ".pdf",
285757
+ "image/png": ".png",
285758
+ "image/jpeg": ".jpg",
285759
+ "image/gif": ".gif",
285760
+ "image/webp": ".webp",
285761
+ "image/svg+xml": ".svg",
285762
+ "application/zip": ".zip"
285763
+ };
285764
+ function realExtension(name) {
285765
+ const ext = path35.posix.extname(name);
285766
+ return /^\.[A-Za-z0-9]{1,8}$/.test(ext) ? ext : "";
285767
+ }
285768
+ function sourceExtension(fileName, contentType) {
285769
+ const own3 = realExtension(displayName2(fileName));
285770
+ if (own3) return own3;
285771
+ const mime = (contentType ?? "").split(";")[0].trim().toLowerCase();
285772
+ return EXT_BY_MIME[mime] ?? "";
285773
+ }
285774
+ function keepExtension(rel, ext) {
285775
+ if (!ext) return rel;
285776
+ const current = realExtension(rel);
285777
+ if (current.toLowerCase() === ext.toLowerCase()) return rel;
285778
+ return `${current ? rel.slice(0, rel.length - current.length) : rel}${ext}`;
285248
285779
  }
285249
285780
  function isTextLike(name, contentType) {
285250
285781
  if (contentType) {
@@ -285532,7 +286063,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
285532
286063
  }
285533
286064
  }
285534
286065
  }
285535
- var CLI_VERSION = "2.2.58" ? "2.2.58" : "dev";
286066
+ var CLI_VERSION = "2.2.60" ? "2.2.60" : "dev";
285536
286067
  function apiHeaders(token, company) {
285537
286068
  const headers = {
285538
286069
  authorization: `Bearer ${token}`,
@@ -286953,7 +287484,7 @@ ${t.ref} ${t.name} [\u5267\u672C=${t.playbookRef}]`);
286953
287484
  );
286954
287485
  const row = findByBlobRef(view.items ?? [], ref2);
286955
287486
  const name = displayName2(row.name);
286956
- const rel = resolveProjectFileOutPath(flags.get("out"), name);
287487
+ const rel = resolveProjectFileOutPath(flags.get("out"), name, row.content_type);
286957
287488
  const bytes2 = await api.bytes(`/api/blobs/${encodeURIComponent((row.blob_ref ?? "").replace(/^blob:/, ""))}`);
286958
287489
  const abs = path36.resolve(process.cwd(), rel);
286959
287490
  fs41.mkdirSync(path36.dirname(abs), { recursive: true });
@@ -289223,7 +289754,7 @@ function shimScript() {
289223
289754
  }
289224
289755
 
289225
289756
  // src/index.ts
289226
- var PKG_VERSION = true ? "2.2.58" : "dev";
289757
+ var PKG_VERSION = true ? "2.2.60" : "dev";
289227
289758
  var LOCAL_BIN = localBin();
289228
289759
  var NPM_PREFIX = npmPrefix();
289229
289760
  var INSTANCE = DEFAULT_INSTANCE;