oasis_test_v2 2.2.68 → 2.2.70

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 +1763 -545
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -444,13 +444,14 @@ function isUnread(eventAt, marker) {
444
444
  function isChatSessionUnread(session, marker) {
445
445
  return session.lastTurnQuality !== void 0 && isUnread(session.touchedAt, marker);
446
446
  }
447
- var chatScope, letterScope, INBOX_BASELINE_SCOPE;
447
+ var chatScope, letterScope, INBOX_BASELINE_SCOPE, INBOX_READ_ALL_SCOPE;
448
448
  var init_inbox_read = __esm({
449
449
  "../contract/src/inbox-read.ts"() {
450
450
  "use strict";
451
451
  chatScope = (sessionId) => `chat:${sessionId}`;
452
452
  letterScope = (annotationId) => `letter:${annotationId}`;
453
453
  INBOX_BASELINE_SCOPE = "inbox-baseline";
454
+ INBOX_READ_ALL_SCOPE = "inbox-read-all";
454
455
  }
455
456
  });
456
457
 
@@ -1464,6 +1465,22 @@ var init_chat = __esm({
1464
1465
  });
1465
1466
 
1466
1467
  // ../contract/src/session-outputs.ts
1468
+ function sessionOutputFilesOfParts(parts) {
1469
+ if (!Array.isArray(parts)) return [];
1470
+ const out = [];
1471
+ for (const raw of parts) {
1472
+ if (!raw || typeof raw !== "object") continue;
1473
+ const p2 = raw;
1474
+ if (p2.type !== "outputs" || !Array.isArray(p2.outputs)) continue;
1475
+ for (const f2 of p2.outputs) {
1476
+ if (!f2 || typeof f2 !== "object") continue;
1477
+ const file = f2;
1478
+ if (typeof file.path !== "string" || !file.path) continue;
1479
+ out.push({ path: file.path, name: typeof file.name === "string" && file.name ? file.name : file.path });
1480
+ }
1481
+ }
1482
+ return out;
1483
+ }
1467
1484
  function diffSessionOutputs(scanned, previous3) {
1468
1485
  const prev = previous3 ?? {};
1469
1486
  const changed = [];
@@ -1484,11 +1501,13 @@ function basenameOf(path44) {
1484
1501
  const i = path44.lastIndexOf("/");
1485
1502
  return i >= 0 ? path44.slice(i + 1) : path44;
1486
1503
  }
1487
- var SESSION_OUTPUTS_DIR;
1504
+ var SESSION_OUTPUTS_DIR, SESSION_OUTPUTS_ITEM_CODE, sessionOutputsItemId;
1488
1505
  var init_session_outputs = __esm({
1489
1506
  "../contract/src/session-outputs.ts"() {
1490
1507
  "use strict";
1491
1508
  SESSION_OUTPUTS_DIR = "outputs";
1509
+ SESSION_OUTPUTS_ITEM_CODE = "session_outputs";
1510
+ sessionOutputsItemId = (messageId) => `oasis-outputs:${messageId}`;
1492
1511
  }
1493
1512
  });
1494
1513
 
@@ -1676,6 +1695,24 @@ var init_chat_turn = __esm({
1676
1695
  });
1677
1696
 
1678
1697
  // ../contract/src/delegation.ts
1698
+ function hasDelegationDelivery(ev) {
1699
+ if (!ev) return false;
1700
+ return (ev.movedFiles ?? 0) > 0 || (ev.unchangedFiles ?? 0) > 0 || (ev.artifacts ?? 0) > 0 || Boolean(ev.roundText?.trim());
1701
+ }
1702
+ function delegationEnding(record9, evidence) {
1703
+ if (record9.state === "done") return "done";
1704
+ if (record9.state !== "failed") return "done";
1705
+ if (record9.failureKind !== "timeout") return "failed";
1706
+ return hasDelegationDelivery(evidence) ? "timeout-delivered" : "timeout-empty";
1707
+ }
1708
+ function delegationEndingOfRecord(record9, artifacts = []) {
1709
+ const last = record9.roundSummaries[record9.roundSummaries.length - 1];
1710
+ const lastRound = last?.round;
1711
+ return delegationEnding(record9, {
1712
+ ...last?.text ? { roundText: last.text } : {},
1713
+ artifacts: lastRound === void 0 ? 0 : artifacts.filter((a) => a.round === lastRound).length
1714
+ });
1715
+ }
1679
1716
  function trimRoundSummariesForReturn(summaries, recentRounds = DELEGATION_RETURN_RECENT_ROUNDS) {
1680
1717
  const keep = Math.max(0, recentRounds);
1681
1718
  const cut = Math.max(0, summaries.length - keep);
@@ -2045,6 +2082,94 @@ var init_chunked_message = __esm({
2045
2082
  }
2046
2083
  });
2047
2084
 
2085
+ // ../contract/src/command-card-copy.ts
2086
+ function stripInternalIds(text7, placeholder = "\u2026") {
2087
+ if (!text7) return text7;
2088
+ let out = text7;
2089
+ for (const re of [PREFIXED_ID_RE, UUID_RE, SHORT_ID_RE]) {
2090
+ out = out.replace(re, placeholder);
2091
+ }
2092
+ return out.replace(new RegExp(`[ \\t]+${placeholder}`, "gu"), placeholder);
2093
+ }
2094
+ function firstSentenceSummary(text7, max = CARD_SUMMARY_MAX) {
2095
+ const raw = (text7 ?? "").trim();
2096
+ if (!raw) return "";
2097
+ const cut = raw.split(/[。!?\n]|——/u)[0].trim() || raw.trim();
2098
+ const clean = stripInternalIds(cut).trim();
2099
+ const cps = [...clean];
2100
+ return cps.length > max ? `${cps.slice(0, max).join("")}\u2026` : clean;
2101
+ }
2102
+ function nodePhrase(args, refs) {
2103
+ const id = argStr(args, "artifactId");
2104
+ const title = id ? refs.nodeTitle?.(id)?.trim() : void 0;
2105
+ return title ? `\u300A${title}\u300B` : NEUTRAL_NODE;
2106
+ }
2107
+ function targetPhrase(naming, neutral) {
2108
+ const summary = firstSentenceSummary(naming?.summary);
2109
+ const nodeTitle = naming?.nodeTitle?.trim();
2110
+ if (summary) {
2111
+ return { phrase: nodeTitle && !summary.includes(nodeTitle) ? `\u300A${nodeTitle}\u300B\u2014\u2014${summary}` : summary, named: true };
2112
+ }
2113
+ if (nodeTitle) return { phrase: `\u300A${nodeTitle}\u300B\u4E0A\u7684${neutral}`, named: true };
2114
+ return { phrase: neutral, named: false };
2115
+ }
2116
+ function escalationPhraseOf(refs, escalationId) {
2117
+ return targetPhrase(escalationId ? refs.escalation?.(escalationId) : void 0, NEUTRAL_ESCALATION);
2118
+ }
2119
+ function gapPhraseOf(refs, gapId) {
2120
+ return targetPhrase(gapId ? refs.gap?.(gapId) : void 0, NEUTRAL_GAP);
2121
+ }
2122
+ function actionPhrase(verb, noun, target, neutralVerb = verb) {
2123
+ return target.named ? `${verb}${noun}\uFF1A${target.phrase}` : `${neutralVerb}${target.phrase}`;
2124
+ }
2125
+ function describeCommandForHuman(command2, args, refs = {}) {
2126
+ const reason = argStr(args, "reason");
2127
+ const reasonTail = reason ? `\uFF08\u7406\u7531\uFF1A${stripInternalIds(reason)}\uFF09` : "";
2128
+ switch (command2) {
2129
+ case "forceConclude":
2130
+ return `\u5F3A\u5236\u6536\u53E3${nodePhrase(args, refs)}${reasonTail}\u2014\u2014\u7ED5\u8FC7\u8BC4\u5BA1\u76F4\u63A5\u76D6\u91CC\u7A0B\u7891\u3002`;
2131
+ case "promote":
2132
+ return `\u628A${nodePhrase(args, refs)}\u8F6C\u4E3A\u957F\u671F\u4FDD\u7559\u7684\u8282\u70B9\uFF08\u4E0D\u518D\u968F\u4EFB\u52A1\u7ED3\u675F\u56DE\u6536\uFF09\u3002`;
2133
+ // continueBatch(续跑额度卡)**故意不在这里**:它的文案由引擎写好放在 `continuationBudget.request.summary`
2134
+ // 里,本来就是「《节点名》尚未完成,已达到自动派发上限。累计已运行 N 轮…」这种名称式写法,没有 id。
2135
+ // 重写一遍只会让同一句话有两个事实源,交给 {@link commandCardDisplayLabel} 的兜底清洗即可。
2136
+ case "resolveEscalation": {
2137
+ const escalation = escalationPhraseOf(refs, argStr(args, "escalationId"));
2138
+ const gapId = argStr(args, "gapId");
2139
+ if (!gapId) return `${actionPhrase("\u5173\u95ED", "\u6C42\u52A9", escalation)}${reasonTail}`;
2140
+ return [
2141
+ reason ? `\u91C7\u7528\u65B9\u6848\uFF1A${stripInternalIds(reason)}` : "\u786E\u8BA4\u6062\u590D\u65B9\u6848",
2142
+ actionPhrase("\u89E3\u51B3", "\u7F3A\u53E3", gapPhraseOf(refs, gapId)),
2143
+ actionPhrase("\u5173\u95ED\u5173\u8054", "\u6C42\u52A9", escalation, "\u5173\u95ED\u5173\u8054\u7684"),
2144
+ "\u7EE7\u7EED\u6267\u884C\uFF1A\u662F\uFF08\u4ECD\u987B\u6EE1\u8DB3\u5176\u4ED6\u963B\u585E\u6761\u4EF6\uFF09"
2145
+ ].join("\uFF1B");
2146
+ }
2147
+ default:
2148
+ return void 0;
2149
+ }
2150
+ }
2151
+ function commandCardDisplayLabel(command2, args, storedLabel, refs = {}) {
2152
+ const described = command2 ? describeCommandForHuman(command2, args ?? {}, refs) : void 0;
2153
+ return described ?? stripInternalIds(storedLabel ?? "");
2154
+ }
2155
+ var PREFIXED_ID_RE, UUID_RE, SHORT_ID_RE, CARD_SUMMARY_MAX, NEUTRAL_NODE, NEUTRAL_ESCALATION, NEUTRAL_GAP, argStr;
2156
+ var init_command_card_copy = __esm({
2157
+ "../contract/src/command-card-copy.ts"() {
2158
+ "use strict";
2159
+ PREFIXED_ID_RE = /(?:artifact|esc|gap|wk|ws|actor|draft|issue|review|rev|work|node|company|chat|sess|int|blob|run|continuation)[::][A-Za-z0-9][A-Za-z0-9:._#-]*/gu;
2160
+ UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/giu;
2161
+ SHORT_ID_RE = /\b(?=[0-9a-f-]*[0-9])(?=[0-9a-f-]*[a-f])[0-9a-f]{8,}(?:-[0-9a-f]{4,})*\b/giu;
2162
+ CARD_SUMMARY_MAX = 48;
2163
+ NEUTRAL_NODE = "\u8FD9\u4E00\u6B65";
2164
+ NEUTRAL_ESCALATION = "\u8FD9\u6761\u6C42\u52A9";
2165
+ NEUTRAL_GAP = "\u8FD9\u4E2A\u7F3A\u53E3";
2166
+ argStr = (args, key2) => {
2167
+ const v2 = args[key2];
2168
+ return typeof v2 === "string" && v2.trim() ? v2.trim() : void 0;
2169
+ };
2170
+ }
2171
+ });
2172
+
2048
2173
  // ../contract/src/index.ts
2049
2174
  var init_src = __esm({
2050
2175
  "../contract/src/index.ts"() {
@@ -2107,6 +2232,7 @@ var init_src = __esm({
2107
2232
  init_chat_turn();
2108
2233
  init_delegation();
2109
2234
  init_delegation();
2235
+ init_delegation();
2110
2236
  init_delegation_prompts();
2111
2237
  init_knowledge();
2112
2238
  init_knowledge_governance();
@@ -2114,6 +2240,7 @@ var init_src = __esm({
2114
2240
  init_file_preview();
2115
2241
  init_file_limits();
2116
2242
  init_chunked_message();
2243
+ init_command_card_copy();
2117
2244
  }
2118
2245
  });
2119
2246
 
@@ -2263,6 +2390,21 @@ function sessionRuntimeOverride(session) {
2263
2390
  if (!nodeId || !runtimeKind) return null;
2264
2391
  return { nodeId, runtimeKind };
2265
2392
  }
2393
+ function chatSessionInitialRuntime(input) {
2394
+ const trimmed = (v2) => typeof v2 === "string" && v2.trim() ? v2.trim() : null;
2395
+ const binding = input.binding?.nodeId && input.binding?.runtimeKind ? { nodeId: input.binding.nodeId, runtimeKind: input.binding.runtimeKind } : null;
2396
+ const requestedOverride = input.requested?.override;
2397
+ const picked = requestedOverride?.nodeId && requestedOverride?.runtimeKind ? { nodeId: requestedOverride.nodeId, runtimeKind: requestedOverride.runtimeKind } : null;
2398
+ const target = picked ?? binding;
2399
+ const followsAgentTarget = !picked || !!binding && picked.nodeId === binding.nodeId && picked.runtimeKind === binding.runtimeKind;
2400
+ const requestedModel = input.requested?.model;
2401
+ const model = requestedModel !== void 0 ? trimmed(requestedModel) : followsAgentTarget ? trimmed(input.agentModel) : null;
2402
+ return {
2403
+ overrideNodeId: target?.nodeId ?? null,
2404
+ overrideRuntimeKind: target?.runtimeKind ?? null,
2405
+ model
2406
+ };
2407
+ }
2266
2408
  function sessionRuntimeOverrideChangesTarget(session, binding) {
2267
2409
  const override = sessionRuntimeOverride(session);
2268
2410
  if (!override) return false;
@@ -2322,7 +2464,7 @@ var init_session_runtime_target = __esm({
2322
2464
  this.name = "SessionRuntimeOverrideError";
2323
2465
  }
2324
2466
  };
2325
- CLEAR_SESSION_RUNTIME_HINT = "\u6E05\u9664\u8FD9\u6761\u4F1A\u8BDD\u7684\u8FD0\u884C\u65F6\u9009\u62E9\u5373\u53EF\u7EE7\u7EED\uFF08\u4F1A\u56DE\u5230\u8FD9\u4F4D Agent \u7684\u9ED8\u8BA4\u7ED1\u5B9A\uFF09\u3002";
2467
+ CLEAR_SESSION_RUNTIME_HINT = "\u5728\u8FD0\u884C\u65F6\u9009\u62E9\u91CC\u70B9\u300C\u6062\u590D\u4E3A\u5458\u5DE5\u9ED8\u8BA4\u8FD0\u884C\u914D\u7F6E\u300D\u5373\u53EF\u7EE7\u7EED\uFF08\u4F1A\u628A\u8FD9\u4F4D Agent \u6B64\u523B\u7684\u9ED8\u8BA4\u914D\u7F6E\u6284\u8FDB\u8FD9\u6761\u4F1A\u8BDD\uFF09\u3002";
2326
2468
  }
2327
2469
  });
2328
2470
 
@@ -3904,9 +4046,57 @@ function newClaudeUsageAcc() {
3904
4046
  costUsd: null,
3905
4047
  messageUsageById: /* @__PURE__ */ new Map(),
3906
4048
  finalUsage: null,
4049
+ restored: null,
3907
4050
  saw: false
3908
4051
  };
3909
4052
  }
4053
+ function findClaudeSessionFile(configDir, cwd, runtimeSessionId) {
4054
+ const root = path7.join(configDir, "projects");
4055
+ const name = `${runtimeSessionId}.jsonl`;
4056
+ const direct = path7.join(root, cwd.replace(/[^a-zA-Z0-9]/g, "-"), name);
4057
+ if (fs7.existsSync(direct)) return direct;
4058
+ try {
4059
+ for (const slug6 of fs7.readdirSync(root)) {
4060
+ const candidate = path7.join(root, slug6, name);
4061
+ if (fs7.existsSync(candidate)) return candidate;
4062
+ }
4063
+ } catch {
4064
+ }
4065
+ return void 0;
4066
+ }
4067
+ function readClaudeRestoredCost(sessionFile) {
4068
+ try {
4069
+ const stat4 = fs7.statSync(sessionFile);
4070
+ if (!stat4.isFile() || stat4.size === 0) return null;
4071
+ const length = Math.min(stat4.size, RESTORED_COST_TAIL_BYTES);
4072
+ const buf = Buffer.alloc(length);
4073
+ const fd = fs7.openSync(sessionFile, "r");
4074
+ try {
4075
+ fs7.readSync(fd, buf, 0, length, stat4.size - length);
4076
+ } finally {
4077
+ fs7.closeSync(fd);
4078
+ }
4079
+ const lines = buf.toString("utf8").split("\n");
4080
+ for (let i = lines.length - 1; i >= 0; i--) {
4081
+ const line2 = lines[i];
4082
+ if (!line2.includes('"cost-state"')) continue;
4083
+ let parsed;
4084
+ try {
4085
+ parsed = JSON.parse(line2);
4086
+ } catch {
4087
+ continue;
4088
+ }
4089
+ if (parsed["type"] !== "cost-state") continue;
4090
+ const cost = parsed["totalCostUSD"];
4091
+ return {
4092
+ usage: parseModelUsage(parsed["modelUsage"]),
4093
+ costUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null
4094
+ };
4095
+ }
4096
+ } catch {
4097
+ }
4098
+ return null;
4099
+ }
3910
4100
  function usageNum(v2) {
3911
4101
  return typeof v2 === "number" && Number.isFinite(v2) && v2 >= 0 ? v2 : void 0;
3912
4102
  }
@@ -3980,14 +4170,34 @@ function accumulateClaudeUsage(acc, p2) {
3980
4170
  acc.saw = true;
3981
4171
  }
3982
4172
  } else if (type === "result") {
3983
- acc.finalUsage = mergeFinalUsage(parseModelUsage(p2["modelUsage"]), parseResultUsage(p2["usage"]));
3984
4173
  const c = p2["total_cost_usd"];
3985
- if (typeof c === "number" && Number.isFinite(c)) {
3986
- acc.costUsd = c;
4174
+ const cost = typeof c === "number" && Number.isFinite(c) ? c : null;
4175
+ const { usage: modelUsage, costUsd } = sinceRestored(acc.restored, parseModelUsage(p2["modelUsage"]), cost);
4176
+ acc.finalUsage = mergeFinalUsage(modelUsage, parseResultUsage(p2["usage"]));
4177
+ if (costUsd != null) {
4178
+ acc.costUsd = costUsd;
3987
4179
  }
3988
4180
  if (acc.finalUsage || acc.costUsd != null) acc.saw = true;
3989
4181
  }
3990
4182
  }
4183
+ function sinceRestored(restored, usage, costUsd) {
4184
+ if (!restored) return { usage, costUsd };
4185
+ const out = usage ? { ...usage } : null;
4186
+ if (out && restored.usage) {
4187
+ for (const k2 of ["input", "output", "cacheRead", "cacheCreation"]) {
4188
+ const base = restored.usage[k2];
4189
+ const total = out[k2];
4190
+ if (total == null || base == null) continue;
4191
+ if (total < base) return { usage, costUsd };
4192
+ out[k2] = total - base;
4193
+ }
4194
+ }
4195
+ if (costUsd != null && restored.costUsd != null) {
4196
+ if (costUsd < restored.costUsd) return { usage, costUsd };
4197
+ return { usage: out, costUsd: costUsd - restored.costUsd };
4198
+ }
4199
+ return { usage: out, costUsd };
4200
+ }
3991
4201
  function newClaudeRunningUsage() {
3992
4202
  return { byMessage: /* @__PURE__ */ new Map(), currentMessageId: null, lastKey: "" };
3993
4203
  }
@@ -4088,13 +4298,14 @@ function classifyExit(result) {
4088
4298
  if (status === 429 || status === 529) return "rate-limit";
4089
4299
  return "error";
4090
4300
  }
4091
- var import_node_child_process3, import_node_crypto6, fs7, path7, readline, CLAUDE_BLOCKED_FLAGS, SUBAGENT_TRANSCRIPT_MAX_BYTES, SUBAGENT_TERMINAL, ONE_SHOT_UNSAFE_TOOLS, ClaudeCodeAdapter;
4301
+ var import_node_child_process3, import_node_crypto6, fs7, os4, path7, readline, CLAUDE_BLOCKED_FLAGS, SUBAGENT_TRANSCRIPT_MAX_BYTES, SUBAGENT_TERMINAL, RESTORED_COST_TAIL_BYTES, ONE_SHOT_UNSAFE_TOOLS, ClaudeCodeAdapter;
4092
4302
  var init_claude_code = __esm({
4093
4303
  "../adapters/src/claude-code/index.ts"() {
4094
4304
  "use strict";
4095
4305
  import_node_child_process3 = require("node:child_process");
4096
4306
  import_node_crypto6 = require("node:crypto");
4097
4307
  fs7 = __toESM(require("node:fs"), 1);
4308
+ os4 = __toESM(require("node:os"), 1);
4098
4309
  path7 = __toESM(require("node:path"), 1);
4099
4310
  readline = __toESM(require("node:readline"), 1);
4100
4311
  init_sync_skills();
@@ -4141,6 +4352,7 @@ var init_claude_code = __esm({
4141
4352
  killed: "stopped",
4142
4353
  stopped: "stopped"
4143
4354
  };
4355
+ RESTORED_COST_TAIL_BYTES = 4 * 1024 * 1024;
4144
4356
  ONE_SHOT_UNSAFE_TOOLS = ["ScheduleWakeup", "CronCreate", "CronDelete", "CronList"];
4145
4357
  ClaudeCodeAdapter = class {
4146
4358
  constructor(opts = {}) {
@@ -4237,6 +4449,9 @@ var init_claude_code = __esm({
4237
4449
  { runtime: "claude-code", actor: job.actor, ...job.dispatchId ? { dispatchId: job.dispatchId } : {} }
4238
4450
  )
4239
4451
  ];
4452
+ const claudeConfigDir = job.env?.["CLAUDE_CONFIG_DIR"] || this.opts.env?.["CLAUDE_CONFIG_DIR"] || process.env["CLAUDE_CONFIG_DIR"] || path7.join(os4.homedir(), ".claude");
4453
+ const resumedSessionFile = job.runtimeSessionId && job.resumeRuntimeSession ? findClaudeSessionFile(claudeConfigDir, dir, job.runtimeSessionId) : void 0;
4454
+ const restoredCost = resumedSessionFile ? readClaudeRestoredCost(resumedSessionFile) : null;
4240
4455
  const otelEnv = this.resolveOtelEnv(job, id);
4241
4456
  const containerized = await maybeContainerize({
4242
4457
  bin: this.opts.claudeBin ?? "claude",
@@ -4364,6 +4579,7 @@ var init_claude_code = __esm({
4364
4579
  const STDERR_MAX_LINES = 40;
4365
4580
  const STDERR_MAX_BYTES = 4e3;
4366
4581
  const usageAcc = newClaudeUsageAcc();
4582
+ usageAcc.restored = restoredCost;
4367
4583
  const runningUsage = newClaudeRunningUsage();
4368
4584
  const usageCheckpointCbs = [];
4369
4585
  let sessionModel;
@@ -4833,7 +5049,7 @@ var init_resilient = __esm({
4833
5049
 
4834
5050
  // ../adapters/src/_core/skill-cache.ts
4835
5051
  function defaultCacheRoot() {
4836
- return path8.join(os4.homedir(), ".oasis", "skill-cache");
5052
+ return path8.join(os5.homedir(), ".oasis", "skill-cache");
4837
5053
  }
4838
5054
  function sessionSkillsSubdir(runtimeKind) {
4839
5055
  switch (runtimeKind) {
@@ -5238,13 +5454,13 @@ async function verifyPendingLink(skillsDir, name, pend, log3) {
5238
5454
  }
5239
5455
  return true;
5240
5456
  }
5241
- var import_node_crypto7, fsp, os4, path8, MANIFEST_SCHEMA_VERSION, STALE_LOCK_MS, LOCK_ACQUIRE_TIMEOUT_MS, LOCK_POLL_MS;
5457
+ var import_node_crypto7, fsp, os5, path8, MANIFEST_SCHEMA_VERSION, STALE_LOCK_MS, LOCK_ACQUIRE_TIMEOUT_MS, LOCK_POLL_MS;
5242
5458
  var init_skill_cache = __esm({
5243
5459
  "../adapters/src/_core/skill-cache.ts"() {
5244
5460
  "use strict";
5245
5461
  import_node_crypto7 = require("node:crypto");
5246
5462
  fsp = __toESM(require("node:fs/promises"), 1);
5247
- os4 = __toESM(require("node:os"), 1);
5463
+ os5 = __toESM(require("node:os"), 1);
5248
5464
  path8 = __toESM(require("node:path"), 1);
5249
5465
  MANIFEST_SCHEMA_VERSION = 2;
5250
5466
  STALE_LOCK_MS = 6e4;
@@ -6434,7 +6650,7 @@ function codexSessionsRoot() {
6434
6650
  const candidates = [];
6435
6651
  const ch = process.env["CODEX_HOME"];
6436
6652
  if (ch) candidates.push(path10.join(ch, "sessions"));
6437
- const home2 = process.env["HOME"] || os5.homedir();
6653
+ const home2 = process.env["HOME"] || os6.homedir();
6438
6654
  if (home2) candidates.push(path10.join(home2, ".codex", "sessions"));
6439
6655
  for (const c of candidates) {
6440
6656
  try {
@@ -6757,7 +6973,7 @@ function codexTurnErrorMessage(value2) {
6757
6973
  const detail = typeof error2?.additionalDetails === "string" ? error2.additionalDetails.trim() : "";
6758
6974
  return [summary, detail].filter(Boolean).join("\n").slice(0, 4e3);
6759
6975
  }
6760
- var import_node_child_process7, import_node_crypto9, fs8, os5, path10, readline2, CODEX_BLOCKED_FLAGS, CODEX_ARGV_PROMPT_MAX_BYTES, CodexRolloutUsageTail, CODEX_APP_SERVER_BLOCKED_FLAGS, CodexAdapter;
6976
+ var import_node_child_process7, import_node_crypto9, fs8, os6, path10, readline2, CODEX_BLOCKED_FLAGS, CODEX_ARGV_PROMPT_MAX_BYTES, CodexRolloutUsageTail, CODEX_APP_SERVER_BLOCKED_FLAGS, CodexAdapter;
6761
6977
  var init_codex = __esm({
6762
6978
  "../adapters/src/codex/index.ts"() {
6763
6979
  "use strict";
@@ -6765,7 +6981,7 @@ var init_codex = __esm({
6765
6981
  init_container_runtime();
6766
6982
  import_node_crypto9 = require("node:crypto");
6767
6983
  fs8 = __toESM(require("node:fs"), 1);
6768
- os5 = __toESM(require("node:os"), 1);
6984
+ os6 = __toESM(require("node:os"), 1);
6769
6985
  path10 = __toESM(require("node:path"), 1);
6770
6986
  readline2 = __toESM(require("node:readline"), 1);
6771
6987
  init_sync_skills();
@@ -9371,7 +9587,7 @@ function pdeathsigAvailable(probe = defaultProbe) {
9371
9587
  return cachedAvailable;
9372
9588
  }
9373
9589
  function defaultProbe() {
9374
- if (os6.platform() !== "linux") return false;
9590
+ if (os7.platform() !== "linux") return false;
9375
9591
  try {
9376
9592
  const help = (0, import_node_child_process9.execFileSync)("setpriv", ["--help"], { stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).toString();
9377
9593
  return help.includes("--pdeathsig");
@@ -9385,12 +9601,12 @@ function pdeathsigWrap(bin, args) {
9385
9601
  function shouldBindRuntimePdeathsig() {
9386
9602
  return process.env["OASIS_BIND_RUNTIME_PDEATHSIG"] === "1" && pdeathsigAvailable();
9387
9603
  }
9388
- var import_node_child_process9, os6, cachedAvailable;
9604
+ var import_node_child_process9, os7, cachedAvailable;
9389
9605
  var init_pdeathsig = __esm({
9390
9606
  "../adapters/src/_core/pdeathsig.ts"() {
9391
9607
  "use strict";
9392
9608
  import_node_child_process9 = require("node:child_process");
9393
- os6 = __toESM(require("node:os"), 1);
9609
+ os7 = __toESM(require("node:os"), 1);
9394
9610
  }
9395
9611
  });
9396
9612
 
@@ -12831,7 +13047,13 @@ async function reconcileChatTurn(turn, deps) {
12831
13047
  ...status === "succeeded" || !restartCause ? {} : { cancelledBy: restartCause }
12832
13048
  });
12833
13049
  if (!ok3) return raced;
12834
- return { settled: true, turnId: turn.id, status, reason: `run \u7EC8\u6001 ${run.status}` };
13050
+ return {
13051
+ settled: true,
13052
+ turnId: turn.id,
13053
+ status,
13054
+ reason: `run \u7EC8\u6001 ${run.status}`,
13055
+ ...run.exitReason ? { exitReason: run.exitReason } : {}
13056
+ };
12835
13057
  }
12836
13058
  async function sweepStaleChatTurns(deps) {
12837
13059
  const before = new Date(deps.now - (deps.staleAfterMs ?? 2 * 6e4)).toISOString();
@@ -12853,7 +13075,8 @@ async function sweepStaleChatTurns(deps) {
12853
13075
  chatSessionId: turn.chatSessionId,
12854
13076
  turnId: turn.id,
12855
13077
  status: settledStatus,
12856
- reason: outcome.reason ?? `\u8F6E\u6B21\u6536\u53E3\u4E3A ${settledStatus}`
13078
+ reason: outcome.reason ?? `\u8F6E\u6B21\u6536\u53E3\u4E3A ${settledStatus}`,
13079
+ ...outcome.settled && outcome.exitReason ? { exitReason: outcome.exitReason } : {}
12857
13080
  }).catch((e) => {
12858
13081
  deps.log?.(`[chat-turn-sweep] \u8F6E\u6B21 ${turn.id} \u6536\u53E3\u540E\u7684\u56DE\u8C03\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u62CD\uFF09: ${String(e)}`);
12859
13082
  });
@@ -125858,7 +126081,7 @@ function isReplyWork(w2) {
125858
126081
  function replyIssueIdOf(w2) {
125859
126082
  return w2.lane === "reply" ? w2.replyToIssueId ?? null : null;
125860
126083
  }
125861
- var BUSINESS_HANDOFF_POLICY_FIELD, BUSINESS_HANDOFF_POLICY_EXEMPT, DEFAULT_CONFIG, deriveWorkId, deriveReviewId, deriveReplyId;
126084
+ var BUSINESS_HANDOFF_POLICY_FIELD, BUSINESS_HANDOFF_POLICY_EXEMPT, DEFAULT_CONFIG, deriveWorkId, deriveReviewId, ENGINE_SYSTEM_ACTOR, REVIEW_EXHAUSTED_ESC_PREFIX, deriveReplyId;
125862
126085
  var init_model = __esm({
125863
126086
  "../engine/src/model.ts"() {
125864
126087
  "use strict";
@@ -125871,6 +126094,8 @@ var init_model = __esm({
125871
126094
  };
125872
126095
  deriveWorkId = (nodeId, version2) => `wk:${nodeId}:${version2}`;
125873
126096
  deriveReviewId = (workId, reviewer, group) => `rv:${workId}:${reviewer}:${group}`;
126097
+ ENGINE_SYSTEM_ACTOR = "actor:system:engine";
126098
+ REVIEW_EXHAUSTED_ESC_PREFIX = "esc:review-exhausted:";
125874
126099
  deriveReplyId = (issueId, salt) => `ir:${issueId}:${salt}`;
125875
126100
  }
125876
126101
  });
@@ -126414,6 +126639,7 @@ var init_kill = __esm({
126414
126639
  function scan(state, ctx) {
126415
126640
  if (state.workorder.dispatchPaused) return { events: [], terminated: false };
126416
126641
  const events = [];
126642
+ closeSettledReviewEscalations(state, events);
126417
126643
  for (const node2 of state.liveNodes()) {
126418
126644
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
126419
126645
  const continuationPolicy = node2.assigneeActorId ? ctx.continuationPolicies?.get(node2.assigneeActorId) : null;
@@ -126942,6 +127168,55 @@ function reviewMaxRetriesOf(policy) {
126942
127168
  const act = policy.resolve({ executor: "agent", scenario: "review" }).timeoutAction;
126943
127169
  return act.kind === "kill-rebuild" ? act.maxRetries : AGENT_REVIEW_MAX_RETRIES;
126944
127170
  }
127171
+ function pushReviewExhaustedEscalation(state, nodeId, workId, reviewId, reviewerActorId, rounds, events) {
127172
+ const node2 = state.node(nodeId);
127173
+ if (!node2) return;
127174
+ if (node2.latestAcceptId === workId) return;
127175
+ const escId = `${REVIEW_EXHAUSTED_ESC_PREFIX}${reviewId}`;
127176
+ if (state.allIssues().some((i) => i.id === escId)) return;
127177
+ events.push({
127178
+ kind: "issue.create",
127179
+ workorderId: state.workorder.id,
127180
+ issueId: escId,
127181
+ issueKind: "escalation",
127182
+ body: `\u300A${node2.title}\u300B\u7684\u8BC4\u5BA1\u8FDE\u7EED ${rounds} \u6B21\u5931\u8D25\uFF0C\u8FD9\u4E00\u8F6E\u8BC4\u5BA1\u6536\u4E0D\u9F50\u2014\u2014\u9700\u8981\u6709\u4EBA\u91CD\u5F00\u3002
127183
+
127184
+ \u672B\u6B21\u8BC4\u5BA1\u4F1A\u8BDD\u7ED3\u675F\u65F6\u6CA1\u6709\u6295\u7968\uFF1B\u62A5\u9519\u539F\u8BDD\u89C1\u8FD9\u4E2A\u8282\u70B9\u7684\u6267\u884C\u8F68\u8FF9\u3002`,
127185
+ aboutNodeId: nodeId,
127186
+ aboutWorkId: workId,
127187
+ raisedByNodeId: null,
127188
+ authorActorId: reviewerActorId,
127189
+ handActorId: null,
127190
+ recipients: [state.workorder.ownerActorId],
127191
+ attachments: [],
127192
+ intent: "advisory"
127193
+ });
127194
+ }
127195
+ function closeSettledReviewEscalations(state, events) {
127196
+ for (const issue2 of state.allIssues()) {
127197
+ if (issue2.resolvedAt !== null || issue2.kind !== "escalation") continue;
127198
+ const id = issue2.id;
127199
+ if (!id.startsWith(REVIEW_EXHAUSTED_ESC_PREFIX)) continue;
127200
+ const node2 = issue2.aboutNodeId ? state.node(issue2.aboutNodeId) : void 0;
127201
+ if (!node2 || node2.cancelledAt) continue;
127202
+ const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
127203
+ if (lw && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "dead") continue;
127204
+ const review = state.review(id.slice(REVIEW_EXHAUSTED_ESC_PREFIX.length));
127205
+ if (!review) continue;
127206
+ const head = state.requirementsOf(node2.id).find((r) => r.reviewGroup === review.reviewGroup)?.latestReviewId ?? null;
127207
+ const note = head !== review.id ? "\u8BC4\u5BA1\u5DF2\u91CD\u65B0\u5B89\u6392" : node2.latestAcceptId === review.targetWorkId ? "\u8FD9\u4E00\u7248\u5DF2\u9A8C\u6536" : null;
127208
+ if (note === null) continue;
127209
+ events.push({
127210
+ kind: "issue.resolve",
127211
+ workorderId: state.workorder.id,
127212
+ issueId: issue2.id,
127213
+ state: "resolved",
127214
+ by: SYSTEM_ENGINE_ACTOR,
127215
+ note,
127216
+ viaWorkId: null
127217
+ });
127218
+ }
127219
+ }
126945
127220
  function processReviews(state, nodeId, workId, ctx, events) {
126946
127221
  const policy = ctx.policy ?? CODE_DEFAULT_POLICY;
126947
127222
  const reqs = state.requirementsOf(nodeId);
@@ -127003,15 +127278,19 @@ function processReviews(state, nodeId, workId, ctx, events) {
127003
127278
  }
127004
127279
  } else if (info.status === "failed") {
127005
127280
  const req = reqs.find((r) => r.reviewGroup === g2);
127006
- const overLimit = reviewRetriesExhausted(g2, reviews, { policy }) !== null;
127281
+ const exhausted = reviewRetriesExhausted(g2, reviews, { policy });
127282
+ const overLimit = exhausted !== null;
127007
127283
  if (overLimit && classify(req.reviewerActorId) === "agent") {
127008
127284
  const lrId = req.latestReviewId;
127009
- if (lrId) events.push({
127010
- kind: "review.kill",
127011
- workorderId: state.workorder.id,
127012
- reviewId: lrId,
127013
- reason: "exhausted"
127014
- });
127285
+ if (lrId) {
127286
+ events.push({
127287
+ kind: "review.kill",
127288
+ workorderId: state.workorder.id,
127289
+ reviewId: lrId,
127290
+ reason: "exhausted"
127291
+ });
127292
+ pushReviewExhaustedEscalation(state, nodeId, workId, lrId, req.reviewerActorId, exhausted.rounds, events);
127293
+ }
127015
127294
  } else if (!overLimit) {
127016
127295
  events.push({
127017
127296
  kind: "review.create",
@@ -127202,6 +127481,7 @@ function assessConvergence(state, ctx) {
127202
127481
  const anyUnfinished = state.liveNodes().some((n) => !n.latestAcceptId);
127203
127482
  return { stuck: !anyRunning && !anyBlockingIssue && allIdle && anyUnfinished, codes };
127204
127483
  }
127484
+ var SYSTEM_ENGINE_ACTOR;
127205
127485
  var init_activate = __esm({
127206
127486
  "../engine/src/activate.ts"() {
127207
127487
  "use strict";
@@ -127216,6 +127496,7 @@ var init_activate = __esm({
127216
127496
  init_model();
127217
127497
  init_views2();
127218
127498
  init_kill();
127499
+ SYSTEM_ENGINE_ACTOR = ENGINE_SYSTEM_ACTOR;
127219
127500
  }
127220
127501
  });
127221
127502
 
@@ -135090,6 +135371,7 @@ var init_issue = __esm({
135090
135371
  "../engine/src/handlers/issue.ts"() {
135091
135372
  "use strict";
135092
135373
  init_plan();
135374
+ init_model();
135093
135375
  init_letters();
135094
135376
  issueCreate = {
135095
135377
  name: "issue/create",
@@ -135181,7 +135463,8 @@ var init_issue = __esm({
135181
135463
  }
135182
135464
  if (issue2.resolvedAt !== null) return;
135183
135465
  const humanResolution = e.by.startsWith("actor:human:") && e.by === ctx.actorId;
135184
- if (e.semanticsVersion === 2 && issue2.kind === "escalation" && !humanResolution) {
135466
+ const engineSelfClosure = e.by === ENGINE_SYSTEM_ACTOR && ctx.actorId === ENGINE_SYSTEM_ACTOR && issue2.id.startsWith(REVIEW_EXHAUSTED_ESC_PREFIX);
135467
+ if (e.semanticsVersion === 2 && issue2.kind === "escalation" && !humanResolution && !engineSelfClosure) {
135185
135468
  throw new Error("\u5173\u95ED\u5347\u7EA7\u9700\u8981\u4EBA\u660E\u786E\u786E\u8BA4\uFF1Bagent \u8BF7\u63D0\u4EA4\u5173\u95ED\u6388\u6743\u7533\u8BF7");
135186
135469
  }
135187
135470
  state.updateIssue(e.issueId, {
@@ -138078,6 +138361,13 @@ function openBackpressureOf(model, id) {
138078
138361
  function unresolvedGapsOf(model, id) {
138079
138362
  return (model.gaps.get(id) ?? []).filter((g2) => !g2.resolved);
138080
138363
  }
138364
+ function findGap(model, gapId) {
138365
+ for (const [artifactId, list2] of model.gaps) {
138366
+ const gap = list2.find((g2) => g2.gapId === gapId);
138367
+ if (gap) return { artifactId, gap };
138368
+ }
138369
+ return null;
138370
+ }
138081
138371
  function unresolvedEscalationsOf(model, id) {
138082
138372
  return (model.escalations.get(id) ?? []).filter((e) => !e.resolved).map((e) => ({
138083
138373
  // ADR 0079 D4:把稳定 id 一并带出——inbox / 卡片要靠它指明「关哪一条」(现状 resolve 是 artifact 级全清)。
@@ -142128,9 +142418,6 @@ ${supplemental.taskAppend.trim()}
142128
142418
  `;
142129
142419
  }
142130
142420
  }
142131
- if (this.opts.executionContinuityInstructions && continuityExposureOn && (spec.action === "produce" || spec.action === "integrate") && continuityArtifact) {
142132
- bundle.files["execution/CONTINUITY.md"] = this.opts.executionContinuityInstructions;
142133
- }
142134
142421
  if (this.opts.resolveExecutionResumePack && continuityExposureOn && (spec.action === "produce" || spec.action === "integrate") && continuityArtifact) {
142135
142422
  let resumePack = null;
142136
142423
  try {
@@ -142738,6 +143025,9 @@ ${supplemental.taskAppend.trim()}
142738
143025
  });
142739
143026
 
142740
143027
  // ../core/src/session-limits.ts
143028
+ function sessionWallClockMs(kind) {
143029
+ return SESSION_WALL_CLOCK_MS[kind];
143030
+ }
142741
143031
  var SESSION_WALL_CLOCK_MS;
142742
143032
  var init_session_limits = __esm({
142743
143033
  "../core/src/session-limits.ts"() {
@@ -142868,6 +143158,41 @@ ${lines.join("\n")}`, "rejected-proposal-repeat");
142868
143158
  }
142869
143159
  });
142870
143160
 
143161
+ // ../core/src/command-card-naming.ts
143162
+ function commandCardRefsOf(model) {
143163
+ const nodeTitle = (id) => model.artifacts.get(id)?.title?.trim() || void 0;
143164
+ return {
143165
+ nodeTitle,
143166
+ escalation: (escalationId) => {
143167
+ const found2 = escalationById(model, escalationId);
143168
+ if (!found2) return void 0;
143169
+ return { summary: found2.reason, ...nodeTitle(found2.artifactId) ? { nodeTitle: nodeTitle(found2.artifactId) } : {} };
143170
+ },
143171
+ gap: (gapId) => {
143172
+ const found2 = findGap(model, gapId);
143173
+ if (!found2) return void 0;
143174
+ return { summary: found2.gap.description, ...nodeTitle(found2.artifactId) ? { nodeTitle: nodeTitle(found2.artifactId) } : {} };
143175
+ }
143176
+ };
143177
+ }
143178
+ function annotateCommandCardLabels(model) {
143179
+ const refs = commandCardRefsOf(model);
143180
+ for (const review of model.pendingReviews.values()) {
143181
+ if (!review.command) continue;
143182
+ review.command = {
143183
+ ...review.command,
143184
+ displayLabel: commandCardDisplayLabel(review.command.command, review.command.args, review.command.effectLabel, refs)
143185
+ };
143186
+ }
143187
+ }
143188
+ var init_command_card_naming = __esm({
143189
+ "../core/src/command-card-naming.ts"() {
143190
+ "use strict";
143191
+ init_src();
143192
+ init_read_model();
143193
+ }
143194
+ });
143195
+
142871
143196
  // ../core/src/queue-retirement.ts
142872
143197
  function isSettledProduction(w2) {
142873
143198
  if (w2.lane === "reply") return false;
@@ -144183,6 +144508,7 @@ var init_kernel_bridge = __esm({
144183
144508
  init_closure_gate2();
144184
144509
  init_kernel();
144185
144510
  init_bridge_readmodel();
144511
+ init_command_card_naming();
144186
144512
  init_propose_guard();
144187
144513
  init_src6();
144188
144514
  init_src6();
@@ -144508,6 +144834,7 @@ var init_kernel_bridge = __esm({
144508
144834
  ...card2.priorRejections && card2.priorRejections.length > 0 ? { priorRejections: card2.priorRejections } : {}
144509
144835
  });
144510
144836
  }
144837
+ annotateCommandCardLabels(fresh);
144511
144838
  Object.assign(this.model, fresh);
144512
144839
  if (!this.sweepingStaleDrafts && this.draftCards.size > 0) {
144513
144840
  this.sweepingStaleDrafts = true;
@@ -146614,6 +146941,7 @@ var init_src7 = __esm({
146614
146941
  init_dispatcher();
146615
146942
  init_session_limits();
146616
146943
  init_rejected_proposals();
146944
+ init_command_card_naming();
146617
146945
  init_kernel_bridge();
146618
146946
  init_bridge_readmodel();
146619
146947
  init_queue_retirement();
@@ -165172,6 +165500,23 @@ async function waitForInflightInsert(rowId, timeoutMs) {
165172
165500
  })]);
165173
165501
  if (timer) clearTimeout(timer);
165174
165502
  }
165503
+ async function waitForAdmittedTurnStart(chatSessionId, deps) {
165504
+ if (deps.liveChat.isRunning(chatSessionId)) return;
165505
+ if (!await deps.ledgerTurnRunning()) return;
165506
+ await new Promise((resolve11) => {
165507
+ let stop = () => {
165508
+ };
165509
+ const timer = setTimeout(() => {
165510
+ stop();
165511
+ resolve11();
165512
+ }, deps.timeoutMs);
165513
+ timer.unref?.();
165514
+ stop = deps.liveChat.onNextTurn(chatSessionId, () => {
165515
+ clearTimeout(timer);
165516
+ resolve11();
165517
+ });
165518
+ });
165519
+ }
165175
165520
  async function appendNowThroughQueue(chatSessionId, text7, deps, clientKey, attachments, retainLocallyOnFailure = false, opts) {
165176
165521
  const newId = deps.newId ?? import_node_crypto22.randomUUID;
165177
165522
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
@@ -200719,6 +201064,150 @@ var init_knowledge2 = __esm({
200719
201064
  }
200720
201065
  });
200721
201066
 
201067
+ // ../server/src/chat-exit.ts
201068
+ function transcriptTail(ref2) {
201069
+ try {
201070
+ const stat4 = fs17.statSync(ref2);
201071
+ const size = Math.min(stat4.size, 4e3);
201072
+ const buf = Buffer.alloc(size);
201073
+ const fd = fs17.openSync(ref2, "r");
201074
+ try {
201075
+ fs17.readSync(fd, buf, 0, size, Math.max(0, stat4.size - size));
201076
+ } finally {
201077
+ fs17.closeSync(fd);
201078
+ }
201079
+ return buf.toString("utf8").trim();
201080
+ } catch {
201081
+ return "";
201082
+ }
201083
+ }
201084
+ function clipLine(line2) {
201085
+ return line2.length > SUMMARY_MAX_CHARS ? `${line2.slice(0, SUMMARY_MAX_CHARS)}\u2026` : line2;
201086
+ }
201087
+ function providerMessage(line2) {
201088
+ const m2 = /['"]message['"]\s*:\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")/.exec(line2);
201089
+ const text7 = (m2?.[1] ?? m2?.[2])?.trim();
201090
+ return text7 ? text7 : void 0;
201091
+ }
201092
+ function failureKind(reason, line2) {
201093
+ if (line2.startsWith(SESSION_STARTUP_FAILED_PREFIX)) return "startup-failed";
201094
+ switch (reason) {
201095
+ case "auth-error":
201096
+ return "auth";
201097
+ case "server-unreachable":
201098
+ return "node-unreachable";
201099
+ case "timeout":
201100
+ return "timeout";
201101
+ case "output-limit":
201102
+ return "output-limit";
201103
+ case "rate-limit":
201104
+ return QUOTA_RE3.test(line2) ? "quota" : "rate-limit";
201105
+ default:
201106
+ break;
201107
+ }
201108
+ if (!line2) return "unknown";
201109
+ if (DISK_FULL_RE.test(line2)) return "disk-full";
201110
+ if (QUOTA_RE3.test(line2)) return "quota";
201111
+ if (AUTH_RE2.test(line2)) return "auth";
201112
+ if (RATE_LIMIT_RE.test(line2)) return "rate-limit";
201113
+ if (MODEL_RE.test(line2)) return "model";
201114
+ if (UNREACHABLE_RE2.test(line2)) return "node-unreachable";
201115
+ return "unknown";
201116
+ }
201117
+ function setChatWallClockMs(ms) {
201118
+ effectiveChatWallClockMs = typeof ms === "number" && ms > 0 ? ms : void 0;
201119
+ }
201120
+ function wallClockText(ms = effectiveChatWallClockMs ?? sessionWallClockMs("chat")) {
201121
+ const minutes = Math.round(ms / 6e4);
201122
+ return minutes > 0 && minutes % 60 === 0 ? `${minutes / 60} \u5C0F\u65F6` : `${minutes} \u5206\u949F`;
201123
+ }
201124
+ function resetTime(line2) {
201125
+ return extractResetHint(line2)?.replace(/^(?:resets?|try again at)\s+/i, "").trim() || void 0;
201126
+ }
201127
+ function failureSummary(kind, ctx) {
201128
+ switch (kind) {
201129
+ case "auth":
201130
+ return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u65F6\u7684\u767B\u5F55\u5DF2\u8FC7\u671F\u6216\u51ED\u8BC1\u65E0\u6548\uFF0C\u8C03\u7528\u4E0D\u4E86\u6A21\u578B\u3002\u8BF7\u8282\u70B9\u7BA1\u7406\u5458\u5728\u90A3\u53F0\u673A\u5668\u4E0A\u91CD\u65B0\u767B\u5F55\u8FD9\u4E2A\u8FD0\u884C\u65F6\uFF08\u6216\u66F4\u65B0 API Key\uFF09\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u7740\u6025\u7684\u8BDD\u4E5F\u53EF\u4EE5${chatRebindHint("\u6362\u4E00\u4E2A\u53EF\u7528\u7684\u8FD0\u884C\u65F6")}\u3002`;
201131
+ case "quota": {
201132
+ const at = resetTime(ctx.line);
201133
+ return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u6A21\u578B\u989D\u5EA6\u5DF2\u7528\u5B8C${at ? `\uFF0C\u9884\u8BA1 ${at} \u6062\u590D` : ""}\u3002\u8BF7\u7B49\u989D\u5EA6\u6062\u590D\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u7740\u6025\u7684\u8BDD\u53EF\u4EE5${chatRebindHint("\u6362\u4E00\u4E2A\u989D\u5EA6\u5145\u8DB3\u7684\u8FD0\u884C\u65F6")}\u3002`;
201134
+ }
201135
+ case "rate-limit":
201136
+ return "\u5BF9\u8BDD\u5931\u8D25\uFF1A\u6A21\u578B\u670D\u52A1\u6682\u65F6\u9650\u6D41\u6216\u7E41\u5FD9\u3002\u8BF7\u7B49\u4E00\u4E24\u5206\u949F\u518D\u53D1\u4E00\u6B21\u3002";
201137
+ case "node-unreachable":
201138
+ return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u8FD9\u4F4D Agent \u7684\u8282\u70B9\u79BB\u7EBF\u6216\u8FDE\u63A5\u4E2D\u65AD\u3002\u8BF7\u786E\u8BA4\u90A3\u53F0\u673A\u5668\u5F00\u7740\u3001\u8282\u70B9\u7A0B\u5E8F\u5728\u8FD0\u884C\u3001\u7F51\u7EDC\u6B63\u5E38\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u4E5F\u53EF\u4EE5${chatRebindHint("\u6362\u4E00\u4E2A\u5728\u7EBF\u7684\u8FD0\u884C\u65F6")}\u3002`;
201139
+ case "model":
201140
+ 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`;
201141
+ case "timeout":
201142
+ return `\u8FD9\u4E00\u6B21\u6267\u884C\u8DD1\u6EE1\u4E86\u5355\u6B21\u8FD0\u884C\u7684\u65F6\u95F4\u4E0A\u9650\uFF08${wallClockText()}\uFF09\uFF0C\u88AB\u7CFB\u7EDF\u505C\u4E0B\u4E86\u3002\u5DF2\u7ECF\u505A\u5B8C\u7684\u90E8\u5206\u4E0D\u4F1A\u4E22\uFF1B\u8981\u63A5\u7740\u505A\u7684\u8BDD\uFF0C\u628A\u5269\u4E0B\u7684\u90E8\u5206\u62C6\u5C0F\u4E00\u4E9B\u518D\u53D1\u4E00\u6B21\u3002`;
201143
+ case "startup-failed":
201144
+ 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`;
201145
+ case "disk-full":
201146
+ return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u8FD9\u4F4D Agent \u7684\u8282\u70B9\u78C1\u76D8\u7A7A\u95F4\u6EE1\u4E86\uFF0C\u8FD9\u4E00\u8F6E\u5199\u4E0D\u4E86\u6587\u4EF6\u3002\u8BF7\u8282\u70B9\u7BA1\u7406\u5458\u6E05\u7406\u90A3\u53F0\u673A\u5668\u7684\u78C1\u76D8\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u7740\u6025\u7684\u8BDD\u4E5F\u53EF\u4EE5${chatRebindHint("\u6362\u4E00\u4E2A\u8FD0\u884C\u65F6")}\u3002`;
201147
+ case "output-limit":
201148
+ 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";
201149
+ case "unknown":
201150
+ if (ctx.line) {
201151
+ return "\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u65F6\u62A5\u9519\u4E86\uFF0C\u6682\u65F6\u6CA1\u8BA4\u51FA\u662F\u4EC0\u4E48\u539F\u56E0\u3002\u53EF\u4EE5\u7A0D\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\uFF0C\u8BF7\u70B9\u300C\u5C55\u5F00\u8BE6\u60C5\u300D\u628A\u539F\u59CB\u62A5\u9519\u53D1\u7ED9\u7BA1\u7406\u5458\u3002";
201152
+ }
201153
+ return ctx.hasDetail ? "\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u65F6\u5F02\u5E38\u9000\u51FA\uFF0C\u6CA1\u6709\u7ED9\u51FA\u62A5\u9519\u539F\u56E0\u3002\u53EF\u4EE5\u7A0D\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\uFF0C\u8BF7\u70B9\u300C\u5C55\u5F00\u8BE6\u60C5\u300D\u628A\u539F\u59CB\u8BB0\u5F55\u53D1\u7ED9\u7BA1\u7406\u5458\u3002" : `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u65F6\u5F02\u5E38\u9000\u51FA\uFF08\u9000\u51FA\u7801 ${ctx.code ?? "\u65E0"}\uFF09\uFF0C\u6CA1\u6709\u7559\u4E0B\u62A5\u9519\u4FE1\u606F\u3002\u53EF\u4EE5\u7A0D\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\uFF0C\u8BF7\u7BA1\u7406\u5458\u5230\u300C\u8FD0\u884C\u8F68\u8FF9\u300D\u91CC\u67E5\u8FD9\u4E00\u8F6E\u3002`;
201154
+ }
201155
+ }
201156
+ function buildChatExitError(info) {
201157
+ const chatExit = chatExitSummary(info);
201158
+ return Object.assign(new Error(chatExit.summary), { chatExit });
201159
+ }
201160
+ function chatExitSummary(info) {
201161
+ const genericLine = `runtime exited with code ${info.code ?? "null"}${info.reason ? ` (${info.reason})` : ""}`;
201162
+ let detail = "";
201163
+ if (info.reason !== "cancelled") {
201164
+ detail = info.errorMessage?.trim() ?? "";
201165
+ if (!detail && info.transcriptRef) detail = transcriptTail(info.transcriptRef);
201166
+ }
201167
+ let summary;
201168
+ let cancelledBy;
201169
+ if (info.reason === "cancelled") {
201170
+ cancelledBy = cancelCauseFromExitMessage(info.errorMessage);
201171
+ summary = chatCancelNote(cancelledBy);
201172
+ } else {
201173
+ const firstErrorLine = (info.errorMessage?.split("\n").map((line2) => line2.trim()).find(Boolean) ?? "").replace(INTERNAL_PREFIX_RE, "");
201174
+ const readable = firstErrorLine ? clipLine(providerMessage(firstErrorLine) ?? firstErrorLine) : "";
201175
+ summary = failureSummary(failureKind(info.reason, firstErrorLine), {
201176
+ readable,
201177
+ line: firstErrorLine,
201178
+ hasDetail: Boolean(detail),
201179
+ code: info.code
201180
+ });
201181
+ }
201182
+ return {
201183
+ ...info.reason ? { reason: info.reason } : {},
201184
+ summary,
201185
+ ...cancelledBy ? { cancelledBy } : {},
201186
+ ...info.reason !== "cancelled" ? { detail: (detail ? `${genericLine}
201187
+
201188
+ ${detail}` : genericLine).slice(0, 4200) } : {}
201189
+ };
201190
+ }
201191
+ var fs17, SUMMARY_MAX_CHARS, INTERNAL_PREFIX_RE, QUOTA_RE3, RATE_LIMIT_RE, AUTH_RE2, MODEL_RE, DISK_FULL_RE, UNREACHABLE_RE2, effectiveChatWallClockMs, chatRebindHint;
201192
+ var init_chat_exit = __esm({
201193
+ "../server/src/chat-exit.ts"() {
201194
+ "use strict";
201195
+ fs17 = __toESM(require("node:fs"), 1);
201196
+ init_src();
201197
+ init_src7();
201198
+ init_chat_cancel_attribution();
201199
+ SUMMARY_MAX_CHARS = 500;
201200
+ INTERNAL_PREFIX_RE = /^\[[a-z][a-z0-9_]*\]\s*/;
201201
+ QUOTA_RE3 = /hit your (?:\w+ )?limit|usage limit|quota|insufficient[_ ](?:balance|credit|funds)|credit balance|billing|\b402\b|额度|余额/i;
201202
+ RATE_LIMIT_RE = /rate.?limit|too many requests|\b429\b|\b529\b|overloaded|限流/i;
201203
+ AUTH_RE2 = /\b401\b|\b403\b|unauthori[sz]ed|authenticat|oauth|invalid[_ -]?(?:api[_ -]?key|token|x-api-key)|api[_ -]?key[^\n]{0,40}\b(?:invalid|expired|revoked)|token[^\n]{0,30}\b(?:expired|invalid|revoked)|not logged in|\/login|credential|凭证|登录/i;
201204
+ 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;
201205
+ DISK_FULL_RE = /\bENOSPC\b|no space left on device|\bEDQUOT\b|disk quota exceeded|\[Errno 28\]|disk (?:is )?full|磁盘(?:空间)?(?:已满|不足)/i;
201206
+ UNREACHABLE_RE2 = /不在线|socket 非 OPEN|帧未送达|失联/;
201207
+ chatRebindHint = (what) => `\u5728\u7EC4\u7EC7\u9875\u8FD9\u4F4D Agent \u7684\u300C\u6A21\u578B\u300D\u9875\u7B7E\u91CC${what}`;
201208
+ }
201209
+ });
201210
+
200722
201211
  // ../server/src/domains/delegations/parent-conversation.ts
200723
201212
  function firstLine2(content3) {
200724
201213
  const line2 = content3.split("\n").map((s2) => s2.trim()).find((s2) => s2.length > 0);
@@ -201119,6 +201608,14 @@ var init_files = __esm({
201119
201608
  });
201120
201609
 
201121
201610
  // ../server/src/domains/delegations/service.ts
201611
+ function exitReasonOf(err) {
201612
+ if (!(err instanceof Error)) return void 0;
201613
+ const chatExit = err.chatExit;
201614
+ return chatExit?.reason;
201615
+ }
201616
+ function roundFailureText(kind, error2) {
201617
+ return kind === "timeout" ? `\u8FD9\u4E00\u6B21\u6267\u884C\u8DD1\u6EE1\u4E86\u5355\u6B21\u8FD0\u884C\u7684\u65F6\u95F4\u4E0A\u9650\uFF08${wallClockText()}\uFF09\uFF0C\u88AB\u7CFB\u7EDF\u505C\u4E0B\u4E86\uFF08${error2}\uFF09` : `\u8FD9\u4E00\u8F6E\u6CA1\u8DD1\u5B8C\uFF1A${error2}`;
201618
+ }
201122
201619
  function deterministicUuid2(seed) {
201123
201620
  const h = (0, import_node_crypto36.createHash)("sha256").update(seed).digest();
201124
201621
  const b2 = Buffer.from(h.subarray(0, 16));
@@ -201181,6 +201678,7 @@ var init_service3 = __esm({
201181
201678
  "use strict";
201182
201679
  import_node_crypto36 = require("node:crypto");
201183
201680
  init_src();
201681
+ init_chat_exit();
201184
201682
  init_parent_conversation();
201185
201683
  init_title();
201186
201684
  init_deletion();
@@ -201868,7 +202366,7 @@ var init_service3 = __esm({
201868
202366
  try {
201869
202367
  await dispatched.done;
201870
202368
  } catch (err) {
201871
- failureKind2 = "interrupted";
202369
+ failureKind2 = exitReasonOf(err) === "timeout" ? "timeout" : "interrupted";
201872
202370
  error2 = err instanceof Error ? err.message : String(err);
201873
202371
  }
201874
202372
  const at = this.now();
@@ -201885,7 +202383,11 @@ var init_service3 = __esm({
201885
202383
  const round = (fresh.roundSummaries[fresh.roundSummaries.length - 1]?.round ?? 0) + 1;
201886
202384
  const newSummary = {
201887
202385
  round,
201888
- text: text7 || (failureKind2 ? `\u8FD9\u4E00\u8F6E\u6CA1\u8DD1\u5B8C\uFF1A${error2}` : ""),
202386
+ /* ⚠️ 到点被停下那一档**不往摘要里合成句子**(Bug 0221):`text` 从此是「专家这一轮到底
202387
+ 写出没写出正文」的判据——回流通告据它判「有没有可验证的交付」(`hasDelegationDelivery`)。
202388
+ 合成的那句话仍写进下面子会话的占位行(用户在子会话里要看到一句交代),
202389
+ 通告里空正文由 `delegationReturnText` 渲染成「(这一轮没有文本产出)」。 */
202390
+ text: text7 || (failureKind2 && failureKind2 !== "timeout" ? roundFailureText(failureKind2, error2) : ""),
201889
202391
  at,
201890
202392
  // 本轮的键 = 派发这一轮时的 dispatchSeq(`record` 就是派发时那一份,轮次账本业务键也由它算)。
201891
202393
  dispatchSeq: record9.dispatchSeq ?? 0
@@ -201895,7 +202397,7 @@ var init_service3 = __esm({
201895
202397
  const stopLoss = consecutiveFails >= CONSECUTIVE_FAIL_LIMIT;
201896
202398
  const chatStore = await this.options.resolveChatSessions(companyId).catch(() => null);
201897
202399
  await chatStore?.updateMessage(childRoundMessageIds(fresh.childSessionId, round).placeholderId, {
201898
- content: text7 || (failureKind2 ? `\u8FD9\u4E00\u8F6E\u6CA1\u8DD1\u5B8C\uFF1A${error2}` : ""),
202400
+ content: text7 || (failureKind2 ? roundFailureText(failureKind2, error2) : ""),
201899
202401
  status: failureKind2 ? "error" : "done",
201900
202402
  completedAt: at
201901
202403
  }).catch(() => void 0);
@@ -201968,9 +202470,11 @@ var init_service3 = __esm({
201968
202470
  if (opts?.outcome === "succeeded") return this.settleSucceededFromSweep(record9, dispatchSeq, companyId);
201969
202471
  const at = this.now();
201970
202472
  const settledSeqAtRound = record9.settledSeq + 1;
202473
+ const timedOut = opts?.exitReason === "timeout";
202474
+ const partialAnswer = timedOut ? await this.readChildRoundAnswer(record9, dispatchSeq + 1, companyId).catch(() => "") : "";
201971
202475
  const newSummary = {
201972
202476
  round: (record9.roundSummaries[record9.roundSummaries.length - 1]?.round ?? 0) + 1,
201973
- text: `\u8FD9\u4E00\u8F6E\u88AB\u65F6\u949F\u95ED\u5408\uFF1A${reason}`,
202477
+ text: timedOut ? partialAnswer : `\u8FD9\u4E00\u8F6E\u88AB\u65F6\u949F\u95ED\u5408\uFF1A${reason}`,
201974
202478
  at,
201975
202479
  settledSeq: settledSeqAtRound,
201976
202480
  dispatchSeq
@@ -201978,7 +202482,8 @@ var init_service3 = __esm({
201978
202482
  const settled = await store.appendRoundSummary(record9.id, newSummary, {
201979
202483
  state: "failed",
201980
202484
  // 退出帧丢失后被时钟闭合 = `interrupted`(§9),不是 `not-started`——它确实起来过。
201981
- failureKind: "interrupted",
202485
+ // run 报的是墙钟超时时归 `timeout`(Bug 0221):同样起来过,但「到点被停」与「断了」说法不同。
202486
+ failureKind: timedOut ? "timeout" : "interrupted",
201982
202487
  consecutiveFails: record9.consecutiveFails + 1,
201983
202488
  settledSeq: settledSeqAtRound,
201984
202489
  settledAt: at
@@ -202579,11 +203084,11 @@ var init_routes2 = __esm({
202579
203084
  });
202580
203085
 
202581
203086
  // ../server/src/domains/delegations/store.ts
202582
- var fs17, path18, touchKey, settlesTerminal, MemoryDelegationStore, FileDelegationStore;
203087
+ var fs18, path18, touchKey, settlesTerminal, MemoryDelegationStore, FileDelegationStore;
202583
203088
  var init_store2 = __esm({
202584
203089
  "../server/src/domains/delegations/store.ts"() {
202585
203090
  "use strict";
202586
- fs17 = __toESM(require("node:fs"), 1);
203091
+ fs18 = __toESM(require("node:fs"), 1);
202587
203092
  path18 = __toESM(require("node:path"), 1);
202588
203093
  init_src();
202589
203094
  touchKey = (delegationId, messageId) => `${delegationId}\0${messageId}`;
@@ -202727,13 +203232,13 @@ var init_store2 = __esm({
202727
203232
  }
202728
203233
  static async open(file) {
202729
203234
  const store = new _FileDelegationStore(file);
202730
- if (fs17.existsSync(file)) {
202731
- const snap = JSON.parse(fs17.readFileSync(file, "utf8"));
203235
+ if (fs18.existsSync(file)) {
203236
+ const snap = JSON.parse(fs18.readFileSync(file, "utf8"));
202732
203237
  for (const d of snap.delegations ?? []) store.rows.set(d.id, d);
202733
203238
  for (const t of snap.touches ?? []) store.touches.set(touchKey(t.delegationId, t.messageId), t);
202734
203239
  } else {
202735
- fs17.mkdirSync(path18.dirname(file), { recursive: true });
202736
- fs17.writeFileSync(file, JSON.stringify({ delegations: [], touches: [] }, null, 2));
203240
+ fs18.mkdirSync(path18.dirname(file), { recursive: true });
203241
+ fs18.writeFileSync(file, JSON.stringify({ delegations: [], touches: [] }, null, 2));
202737
203242
  }
202738
203243
  return store;
202739
203244
  }
@@ -202742,7 +203247,7 @@ var init_store2 = __esm({
202742
203247
  delegations: [...this.rows.values()],
202743
203248
  touches: [...this.touches.values()]
202744
203249
  };
202745
- fs17.writeFileSync(this.file, JSON.stringify(snapshot, null, 2));
203250
+ fs18.writeFileSync(this.file, JSON.stringify(snapshot, null, 2));
202746
203251
  }
202747
203252
  async create(record9) {
202748
203253
  const r = await super.create(record9);
@@ -202946,8 +203451,18 @@ function delegationReturnText(terminal, opts) {
202946
203451
  const { record: record9 } = terminal;
202947
203452
  const who = terminal.expertName ?? record9.expertActorId;
202948
203453
  const name = record9.label ? `${who}\uFF08${record9.label}\uFF09` : who;
202949
- const head = record9.state === "done" ? `\u3010\u7CFB\u7EDF\u3011${name} \u5DF2\u5B8C\u6210\u3002` : `\u3010\u7CFB\u7EDF\u3011${name} \u65E0\u6CD5\u5B8C\u6210${record9.failureKind ? `\uFF08${record9.failureKind}\uFF09` : ""}\u3002`;
202950
- const lines = [head, `\u59D4\u6D3E id\uFF1A${record9.id.slice(0, 8)}\u3000label\uFF1A${record9.label ?? "\uFF08\u65E0\uFF09"}`];
203454
+ const lastRoundText = record9.roundSummaries[record9.roundSummaries.length - 1]?.text;
203455
+ const ending = delegationEnding(record9, {
203456
+ movedFiles: opts.movedPaths.length,
203457
+ unchangedFiles: opts.unchangedPaths?.length ?? 0,
203458
+ ...lastRoundText ? { roundText: lastRoundText } : {}
203459
+ });
203460
+ const cap = wallClockText();
203461
+ const head = ending === "done" ? `\u3010\u7CFB\u7EDF\u3011${name} \u5DF2\u5B8C\u6210\u3002` : ending === "timeout-delivered" ? `\u3010\u7CFB\u7EDF\u3011${name} \u8FD9\u4E00\u6B21\u6267\u884C\u8DD1\u6EE1\u4E86\u5355\u6B21\u8FD0\u884C\u7684\u65F6\u95F4\u4E0A\u9650\uFF08${cap}\uFF09\uFF0C\u88AB\u7CFB\u7EDF\u505C\u4E0B\u4E86\u2014\u2014\u5DF2\u7ECF\u4EA4\u56DE\u7684\u5185\u5BB9\u89C1\u4E0B\u3002` : ending === "timeout-empty" ? `\u3010\u7CFB\u7EDF\u3011${name} \u8FD9\u4E00\u6B21\u6267\u884C\u8DD1\u6EE1\u4E86\u5355\u6B21\u8FD0\u884C\u7684\u65F6\u95F4\u4E0A\u9650\uFF08${cap}\uFF09\uFF0C\u88AB\u7CFB\u7EDF\u505C\u4E0B\u4E86\uFF0C\u8FD9\u4E00\u8F6E\u6CA1\u6709\u4EA4\u56DE\u4EFB\u4F55\u5185\u5BB9\u3002` : `\u3010\u7CFB\u7EDF\u3011${name} \u65E0\u6CD5\u5B8C\u6210\u3002`;
203462
+ const lines = [
203463
+ head,
203464
+ `\u59D4\u6D3E id\uFF1A${record9.id.slice(0, 8)}\u3000label\uFF1A${record9.label ?? "\uFF08\u65E0\uFF09"}` + (record9.failureKind ? `\u3000\u7ED3\u675F\u539F\u56E0\uFF1A${record9.failureKind}` : "")
203465
+ ];
202951
203466
  const trimmed = trimRoundSummariesForReturn(record9.roundSummaries, opts.recentRounds);
202952
203467
  if (trimmed.length > 0) {
202953
203468
  lines.push("", `\u4E13\u5BB6\u5404\u8F6E\u5C0F\u7ED3\uFF08\u5171 ${trimmed.length} \u8F6E\uFF1B\u6700\u8FD1 ${opts.recentRounds} \u8F6E\u7ED9\u5168\u6587\uFF0C\u66F4\u65E9\u7684\u53EA\u7559\u9996\u884C\uFF0C\u5168\u6587\u7528 \`oasis delegate --status\` \u770B\uFF09\uFF1A`);
@@ -202982,7 +203497,7 @@ function delegationReturnText(terminal, opts) {
202982
203497
  }
202983
203498
  lines.push(
202984
203499
  "",
202985
- record9.state === "done" ? "\u8FD9\u4E9B\u662F\u6750\u6599\uFF0C\u4E0D\u662F\u7ED9\u7528\u6237\u7684\u6210\u54C1\uFF1A\u4F60\u6709\u8FD9\u6761\u5BF9\u8BDD\u7684\u5B8C\u6574\u4E0A\u4E0B\u6587\uFF0C\u81EA\u5DF1\u5224\u65AD\u8BE5\u8BB2\u54EA\u4E00\u53E5\u3001\u8981\u4E0D\u8981\u63A5\u7740\u5F80\u4E0B\u505A\u3002" : "\u5148\u81EA\u5DF1\u5224\u65AD\u80FD\u4E0D\u80FD\u63A5\u4F4F\uFF1A\u63A5\u7BA1\u3001\u6362\u4E2A\u505A\u6CD5\u3001\u8FD8\u662F\u91CD\u6D3E\u3002**\u522B\u628A\u4E13\u4E1A\u5931\u8D25\u539F\u6837\u4E22\u56DE\u7ED9\u7528\u6237**\u3002"
203500
+ record9.state === "done" ? "\u8FD9\u4E9B\u662F\u6750\u6599\uFF0C\u4E0D\u662F\u7ED9\u7528\u6237\u7684\u6210\u54C1\uFF1A\u4F60\u6709\u8FD9\u6761\u5BF9\u8BDD\u7684\u5B8C\u6574\u4E0A\u4E0B\u6587\uFF0C\u81EA\u5DF1\u5224\u65AD\u8BE5\u8BB2\u54EA\u4E00\u53E5\u3001\u8981\u4E0D\u8981\u63A5\u7740\u5F80\u4E0B\u505A\u3002" : ending === "timeout-delivered" ? "**\u8FD9\u4E0D\u662F\u4E13\u4E1A\u5931\u8D25\uFF0C\u662F\u65F6\u95F4\u5230\u4E86**\uFF1A\u5148\u770B\u4E0A\u9762\u5DF2\u7ECF\u4EA4\u56DE\u7684\u4E1C\u897F\uFF0C\u80FD\u7528\u5C31\u63A5\u7740\u7528\uFF1B\u8FD8\u6CA1\u505A\u5B8C\u7684\u90E8\u5206\uFF0C\u62C6\u6210\u4E00\u8F6E\u8DD1\u5F97\u5B8C\u7684\u91CF\u518D\u6D3E\u4E00\u6B21\u3002\u7ED9\u7528\u6237\u4EA4\u4EE3\u65F6\u522B\u8BF4\u300C\u65E0\u6CD5\u5B8C\u6210\u300D\u2014\u2014\u5DF2\u7ECF\u505A\u6210\u7684\u90E8\u5206\u7167\u5B9E\u8BF4\u3002" : ending === "timeout-empty" ? "**\u8FD9\u4E0D\u662F\u4E13\u4E1A\u5931\u8D25\uFF0C\u662F\u65F6\u95F4\u5230\u4E86**\uFF1A\u8FD9\u4E00\u8F6E\u6CA1\u6709\u7559\u4E0B\u53EF\u7528\u7684\u4EA7\u51FA\uFF0C\u628A\u4EFB\u52A1\u62C6\u5C0F\u4E00\u4E9B\u518D\u6D3E\u4E00\u6B21\u3002" : "\u5148\u81EA\u5DF1\u5224\u65AD\u80FD\u4E0D\u80FD\u63A5\u4F4F\uFF1A\u63A5\u7BA1\u3001\u6362\u4E2A\u505A\u6CD5\u3001\u8FD8\u662F\u91CD\u6D3E\u3002**\u522B\u628A\u4E13\u4E1A\u5931\u8D25\u539F\u6837\u4E22\u56DE\u7ED9\u7528\u6237**\u3002"
202986
203501
  );
202987
203502
  if (opts.movedPaths.length > 0 || (opts.unchangedPaths?.length ?? 0) > 0) {
202988
203503
  lines.push(
@@ -203136,6 +203651,7 @@ var init_return_flow = __esm({
203136
203651
  "use strict";
203137
203652
  init_chat_session();
203138
203653
  init_src();
203654
+ init_chat_exit();
203139
203655
  init_chat_system_message();
203140
203656
  init_chat_broadcast();
203141
203657
  init_files();
@@ -208277,7 +208793,7 @@ async function resolveProposeContent(kernel, blobs, args, artifactId, ctx) {
208277
208793
  }
208278
208794
  return resolveContent2(blobs, args);
208279
208795
  }
208280
- function authorizedCommandReceipt(command2, args, actor) {
208796
+ function authorizedCommandReceipt(command2, args, actor, refs = {}) {
208281
208797
  switch (command2) {
208282
208798
  case "continueBatch":
208283
208799
  return { message: `\u5DF2\u6388\u6743\u518D\u8FD0\u884C ${args.rounds} \u8F6E\uFF1B\u5176\u4ED6\u963B\u585E\u89E3\u9664\u540E\u7EE7\u7EED\u3002` };
@@ -208286,7 +208802,7 @@ function authorizedCommandReceipt(command2, args, actor) {
208286
208802
  case "promote":
208287
208803
  return { message: "promoted to persistent" };
208288
208804
  case "resolveEscalation":
208289
- return { message: optStr(args, "gapId") ? `\u5DF2\u786E\u8BA4\u6062\u590D\u65B9\u6848\uFF0C\u89E3\u51B3\u7F3A\u53E3 ${str(args, "gapId")}\u3001\u5173\u95ED\u5173\u8054\u4E0A\u62A5 ${str(args, "escalationId")}\uFF1B\u8C03\u5EA6\u5668\u5C06\u5728\u5176\u4ED6\u963B\u585E\u89E3\u9664\u540E\u7EE7\u7EED\u6267\u884C\u3002` : `\u5DF2\u5173\u95ED\u8BE5\u6761\u4E0A\u62A5\uFF08${str(args, "escalationId")}\uFF09\u3002` };
208805
+ return { message: optStr(args, "gapId") ? `\u5DF2\u786E\u8BA4\u6062\u590D\u65B9\u6848\uFF0C${actionPhrase("\u89E3\u51B3", "\u7F3A\u53E3", gapPhraseOf(refs, optStr(args, "gapId")))}\u3001${actionPhrase("\u5173\u95ED\u5173\u8054", "\u6C42\u52A9", escalationPhraseOf(refs, optStr(args, "escalationId")), "\u5173\u95ED\u5173\u8054\u7684")}\uFF1B\u8C03\u5EA6\u5668\u5C06\u5728\u5176\u4ED6\u963B\u585E\u89E3\u9664\u540E\u7EE7\u7EED\u6267\u884C\u3002` : `\u5DF2${actionPhrase("\u5173\u95ED", "\u6C42\u52A9", escalationPhraseOf(refs, optStr(args, "escalationId")))}\u3002` };
208290
208806
  default:
208291
208807
  throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u65E0\u56DE\u6267\u6784\u9020 ${command2}`);
208292
208808
  }
@@ -209008,7 +209524,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
209008
209524
  ...optStr(args, "reason") !== void 0 ? { reason: optStr(args, "reason") } : {},
209009
209525
  ...escId !== void 0 ? { escalationId: escId } : {}
209010
209526
  });
209011
- return { message: escId !== void 0 ? `\u5DF2\u5173\u95ED\u8BE5\u6761\u4E0A\u62A5\uFF08${escId}\uFF09\u3002` : "\u8BE5\u8282\u70B9\u7684\u672A\u89E3\u4E0A\u62A5\u5DF2\u5168\u90E8\u5173\u95ED\u3002" };
209527
+ return { message: escId !== void 0 ? `\u5DF2${actionPhrase("\u5173\u95ED", "\u6C42\u52A9", escalationPhraseOf(commandCardRefsOf(kernel.model), escId))}\u3002` : "\u8BE5\u8282\u70B9\u7684\u672A\u89E3\u4E0A\u62A5\u5DF2\u5168\u90E8\u5173\u95ED\u3002" };
209012
209528
  }
209013
209529
  case "addRevision": {
209014
209530
  const addRevId = str(args, "artifactId");
@@ -209074,7 +209590,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
209074
209590
  }
209075
209591
  case "promote": {
209076
209592
  await kernel.promote({ artifactId: str(args, "artifactId"), actor });
209077
- return authorizedCommandReceipt(command2, args, actor);
209593
+ return authorizedCommandReceipt(command2, args, actor, commandCardRefsOf(kernel.model));
209078
209594
  }
209079
209595
  case "hold": {
209080
209596
  const artifactId = str(args, "artifactId");
@@ -209118,7 +209634,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
209118
209634
  reason: str(args, "reason"),
209119
209635
  note: optStr(args, "note")
209120
209636
  });
209121
- return authorizedCommandReceipt(command2, args, actor);
209637
+ return authorizedCommandReceipt(command2, args, actor, commandCardRefsOf(kernel.model));
209122
209638
  }
209123
209639
  case "unlink": {
209124
209640
  await kernel.unlinkInput({ artifactId: str(args, "artifactId"), to: str(args, "to"), actor });
@@ -209644,19 +210160,20 @@ async function startOasisServer(opts) {
209644
210160
  return { accepted: r.accepted, ...r.reason ? { reason: r.reason } : {}, queued: r.queued, insertStatus: r.insertStatus };
209645
210161
  };
209646
210162
  const GRAPH_EDIT_SUMMARY_MAX = 60;
210163
+ const NEUTRAL_APPROVER = "\u8D1F\u8D23\u4EBA";
209647
210164
  const truncateSummary = (s2) => {
209648
210165
  const cps = [...s2];
209649
210166
  return cps.length > GRAPH_EDIT_SUMMARY_MAX ? `${cps.slice(0, GRAPH_EDIT_SUMMARY_MAX).join("")}\u2026` : s2;
209650
210167
  };
209651
210168
  const authorizationStatusText = (resolution, ctx) => {
209652
210169
  const approved = resolution.status === "approved";
209653
- const by = ctx.authorizedBy ?? "\u7BA1\u7406\u8005";
210170
+ const by = ctx.authorizedByName?.trim() || NEUTRAL_APPROVER;
209654
210171
  const reason = resolution.status === "denied" ? resolution.reason ?? ctx.reason : ctx.reason;
209655
210172
  if (ctx.kind === "graph-edit") {
209656
210173
  const summary = truncateSummary((ctx.effectLabel ?? "").trim()) || "\uFF08\u672A\u586B\u5199\u7406\u7531\u7684\u56FE\u53D8\u66F4\uFF09";
209657
210174
  return approved ? `\u300C${summary}\u300D\u7684\u53D8\u66F4\u5DF2\u7531 ${by} \u6279\u51C6\u5E76\u843D\u5730\u3002` : `\u300C${summary}\u300D\u7684\u53D8\u66F4\u5DF2\u88AB ${by} \u9A73\u56DE${reason ? `\uFF1B\u7406\u7531\uFF1A${reason}` : ""}\u3002`;
209658
210175
  }
209659
- const label2 = ctx.effectLabel || ctx.command || "";
210176
+ const label2 = ctx.displayLabel || stripInternalIds(ctx.effectLabel ?? "") || ctx.command || "";
209660
210177
  return approved ? `\u547D\u4EE4\u5DF2\u7531 ${by} \u6279\u51C6\u5E76\u4EE5\u5176\u540D\u4E49\u6267\u884C${label2 ? `\uFF1A${label2}` : ""}\u3002` : `\u547D\u4EE4\u5DF2\u88AB ${by} \u9A73\u56DE${reason ? `\uFF1A${reason}` : ""}\u3002`;
209661
210178
  };
209662
210179
  const GRAPH_EDIT_IN_FLIGHT_SUFFIX = "\uFF08\u8FD9\u6761\u662F\u5728\u4F60\u672C\u8F6E\u8FDB\u884C\u4E2D\u63D2\u5165\u7684\u901A\u77E5\uFF1A\u56DE\u5E94\u5B8C\u540E\u63A5\u7740\u505A\u4F60\u624B\u4E0A\u8FD8\u6CA1\u505A\u5B8C\u7684\u4E8B\uFF0C\u522B\u5C31\u6B64\u6536\u5C3E\u3002\uFF09";
@@ -211079,13 +211596,23 @@ async function startOasisServer(opts) {
211079
211596
  const { draftId } = JSON.parse(rawBody);
211080
211597
  const review = engine.kernel.model.pendingReviews.get(draftId);
211081
211598
  if (!review) throw new Error(`\u8349\u6848\u4E0D\u5B58\u5728\u6216\u5DF2\u5904\u7406\uFF1A${draftId}`);
211599
+ const authorizedByName = (await (engine.registry ?? opts.registry)?.getActor(actor).catch(() => null))?.name?.trim();
211082
211600
  if (review.kind === "command" && review.command) {
211083
211601
  const { command: command2, artifactId } = await engine.kernel.applyAuthorizedCommand({ draftId, actor });
211084
- const receipt = authorizedCommandReceipt(command2, review.command.args, actor);
211602
+ const receipt = authorizedCommandReceipt(command2, review.command.args, actor, commandCardRefsOf(engine.kernel.model));
211085
211603
  notifyReviewResolved(
211086
211604
  draftId,
211087
211605
  { status: "approved", command: command2, effectLabel: review.command.effectLabel, authorizedBy: actor, receipt },
211088
- { origin: review.origin, kind: "command", authorizedBy: actor, effectLabel: review.command.effectLabel, command: command2 },
211606
+ {
211607
+ origin: review.origin,
211608
+ kind: "command",
211609
+ authorizedBy: actor,
211610
+ ...authorizedByName ? { authorizedByName } : {},
211611
+ // effectLabel 带 id:RESUME 提示词给起草 agent 用;displayLabel 是状态条(给人)那句。
211612
+ effectLabel: review.command.effectLabel,
211613
+ ...review.command.displayLabel ? { displayLabel: review.command.displayLabel } : {},
211614
+ command: command2
211615
+ },
211089
211616
  chatStoreNowOrNull,
211090
211617
  currentCompanyId
211091
211618
  );
@@ -211104,7 +211631,7 @@ async function startOasisServer(opts) {
211104
211631
  { status: "approved", command: "", effectLabel: review.plan.reason, authorizedBy: actor },
211105
211632
  // 状态条要的两样也得进 resume 上下文(不能只放在 resolution 里):`effectLabel` 是改图卡的
211106
211633
  // 「改动摘要」(= plan.reason,见 diagnosis 口径 1),`authorizedBy` 是「谁批的」。
211107
- { origin: review.origin, kind: review.kind ?? "graph-edit", authorizedBy: actor, effectLabel: review.plan.reason },
211634
+ { origin: review.origin, kind: review.kind ?? "graph-edit", authorizedBy: actor, ...authorizedByName ? { authorizedByName } : {}, effectLabel: review.plan.reason },
211108
211635
  chatStoreNowOrNull,
211109
211636
  // 续跑那一轮照样可能往 outputs/ 写东西——按公司出产出卡(与命令卡那支同口径)。
211110
211637
  currentCompanyId
@@ -211134,6 +211661,7 @@ async function startOasisServer(opts) {
211134
211661
  return;
211135
211662
  }
211136
211663
  const { recorded } = await engine.kernel.rejectIntervention({ draftId, anchor: review.anchor, actor, ...reason !== void 0 ? { reason } : {} });
211664
+ const authorizedByName = (await (engine.registry ?? opts.registry)?.getActor(actor).catch(() => null))?.name?.trim();
211137
211665
  notifyReviewResolved(
211138
211666
  draftId,
211139
211667
  { status: "denied", ...reason !== void 0 ? { reason } : {} },
@@ -211141,10 +211669,13 @@ async function startOasisServer(opts) {
211141
211669
  origin: review.origin,
211142
211670
  kind: review.kind ?? "graph-edit",
211143
211671
  authorizedBy: actor,
211672
+ ...authorizedByName ? { authorizedByName } : {},
211144
211673
  // 状态条要的那段「效果标签 / 改动摘要」:命令卡用命令的效果标签,改图卡用 `plan.reason`
211145
211674
  // ——与 apply-draft 改图那一支同源(那里传的就是 `review.plan.reason`),否则驳回档的
211146
211675
  // 改图卡状态条会永远拿不到摘要、只能落占位符(diagnosis 口径 1)。
211147
211676
  ...review.command?.effectLabel || review.plan.reason ? { effectLabel: review.command?.effectLabel || review.plan.reason } : {},
211677
+ // Bug 0215:驳回档的状态条同样走人读那句(命令卡才有;改图卡的摘要仍是 plan.reason)。
211678
+ ...review.command?.displayLabel ? { displayLabel: review.command.displayLabel } : {},
211148
211679
  ...review.command?.command ? { command: review.command.command } : {},
211149
211680
  ...reason !== void 0 ? { reason } : {}
211150
211681
  },
@@ -211975,6 +212506,15 @@ async function startOasisServer(opts) {
211975
212506
  res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "EMPTY_TEXT" }));
211976
212507
  return;
211977
212508
  }
212509
+ await waitForAdmittedTurnStart(sid, {
212510
+ liveChat,
212511
+ ledgerTurnRunning: async () => {
212512
+ const turns = opts.resolveChatTurns ? await opts.resolveChatTurns(currentCompanyId) : opts.chatTurns;
212513
+ const status = (await turns?.activeTurn(sid).catch(() => null))?.status;
212514
+ return status === "reserved" || status === "running";
212515
+ },
212516
+ timeoutMs: APPEND_TURN_START_WAIT_MS
212517
+ });
211978
212518
  await respondAppend(res, wantsAppendProgress(req), (onAwaitingConsumption) => appendNowThroughQueue2(sid, text7, clientKey, insertAttachments, perCompanyStore, retainLocallyOnFailure, onAwaitingConsumption, resend));
211979
212519
  return;
211980
212520
  }
@@ -213900,7 +214440,10 @@ ${composed}`;
213900
214440
  auth: {
213901
214441
  actor,
213902
214442
  ...companyId !== void 0 ? { companyId } : {},
213903
- ...dispatch && Object.keys(dispatch).length > 0 ? { dispatch } : {}
214443
+ ...dispatch && Object.keys(dispatch).length > 0 ? { dispatch } : {},
214444
+ // 签名 scopes 原样透出(ADR 0609 D1):部署级 operator-token 靠它被认出来。
214445
+ // 只来自签名 token;cookie / 简单 token 没有这一项。
214446
+ ...domainClaims?.scopes ? { scopes: domainClaims.scopes } : {}
213904
214447
  },
213905
214448
  raw: req
213906
214449
  });
@@ -214043,7 +214586,7 @@ async function markDeliveredWithRetry(store, messageId, attempts = 3) {
214043
214586
  }
214044
214587
  return false;
214045
214588
  }
214046
- var http, import_node_crypto41, githubAppPending, connectorAuthSnapshots, CONNECTOR_AUTH_SNAPSHOT_TTL_MS, defaultResolveActor, enc, CHAT_OUTPUT_LIMIT, STREAMING_PATHS, PLANNER_BLOCKING_CODES, WILL_VERBS, GOVERNANCE_COMMANDS, READ_ONLY_COMMANDS, isAgent, isHuman;
214589
+ var http, import_node_crypto41, githubAppPending, connectorAuthSnapshots, CONNECTOR_AUTH_SNAPSHOT_TTL_MS, defaultResolveActor, enc, CHAT_OUTPUT_LIMIT, APPEND_TURN_START_WAIT_MS, STREAMING_PATHS, PLANNER_BLOCKING_CODES, WILL_VERBS, GOVERNANCE_COMMANDS, READ_ONLY_COMMANDS, isAgent, isHuman;
214047
214590
  var init_server3 = __esm({
214048
214591
  "../server/src/server.ts"() {
214049
214592
  "use strict";
@@ -214058,6 +214601,7 @@ var init_server3 = __esm({
214058
214601
  init_src();
214059
214602
  init_src();
214060
214603
  init_src();
214604
+ init_src();
214061
214605
  init_file_http();
214062
214606
  init_build_info();
214063
214607
  init_artifact_content2();
@@ -214117,6 +214661,7 @@ var init_server3 = __esm({
214117
214661
  defaultResolveActor = (token) => token.startsWith("token:") ? token.slice("token:".length) : null;
214118
214662
  enc = (s2) => new TextEncoder().encode(s2);
214119
214663
  CHAT_OUTPUT_LIMIT = 16e3;
214664
+ APPEND_TURN_START_WAIT_MS = 1e4;
214120
214665
  STREAMING_PATHS = ["/api/events", "/api/chat", "/api/trajectory/events", "/api/intervention/wait"];
214121
214666
  PLANNER_BLOCKING_CODES = /* @__PURE__ */ new Set([
214122
214667
  "title_collision",
@@ -214207,12 +214752,12 @@ function createTokenIssuer(opts = {}) {
214207
214752
  function createNodeTokenStore(file) {
214208
214753
  const read3 = () => {
214209
214754
  try {
214210
- return JSON.parse(fs18.readFileSync(file, "utf8"));
214755
+ return JSON.parse(fs19.readFileSync(file, "utf8"));
214211
214756
  } catch {
214212
214757
  return {};
214213
214758
  }
214214
214759
  };
214215
- const write = (table) => fs18.writeFileSync(file, JSON.stringify(table, null, 2), { mode: 384 });
214760
+ const write = (table) => fs19.writeFileSync(file, JSON.stringify(table, null, 2), { mode: 384 });
214216
214761
  return {
214217
214762
  issue(nodeId) {
214218
214763
  const table = read3();
@@ -214238,14 +214783,14 @@ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3, file) {
214238
214783
  const read3 = () => {
214239
214784
  if (!file) return table;
214240
214785
  try {
214241
- return new Map(Object.entries(JSON.parse(fs18.readFileSync(file, "utf8"))));
214786
+ return new Map(Object.entries(JSON.parse(fs19.readFileSync(file, "utf8"))));
214242
214787
  } catch {
214243
214788
  return /* @__PURE__ */ new Map();
214244
214789
  }
214245
214790
  };
214246
214791
  const write = (m2) => {
214247
214792
  if (!file) return;
214248
- fs18.writeFileSync(file, JSON.stringify(Object.fromEntries(m2), null, 2), { mode: 384 });
214793
+ fs19.writeFileSync(file, JSON.stringify(Object.fromEntries(m2), null, 2), { mode: 384 });
214249
214794
  };
214250
214795
  const table = /* @__PURE__ */ new Map();
214251
214796
  const prune = () => {
@@ -214307,12 +214852,12 @@ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3, file) {
214307
214852
  }
214308
214853
  };
214309
214854
  }
214310
- var import_node_crypto42, fs18, path19, SESSION_TOKEN_PREFIX, b64url2, fromB64url, readOrCreateSecret, sign, signatureMatches, isTokenClaims;
214855
+ var import_node_crypto42, fs19, path19, SESSION_TOKEN_PREFIX, b64url2, fromB64url, readOrCreateSecret, sign, signatureMatches, isTokenClaims;
214311
214856
  var init_tokens = __esm({
214312
214857
  "../server/src/tokens.ts"() {
214313
214858
  "use strict";
214314
214859
  import_node_crypto42 = require("node:crypto");
214315
- fs18 = __toESM(require("node:fs"), 1);
214860
+ fs19 = __toESM(require("node:fs"), 1);
214316
214861
  path19 = __toESM(require("node:path"), 1);
214317
214862
  SESSION_TOKEN_PREFIX = "oat_v2_";
214318
214863
  b64url2 = (value2) => Buffer.from(value2).toString("base64url");
@@ -214320,13 +214865,13 @@ var init_tokens = __esm({
214320
214865
  readOrCreateSecret = (file) => {
214321
214866
  if (!file) return (0, import_node_crypto42.randomBytes)(32);
214322
214867
  try {
214323
- const raw = fs18.readFileSync(file, "utf8").trim();
214868
+ const raw = fs19.readFileSync(file, "utf8").trim();
214324
214869
  if (raw) return Buffer.from(raw, "base64url");
214325
214870
  } catch {
214326
214871
  }
214327
214872
  const secret = (0, import_node_crypto42.randomBytes)(32);
214328
- fs18.mkdirSync(path19.dirname(file), { recursive: true });
214329
- fs18.writeFileSync(file, secret.toString("base64url"), { mode: 384 });
214873
+ fs19.mkdirSync(path19.dirname(file), { recursive: true });
214874
+ fs19.writeFileSync(file, secret.toString("base64url"), { mode: 384 });
214330
214875
  return secret;
214331
214876
  };
214332
214877
  sign = (secret, payload2) => (0, import_node_crypto42.createHmac)("sha256", secret).update(payload2).digest("base64url");
@@ -214351,6 +214896,18 @@ var init_tokens = __esm({
214351
214896
  }
214352
214897
  });
214353
214898
 
214899
+ // ../server/src/auth/operator.ts
214900
+ function isOperatorRequest(auth) {
214901
+ return auth.scopes?.includes(OPERATOR_SCOPE) === true;
214902
+ }
214903
+ var OPERATOR_SCOPE;
214904
+ var init_operator = __esm({
214905
+ "../server/src/auth/operator.ts"() {
214906
+ "use strict";
214907
+ OPERATOR_SCOPE = "oasis:operator";
214908
+ }
214909
+ });
214910
+
214354
214911
  // ../server/src/session-dataplane-token.ts
214355
214912
  function createDataplaneTokenIssuer(opts = {}) {
214356
214913
  const secret = readOrCreateSecret2(opts.secretFile);
@@ -214380,12 +214937,12 @@ function createDataplaneTokenIssuer(opts = {}) {
214380
214937
  }
214381
214938
  };
214382
214939
  }
214383
- var import_node_crypto43, fs19, path20, PREFIX2, b64url3, fromB64url2, sign2, sigMatches, readOrCreateSecret2, isClaims, isDataplaneToken;
214940
+ var import_node_crypto43, fs20, path20, PREFIX2, b64url3, fromB64url2, sign2, sigMatches, readOrCreateSecret2, isClaims, isDataplaneToken;
214384
214941
  var init_session_dataplane_token = __esm({
214385
214942
  "../server/src/session-dataplane-token.ts"() {
214386
214943
  "use strict";
214387
214944
  import_node_crypto43 = require("node:crypto");
214388
- fs19 = __toESM(require("node:fs"), 1);
214945
+ fs20 = __toESM(require("node:fs"), 1);
214389
214946
  path20 = __toESM(require("node:path"), 1);
214390
214947
  init_src();
214391
214948
  PREFIX2 = "osd_";
@@ -214400,13 +214957,13 @@ var init_session_dataplane_token = __esm({
214400
214957
  readOrCreateSecret2 = (file) => {
214401
214958
  if (!file) return (0, import_node_crypto43.randomBytes)(32);
214402
214959
  try {
214403
- const raw = fs19.readFileSync(file, "utf8").trim();
214960
+ const raw = fs20.readFileSync(file, "utf8").trim();
214404
214961
  if (raw) return Buffer.from(raw, "base64url");
214405
214962
  } catch {
214406
214963
  }
214407
214964
  const secret = (0, import_node_crypto43.randomBytes)(32);
214408
- fs19.mkdirSync(path20.dirname(file), { recursive: true });
214409
- fs19.writeFileSync(file, secret.toString("base64url"), { mode: 384 });
214965
+ fs20.mkdirSync(path20.dirname(file), { recursive: true });
214966
+ fs20.writeFileSync(file, secret.toString("base64url"), { mode: 384 });
214410
214967
  return secret;
214411
214968
  };
214412
214969
  isClaims = (v2) => {
@@ -220004,13 +220561,13 @@ function applyTurnPatch(t, patch) {
220004
220561
  ...patch.lastError !== void 0 ? { lastError: patch.lastError } : {}
220005
220562
  };
220006
220563
  }
220007
- var import_node_crypto46, fs20, path21, NdjsonOplogStore, DirBlobStore, MUTATORS, FileTypeRegistryStore, FileRoleRegistryStore, FileRegistryStore, FileAssistantBindStore, FileHumanPrefsStore, FileProjectStateStore, FileArtifactStateStore, FileTraceStore, MemoryChatSessionStore, FileChatSessionStore, MemoryChatTurnStore, FileChatTurnStore, MemoryReadMarkerStore, FileReadMarkerStore;
220564
+ var import_node_crypto46, fs21, path21, NdjsonOplogStore, DirBlobStore, MUTATORS, FileTypeRegistryStore, FileRoleRegistryStore, FileRegistryStore, FileAssistantBindStore, FileHumanPrefsStore, FileProjectStateStore, FileArtifactStateStore, FileTraceStore, MemoryChatSessionStore, FileChatSessionStore, MemoryChatTurnStore, FileChatTurnStore, MemoryReadMarkerStore, FileReadMarkerStore;
220008
220565
  var init_dev_store = __esm({
220009
220566
  "../server/src/dev-store.ts"() {
220010
220567
  "use strict";
220011
220568
  init_src();
220012
220569
  import_node_crypto46 = require("node:crypto");
220013
- fs20 = __toESM(require("node:fs"), 1);
220570
+ fs21 = __toESM(require("node:fs"), 1);
220014
220571
  path21 = __toESM(require("node:path"), 1);
220015
220572
  init_src();
220016
220573
  init_src9();
@@ -220024,8 +220581,8 @@ var init_dev_store = __esm({
220024
220581
  }
220025
220582
  static async open(file) {
220026
220583
  const memory = new MemoryOplogStore();
220027
- if (fs20.existsSync(file)) {
220028
- for (const line2 of fs20.readFileSync(file, "utf8").split("\n")) {
220584
+ if (fs21.existsSync(file)) {
220585
+ for (const line2 of fs21.readFileSync(file, "utf8").split("\n")) {
220029
220586
  if (!line2.trim()) continue;
220030
220587
  const stored = JSON.parse(line2);
220031
220588
  const { artifactId, seq, ...op } = stored;
@@ -220033,15 +220590,15 @@ var init_dev_store = __esm({
220033
220590
  if (assigned !== seq) throw new Error(`corrupt ndjson log: seq mismatch at ${line2}`);
220034
220591
  }
220035
220592
  } else {
220036
- fs20.mkdirSync(path21.dirname(file), { recursive: true });
220037
- fs20.writeFileSync(file, "");
220593
+ fs21.mkdirSync(path21.dirname(file), { recursive: true });
220594
+ fs21.writeFileSync(file, "");
220038
220595
  }
220039
220596
  return new _NdjsonOplogStore(file, memory);
220040
220597
  }
220041
220598
  async append(artifactId, op, expectedSeq) {
220042
220599
  const seq = await this.memory.append(artifactId, op, expectedSeq);
220043
220600
  const stored = { ...op, artifactId, seq };
220044
- fs20.appendFileSync(this.file, JSON.stringify(stored) + "\n");
220601
+ fs21.appendFileSync(this.file, JSON.stringify(stored) + "\n");
220045
220602
  return seq;
220046
220603
  }
220047
220604
  read(artifactId, fromSeq) {
@@ -220057,7 +220614,7 @@ var init_dev_store = __esm({
220057
220614
  DirBlobStore = class {
220058
220615
  constructor(dir) {
220059
220616
  this.dir = dir;
220060
- fs20.mkdirSync(dir, { recursive: true });
220617
+ fs21.mkdirSync(dir, { recursive: true });
220061
220618
  }
220062
220619
  fileOf(hash3) {
220063
220620
  return path21.join(this.dir, hash3);
@@ -220065,31 +220622,31 @@ var init_dev_store = __esm({
220065
220622
  async put(bytes2) {
220066
220623
  const hash3 = (0, import_node_crypto46.createHash)("sha256").update(bytes2).digest("hex");
220067
220624
  const file = this.fileOf(hash3);
220068
- if (!fs20.existsSync(file)) fs20.writeFileSync(file, bytes2);
220625
+ if (!fs21.existsSync(file)) fs21.writeFileSync(file, bytes2);
220069
220626
  return hash3;
220070
220627
  }
220071
220628
  async meta(hash3) {
220072
220629
  const file = this.fileOf(hash3);
220073
- if (!fs20.existsSync(file)) return null;
220074
- const size = fs20.statSync(file).size;
220075
- const fd = fs20.openSync(file, "r");
220630
+ if (!fs21.existsSync(file)) return null;
220631
+ const size = fs21.statSync(file).size;
220632
+ const fd = fs21.openSync(file, "r");
220076
220633
  const head = Buffer.alloc(16);
220077
- fs20.readSync(fd, head, 0, 16, 0);
220078
- fs20.closeSync(fd);
220634
+ fs21.readSync(fd, head, 0, 16, 0);
220635
+ fs21.closeSync(fd);
220079
220636
  const contentType = sniffContentType(new Uint8Array(head));
220080
220637
  return { size, ...contentType ? { contentType } : {} };
220081
220638
  }
220082
220639
  async get(hash3) {
220083
220640
  const file = this.fileOf(hash3);
220084
- if (!fs20.existsSync(file)) throw new BlobNotFoundError(hash3);
220085
- return new Uint8Array(fs20.readFileSync(file));
220641
+ if (!fs21.existsSync(file)) throw new BlobNotFoundError(hash3);
220642
+ return new Uint8Array(fs21.readFileSync(file));
220086
220643
  }
220087
220644
  async has(hash3) {
220088
- return fs20.existsSync(this.fileOf(hash3));
220645
+ return fs21.existsSync(this.fileOf(hash3));
220089
220646
  }
220090
220647
  async sweep(reachable) {
220091
- for (const name of fs20.readdirSync(this.dir)) {
220092
- if (!reachable.has(name)) fs20.unlinkSync(this.fileOf(name));
220648
+ for (const name of fs21.readdirSync(this.dir)) {
220649
+ if (!reachable.has(name)) fs21.unlinkSync(this.fileOf(name));
220093
220650
  }
220094
220651
  }
220095
220652
  };
@@ -220118,7 +220675,7 @@ var init_dev_store = __esm({
220118
220675
  this.file = file;
220119
220676
  }
220120
220677
  static async open(file) {
220121
- const seed = fs20.existsSync(file) ? JSON.parse(fs20.readFileSync(file, "utf8")) : [];
220678
+ const seed = fs21.existsSync(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : [];
220122
220679
  return new _FileTypeRegistryStore(file, seed);
220123
220680
  }
220124
220681
  async put(def) {
@@ -220131,8 +220688,8 @@ var init_dev_store = __esm({
220131
220688
  }
220132
220689
  async save() {
220133
220690
  const tmp = `${this.file}.tmp`;
220134
- fs20.writeFileSync(tmp, JSON.stringify(await this.list(), null, 2));
220135
- fs20.renameSync(tmp, this.file);
220691
+ fs21.writeFileSync(tmp, JSON.stringify(await this.list(), null, 2));
220692
+ fs21.renameSync(tmp, this.file);
220136
220693
  }
220137
220694
  };
220138
220695
  FileRoleRegistryStore = class _FileRoleRegistryStore extends MemoryRoleRegistryStore {
@@ -220141,7 +220698,7 @@ var init_dev_store = __esm({
220141
220698
  this.file = file;
220142
220699
  }
220143
220700
  static async open(file) {
220144
- const seed = fs20.existsSync(file) ? JSON.parse(fs20.readFileSync(file, "utf8")) : [];
220701
+ const seed = fs21.existsSync(file) ? JSON.parse(fs21.readFileSync(file, "utf8")) : [];
220145
220702
  return new _FileRoleRegistryStore(file, seed);
220146
220703
  }
220147
220704
  async put(def) {
@@ -220154,8 +220711,8 @@ var init_dev_store = __esm({
220154
220711
  }
220155
220712
  async save() {
220156
220713
  const tmp = `${this.file}.tmp`;
220157
- fs20.writeFileSync(tmp, JSON.stringify(await this.list(), null, 2));
220158
- fs20.renameSync(tmp, this.file);
220714
+ fs21.writeFileSync(tmp, JSON.stringify(await this.list(), null, 2));
220715
+ fs21.renameSync(tmp, this.file);
220159
220716
  }
220160
220717
  };
220161
220718
  FileRegistryStore = class _FileRegistryStore extends MemoryRegistryStore {
@@ -220173,8 +220730,8 @@ var init_dev_store = __esm({
220173
220730
  }
220174
220731
  static async open(file) {
220175
220732
  const store = new _FileRegistryStore(file);
220176
- if (fs20.existsSync(file)) {
220177
- const snap = JSON.parse(fs20.readFileSync(file, "utf8"));
220733
+ if (fs21.existsSync(file)) {
220734
+ const snap = JSON.parse(fs21.readFileSync(file, "utf8"));
220178
220735
  for (const a of snap.actors) await MemoryRegistryStore.prototype.upsertActor.call(store, a);
220179
220736
  for (const c of snap.configs) await MemoryRegistryStore.prototype.appendConfig.call(store, c);
220180
220737
  for (const t of snap.teams) await MemoryRegistryStore.prototype.upsertTeam.call(store, t);
@@ -220215,7 +220772,7 @@ var init_dev_store = __esm({
220215
220772
  skillFiles: skillFilesEntries,
220216
220773
  connections
220217
220774
  };
220218
- fs20.writeFileSync(this.file, JSON.stringify(snap, null, 2));
220775
+ fs21.writeFileSync(this.file, JSON.stringify(snap, null, 2));
220219
220776
  }
220220
220777
  };
220221
220778
  FileAssistantBindStore = class _FileAssistantBindStore extends MemoryAssistantBindStore {
@@ -220233,11 +220790,11 @@ var init_dev_store = __esm({
220233
220790
  }
220234
220791
  static async open(file) {
220235
220792
  const store = new _FileAssistantBindStore(file);
220236
- if (fs20.existsSync(file)) store._load(JSON.parse(fs20.readFileSync(file, "utf8")));
220793
+ if (fs21.existsSync(file)) store._load(JSON.parse(fs21.readFileSync(file, "utf8")));
220237
220794
  return store;
220238
220795
  }
220239
220796
  async save() {
220240
- fs20.writeFileSync(this.file, JSON.stringify(this._dump(), null, 2));
220797
+ fs21.writeFileSync(this.file, JSON.stringify(this._dump(), null, 2));
220241
220798
  }
220242
220799
  };
220243
220800
  FileHumanPrefsStore = class _FileHumanPrefsStore extends MemoryHumanPrefsStore {
@@ -220255,11 +220812,11 @@ var init_dev_store = __esm({
220255
220812
  }
220256
220813
  static async open(file) {
220257
220814
  const store = new _FileHumanPrefsStore(file);
220258
- if (fs20.existsSync(file)) store._load(JSON.parse(fs20.readFileSync(file, "utf8")));
220815
+ if (fs21.existsSync(file)) store._load(JSON.parse(fs21.readFileSync(file, "utf8")));
220259
220816
  return store;
220260
220817
  }
220261
220818
  async save() {
220262
- fs20.writeFileSync(this.file, JSON.stringify(this._dump(), null, 2));
220819
+ fs21.writeFileSync(this.file, JSON.stringify(this._dump(), null, 2));
220263
220820
  }
220264
220821
  };
220265
220822
  FileProjectStateStore = class _FileProjectStateStore extends MemoryProjectStateStore {
@@ -220269,8 +220826,8 @@ var init_dev_store = __esm({
220269
220826
  }
220270
220827
  static async open(file) {
220271
220828
  const store = new _FileProjectStateStore(file);
220272
- if (fs20.existsSync(file)) {
220273
- const snap = JSON.parse(fs20.readFileSync(file, "utf8"));
220829
+ if (fs21.existsSync(file)) {
220830
+ const snap = JSON.parse(fs21.readFileSync(file, "utf8"));
220274
220831
  for (const project of snap.projects ?? []) await MemoryProjectStateStore.prototype.upsertProject.call(store, project);
220275
220832
  const membersByProject = /* @__PURE__ */ new Map();
220276
220833
  for (const member of snap.members ?? []) {
@@ -220282,8 +220839,8 @@ var init_dev_store = __esm({
220282
220839
  await MemoryProjectStateStore.prototype.replaceProjectMembers.call(store, projectId2, members);
220283
220840
  }
220284
220841
  } else {
220285
- fs20.mkdirSync(path21.dirname(file), { recursive: true });
220286
- fs20.writeFileSync(file, JSON.stringify({ projects: [], members: [] }, null, 2));
220842
+ fs21.mkdirSync(path21.dirname(file), { recursive: true });
220843
+ fs21.writeFileSync(file, JSON.stringify({ projects: [], members: [] }, null, 2));
220287
220844
  }
220288
220845
  return store;
220289
220846
  }
@@ -220302,7 +220859,7 @@ var init_dev_store = __esm({
220302
220859
  async save() {
220303
220860
  const projects = await this.listProjects();
220304
220861
  const members = (await Promise.all(projects.map((project) => this.listProjectMembers(project.id)))).flat();
220305
- fs20.writeFileSync(this.file, JSON.stringify({ projects, members }, null, 2));
220862
+ fs21.writeFileSync(this.file, JSON.stringify({ projects, members }, null, 2));
220306
220863
  }
220307
220864
  };
220308
220865
  FileArtifactStateStore = class _FileArtifactStateStore extends MemoryArtifactStateStore {
@@ -220312,8 +220869,8 @@ var init_dev_store = __esm({
220312
220869
  }
220313
220870
  static async open(file) {
220314
220871
  const store = new _FileArtifactStateStore(file);
220315
- if (fs20.existsSync(file)) {
220316
- const snap = JSON.parse(fs20.readFileSync(file, "utf8"));
220872
+ if (fs21.existsSync(file)) {
220873
+ const snap = JSON.parse(fs21.readFileSync(file, "utf8"));
220317
220874
  for (const artifact of snap.artifacts ?? []) await MemoryArtifactStateStore.prototype.upsertArtifact.call(store, artifact);
220318
220875
  for (const revision of snap.revisions ?? []) await MemoryArtifactStateStore.prototype.upsertRevision.call(store, revision);
220319
220876
  for (const event of snap.events ?? []) await MemoryArtifactStateStore.prototype.appendEvent.call(store, event);
@@ -220323,8 +220880,8 @@ var init_dev_store = __esm({
220323
220880
  for (const ws of snap.workspaceDispatchHeld ?? []) await MemoryArtifactStateStore.prototype.setWorkspaceDispatchHold.call(store, ws, true);
220324
220881
  for (const f2 of snap.workOrderFiles ?? []) await MemoryArtifactStateStore.prototype.addWorkOrderFile.call(store, f2);
220325
220882
  } else {
220326
- fs20.mkdirSync(path21.dirname(file), { recursive: true });
220327
- fs20.writeFileSync(file, JSON.stringify({ artifacts: [], revisions: [], events: [], annotations: [], nodeOutputs: [], workspaceBindings: [], workspaceDispatchHeld: [], workOrderFiles: [] }, null, 2));
220883
+ fs21.mkdirSync(path21.dirname(file), { recursive: true });
220884
+ fs21.writeFileSync(file, JSON.stringify({ artifacts: [], revisions: [], events: [], annotations: [], nodeOutputs: [], workspaceBindings: [], workspaceDispatchHeld: [], workOrderFiles: [] }, null, 2));
220328
220885
  }
220329
220886
  return store;
220330
220887
  }
@@ -220361,7 +220918,7 @@ var init_dev_store = __esm({
220361
220918
  await this.save();
220362
220919
  }
220363
220920
  async save() {
220364
- fs20.writeFileSync(this.file, JSON.stringify(await this.listAll(), null, 2));
220921
+ fs21.writeFileSync(this.file, JSON.stringify(await this.listAll(), null, 2));
220365
220922
  }
220366
220923
  };
220367
220924
  FileTraceStore = class _FileTraceStore extends MemoryTraceStore {
@@ -220388,25 +220945,25 @@ var init_dev_store = __esm({
220388
220945
  return path21.join(this.eventsDir, `${encodeURIComponent(runId)}.ndjson`);
220389
220946
  }
220390
220947
  appendLine(file, obj2) {
220391
- fs20.appendFileSync(file, JSON.stringify(obj2) + "\n");
220948
+ fs21.appendFileSync(file, JSON.stringify(obj2) + "\n");
220392
220949
  }
220393
220950
  static replay(file, fn) {
220394
- if (!fs20.existsSync(file)) return;
220395
- for (const line2 of fs20.readFileSync(file, "utf8").split("\n")) {
220951
+ if (!fs21.existsSync(file)) return;
220952
+ for (const line2 of fs21.readFileSync(file, "utf8").split("\n")) {
220396
220953
  if (!line2.trim()) continue;
220397
220954
  fn(JSON.parse(line2));
220398
220955
  }
220399
220956
  }
220400
220957
  static async open(dir) {
220401
220958
  const store = new _FileTraceStore(dir);
220402
- fs20.mkdirSync(store.eventsDir, { recursive: true });
220959
+ fs21.mkdirSync(store.eventsDir, { recursive: true });
220403
220960
  _FileTraceStore.replay(store.runsFile, (r) => store.restoreRun(r));
220404
- for (const name of fs20.readdirSync(store.eventsDir)) {
220961
+ for (const name of fs21.readdirSync(store.eventsDir)) {
220405
220962
  _FileTraceStore.replay(path21.join(store.eventsDir, name), (e) => store.restoreEvent(e));
220406
220963
  }
220407
220964
  _FileTraceStore.replay(store.toolCallsFile, (c) => store.restoreToolCall(c));
220408
220965
  _FileTraceStore.replay(store.linksFile, (l) => store.restoreLink(l));
220409
- store.restoreContinuity(fs20.existsSync(store.continuityFile) ? JSON.parse(fs20.readFileSync(store.continuityFile, "utf8")) : {});
220966
+ store.restoreContinuity(fs21.existsSync(store.continuityFile) ? JSON.parse(fs21.readFileSync(store.continuityFile, "utf8")) : {});
220410
220967
  return store;
220411
220968
  }
220412
220969
  async createRun(input) {
@@ -220458,8 +221015,8 @@ var init_dev_store = __esm({
220458
221015
  }
220459
221016
  saveContinuity() {
220460
221017
  const tmp = `${this.continuityFile}.tmp`;
220461
- fs20.writeFileSync(tmp, JSON.stringify(this.continuitySnapshot(), null, 2));
220462
- fs20.renameSync(tmp, this.continuityFile);
221018
+ fs21.writeFileSync(tmp, JSON.stringify(this.continuitySnapshot(), null, 2));
221019
+ fs21.renameSync(tmp, this.continuityFile);
220463
221020
  }
220464
221021
  async appendEvents(runId, inputs) {
220465
221022
  const existing = await super.listEvents(runId, {});
@@ -220920,8 +221477,8 @@ var init_dev_store = __esm({
220920
221477
  }
220921
221478
  static async open(file) {
220922
221479
  const store = new _FileChatSessionStore(file);
220923
- if (fs20.existsSync(file)) {
220924
- const snap = JSON.parse(fs20.readFileSync(file, "utf8"));
221480
+ if (fs21.existsSync(file)) {
221481
+ const snap = JSON.parse(fs21.readFileSync(file, "utf8"));
220925
221482
  for (const [sid, copies] of Object.entries(snap.outputCopies ?? {})) store.outputCopies.set(sid, copies);
220926
221483
  for (const [sid, fps] of Object.entries(snap.outputFingerprints ?? {})) store.outputFingerprints.set(sid, fps);
220927
221484
  for (const [sid, issues] of Object.entries(snap.fileIssues ?? {})) store.fileIssues.set(sid, issues);
@@ -220931,8 +221488,8 @@ var init_dev_store = __esm({
220931
221488
  for (const [sid, kinds] of Object.entries(snap.sessionWorkOrderKinds ?? {})) store.sessionWorkOrderKinds.set(sid, new Map(Object.entries(kinds)));
220932
221489
  for (const [sid, list2] of Object.entries(snap.pending ?? {})) store.pending.set(sid, list2);
220933
221490
  } else {
220934
- fs20.mkdirSync(path21.dirname(file), { recursive: true });
220935
- fs20.writeFileSync(file, JSON.stringify({ sessions: [], messages: {}, sessionWorkOrders: {} }, null, 2));
221491
+ fs21.mkdirSync(path21.dirname(file), { recursive: true });
221492
+ fs21.writeFileSync(file, JSON.stringify({ sessions: [], messages: {}, sessionWorkOrders: {} }, null, 2));
220936
221493
  }
220937
221494
  return store;
220938
221495
  }
@@ -221026,7 +221583,7 @@ var init_dev_store = __esm({
221026
221583
  sessionWorkOrderKinds: Object.fromEntries([...this.sessionWorkOrderKinds].map(([k2, v2]) => [k2, Object.fromEntries(v2)])),
221027
221584
  pending: Object.fromEntries(this.pending)
221028
221585
  };
221029
- fs20.writeFileSync(this.file, JSON.stringify(snap, null, 2));
221586
+ fs21.writeFileSync(this.file, JSON.stringify(snap, null, 2));
221030
221587
  }
221031
221588
  };
221032
221589
  MemoryChatTurnStore = class {
@@ -221109,17 +221666,17 @@ var init_dev_store = __esm({
221109
221666
  }
221110
221667
  static async open(file) {
221111
221668
  const store = new _FileChatTurnStore(file);
221112
- if (fs20.existsSync(file)) {
221113
- const snap = JSON.parse(fs20.readFileSync(file, "utf8"));
221669
+ if (fs21.existsSync(file)) {
221670
+ const snap = JSON.parse(fs21.readFileSync(file, "utf8"));
221114
221671
  for (const t of snap.turns ?? []) store.turns.set(t.id, t);
221115
221672
  } else {
221116
- fs20.mkdirSync(path21.dirname(file), { recursive: true });
221117
- fs20.writeFileSync(file, JSON.stringify({ turns: [] }, null, 2));
221673
+ fs21.mkdirSync(path21.dirname(file), { recursive: true });
221674
+ fs21.writeFileSync(file, JSON.stringify({ turns: [] }, null, 2));
221118
221675
  }
221119
221676
  return store;
221120
221677
  }
221121
221678
  persist() {
221122
- fs20.writeFileSync(this.file, JSON.stringify({ turns: [...this.turns.values()] }, null, 2));
221679
+ fs21.writeFileSync(this.file, JSON.stringify({ turns: [...this.turns.values()] }, null, 2));
221123
221680
  }
221124
221681
  };
221125
221682
  MemoryReadMarkerStore = class _MemoryReadMarkerStore {
@@ -221145,12 +221702,12 @@ var init_dev_store = __esm({
221145
221702
  }
221146
221703
  static async open(file) {
221147
221704
  const store = new _FileReadMarkerStore(file);
221148
- if (fs20.existsSync(file)) {
221149
- const snap = JSON.parse(fs20.readFileSync(file, "utf8"));
221705
+ if (fs21.existsSync(file)) {
221706
+ const snap = JSON.parse(fs21.readFileSync(file, "utf8"));
221150
221707
  for (const m2 of snap.markers ?? []) store.markers.set(`${m2.actorId}\0${m2.scope}`, m2);
221151
221708
  } else {
221152
- fs20.mkdirSync(path21.dirname(file), { recursive: true });
221153
- fs20.writeFileSync(file, JSON.stringify({ markers: [] }, null, 2));
221709
+ fs21.mkdirSync(path21.dirname(file), { recursive: true });
221710
+ fs21.writeFileSync(file, JSON.stringify({ markers: [] }, null, 2));
221154
221711
  }
221155
221712
  return store;
221156
221713
  }
@@ -221159,18 +221716,18 @@ var init_dev_store = __esm({
221159
221716
  this.save();
221160
221717
  }
221161
221718
  save() {
221162
- fs20.writeFileSync(this.file, JSON.stringify({ markers: [...this.markers.values()] }, null, 2));
221719
+ fs21.writeFileSync(this.file, JSON.stringify({ markers: [...this.markers.values()] }, null, 2));
221163
221720
  }
221164
221721
  };
221165
221722
  }
221166
221723
  });
221167
221724
 
221168
221725
  // ../server/src/control-plane-file-store.ts
221169
- var fs21, path22, MUTATORS2, SNAPSHOT_VERSION, FileControlPlaneStore;
221726
+ var fs22, path22, MUTATORS2, SNAPSHOT_VERSION, FileControlPlaneStore;
221170
221727
  var init_control_plane_file_store = __esm({
221171
221728
  "../server/src/control-plane-file-store.ts"() {
221172
221729
  "use strict";
221173
- fs21 = __toESM(require("node:fs"), 1);
221730
+ fs22 = __toESM(require("node:fs"), 1);
221174
221731
  path22 = __toESM(require("node:path"), 1);
221175
221732
  init_src9();
221176
221733
  MUTATORS2 = [
@@ -221213,8 +221770,8 @@ var init_control_plane_file_store = __esm({
221213
221770
  }
221214
221771
  static async open(file) {
221215
221772
  const store = new _FileControlPlaneStore(file);
221216
- if (fs21.existsSync(file)) {
221217
- const snap = JSON.parse(fs21.readFileSync(file, "utf8"));
221773
+ if (fs22.existsSync(file)) {
221774
+ const snap = JSON.parse(fs22.readFileSync(file, "utf8"));
221218
221775
  const needsOnboardingBackfill = (snap.version ?? 1) < 2;
221219
221776
  const backfillAt = (/* @__PURE__ */ new Date()).toISOString();
221220
221777
  for (const c of snap.companies ?? []) await MemoryControlPlaneStore.prototype.createCompany.call(store, c);
@@ -221232,7 +221789,7 @@ var init_control_plane_file_store = __esm({
221232
221789
  for (const op of snap.offboardings ?? []) store.offboardings.set(`${op.companyId}\0${op.operationId}`, op);
221233
221790
  if (needsOnboardingBackfill) await store.save();
221234
221791
  } else {
221235
- fs21.mkdirSync(path22.dirname(file), { recursive: true });
221792
+ fs22.mkdirSync(path22.dirname(file), { recursive: true });
221236
221793
  }
221237
221794
  return store;
221238
221795
  }
@@ -221269,7 +221826,7 @@ var init_control_plane_file_store = __esm({
221269
221826
  // 交接操作无按公司列举的读面(按 operationId / accountId 定位),直接读 protected map。
221270
221827
  offboardings: [...this.offboardings.values()]
221271
221828
  };
221272
- fs21.writeFileSync(this.file, JSON.stringify(snap, null, 2));
221829
+ fs22.writeFileSync(this.file, JSON.stringify(snap, null, 2));
221273
221830
  }
221274
221831
  };
221275
221832
  }
@@ -223441,8 +223998,12 @@ var init_dispatch_binding_diagnosis = __esm({
223441
223998
  });
223442
223999
 
223443
224000
  // ../server/src/zero-output-breaker.ts
224001
+ function roundKeyOfZeroOutputRun(run) {
224002
+ return run.workId ?? run.id.replace(PROTOCOL_RECOVERY_SUFFIX, "");
224003
+ }
223444
224004
  function absorb(b2, r, startedMs) {
223445
224005
  b2.count += 1;
224006
+ b2.rounds.add(roundKeyOfZeroOutputRun(r));
223446
224007
  b2.costUsdMicros += r.costUsdMicros ?? 0;
223447
224008
  if (startedMs >= b2.latestStartedMs) {
223448
224009
  b2.latestStartedMs = startedMs;
@@ -223510,6 +224071,7 @@ async function scanZeroOutputBreaker(deps) {
223510
224071
  actorId: axis === "count" ? b2.actorId : null,
223511
224072
  cls: axis === "count" ? b2.cls : null,
223512
224073
  count: b2.count,
224074
+ workCount: b2.rounds.size,
223513
224075
  costUsdMicros: b2.costUsdMicros,
223514
224076
  windowMs: deps.windowMs,
223515
224077
  latestRunId: b2.latestRunId,
@@ -223519,7 +224081,7 @@ async function scanZeroOutputBreaker(deps) {
223519
224081
  const who = axis === "count" ? `${b2.actorId}\uFF08${b2.cls}\uFF09` : "\u8BE5\u8282\u70B9\u5168\u90E8\u6267\u884C\u4EBA\u5408\u8BA1";
223520
224082
  const cidTag = deps.defaultCompanyId && b2.companyId !== deps.defaultCompanyId ? ` [${b2.companyId}]` : "";
223521
224083
  deps.log?.(
223522
- `[zero-output-breaker] ${b2.artifactId}${cidTag}\uFF1A\u6700\u8FD1 ${Math.round(deps.windowMs / 6e4)} \u5206\u949F\u5185 ${who} \u7D2F\u8BA1 ${b2.count} \u6B21\u96F6\u4EA7\u51FA\uFF08\u7EA6 $${(b2.costUsdMicros / 1e6).toFixed(1)}\uFF09\u2192 ${axis} \u8F74\u8DF3\u95F8`
224084
+ `[zero-output-breaker] ${b2.artifactId}${cidTag}\uFF1A\u6700\u8FD1 ${Math.round(deps.windowMs / 6e4)} \u5206\u949F\u5185 ${who} \u7D2F\u8BA1 ${b2.count} \u6B21\u96F6\u4EA7\u51FA\uFF08${b2.rounds.size} \u8F6E\uFF0C\u7EA6 $${(b2.costUsdMicros / 1e6).toFixed(1)}\uFF09\u2192 ${axis} \u8F74\u8DF3\u95F8`
223523
224085
  );
223524
224086
  };
223525
224087
  const byKey = (a, b2) => a.artifactId.localeCompare(b2.artifactId) || (a.actorId ?? "").localeCompare(b2.actorId ?? "");
@@ -223545,32 +224107,35 @@ async function scanZeroOutputBreaker(deps) {
223545
224107
  function formatZeroOutputNotice(t) {
223546
224108
  const usd2 = (t.costUsdMicros / 1e6).toFixed(1);
223547
224109
  const mins = Math.round(t.windowMs / 6e4);
224110
+ const runs = t.count > t.workCount ? `\u5171 ${t.count} \u6B21\u8FD0\u884C\uFF0C\u542B\u540C\u4E00\u8F6E\u5185\u7684\u81EA\u52A8\u7EED\u8DD1\uFF1B` : "";
223548
224111
  const remark = t.closingRemark && t.closingRemark.trim() ? `
223549
224112
 
223550
224113
  \u6700\u8FD1\u4E00\u6B21\u8FD0\u884C\u7684\u6536\u5C3E\u53D1\u8A00\uFF08\u5F80\u5F80\u76F4\u63A5\u5199\u7740\u5361\u5728\u54EA\uFF09\uFF1A
223551
224114
  ${t.closingRemark.trim()}` : "";
223552
224115
  const tail = `\u8FD9\u4E0D\u662F\u5728\u5224\u67D0\u4E00\u6B21\u8FD0\u884C\u5931\u8D25\u2014\u2014\u662F\u5728\u8FD9\u6BB5\u65F6\u95F4\u91CC\u53CD\u590D\u96F6\u4EA7\u51FA\u3002\u65E0\u8BBA\u6BCF\u4E00\u6B21\u9000\u51FA\u591A\u4E48\u300C\u6B63\u5E38\u300D\uFF0C\u53CD\u590D\u5230\u8FD9\u4E2A\u6B21\u6570\u901A\u5E38\u610F\u5473\u7740\u5361\u5728\u4E00\u4E2A\u53CD\u590D\u91CD\u8BD5\u4E5F\u8FC7\u4E0D\u53BB\u7684\u574E\u4E0A\uFF08\u7F3A\u51ED\u636E\u3001\u4E0A\u6E38\u670D\u52A1\u8FDE\u4E0D\u4E0A\u3001\u6216\u649E\u4E0A\u4E86\u522B\u7684\u6B7B\u5FAA\u73AF\uFF09\u3002\u8BF7\u770B\u4E00\u773C\u6839\u56E0\uFF1B\u5904\u7406\u5B8C\u7528 \`oasis release\` \u6062\u590D\u3002` + remark;
223553
224116
  if (t.axis === "cost") {
223554
- return `\u8FD9\u4E2A\u4EA7\u7269\u5728\u6700\u8FD1 ${mins} \u5206\u949F\u5185\u7D2F\u8BA1\u6709 ${t.count} \u6B21\u8FD0\u884C**\u4EC0\u4E48\u90FD\u6CA1\u4EA4\u51FA\u6765**\uFF0C\u8FD9\u4E9B\u7A7A\u8DD1\u5408\u8BA1\u5DF2\u7ECF\u82B1\u6389\u7EA6 $${usd2}\uFF0C\u5DF2\u81EA\u52A8\u6682\u505C\u6D3E\u53D1\u4EE5\u6B62\u635F\u3002
224117
+ return `\u8FD9\u4E2A\u4EA7\u7269\u5728\u6700\u8FD1 ${mins} \u5206\u949F\u5185\u6709 ${t.workCount} \u8F6E**\u4EC0\u4E48\u90FD\u6CA1\u4EA4\u51FA\u6765**\uFF08${runs}\u8FD9\u4E9B\u7A7A\u8DD1\u5408\u8BA1\u5DF2\u7ECF\u82B1\u6389\u7EA6 $${usd2}\uFF09\uFF0C\u5DF2\u81EA\u52A8\u6682\u505C\u6D3E\u53D1\u4EE5\u6B62\u635F\u3002
223555
224118
 
223556
224119
  ` + tail;
223557
224120
  }
223558
224121
  const who = t.cls === "review" ? `\u8BC4\u5BA1\u5458 ${t.actorId}` : `\u6267\u884C\u4EBA ${t.actorId}`;
223559
224122
  const what = t.cls === "review" ? `\u5DF2\u505C\u6389\u8FD9\u540D\u8BC4\u5BA1\u5458\u5728\u672C\u8282\u70B9\u4E0A\u7684\u8BC4\u5BA1\uFF08**\u4EA4\u4ED8\u5185\u5BB9\u539F\u6837\u4FDD\u7559**\uFF0C\u6062\u590D\u540E\u53EF\u7EE7\u7EED\u6295\u7968\uFF09` : `\u5DF2\u81EA\u52A8\u6682\u505C\u8BE5\u4EA7\u7269\u7684\u6D3E\u53D1\u4EE5\u6B62\u635F`;
223560
- return `${who} \u5728\u6700\u8FD1 ${mins} \u5206\u949F\u5185\u4E3A\u8FD9\u4E2A\u4EA7\u7269\u8DD1\u4E86 ${t.count} \u6B21\uFF0C**\u6BCF\u6B21\u90FD\u4EC0\u4E48\u90FD\u6CA1\u4EA4\u51FA\u6765**\uFF08\u8FD9\u4E9B\u7A7A\u8DD1\u7D2F\u8BA1\u82B1\u8D39\u7EA6 $${usd2}\uFF09\uFF0C${what}\u3002
224123
+ return `${who} \u5728\u6700\u8FD1 ${mins} \u5206\u949F\u5185\u4E3A\u8FD9\u4E2A\u4EA7\u7269\u8DD1\u4E86 ${t.workCount} \u8F6E\uFF0C**\u6BCF\u4E00\u8F6E\u90FD\u4EC0\u4E48\u90FD\u6CA1\u4EA4\u51FA\u6765**\uFF08${runs}\u8FD9\u4E9B\u7A7A\u8DD1\u7D2F\u8BA1\u82B1\u8D39\u7EA6 $${usd2}\uFF09\uFF0C${what}\u3002
223561
224124
 
223562
224125
  ` + tail;
223563
224126
  }
223564
- var emptyBucket;
224127
+ var PROTOCOL_RECOVERY_SUFFIX, emptyBucket;
223565
224128
  var init_zero_output_breaker = __esm({
223566
224129
  "../server/src/zero-output-breaker.ts"() {
223567
224130
  "use strict";
224131
+ PROTOCOL_RECOVERY_SUFFIX = /(?::protocol-recovery)+$/;
223568
224132
  emptyBucket = (artifactId, companyId, actorId, cls) => ({
223569
224133
  artifactId,
223570
224134
  companyId,
223571
224135
  actorId,
223572
224136
  cls,
223573
224137
  count: 0,
224138
+ rounds: /* @__PURE__ */ new Set(),
223574
224139
  costUsdMicros: 0,
223575
224140
  latestRunId: "",
223576
224141
  latestStartedMs: -Infinity
@@ -223970,6 +224535,11 @@ function chatRunLedgerFields(req) {
223970
224535
  if (req.executionProfile === "discussion-background") return { chatSessionId: null, chatTurnId: null };
223971
224536
  return { chatSessionId: req.chatSessionId ?? null, chatTurnId: req.chatTurnId ?? null };
223972
224537
  }
224538
+ function chatExitModelFields(runtimeReportedModel, dispatchedModel, dispatchedModelSource) {
224539
+ const effectiveModel = runtimeReportedModel || dispatchedModel || null;
224540
+ if (!effectiveModel) return {};
224541
+ return { effectiveModel, modelSource: runtimeReportedModel ? "runtime" : dispatchedModelSource };
224542
+ }
223973
224543
  function createUsageLedgerResolver(deps) {
223974
224544
  const safeCompanyOfRun = (runId) => deps.companyOfRun(runId).catch(() => null);
223975
224545
  return async (dispatchId) => {
@@ -223985,6 +224555,15 @@ function createUsageLedgerResolver(deps) {
223985
224555
  return chatCid ? { runId: stable, store: await deps.storeFor(chatCid) } : null;
223986
224556
  };
223987
224557
  }
224558
+ async function backfillRunModel(target, run, model, warn) {
224559
+ if (!model || model.trim().length === 0) return;
224560
+ if (run.effectiveModel && run.effectiveModel.trim().length > 0) return;
224561
+ try {
224562
+ await target.store.updateRun(target.runId, { effectiveModel: model, modelSource: "runtime" });
224563
+ } catch (err) {
224564
+ warn(`[usage-ledger] run ${target.runId} \u7684\u6A21\u578B\u540D\u8865\u5199\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u4F1A\u8BDD\uFF09\uFF1A${String(err)}`);
224565
+ }
224566
+ }
223988
224567
  function createUsageLedgerFrameHandler(deps) {
223989
224568
  const warn = deps.warn ?? defaultWarn;
223990
224569
  const marker = deps.marker ?? new RuntimeStartMarker({ warn });
@@ -224046,6 +224625,7 @@ function createUsageLedgerFrameHandler(deps) {
224046
224625
  usage: msg.usage,
224047
224626
  model: msg.model ?? null
224048
224627
  });
224628
+ await backfillRunModel(target, run, msg.model, warn);
224049
224629
  } catch (err) {
224050
224630
  warn(`[usage-ledger] \u5904\u7406 ${msg.type} dispatch=${dispatchId} \u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u4F1A\u8BDD\uFF09\uFF1A${String(err)}`);
224051
224631
  }
@@ -225048,18 +225628,27 @@ function wireRecoveredChatTurn(deps) {
225048
225628
  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"}`);
225049
225629
  }
225050
225630
  if (failed) {
225051
- const exitAttribution = exitIsFailure ? attributeChatExit(exitErrorText(info), {
225052
- ...info.reason ? { reason: info.reason } : {},
225053
- ...cancelCauseFromExitMessage(info.errorMessage) ? { cancelledBy: cancelCauseFromExitMessage(info.errorMessage) } : {}
225631
+ const exitSummary = exitIsFailure ? chatExitSummary(info) : void 0;
225632
+ const exitAttribution = exitSummary ? attributeChatExit(exitSummary.summary, {
225633
+ ...exitSummary.reason ? { reason: exitSummary.reason } : {},
225634
+ ...exitSummary.detail ? { detail: exitSummary.detail } : {},
225635
+ ...exitSummary.cancelledBy ? { cancelledBy: exitSummary.cancelledBy } : {}
225054
225636
  }, live.cancelCause()) : void 0;
225055
225637
  const errText2 = [
225056
225638
  exitAttribution?.text,
225057
225639
  droppedFrames > 0 ? outputGapNotice(droppedFrames) : void 0
225058
225640
  ].filter((t) => Boolean(t)).join("\n\n");
225059
- live.emit({ type: "error", text: errText2, ...exitAttribution?.extra.cancelledBy ? { cancelledBy: exitAttribution.extra.cancelledBy } : {} });
225641
+ live.emit({
225642
+ type: "error",
225643
+ text: errText2,
225644
+ ...exitAttribution?.extra.detail ? { detail: exitAttribution.extra.detail } : {},
225645
+ ...exitAttribution?.extra.cancelledBy ? { cancelledBy: exitAttribution.extra.cancelledBy } : {}
225646
+ });
225060
225647
  const errReason = exitIsFailure ? info.reason : OUTPUT_GAP_ERROR_CODE;
225061
225648
  const errExtra = {
225062
225649
  ...errReason ? { reason: errReason } : {},
225650
+ /* 运行时原文只进 detail:刷新后读的是这一行,「展开详情」要能翻出退出码与原始报错。 */
225651
+ ...exitAttribution?.extra.detail ? { detail: exitAttribution.extra.detail } : {},
225063
225652
  ...exitAttribution?.extra.cancelledBy ? { cancelledBy: exitAttribution.extra.cancelledBy } : {}
225064
225653
  };
225065
225654
  const errorOutcome = itemLedger.recordError(errText2, errExtra);
@@ -225100,7 +225689,7 @@ function wireRecoveredChatTurn(deps) {
225100
225689
  touchedAt: finishedAt
225101
225690
  }).catch(() => void 0);
225102
225691
  if (deps.onTurnSettled) {
225103
- await deps.onTurnSettled(failed ? "failed" : "succeeded", finishedAt).catch((err) => {
225692
+ await deps.onTurnSettled(failed ? "failed" : "succeeded", finishedAt, info.reason ?? void 0).catch((err) => {
225104
225693
  log3(`[chat-recovery] \u4F1A\u8BDD ${plan.chatSessionId} \u6536\u53E3\u8BB0\u8D26\u5931\u8D25\uFF08\u9000\u56DE\u6E05\u626B\u62CD\u515C\u5E95\uFF09: ${String(err)}`);
225105
225694
  });
225106
225695
  }
@@ -225297,13 +225886,26 @@ async function reconcileChatExitFrame(deps) {
225297
225886
  if (part) parts.push({ ...part, seq: ++seq });
225298
225887
  }
225299
225888
  if (failed) {
225889
+ const exitSummary = exitIsFailure ? chatExitSummary(info) : void 0;
225300
225890
  const errText2 = [
225301
- exitIsFailure ? exitErrorText(info) : void 0,
225891
+ exitSummary?.summary,
225302
225892
  droppedFrames > 0 ? outputGapNotice(droppedFrames) : void 0
225303
225893
  ].filter((t) => Boolean(t)).join("\n\n");
225304
225894
  const errReason = exitIsFailure ? info.reason : OUTPUT_GAP_ERROR_CODE;
225305
- itemLedger.recordError(errText2, { ...errReason ? { reason: errReason } : {} });
225306
- parts.push({ type: "error", seq: ++seq, text: errText2 });
225895
+ const errExtra = {
225896
+ ...errReason ? { reason: errReason } : {},
225897
+ ...exitSummary?.detail ? { detail: exitSummary.detail } : {},
225898
+ ...exitSummary?.cancelledBy ? { cancelledBy: exitSummary.cancelledBy } : {}
225899
+ };
225900
+ itemLedger.recordError(errText2, errExtra);
225901
+ parts.push({
225902
+ type: "error",
225903
+ seq: ++seq,
225904
+ text: errText2,
225905
+ ...exitIsFailure && info.reason ? { reason: info.reason } : {},
225906
+ ...exitSummary?.detail ? { detail: exitSummary.detail } : {},
225907
+ ...exitSummary?.cancelledBy ? { cancelledBy: exitSummary.cancelledBy } : {}
225908
+ });
225307
225909
  }
225308
225910
  normalizer.turnFinished(failed ? "failed" : "completed");
225309
225911
  itemLedger.finish(failed ? "failed" : "completed");
@@ -225364,7 +225966,8 @@ async function reconcileChatExitFrame(deps) {
225364
225966
  chatSessionId: row.sessionId,
225365
225967
  turnId: turn.id,
225366
225968
  status,
225367
- reason: `\u505C\u673A\u7A97\u53E3\u5185\u7ED3\u675F\u7684\u8F6E\u4EE5 ${status} \u6536\u5C3E`
225969
+ reason: `\u505C\u673A\u7A97\u53E3\u5185\u7ED3\u675F\u7684\u8F6E\u4EE5 ${status} \u6536\u5C3E`,
225970
+ ...info.reason ? { exitReason: info.reason } : {}
225368
225971
  });
225369
225972
  } catch (err) {
225370
225973
  deps.log?.(`[chat-recovery] \u8F6E\u6B21 ${turn.id} \u6536\u53E3\u540E\u7684\u8BB0\u8D26/\u5916\u62A5\u5931\u8D25\uFF08\u884C\u5DF2\u6536\u5C3E\uFF09: ${String(err)}`);
@@ -225520,7 +226123,7 @@ async function recoverChatTurnsAcrossCompanies(deps) {
225520
226123
  }
225521
226124
  const handle = deps.recover(plan);
225522
226125
  const settleTurnId = turn?.id;
225523
- const onTurnSettled = settleTurnId && stores.settleTurn ? async (status, completedAt) => {
226126
+ const onTurnSettled = settleTurnId && stores.settleTurn ? async (status, completedAt, exitReason) => {
225524
226127
  await stores.settleTurn(settleTurnId, {
225525
226128
  status,
225526
226129
  completedAt,
@@ -225532,6 +226135,7 @@ async function recoverChatTurnsAcrossCompanies(deps) {
225532
226135
  turnId: settleTurnId,
225533
226136
  status,
225534
226137
  reason: `\u91CD\u6302\u7684\u8F6E\u4EE5 ${status} \u6536\u5C3E`,
226138
+ ...exitReason ? { exitReason } : {},
225535
226139
  ...companyId ? { companyId } : {}
225536
226140
  });
225537
226141
  } : void 0;
@@ -225577,6 +226181,7 @@ var init_chat_recovery = __esm({
225577
226181
  init_chat_assistant_turn();
225578
226182
  init_src2();
225579
226183
  init_session_exit_outcome();
226184
+ init_chat_exit();
225580
226185
  init_chat_cancel_attribution();
225581
226186
  asObj3 = (v2) => v2 && typeof v2 === "object" && !Array.isArray(v2) ? v2 : void 0;
225582
226187
  partsOf = (parts) => Array.isArray(parts) ? parts.filter((p2) => p2 && typeof p2 === "object" && typeof p2.type === "string") : [];
@@ -225826,6 +226431,8 @@ var init_daemon_hub = __esm({
225826
226431
  sessionMeta = /* @__PURE__ */ new Map();
225827
226432
  sessionConnectListeners = [];
225828
226433
  sessionDisconnectListeners = [];
226434
+ /** Bug 0224:等某条会话连上数据面的一次性等待者(dispatchId → 兑现函数)。见 {@link waitForSession}。 */
226435
+ sessionReadyWaiters = /* @__PURE__ */ new Map();
225829
226436
  // ── ADR 0300 D2「日志复制」:帧号 + 累积 ackSeq + 幂等摄入 ────────────────────────────
225830
226437
  // 只对自报 supportsFrameSeq 的会话生效;老会话这三张表始终为空、逐字走原 outbox 语义。
225831
226438
  /** 自报支持帧号的 dispatchId。 */
@@ -226089,6 +226696,11 @@ var init_daemon_hub = __esm({
226089
226696
  ...msg.jobArtifactId ? { jobArtifactId: msg.jobArtifactId } : {}
226090
226697
  });
226091
226698
  for (const l of this.sessionConnectListeners) l({ dispatchId, sessionId: msg.sessionId, nodeId });
226699
+ const readyWaiters = this.sessionReadyWaiters.get(dispatchId);
226700
+ if (readyWaiters) {
226701
+ this.sessionReadyWaiters.delete(dispatchId);
226702
+ for (const w2 of readyWaiters) w2(true);
226703
+ }
226092
226704
  void this.judgeSession(dispatchId, ws);
226093
226705
  }
226094
226706
  if (msg.supportsFrameSeq) {
@@ -226260,6 +226872,35 @@ var init_daemon_hub = __esm({
226260
226872
  addSessionDisconnectListener(l) {
226261
226873
  this.sessionDisconnectListeners.push(l);
226262
226874
  }
226875
+ /**
226876
+ * Bug 0224:等这次派发的会话进程连上数据面(`session_hello` 登记完),最多等 `timeoutMs`。
226877
+ * 已连着立即回 true;等到了回 true;超时回 false。
226878
+ *
226879
+ * 为什么要等:每一轮开始时节点另起一个会话进程,它自己连回来、在 hello 里报「能回报插话读到的位置」。
226880
+ * 连上之前 {@link supportsInputConsumptionReceipts} 只能答 false——那不是「不支持」,是「还没到」。
226881
+ */
226882
+ waitForSession(dispatchId, timeoutMs) {
226883
+ if (this.hasSession(dispatchId)) return Promise.resolve(true);
226884
+ return new Promise((resolve11) => {
226885
+ let waiters = this.sessionReadyWaiters.get(dispatchId);
226886
+ if (!waiters) {
226887
+ waiters = /* @__PURE__ */ new Set();
226888
+ this.sessionReadyWaiters.set(dispatchId, waiters);
226889
+ }
226890
+ const done = (connected) => {
226891
+ clearTimeout(timer);
226892
+ resolve11(connected);
226893
+ };
226894
+ const timer = setTimeout(() => {
226895
+ const set = this.sessionReadyWaiters.get(dispatchId);
226896
+ set?.delete(done);
226897
+ if (set && set.size === 0) this.sessionReadyWaiters.delete(dispatchId);
226898
+ resolve11(false);
226899
+ }, timeoutMs);
226900
+ timer.unref?.();
226901
+ waiters.add(done);
226902
+ });
226903
+ }
226263
226904
  /** 这次派发有没有一根活着的数据面连接(有 = 会话自己在说话,不必再问节点)。 */
226264
226905
  hasSession(dispatchId) {
226265
226906
  return this.sessionSockets.get(dispatchId)?.readyState === import_websocket.default.OPEN;
@@ -231236,7 +231877,7 @@ function createRegistryAuditSink(file) {
231236
231877
  return {
231237
231878
  /** 登记审计:落盘 + 推 registry 通知(前端据此重拉登记类投影)。 */
231238
231879
  registryAudit: (record9) => {
231239
- fs22.appendFile(file, JSON.stringify(record9) + "\n", () => {
231880
+ fs23.appendFile(file, JSON.stringify(record9) + "\n", () => {
231240
231881
  });
231241
231882
  notify({ kind: record9.kind, actor: record9.actor, target: record9.target, timestamp: record9.timestamp });
231242
231883
  },
@@ -231250,7 +231891,7 @@ function createRegistryAuditSink(file) {
231250
231891
  * 得先让通知带组织、`/api/events` 按组织过滤(recvvzbhOGiS7l 的完整修复),再放回来。
231251
231892
  */
231252
231893
  credentialAudit: (record9) => {
231253
- fs22.appendFile(file, JSON.stringify(record9) + "\n", () => {
231894
+ fs23.appendFile(file, JSON.stringify(record9) + "\n", () => {
231254
231895
  });
231255
231896
  },
231256
231897
  /** runtime 状态变了:只发无内容的失效提示(ADR 0576)。 */
@@ -231267,11 +231908,11 @@ function createRegistryAuditSink(file) {
231267
231908
  }
231268
231909
  };
231269
231910
  }
231270
- var fs22;
231911
+ var fs23;
231271
231912
  var init_registry_audit_sink = __esm({
231272
231913
  "../server/src/domains/actors/registry-audit-sink.ts"() {
231273
231914
  "use strict";
231274
- fs22 = __toESM(require("node:fs"), 1);
231915
+ fs23 = __toESM(require("node:fs"), 1);
231275
231916
  }
231276
231917
  });
231277
231918
 
@@ -233201,8 +233842,18 @@ var init_review_requirement_activity = __esm({
233201
233842
  });
233202
233843
 
233203
233844
  // ../server/src/domains/collab/activity.ts
233845
+ function coordinatorCardGapId(cardId) {
233846
+ return cardId.startsWith(COORDINATOR_CARD_PREFIX) ? cardId.slice(COORDINATOR_CARD_PREFIX.length) : void 0;
233847
+ }
233848
+ function coordinatorRunOfGap(gap, runs) {
233849
+ const opensAt = gap.createdAt;
233850
+ const closesAt = gap.resolvedAt ?? null;
233851
+ const overlapping = runs.filter((r) => (!closesAt || r.startedAt <= closesAt) && (!r.endedAt || r.endedAt >= opensAt)).sort((a, b2) => a.startedAt.localeCompare(b2.startedAt) || a.id.localeCompare(b2.id));
233852
+ return overlapping.find((r) => r.startedAt >= opensAt) ?? overlapping.at(-1);
233853
+ }
233204
233854
  function escalationKindOf(issueId, authorActorId) {
233205
233855
  if (issueId.startsWith(RETRY_EXHAUSTED_ESC_PREFIX)) return "retry_exhausted";
233856
+ if (issueId.startsWith(REVIEW_EXHAUSTED_ESC_PREFIX2)) return "review_exhausted";
233206
233857
  return isSystemActorId(authorActorId) ? "output_stall" : "manual";
233207
233858
  }
233208
233859
  function parseStopLossFacts(kind, body2) {
@@ -233211,7 +233862,7 @@ function parseStopLossFacts(kind, body2) {
233211
233862
  const reason = /末次失败原因(原文):([^\n]+)/.exec(body2)?.[1]?.trim();
233212
233863
  return { ...n2 ? { count: Number(n2) } : {}, ...reason ? { lastReason: firstSentence(reason) } : {} };
233213
233864
  }
233214
- const n = /(?:跑了|累计有)\s*(\d+)\s*次/.exec(body2)?.[1];
233865
+ const n = /(?:跑了|有)\s*(\d+)\s*轮/.exec(body2)?.[1] ?? /(?:跑了|累计有)\s*(\d+)\s*次/.exec(body2)?.[1];
233215
233866
  const rawCost = /约\s*\$([\d.]+)/.exec(body2)?.[1];
233216
233867
  const cost = rawCost !== void 0 && Number.isFinite(Number(rawCost)) ? `$${Number(rawCost).toFixed(2)}` : void 0;
233217
233868
  return { ...n ? { count: Number(n) } : {}, ...cost ? { cost } : {} };
@@ -233569,7 +234220,7 @@ function buildWorkorderActivity(input) {
233569
234220
  card2.segmentOpen = false;
233570
234221
  card2.status = interrupt.kind === "gap" ? ACTIVITY_COPY.interruptedByGap(name, nameOf(managerId ?? ownerId)) : ACTIVITY_COPY.interruptedByIssue(name);
233571
234222
  if (interrupt.kind === "gap" && interrupt.authorId) {
233572
- card2.actions = [{ kind: "open_thread", label: BUTTON.gapThread, target: gapThreadSessionId(interrupt.authorId) }];
234223
+ card2.actions = [{ kind: "open_thread", label: BUTTON.view, target: gapThreadSessionId(interrupt.authorId) }];
233573
234224
  }
233574
234225
  if (interrupt.at > card2.updatedAt) card2.updatedAt = interrupt.at;
233575
234226
  } else if (supersededBy) {
@@ -233623,6 +234274,7 @@ function buildWorkorderActivity(input) {
233623
234274
  const recoveryLetterIssueIds = /* @__PURE__ */ new Set();
233624
234275
  const plainComments = [];
233625
234276
  const escalations = [];
234277
+ const gapCreatedSeq = /* @__PURE__ */ new Map();
233626
234278
  const plainResolveEvents = /* @__PURE__ */ new Map();
233627
234279
  const acceptCards = /* @__PURE__ */ new Map();
233628
234280
  const standalone = [];
@@ -233879,7 +234531,10 @@ function buildWorkorderActivity(input) {
233879
234531
  });
233880
234532
  break;
233881
234533
  }
233882
- if (isGap) break;
234534
+ if (isGap) {
234535
+ gapCreatedSeq.set(issueId, rec2.seq);
234536
+ break;
234537
+ }
233883
234538
  if (isRecoveryLetter) recoveryLetterIssueIds.add(issueId);
233884
234539
  const nodeAssignee = nodeId ? nodeById.get(nodeId)?.assigneeActorId : void 0;
233885
234540
  const commentCounterpart = nodeAssignee ?? ownerId;
@@ -233905,7 +234560,7 @@ function buildWorkorderActivity(input) {
233905
234560
  全库实测最长 8096 字印两遍(`ws:wo-56bba3c1`),一张卡顶掉整屏。 */
233906
234561
  artifacts: [],
233907
234562
  /* 缺口不出按钮(处置在缺口线上,由「任务内沟通」那一列进);
233908
- 留言给「去查看」直达留言线——线是投影出来的、恒存在,id 这里就能算;
234563
+ 留言给「查看留言」直达留言线——线是投影出来的、恒存在,id 这里就能算;
233909
234564
  回压仍给「去处理」落到产物预览栏(那儿才有批注的处置面)。
233910
234565
  留言算不出对面是谁(该产物没负责人)时退回「去处理」,别给一个开不了的落点。 */
233911
234566
  actions: isBackpressure || !commentTargetId ? [{ kind: "open_issue", label: BUTTON.handle, target: issueId }] : [{ kind: "open_thread", label: BUTTON.view, target: commentThreadSessionId(authorId, commentTargetId) }]
@@ -234079,150 +234734,135 @@ function buildWorkorderActivity(input) {
234079
234734
  commentIssueIds: ids2
234080
234735
  });
234081
234736
  }
234082
- {
234083
- const groupKeyOf2 = (e, kind) => kind === "output_stall" ? `${e.nodeId ?? ""}|${kind}` : `solo|${e.id}`;
234084
- const groups = /* @__PURE__ */ new Map();
234085
- for (const e of [...escalations].sort((a, b2) => a.at.localeCompare(b2.at) || a.seq - b2.seq)) {
234086
- const kind = escalationKindOf(e.id, issueById.get(e.id)?.authorActorId ?? e.authorId);
234087
- const key2 = groupKeyOf2(e, kind);
234088
- groups.set(key2, [...groups.get(key2) ?? [], { ...e, kind }]);
234089
- }
234090
- for (const members of groups.values()) {
234091
- const first = members[0];
234092
- const latest = members.at(-1);
234093
- const kind = latest.kind;
234094
- const resolvedAt = (id) => issueById.get(id)?.resolvedAt ?? null;
234095
- const open3 = members.filter((m2) => !resolvedAt(m2.id));
234096
- const target = resolveEscalationRecipient({
234097
- recipients: latest.recipients,
234098
- authorActorId: latest.authorId,
234099
- stuckActorId: latest.stuckActorId,
234100
- ownerActorId: ownerId,
234101
- managerActorId: managerId
234102
- });
234103
- const stuckActor = (isStopLoss(kind) ? latest.nodeId ? nodeById.get(latest.nodeId)?.assigneeActorId : void 0 : void 0) ?? latest.stuckActorId;
234104
- const active = open3.at(-1) ?? latest;
234105
- const inDiscussion = open3.some((m2) => input.escalationsInDiscussion?.has(m2.id));
234106
- const stamps = [
234107
- ...members.map((m2) => m2.at),
234108
- ...members.map((m2) => resolvedAt(m2.id)).filter((t) => !!t)
234109
- ].sort();
234110
- const gapBehind = (id) => {
234111
- const gapId = issueById.get(id)?.gapId;
234112
- return typeof gapId === "string" && !!issueById.get(gapId);
234113
- };
234114
- const head = kind === "retry_exhausted" ? ACTIVITY_COPY.escalationHeadRetryExhausted(nodeName(latest.nodeId)) : kind === "output_stall" ? ACTIVITY_COPY.escalationHeadOutputStall(nodeName(latest.nodeId)) : gapBehind(latest.id) ? ACTIVITY_COPY.escalationHeadGapEscalated(nameOf(latest.authorId), nodeName(latest.nodeId)) : ACTIVITY_COPY.escalationHeadManual(nameOf(stuckActor));
234115
- const howTo = kind === "retry_exhausted" ? ACTIVITY_COPY.escalationHowToRetryExhausted : kind === "output_stall" ? ACTIVITY_COPY.escalationHowToOutputStall : void 0;
234116
- const closing = issueById.get(latest.id);
234117
- const closeEvent = plainResolveEvents.get(latest.id);
234118
- const closedBy = text5(closing?.resolvedBy) ?? closeEvent?.by ?? target;
234119
- const closingNote = text5(closing?.resolvedNote) ?? text5(closeEvent?.note);
234120
- const noteOf = (id) => text5(issueById.get(id)?.resolvedNote) ?? text5(plainResolveEvents.get(id)?.note);
234121
- const rerunClosed = closingNote === ESCALATION_RERUN_NOTE;
234122
- const rerunOrdinal = escalations.filter((e) => e.nodeId === latest.nodeId && noteOf(e.id) === ESCALATION_RERUN_NOTE && (issueById.get(e.id)?.resolvedAt ?? "") <= (issueById.get(latest.id)?.resolvedAt ?? "")).length;
234123
- const doneStatus = rerunClosed ? ACTIVITY_COPY.escalationRerun(nameOf(closedBy), Math.max(rerunOrdinal, 1)) : ACTIVITY_COPY.escalationHandled(head, nameOf(closedBy), closingNote);
234124
- const retryExhaustedSoFar = escalations.filter((e) => e.nodeId === latest.nodeId && escalationKindOf(e.id, issueById.get(e.id)?.authorActorId ?? e.authorId) === "retry_exhausted" && e.at <= latest.at).length;
234125
- const viewerIsTarget = !input.viewerActorId || input.viewerActorId === target;
234126
- const offerRerun = kind === "retry_exhausted" && viewerIsTarget && retryExhaustedSoFar < 2 && !!latest.nodeId;
234127
- issueCards.set(first.id, {
234128
- id: `issue:${first.id}`,
234129
- seq: first.seq,
234130
- at: first.at,
234131
- updatedAt: stamps.at(-1) ?? first.at,
234132
- ...first.nodeId ? { nodeId: first.nodeId } : {},
234133
- executorId: target,
234134
- /* 代操作人只在**还没关**的时候带:那是「谁替谁把这条上报派成待办」的事实。
234135
- 关掉之后归因只认这次收口(`resolvedBy`),不沿用派发待办时的代操作者。 */
234136
- ...open3.length > 0 && active.handActorId ? { handActorId: active.handActorId } : {},
234137
- escalationKind: kind,
234138
- /* 大状态三档(F5):待处理 → 处理中(讨论房里已经有人说过话)→ 已完成。
234139
- 颜色跟着「这一段还开着吗」走:没关 ⇒ 暖橙(与「待开始」同一档),关了 ⇒ 灰。
234140
- 关掉那一档不覆写 `phaseLabel`,用四档默认的「已完成」。 */
234141
- phase: open3.length === 0 ? "done" : inDiscussion ? "running" : "not_started",
234142
- segmentOpen: open3.length > 0,
234143
- ...open3.length === 0 ? {} : { phaseLabel: inDiscussion ? PHASE_LABEL.escalationInProgress : PHASE_LABEL.escalationPending },
234144
- status: open3.length === 0 ? doneStatus : ACTIVITY_COPY.escalationPendingWith(head, nameOf(target), howTo),
234145
- /* 上报正文**不上卡**——与缺口 / 回压同一条口径。此前这里把整封信塞进展开区,
234146
- `ws:wo-635d7add` 那条渲染出的是一屏字,连内部 artifact id 都印在卡面上。
234147
- 全文的去处,就是那个「去处理」按钮。 */
234148
- artifacts: [],
234149
- /* 「去处理」进**还开着的那一条**上报自己的讨论房(`discuss_escalation`)——合并卡上点进去
234150
- 必须是要人处理的那一条,不是第一条。房间不在这里造:前端拿 escalationId 去打
234151
- `POST /api/inbox/discuss`,那个端点按项幂等(ADR 0079 D1「一上报一房」)。
234152
- 按钮文案按**查看者**分两态(Bug 0151 / PR #2003 / 用例 T-31):不是收件人就写「去查看」
234153
- ——他点进去只有只读视图,按钮不该承诺一件他做不到的事。查看者取不到时保持「去处理」。
234154
- 关掉之后按钮不收走、只换成「去查看」:那间讨论房是这条上报唯一的来龙去脉。 */
234155
- actions: open3.length === 0 ? [{ kind: "discuss_escalation", label: BUTTON.view, target: latest.id }] : offerRerun ? [
234156
- { kind: "rerun_node", label: BUTTON.rerun, target: active.id, variant: "primary" },
234157
- { kind: "discuss_escalation", label: BUTTON.handle, target: active.id, variant: "secondary" }
234158
- ] : [{
234159
- kind: "discuss_escalation",
234160
- label: viewerIsTarget ? BUTTON.handle : BUTTON.view,
234161
- target: active.id
234162
- }]
234163
- });
234164
- }
234737
+ for (const e of [...escalations].sort((a, b2) => a.at.localeCompare(b2.at) || a.seq - b2.seq)) {
234738
+ const kind = escalationKindOf(e.id, issueById.get(e.id)?.authorActorId ?? e.authorId);
234739
+ const resolvedAt = (id) => issueById.get(id)?.resolvedAt ?? null;
234740
+ const closedAt = resolvedAt(e.id);
234741
+ const open3 = !closedAt;
234742
+ const target = resolveEscalationRecipient({
234743
+ recipients: e.recipients,
234744
+ authorActorId: e.authorId,
234745
+ stuckActorId: e.stuckActorId,
234746
+ ownerActorId: ownerId,
234747
+ managerActorId: managerId
234748
+ });
234749
+ const stuckActor = (isStopLoss(kind) ? e.nodeId ? nodeById.get(e.nodeId)?.assigneeActorId : void 0 : void 0) ?? e.stuckActorId;
234750
+ const inDiscussion = open3 && !!input.escalationsInDiscussion?.has(e.id);
234751
+ const gapBehind = (id) => {
234752
+ const gapId = issueById.get(id)?.gapId;
234753
+ return typeof gapId === "string" && !!issueById.get(gapId);
234754
+ };
234755
+ const head = kind === "retry_exhausted" ? ACTIVITY_COPY.escalationHeadRetryExhausted(nodeName(e.nodeId)) : kind === "review_exhausted" ? ACTIVITY_COPY.escalationHeadReviewExhausted(nodeName(e.nodeId)) : kind === "output_stall" ? ACTIVITY_COPY.escalationHeadOutputStall(nodeName(e.nodeId)) : gapBehind(e.id) ? ACTIVITY_COPY.escalationHeadGapEscalated(nameOf(e.authorId), nodeName(e.nodeId)) : ACTIVITY_COPY.escalationHeadManual(nameOf(stuckActor));
234756
+ const howTo = kind === "retry_exhausted" ? ACTIVITY_COPY.escalationHowToRetryExhausted : kind === "review_exhausted" ? ACTIVITY_COPY.escalationHowToReviewExhausted : kind === "output_stall" ? ACTIVITY_COPY.escalationHowToOutputStall : void 0;
234757
+ const closing = issueById.get(e.id);
234758
+ const closeEvent = plainResolveEvents.get(e.id);
234759
+ const closedBy = text5(closing?.resolvedBy) ?? closeEvent?.by ?? target;
234760
+ const closingNote = text5(closing?.resolvedNote) ?? text5(closeEvent?.note);
234761
+ const noteOf = (id) => text5(issueById.get(id)?.resolvedNote) ?? text5(plainResolveEvents.get(id)?.note);
234762
+ const rerunClosed = closingNote === ESCALATION_RERUN_NOTE;
234763
+ const rerunOrdinal = escalations.filter((x2) => x2.nodeId === e.nodeId && noteOf(x2.id) === ESCALATION_RERUN_NOTE && (resolvedAt(x2.id) ?? "") <= (closedAt ?? "")).length;
234764
+ const doneStatus = rerunClosed ? ACTIVITY_COPY.escalationRerun(nameOf(closedBy), Math.max(rerunOrdinal, 1)) : ACTIVITY_COPY.escalationHandled(head, nameOf(closedBy), closingNote);
234765
+ const retryExhaustedSoFar = escalations.filter((x2) => x2.nodeId === e.nodeId && escalationKindOf(x2.id, issueById.get(x2.id)?.authorActorId ?? x2.authorId) === "retry_exhausted" && x2.at <= e.at).length;
234766
+ const viewerIsTarget = !input.viewerActorId || input.viewerActorId === target;
234767
+ const offerRerun = kind === "retry_exhausted" && viewerIsTarget && retryExhaustedSoFar < 2 && !!e.nodeId;
234768
+ issueCards.set(e.id, {
234769
+ id: `issue:${e.id}`,
234770
+ seq: e.seq,
234771
+ at: e.at,
234772
+ updatedAt: closedAt ?? e.at,
234773
+ ...e.nodeId ? { nodeId: e.nodeId } : {},
234774
+ executorId: target,
234775
+ /* 代操作人只在**还没关**的时候带:那是「谁替谁把这条上报派成待办」的事实。
234776
+ 关掉之后归因只认这次收口(`resolvedBy`),不沿用派发待办时的代操作者。 */
234777
+ ...open3 && e.handActorId ? { handActorId: e.handActorId } : {},
234778
+ escalationKind: kind,
234779
+ /* 大状态三档(F5):待处理 → 处理中(讨论房里已经有人说过话)→ 已完成。
234780
+ 关掉那一档不覆写 `phaseLabel`,用四档默认的「已完成」。 */
234781
+ phase: !open3 ? "done" : inDiscussion ? "running" : "not_started",
234782
+ segmentOpen: open3,
234783
+ /* 颜色(奕璇 2026-09-22 定的判据「是不是在等人动手」):**上报没关 ⇒ 砖红**——这一段就卡在人
234784
+ 身上,卡面给的正是「去处理」/「重新运行」。关了 ⇒ 不再等人,`segmentOpen` 也随之为假 ⇒ 灰。
234785
+ ⚠ **不按查看者分**:非收件人看到的按钮词是「查看留言」(Bug 0151 / 用例 T-31),但这件事照样
234786
+ 卡在人身上——颜色说的是这一段的状态,不是「轮到你了没有」。 */
234787
+ awaitingHuman: open3,
234788
+ ...open3 ? { phaseLabel: inDiscussion ? PHASE_LABEL.escalationInProgress : PHASE_LABEL.escalationPending } : {},
234789
+ status: open3 ? ACTIVITY_COPY.escalationPendingWith(head, nameOf(target), howTo) : doneStatus,
234790
+ /* 上报正文**不上卡**——与缺口 / 回压同一条口径。此前这里把整封信塞进展开区,
234791
+ `ws:wo-635d7add` 那条渲染出的是一屏字,连内部 artifact id 都印在卡面上。
234792
+ 全文的去处,就是那个「去处理」按钮。 */
234793
+ artifacts: [],
234794
+ /* 「去处理」进**这条上报自己的**讨论房(`discuss_escalation`)——卡与房一一对应(ADR 0079 D1
234795
+ 「一上报一房」),所以关掉之后这张卡仍然指着当初处理它的那间房,不会被后来新开的上报改指。
234796
+ 房间不在这里造:前端拿 escalationId 去打 `POST /api/inbox/discuss`,那个端点按项幂等。
234797
+ 按钮文案按**查看者**分两态(Bug 0151 / PR #2003 / 用例 T-31):不是收件人就写「查看留言」
234798
+ ——他点进去只有只读视图,按钮不该承诺一件他做不到的事。查看者取不到时保持「去处理」。
234799
+ 关掉之后按钮不收走、只换成「查看留言」:那间讨论房是这条上报唯一的来龙去脉。 */
234800
+ actions: !open3 ? [{ kind: "discuss_escalation", label: BUTTON.view, target: e.id }] : offerRerun ? [
234801
+ { kind: "rerun_node", label: BUTTON.rerun, target: e.id, variant: "primary" },
234802
+ { kind: "discuss_escalation", label: BUTTON.handle, target: e.id, variant: "secondary" }
234803
+ ] : [{
234804
+ kind: "discuss_escalation",
234805
+ label: viewerIsTarget ? BUTTON.handle : BUTTON.view,
234806
+ target: e.id
234807
+ }]
234808
+ });
234165
234809
  }
234166
234810
  const coordinatorCards = [];
234811
+ const gapsClosedByCoordinatorCard = /* @__PURE__ */ new Set();
234167
234812
  {
234168
- const runs = [...input.coordinatorRuns ?? []].filter((r) => !!r.id && !!r.nodeId).sort((a, b2) => a.startedAt.localeCompare(b2.startedAt) || a.id.localeCompare(b2.id));
234169
- const episodes = [];
234170
- for (const r of runs) {
234171
- const prev = episodes.at(-1);
234172
- const overlapping = !!prev && prev.nodeId === r.nodeId && prev.actorId === r.actorId && (prev.endedAt === null || r.startedAt <= prev.endedAt);
234173
- if (prev && overlapping) {
234174
- prev.endedAt = prev.endedAt === null || !r.endedAt ? null : r.endedAt > prev.endedAt ? r.endedAt : prev.endedAt;
234175
- prev.statuses.push(r.status ?? null);
234176
- continue;
234177
- }
234178
- episodes.push({
234179
- id: r.id,
234180
- nodeId: r.nodeId,
234181
- actorId: r.actorId,
234182
- startedAt: r.startedAt,
234183
- endedAt: r.endedAt ?? null,
234184
- statuses: [r.status ?? null]
234185
- });
234186
- }
234187
- for (const run of episodes) {
234188
- const name = nodeName(run.nodeId);
234189
- const endedAt = run.endedAt;
234190
- const inWindow2 = (at) => !!at && at >= run.startedAt && (!endedAt || at <= endedAt);
234191
- const escalated = snap.issues.find((i) => i.kind === "escalation" && (i.aboutNodeId ?? i.raisedByNodeId) === run.nodeId && escalationKindOf(i.id, i.authorActorId) === "manual" && inWindow2(i.createdAt));
234192
- const fixedGap = snap.issues.find((i) => i.kind === "gap" && (i.aboutNodeId ?? i.raisedByNodeId) === run.nodeId && inWindow2(i.resolvedAt));
234193
- const gapOnNode = snap.issues.find((i) => i.kind === "gap" && (i.aboutNodeId ?? i.raisedByNodeId) === run.nodeId && i.createdAt <= (endedAt ?? run.startedAt));
234194
- const failedRun = !!endedAt && run.statuses.every((st) => !!st && st !== "succeeded");
234813
+ const gaps = [...snap.issues].filter((i) => i.kind === "gap" && !!(i.aboutNodeId ?? i.raisedByNodeId)).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || a.id.localeCompare(b2.id));
234814
+ for (const gap of gaps) {
234815
+ const gapId = gap.id;
234816
+ const nodeId = gap.aboutNodeId ?? gap.raisedByNodeId;
234817
+ const name = nodeName(nodeId);
234818
+ const resolvedAt = gap.resolvedAt ?? null;
234819
+ const inWindow2 = (at) => !!at && at >= gap.createdAt && (!resolvedAt || at <= resolvedAt);
234820
+ const escalated = snap.issues.find((i) => i.kind === "escalation" && escalationKindOf(i.id, i.authorActorId) === "manual" && (i.gapId === gapId || (i.aboutNodeId ?? i.raisedByNodeId) === nodeId && inWindow2(i.createdAt)));
234821
+ const run = coordinatorRunOfGap(
234822
+ { createdAt: gap.createdAt, resolvedAt },
234823
+ (input.coordinatorRuns ?? []).filter((r) => !!r.id && r.nodeId === nodeId)
234824
+ );
234825
+ const failedRun = !!run?.endedAt && !!run.status && run.status !== "succeeded";
234826
+ const coordinatorId = gap.resolvedBy ?? run?.actorId ?? managerId ?? ownerId;
234827
+ const executorId = gap.authorActorId;
234195
234828
  const recipient = escalated ? resolveEscalationRecipient({
234196
234829
  recipients: (snap.issueRecipients ?? []).filter((r) => r.issueId === escalated.id).map((r) => r.actorId),
234197
234830
  authorActorId: escalated.authorActorId,
234198
- stuckActorId: gapOnNode?.authorActorId ?? escalated.authorActorId,
234831
+ stuckActorId: executorId,
234199
234832
  ownerActorId: ownerId,
234200
234833
  managerActorId: managerId
234201
234834
  }) : void 0;
234202
- coordinatorCards.push({
234203
- id: `${COORDINATOR_CARD_PREFIX}${run.id}`,
234204
- seq: 0,
234205
- at: run.startedAt,
234206
- updatedAt: endedAt ?? run.startedAt,
234207
- nodeId: run.nodeId,
234208
- executorId: run.actorId,
234209
- /* 大状态用四档默认词(进行中 / 已完成),不覆写 `phaseLabel`——「补上了还是升级了」
234210
- 在小状态那句话里说得比一个词清楚。颜色跟着「这一段还开着吗」走。 */
234211
- phase: endedAt ? "done" : "running",
234212
- segmentOpen: !endedAt,
234213
- status: !endedAt ? ACTIVITY_COPY.coordinatingNode(name) : escalated ? ACTIVITY_COPY.coordinatorEscalated(nameOf(run.actorId), name, nameOf(recipient)) : fixedGap ? ACTIVITY_COPY.coordinatorFixedGap(nameOf(run.actorId), nameOf(gapOnNode?.authorActorId)) : failedRun ? ACTIVITY_COPY.coordinatorFailed(name) : ACTIVITY_COPY.coordinatorInconclusive(nameOf(run.actorId), name),
234835
+ const segmentOpen = !resolvedAt && !escalated;
234836
+ const draft = {
234837
+ id: coordinatorCardId(gapId),
234838
+ seq: gapCreatedSeq.get(gapId) ?? 0,
234839
+ at: gap.createdAt,
234840
+ updatedAt: [gap.createdAt, resolvedAt, escalated?.createdAt, run?.endedAt].filter((t) => !!t).sort().at(-1),
234841
+ nodeId,
234842
+ executorId: coordinatorId,
234843
+ /* 大状态用四档默认词(进行中 / 已完成 / 执行中断),不覆写 `phaseLabel`——「补上了还是升级了」
234844
+ 在小状态那句话里说得比一个词清楚。颜色跟着 `segmentOpen` 走。 */
234845
+ phase: !segmentOpen ? "done" : failedRun ? "stuck" : "running",
234846
+ segmentOpen,
234847
+ /* 协调者卡**从不砖红**:缺口的第一接手人恒是协调者本人,这一段没有人在等着动手
234848
+ (奕璇 2026-09-22 的判据)。连它自己那一段跑挂了(`failedRun` ⇒ `phase: "stuck"`)也一样,
234849
+ 那时按 `segmentOpen` 走暖橙——真要人动手的是它升级出去的那张升级卡,红归那一张。
234850
+ 显式写 `false` 而不是省略:省略 = 不表态,前端会退回老规则(`stuck` + 开着 ⇒ 砖红)。 */
234851
+ awaitingHuman: false,
234852
+ status: resolvedAt ? ACTIVITY_COPY.coordinatorFixedGap(nameOf(coordinatorId), nameOf(executorId)) : escalated ? ACTIVITY_COPY.coordinatorEscalated(nameOf(coordinatorId), name, nameOf(recipient)) : failedRun ? ACTIVITY_COPY.coordinatorFailed(name) : ACTIVITY_COPY.coordinatingGap(nameOf(coordinatorId), name),
234214
234853
  artifacts: [],
234215
- /* 点进去 = **这段协调轨迹**(run 在 `agent_runs` 里,`runId` 直接给)+ 缺口线。 */
234216
- traceAvailable: true,
234217
- runId: run.id,
234218
- actions: gapOnNode ? [{ kind: "open_thread", label: BUTTON.gapThread, target: gapThreadSessionId(gapOnNode.authorActorId) }] : []
234219
- });
234854
+ /* 点进去 = **这段协调轨迹**(run 在 `agent_runs` 里,`runId` 直接给)+ 缺口线。
234855
+ 查不到那一段运行 ⇒ 不给轨迹入口,卡与那颗「查看留言」照旧。 */
234856
+ ...run ? { traceAvailable: true, runId: run.id } : {},
234857
+ actions: [{ kind: "open_thread", label: BUTTON.view, target: gapThreadSessionId(executorId) }]
234858
+ };
234859
+ coordinatorCards.push(draft);
234860
+ if (resolvedAt) gapsClosedByCoordinatorCard.add(gapId);
234220
234861
  }
234221
234862
  }
234222
- for (const [issueId, d] of [...issueCards]) {
234863
+ for (const issueId of [...issueCards.keys()]) {
234223
234864
  if (!issueId.startsWith(REWORK_ANNOTATION_PREFIX)) continue;
234224
- const covered = coordinatorCards.some((c) => c.nodeId === d.nodeId && c.at <= d.at && d.at <= c.updatedAt);
234225
- if (covered) issueCards.delete(issueId);
234865
+ if (gapsClosedByCoordinatorCard.has(issueId.slice(REWORK_ANNOTATION_PREFIX.length))) issueCards.delete(issueId);
234226
234866
  }
234227
234867
  const lastFailureOf = (card2, trip) => {
234228
234868
  const message = (input.runFailures ?? []).filter((f2) => f2.nodeId === card2.nodeId && f2.startedAt >= card2.at && f2.startedAt <= trip.at && f2.message.trim()).sort((a, b2) => a.startedAt.localeCompare(b2.startedAt)).at(-1)?.message;
@@ -234349,6 +234989,7 @@ function buildWorkorderActivity(input) {
234349
234989
  d.phase = "done";
234350
234990
  delete d.phaseLabel;
234351
234991
  d.segmentOpen = false;
234992
+ d.awaitingHuman = false;
234352
234993
  d.status = ACTIVITY_COPY.closedNoAction(terminal);
234353
234994
  if (!d.commentIssueIds) d.actions = [];
234354
234995
  }
@@ -234417,6 +235058,7 @@ function buildWorkorderActivity(input) {
234417
235058
  phase: d.phase,
234418
235059
  ...d.phaseLabel ? { phaseLabel: d.phaseLabel } : {},
234419
235060
  ...d.segmentOpen === void 0 ? {} : { segmentOpen: d.segmentOpen },
235061
+ ...d.awaitingHuman === void 0 ? {} : { awaitingHuman: d.awaitingHuman },
234420
235062
  ...d.escalationKind ? { escalationKind: d.escalationKind } : {},
234421
235063
  ...d.noticeTitle ? { noticeTitle: d.noticeTitle } : {},
234422
235064
  status: d.status,
@@ -234434,7 +235076,7 @@ function buildWorkorderActivity(input) {
234434
235076
  cards.sort((a, b2) => a.at.localeCompare(b2.at) || a.seq - b2.seq || a.id.localeCompare(b2.id));
234435
235077
  return { workorderId, cards, truncated };
234436
235078
  }
234437
- var ACTIVITY_EVENT_KINDS, LEGACY_PAUSE_REASON, ACTIVITY_EVENT_LIMIT, UPSTREAM_ACK_NOTE_CAP, ACTIVITY_COPY, REWORK_ANNOTATION_PREFIX, RECOVERY_ANNOTATION_PREFIX, ENGINE_RETRY_EXHAUSTED, BUTTON, PHASE_LABEL, COORDINATOR_CARD_PREFIX, RETRY_EXHAUSTED_ESC_PREFIX, isStopLoss;
235079
+ var ACTIVITY_EVENT_KINDS, LEGACY_PAUSE_REASON, ACTIVITY_EVENT_LIMIT, UPSTREAM_ACK_NOTE_CAP, ACTIVITY_COPY, REWORK_ANNOTATION_PREFIX, RECOVERY_ANNOTATION_PREFIX, ENGINE_RETRY_EXHAUSTED, BUTTON, PHASE_LABEL, COORDINATOR_CARD_PREFIX, coordinatorCardId, RETRY_EXHAUSTED_ESC_PREFIX, REVIEW_EXHAUSTED_ESC_PREFIX2, isStopLoss;
234438
235080
  var init_activity2 = __esm({
234439
235081
  "../server/src/domains/collab/activity.ts"() {
234440
235082
  "use strict";
@@ -234567,9 +235209,10 @@ var init_activity2 = __esm({
234567
235209
  改完之后两种止损的**执行者是一样的**(执行卡挂执行人、升级卡挂等处理的那个人),
234568
235210
  能不能分辨全落在这几句话上。投影侧怎么判是哪一种见 {@link escalationKindOf}。
234569
235211
  次数与花费**取上报正文里系统自己算出来的那个数**(引擎 `activate.ts` 的 `连续 N 次执行失败`、
234570
- serve `zero-output-breaker.ts` 的 `跑了 N 次 … 约 $X`)——本投影手上没有第二份可信来源:
234571
- 保险丝数的是 30 分钟窗口里的 `agent_runs` 空跑数(`ws:wo-7cf619ff` 实测 5 次),而快照里
234572
- 同一节点只有 3 条 work,按 work 数自己算会说出一个跟上报正文对不上的数。 */
235212
+ serve `zero-output-breaker.ts` 的 `跑了 N 轮 … 约 $X`)——本投影手上没有第二份可信来源,
235213
+ 上报正文是唯一事实源(奕璇 2026-09-22 定)。两头的口径都已经是**轮**:引擎那条本来就数
235214
+ work,保险丝那条 Bug 0211 起把同一轮里的协议续唤 run 折回去再数(老正文不回填,见
235215
+ {@link parseStopLossFacts})。 */
234573
235216
  retryExhausted: (executor, node2, n, lastReason) => `${executor} \u8FDE\u7EED ${n} \u6B21\u6CA1\u8DD1\u6210\u300A${node2}\u300B\uFF0C\u91CD\u8BD5\u5DF2\u7528\u5C3D${lastReason ? `\uFF08\u672B\u6B21\uFF1A${lastReason}\uFF09` : ""}\u3002`,
234574
235217
  outputStalled: (executor, node2, n, cost) => `${executor} \u8FDE\u7EED ${n} \u6B21\u6CA1\u4EA4\u51FA\u300A${node2}\u300B\u7684\u4EFB\u4F55\u6210\u679C${cost ? `\uFF08\u7EA6 ${cost}\uFF09` : ""}\uFF0C\u7CFB\u7EDF\u5DF2\u6B62\u635F\u3002`,
234575
235218
  /* ── 升级卡(上报)的整句 ───────────────────────────────────────────────────────────
@@ -234578,6 +235221,16 @@ var init_activity2 = __esm({
234578
235221
  同一件事,只换后半句(「等 @X 处理」→「X 已处理:…」)。 */
234579
235222
  escalationHeadRetryExhausted: (node2) => `\u300A${node2}\u300B\u91CD\u8BD5\u5DF2\u7528\u5C3D\uFF0C`,
234580
235223
  escalationHeadOutputStall: (node2) => `\u7CFB\u7EDF\u6B62\u635F\uFF1A\u300A${node2}\u300B\u505C\u4F4F\u4E86\uFF0C`,
235224
+ /**
235225
+ * 评审连败熔断(Bug 0212):卡住的是**这一轮评审**,不是那一版活——话不能写成「执行异常」。
235226
+ *
235227
+ * 🚨 **这里没有「(末次:…)」**,与连败熔断卡不同:报错原话只在 `agent_runs.error_message`,
235228
+ * 而**评审派发的 run 行 `node_id` 与 `work_order_id` 双双为 null**(v2 `ws:wo-65841524` 实测:
235229
+ * qa-quality 连败那 4 条只有 `artifact_id`)——`listRunFailures` 按 `work_order_id` 圈范围,
235230
+ * 这批行根本取不到。硬拼一句取不到值的模板只会永远不出现,不如不写。
235231
+ * 要补这半句,得先让评审派发把 `work_order_id` / `node_id` 记进 `agent_runs`(另开单)。
235232
+ */
235233
+ escalationHeadReviewExhausted: (node2) => `\u300A${node2}\u300B\u7684\u8BC4\u5BA1\u8FDE\u7740\u6CA1\u8DD1\u6210\uFF0C\u8FD9\u4E00\u8F6E\u6536\u4E0D\u9F50\uFF0C`,
234581
235234
  /** 人工上报里**关联着缺口**的那一档:协调者接了缺口、补不了,于是升级给人(四种停法的第二种)。 */
234582
235235
  escalationHeadGapEscalated: (coordinator, node2) => `${coordinator} \u8865\u4E0D\u4E86\u300A${node2}\u300B\u7684\u7F3A\u53E3\uFF0C`,
234583
235236
  /** 其余人工上报:主语是卡住的那个执行人(异常是它的,不是上报作者的)。 */
@@ -234593,6 +235246,8 @@ var init_activity2 = __esm({
234593
235246
  /** 「你要做什么」那半句——两种系统止损各一份(F6 的立论就在这里:执行者一样,只有话不一样)。 */
234594
235247
  escalationHowToRetryExhausted: "\u5904\u7406\u5E76\u5173\u6389\u8FD9\u6761\u4E0A\u62A5\u540E\uFF0C\u8FD9\u4E00\u6B65\u4F1A\u81EA\u52A8\u91CD\u8BD5\uFF1B\u591A\u534A\u662F\u989D\u5EA6 / \u767B\u5F55\u8FC7\u671F / \u9650\u6D41\u8FD9\u7C7B\u73AF\u5883\u95EE\u9898\uFF0C\u5148\u770B\u8F68\u8FF9\u518D\u51B3\u5B9A\u91CD\u8BD5\u3002",
234595
235248
  escalationHowToOutputStall: "\u5904\u7406\u5E76\u5173\u6389\u8FD9\u6761\u4E0A\u62A5\u540E\uFF0C\u8FD9\u4E00\u6B65\u4F1A\u81EA\u52A8\u91CD\u8BD5\uFF1B\u4E0D\u67E5\u6E05\u539F\u56E0\u5C31\u91CD\u8BD5\u4F1A\u7EE7\u7EED\u7A7A\u70E7\uFF0C\u5148\u8FDB\u8BA8\u8BBA\u623F\u3002",
235249
+ /** 评审熔断那一份:关掉之后重开的是**评审**,不是这一步的活(活已经交了,等的是审)。 */
235250
+ escalationHowToReviewExhausted: "\u5904\u7406\u5E76\u5173\u6389\u8FD9\u6761\u4E0A\u62A5\u540E\uFF0C\u8FD9\u4E00\u8F6E\u8BC4\u5BA1\u4F1A\u81EA\u52A8\u91CD\u5F00\uFF1B\u591A\u534A\u662F\u8BC4\u5BA1\u5458\u7684\u8FD0\u884C\u73AF\u5883\u51FA\u4E86\u95EE\u9898\uFF08\u989D\u5EA6 / \u767B\u5F55\u8FC7\u671F / \u9650\u6D41\uFF09\uFF0C\u5148\u770B\u8F68\u8FF9\u518D\u51B3\u5B9A\u3002",
234596
235251
  /** 关掉了:接着开头那半句说「谁处理的、怎么处理的」。没写处置说明就只说「已处理」。
234597
235252
  * 处置说明常常是人自己写的一整段、末尾自带句号——**别再补一个**,否则卡面收在「。。」上。 */
234598
235253
  escalationHandled: (head, by, note) => `${head}${by} \u5DF2\u5904\u7406${note ? `\uFF1A${note}` : ""}${note && /[。!?.!?]$/.test(note) ? "" : "\u3002"}`,
@@ -234601,15 +235256,20 @@ var init_activity2 = __esm({
234601
235256
  * ——人并没有写说明,说明是这颗按钮替他写的。次数是**这个节点上第几次这么重试**。
234602
235257
  */
234603
235258
  escalationRerun: (by, n) => `${by} \u91CD\u65B0\u8FD0\u884C\u4E86\u8FD9\u4E00\u6B65\uFF08\u7B2C ${n} \u6B21\u91CD\u8BD5\uFF09\u3002`,
234604
- /* ── 协调者卡(F2,新增)──────────────────────────────────────────────────────────
234605
- 对应「协调者处理这个节点的卡点」那一段运行(`agent_runs` 里 id 为 `coordinate:<uuid>` 的行,
234606
- 生产 9/10 起 9 天 71 段、17 张单,此前一张卡都没有)。它不在引擎快照里,由 handler 喂进来。 */
234607
- coordinatingNode: (node2) => `\u6B63\u5728\u5904\u7406\u300A${node2}\u300B\u7684\u7F3A\u53E3\u3002`,
235259
+ /* ── 协调者卡(F2)──────────────────────────────────────────────────────────────────
235260
+ 「协调者处理**某条缺口**的那一段」。三句话对应缺口的三种去向(已补齐 / 补不了升级给人 /
235261
+ 还开着),判据全在缺口这条 issue 上,不看旁账那条 `coordinate:` 运行有没有留下结论
235262
+ (Bug 0213;前两句 T-104 / T-113 已验收,逐字不动)。 */
235263
+ coordinatingGap: (coordinator, node2) => `${coordinator} \u6B63\u5728\u5904\u7406\u300A${node2}\u300B\u7684\u7F3A\u53E3\u3002`,
234608
235264
  coordinatorFixedGap: (coordinator, executor) => `${coordinator} \u5DF2\u8865\u9F50\u7F3A\u53E3\uFF0C${executor} \u7EE7\u7EED\u6267\u884C\u3002`,
234609
235265
  coordinatorEscalated: (coordinator, node2, target) => `${coordinator} \u8865\u4E0D\u4E86\u300A${node2}\u300B\u7684\u7F3A\u53E3\uFF0C\u5DF2\u5347\u7EA7\u7ED9 @${target} \u5904\u7406\u3002`,
234610
- /** 这一段结束了,缺口既没补上也没升级——如实说到此为止,别替它宣布一个没发生的结局。 */
234611
- coordinatorInconclusive: (coordinator, node2) => `${coordinator} \u5904\u7406\u300A${node2}\u300B\u7684\u7F3A\u53E3\uFF0C\u8FD9\u4E00\u6BB5\u6CA1\u6709\u7ED3\u8BBA\u3002`,
234612
- /** 协调者这一段自己没跑成(run 判 failed / orphaned / timeout)——如实说,别冒充「已处理」。 */
235266
+ /**
235267
+ * 缺口还开着、也没升级给人,而这一段协调运行已经结束且没跑成(failed / orphaned / timeout)——
235268
+ * 如实说这一轮没跑成,别说「正在处理」(没人在处理),也别替它宣布一个没发生的结局。
235269
+ *
235270
+ * 🚨 兜底句「{协调者} 处理《X》的缺口,这一段没有结论」**已删**(Bug 0213):那句是拿 run 的结论
235271
+ * 拼卡时的产物——取不到结论就替这一段下一个空结论。缺口自己的状态永远答得出「补上了没有」。
235272
+ */
234613
235273
  coordinatorFailed: (node2) => `\u5904\u7406\u300A${node2}\u300B\u7684\u7F3A\u53E3\u65F6\u8FD9\u4E00\u8F6E\u6CA1\u8DD1\u6210\u3002`,
234614
235274
  /**
234615
235275
  * 这一轮**被计划变更打断**(P15)。与卡点中断同级:计划一动,这一轮就不再是「还在跑」,
@@ -234724,7 +235384,7 @@ var init_activity2 = __esm({
234724
235384
  *
234725
235385
  * 此前走 `issueOpen`:正文整段当小状态,`detail` 又是同一段。全库实测最长
234726
235386
  * **8096 字印两遍**(`ws:wo-56bba3c1`),一张卡顶掉整屏。正文的去处是「留言线」——
234727
- * 与缺口线同一套投影(`gap-thread.ts` 的 `buildCommentThread`),卡上给「去查看」。
235387
+ * 与缺口线同一套投影(`gap-thread.ts` 的 `buildCommentThread`),卡上给「查看留言」。
234728
235388
  */
234729
235389
  commentRaised: (author, target, node2) => `${author} \u5728\u300A${node2}\u300B\u7559\u8A00\uFF0C\u9700\u8981 @${target} \u67E5\u770B\u3002`,
234730
235390
  /* ── 普通留言卡(奕璇 2026-09-18 定,ADR 0594)────────────────────────────────────────
@@ -234766,9 +235426,17 @@ var init_activity2 = __esm({
234766
235426
  acceptPass: "\u9A8C\u6536\u901A\u8FC7",
234767
235427
  acceptFail: "\u9A8C\u6536\u4E0D\u901A\u8FC7",
234768
235428
  handle: "\u53BB\u5904\u7406",
234769
- view: "\u53BB\u67E5\u770B",
234770
- /** 缺口线(执行人 ↔ 协调者)。缺口卡删掉之后,正文与来回的入口挂在执行卡与协调者卡上(F4)。 */
234771
- gapThread: "\u53BB\u7F3A\u53E3\u7EBF",
235429
+ /**
235430
+ * **只读进房**那一档:留言线、缺口线(执行人 ↔ 协调者)、上报讨论房(旁观者 / 已关掉)共用这一个词。
235431
+ *
235432
+ * 2026-09-22 奕璇拍板把旧的两个名字**并成一个**:缺口线那两处原来写「去缺口线」
235433
+ * (`BUTTON.gapThread`),其余只读入口原来写「去查看」——三处点进去做的是同一件事:
235434
+ * **进这条线看聊天记录**,用户视角没有第二、第三个名字。目标(`open_thread` /
235435
+ * `discuss_escalation` 与 target)一个字没动,只是不再自造第二个词。
235436
+ * 于是卡上只剩三种按钮:「去处理」(等人动手)、「查看留言」(只读进房)、
235437
+ * 「重新运行」(连败熔断升级卡的主按钮)。
235438
+ */
235439
+ view: "\u67E5\u770B\u7559\u8A00",
234772
235440
  /** 连败熔断升级卡的主按钮(F7):重开这一步,并把那条上报一并关掉。 */
234773
235441
  rerun: "\u91CD\u65B0\u8FD0\u884C"
234774
235442
  };
@@ -234781,8 +235449,10 @@ var init_activity2 = __esm({
234781
235449
  escalationInProgress: "\u5904\u7406\u4E2D"
234782
235450
  };
234783
235451
  COORDINATOR_CARD_PREFIX = "coordinator:";
235452
+ coordinatorCardId = (gapId) => `${COORDINATOR_CARD_PREFIX}${gapId}`;
234784
235453
  RETRY_EXHAUSTED_ESC_PREFIX = "esc:retry-exhausted:";
234785
- isStopLoss = (kind) => kind !== "manual";
235454
+ REVIEW_EXHAUSTED_ESC_PREFIX2 = "esc:review-exhausted:";
235455
+ isStopLoss = (kind) => kind === "retry_exhausted" || kind === "output_stall";
234786
235456
  }
234787
235457
  });
234788
235458
 
@@ -234800,10 +235470,12 @@ function buildWorkorderActivityTrace(input) {
234800
235470
  const reviews = activityReviews(snap, input.events ?? []);
234801
235471
  const reviewTarget = reviewCardTarget(cardId, reviews);
234802
235472
  if (reviewTarget) return buildReviewHistory(input, reviewTarget.targetWorkId, reviewTarget.reviewerActorId, reviews);
234803
- if (cardId.startsWith(COORDINATOR_CARD_PREFIX)) {
234804
- const runId = cardId.slice(COORDINATOR_CARD_PREFIX.length);
234805
- const run = (input.coordinatorRuns ?? []).find((r) => r.id === runId);
234806
- if (!run) return empty2;
235473
+ const coordinatorGapId = coordinatorCardGapId(cardId);
235474
+ if (coordinatorGapId !== void 0) {
235475
+ const gap2 = snap.issues.find((i) => i.id === coordinatorGapId && i.kind === "gap");
235476
+ const gapNodeId = gap2 ? gap2.aboutNodeId ?? gap2.raisedByNodeId : null;
235477
+ const run = gap2 && gapNodeId ? coordinatorRunOfGap(gap2, (input.coordinatorRuns ?? []).filter((r) => r.nodeId === gapNodeId)) : void 0;
235478
+ if (!run) return { ...empty2, notice: "\u534F\u8C03\u8005\u5904\u7406\u8FD9\u6761\u7F3A\u53E3\u7684\u90A3\u4E00\u6BB5\u6CA1\u6709\u7559\u4E0B\u8FD0\u884C\u8BB0\u5F55\uFF0C\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u8FD0\u884C\u8F68\u8FF9\u3002" };
234807
235479
  const node2 = snap.nodes.find((n) => n.id === run.nodeId);
234808
235480
  const spec = node2?.spec?.trim();
234809
235481
  return {
@@ -235080,11 +235752,14 @@ var init_activity_trace = __esm({
235080
235752
  });
235081
235753
 
235082
235754
  // ../server/src/domains/collab/inbox.ts
235755
+ function commandCardLine(command2) {
235756
+ return command2.displayLabel?.trim() || stripInternalIds(command2.effectLabel ?? "");
235757
+ }
235083
235758
  function changeConfirmSummary(r) {
235084
235759
  if (r.continuation) return `\u81EA\u52A8\u7EED\u8DD1\u5DF2\u5230\u9650\uFF0C\u7B49\u5F85\u4F60\u5BA1\u6838\uFF1A${r.plan.reason}`;
235085
235760
  const why = r.plan.reason.slice(0, 100);
235086
235761
  if (r.kind === "command" && r.command) {
235087
- return `\u7BA1\u7406\u8005\u8BF7\u6C42\u6267\u884C\u4E00\u4E2A\u9AD8\u5371\u64CD\u4F5C\uFF0C\u7B49\u4F60\u6388\u6743\uFF1A${r.command.effectLabel}`;
235762
+ return `\u7BA1\u7406\u8005\u8BF7\u6C42\u6267\u884C\u4E00\u4E2A\u9AD8\u5371\u64CD\u4F5C\uFF0C\u7B49\u4F60\u6388\u6743\uFF1A${commandCardLine(r.command)}`;
235088
235763
  }
235089
235764
  const ops = r.plan.ops ?? [];
235090
235765
  if (ops.length > 0 && ops.every((o) => o.action === "resolveEscalation")) {
@@ -235260,7 +235935,7 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
235260
235935
  // 仍回落工单详情。
235261
235936
  ...r.origin ? { chatSessionId: r.origin } : {},
235262
235937
  ...anchorArt ? { title: nodeLabel(anchorArt) } : {},
235263
- detail: `${actorName(r.proposedBy) || "\u7BA1\u7406\u8005"}\uFF1A${r.command?.effectLabel ?? firstSentence2(r.plan.reason)}`,
235938
+ detail: `${actorName(r.proposedBy) || "\u7BA1\u7406\u8005"}\uFF1A${r.command ? commandCardLine(r.command) : firstSentence2(r.plan.reason)}`,
235264
235939
  ...r.kind !== "command" && opCount > 0 ? { impact: `\u6D89\u53CA ${opCount} \u5904\u6539\u52A8${r.nonMonotonicOps.length > 0 ? " \xB7 \u542B\u9500\u6BC1\u6027" : ""}` } : {},
235265
235940
  actor: actorRef2(r.proposedBy),
235266
235941
  ...overdueField(r.at)
@@ -235482,6 +236157,15 @@ function backfillInboxUnread(items, markers) {
235482
236157
  item.unread = !Number.isFinite(effective) ? true : sinceMs > effective;
235483
236158
  }
235484
236159
  }
236160
+ function applyReadAllWatermark(items, markers) {
236161
+ const at = markers.find((m2) => m2.scope === INBOX_READ_ALL_SCOPE)?.readAt;
236162
+ const atMs = at ? Date.parse(at) : Number.NaN;
236163
+ if (!Number.isFinite(atMs)) return;
236164
+ for (const item of items) {
236165
+ const sinceMs = Date.parse(item.since);
236166
+ if (Number.isFinite(sinceMs) && sinceMs <= atMs) item.unread = false;
236167
+ }
236168
+ }
235485
236169
  var C_ACTION_LABEL;
235486
236170
  var init_inbox = __esm({
235487
236171
  "../server/src/domains/collab/inbox.ts"() {
@@ -236060,6 +236744,16 @@ var init_notification_store = __esm({
236060
236744
  }
236061
236745
  if (changed) this.save();
236062
236746
  }
236747
+ async markAllRead(recipient, at) {
236748
+ let n = 0;
236749
+ for (const [id, r] of this.rows) {
236750
+ if (r.recipient !== recipient || r.readAt) continue;
236751
+ this.rows.set(id, { ...r, readAt: at });
236752
+ n++;
236753
+ }
236754
+ if (n > 0) this.save();
236755
+ return n;
236756
+ }
236063
236757
  async countUnread(recipient) {
236064
236758
  let pending = 0;
236065
236759
  let all2 = 0;
@@ -236250,6 +236944,13 @@ var init_notification_store = __esm({
236250
236944
  [recipient, [...ids2], at]
236251
236945
  );
236252
236946
  }
236947
+ async markAllRead(recipient, at) {
236948
+ const r = await this.sql.query(
236949
+ `UPDATE ${this.t} SET read_at = $2::timestamptz WHERE recipient = $1 AND read_at IS NULL`,
236950
+ [recipient, at]
236951
+ );
236952
+ return r.rowCount ?? 0;
236953
+ }
236253
236954
  async countUnread(recipient) {
236254
236955
  const r = await this.sql.query(
236255
236956
  `SELECT count(*) FILTER (WHERE status = 'pending')::int AS pending, count(*)::int AS "all"
@@ -236780,7 +237481,7 @@ var init_workorder_metrics = __esm({
236780
237481
  });
236781
237482
 
236782
237483
  // ../server/src/domains/usage/statistics-model.ts
236783
- var UsageQueryError;
237484
+ var UsageQueryError, ZERO_USAGE_AMOUNT;
236784
237485
  var init_statistics_model = __esm({
236785
237486
  "../server/src/domains/usage/statistics-model.ts"() {
236786
237487
  "use strict";
@@ -236791,6 +237492,13 @@ var init_statistics_model = __esm({
236791
237492
  this.name = "UsageQueryError";
236792
237493
  }
236793
237494
  };
237495
+ ZERO_USAGE_AMOUNT = {
237496
+ tokens: { input: 0, cache: 0, cacheRead: 0, cacheCreation: 0, output: 0, total: 0 },
237497
+ costUsdMicros: 0,
237498
+ costParts: { input: 0, cacheRead: 0, cacheCreation: 0, output: 0, unsplit: 0 },
237499
+ costKnown: "full",
237500
+ costUnknownTokens: 0
237501
+ };
236794
237502
  }
236795
237503
  });
236796
237504
 
@@ -236999,6 +237707,16 @@ function effectivePoints(run) {
236999
237707
  }
237000
237708
  return points;
237001
237709
  }
237710
+ function ledgerRunModel(run) {
237711
+ const onRow = resolvedModel(run.effectiveModel);
237712
+ if (onRow) return onRow;
237713
+ let best = null;
237714
+ for (const p2 of run.points) {
237715
+ const m2 = resolvedModel(p2.model);
237716
+ if (m2 && (!best || p2.seq < best.seq)) best = { seq: p2.seq, model: m2 };
237717
+ }
237718
+ return best?.model ?? null;
237719
+ }
237002
237720
  function distribute(total, weights) {
237003
237721
  const sum = weights.reduce((s2, w2) => s2 + w2, 0);
237004
237722
  if (sum <= 0) return weights.map(() => 0);
@@ -237166,6 +237884,11 @@ function bucketIndex(starts, ms) {
237166
237884
  }
237167
237885
  return ans;
237168
237886
  }
237887
+ function pushView(map, key2, v2) {
237888
+ const list2 = map.get(key2);
237889
+ if (list2) list2.push(v2);
237890
+ else map.set(key2, [v2]);
237891
+ }
237169
237892
  function getCell(map, key2) {
237170
237893
  let c = map.get(key2);
237171
237894
  if (!c) {
@@ -237336,14 +238059,15 @@ function runtimeRows(facts, relations) {
237336
238059
  if (!v2.relevant) continue;
237337
238060
  const key2 = keyOf4(v2.run.runtimeInstanceId, v2.run.runtimeKind);
237338
238061
  getCell(cells, key2).addView(v2);
237339
- modelsOf(key2).cell(v2.run.effectiveModel).addView(v2);
238062
+ modelsOf(key2).cell(ledgerRunModel(v2.run)).addView(v2);
237340
238063
  }
237341
238064
  for (const r of facts.rounds) {
237342
238065
  const key2 = keyOf4(r.runtimeInstanceId, r.runtimeKind);
237343
238066
  const c = getCell(cells, key2);
237344
238067
  c.active = true;
237345
238068
  c.runCount += 1;
237346
- const m2 = modelsOf(key2).cell(runById.get(r.firstRunId)?.effectiveModel ?? null);
238069
+ const firstRun = runById.get(r.firstRunId);
238070
+ const m2 = modelsOf(key2).cell(firstRun ? ledgerRunModel(firstRun) : null);
237347
238071
  m2.active = true;
237348
238072
  m2.runCount += 1;
237349
238073
  }
@@ -237403,12 +238127,15 @@ function attributed(facts, relations) {
237403
238127
  const taskCells = /* @__PURE__ */ new Map();
237404
238128
  const taskActorCells = /* @__PURE__ */ new Map();
237405
238129
  const convSessionCells = /* @__PURE__ */ new Map();
238130
+ const convViews = /* @__PURE__ */ new Map();
238131
+ const taskViews = /* @__PURE__ */ new Map();
237406
238132
  const unlinked = new Cell();
237407
238133
  for (const v2 of facts.views) {
237408
238134
  if (!v2.relevant) continue;
237409
238135
  const attr = runAttribution(relations, v2.run, memo);
237410
238136
  if (attr.kind === "task") {
237411
238137
  getCell(taskCells, attr.workorderId).addView(v2);
238138
+ pushView(taskViews, attr.workorderId, v2);
237412
238139
  let byActor2 = taskActorCells.get(attr.workorderId);
237413
238140
  if (!byActor2) {
237414
238141
  byActor2 = /* @__PURE__ */ new Map();
@@ -237422,6 +238149,7 @@ function attributed(facts, relations) {
237422
238149
  convSessionCells.set(attr.rootSessionId, bySession);
237423
238150
  }
237424
238151
  getCell(bySession, attr.sessionId).addView(v2);
238152
+ pushView(convViews, attr.rootSessionId, v2);
237425
238153
  } else unlinked.addView(v2);
237426
238154
  }
237427
238155
  const taskAmount = /* @__PURE__ */ new Map();
@@ -237453,8 +238181,13 @@ function attributed(facts, relations) {
237453
238181
  }
237454
238182
  const conversations = [];
237455
238183
  const convGroupAcc = /* @__PURE__ */ new Map();
238184
+ const treeViews = /* @__PURE__ */ new Map();
237456
238185
  for (const rootId of /* @__PURE__ */ new Set([...convSessionCells.keys(), ...tasksByRoot.keys()])) {
237457
238186
  const root = relations.sessions.get(rootId);
238187
+ treeViews.set(rootId, [
238188
+ ...convViews.get(rootId) ?? [],
238189
+ ...(tasksByRoot.get(rootId) ?? []).flatMap((workorderId) => taskViews.get(workorderId) ?? [])
238190
+ ]);
237458
238191
  const group = new Cell();
237459
238192
  const rows = [];
237460
238193
  const sessionRows = [];
@@ -237576,9 +238309,83 @@ function attributed(facts, relations) {
237576
238309
  { amount: a.amount, name: a.name, id: a.projectId ?? "" },
237577
238310
  { amount: b2.amount, name: b2.name, id: b2.projectId ?? "" }
237578
238311
  ));
237579
- return { conversations, tasks, projects };
238312
+ return { conversations, tasks, projects, treeViews };
238313
+ }
238314
+ function deriveTreeDuration(views, now) {
238315
+ const nowMs = now.getTime();
238316
+ const spans = [];
238317
+ let countedRuns = 0;
238318
+ let unstartedRuns = 0;
238319
+ let inProgress = false;
238320
+ let firstMs = Infinity;
238321
+ let lastMs = -Infinity;
238322
+ let runtimeMs = 0;
238323
+ for (const v2 of views) {
238324
+ const startMs = v2.run.runtimeStartedAt ? Date.parse(v2.run.runtimeStartedAt) : NaN;
238325
+ if (!Number.isFinite(startMs)) {
238326
+ unstartedRuns += 1;
238327
+ continue;
238328
+ }
238329
+ const endedMs = v2.run.endedAt ? Date.parse(v2.run.endedAt) : NaN;
238330
+ const running = !Number.isFinite(endedMs) && !isRunEnded(v2.run);
238331
+ if (running) inProgress = true;
238332
+ const rawEnd = Number.isFinite(endedMs) ? endedMs : running ? nowMs : startMs;
238333
+ const end = Math.max(rawEnd, startMs);
238334
+ countedRuns += 1;
238335
+ runtimeMs += end - startMs;
238336
+ spans.push({ start: startMs, end });
238337
+ if (startMs < firstMs) firstMs = startMs;
238338
+ if (end > lastMs) lastMs = end;
238339
+ }
238340
+ if (countedRuns === 0) return { ...ZERO_DURATION, unstartedRuns };
238341
+ spans.sort((a, b2) => a.start - b2.start || a.end - b2.end);
238342
+ let activeMs = 0;
238343
+ let curStart = spans[0].start;
238344
+ let curEnd = spans[0].end;
238345
+ for (const span of spans.slice(1)) {
238346
+ if (span.start > curEnd) {
238347
+ activeMs += curEnd - curStart;
238348
+ curStart = span.start;
238349
+ curEnd = span.end;
238350
+ } else if (span.end > curEnd) curEnd = span.end;
238351
+ }
238352
+ activeMs += curEnd - curStart;
238353
+ return {
238354
+ firstStartedAt: new Date(firstMs).toISOString(),
238355
+ lastEndedAt: new Date(lastMs).toISOString(),
238356
+ inProgress,
238357
+ wallClockMs: lastMs - firstMs,
238358
+ runtimeMs,
238359
+ activeMs,
238360
+ countedRuns,
238361
+ unstartedRuns
238362
+ };
237580
238363
  }
237581
- var UNKNOWN_AGENT_NAME, UNREGISTERED_RUNTIME_NAME, UNSPECIFIED_PROJECT_NAME, UNLINKED_ROW_TITLE, UNTITLED_CONVERSATION, BUCKET_KEYS, AmountAcc, FlagAcc, Cell, collator, ModelCells;
238364
+ function buildChatSessionUsage(sessionId, input) {
238365
+ const facts = computeFacts(input);
238366
+ const lists = attributed(facts, input.relations);
238367
+ const attr = sessionAttribution(input.relations, sessionId, /* @__PURE__ */ new Map());
238368
+ const rootId = attr.kind === "conversation" ? attr.rootSessionId : sessionId;
238369
+ const session = input.relations.sessions.get(rootId);
238370
+ const group = lists.conversations.find((g2) => g2.sessionId === rootId) ?? {
238371
+ sessionId: rootId,
238372
+ title: session?.title || UNTITLED_CONVERSATION,
238373
+ projectId: session?.projectId ?? null,
238374
+ createdAt: session?.createdAt ?? null,
238375
+ amount: ZERO_USAGE_AMOUNT,
238376
+ flags: { inProgress: false, usagePending: false, costUnknownRuns: 0 },
238377
+ rows: []
238378
+ };
238379
+ return {
238380
+ companyId: input.companyId,
238381
+ sessionId,
238382
+ window: input.resolved.window,
238383
+ generatedAt: input.now.toISOString(),
238384
+ group,
238385
+ duration: deriveTreeDuration(lists.treeViews.get(rootId) ?? [], input.now)
238386
+ };
238387
+ }
238388
+ var UNKNOWN_AGENT_NAME, UNREGISTERED_RUNTIME_NAME, UNSPECIFIED_PROJECT_NAME, UNLINKED_ROW_TITLE, UNTITLED_CONVERSATION, BUCKET_KEYS, AmountAcc, FlagAcc, Cell, collator, ModelCells, ZERO_DURATION;
237582
238389
  var init_statistics = __esm({
237583
238390
  "../server/src/domains/usage/statistics.ts"() {
237584
238391
  "use strict";
@@ -237586,6 +238393,7 @@ var init_statistics = __esm({
237586
238393
  init_uncategorized_project();
237587
238394
  init_model_pricing();
237588
238395
  init_accounting();
238396
+ init_statistics_model();
237589
238397
  UNKNOWN_AGENT_NAME = "\u672A\u77E5 Agent";
237590
238398
  UNREGISTERED_RUNTIME_NAME = "\u672A\u767B\u8BB0\u8FD0\u884C\u65F6";
237591
238399
  UNSPECIFIED_PROJECT_NAME = UNCATEGORIZED_PROJECT_NAME;
@@ -237704,6 +238512,16 @@ var init_statistics = __esm({
237704
238512
  return [...this.cells].map(([model, c]) => ({ model, runCount: c.runCount, amount: c.amount.toAmount(), flags: c.flags.toFlags() })).filter((r) => r.runCount > 0 || r.amount.tokens.total > 0 || r.amount.costUsdMicros > 0 || r.amount.costKnown !== "full").sort((a, b2) => b2.amount.tokens.total - a.amount.tokens.total || b2.runCount - a.runCount || (a.model === null ? 1 : b2.model === null ? -1 : collator.compare(a.model, b2.model)));
237705
238513
  }
237706
238514
  };
238515
+ ZERO_DURATION = {
238516
+ firstStartedAt: null,
238517
+ lastEndedAt: null,
238518
+ inProgress: false,
238519
+ wallClockMs: null,
238520
+ runtimeMs: 0,
238521
+ activeMs: 0,
238522
+ countedRuns: 0,
238523
+ unstartedRuns: 0
238524
+ };
237707
238525
  }
237708
238526
  });
237709
238527
 
@@ -237959,6 +238777,22 @@ function createUsageReadService(deps) {
237959
238777
  logRequest({ companyId, op: "breakdown", range: r.query.range, dimension, runs: r.runs.length, companyScoped: r.target.companyScoped, startedMs });
237960
238778
  return body2;
237961
238779
  },
238780
+ async conversationUsage(companyId, sessionId, raw) {
238781
+ const startedMs = Date.now();
238782
+ const loads = requestLoads(companyId);
238783
+ const r = await readLedger(companyId, { ...raw, range: raw.range ?? "all" }, loads);
238784
+ const relations = await loadRelations(companyId, loads);
238785
+ const body2 = buildChatSessionUsage(sessionId, {
238786
+ companyId,
238787
+ resolved: r.resolved,
238788
+ runs: r.runs,
238789
+ coverage: r.coverage,
238790
+ now: r.now,
238791
+ relations
238792
+ });
238793
+ logRequest({ companyId, op: "conversation", range: r.query.range, runs: r.runs.length, companyScoped: r.target.companyScoped, startedMs });
238794
+ return body2;
238795
+ },
237962
238796
  async runtimeLedger(companyId, runtimeInstanceId, raw) {
237963
238797
  const startedMs = Date.now();
237964
238798
  const loads = requestLoads(companyId);
@@ -238936,6 +239770,7 @@ function collabDomain(opts) {
238936
239770
  backfillInboxUnread(waitingItems, markers);
238937
239771
  }
238938
239772
  const items = mergeInbox(kernelItems, [...chatItems, ...waitingItems]);
239773
+ if (myReadMarkers) applyReadAllWatermark(items, markers);
238939
239774
  return { status: 200, body: { items: myReadMarkers ? withEscalationAttention(items, req.auth.companyId ?? opts.defaultCompanyId ?? "", markers, Date.now(), opts.escalationReminders) : items } };
238940
239775
  });
238941
239776
  router.post("/api/inbox/escalations/:id/acknowledge", async (req) => {
@@ -239205,6 +240040,28 @@ function collabDomain(opts) {
239205
240040
  await marks.markRead(me, scope2, (/* @__PURE__ */ new Date()).toISOString());
239206
240041
  return { status: 200, body: { ok: true } };
239207
240042
  });
240043
+ router.post("/api/inbox/read-all", async (req) => {
240044
+ const me = req.auth.actor;
240045
+ if (!isHumanActor(me)) {
240046
+ return { status: 403, body: { error: { code: "human_only", message: "\u53EA\u6709\u4EBA\u7C7B\u8EAB\u4EFD\u53EF\u4EE5\u6807\u8BB0\u5DF2\u8BFB" } } };
240047
+ }
240048
+ const marks = opts.resolveReadMarkers ? await opts.resolveReadMarkers(req.auth.companyId).catch(() => void 0) : opts.readMarkers;
240049
+ let ledger;
240050
+ if (opts.resolveNotificationLedger) {
240051
+ try {
240052
+ ledger = await opts.resolveNotificationLedger(req.auth.companyId);
240053
+ } catch (err) {
240054
+ console.warn(`[notifications] \u65E0\u53EF\u4FE1\u516C\u53F8\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u6807\u5DF2\u8BFB\uFF08\u4E0D\u56DE\u843D\u9ED8\u8BA4\u516C\u53F8\uFF09: ${String(err)}`);
240055
+ }
240056
+ }
240057
+ if (!marks && !ledger) {
240058
+ return { status: 503, body: { error: { code: "unavailable", message: "\u5DF2\u8BFB\u6C34\u4F4D\u4E0E\u901A\u77E5\u53F0\u8D26\u90FD\u4E0D\u53EF\u7528" } } };
240059
+ }
240060
+ const readAt = (/* @__PURE__ */ new Date()).toISOString();
240061
+ const notifications = ledger ? await ledger.markAllRead(me, readAt) : 0;
240062
+ if (marks) await marks.markRead(me, INBOX_READ_ALL_SCOPE, readAt);
240063
+ return { status: 200, body: { ok: true, readAt, notifications } };
240064
+ });
239208
240065
  router.get("/api/audit", async (req) => {
239209
240066
  const { engineStore, oplog, kernel } = await resolveCtx(req.auth.companyId);
239210
240067
  const q2 = req.query;
@@ -241643,9 +242500,17 @@ function bucketLabelOf(slice, index2) {
241643
242500
  function runtimeUsageFromLedger(runtimeInstanceId, slice) {
241644
242501
  const rows = /* @__PURE__ */ new Map();
241645
242502
  const runById = new Map(slice.runs.map((r) => [r.id, r]));
242503
+ const modelByRun = /* @__PURE__ */ new Map();
242504
+ const modelOf = (run) => {
242505
+ const hit = modelByRun.get(run.id);
242506
+ if (hit !== void 0) return hit;
242507
+ const m2 = ledgerRunModel(run);
242508
+ modelByRun.set(run.id, m2);
242509
+ return m2;
242510
+ };
241646
242511
  const rowOf = (bucketIndex2, run) => {
241647
242512
  const bucket2 = bucketLabelOf(slice, bucketIndex2);
241648
- const model = resolvedModel(run.effectiveModel);
242513
+ const model = modelOf(run);
241649
242514
  const key2 = `${bucket2}\0${run.actorId}\0${model ?? ""}`;
241650
242515
  let row = rows.get(key2);
241651
242516
  if (!row) {
@@ -242641,11 +243506,11 @@ var init_roles = __esm({
242641
243506
  });
242642
243507
 
242643
243508
  // ../server/src/node-store.ts
242644
- var fs25, MemoryNodeStore, FileNodeStore;
243509
+ var fs26, MemoryNodeStore, FileNodeStore;
242645
243510
  var init_node_store = __esm({
242646
243511
  "../server/src/node-store.ts"() {
242647
243512
  "use strict";
242648
- fs25 = __toESM(require("node:fs"), 1);
243513
+ fs26 = __toESM(require("node:fs"), 1);
242649
243514
  MemoryNodeStore = class {
242650
243515
  nodes = /* @__PURE__ */ new Map();
242651
243516
  runtimes = /* @__PURE__ */ new Map();
@@ -242770,16 +243635,16 @@ var init_node_store = __esm({
242770
243635
  static async open(file) {
242771
243636
  const s2 = new _FileNodeStore(file);
242772
243637
  try {
242773
- const snap = JSON.parse(fs25.readFileSync(file, "utf8"));
243638
+ const snap = JSON.parse(fs26.readFileSync(file, "utf8"));
242774
243639
  for (const n of snap.nodes ?? []) s2.nodes.set(n.id, n);
242775
243640
  for (const r of snap.runtimes ?? []) s2.runtimes.set(r.id, r);
242776
243641
  } catch {
242777
- fs25.writeFileSync(file, JSON.stringify({ nodes: [], runtimes: [] }, null, 2));
243642
+ fs26.writeFileSync(file, JSON.stringify({ nodes: [], runtimes: [] }, null, 2));
242778
243643
  }
242779
243644
  return s2;
242780
243645
  }
242781
243646
  flush() {
242782
- fs25.writeFileSync(this.file, JSON.stringify({
243647
+ fs26.writeFileSync(this.file, JSON.stringify({
242783
243648
  nodes: [...this.nodes.values()],
242784
243649
  runtimes: [...this.runtimes.values()]
242785
243650
  }, null, 2));
@@ -243570,9 +244435,9 @@ function prepareReadonlyGitBundles(input) {
243570
244435
  if (repos.length === 0) return null;
243571
244436
  const targets = collectTargets(repos, input.pinnedRevisions);
243572
244437
  if (targets.length === 0) return null;
243573
- const bundleRoot = input.bundleRoot ?? os7.tmpdir();
243574
- fs26.mkdirSync(bundleRoot, { recursive: true });
243575
- const root = fs26.mkdtempSync(path24.join(bundleRoot, "oasis-git-bundles-"));
244438
+ const bundleRoot = input.bundleRoot ?? os8.tmpdir();
244439
+ fs27.mkdirSync(bundleRoot, { recursive: true });
244440
+ const root = fs27.mkdtempSync(path24.join(bundleRoot, "oasis-git-bundles-"));
243576
244441
  const files = {};
243577
244442
  const binaryFiles = {};
243578
244443
  const metas = [];
@@ -243580,7 +244445,7 @@ function prepareReadonlyGitBundles(input) {
243580
244445
  for (const target of targets) {
243581
244446
  const generated = createBundle(root, target, input);
243582
244447
  files[generated.metaPath] = JSON.stringify(generated.meta, null, 2) + "\n";
243583
- binaryFiles[generated.bundlePath] = fs26.readFileSync(generated.absoluteBundlePath).toString("base64");
244448
+ binaryFiles[generated.bundlePath] = fs27.readFileSync(generated.absoluteBundlePath).toString("base64");
243584
244449
  metas.push(generated.meta);
243585
244450
  }
243586
244451
  files["inputs/git-bundles/MANIFEST.json"] = JSON.stringify({
@@ -243601,10 +244466,10 @@ function prepareReadonlyGitBundles(input) {
243601
244466
  return {
243602
244467
  files,
243603
244468
  binaryFiles,
243604
- cleanup: () => fs26.rmSync(root, { recursive: true, force: true })
244469
+ cleanup: () => fs27.rmSync(root, { recursive: true, force: true })
243605
244470
  };
243606
244471
  } catch (err) {
243607
- fs26.rmSync(root, { recursive: true, force: true });
244472
+ fs27.rmSync(root, { recursive: true, force: true });
243608
244473
  throw err;
243609
244474
  }
243610
244475
  }
@@ -243663,7 +244528,7 @@ function createBundle(root, target, input) {
243663
244528
  if (!HEX40.test(commit)) throw new GitReadonlyBundleError("COMMIT_NOT_FOUND", `commit \u683C\u5F0F\u4E0D\u662F 40 \u4F4D sha: ${commit.slice(0, 48)}`);
243664
244529
  const safeRepo = repo.id.replace(/[^a-zA-Z0-9_-]+/g, "_");
243665
244530
  const work = path24.join(root, safeRepo);
243666
- fs26.mkdirSync(work, { recursive: true });
244531
+ fs27.mkdirSync(work, { recursive: true });
243667
244532
  const source = ensureSourceRepo(work, repo, commit, input.env);
243668
244533
  const tree = git2(source, ["rev-parse", `${commit}^{tree}`], input.env, "COMMIT_NOT_FOUND").trim();
243669
244534
  if (target.expectedTree && target.expectedTree.toLowerCase() !== tree.toLowerCase()) {
@@ -243690,8 +244555,8 @@ function createBundle(root, target, input) {
243690
244555
  } catch {
243691
244556
  }
243692
244557
  }
243693
- const bytes2 = fs26.statSync(absBundle).size;
243694
- const digest = (0, import_node_crypto62.createHash)("sha256").update(fs26.readFileSync(absBundle)).digest("hex");
244558
+ const bytes2 = fs27.statSync(absBundle).size;
244559
+ const digest = (0, import_node_crypto62.createHash)("sha256").update(fs27.readFileSync(absBundle)).digest("hex");
243695
244560
  const relBase = `inputs/git-bundles/${safeRepo}-${commit.slice(0, 12)}`;
243696
244561
  return {
243697
244562
  absoluteBundlePath: absBundle,
@@ -243715,7 +244580,7 @@ function createBundle(root, target, input) {
243715
244580
  };
243716
244581
  }
243717
244582
  function verifyBundleReadable(dir, bundlePath, _bundleRef, commit, expectedTree, env) {
243718
- fs26.rmSync(dir, { recursive: true, force: true });
244583
+ fs27.rmSync(dir, { recursive: true, force: true });
243719
244584
  try {
243720
244585
  (0, import_node_child_process16.execFileSync)("git", ["clone", bundlePath, dir], { stdio: "ignore", ...env ? { env } : {} });
243721
244586
  git2(dir, ["cat-file", "-e", `${commit}^{commit}`], env, "COMMIT_NOT_FOUND");
@@ -243757,8 +244622,8 @@ function ensureSourceRepo(work, repo, commit, env) {
243757
244622
  }
243758
244623
  }
243759
244624
  function gitInitBare(dir, env) {
243760
- if (fs26.existsSync(path24.join(dir, "objects"))) return;
243761
- fs26.mkdirSync(dir, { recursive: true });
244625
+ if (fs27.existsSync(path24.join(dir, "objects"))) return;
244626
+ fs27.mkdirSync(dir, { recursive: true });
243762
244627
  (0, import_node_child_process16.execFileSync)("git", ["init", "--bare", dir], { stdio: "ignore", ...env ? { env } : {} });
243763
244628
  }
243764
244629
  function git2(cwd, args, env, fallback, timeout) {
@@ -243787,14 +244652,14 @@ function classifyGitError(fallback, err, where) {
243787
244652
  function oneLine(msg) {
243788
244653
  return msg.split("\n").map((s2) => s2.trim()).filter(Boolean).slice(0, 3).join(" ");
243789
244654
  }
243790
- var import_node_child_process16, import_node_crypto62, fs26, os7, path24, GitReadonlyBundleError, HEX40, FETCH_TIMEOUT_MS2;
244655
+ var import_node_child_process16, import_node_crypto62, fs27, os8, path24, GitReadonlyBundleError, HEX40, FETCH_TIMEOUT_MS2;
243791
244656
  var init_readonly_bundle = __esm({
243792
244657
  "../server/src/git/readonly-bundle.ts"() {
243793
244658
  "use strict";
243794
244659
  import_node_child_process16 = require("node:child_process");
243795
244660
  import_node_crypto62 = require("node:crypto");
243796
- fs26 = __toESM(require("node:fs"), 1);
243797
- os7 = __toESM(require("node:os"), 1);
244661
+ fs27 = __toESM(require("node:fs"), 1);
244662
+ os8 = __toESM(require("node:os"), 1);
243798
244663
  path24 = __toESM(require("node:path"), 1);
243799
244664
  init_remote_util();
243800
244665
  GitReadonlyBundleError = class extends Error {
@@ -244601,6 +245466,7 @@ function parseFilters(q2) {
244601
245466
  const actorId = q2.get("actorId");
244602
245467
  const artifactId = q2.get("artifactId");
244603
245468
  const projectId2 = q2.get("projectId");
245469
+ const chatSessionId = q2.get("chatSessionId");
244604
245470
  const runtimeKind = q2.get("runtimeKind");
244605
245471
  const status = q2.get("status");
244606
245472
  const rootRunId = q2.get("rootRunId");
@@ -244611,6 +245477,7 @@ function parseFilters(q2) {
244611
245477
  if (actorId) f2.actorId = actorId;
244612
245478
  if (artifactId) f2.artifactId = artifactId;
244613
245479
  if (projectId2) f2.projectId = projectId2;
245480
+ if (chatSessionId) f2.chatSessionId = chatSessionId;
244614
245481
  if (runtimeKind) f2.runtimeKind = runtimeKind;
244615
245482
  if (status) f2.status = status;
244616
245483
  if (rootRunId) f2.rootRunId = rootRunId;
@@ -246517,6 +247384,19 @@ function createChatSessionsDomain(opts) {
246517
247384
  const binding = await bindingFor(companyId, aiActorId).catch(() => null);
246518
247385
  return binding?.nodeId ?? null;
246519
247386
  }
247387
+ async function agentDefaultRuntime(companyId, aiActorId) {
247388
+ const binding = await bindingFor(companyId, aiActorId).catch(() => null);
247389
+ let model = null;
247390
+ try {
247391
+ model = opts.resolveActorModel ? await opts.resolveActorModel(companyId, aiActorId) : (await registry2.latestConfig(aiActorId))?.model ?? null;
247392
+ } catch {
247393
+ model = null;
247394
+ }
247395
+ return {
247396
+ binding: binding ? { nodeId: binding.nodeId, runtimeKind: binding.runtimeKind } : null,
247397
+ model: normalizeModel(model)
247398
+ };
247399
+ }
246520
247400
  function normalizeModel(raw) {
246521
247401
  return typeof raw === "string" && raw.trim() ? raw.trim() : null;
246522
247402
  }
@@ -246581,9 +247461,12 @@ function createChatSessionsDomain(opts) {
246581
247461
  }
246582
247462
  return manager ? manager : null;
246583
247463
  }
246584
- function ensureSessionReadable(session, actor, kernel) {
247464
+ function ownsSessionOrOperator(session, auth) {
247465
+ return session.humanActorId === auth.actor || isOperatorRequest(auth);
247466
+ }
247467
+ function ensureSessionReadable(session, auth, kernel) {
246585
247468
  if (!session) throw new ApiError(404, "NOT_FOUND", "session not found");
246586
- if (session.humanActorId === actor) return session;
247469
+ if (ownsSessionOrOperator(session, auth)) return session;
246587
247470
  if (session.workspace) {
246588
247471
  const coordinator = coordinatorOfWorkspace(kernel, session.workspace);
246589
247472
  if (coordinator && session.aiActorId === coordinator) return session;
@@ -246681,7 +247564,16 @@ function createChatSessionsDomain(opts) {
246681
247564
  if (isDeletedActor(aiActor)) {
246682
247565
  throw new ApiError(409, ACTOR_DELETED_ERROR_CODE, ACTOR_DELETED_CHAT_MESSAGE);
246683
247566
  }
246684
- const runtimeId = await currentRuntimeId(req.auth.companyId, body2.aiActorId) ?? "unknown";
247567
+ const agentDefault = await agentDefaultRuntime(req.auth.companyId, body2.aiActorId);
247568
+ const initial = chatSessionInitialRuntime({
247569
+ binding: agentDefault.binding,
247570
+ agentModel: agentDefault.model,
247571
+ requested: {
247572
+ override: normalizeOverride(body2.overrideNodeId) && normalizeOverride(body2.overrideRuntimeKind) ? { nodeId: normalizeOverride(body2.overrideNodeId), runtimeKind: normalizeOverride(body2.overrideRuntimeKind) } : null,
247573
+ ...Object.prototype.hasOwnProperty.call(body2, "model") ? { model: normalizeModel(body2.model) } : {}
247574
+ }
247575
+ });
247576
+ const runtimeId = agentDefault.binding?.nodeId ?? "unknown";
246685
247577
  const now = (/* @__PURE__ */ new Date()).toISOString();
246686
247578
  const session = {
246687
247579
  id: (0, import_node_crypto65.randomUUID)(),
@@ -246698,12 +247590,12 @@ function createChatSessionsDomain(opts) {
246698
247590
  // 两个入口页面行为一致。区别于 linkWorkOrder(出处关联,建单脑暴也有、不构成绑定)。
246699
247591
  workspace: sessionWorkspaceToStore(body2.workspace),
246700
247592
  projectId: sessionProjectIdToStore(body2.projectId),
246701
- // 会话级模型覆盖(口径 A):建会话时就带上选择;空串/空白归 null = 沿用旧解析,不写员工全局配置。
246702
- model: normalizeModel(body2.model),
246703
- /* 会话级 runtime 覆盖(ADR 0604):欢迎态在 chip 带上选好的节点/runtime 随 create 一起写进去
246704
- (与项目、模型同一刻、同一条路),不做「发完再 PATCH」。两列都为 null = 跟随员工 binding。 */
246705
- overrideNodeId: normalizeOverride(body2.overrideNodeId),
246706
- overrideRuntimeKind: normalizeOverride(body2.overrideRuntimeKind),
247593
+ /* 三列一起由 `chatSessionInitialRuntime` 给(ADR 0613):用户在欢迎态挑过就用他挑的,
247594
+ 没挑就抄员工此刻的默认运行配置。**员工没绑定 = 三列留空**,逐字回到 ADR 0604 的旧语义
247595
+ (跟随员工 binding),绑上之后那条路照旧管用。 */
247596
+ model: initial.model,
247597
+ overrideNodeId: initial.overrideNodeId,
247598
+ overrideRuntimeKind: initial.overrideRuntimeKind,
246707
247599
  // ADR-0058:分析会话指向被分析 run;非空即"这是分析会话"(不进主聊天列表、进 trace 分析视图)。
246708
247600
  analyzedRunId: body2.analyzedRunId ?? null
246709
247601
  };
@@ -246748,7 +247640,7 @@ function createChatSessionsDomain(opts) {
246748
247640
  router.get("/api/chat-sessions/:id", async (req) => {
246749
247641
  const store2 = await storeFor(req);
246750
247642
  const [raw, kernel] = await Promise.all([store2.getSession(req.params.id), kernelFor(req)]);
246751
- const session = ensureSessionReadable(raw, req.auth.actor, kernel);
247643
+ const session = ensureSessionReadable(raw, req.auth, kernel);
246752
247644
  const [window3, curRuntimeId] = await Promise.all([
246753
247645
  listRecentTurns(store2, session.id, parseTurns(req.query?.get("turns"))),
246754
247646
  currentRuntimeId(req.auth.companyId, session.aiActorId)
@@ -246803,6 +247695,19 @@ function createChatSessionsDomain(opts) {
246803
247695
  });
246804
247696
  return { status: 200, body: await store2.getSession(req.params.id) };
246805
247697
  });
247698
+ router.post("/api/chat-sessions/:id/runtime/reset-to-agent-default", async (req) => {
247699
+ const store2 = await storeFor(req);
247700
+ const session = await store2.getSession(req.params.id);
247701
+ if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
247702
+ const agentDefault = await agentDefaultRuntime(req.auth.companyId, session.aiActorId);
247703
+ const initial = chatSessionInitialRuntime({ binding: agentDefault.binding, agentModel: agentDefault.model });
247704
+ await store2.updateSession(req.params.id, {
247705
+ overrideNodeId: initial.overrideNodeId,
247706
+ overrideRuntimeKind: initial.overrideRuntimeKind,
247707
+ model: initial.model
247708
+ });
247709
+ return { status: 200, body: await store2.getSession(req.params.id) };
247710
+ });
246806
247711
  router.delete("/api/chat-sessions/:id", async (req) => {
246807
247712
  const store2 = await storeFor(req);
246808
247713
  const session = await store2.getSession(req.params.id);
@@ -246889,7 +247794,7 @@ function createChatSessionsDomain(opts) {
246889
247794
  if (!items) throw new ApiError(404, "NOT_FOUND", "\u672C\u5B9E\u4F8B\u672A\u63A5 chat_items \u5FEB\u7167\u8BFB\u53E3");
246890
247795
  const store2 = await storeFor(req);
246891
247796
  const [session, kernel] = await Promise.all([store2.getSession(req.params.id), kernelFor(req)]);
246892
- ensureSessionReadable(session, req.auth.actor, kernel);
247797
+ ensureSessionReadable(session, req.auth, kernel);
246893
247798
  const chatSessionId = req.params.id;
246894
247799
  const sinceVersion = Math.max(0, Number(req.query?.get("sinceVersion") ?? "0") || 0);
246895
247800
  const beforeOrdRaw = Number(req.query?.get("beforeOrd") ?? "0") || 0;
@@ -246964,6 +247869,14 @@ function createChatSessionsDomain(opts) {
246964
247869
  snapshotEntries.splice(at + 1, 0, entry);
246965
247870
  }
246966
247871
  }
247872
+ const outputCards = await sessionOutputCards(snapshotEntries, store2, chatSessionId).catch((e) => {
247873
+ console.warn(`[chat] \u4EA7\u51FA\u5361\u73B0\u62FC\u5931\u8D25\uFF08session=${chatSessionId}\uFF09\uFF0C\u8FD9\u6B21\u5FEB\u7167\u4E0D\u5E26\u5361: ${String(e)}`);
247874
+ return [];
247875
+ });
247876
+ for (const { after, entry } of outputCards) {
247877
+ const at = snapshotEntries.findIndex((e) => e.itemId === after);
247878
+ snapshotEntries.splice(at + 1, 0, entry);
247879
+ }
246967
247880
  const oldestOrd = snapshotEntries.find((e) => e.ord !== null)?.ord ?? null;
246968
247881
  const body2 = {
246969
247882
  chatSessionId,
@@ -247060,7 +247973,7 @@ function createChatSessionsDomain(opts) {
247060
247973
  router.get("/api/chat-sessions/:id/delegations", async (req) => {
247061
247974
  const store2 = await storeFor(req);
247062
247975
  const session = await store2.getSession(req.params.id);
247063
- if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
247976
+ if (!session || !ownsSessionOrOperator(session, req.auth)) throw new ApiError(404, "NOT_FOUND", "session not found");
247064
247977
  const wiring = opts.delegations;
247065
247978
  if (!wiring || !wiring.enabled()) return { status: 200, body: { items: [] } };
247066
247979
  const companyId = session.companyId ?? req.auth.companyId ?? void 0;
@@ -247081,6 +247994,9 @@ function createChatSessionsDomain(opts) {
247081
247994
  ...expert?.avatar ? { expertAvatar: expert.avatar } : {},
247082
247995
  state: record9.state,
247083
247996
  ...record9.failureKind ? { failureKind: record9.failureKind } : {},
247997
+ /* 给人看的那一档由服务端算(Bug 0221):委派卡、徽章、子会话面板三处渲染同一个值。 */
247998
+ ending: delegationEndingOfRecord(record9, delegationArtifactsView(record9)),
247999
+ settledSeq: record9.settledSeq,
247084
248000
  ...record9.label ? { label: record9.label } : {},
247085
248001
  ...record9.taskDigest ? { taskDigest: record9.taskDigest } : {},
247086
248002
  parentMessageId: record9.parentMessageId,
@@ -247126,7 +248042,7 @@ function createChatSessionsDomain(opts) {
247126
248042
  router.get("/api/chat-sessions/:id/work-orders", async (req) => {
247127
248043
  const store2 = await storeFor(req);
247128
248044
  const session = await store2.getSession(req.params.id);
247129
- if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
248045
+ if (!session || !ownsSessionOrOperator(session, req.auth)) throw new ApiError(404, "NOT_FOUND", "session not found");
247130
248046
  const workOrderIds = await store2.listWorkOrdersForSession(req.params.id);
247131
248047
  const wkKernel = await kernelFor(req);
247132
248048
  if (!wkKernel || workOrderIds.length === 0) return { status: 200, body: { items: [] } };
@@ -247149,6 +248065,29 @@ function createChatSessionsDomain(opts) {
247149
248065
  });
247150
248066
  return { status: 200, body: { items } };
247151
248067
  });
248068
+ router.get("/api/chat-sessions/:id/usage", async (req) => {
248069
+ const store2 = await storeFor(req);
248070
+ const session = await store2.getSession(req.params.id);
248071
+ if (!session || !ownsSessionOrOperator(session, req.auth)) throw new ApiError(404, "NOT_FOUND", "session not found");
248072
+ const companyId = req.auth.companyId ?? session.companyId ?? opts.defaultCompanyId;
248073
+ if (!opts.usageRead || !companyId) {
248074
+ return { status: 503, body: { error: { code: "usage_ledger_unavailable", message: "\u4F7F\u7528\u4E0E\u7EDF\u8BA1\u6570\u636E\u6E90\u672A\u63A5\u5165" } } };
248075
+ }
248076
+ try {
248077
+ return {
248078
+ status: 200,
248079
+ body: await opts.usageRead.conversationUsage(companyId, session.id, {
248080
+ range: req.query.get("range") ?? void 0,
248081
+ asOf: req.query.get("asOf") ?? void 0,
248082
+ timeZone: req.query.get("timeZone") ?? void 0
248083
+ })
248084
+ };
248085
+ } catch (err) {
248086
+ const mapped = usageErrorResponse(err);
248087
+ if (mapped) return mapped;
248088
+ throw err;
248089
+ }
248090
+ });
247152
248091
  router.get("/api/chat-broadcasts/pending", async (req) => {
247153
248092
  if (!opts.broadcastOps) throw new ApiError(404, "NOT_FOUND", "\u672C\u5B9E\u4F8B\u672A\u542F\u7528\u5DE5\u5355\u7EC8\u6001\u64AD\u62A5");
247154
248093
  const store2 = await storeFor(req);
@@ -247463,6 +248402,44 @@ function createChatSessionsDomain(opts) {
247463
248402
  });
247464
248403
  };
247465
248404
  }
248405
+ async function sessionOutputCards(entries, store, chatSessionId) {
248406
+ const lastEntryOf = /* @__PURE__ */ new Map();
248407
+ for (const e of entries) {
248408
+ if (e.role === "assistant" && e.messageId) lastEntryOf.set(e.messageId, e);
248409
+ }
248410
+ if (lastEntryOf.size === 0) return [];
248411
+ const ids2 = [...lastEntryOf.keys()];
248412
+ const rows = store.listMessageOutputParts ? await store.listMessageOutputParts(chatSessionId, ids2) : (await store.listMessages(chatSessionId)).filter((m2) => m2.role === "assistant" && lastEntryOf.has(m2.id)).map((m2) => ({ messageId: m2.id, outputs: Array.isArray(m2.parts) ? m2.parts.filter((p2) => p2?.type === "outputs") : [] }));
248413
+ const out = [];
248414
+ for (const { messageId, outputs } of rows) {
248415
+ const anchor = lastEntryOf.get(messageId);
248416
+ const files = sessionOutputFilesOfParts(outputs);
248417
+ if (!anchor || files.length === 0) continue;
248418
+ const anchorPayload = anchor.payload && typeof anchor.payload === "object" && !Array.isArray(anchor.payload) ? anchor.payload : {};
248419
+ out.push({
248420
+ after: anchor.itemId,
248421
+ entry: {
248422
+ itemId: sessionOutputsItemId(messageId),
248423
+ turnId: anchor.turnId,
248424
+ itemType: "control",
248425
+ role: "assistant",
248426
+ status: "completed",
248427
+ itemVersion: anchor.itemVersion,
248428
+ seq: anchor.seq,
248429
+ ord: anchor.ord,
248430
+ messageId,
248431
+ updatedAt: anchor.updatedAt,
248432
+ payload: {
248433
+ code: SESSION_OUTPUTS_ITEM_CODE,
248434
+ files: files.map((f2) => ({ path: f2.path, name: f2.name })),
248435
+ ...anchorPayload.createdAt !== void 0 ? { createdAt: anchorPayload.createdAt } : {},
248436
+ ...anchorPayload.completedAt !== void 0 ? { completedAt: anchorPayload.completedAt } : {}
248437
+ }
248438
+ }
248439
+ });
248440
+ }
248441
+ return out;
248442
+ }
247466
248443
  async function submissionCardProjection(entries, deps) {
247467
248444
  const out = { add: [], resolved: [] };
247468
248445
  const turns = deps.turns;
@@ -247605,6 +248582,8 @@ var init_chat_sessions2 = __esm({
247605
248582
  init_persistent_files();
247606
248583
  init_files_view();
247607
248584
  init_router();
248585
+ init_operator();
248586
+ init_usage_read();
247608
248587
  init_collab();
247609
248588
  init_gap_thread();
247610
248589
  init_escalation_opening();
@@ -247619,6 +248598,7 @@ var init_chat_sessions2 = __esm({
247619
248598
  init_company_data_store_router();
247620
248599
  init_workorder_terminal();
247621
248600
  init_session_workdir();
248601
+ init_session_runtime_target();
247622
248602
  init_uncategorized_project();
247623
248603
  init_chat_submission();
247624
248604
  init_project_bundle();
@@ -251454,14 +252434,41 @@ function daemonMetaOf(hub, nodeId) {
251454
252434
  if (typeof connectedDaemons !== "function") return void 0;
251455
252435
  return connectedDaemons.call(hub).find((candidate) => candidate.daemonId === nodeId)?.meta;
251456
252436
  }
251457
- function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs, confirm) {
252437
+ function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs, sessionReadyWaitMs, confirm) {
251458
252438
  const key2 = input.requireConsumptionReceipt ? input.clientMessageKey : void 0;
251459
- if (!key2 || !confirm) return sendAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs);
251460
- return sendAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs).then((result) => ({ ...result, receiptProbe: { nodeId, dispatchId, confirm: () => confirm(key2) } }));
252439
+ if (!key2 || !confirm) return sendAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs, sessionReadyWaitMs);
252440
+ return sendAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs, sessionReadyWaitMs).then((result) => ({ ...result, receiptProbe: { nodeId, dispatchId, confirm: () => confirm(key2) } }));
252441
+ }
252442
+ async function waitSessionReady(hub, dispatchId, entry, timeoutMs) {
252443
+ const deadline = Date.now() + timeoutMs;
252444
+ const ready = () => hub.hasSession?.(dispatchId) === true && entry.deliveryState === "started";
252445
+ while (!entry.exited && !ready()) {
252446
+ const left = deadline - Date.now();
252447
+ if (left <= 0) return;
252448
+ await new Promise((resolve11) => {
252449
+ let settled = false;
252450
+ const finish = () => {
252451
+ if (settled) return;
252452
+ settled = true;
252453
+ const i = entry.readyWaiters?.indexOf(finish) ?? -1;
252454
+ if (i >= 0) entry.readyWaiters.splice(i, 1);
252455
+ resolve11();
252456
+ };
252457
+ (entry.readyWaiters ??= []).push(finish);
252458
+ if (hub.hasSession?.(dispatchId) !== true) void hub.waitForSession(dispatchId, left).then(finish, finish);
252459
+ else {
252460
+ const t = setTimeout(finish, left);
252461
+ t.unref?.();
252462
+ }
252463
+ });
252464
+ }
251461
252465
  }
251462
- function sendAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
252466
+ function sendAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs, sessionReadyWaitMs) {
251463
252467
  const unknownIfInterrupting = input.interrupt ? { interrupt: "unknown" } : {};
251464
252468
  if (entry.exited) return Promise.resolve({ accepted: false, reason: "session-closing", ...unknownIfInterrupting });
252469
+ if (input.requireConsumptionReceipt && sessionReadyWaitMs > 0 && (hub.hasSession?.(dispatchId) === false || entry.deliveryState !== "started") && typeof hub.waitForSession === "function" && daemonMetaOf(hub, nodeId)?.supportsInputReceiptQuery === true) {
252470
+ return waitSessionReady(hub, dispatchId, entry, sessionReadyWaitMs).then(() => sendAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs, 0));
252471
+ }
251465
252472
  if (input.requireConsumptionReceipt && !hub.supportsInputConsumptionReceipts?.(dispatchId)) {
251466
252473
  return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
251467
252474
  }
@@ -251498,6 +252505,7 @@ var init_daemon_adapter = __esm({
251498
252505
  this.nodeLostGraceMs = opts.nodeLostGraceMs ?? graceFromEnv(12e4);
251499
252506
  this.ackTimeoutMs = opts.ackTimeoutMs ?? ackMsFromEnv(15e3);
251500
252507
  this.appendTimeoutMs = opts.appendTimeoutMs ?? 1e4;
252508
+ this.sessionReadyWaitMs = opts.sessionReadyWaitMs ?? 1e4;
251501
252509
  this.receiptQueryTimeoutMs = opts.receiptQueryTimeoutMs ?? 5e3;
251502
252510
  this.log = opts.log ?? ((m2) => console.warn(m2));
251503
252511
  this.health = opts.health;
@@ -251519,6 +252527,7 @@ var init_daemon_adapter = __esm({
251519
252527
  ackTimeoutMs;
251520
252528
  /** ADR-0093:append_input 回执时限;超时(老节点忽略帧)→ unsupported,上层降级 kill+resume。 */
251521
252529
  appendTimeoutMs;
252530
+ sessionReadyWaitMs;
251522
252531
  log;
251523
252532
  health;
251524
252533
  stashEnabled;
@@ -251671,6 +252680,7 @@ var init_daemon_adapter = __esm({
251671
252680
  clearTimeout(w2.timer);
251672
252681
  w2.resolve({ accepted: false, reason: w2.requireConsumptionReceipt ? "unconfirmed" : "session-closing" });
251673
252682
  }
252683
+ for (const w2 of entry.readyWaiters?.splice(0) ?? []) w2();
251674
252684
  if (info.reason === "server-unreachable") this.health?.recordUnreachable(entry.nodeId, info.reason);
251675
252685
  else this.health?.recordReachable(entry.nodeId);
251676
252686
  for (const cb of entry.exitCbs.splice(0)) cb(info);
@@ -251730,6 +252740,7 @@ var init_daemon_adapter = __esm({
251730
252740
  entry.ackTimer = void 0;
251731
252741
  }
251732
252742
  if (entry) entry.deliveryState = state;
252743
+ if (entry && state === "started") for (const w2 of entry.readyWaiters?.splice(0) ?? []) w2();
251733
252744
  }
251734
252745
  onNodeDown(daemonId) {
251735
252746
  if (this.reapTimers.has(daemonId)) return;
@@ -251938,6 +252949,7 @@ var init_daemon_adapter = __esm({
251938
252949
  }
251939
252950
  const hub = this.hub;
251940
252951
  const appendTimeoutMs = this.appendTimeoutMs;
252952
+ const sessionReadyWaitMs = this.sessionReadyWaitMs;
251941
252953
  const confirm = (key2) => this.confirmInputReceipt(nodeId, dispatchId, key2);
251942
252954
  return {
251943
252955
  id: dispatchId,
@@ -251971,7 +252983,7 @@ var init_daemon_adapter = __esm({
251971
252983
  } : {},
251972
252984
  // ADR-0093 对话追加:经 WS 把输入送到节点上的会话,等 append_result 回执;节点不认帧
251973
252985
  // (老节点)则超时 unsupported,上层降级 kill+resume。
251974
- appendInput: (input) => remoteAppendInput(hub, nodeId, dispatchId, entry, input, appendTimeoutMs, confirm)
252986
+ appendInput: (input) => remoteAppendInput(hub, nodeId, dispatchId, entry, input, appendTimeoutMs, sessionReadyWaitMs, confirm)
251975
252987
  };
251976
252988
  }
251977
252989
  recover(dispatchId, nodeId, runtimeKind, actorId) {
@@ -252029,6 +253041,7 @@ var init_daemon_adapter = __esm({
252029
253041
  };
252030
253042
  const hub = this.hub;
252031
253043
  const appendTimeoutMs = this.appendTimeoutMs;
253044
+ const sessionReadyWaitMs = this.sessionReadyWaitMs;
252032
253045
  const confirm = (key2) => this.confirmInputReceipt(nodeId, dispatchId, key2);
252033
253046
  return {
252034
253047
  id: dispatchId,
@@ -252068,7 +253081,7 @@ var init_daemon_adapter = __esm({
252068
253081
  }
252069
253082
  } : {},
252070
253083
  // ADR-0093 对话追加(重挂句柄同 spawn 句柄)。
252071
- appendInput: (input) => remoteAppendInput(hub, nodeId, dispatchId, entry, input, appendTimeoutMs, confirm)
253084
+ appendInput: (input) => remoteAppendInput(hub, nodeId, dispatchId, entry, input, appendTimeoutMs, sessionReadyWaitMs, confirm)
252072
253085
  };
252073
253086
  }
252074
253087
  };
@@ -252382,22 +253395,22 @@ var init_engine = __esm({
252382
253395
  });
252383
253396
 
252384
253397
  // ../server/src/mirror/fs-state.ts
252385
- var fs29, FsMirrorStateStore;
253398
+ var fs30, FsMirrorStateStore;
252386
253399
  var init_fs_state = __esm({
252387
253400
  "../server/src/mirror/fs-state.ts"() {
252388
253401
  "use strict";
252389
- fs29 = __toESM(require("node:fs"), 1);
253402
+ fs30 = __toESM(require("node:fs"), 1);
252390
253403
  FsMirrorStateStore = class {
252391
253404
  constructor(file) {
252392
253405
  this.file = file;
252393
- if (fs29.existsSync(file)) {
252394
- const obj2 = JSON.parse(fs29.readFileSync(file, "utf8"));
253406
+ if (fs30.existsSync(file)) {
253407
+ const obj2 = JSON.parse(fs30.readFileSync(file, "utf8"));
252395
253408
  for (const [k2, v2] of Object.entries(obj2)) this.map.set(k2, v2);
252396
253409
  }
252397
253410
  }
252398
253411
  map = /* @__PURE__ */ new Map();
252399
253412
  flush() {
252400
- fs29.writeFileSync(this.file, JSON.stringify(Object.fromEntries(this.map), null, 2));
253413
+ fs30.writeFileSync(this.file, JSON.stringify(Object.fromEntries(this.map), null, 2));
252401
253414
  }
252402
253415
  async get(artifactId) {
252403
253416
  return this.map.get(artifactId) ?? null;
@@ -258375,6 +259388,7 @@ var init_postgres_trace = __esm({
258375
259388
  if (filter.actorId) push2((n) => `r.actor_id = $${n}`, filter.actorId);
258376
259389
  if (filter.artifactId) push2((n) => `r.artifact_id = $${n}`, filter.artifactId);
258377
259390
  if (filter.projectId) push2((n) => `r.project_id = $${n}`, filter.projectId);
259391
+ if (filter.chatSessionId) push2((n) => `r.chat_session_id = $${n}`, filter.chatSessionId);
258378
259392
  if (filter.runtimeKind) push2((n) => `r.runtime_kind = $${n}`, filter.runtimeKind);
258379
259393
  if (filter.status) push2((n) => `r.status = $${n}`, filter.status);
258380
259394
  if (filter.rootRunId) push2((n) => `r.root_run_id = $${n}`, filter.rootRunId);
@@ -261775,6 +262789,34 @@ var init_postgres_chat_sessions = __esm({
261775
262789
  attachments: row.attachments ?? []
261776
262790
  }));
261777
262791
  }
262792
+ /**
262793
+ * `/items` 快照出产出卡的窄读(Bug 0217):按主键批量命中,在库里把 `parts` 展开、只挑 `type='outputs'`
262794
+ * 那几段聚回一个数组送出来——整轮过程时间线不出库。只看 assistant 行(outputs 段只在助手轮收尾写)。
262795
+ */
262796
+ async listMessageOutputParts(sessionId, messageIds) {
262797
+ if (messageIds.length === 0) return [];
262798
+ const r = await this.pool.query(
262799
+ `SELECT m.id, o.outputs
262800
+ FROM ${this.s}.chat_messages m
262801
+ JOIN ${this.s}.chat_sessions s ON s.id = m.session_id
262802
+ CROSS JOIN LATERAL (
262803
+ SELECT jsonb_agg(p ORDER BY i) AS outputs
262804
+ FROM jsonb_array_elements(CASE WHEN jsonb_typeof(m.parts) = 'array' THEN m.parts ELSE '[]'::jsonb END)
262805
+ WITH ORDINALITY AS e(p, i)
262806
+ WHERE p->>'type' = 'outputs'
262807
+ ) o
262808
+ WHERE m.id = ANY($1::text[])
262809
+ AND m.session_id = $2
262810
+ AND s.company_id = $3
262811
+ AND m.role = 'assistant'
262812
+ AND o.outputs IS NOT NULL`,
262813
+ [[...messageIds], sessionId, this.companyId]
262814
+ );
262815
+ return r.rows.map((row) => ({
262816
+ messageId: row.id,
262817
+ outputs: Array.isArray(row.outputs) ? row.outputs : []
262818
+ }));
262819
+ }
261778
262820
  /**
261779
262821
  * 末尾 N 轮视窗(详情读侧默认):先定位「从尾部数第 N 条 user 消息」的 seq 作为窗口起点,再取
261780
262822
  * seq >= 起点的全部消息。语义与内存版 windowMessagesByTurns 一致,但把裁剪下推到 SQL——长会话
@@ -263212,6 +264254,68 @@ var init_chat_items_ord_backfill = __esm({
263212
264254
  }
263213
264255
  });
263214
264256
 
264257
+ // ../storage/src/chat-session-runtime-pin-backfill.ts
264258
+ async function hasChatSessions(pool, schema) {
264259
+ const r = await pool.query(`SELECT to_regclass($1) IS NOT NULL AS ok`, [`${schema}.chat_sessions`]);
264260
+ return r.rows[0]?.ok === true;
264261
+ }
264262
+ async function pinChatSessionsToAgentDefault(pool, schema, opts) {
264263
+ const s2 = quoteIdent2(schema);
264264
+ const dryRun = opts.dryRun === true;
264265
+ const base = {
264266
+ schema,
264267
+ companyId: opts.companyId,
264268
+ scanned: 0,
264269
+ pinned: 0,
264270
+ leftUnbound: 0,
264271
+ unresolved: 0,
264272
+ dryRun
264273
+ };
264274
+ if (!await hasChatSessions(pool, schema)) return base;
264275
+ const pending = await pool.query(
264276
+ `SELECT ai_actor_id, count(*)::int AS n
264277
+ FROM ${s2}.chat_sessions
264278
+ WHERE company_id = $1
264279
+ AND (override_node_id IS NULL OR override_runtime_kind IS NULL)
264280
+ GROUP BY ai_actor_id`,
264281
+ [opts.companyId]
264282
+ );
264283
+ for (const row of pending.rows) {
264284
+ base.scanned += row.n;
264285
+ let binding;
264286
+ try {
264287
+ binding = await opts.resolveBinding(row.ai_actor_id);
264288
+ } catch {
264289
+ base.unresolved += row.n;
264290
+ continue;
264291
+ }
264292
+ if (!binding?.nodeId || !binding?.runtimeKind) {
264293
+ base.leftUnbound += row.n;
264294
+ continue;
264295
+ }
264296
+ if (dryRun) {
264297
+ base.pinned += row.n;
264298
+ continue;
264299
+ }
264300
+ const upd = await pool.query(
264301
+ `UPDATE ${s2}.chat_sessions
264302
+ SET override_node_id = $3, override_runtime_kind = $4
264303
+ WHERE company_id = $1
264304
+ AND ai_actor_id = $2
264305
+ AND (override_node_id IS NULL OR override_runtime_kind IS NULL)`,
264306
+ [opts.companyId, row.ai_actor_id, binding.nodeId, binding.runtimeKind]
264307
+ );
264308
+ base.pinned += upd.rowCount ?? 0;
264309
+ }
264310
+ return base;
264311
+ }
264312
+ var init_chat_session_runtime_pin_backfill = __esm({
264313
+ "../storage/src/chat-session-runtime-pin-backfill.ts"() {
264314
+ "use strict";
264315
+ init_pg_ident();
264316
+ }
264317
+ });
264318
+
263215
264319
  // ../storage/src/postgres-delegations.ts
263216
264320
  function toRecord2(row) {
263217
264321
  return {
@@ -267580,7 +268684,7 @@ var init_cutover_aware_trace = __esm({
267580
268684
  const s2 = await this.pick();
267581
268685
  return typeof s2.runtimeRunActivity === "function" ? s2.runtimeRunActivity(runtimeInstanceId) : null;
267582
268686
  }
267583
- /** 协调运行(任务动态协调者卡):落点 store 没实现就当没有这份数据,不出卡、不报错。 */
268687
+ /** 协调运行(协调者卡的轨迹入口):落点 store 没实现就当没有这份数据,卡照出、只是没图标,不报错。 */
267584
268688
  async listCoordinatorRuns(workOrderId) {
267585
268689
  const store = await this.pick();
267586
268690
  return await store.listCoordinatorRuns?.(workOrderId) ?? [];
@@ -267907,7 +269011,7 @@ var init_cutover_aware_trace = __esm({
267907
269011
  /**
267908
269012
  * 协调运行按工单扇出(任务动态协调者卡)。与 `FanOutDispatchLedger.listByWorkorder` 同理:
267909
269013
  * **一张工单只属于一家公司**,所以逐家查出来的并集恰好就是那一家的行;漏扇会让非默认公司的
267910
- * 协调者卡整片空白。某家查不动只记一笔,不让整块读失败。
269014
+ * 协调者卡全都点不进轨迹。某家查不动只记一笔,不让整块读失败。
267911
269015
  */
267912
269016
  async listCoordinatorRuns(workOrderId) {
267913
269017
  const out = [];
@@ -270859,7 +271963,8 @@ async function backfillUsageLedger(ledger, opts) {
270859
271963
  manifestId: opts.dryRun ? null : opts.manifestId,
270860
271964
  usagePoints: emptyStep(),
270861
271965
  runtimeStartedAt: emptyStep(),
270862
- chat: { ...emptyStep(), notFound: 0, ambiguous: 0, turnFilled: 0 }
271966
+ chat: { ...emptyStep(), notFound: 0, ambiguous: 0, turnFilled: 0 },
271967
+ claudeResume: null
270863
271968
  };
270864
271969
  const sample = (step, id) => {
270865
271970
  if (step.samples.length < sampleSize) step.samples.push(id);
@@ -270997,8 +272102,134 @@ async function backfillUsageLedger(ledger, opts) {
270997
272102
  }
270998
272103
  });
270999
272104
  }
272105
+ if (opts.claudeResumeBefore) {
272106
+ report.claudeResume = await fixClaudeResumeUsage(ledger.pool, s2, opts.claudeResumeBefore, {
272107
+ dryRun: opts.dryRun,
272108
+ manifestId: opts.manifestId,
272109
+ batch,
272110
+ sample: (step, id) => sample(step, id),
272111
+ now
272112
+ });
272113
+ }
272114
+ return report;
272115
+ }
272116
+ function claudeResumeNetUsage(raw) {
272117
+ const out = [];
272118
+ let prev = null;
272119
+ for (const u of raw) {
272120
+ const cost = reportedCost(u["costUsdMicros"]);
272121
+ if (cost === null) {
272122
+ out.push(u);
272123
+ continue;
272124
+ }
272125
+ const prevCost = prev ? reportedCost(prev["costUsdMicros"]) : null;
272126
+ const cumulative = prev !== null && prevCost !== null && cost >= prevCost && BUCKETS.every(([k2]) => usageBucket(u[k2]) >= usageBucket(prev[k2]));
272127
+ if (!cumulative) {
272128
+ out.push(u);
272129
+ prev = u;
272130
+ continue;
272131
+ }
272132
+ const input = usageBucket(u["inputTokens"]) - usageBucket(prev["inputTokens"]);
272133
+ const output = usageBucket(u["outputTokens"]) - usageBucket(prev["outputTokens"]);
272134
+ out.push({
272135
+ ...u,
272136
+ inputTokens: input,
272137
+ outputTokens: output,
272138
+ totalTokens: input + output,
272139
+ cacheReadTokens: usageBucket(u["cacheReadTokens"]) - usageBucket(prev["cacheReadTokens"]),
272140
+ cacheCreationTokens: usageBucket(u["cacheCreationTokens"]) - usageBucket(prev["cacheCreationTokens"]),
272141
+ costUsdMicros: cost - prevCost
272142
+ });
272143
+ prev = u;
272144
+ }
272145
+ return out;
272146
+ }
272147
+ async function fixClaudeResumeUsage(pool, s2, before, o) {
272148
+ const report = { ...emptyStep(), sessions: 0, costBefore: 0, costAfter: 0 };
272149
+ const audited = /* @__PURE__ */ new Map();
272150
+ const auditRows = await pool.query(
272151
+ `SELECT run_id, manifest_id, before, after FROM ${s2}.agent_run_usage_backfill_audit
272152
+ WHERE field = 'claude_resume_usage' ORDER BY id`
272153
+ );
272154
+ for (const a of auditRows.rows) {
272155
+ audited.set(a.run_id, { manifest: a.manifest_id, before: a.before, after: a.after });
272156
+ }
272157
+ const { rows } = await pool.query(
272158
+ `SELECT r.id, r.usage, COALESCE(r.runtime_session_id, r.metadata->>'runtimeSessionId') AS rt
272159
+ FROM ${s2}.agent_runs r
272160
+ WHERE r.runtime_kind = 'claude-code' AND ${ENDED_SQL} AND jsonb_typeof(r.usage) = 'object'
272161
+ AND COALESCE(r.runtime_session_id, r.metadata->>'runtimeSessionId') IS NOT NULL
272162
+ AND r.started_at < $1
272163
+ ORDER BY rt, r.started_at, r.id`,
272164
+ [before]
272165
+ );
272166
+ const chains = /* @__PURE__ */ new Map();
272167
+ for (const row of rows) {
272168
+ const current = row.usage;
272169
+ let raw = current;
272170
+ const last = audited.get(row.id);
272171
+ if (last && !last.manifest.endsWith(":undo")) {
272172
+ const after = last.after?.usage;
272173
+ const prior = last.before?.usage;
272174
+ if (after && prior && sameUsage(after, current)) raw = prior;
272175
+ }
272176
+ const rt = row.rt;
272177
+ const chain = chains.get(rt) ?? [];
272178
+ chains.set(rt, chain);
272179
+ chain.push({ id: row.id, current, raw });
272180
+ }
272181
+ report.sessions = chains.size;
272182
+ const plan = [];
272183
+ for (const chain of chains.values()) {
272184
+ const net = claudeResumeNetUsage(chain.map((r) => r.raw));
272185
+ chain.forEach((r, i) => {
272186
+ if (sameUsage(net[i], r.current)) return;
272187
+ plan.push({ id: r.id, current: r.current, next: net[i] });
272188
+ });
272189
+ }
272190
+ report.candidates = plan.length;
272191
+ for (const p2 of plan) {
272192
+ o.sample(report, p2.id);
272193
+ report.costBefore += reportedCost(p2.current["costUsdMicros"]) ?? 0;
272194
+ report.costAfter += reportedCost(p2.next["costUsdMicros"]) ?? 0;
272195
+ }
272196
+ if (o.dryRun) return report;
272197
+ for (let i = 0; i < plan.length; i += o.batch) {
272198
+ const slice = plan.slice(i, i + o.batch);
272199
+ await inTx(pool, async (c) => {
272200
+ const at = o.now();
272201
+ for (const p2 of slice) {
272202
+ const r = await c.query(
272203
+ `UPDATE ${s2}.agent_runs SET usage = $2::jsonb WHERE id = $1 AND usage = $3::jsonb RETURNING id`,
272204
+ [p2.id, JSON.stringify(p2.next), JSON.stringify(p2.current)]
272205
+ );
272206
+ if (!r.rowCount) continue;
272207
+ await setFinalPoint(c, s2, p2.id, p2.next);
272208
+ report.changed += 1;
272209
+ await audit(
272210
+ c,
272211
+ s2,
272212
+ o.manifestId,
272213
+ p2.id,
272214
+ "claude_resume_usage",
272215
+ { usage: p2.current },
272216
+ { usage: p2.next },
272217
+ at
272218
+ );
272219
+ }
272220
+ });
272221
+ }
271000
272222
  return report;
271001
272223
  }
272224
+ async function setFinalPoint(c, s2, runId, usage) {
272225
+ const f2 = fourOf(usage);
272226
+ await c.query(
272227
+ `UPDATE ${s2}.agent_run_usage_points
272228
+ SET input_tokens = $3, output_tokens = $4, cache_read_tokens = $5, cache_creation_tokens = $6, cost_usd_micros = $7
272229
+ WHERE run_id = $1 AND seq = $2`,
272230
+ [runId, USAGE_POINT_FINAL_SEQ, f2.inputTokens, f2.outputTokens, f2.cacheReadTokens, f2.cacheCreationTokens, reportedCost(usage["costUsdMicros"])]
272231
+ );
272232
+ }
271002
272233
  function usageLedgerViolationCount(r) {
271003
272234
  return Object.values(r.violations).reduce((n, ids2) => n + ids2.length, 0);
271004
272235
  }
@@ -271092,6 +272323,17 @@ async function undoUsageLedgerBackfill(ledger, manifestId, opts = {}) {
271092
272323
  } else if (before !== null) {
271093
272324
  ok3 = await insertPoint(c, s2, runId, before);
271094
272325
  }
272326
+ } else if (field === "claude_resume_usage") {
272327
+ const prior = before?.usage;
272328
+ const cur = after?.usage;
272329
+ if (prior && cur) {
272330
+ const r = await c.query(
272331
+ `UPDATE ${s2}.agent_runs SET usage = $2::jsonb WHERE id = $1 AND usage = $3::jsonb RETURNING id`,
272332
+ [runId, JSON.stringify(prior), JSON.stringify(cur)]
272333
+ );
272334
+ ok3 = (r.rowCount ?? 0) > 0;
272335
+ if (ok3) await setFinalPoint(c, s2, runId, prior);
272336
+ }
271095
272337
  } else if (field === "runtime_started_at" || field === "chat_session_id" || field === "chat_turn_id") {
271096
272338
  const r = await c.query(
271097
272339
  `UPDATE ${s2}.agent_runs SET ${field} = $2 WHERE id = $1 AND ${field} IS NOT DISTINCT FROM $3`,
@@ -271140,7 +272382,7 @@ async function audit(c, s2, manifestId, runId, field, before, after, createdAt)
271140
272382
  [manifestId, runId, field, before == null ? null : JSON.stringify(before), after == null ? null : JSON.stringify(after), createdAt]
271141
272383
  );
271142
272384
  }
271143
- var import_node_crypto82, BUCKETS, ENDED_SQL, START_EVIDENCE_SQL, CHAT_RUN_SQL, emptyStep, ChatRunLookup;
272385
+ var import_node_crypto82, BUCKETS, ENDED_SQL, START_EVIDENCE_SQL, CHAT_RUN_SQL, emptyStep, fourOf, sameUsage, ChatRunLookup;
271144
272386
  var init_usage_ledger_backfill = __esm({
271145
272387
  "../storage/src/usage-ledger-backfill.ts"() {
271146
272388
  "use strict";
@@ -271163,6 +272405,13 @@ var init_usage_ledger_backfill = __esm({
271163
272405
  )`;
271164
272406
  CHAT_RUN_SQL = `r.id LIKE 'chat-run:%'`;
271165
272407
  emptyStep = () => ({ candidates: 0, changed: 0, samples: [] });
272408
+ fourOf = (u) => ({
272409
+ inputTokens: usageBucket(u["inputTokens"]),
272410
+ outputTokens: usageBucket(u["outputTokens"]),
272411
+ cacheReadTokens: usageBucket(u["cacheReadTokens"]),
272412
+ cacheCreationTokens: usageBucket(u["cacheCreationTokens"])
272413
+ });
272414
+ sameUsage = (a, b2) => BUCKETS.every(([k2]) => usageBucket(a[k2]) === usageBucket(b2[k2])) && reportedCost(a["costUsdMicros"]) === reportedCost(b2["costUsdMicros"]);
271166
272415
  ChatRunLookup = class {
271167
272416
  constructor(sources) {
271168
272417
  this.sources = sources;
@@ -271314,6 +272563,7 @@ __export(src_exports, {
271314
272563
  buildTrigger: () => buildTrigger,
271315
272564
  buildWorkorderIndex: () => buildWorkorderIndex,
271316
272565
  canonicalTypeDefHash: () => canonicalTypeDefHash,
272566
+ claudeResumeNetUsage: () => claudeResumeNetUsage,
271317
272567
  companySchemaName: () => companySchemaName,
271318
272568
  copyActorMemoriesIdempotent: () => copyActorMemoriesIdempotent,
271319
272569
  copyActorsIdempotent: () => copyActorsIdempotent,
@@ -271351,6 +272601,7 @@ __export(src_exports, {
271351
272601
  newUsageBackfillManifestId: () => newUsageBackfillManifestId,
271352
272602
  openUsageLedgerForBackfill: () => openUsageLedgerForBackfill,
271353
272603
  payloadChecksum: () => payloadChecksum,
272604
+ pinChatSessionsToAgentDefault: () => pinChatSessionsToAgentDefault,
271354
272605
  planSessionOrd: () => planSessionOrd,
271355
272606
  provisionCompanyDataPlaneSchema: () => provisionCompanyDataPlaneSchema,
271356
272607
  provisionNewCompanyDataPlane: () => provisionNewCompanyDataPlane,
@@ -271396,6 +272647,7 @@ var init_src10 = __esm({
271396
272647
  init_postgres_chat_turns();
271397
272648
  init_postgres_chat_items();
271398
272649
  init_chat_items_ord_backfill();
272650
+ init_chat_session_runtime_pin_backfill();
271399
272651
  init_postgres_delegations();
271400
272652
  init_postgres_nodes();
271401
272653
  init_postgres_model_prices();
@@ -272632,6 +273884,7 @@ __export(src_exports2, {
272632
273884
  NdjsonOplogStore: () => NdjsonOplogStore,
272633
273885
  NodeHealthTracker: () => NodeHealthTracker,
272634
273886
  NotificationLedgerWorker: () => NotificationLedgerWorker,
273887
+ OPERATOR_SCOPE: () => OPERATOR_SCOPE,
272635
273888
  ORPHANED_TURN_ASSISTANT_NOTICE: () => ORPHANED_TURN_ASSISTANT_NOTICE,
272636
273889
  OUTPUT_COPY_LIMITS: () => OUTPUT_COPY_LIMITS,
272637
273890
  PARENT_CONVERSATION_PATH: () => PARENT_CONVERSATION_PATH,
@@ -272723,11 +273976,13 @@ __export(src_exports2, {
272723
273976
  chatBroadcastEnabledFromEnv: () => chatBroadcastEnabledFromEnv,
272724
273977
  chatDelegationEnabledFromEnv: () => chatDelegationEnabledFromEnv,
272725
273978
  chatDispatchScope: () => chatDispatchScope,
273979
+ chatExitModelFields: () => chatExitModelFields,
272726
273980
  chatInterruptCauseOf: () => chatInterruptCauseOf,
272727
273981
  chatInterruptItemId: () => chatInterruptItemId,
272728
273982
  chatItemsDoubleWriteEnabled: () => chatItemsDoubleWriteEnabled,
272729
273983
  chatNodeChanged: () => chatNodeChanged,
272730
273984
  chatRunLedgerFields: () => chatRunLedgerFields,
273985
+ chatSessionInitialRuntime: () => chatSessionInitialRuntime,
272731
273986
  childRoundMessageIds: () => childRoundMessageIds,
272732
273987
  clampObservedAt: () => clampObservedAt,
272733
273988
  classifyPage: () => classifyPage,
@@ -272868,6 +274123,7 @@ __export(src_exports2, {
272868
274123
  isKnowledgeQueryPlanV2: () => isKnowledgeQueryPlanV2,
272869
274124
  isLocationFailureCode: () => isLocationFailureCode,
272870
274125
  isNodeHealthy: () => isNodeHealthy,
274126
+ isOperatorRequest: () => isOperatorRequest,
272871
274127
  isRunTerminalStatus: () => isRunTerminalStatus,
272872
274128
  isRuntimeDrifted: () => isRuntimeDrifted,
272873
274129
  isRuntimeRunReader: () => isRuntimeRunReader,
@@ -273097,6 +274353,7 @@ var init_src11 = __esm({
273097
274353
  "use strict";
273098
274354
  init_server3();
273099
274355
  init_tokens();
274356
+ init_operator();
273100
274357
  init_session_dataplane_token();
273101
274358
  init_dev_store();
273102
274359
  init_control_plane_file_store();
@@ -273385,7 +274642,23 @@ function parseUsageLedgerBackfillArgs(flags) {
273385
274642
  if (modes.length !== 1) {
273386
274643
  throw new UsageLedgerBackfillArgError("--dry-run / --execute / --verify / --undo \u5FC5\u987B\u4E14\u53EA\u80FD\u9009\u4E00\u4E2A");
273387
274644
  }
273388
- return { scope: company !== void 0 ? { companyId: company } : { all: true }, mode: modes[0] };
274645
+ const mode = modes[0];
274646
+ let claudeResumeBefore;
274647
+ if (flags.has("claude-resume-before")) {
274648
+ const raw = flags.get("claude-resume-before");
274649
+ if (raw === "" || raw === "true" || Number.isNaN(Date.parse(raw))) {
274650
+ throw new UsageLedgerBackfillArgError("--claude-resume-before \u9700\u8981 ISO \u65F6\u523B\uFF08\u5982 2026-09-23T00:00:00Z\uFF09\uFF1A\u4FEE\u597D\u7684\u8282\u70B9\u5305\u5728\u5168\u90E8\u8282\u70B9\u88C5\u4E0A\u7684\u65F6\u523B");
274651
+ }
274652
+ if (mode.kind !== "dry-run" && mode.kind !== "execute") {
274653
+ throw new UsageLedgerBackfillArgError("--claude-resume-before \u53EA\u80FD\u914D --dry-run \u6216 --execute\uFF08\u64A4\u9500\u7167\u5E38\u7528 --undo\uFF09");
274654
+ }
274655
+ claudeResumeBefore = new Date(raw).toISOString();
274656
+ }
274657
+ return {
274658
+ scope: company !== void 0 ? { companyId: company } : { all: true },
274659
+ mode,
274660
+ ...claudeResumeBefore ? { claudeResumeBefore } : {}
274661
+ };
273389
274662
  }
273390
274663
  async function runUsageLedgerBackfill(flags, io) {
273391
274664
  let args;
@@ -273427,7 +274700,11 @@ ${USAGE_LEDGER_BACKFILL_USAGE}`);
273427
274700
  const dryRun = args.mode.kind === "dry-run";
273428
274701
  const manifestId2 = dryRun ? void 0 : storage.newUsageBackfillManifestId();
273429
274702
  for (const l of ledgers) {
273430
- const r = await storage.backfillUsageLedger(l, { dryRun, ...manifestId2 ? { manifestId: manifestId2 } : {} });
274703
+ const r = await storage.backfillUsageLedger(l, {
274704
+ dryRun,
274705
+ ...manifestId2 ? { manifestId: manifestId2 } : {},
274706
+ ...args.claudeResumeBefore ? { claudeResumeBefore: args.claudeResumeBefore } : {}
274707
+ });
273431
274708
  const n = (step) => dryRun ? `\u5F85\u6539 ${step.candidates}` : `\u5DF2\u6539 ${step.changed}/${step.candidates}`;
273432
274709
  io.out(`\u2014 \u8D26\u672C ${r.label}\uFF08schema=${r.schema}\uFF09`);
273433
274710
  io.out(` 1 \u7528\u91CF\u70B9(backfill)\uFF1A${n(r.usagePoints)}${samples(r.usagePoints.samples)}`);
@@ -273435,6 +274712,12 @@ ${USAGE_LEDGER_BACKFILL_USAGE}`);
273435
274712
  io.out(
273436
274713
  ` 3 \u4F1A\u8BDD/\u8F6E\u6B21\uFF1A${n(r.chat)}\uFF08\u5176\u4E2D\u8865\u8F6E\u6B21 ${r.chat.turnFilled}\uFF1B\u67E5\u4E0D\u5230 ${r.chat.notFound}\u3001\u6B67\u4E49 ${r.chat.ambiguous}\uFF0C\u4FDD\u6301 NULL\uFF09` + samples(r.chat.samples)
273437
274714
  );
274715
+ if (r.claudeResume) {
274716
+ const c = r.claudeResume;
274717
+ io.out(
274718
+ ` 4 Claude \u7EED\u8DD1\u7D2F\u8BA1\u503C\u6263\u56DE\uFF08\u5F00\u8DD1\u65E9\u4E8E ${args.claudeResumeBefore}\uFF0C${c.sessions} \u6761\u4F1A\u8BDD\u7EBF\uFF09\uFF1A${n(c)}\uFF1B\u8FD9\u4E9B run \u7684\u8D39\u7528\u5408\u8BA1 $${(c.costBefore / 1e6).toFixed(2)} \u2192 $${(c.costAfter / 1e6).toFixed(2)}` + samples(c.samples)
274719
+ );
274720
+ }
273438
274721
  }
273439
274722
  if (dryRun) io.out("[usage-ledger-backfill] dry-run\uFF1A\u4E00\u884C\u672A\u5199\u3002\u4E0B\u4E00\u6B65 --execute\u3002");
273440
274723
  else {
@@ -273482,7 +274765,7 @@ var USAGE_LEDGER_BACKFILL_USAGE, UsageLedgerBackfillArgError, MAX_LISTED;
273482
274765
  var init_usage_ledger_backfill_command = __esm({
273483
274766
  "../cli/src/usage-ledger-backfill-command.ts"() {
273484
274767
  "use strict";
273485
- USAGE_LEDGER_BACKFILL_USAGE = "\u7528\u6CD5\uFF1Aoasis usage-ledger-backfill (--company <id> | --all) (--dry-run | --execute | --verify | --undo <manifestId>)";
274768
+ USAGE_LEDGER_BACKFILL_USAGE = "\u7528\u6CD5\uFF1Aoasis usage-ledger-backfill (--company <id> | --all) (--dry-run | --execute | --verify | --undo <manifestId>) [--claude-resume-before <ISO \u65F6\u523B>]";
273486
274769
  UsageLedgerBackfillArgError = class extends Error {
273487
274770
  };
273488
274771
  MAX_LISTED = 50;
@@ -273492,7 +274775,7 @@ var init_usage_ledger_backfill_command = __esm({
273492
274775
  // src/index.ts
273493
274776
  var fs48 = __toESM(require("node:fs"));
273494
274777
  init_src();
273495
- var os14 = __toESM(require("node:os"));
274778
+ var os15 = __toESM(require("node:os"));
273496
274779
  var path43 = __toESM(require("node:path"));
273497
274780
  var import_node_child_process24 = require("node:child_process");
273498
274781
  var import_node_crypto93 = require("node:crypto");
@@ -273500,7 +274783,7 @@ var import_node_crypto93 = require("node:crypto");
273500
274783
  // ../cli/src/cli.ts
273501
274784
  init_src();
273502
274785
  var fs42 = __toESM(require("node:fs"), 1);
273503
- var os12 = __toESM(require("node:os"), 1);
274786
+ var os13 = __toESM(require("node:os"), 1);
273504
274787
  var path37 = __toESM(require("node:path"), 1);
273505
274788
  var import_node_crypto91 = require("node:crypto");
273506
274789
 
@@ -273510,7 +274793,7 @@ init_chat_cancel_attribution();
273510
274793
  init_src();
273511
274794
  init_src11();
273512
274795
  var fs34 = __toESM(require("node:fs"), 1);
273513
- var os8 = __toESM(require("node:os"), 1);
274796
+ var os9 = __toESM(require("node:os"), 1);
273514
274797
  var path28 = __toESM(require("node:path"), 1);
273515
274798
  var import_node_child_process17 = require("node:child_process");
273516
274799
  var import_node_crypto87 = require("node:crypto");
@@ -275480,132 +276763,7 @@ init_src11();
275480
276763
  init_src4();
275481
276764
 
275482
276765
  // ../cli/src/chat-exit.ts
275483
- var fs31 = __toESM(require("node:fs"), 1);
275484
- init_src();
275485
- init_src7();
275486
- init_chat_cancel_attribution();
275487
- function transcriptTail(ref2) {
275488
- try {
275489
- const stat4 = fs31.statSync(ref2);
275490
- const size = Math.min(stat4.size, 4e3);
275491
- const buf = Buffer.alloc(size);
275492
- const fd = fs31.openSync(ref2, "r");
275493
- try {
275494
- fs31.readSync(fd, buf, 0, size, Math.max(0, stat4.size - size));
275495
- } finally {
275496
- fs31.closeSync(fd);
275497
- }
275498
- return buf.toString("utf8").trim();
275499
- } catch {
275500
- return "";
275501
- }
275502
- }
275503
- var SUMMARY_MAX_CHARS = 500;
275504
- function clipLine(line2) {
275505
- return line2.length > SUMMARY_MAX_CHARS ? `${line2.slice(0, SUMMARY_MAX_CHARS)}\u2026` : line2;
275506
- }
275507
- function providerMessage(line2) {
275508
- const m2 = /['"]message['"]\s*:\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")/.exec(line2);
275509
- const text7 = (m2?.[1] ?? m2?.[2])?.trim();
275510
- return text7 ? text7 : void 0;
275511
- }
275512
- var INTERNAL_PREFIX_RE = /^\[[a-z][a-z0-9_]*\]\s*/;
275513
- var QUOTA_RE3 = /hit your (?:\w+ )?limit|usage limit|quota|insufficient[_ ](?:balance|credit|funds)|credit balance|billing|\b402\b|额度|余额/i;
275514
- var RATE_LIMIT_RE = /rate.?limit|too many requests|\b429\b|\b529\b|overloaded|限流/i;
275515
- var AUTH_RE2 = /\b401\b|\b403\b|unauthori[sz]ed|authenticat|oauth|invalid[_ -]?(?:api[_ -]?key|token|x-api-key)|api[_ -]?key[^\n]{0,40}\b(?:invalid|expired|revoked)|token[^\n]{0,30}\b(?:expired|invalid|revoked)|not logged in|\/login|credential|凭证|登录/i;
275516
- 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;
275517
- var DISK_FULL_RE = /\bENOSPC\b|no space left on device|\bEDQUOT\b|disk quota exceeded|\[Errno 28\]|disk (?:is )?full|磁盘(?:空间)?(?:已满|不足)/i;
275518
- var UNREACHABLE_RE2 = /不在线|socket 非 OPEN|帧未送达|失联/;
275519
- function failureKind(reason, line2) {
275520
- if (line2.startsWith(SESSION_STARTUP_FAILED_PREFIX)) return "startup-failed";
275521
- switch (reason) {
275522
- case "auth-error":
275523
- return "auth";
275524
- case "server-unreachable":
275525
- return "node-unreachable";
275526
- case "timeout":
275527
- return "timeout";
275528
- case "output-limit":
275529
- return "output-limit";
275530
- case "rate-limit":
275531
- return QUOTA_RE3.test(line2) ? "quota" : "rate-limit";
275532
- default:
275533
- break;
275534
- }
275535
- if (!line2) return "unknown";
275536
- if (DISK_FULL_RE.test(line2)) return "disk-full";
275537
- if (QUOTA_RE3.test(line2)) return "quota";
275538
- if (AUTH_RE2.test(line2)) return "auth";
275539
- if (RATE_LIMIT_RE.test(line2)) return "rate-limit";
275540
- if (MODEL_RE.test(line2)) return "model";
275541
- if (UNREACHABLE_RE2.test(line2)) return "node-unreachable";
275542
- return "unknown";
275543
- }
275544
- var chatRebindHint = (what) => `\u5728\u7EC4\u7EC7\u9875\u8FD9\u4F4D Agent \u7684\u300C\u6A21\u578B\u300D\u9875\u7B7E\u91CC${what}`;
275545
- function resetTime(line2) {
275546
- return extractResetHint(line2)?.replace(/^(?:resets?|try again at)\s+/i, "").trim() || void 0;
275547
- }
275548
- function failureSummary(kind, ctx) {
275549
- switch (kind) {
275550
- case "auth":
275551
- return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u65F6\u7684\u767B\u5F55\u5DF2\u8FC7\u671F\u6216\u51ED\u8BC1\u65E0\u6548\uFF0C\u8C03\u7528\u4E0D\u4E86\u6A21\u578B\u3002\u8BF7\u8282\u70B9\u7BA1\u7406\u5458\u5728\u90A3\u53F0\u673A\u5668\u4E0A\u91CD\u65B0\u767B\u5F55\u8FD9\u4E2A\u8FD0\u884C\u65F6\uFF08\u6216\u66F4\u65B0 API Key\uFF09\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u7740\u6025\u7684\u8BDD\u4E5F\u53EF\u4EE5${chatRebindHint("\u6362\u4E00\u4E2A\u53EF\u7528\u7684\u8FD0\u884C\u65F6")}\u3002`;
275552
- case "quota": {
275553
- const at = resetTime(ctx.line);
275554
- return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u6A21\u578B\u989D\u5EA6\u5DF2\u7528\u5B8C${at ? `\uFF0C\u9884\u8BA1 ${at} \u6062\u590D` : ""}\u3002\u8BF7\u7B49\u989D\u5EA6\u6062\u590D\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u7740\u6025\u7684\u8BDD\u53EF\u4EE5${chatRebindHint("\u6362\u4E00\u4E2A\u989D\u5EA6\u5145\u8DB3\u7684\u8FD0\u884C\u65F6")}\u3002`;
275555
- }
275556
- case "rate-limit":
275557
- return "\u5BF9\u8BDD\u5931\u8D25\uFF1A\u6A21\u578B\u670D\u52A1\u6682\u65F6\u9650\u6D41\u6216\u7E41\u5FD9\u3002\u8BF7\u7B49\u4E00\u4E24\u5206\u949F\u518D\u53D1\u4E00\u6B21\u3002";
275558
- case "node-unreachable":
275559
- return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u8FD9\u4F4D Agent \u7684\u8282\u70B9\u79BB\u7EBF\u6216\u8FDE\u63A5\u4E2D\u65AD\u3002\u8BF7\u786E\u8BA4\u90A3\u53F0\u673A\u5668\u5F00\u7740\u3001\u8282\u70B9\u7A0B\u5E8F\u5728\u8FD0\u884C\u3001\u7F51\u7EDC\u6B63\u5E38\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u4E5F\u53EF\u4EE5${chatRebindHint("\u6362\u4E00\u4E2A\u5728\u7EBF\u7684\u8FD0\u884C\u65F6")}\u3002`;
275560
- case "model":
275561
- 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`;
275562
- case "timeout":
275563
- 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";
275564
- case "startup-failed":
275565
- 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`;
275566
- case "disk-full":
275567
- return `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u8FD9\u4F4D Agent \u7684\u8282\u70B9\u78C1\u76D8\u7A7A\u95F4\u6EE1\u4E86\uFF0C\u8FD9\u4E00\u8F6E\u5199\u4E0D\u4E86\u6587\u4EF6\u3002\u8BF7\u8282\u70B9\u7BA1\u7406\u5458\u6E05\u7406\u90A3\u53F0\u673A\u5668\u7684\u78C1\u76D8\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u7740\u6025\u7684\u8BDD\u4E5F\u53EF\u4EE5${chatRebindHint("\u6362\u4E00\u4E2A\u8FD0\u884C\u65F6")}\u3002`;
275568
- case "output-limit":
275569
- 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";
275570
- case "unknown":
275571
- if (ctx.line) {
275572
- return "\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u65F6\u62A5\u9519\u4E86\uFF0C\u6682\u65F6\u6CA1\u8BA4\u51FA\u662F\u4EC0\u4E48\u539F\u56E0\u3002\u53EF\u4EE5\u7A0D\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\uFF0C\u8BF7\u70B9\u300C\u5C55\u5F00\u8BE6\u60C5\u300D\u628A\u539F\u59CB\u62A5\u9519\u53D1\u7ED9\u7BA1\u7406\u5458\u3002";
275573
- }
275574
- return ctx.hasDetail ? "\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u65F6\u5F02\u5E38\u9000\u51FA\uFF0C\u6CA1\u6709\u7ED9\u51FA\u62A5\u9519\u539F\u56E0\u3002\u53EF\u4EE5\u7A0D\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\uFF0C\u8BF7\u70B9\u300C\u5C55\u5F00\u8BE6\u60C5\u300D\u628A\u539F\u59CB\u8BB0\u5F55\u53D1\u7ED9\u7BA1\u7406\u5458\u3002" : `\u5BF9\u8BDD\u5931\u8D25\uFF1A\u8FD0\u884C\u65F6\u5F02\u5E38\u9000\u51FA\uFF08\u9000\u51FA\u7801 ${ctx.code ?? "\u65E0"}\uFF09\uFF0C\u6CA1\u6709\u7559\u4E0B\u62A5\u9519\u4FE1\u606F\u3002\u53EF\u4EE5\u7A0D\u540E\u518D\u53D1\u4E00\u6B21\uFF1B\u53CD\u590D\u51FA\u73B0\u7684\u8BDD\uFF0C\u8BF7\u7BA1\u7406\u5458\u5230\u300C\u8FD0\u884C\u8F68\u8FF9\u300D\u91CC\u67E5\u8FD9\u4E00\u8F6E\u3002`;
275575
- }
275576
- }
275577
- function buildChatExitError(info) {
275578
- const genericLine = `runtime exited with code ${info.code ?? "null"}${info.reason ? ` (${info.reason})` : ""}`;
275579
- let detail = "";
275580
- if (info.reason !== "cancelled") {
275581
- detail = info.errorMessage?.trim() ?? "";
275582
- if (!detail && info.transcriptRef) detail = transcriptTail(info.transcriptRef);
275583
- }
275584
- let summary;
275585
- let cancelledBy;
275586
- if (info.reason === "cancelled") {
275587
- cancelledBy = cancelCauseFromExitMessage(info.errorMessage);
275588
- summary = chatCancelNote(cancelledBy);
275589
- } else {
275590
- const firstErrorLine = (info.errorMessage?.split("\n").map((line2) => line2.trim()).find(Boolean) ?? "").replace(INTERNAL_PREFIX_RE, "");
275591
- const readable = firstErrorLine ? clipLine(providerMessage(firstErrorLine) ?? firstErrorLine) : "";
275592
- summary = failureSummary(failureKind(info.reason, firstErrorLine), {
275593
- readable,
275594
- line: firstErrorLine,
275595
- hasDetail: Boolean(detail),
275596
- code: info.code
275597
- });
275598
- }
275599
- const chatExit = {
275600
- ...info.reason ? { reason: info.reason } : {},
275601
- summary,
275602
- ...cancelledBy ? { cancelledBy } : {},
275603
- ...detail ? { detail: `${genericLine}
275604
-
275605
- ${detail}`.slice(0, 4200) } : {}
275606
- };
275607
- return Object.assign(new Error(summary), { chatExit });
275608
- }
276766
+ init_chat_exit();
275609
276767
 
275610
276768
  // ../cli/src/chat-task-header.ts
275611
276769
  init_src11();
@@ -276398,15 +277556,15 @@ function oasisWrapperScript() {
276398
277556
  const main = path28.join(repoRoot, "packages/cli/src/main.ts");
276399
277557
  const key2 = (0, import_node_crypto87.createHash)("sha256").update(repoRoot).digest("hex").slice(0, 12);
276400
277558
  const uid = typeof process.getuid === "function" ? process.getuid() : "na";
276401
- const stable = path28.join(os8.tmpdir(), `oasis-cli-wrapper-${uid}-${key2}`);
277559
+ const stable = path28.join(os9.tmpdir(), `oasis-cli-wrapper-${uid}-${key2}`);
276402
277560
  let dir;
276403
277561
  try {
276404
277562
  fs34.mkdirSync(stable, { recursive: true, mode: 448 });
276405
277563
  const st = fs34.lstatSync(stable);
276406
277564
  const mine = st.isDirectory() && !st.isSymbolicLink() && (typeof process.getuid !== "function" || st.uid === process.getuid());
276407
- dir = mine ? stable : fs34.mkdtempSync(path28.join(os8.tmpdir(), "oasis-cli-"));
277565
+ dir = mine ? stable : fs34.mkdtempSync(path28.join(os9.tmpdir(), "oasis-cli-"));
276408
277566
  } catch {
276409
- dir = fs34.mkdtempSync(path28.join(os8.tmpdir(), "oasis-cli-"));
277567
+ dir = fs34.mkdtempSync(path28.join(os9.tmpdir(), "oasis-cli-"));
276410
277568
  }
276411
277569
  const wrapper = path28.join(dir, "oasis");
276412
277570
  fs34.writeFileSync(wrapper, `#!/usr/bin/env bash
@@ -276632,7 +277790,8 @@ async function startServe(opts) {
276632
277790
  const reloadTimeoutPolicy = () => {
276633
277791
  try {
276634
277792
  activeTimeoutPolicy = loadPolicyFromFile(resolvePolicyFilePath());
276635
- console.log(`[serve] timeout-policy \u5DF2\u70ED\u8F7D\uFF08version=${activeTimeoutPolicy.version}\uFF09`);
277793
+ syncChatWallClockCopy();
277794
+ console.log(`[serve] timeout-policy \u5DF2\u70ED\u8F7D\uFF08version=${activeTimeoutPolicy.version}\uFF0Cchat \u5899\u949F=${wallClockForKind("chat")}ms\uFF09`);
276636
277795
  return { ok: true, version: activeTimeoutPolicy.version };
276637
277796
  } catch (err) {
276638
277797
  console.error(`[serve] timeout-policy \u70ED\u8F7D\u5931\u8D25\uFF0C\u4FDD\u7559\u65E7\u7B56\u7565\uFF1A${err.message}`);
@@ -276646,6 +277805,8 @@ async function startServe(opts) {
276646
277805
  return (act.cadence.find((s2) => s2.reassignAfter) ?? act.cadence[act.cadence.length - 1]).atMs;
276647
277806
  };
276648
277807
  const wallClockForKind = (runtimeKind) => activeTimeoutPolicy.resolve({ executor: "agent", scenario: "wall-clock", runtimeKind }).wallClockMs ?? 120 * 6e4;
277808
+ const syncChatWallClockCopy = () => setChatWallClockMs(wallClockForKind("chat"));
277809
+ syncChatWallClockCopy();
276649
277810
  const loadJson = (name) => {
276650
277811
  const file = path28.join(opts.dir, name);
276651
277812
  return fs34.existsSync(file) ? JSON.parse(fs34.readFileSync(file, "utf8")) : void 0;
@@ -276767,8 +277928,8 @@ async function startServe(opts) {
276767
277928
  );
276768
277929
  const issuer = createTokenIssuer({ secretFile: path28.join(opts.dir, "session-token-secret") });
276769
277930
  const apiActivity = createApiActivityLedger();
276770
- const operatorActor = `actor:human:${os8.userInfo().username}`;
276771
- const operatorToken = issuer.issue(operatorActor);
277931
+ const operatorActor = `actor:human:${os9.userInfo().username}`;
277932
+ const operatorToken = issuer.issue(operatorActor, { scopes: [OPERATOR_SCOPE] });
276772
277933
  fs34.writeFileSync(path28.join(opts.dir, "operator-token"), operatorToken, { mode: 384 });
276773
277934
  let nodeTokens = createNodeTokenStore(path28.join(opts.dir, "node-tokens.json"));
276774
277935
  const enrollTokens = createEnrollTokenStore(30 * 60 * 1e3, path28.join(opts.dir, "enroll-tokens.json"));
@@ -278605,6 +279766,37 @@ async function startServe(opts) {
278605
279766
  return plan && plan.injectedHistoryCount > 0 ? plan.runtimeMessage : void 0;
278606
279767
  };
278607
279768
  const resolveCompanyBinding = createCompanyBindingResolver({ actors, defaultCompanyId });
279769
+ if (pgPool) {
279770
+ const pinPool = pgPool;
279771
+ void (async () => {
279772
+ try {
279773
+ const companies2 = await controlPlaneStore.listCompanies();
279774
+ const total = { scanned: 0, pinned: 0, leftUnbound: 0, unresolved: 0 };
279775
+ for (const c of companies2) {
279776
+ const rep = await pinChatSessionsToAgentDefault(pinPool, engineSchemaFor(c.id), {
279777
+ companyId: c.id,
279778
+ // 与派发、与委派回流、与 chat 工作区读口**同一个** resolver 实例——存量钉住的目标必须
279779
+ // 与「不钉时这一轮会派到哪」逐字相同,否则这条迁移本身就改了行为。
279780
+ resolveBinding: async (actorId) => {
279781
+ const b2 = await resolveCompanyBinding(c.id, actorId);
279782
+ return b2 ? { nodeId: b2.nodeId, runtimeKind: b2.runtimeKind } : null;
279783
+ }
279784
+ });
279785
+ total.scanned += rep.scanned;
279786
+ total.pinned += rep.pinned;
279787
+ total.leftUnbound += rep.leftUnbound;
279788
+ total.unresolved += rep.unresolved;
279789
+ }
279790
+ if (total.scanned > 0) {
279791
+ console.log(
279792
+ `[chat-runtime-pin] ADR 0613 \u5B58\u91CF\u4F1A\u8BDD\u9489\u76EE\u6807\uFF1A\u547D\u4E2D ${total.scanned} \u6761\u3001\u5199\u5165 ${total.pinned} \u6761\u3001\u5458\u5DE5\u672A\u7ED1\u5B9A\u7559\u7A7A ${total.leftUnbound} \u6761\u3001binding \u89E3\u6790\u5931\u8D25 ${total.unresolved} \u6761`
279793
+ );
279794
+ }
279795
+ } catch (err) {
279796
+ console.warn(`[chat-runtime-pin] \u5B58\u91CF\u4F1A\u8BDD\u9489\u76EE\u6807\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u542F\u52A8\uFF0C\u4E0B\u6B21\u542F\u52A8\u5E42\u7B49\u91CD\u8BD5\uFF09\uFF1A`, err);
279797
+ }
279798
+ })();
279799
+ }
278608
279800
  const delegationWorkdirLocator = buildDelegationWorkdirLocator({
278609
279801
  // ⭐ 与派发、与 chat-sessions 域**同一个 resolver 实例**(评审 blocker ① 五修):
278610
279802
  // 生产 `resolveCompanyBinding` 在上文构造一次,喂给这里、`dispatchChatImpl`、
@@ -279036,7 +280228,7 @@ async function startServe(opts) {
279036
280228
  // 派发台账:动态卡的 runId 与「执行轨迹」抽屉靠它把 work/review 硬关联到 agent_runs.id。
279037
280229
  // 与上面 createServer 的 `dispatchesOfWorkorder`(timeline 用)是同一个闭包,别分叉。
279038
280230
  dispatchesOfWorkorder: (wid) => fanOutLedger.listByWorkorder(wid),
279039
- // 任务动态**协调者卡**(F2):协调者不走 dispatcher,台账里没有它,只能从 agent_runs 取。
280231
+ // 任务动态**协调者卡**(F2)的轨迹入口:协调者不走 dispatcher,台账里没有它,只能从 agent_runs 取。
279040
280232
  // 扇出理由同 `dispatchesOfWorkorder`:这个回调拿不到公司,而一张工单只属于一家。
279041
280233
  coordinatorRunsOfWorkorder: (wid) => fanOutTrace.listCoordinatorRuns(wid),
279042
280234
  // 连败熔断卡面的「末次:{失败原文首句}」:原话只在 `agent_runs.error_message`,快照里只有分类词。
@@ -279113,6 +280305,13 @@ async function startServe(opts) {
279113
280305
  // store(`public.actor_bindings`),org-registry cutover 之后它是冻结旧镜像——只留给
279114
280306
  // 本域其它老读口,工作区定位一律不许再用它。
279115
280307
  resolveBinding: resolveCompanyBinding,
280308
+ /* ADR 0613:建会话 / 「恢复为员工默认运行配置」这两下要读员工详情页配的型号,按**当前公司**读
280309
+ (与 `resolveBinding` 同一条理由:全局 `registry` 对已切 `actor-config` 的公司是冻结旧镜像)。
280310
+ ⚠ 只有这两下读它;解析每一轮的模型依旧不读员工配置(ADR 0610 D1)。 */
280311
+ resolveActorModel: async (cid, actorId) => {
280312
+ const ctx = await actors.resolveCtx(cid ?? defaultCompanyId);
280313
+ return (await ctx.service.latestConfig(actorId))?.model ?? null;
280314
+ },
279116
280315
  // 按 runId 路由:切换后新会话的事件落在本公司库里,共享 store 会一条都补不出 parts。
279117
280316
  // ⚠ `runRoutedTrace` 声明在本行**之后**:包成箭头函数避 TDZ(本域只需要 listEvents)。
279118
280317
  trace: { listEvents: (runId, o) => runRoutedTrace.listEvents(runId, o) },
@@ -279200,7 +280399,11 @@ async function startServe(opts) {
279200
280399
  resolveChatItems: (companyId) => chatItemsFor(companyId),
279201
280400
  liveChat,
279202
280401
  // ADR-0587:列表行派生 `unread`,与 collab 域收件箱读同一份按公司的水位(无 PG 时闭包自己回落单实例)。
279203
- resolveReadMarkers: readMarkerStoreFor
280402
+ resolveReadMarkers: readMarkerStoreFor,
280403
+ // ADR 0610 D4:`GET /api/chat-sessions/:id/usage` 的取数口——**与 collab 域是同一个实例**,
280404
+ // 按会话取数与组织统计因此永远同一份账本、同一套归属,不会出现两条路各算各的。
280405
+ usageRead,
280406
+ defaultCompanyId
279204
280407
  })] : []
279205
280408
  ],
279206
280409
  workorderDrafts,
@@ -279747,7 +280950,9 @@ async function startServe(opts) {
279747
280950
  ...c.dispatchId !== void 0 ? { dispatchId: c.dispatchId } : {},
279748
280951
  // ADR-0078 D3:写入围栏的钥匙
279749
280952
  ...c.artifactId !== void 0 ? { artifactId: c.artifactId } : {},
279750
- ...c.sessionId !== void 0 ? { sessionId: c.sessionId } : {}
280953
+ ...c.sessionId !== void 0 ? { sessionId: c.sessionId } : {},
280954
+ // ADR 0610 D1:签名 scopes 透给域路由,`isOperatorRequest` 据此认出部署级 operator-token。
280955
+ ...c.scopes !== void 0 ? { scopes: c.scopes } : {}
279751
280956
  } : null;
279752
280957
  },
279753
280958
  /**
@@ -280071,9 +281276,10 @@ async function startServe(opts) {
280071
281276
  const provision = isolatedKnowledgeTurn ? { env: {}, wrapperPaths: [], requiredTools: [], requiredToolVersions: {}, connectorCreds: [], cleanup: async () => void 0 } : await buildActorProvision(actorService, actorId, chatDispatchScope(priorChatSession));
280072
281277
  const actorCtx = await buildActorContext(actorService, actorId).catch(() => null);
280073
281278
  const sessionModel = configuredModel(sessionModelOverride);
280074
- const chatResolvedModel = sessionModel ?? configuredModel(actorCtx?.config?.model);
281279
+ const actorConfiguredModel = isolatedBackground ? configuredModel(actorCtx?.config?.model) : void 0;
281280
+ const chatResolvedModel = sessionModel ?? actorConfiguredModel;
280075
281281
  const chatModel = chatResolvedModel ?? await registryStore.getRuntimeConfig(`runtime:${binding.nodeId}:${binding.runtimeKind}`).then((c) => configuredModel(c?.model)).catch(() => void 0);
280076
- const chatModelSource = sessionModel ? "session_override" : chatResolvedModel ? "actor_config" : chatModel ? "runtime_default" : null;
281282
+ const chatModelSource = sessionModel ? "session_override" : actorConfiguredModel ? "actor_config" : chatModel ? "runtime_default" : null;
280077
281283
  const attachmentFiles = { ...preparedAttachments.files, ...restore ? { "SESSION_FILES.md": restore.index } : {} };
280078
281284
  const attachmentBinary = { ...preparedAttachments.binaryFiles };
280079
281285
  const attachmentPaths = [...preparedAttachments.paths];
@@ -280290,8 +281496,7 @@ async function startServe(opts) {
280290
281496
  startedAt: endedAt
280291
281497
  }]);
280292
281498
  const usage = info.usage ?? null;
280293
- const effectiveModel = info.model ?? chatModel ?? null;
280294
- const modelSource = info.model ? "runtime" : chatModelSource;
281499
+ const modelFields = chatExitModelFields(info.model, chatModel, chatModelSource);
280295
281500
  let nativeSessionMetadata;
280296
281501
  if (info.runtimeSessionId && info.runtimeSessionId !== runtimeSessionId) {
280297
281502
  const run = await (await traceStoreFor(runCompanyId)).getRun(traceRunId).catch(() => null);
@@ -280306,8 +281511,7 @@ async function startServe(opts) {
280306
281511
  ...!isHealthySessionExit(info) && info.reason !== "cancelled" ? { errorCode: info.reason ?? "error", errorMessage: formatExitError(info) } : {},
280307
281512
  transcriptRef: info.transcriptRef ?? null,
280308
281513
  usage,
280309
- effectiveModel,
280310
- modelSource,
281514
+ ...modelFields,
280311
281515
  ...nativeSessionMetadata ? { metadata: nativeSessionMetadata } : {}
280312
281516
  });
280313
281517
  if (usage) await settleRunUsage(await traceStoreFor(runCompanyId), traceRunId);
@@ -280420,7 +281624,11 @@ async function startServe(opts) {
280420
281624
  info.chatSessionId,
280421
281625
  `\u5B50\u4F1A\u8BDD\u8FD9\u4E00\u8F6E\u88AB\u6062\u590D\u5668\u6536\u53E3\u4E3A ${info.status}\uFF08${info.reason}\uFF09`,
280422
281626
  info.companyId,
280423
- { outcome: info.status === "succeeded" ? "succeeded" : "failed", ...info.turnId ? { turnId: info.turnId } : {} }
281627
+ {
281628
+ outcome: info.status === "succeeded" ? "succeeded" : "failed",
281629
+ ...info.turnId ? { turnId: info.turnId } : {},
281630
+ ...info.exitReason ? { exitReason: info.exitReason } : {}
281631
+ }
280424
281632
  );
280425
281633
  };
280426
281634
  const handshakeThrottleOpts = {};
@@ -282883,6 +284091,11 @@ ${nodeFault}` : "");
282883
284091
  const zbCostThresholdMicros = opts.zeroOutputBreaker?.costThresholdMicros ?? (envInt("OASIS_ZERO_OUTPUT_BREAKER_COST_USD") ?? 0) * 1e6;
282884
284092
  if (zbEnabled && zbIntervalMs > 0 && (zbThreshold > 0 || zbCostThresholdMicros > 0)) {
282885
284093
  const zbActions = /* @__PURE__ */ new Set(["produce", "review"]);
284094
+ const engineWorkIdOf = (metadata) => {
284095
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return null;
284096
+ const v2 = metadata["engineWorkId"];
284097
+ return typeof v2 === "string" && v2.trim() ? v2 : null;
284098
+ };
282886
284099
  const listRecentZeroOutputRuns = async (sinceIso) => {
282887
284100
  const items = await fanOutTrace.listRunsAcrossWithCompany(
282888
284101
  { status: "no-output", from: sinceIso, limit: 1e3 },
@@ -282896,7 +284109,10 @@ ${nodeFault}` : "");
282896
284109
  actorId: r.actorId,
282897
284110
  action: r.action,
282898
284111
  startedAt: r.startedAt,
282899
- costUsdMicros: r.usage?.costUsdMicros ?? null
284112
+ costUsdMicros: r.usage?.costUsdMicros ?? null,
284113
+ // ★ 轮次维度(Bug 0211):同一轮里的协议续唤各是一条 run,只数 run 会让上报正文那个数
284114
+ // 比人点开轨迹数到的段数大。**只影响正文措辞,不影响跳闸判据**(阈值仍按 run 数判)。
284115
+ workId: engineWorkIdOf(r.metadata)
282900
284116
  }));
282901
284117
  };
282902
284118
  const latestClosingRemark = async (runId) => {
@@ -285433,7 +286649,7 @@ async function startNode(opts) {
285433
286649
  }
285434
286650
 
285435
286651
  // ../cli/src/exec.ts
285436
- var os11 = __toESM(require("node:os"), 1);
286652
+ var os12 = __toESM(require("node:os"), 1);
285437
286653
  var UnsafeExecArgvError = class extends Error {
285438
286654
  code = "ERR_UNSAFE_EXEC_ARGV";
285439
286655
  constructor(message) {
@@ -285470,7 +286686,7 @@ function planExec(opts) {
285470
286686
  function computeExitCode(code2, signal) {
285471
286687
  if (code2 !== null) return code2;
285472
286688
  if (signal) {
285473
- const num4 = os11.constants.signals[signal];
286689
+ const num4 = os12.constants.signals[signal];
285474
286690
  return 128 + (typeof num4 === "number" ? num4 : 1);
285475
286691
  }
285476
286692
  return 1;
@@ -285720,7 +286936,7 @@ var COMMAND_DECLS = {
285720
286936
  },
285721
286937
  "usage-ledger-backfill": {
285722
286938
  group: "\u57FA\u7840\u8BBE\u65BD",
285723
- usage: "oasis usage-ledger-backfill (--company <id> | --all) (--dry-run | --execute | --verify | --undo <manifestId>)",
286939
+ usage: "oasis usage-ledger-backfill (--company <id> | --all) (--dry-run | --execute | --verify | --undo <manifestId>) [--claude-resume-before <ISO>]",
285724
286940
  description: "\u7528\u91CF\u8D26\u672C\u5386\u53F2\u56DE\u586B\uFF08\u4F7F\u7528\u4E0E\u7EDF\u8BA1\uFF09\uFF1A\u8865\u7EC8\u503C\u7528\u91CF\u70B9\u3001runtime_started_at\u3001chat \u4F1A\u8BDD/\u8F6E\u6B21\uFF1B\u5E42\u7B49\uFF0C\u6539\u52A8\u5199\u5BA1\u8BA1\u8868\uFF0C\u53EF\u6309 manifest \u64A4\u9500\u3002\u4EE5 serve \u7684\u73AF\u5883\u5728\u90E8\u7F72\u673A\u4E0A\u6267\u884C\u3002",
285725
286941
  flags: [
285726
286942
  { name: "company", desc: "\u53EA\u5904\u7406\u8FD9\u5BB6\u516C\u53F8\u7684\u8D26\u672C\uFF08\u4ECD\u5728 legacy \u7684\u516C\u53F8 = \u5171\u4EAB\u8D26\u672C + \u672C\u516C\u53F8 chat \u53CD\u67E5\uFF09" },
@@ -285728,10 +286944,12 @@ var COMMAND_DECLS = {
285728
286944
  { name: "dry-run", desc: "\u53EA\u62A5\u6BCF\u4E2A\u8D26\u672C\u5404\u6B65\u5F85\u6539\u6761\u6570\u548C\u6837\u4F8B run id\uFF0C\u4E00\u884C\u4E0D\u5199", boolean: true },
285729
286945
  { name: "execute", desc: "\u5199\u5165\uFF1B\u7ED3\u675F\u65F6\u6253\u5370 manifestId", boolean: true },
285730
286946
  { name: "verify", desc: "\u6838\u5BF9 I6\uFF08\u7EC8\u503C\u70B9 = agent_runs.usage\u3001\u68C0\u67E5\u70B9\u4E0D\u8D85\u7EC8\u503C\uFF09\u4E0E\u542F\u52A8\u8BC1\u636E\uFF1B\u6709\u8FDD\u4F8B\u9000\u51FA\u7801 1", boolean: true },
285731
- { name: "undo", desc: "\u6309\u5BA1\u8BA1\u8868\u628A\u67D0\u4E2A manifestId \u7684\u6539\u52A8\u6062\u590D\u539F\u503C" }
286947
+ { name: "undo", desc: "\u6309\u5BA1\u8BA1\u8868\u628A\u67D0\u4E2A manifestId \u7684\u6539\u52A8\u6062\u590D\u539F\u503C" },
286948
+ { name: "claude-resume-before", desc: "\u52A0\u8DD1\u7B2C 4 \u6B65\uFF1A\u5F00\u8DD1\u65E9\u4E8E\u8FD9\u4E2A\u65F6\u523B\u7684 claude-code run\uFF0C\u628A\u7EED\u8DD1\u5E26\u8FDB\u6765\u7684\u6574\u6761\u4F1A\u8BDD\u7EBF\u7D2F\u8BA1\u503C\u6263\u56DE\u6210\u672C\u8F6E\u51C0\u503C\uFF08\u53D6\u4FEE\u597D\u7684\u8282\u70B9\u5305\u5728\u5168\u90E8\u8282\u70B9\u88C5\u4E0A\u7684\u65F6\u523B\uFF09" }
285732
286949
  ],
285733
286950
  examples: [
285734
286951
  "oasis usage-ledger-backfill --all --dry-run",
286952
+ "oasis usage-ledger-backfill --all --dry-run --claude-resume-before 2026-09-23T00:00:00Z",
285735
286953
  "oasis usage-ledger-backfill --all --execute",
285736
286954
  "oasis usage-ledger-backfill --all --verify"
285737
286955
  ]
@@ -288074,7 +289292,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
288074
289292
  }
288075
289293
  }
288076
289294
  }
288077
- var CLI_VERSION = "2.2.68" ? "2.2.68" : "dev";
289295
+ var CLI_VERSION = "2.2.70" ? "2.2.70" : "dev";
288078
289296
  function apiHeaders(token, company) {
288079
289297
  const headers = {
288080
289298
  authorization: `Bearer ${token}`,
@@ -288309,7 +289527,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
288309
289527
  const res = await fetch(`${httpBase2}/api/nodes/exchange`, {
288310
289528
  method: "POST",
288311
289529
  headers: { "content-type": "application/json" },
288312
- body: JSON.stringify({ enrollToken, nodeId, hostname: os12.hostname(), serverUrl, ...companyId ? { companyId } : {} })
289530
+ body: JSON.stringify({ enrollToken, nodeId, hostname: os13.hostname(), serverUrl, ...companyId ? { companyId } : {} })
288313
289531
  });
288314
289532
  if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
288315
289533
  const body2 = await res.json();
@@ -288342,7 +289560,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
288342
289560
  const binPath = path37.join(prefix, "bin", "oasis_test");
288343
289561
  const svcArgs = `node --server-ws ${serverUrl} --dir ${dir}${nodeName ? ` --name ${nodeName}` : ""}`;
288344
289562
  if (process.platform === "linux") {
288345
- const svcFile = path37.join(os12.homedir(), ".config/systemd/user/oasis-node.service");
289563
+ const svcFile = path37.join(os13.homedir(), ".config/systemd/user/oasis-node.service");
288346
289564
  fs42.mkdirSync(path37.dirname(svcFile), { recursive: true });
288347
289565
  if (spawnSync3("systemctl", ["--user", "is-active", "--quiet", "oasis-node"], {}).status === 0)
288348
289566
  spawnSync3("systemctl", ["--user", "stop", "oasis-node"], { stdio: "inherit" });
@@ -288355,7 +289573,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
288355
289573
  `ExecStart=${binPath} ${svcArgs}`,
288356
289574
  // Carry the install-time PATH so the detached daemon detects CLIs in user dirs
288357
289575
  // (~/.npm-global/bin, ~/.local/bin) — systemd --user otherwise gives a minimal PATH.
288358
- `Environment=PATH=${process.env["PATH"] ?? ""}:${path37.join(os12.homedir(), ".npm-global/bin")}:${path37.join(os12.homedir(), ".local/bin")}:/usr/local/bin`,
289576
+ `Environment=PATH=${process.env["PATH"] ?? ""}:${path37.join(os13.homedir(), ".npm-global/bin")}:${path37.join(os13.homedir(), ".local/bin")}:/usr/local/bin`,
288359
289577
  "Restart=on-failure",
288360
289578
  "RestartSec=5",
288361
289579
  "StandardOutput=journal",
@@ -288367,23 +289585,23 @@ async function runCli(argv, println = console.log, progressln = console.error) {
288367
289585
  ].join("\n"));
288368
289586
  spawnSync3("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
288369
289587
  spawnSync3("systemctl", ["--user", "enable", "--now", "oasis-node"], { stdio: "inherit" });
288370
- spawnSync3("loginctl", ["enable-linger", os12.userInfo().username], { stdio: "ignore" });
289588
+ spawnSync3("loginctl", ["enable-linger", os13.userInfo().username], { stdio: "ignore" });
288371
289589
  println("\u2713 Oasis node service started (systemd --user oasis-node)");
288372
289590
  println(" Status : systemctl --user status oasis-node");
288373
289591
  println(" Logs : journalctl --user -u oasis-node -f");
288374
289592
  println(" Stop : systemctl --user stop oasis-node");
288375
289593
  } else if (process.platform === "darwin") {
288376
- const plist = path37.join(os12.homedir(), "Library/LaunchAgents/com.oasis.node.plist");
289594
+ const plist = path37.join(os13.homedir(), "Library/LaunchAgents/com.oasis.node.plist");
288377
289595
  fs42.mkdirSync(path37.dirname(plist), { recursive: true });
288378
289596
  spawnSync3("launchctl", ["unload", plist], { stdio: "ignore" });
288379
289597
  const argXml = [binPath, "node", "--server-ws", serverUrl, "--dir", dir, ...nodeName ? ["--name", nodeName] : []].map((a) => `<string>${a}</string>`).join("");
288380
- fs42.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.oasis.node</string><key>ProgramArguments</key><array>${argXml}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>${os12.homedir()}/.oasis-node.log</string><key>StandardErrorPath</key><string>${os12.homedir()}/.oasis-node.err</string></dict></plist>`);
289598
+ fs42.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.oasis.node</string><key>ProgramArguments</key><array>${argXml}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>${os13.homedir()}/.oasis-node.log</string><key>StandardErrorPath</key><string>${os13.homedir()}/.oasis-node.err</string></dict></plist>`);
288381
289599
  spawnSync3("launchctl", ["load", plist], { stdio: "inherit" });
288382
289600
  println("\u2713 Oasis node service started (macOS LaunchAgent com.oasis.node)");
288383
- println(` Logs : tail -f ${os12.homedir()}/.oasis-node.log`);
289601
+ println(` Logs : tail -f ${os13.homedir()}/.oasis-node.log`);
288384
289602
  println(` Stop : launchctl unload ${plist}`);
288385
289603
  } else {
288386
- spawnSync3("sh", ["-c", `nohup ${binPath} ${svcArgs} > ${path37.join(os12.homedir(), ".oasis-node.log")} 2>&1 &`], { stdio: "inherit" });
289604
+ spawnSync3("sh", ["-c", `nohup ${binPath} ${svcArgs} > ${path37.join(os13.homedir(), ".oasis-node.log")} 2>&1 &`], { stdio: "inherit" });
288387
289605
  println(`\u2713 Oasis node started in background (nohup). Logs: ~/.oasis-node.log`);
288388
289606
  println(" Note: no automatic restart on reboot in this environment.");
288389
289607
  }
@@ -288492,8 +289710,8 @@ async function runCli(argv, println = console.log, progressln = console.error) {
288492
289710
  if (command2 === "serve" && positional[0] === "install") {
288493
289711
  const absDir = path37.resolve(dir);
288494
289712
  const name = `oasis-serve-${path37.basename(absDir)}`;
288495
- const unitDir = flags.get("unit-dir") ?? path37.join(os12.homedir(), ".config", "systemd", "user");
288496
- const bin = flags.get("bin") ?? path37.join(os12.homedir(), ".local", "bin", "oasis");
289713
+ const unitDir = flags.get("unit-dir") ?? path37.join(os13.homedir(), ".config", "systemd", "user");
289714
+ const bin = flags.get("bin") ?? path37.join(os13.homedir(), ".local", "bin", "oasis");
288497
289715
  const port = flags.get("port") ?? "7320";
288498
289716
  const extra = (flags.get("dispatch") === "true" ? ` --dispatch true` : "") + (flags.has("model") ? ` --model ${flags.get("model")}` : "");
288499
289717
  const unit = [
@@ -288982,7 +290200,7 @@ Trace: ${run.traceRunId}`);
288982
290200
  if (plan.inject.mode === "env") {
288983
290201
  childEnv[plan.inject.envName] = value2;
288984
290202
  } else {
288985
- tmpDir = await fs42.promises.mkdtemp(path37.join(os12.tmpdir(), "oasis-var-exec-"));
290203
+ tmpDir = await fs42.promises.mkdtemp(path37.join(os13.tmpdir(), "oasis-var-exec-"));
288986
290204
  const keyfile = path37.join(tmpDir, "value");
288987
290205
  await fs42.promises.writeFile(keyfile, value2, { mode: 384 });
288988
290206
  childEnv[plan.inject.fileEnv] = keyfile;
@@ -289868,7 +291086,7 @@ ${res.warning}`);
289868
291086
  for (const a of st.artifacts) println(` - ${a.path}\uFF08${a.kind ?? "?"}\uFF09status=${a.status ?? "?"}${a.mtime ? ` \xB7 ${new Date(a.mtime).toISOString()}` : ""}`);
289869
291087
  if (flags.get("pull") === "true") {
289870
291088
  const odPublic2 = (flags.get("od-public-url") ?? process.env["OASIS_OPEN_DESIGN_PUBLIC_URL"] ?? "https://design.open-friday.com").replace(/\/$/, "");
289871
- const { dir: dir3, entry: entry2 } = await harvestBoard(odBase2, fetch, odPublic2, os12.tmpdir(), st.boardKey, st.runs[0]?.runId ?? "status-pull");
291089
+ const { dir: dir3, entry: entry2 } = await harvestBoard(odBase2, fetch, odPublic2, os13.tmpdir(), st.boardKey, st.runs[0]?.runId ?? "status-pull");
289872
291090
  println(`[design] \u5DF2\u62C9\u5230\u672C\u5730\uFF1A${dir3}\uFF08\u5165\u53E3 ${entry2}\uFF09`);
289873
291091
  println(`[design] \u8BFB\u5B8C\u6EE1\u610F\u5373\u4EA4\u7A3F\uFF1Aoasis propose ${artifactId2} --from-dir ${dir3} --reason "<\u8FD9\u7248\u505A\u4E86\u4EC0\u4E48>"`);
289874
291092
  } else {
@@ -291676,10 +292894,10 @@ function runtimeAssetSourceRoots(input) {
291676
292894
 
291677
292895
  // src/paths.ts
291678
292896
  var fs46 = __toESM(require("node:fs"));
291679
- var os13 = __toESM(require("node:os"));
292897
+ var os14 = __toESM(require("node:os"));
291680
292898
  var path41 = __toESM(require("node:path"));
291681
292899
  var DEFAULT_INSTANCE = "default";
291682
- var home = () => os13.homedir();
292900
+ var home = () => os14.homedir();
291683
292901
  var oasisDir = () => path41.join(home(), ".oasis");
291684
292902
  var versionsDir = () => path41.join(oasisDir(), "versions");
291685
292903
  var versionDir = (label2) => path41.join(versionsDir(), label2);
@@ -291880,7 +293098,7 @@ function shimScript() {
291880
293098
  }
291881
293099
 
291882
293100
  // src/index.ts
291883
- var PKG_VERSION = true ? "2.2.68" : "dev";
293101
+ var PKG_VERSION = true ? "2.2.70" : "dev";
291884
293102
  var LOCAL_BIN = localBin();
291885
293103
  var NPM_PREFIX = npmPrefix();
291886
293104
  var INSTANCE = DEFAULT_INSTANCE;
@@ -291986,7 +293204,7 @@ function installSoftware(label2) {
291986
293204
  function setupAutostart(name) {
291987
293205
  const inst = instancePaths(name);
291988
293206
  const cmd = `${process.execPath} "${inst.binFile}" --_daemon --instance ${name}`;
291989
- const home2 = os14.homedir();
293207
+ const home2 = os15.homedir();
291990
293208
  const pnpmHome = process.env["PNPM_HOME"] || path43.join(home2, ".local", "share", "pnpm");
291991
293209
  const extraBins = [
291992
293210
  path43.join(home2, ".npm-global", "bin"),
@@ -291996,7 +293214,7 @@ function setupAutostart(name) {
291996
293214
  ];
291997
293215
  const daemonPath = sanitizeDaemonPath([process.env["PATH"] ?? "", ...extraBins].filter(Boolean).join(":"));
291998
293216
  if (process.platform === "linux") {
291999
- const unitDir = path43.join(os14.homedir(), ".config", "systemd", "user");
293217
+ const unitDir = path43.join(os15.homedir(), ".config", "systemd", "user");
292000
293218
  fs48.mkdirSync(unitDir, { recursive: true });
292001
293219
  const unit = systemdUnitName(name);
292002
293220
  fs48.mkdirSync(path43.dirname(inst.logFile), { recursive: true });
@@ -292021,7 +293239,7 @@ function setupAutostart(name) {
292021
293239
  return false;
292022
293240
  } else if (process.platform === "darwin") {
292023
293241
  const label2 = launchdLabel(name);
292024
- const plist = path43.join(os14.homedir(), "Library", "LaunchAgents", `${label2}.plist`);
293242
+ const plist = path43.join(os15.homedir(), "Library", "LaunchAgents", `${label2}.plist`);
292025
293243
  fs48.mkdirSync(path43.dirname(plist), { recursive: true });
292026
293244
  fs48.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
292027
293245
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -292203,7 +293421,7 @@ async function startOneInstance(o) {
292203
293421
  const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
292204
293422
  method: "POST",
292205
293423
  headers: { "content-type": "application/json" },
292206
- body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os14.hostname(), serverUrl, ...companyId ? { companyId } : {} })
293424
+ body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os15.hostname(), serverUrl, ...companyId ? { companyId } : {} })
292207
293425
  });
292208
293426
  if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
292209
293427
  const body2 = await res.json();
@@ -292268,7 +293486,7 @@ function derivedNodeIdFor(name) {
292268
293486
  if (name === DEFAULT_INSTANCE) return resolveNodeId();
292269
293487
  const user = (() => {
292270
293488
  try {
292271
- return os14.userInfo().username;
293489
+ return os15.userInfo().username;
292272
293490
  } catch {
292273
293491
  return process.env["USER"] ?? process.env["USERNAME"] ?? "unknown";
292274
293492
  }