oasis_test_v2 2.2.30 → 2.2.32

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 +1016 -357
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1295,11 +1295,12 @@ var init_chat_session = __esm({
1295
1295
  });
1296
1296
 
1297
1297
  // ../contract/src/live-protocol.ts
1298
- var NORMALIZED_PROVIDER_EVENT_VERSION;
1298
+ var NORMALIZED_PROVIDER_EVENT_VERSION, SUBAGENT_TASK_CODE;
1299
1299
  var init_live_protocol = __esm({
1300
1300
  "../contract/src/live-protocol.ts"() {
1301
1301
  "use strict";
1302
1302
  NORMALIZED_PROVIDER_EVENT_VERSION = 1;
1303
+ SUBAGENT_TASK_CODE = "subagent_task";
1303
1304
  }
1304
1305
  });
1305
1306
 
@@ -23127,14 +23128,132 @@ var init_filter_extra_args = __esm({
23127
23128
  });
23128
23129
 
23129
23130
  // ../adapters/src/claude-code/index.ts
23130
- function createClaudeLiveParserState(streamId) {
23131
+ function createClaudeLiveParserState(streamId, opts = {}) {
23131
23132
  return {
23132
23133
  streamId,
23133
23134
  turnNumber: 0,
23134
23135
  turnId: `claude-turn:${streamId}:0`,
23135
- blockIds: /* @__PURE__ */ new Map()
23136
+ blockIds: /* @__PURE__ */ new Map(),
23137
+ agentTasks: /* @__PURE__ */ new Map(),
23138
+ subagentCalls: /* @__PURE__ */ new Map(),
23139
+ ...opts.readSubagentUsage ? { readSubagentUsage: opts.readSubagentUsage } : {}
23136
23140
  };
23137
23141
  }
23142
+ function numberOf(value2) {
23143
+ return typeof value2 === "number" && Number.isFinite(value2) ? value2 : void 0;
23144
+ }
23145
+ function subagentCallOf(message) {
23146
+ const id = stringOf(message?.id);
23147
+ const usage = parseResultUsage(message?.usage);
23148
+ if (!id || !usage) return void 0;
23149
+ return {
23150
+ id,
23151
+ call: {
23152
+ model: stringOf(message?.model) ?? "unknown",
23153
+ tokens: {
23154
+ inputTokens: usage.input ?? 0,
23155
+ outputTokens: usage.output ?? 0,
23156
+ cacheReadTokens: usage.cacheRead ?? 0,
23157
+ cacheCreationTokens: usage.cacheCreation ?? 0
23158
+ }
23159
+ }
23160
+ };
23161
+ }
23162
+ function recordSubagentCall(calls, id, call) {
23163
+ const prev = calls.get(id);
23164
+ calls.set(id, prev ? {
23165
+ model: call.model,
23166
+ tokens: {
23167
+ inputTokens: Math.max(prev.tokens.inputTokens, call.tokens.inputTokens),
23168
+ outputTokens: Math.max(prev.tokens.outputTokens, call.tokens.outputTokens),
23169
+ cacheReadTokens: Math.max(prev.tokens.cacheReadTokens, call.tokens.cacheReadTokens),
23170
+ cacheCreationTokens: Math.max(prev.tokens.cacheCreationTokens, call.tokens.cacheCreationTokens)
23171
+ }
23172
+ } : call);
23173
+ }
23174
+ function byModelOf(calls) {
23175
+ const out = {};
23176
+ for (const { model, tokens } of calls.values()) {
23177
+ const acc = out[model] ?? (out[model] = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 });
23178
+ acc.inputTokens += tokens.inputTokens;
23179
+ acc.outputTokens += tokens.outputTokens;
23180
+ acc.cacheReadTokens += tokens.cacheReadTokens;
23181
+ acc.cacheCreationTokens += tokens.cacheCreationTokens;
23182
+ }
23183
+ return out;
23184
+ }
23185
+ function readSubagentTranscriptUsage(outputFile) {
23186
+ try {
23187
+ const stat3 = fs7.statSync(outputFile);
23188
+ if (!stat3.isFile() || stat3.size > SUBAGENT_TRANSCRIPT_MAX_BYTES) return void 0;
23189
+ const calls = /* @__PURE__ */ new Map();
23190
+ for (const line of fs7.readFileSync(outputFile, "utf8").split("\n")) {
23191
+ if (!line.includes('"assistant"') || !line.includes('"usage"')) continue;
23192
+ let parsed;
23193
+ try {
23194
+ parsed = JSON.parse(line);
23195
+ } catch {
23196
+ continue;
23197
+ }
23198
+ if (parsed.type !== "assistant") continue;
23199
+ const hit = subagentCallOf(objectOf(parsed.message));
23200
+ if (hit) recordSubagentCall(calls, hit.id, hit.call);
23201
+ }
23202
+ return calls.size > 0 ? byModelOf(calls) : void 0;
23203
+ } catch {
23204
+ return void 0;
23205
+ }
23206
+ }
23207
+ function subagentTaskEvents(parsed, state) {
23208
+ const taskId = stringOf(parsed.task_id);
23209
+ if (!taskId) return [];
23210
+ const emit = (toolUseId, task) => [
23211
+ liveEvent(state, `${SUBAGENT_TASK_CODE}:${toolUseId}:${task.status}`, "control", "completed", {
23212
+ code: SUBAGENT_TASK_CODE,
23213
+ toolCallId: toolUseId,
23214
+ task: jsonOf(task)
23215
+ })
23216
+ ];
23217
+ if (parsed.subtype === "task_started") {
23218
+ const toolUseId = stringOf(parsed.tool_use_id);
23219
+ if (parsed.task_type !== "local_agent" || !toolUseId) return [];
23220
+ const description = stringOf(parsed.description);
23221
+ const subagentType = stringOf(parsed.subagent_type);
23222
+ const info = {
23223
+ ...description ? { description } : {},
23224
+ ...subagentType ? { subagentType } : {},
23225
+ background: parsed.is_backgrounded === true
23226
+ };
23227
+ state.agentTasks.set(taskId, { toolUseId, ...info });
23228
+ return emit(toolUseId, { status: "running", ...info });
23229
+ }
23230
+ if (parsed.subtype === "task_notification") {
23231
+ const known = state.agentTasks.get(taskId);
23232
+ if (!known) return [];
23233
+ state.agentTasks.delete(taskId);
23234
+ const { toolUseId, ...info } = known;
23235
+ const usage = objectOf(parsed.usage);
23236
+ const summary = stringOf(parsed.summary);
23237
+ const totalTokens = numberOf(usage?.total_tokens);
23238
+ const toolUses = numberOf(usage?.tool_uses);
23239
+ const durationMs3 = numberOf(usage?.duration_ms);
23240
+ const outputFile = stringOf(parsed.output_file);
23241
+ const fromTranscript = outputFile ? state.readSubagentUsage?.(outputFile) : void 0;
23242
+ const streamCalls = state.subagentCalls.get(toolUseId);
23243
+ state.subagentCalls.delete(toolUseId);
23244
+ const tokenUsage = fromTranscript && Object.keys(fromTranscript).length > 0 ? { byModel: fromTranscript, source: "transcript" } : streamCalls?.size ? { byModel: byModelOf(streamCalls), source: "stream" } : void 0;
23245
+ return emit(toolUseId, {
23246
+ ...info,
23247
+ status: SUBAGENT_TERMINAL[stringOf(parsed.status) ?? ""] ?? "completed",
23248
+ ...summary ? { summary } : {},
23249
+ ...totalTokens !== void 0 ? { totalTokens } : {},
23250
+ ...toolUses !== void 0 ? { toolUses } : {},
23251
+ ...durationMs3 !== void 0 ? { durationMs: durationMs3 } : {},
23252
+ ...tokenUsage ? { usage: tokenUsage } : {}
23253
+ });
23254
+ }
23255
+ return [];
23256
+ }
23138
23257
  function objectOf(value2) {
23139
23258
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2) ? value2 : void 0;
23140
23259
  }
@@ -23167,6 +23286,18 @@ function normalizeClaudeLiveLine(line, state) {
23167
23286
  const failed = parsed.is_error === true || parsed.subtype === "error";
23168
23287
  return [liveEvent(state, itemId, "control", failed ? "turn_failed" : "turn_completed", jsonOf(parsed))];
23169
23288
  }
23289
+ if (parsed.type === "system") return subagentTaskEvents(parsed, state);
23290
+ if (parsed.type === "assistant") {
23291
+ const parent = stringOf(parsed.parent_tool_use_id);
23292
+ const hit = parent ? subagentCallOf(objectOf(parsed.message)) : void 0;
23293
+ if (parent && hit) {
23294
+ const calls = state.subagentCalls.get(parent) ?? /* @__PURE__ */ new Map();
23295
+ recordSubagentCall(calls, hit.id, hit.call);
23296
+ state.subagentCalls.set(parent, calls);
23297
+ }
23298
+ return [];
23299
+ }
23300
+ if ((parsed.type === "user" || parsed.type === "stream_event") && stringOf(parsed.parent_tool_use_id)) return [];
23170
23301
  if (parsed.type === "user") {
23171
23302
  const content3 = Array.isArray(objectOf(parsed.message)?.content) ? objectOf(parsed.message).content : [];
23172
23303
  return content3.flatMap((block) => {
@@ -23237,19 +23368,21 @@ function normalizeClaudeStreamLine(line) {
23237
23368
  const message = parsed["message"];
23238
23369
  const content3 = Array.isArray(message?.content) ? message.content : [];
23239
23370
  const out = [];
23371
+ const parentToolCallId = stringOf(parsed["parent_tool_use_id"]);
23372
+ const sub = parentToolCallId ? { parentToolCallId } : {};
23240
23373
  for (const block of content3) {
23241
23374
  switch (block["type"]) {
23242
23375
  case "text":
23243
- if (type === "assistant") out.push({ kind: "message", payload: { text: block["text"] } });
23376
+ if (type === "assistant") out.push({ kind: "message", payload: { text: block["text"], ...sub } });
23244
23377
  break;
23245
23378
  case "thinking":
23246
- out.push({ kind: "thought", payload: { text: block["thinking"] } });
23379
+ out.push({ kind: "thought", payload: { text: block["thinking"], ...sub } });
23247
23380
  break;
23248
23381
  case "tool_use":
23249
- out.push({ kind: "tool_use", payload: { id: block["id"], name: block["name"], input: block["input"] } });
23382
+ out.push({ kind: "tool_use", payload: { id: block["id"], name: block["name"], input: block["input"], ...sub } });
23250
23383
  break;
23251
23384
  case "tool_result":
23252
- out.push({ kind: "tool_result", payload: { tool_use_id: block["tool_use_id"], content: block["content"], is_error: block["is_error"] ?? false } });
23385
+ out.push({ kind: "tool_result", payload: { tool_use_id: block["tool_use_id"], content: block["content"], is_error: block["is_error"] ?? false, ...sub } });
23253
23386
  break;
23254
23387
  default:
23255
23388
  break;
@@ -23350,7 +23483,7 @@ function accumulateClaudeUsage(acc, p2) {
23350
23483
  acc.saw = true;
23351
23484
  }
23352
23485
  } else if (type === "result") {
23353
- acc.finalUsage = mergeFinalUsage(parseResultUsage(p2["usage"]), parseModelUsage(p2["modelUsage"]));
23486
+ acc.finalUsage = mergeFinalUsage(parseModelUsage(p2["modelUsage"]), parseResultUsage(p2["usage"]));
23354
23487
  const c = p2["total_cost_usd"];
23355
23488
  if (typeof c === "number" && Number.isFinite(c)) {
23356
23489
  acc.costUsd = c;
@@ -23400,7 +23533,7 @@ function classifyExit(result) {
23400
23533
  if (status === 429 || status === 529) return "rate-limit";
23401
23534
  return "error";
23402
23535
  }
23403
- var import_node_child_process3, import_node_crypto8, fs7, path7, readline, CLAUDE_BLOCKED_FLAGS, ONE_SHOT_UNSAFE_TOOLS, ClaudeCodeAdapter;
23536
+ var import_node_child_process3, import_node_crypto8, fs7, path7, readline, CLAUDE_BLOCKED_FLAGS, SUBAGENT_TRANSCRIPT_MAX_BYTES, SUBAGENT_TERMINAL, ONE_SHOT_UNSAFE_TOOLS, ClaudeCodeAdapter;
23404
23537
  var init_claude_code = __esm({
23405
23538
  "../adapters/src/claude-code/index.ts"() {
23406
23539
  "use strict";
@@ -23415,6 +23548,7 @@ var init_claude_code = __esm({
23415
23548
  init_resume_classify();
23416
23549
  init_root_bypass();
23417
23550
  init_session_paths();
23551
+ init_src();
23418
23552
  init_transcript();
23419
23553
  init_filter_extra_args();
23420
23554
  CLAUDE_BLOCKED_FLAGS = /* @__PURE__ */ new Map([
@@ -23445,6 +23579,13 @@ var init_claude_code = __esm({
23445
23579
  ["--include-partial-messages", "boolean"]
23446
23580
  // 同上
23447
23581
  ]);
23582
+ SUBAGENT_TRANSCRIPT_MAX_BYTES = 32 * 1024 * 1024;
23583
+ SUBAGENT_TERMINAL = {
23584
+ completed: "completed",
23585
+ failed: "failed",
23586
+ killed: "stopped",
23587
+ stopped: "stopped"
23588
+ };
23448
23589
  ONE_SHOT_UNSAFE_TOOLS = ["ScheduleWakeup", "CronCreate", "CronDelete", "CronList"];
23449
23590
  ClaudeCodeAdapter = class {
23450
23591
  constructor(opts = {}) {
@@ -23670,7 +23811,7 @@ var init_claude_code = __esm({
23670
23811
  const usageAcc = newClaudeUsageAcc();
23671
23812
  let sessionModel;
23672
23813
  const outputCbs = [];
23673
- const liveParser = createClaudeLiveParserState(`claude-live:${id}`);
23814
+ const liveParser = createClaudeLiveParserState(`claude-live:${id}`, { readSubagentUsage: readSubagentTranscriptUsage });
23674
23815
  if (this.opts.streamJson) {
23675
23816
  const rl = readline.createInterface({ input: child.stdout });
23676
23817
  rl.on("line", (line) => {
@@ -28919,6 +29060,14 @@ var init_keys = __esm({
28919
29060
  });
28920
29061
 
28921
29062
  // ../adapters/src/_core/normalizer/normalizer.ts
29063
+ function parentToolCallIdOf(payload) {
29064
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return void 0;
29065
+ const id = payload.parentToolCallId;
29066
+ return typeof id === "string" && id !== "" ? id : void 0;
29067
+ }
29068
+ function textBlocksOf(blocks) {
29069
+ return blocks.map((b2) => b2 && typeof b2 === "object" && typeof b2.text === "string" ? b2.text : "").filter(Boolean).join("\n");
29070
+ }
28922
29071
  var ProviderStreamNormalizer;
28923
29072
  var init_normalizer = __esm({
28924
29073
  "../adapters/src/_core/normalizer/normalizer.ts"() {
@@ -28955,6 +29104,8 @@ var init_normalizer = __esm({
28955
29104
  producedAny = false;
28956
29105
  /** Receipts wait for a whole output block; they never split its bytes. */
28957
29106
  pendingInputs = /* @__PURE__ */ new Map();
29107
+ /** 子代理正文/思考块的序号(它们没有 provider id)。严格递增、turn 收尾清零。 */
29108
+ subagentBlocks = 0;
28958
29109
  constructor(opts) {
28959
29110
  this.providerName = opts.providerName;
28960
29111
  this.hasLiveProtocol = opts.hasLiveProtocol === true;
@@ -28975,6 +29126,11 @@ var init_normalizer = __esm({
28975
29126
  }
28976
29127
  /** 遥测事件。 */
28977
29128
  onTelemetry(event) {
29129
+ const parentToolCallId = parentToolCallIdOf(event.payload);
29130
+ if (parentToolCallId) {
29131
+ this.subagentItem(event, parentToolCallId);
29132
+ return;
29133
+ }
28978
29134
  if (this.hasLiveProtocol && event.kind !== "tool_use") return;
28979
29135
  if (event.kind === "message" && this.sawTextOutput) return;
28980
29136
  const part = chatPartFromTrajectoryEvent(event);
@@ -29008,6 +29164,17 @@ var init_normalizer = __esm({
29008
29164
  this.closeOpen("completed");
29009
29165
  return;
29010
29166
  }
29167
+ if (event.itemType === "control" && payload.code === SUBAGENT_TASK_CODE && typeof payload.toolCallId === "string" && payload.task && typeof payload.task === "object") {
29168
+ this.emitItem({
29169
+ key: providerItemKey(this.providerName, event.itemId),
29170
+ kind: "control",
29171
+ role: "assistant",
29172
+ origin: "live",
29173
+ opShape: { op: "set_status", status: "completed" },
29174
+ attrs: { code: SUBAGENT_TASK_CODE, toolCallId: payload.toolCallId, task: payload.task }
29175
+ });
29176
+ return;
29177
+ }
29011
29178
  this.turnId = event.turnId || this.turnId;
29012
29179
  if (event.operation === "turn_completed" || event.operation === "turn_failed") {
29013
29180
  this.turnFinished(event.operation === "turn_failed" ? "failed" : "completed");
@@ -29041,11 +29208,52 @@ var init_normalizer = __esm({
29041
29208
  }
29042
29209
  if (event.operation === "completed" && event.itemType === "tool" && event.payload !== void 0) {
29043
29210
  const id = typeof payload.tool_use_id === "string" ? payload.tool_use_id : event.itemId;
29044
- const output = typeof payload.content === "string" ? payload.content : typeof payload.output === "string" ? payload.output : "";
29211
+ const output = typeof payload.content === "string" ? payload.content : Array.isArray(payload.content) ? textBlocksOf(payload.content) : typeof payload.output === "string" ? payload.output : "";
29045
29212
  this.toolResult("live", id, void 0, output, payload.is_error === true);
29046
29213
  return;
29047
29214
  }
29048
29215
  }
29216
+ /**
29217
+ * 子代理内部的一块(带 parentToolCallId 的 telemetry)。子代理没有 partial stream,每块到达时
29218
+ * 就是完整的,所以不走散文的「开一段 → 续写」,也不过认领闸、不关主线程开着的那段——后台子代理
29219
+ * 与主线程并发时,主线程正文不能被它切成几截。
29220
+ */
29221
+ subagentItem(event, parentToolCallId) {
29222
+ const part = chatPartFromTrajectoryEvent(event);
29223
+ if (!part) return;
29224
+ const sub = { parentToolCallId };
29225
+ if (part.type === "text" || part.type === "thinking") {
29226
+ const kind = part.type;
29227
+ const key = providerItemKey(this.providerName, `sub:${parentToolCallId}:${kind}`, this.subagentBlocks++);
29228
+ this.emitItem({ key, kind, role: "assistant", origin: "telemetry", opShape: { op: "set_text", text: part.text ?? "" }, attrs: sub });
29229
+ this.emitItem({ key, kind, role: "assistant", origin: "telemetry", opShape: { op: "set_status", status: "completed" }, attrs: sub });
29230
+ return;
29231
+ }
29232
+ if (part.type === "tool_use") {
29233
+ const id = part.toolCallId;
29234
+ this.emitItem({
29235
+ key: id ? providerItemKey(this.providerName, `tool_use:${id}`) : this.nextFallbackKey("tool_call"),
29236
+ kind: "tool_call",
29237
+ role: "assistant",
29238
+ origin: "telemetry",
29239
+ opShape: { op: "patch_input", input: part.input ?? null },
29240
+ attrs: { toolName: part.toolName ?? "tool", ...id ? { toolCallId: id } : {}, ...sub }
29241
+ });
29242
+ return;
29243
+ }
29244
+ if (part.type === "tool_result") {
29245
+ const id = part.toolCallId;
29246
+ const output = part.output ?? "";
29247
+ this.emitItem({
29248
+ key: id ? providerItemKey(this.providerName, `tool_result:${id}`) : this.nextFallbackKey("tool_result"),
29249
+ kind: "tool_result",
29250
+ role: "assistant",
29251
+ origin: "telemetry",
29252
+ opShape: { op: "set_text", text: output },
29253
+ attrs: { output, isError: part.isError === true, ...id ? { toolCallId: id } : {}, ...sub }
29254
+ });
29255
+ }
29256
+ }
29049
29257
  // ── turn 生命周期 ──────────────────────────────────────────────────────────
29050
29258
  /** turn 收尾:关掉开着的散文、清兜底 index、报一条 turn 信号(服务端据此清 itemKeyMap)。 */
29051
29259
  turnFinished(signal) {
@@ -29057,6 +29265,7 @@ var init_normalizer = __esm({
29057
29265
  this.closedKeys.clear();
29058
29266
  this.sawTextOutput = false;
29059
29267
  this.producedAny = false;
29268
+ this.subagentBlocks = 0;
29060
29269
  if (!produced) return;
29061
29270
  this.emit({ normalizedVersion: NORMALIZED_PROVIDER_EVENT_VERSION, type: "turn", turnId: this.turnId, signal });
29062
29271
  }
@@ -29282,6 +29491,341 @@ var init_src6 = __esm({
29282
29491
  }
29283
29492
  });
29284
29493
 
29494
+ // ../server/src/domains/actors/model-pricing.ts
29495
+ function lookupInMap(map, prefixKeys, candidates) {
29496
+ for (const candidate of candidates) {
29497
+ const exact = map instanceof Map ? map.get(candidate) : map[candidate];
29498
+ if (exact) return exact;
29499
+ }
29500
+ const prefix = prefixKeys.find((p2) => candidates.some((candidate) => candidate.startsWith(`${p2}-`) || candidate.startsWith(`${p2}:`)));
29501
+ if (!prefix) return null;
29502
+ return map instanceof Map ? map.get(prefix) ?? null : map[prefix] ?? null;
29503
+ }
29504
+ function stripBracketTier(model) {
29505
+ return model.replace(/\[[^\]]*\]/g, "").trim();
29506
+ }
29507
+ function modelCandidates(model) {
29508
+ const normalized4 = model.trim().toLowerCase();
29509
+ if (!normalized4) return [];
29510
+ const base = [
29511
+ normalized4,
29512
+ normalized4.includes(":") ? normalized4.slice(normalized4.lastIndexOf(":") + 1) : "",
29513
+ normalized4.includes("/") ? normalized4.slice(normalized4.lastIndexOf("/") + 1) : ""
29514
+ ].filter(Boolean);
29515
+ const stripped = base.map(stripBracketTier).filter((m2) => m2 && !base.includes(m2));
29516
+ return [...base, ...new Set(stripped)];
29517
+ }
29518
+ function setModelPriceCacheLoader(loader) {
29519
+ cacheLoader = loader;
29520
+ }
29521
+ async function warmModelPriceCache() {
29522
+ if (!cacheLoader) return 0;
29523
+ try {
29524
+ const next = await cacheLoader();
29525
+ dbPrices = next;
29526
+ dbPrefixKeys = [...next.keys()].sort((a, b2) => b2.length - a.length);
29527
+ cacheLoadedAt = Date.now();
29528
+ return next.size;
29529
+ } catch (err) {
29530
+ console.warn(`[model-pricing] \u9884\u70ED\u4EF7\u683C\u7F13\u5B58\u5931\u8D25\uFF0C\u6CBF\u7528\u9759\u6001\u5FEB\u7167\uFF1A${String(err)}`);
29531
+ return 0;
29532
+ }
29533
+ }
29534
+ function maybeRefreshCache() {
29535
+ if (!cacheLoader) return;
29536
+ if (dbPrices && Date.now() - cacheLoadedAt < CACHE_TTL_MS2) return;
29537
+ if (cacheRefreshInFlight) return;
29538
+ cacheRefreshInFlight = warmModelPriceCache().catch(() => 0).then(() => {
29539
+ cacheRefreshInFlight = null;
29540
+ });
29541
+ }
29542
+ function priceForModel(model) {
29543
+ if (!model) return null;
29544
+ const candidates = modelCandidates(model);
29545
+ if (candidates.length === 0) return null;
29546
+ maybeRefreshCache();
29547
+ if (dbPrices && dbPrices.size > 0) {
29548
+ const fromDb = lookupInMap(dbPrices, dbPrefixKeys, candidates);
29549
+ if (fromDb) return fromDb;
29550
+ }
29551
+ return lookupInMap(MODEL_PRICES, STATIC_PREFIX_PRICES, candidates);
29552
+ }
29553
+ function estimateUsageCostBreakdown(model, usage) {
29554
+ const price = priceForModel(model);
29555
+ if (!price) return null;
29556
+ const inputTokens = Math.max(usage.inputTokens ?? 0, 0);
29557
+ const outputTokens = Math.max(usage.outputTokens ?? 0, 0);
29558
+ const cacheReadTokens = Math.max(usage.cacheReadTokens ?? 0, 0);
29559
+ const cacheCreationTokens = Math.max(usage.cacheCreationTokens ?? 0, 0);
29560
+ const cacheReadPrice = price.cachedInputPerMillion ?? price.inputPerMillion * 0.1;
29561
+ return {
29562
+ currency: price.currency,
29563
+ inputMicros: Math.round(inputTokens * price.inputPerMillion / 1e6 * 1e6),
29564
+ cacheMicros: Math.round(
29565
+ (cacheCreationTokens * price.inputPerMillion + cacheReadTokens * cacheReadPrice) / 1e6 * 1e6
29566
+ ),
29567
+ outputMicros: Math.round(outputTokens * price.outputPerMillion / 1e6 * 1e6)
29568
+ };
29569
+ }
29570
+ function estimateUsageCost(model, usage) {
29571
+ const breakdown = estimateUsageCostBreakdown(model, usage);
29572
+ if (!breakdown) return null;
29573
+ return {
29574
+ currency: breakdown.currency,
29575
+ micros: breakdown.inputMicros + breakdown.cacheMicros + breakdown.outputMicros
29576
+ };
29577
+ }
29578
+ function displayCostUsdMicros(model, usage) {
29579
+ const stored = Math.max(usage.costUsdMicros ?? 0, 0);
29580
+ if (stored > 0) return stored;
29581
+ const estimated = estimateUsageCost(model, usage);
29582
+ if (!estimated || estimated.micros <= 0) return 0;
29583
+ if (estimated.currency === "USD") return estimated.micros;
29584
+ return Math.round(estimated.micros / DISPLAY_CNY_PER_USD);
29585
+ }
29586
+ var FOREIGN_MODEL_MARKUP, usd, cny, MODEL_PRICES, STATIC_PREFIX_PRICES, CACHE_TTL_MS2, dbPrices, dbPrefixKeys, cacheLoadedAt, cacheLoader, cacheRefreshInFlight;
29587
+ var init_model_pricing = __esm({
29588
+ "../server/src/domains/actors/model-pricing.ts"() {
29589
+ "use strict";
29590
+ init_src();
29591
+ FOREIGN_MODEL_MARKUP = 1.08;
29592
+ usd = (inputPerMillion, outputPerMillion, cachedInputPerMillion) => ({
29593
+ currency: "USD",
29594
+ inputPerMillion: inputPerMillion * FOREIGN_MODEL_MARKUP,
29595
+ outputPerMillion: outputPerMillion * FOREIGN_MODEL_MARKUP,
29596
+ ...cachedInputPerMillion !== void 0 ? { cachedInputPerMillion: cachedInputPerMillion * FOREIGN_MODEL_MARKUP } : {}
29597
+ });
29598
+ cny = (inputPerMillion, outputPerMillion, cachedInputPerMillion) => ({
29599
+ currency: "CNY",
29600
+ inputPerMillion,
29601
+ outputPerMillion,
29602
+ ...cachedInputPerMillion !== void 0 ? { cachedInputPerMillion } : {}
29603
+ });
29604
+ MODEL_PRICES = {
29605
+ "gpt-5.5": usd(5, 30, 0.5),
29606
+ "gpt-5.5-pro": usd(30, 180),
29607
+ "chat-latest": usd(5, 30, 0.5),
29608
+ "gpt-5.4-mini": usd(0.75, 4.5, 0.075),
29609
+ "gpt-5.4-nano": usd(0.2, 1.25, 0.02),
29610
+ "gpt-5.4": usd(2.5, 15, 0.25),
29611
+ "gpt-5.4-pro": usd(30, 180),
29612
+ "gpt-5.3-codex": usd(1.75, 14, 0.175),
29613
+ "gpt-5.2": usd(1.75, 14, 0.175),
29614
+ "gpt-5.2-chat-latest": usd(1.75, 14, 0.175),
29615
+ "gpt-5.2-pro": usd(21, 168),
29616
+ "gpt-5.2-codex": usd(1.75, 14, 0.175),
29617
+ "gpt-5.1": usd(1.25, 10, 0.125),
29618
+ "gpt-5.1-chat-latest": usd(1.25, 10, 0.125),
29619
+ "gpt-5.1-codex": usd(1.25, 10, 0.125),
29620
+ "gpt-5.1-codex-max": usd(1.25, 10, 0.125),
29621
+ "gpt-5.1-codex-mini": usd(0.25, 2, 0.025),
29622
+ "gpt-5-pro": usd(15, 120),
29623
+ "gpt-5-codex": usd(1.25, 10, 0.125),
29624
+ "gpt-5-search-api": usd(1.25, 10, 0.125),
29625
+ "gpt-5": usd(1.25, 10, 0.125),
29626
+ "gpt-5-mini": usd(0.25, 2, 0.025),
29627
+ "gpt-5-nano": usd(0.05, 0.4, 5e-3),
29628
+ "gpt-5-chat-latest": usd(1.25, 10, 0.125),
29629
+ "gpt-4.1": usd(2, 8, 0.5),
29630
+ "gpt-4.1-2025-04-14": usd(2, 8, 0.5),
29631
+ "gpt-4.1-mini": usd(0.4, 1.6, 0.1),
29632
+ "gpt-4.1-mini-2025-04-14": usd(0.4, 1.6, 0.1),
29633
+ "gpt-4.1-nano": usd(0.1, 0.4, 0.025),
29634
+ "gpt-4.1-nano-2025-04-14": usd(0.1, 0.4, 0.025),
29635
+ "o4-mini": usd(1.1, 4.4, 0.275),
29636
+ // o3 / o3-mini 在 codex 静态目录里可选,却一直不在价目快照里(费用记 0)。
29637
+ // 这两行按 OpenAI 公开价填,不是智增增快照里的行;供应商价目表同步到时以 DB 为准。
29638
+ "o3": usd(2, 8),
29639
+ "o3-mini": usd(1.1, 4.4),
29640
+ "claude-fable-5-1": usd(10, 50),
29641
+ "claude-fable-5": usd(10, 50),
29642
+ "claude-mythos-5-1": usd(10, 50),
29643
+ "claude-mythos-5": usd(10, 50),
29644
+ // claude-opus-5 是 Claude Code 当代默认款,却一直不在这张快照里:查不到价 → 费用记 0
29645
+ // (与 2026-09-04 那次 `claude-opus-5[1m]` 记 0 同一类坑)。按 Opus 档 $5/$25 补上。
29646
+ "claude-opus-5": usd(5, 25),
29647
+ "claude-opus-4-8": usd(5, 25),
29648
+ "claude-opus-4-7": usd(5, 25),
29649
+ "claude-sonnet-5": usd(2, 10),
29650
+ "claude-sonnet-4-6": usd(3, 15),
29651
+ "claude-opus-4-6": usd(5, 25),
29652
+ "claude-opus-4-5-20251101": usd(5, 25),
29653
+ "claude-opus-4-5": usd(5, 25),
29654
+ "claude-sonnet-4-5-20250929": usd(3, 15),
29655
+ // 短名同样要有:前缀比对是「表里的键是模型名的前缀」,带日期的那行匹配不上不带日期的短名。
29656
+ // claude-sonnet-4-5 从 2026-07-04 起就在目录里可选,却一直查不到价(费用记 0),本次一并补。
29657
+ "claude-sonnet-4-5": usd(3, 15),
29658
+ "claude-opus-4-1-20250805": usd(15, 75),
29659
+ "claude-opus-4-20250514": usd(15, 75),
29660
+ "claude-sonnet-4-20250514": usd(3, 15),
29661
+ "claude-3-7-sonnet-20250219": usd(3, 15),
29662
+ "claude-3-5-sonnet-20241022": usd(3, 15),
29663
+ "claude-3-5-sonnet-20240620": usd(3, 15),
29664
+ "claude-3-sonnet-20240229": usd(3, 15),
29665
+ "claude-haiku-4-5-20251001": usd(1, 5),
29666
+ // codebuddy 目录用点号写法(claude-sonnet-4.6),与上面的连字符写法是同一批模型,
29667
+ // 但字符串对不上就查不到价 → 记 0。同价另起别名行。
29668
+ "claude-sonnet-4.6": usd(3, 15),
29669
+ "claude-opus-4.7": usd(5, 25),
29670
+ "gemini-3.1-pro": usd(2, 12),
29671
+ "gemini-3-pro-preview": usd(2, 12),
29672
+ // Claude Code 回报的可能是不带日期的短名,短名匹配不上带日期的那行(前缀比对是「表里的键是模型名的前缀」)。
29673
+ "claude-haiku-4-5": usd(1, 5),
29674
+ "gemini-3.5-flash": usd(1.5, 9),
29675
+ "gemini-3.1-pro-preview": usd(2, 12),
29676
+ "gemini-3.1-pro-preview-customtools": usd(2, 12),
29677
+ "gemini-3.1-flash-lite-preview": usd(0.25, 1.5),
29678
+ "gemini-3-flash-preview": usd(0.5, 3),
29679
+ "gemini-2.5-pro": usd(1.25, 10),
29680
+ "gemini-2.5-flash": usd(0.3, 2.5),
29681
+ "gemini-2.5-flash-lite": usd(0.1, 0.4),
29682
+ "gemini-2.5-flash-preview-09-2025": usd(0.3, 2.5),
29683
+ "gemini-2.0-flash": usd(0.1, 0.4),
29684
+ "deepseek-v4-flash": cny(1, 2, 0.02),
29685
+ "deepseek-v4-pro": cny(3, 6, 0.025),
29686
+ "deepseek-chat": cny(2, 3),
29687
+ "deepseek-reasoner": cny(2, 3),
29688
+ "deepseek-v3.2": cny(2, 3),
29689
+ "deepseek-coder": cny(2, 3),
29690
+ "deepseek-r1": cny(4, 16),
29691
+ "deepseek-r1-0528": cny(4, 16),
29692
+ "deepseek-v3": cny(2, 8),
29693
+ "deepseek-v3-0324": cny(2, 8),
29694
+ "deepseek-v3.1": cny(4, 12),
29695
+ "qwen3-max": cny(6, 24),
29696
+ "qwen3-max-preview": cny(6, 24),
29697
+ "qwen3-coder-plus": cny(4, 16),
29698
+ "qwen3-235b-a22b": cny(4, 40),
29699
+ "qwen3-32b": cny(2, 20),
29700
+ "qwen3-30b-a3b": cny(1.5, 15),
29701
+ "qwen3-14b": cny(1, 10),
29702
+ "qwen3-8b": cny(0.5, 5),
29703
+ "qwen3-4b": cny(0.3, 3),
29704
+ "qwen3-1.7b": cny(0.3, 3),
29705
+ "qwen3-0.6b": cny(0.3, 3),
29706
+ "qwq-plus": cny(1.6, 4),
29707
+ "qwq-plus-latest": cny(1.6, 4),
29708
+ "qwq-plus-2025-03-05": cny(1.6, 4),
29709
+ "kimi-k2.7-code": cny(6.5, 27),
29710
+ "kimi-k2.6": cny(6.5, 27),
29711
+ "kimi-k2.5": cny(4, 21),
29712
+ "kimi-k2-0905-preview": cny(4, 16),
29713
+ "kimi-k2-0711-preview": cny(4, 16),
29714
+ "kimi-k2-turbo-preview": cny(8, 58)
29715
+ };
29716
+ STATIC_PREFIX_PRICES = Object.keys(MODEL_PRICES).sort((a, b2) => b2.length - a.length);
29717
+ CACHE_TTL_MS2 = 5 * 6e4;
29718
+ dbPrices = null;
29719
+ dbPrefixKeys = [];
29720
+ cacheLoadedAt = 0;
29721
+ cacheLoader = null;
29722
+ cacheRefreshInFlight = null;
29723
+ }
29724
+ });
29725
+
29726
+ // ../server/src/domains/usage/accounting.ts
29727
+ function nonNegative(v2) {
29728
+ return Math.max(v2 ?? 0, 0);
29729
+ }
29730
+ function resolvedModel(model) {
29731
+ const trimmed = model?.trim();
29732
+ return trimmed ? trimmed : null;
29733
+ }
29734
+ function splitByWeight(total, w2) {
29735
+ const sum = w2.input + w2.cache + w2.output;
29736
+ if (total === 0) return { input: 0, cache: 0, output: 0 };
29737
+ if (sum <= 0) return { input: 0, cache: 0, output: total };
29738
+ const input = Math.round(total * w2.input / sum);
29739
+ const cache = Math.round(total * w2.cache / sum);
29740
+ return { input, cache, output: total - input - cache };
29741
+ }
29742
+ function toDisplayUsd(currency, micros) {
29743
+ return currency === "USD" ? micros : Math.round(micros / DISPLAY_CNY_PER_USD);
29744
+ }
29745
+ function accountUsage(model, usage) {
29746
+ const resolved = resolvedModel(model);
29747
+ if (!resolved || !usage) return null;
29748
+ const input = nonNegative(usage.inputTokens);
29749
+ const output = nonNegative(usage.outputTokens);
29750
+ const cache = nonNegative(usage.cacheReadTokens) + nonNegative(usage.cacheCreationTokens);
29751
+ const tokens = { input, cache, output, total: input + cache + output };
29752
+ const estimated = estimateUsageCostBreakdown(resolved, usage);
29753
+ const weights = estimated ? { input: estimated.inputMicros, cache: estimated.cacheMicros, output: estimated.outputMicros } : null;
29754
+ const reported = Math.max(usage.costUsdMicros ?? 0, 0);
29755
+ if (reported > 0) {
29756
+ const parts = weights && estimated?.currency === "USD" ? splitByWeight(reported, weights) : { input: 0, cache: 0, output: reported };
29757
+ return {
29758
+ tokens,
29759
+ currency: "USD",
29760
+ amountMicros: reported,
29761
+ parts,
29762
+ displayUsdMicros: reported,
29763
+ displayParts: parts,
29764
+ source: "reported"
29765
+ };
29766
+ }
29767
+ if (estimated && weights) {
29768
+ const amount = weights.input + weights.cache + weights.output;
29769
+ const displayUsdMicros = toDisplayUsd(estimated.currency, amount);
29770
+ return {
29771
+ tokens,
29772
+ currency: estimated.currency,
29773
+ amountMicros: amount,
29774
+ parts: weights,
29775
+ displayUsdMicros,
29776
+ displayParts: estimated.currency === "USD" ? weights : splitByWeight(displayUsdMicros, weights),
29777
+ source: "estimated"
29778
+ };
29779
+ }
29780
+ return {
29781
+ tokens,
29782
+ currency: "USD",
29783
+ amountMicros: 0,
29784
+ parts: { input: 0, cache: 0, output: 0 },
29785
+ displayUsdMicros: 0,
29786
+ displayParts: { input: 0, cache: 0, output: 0 },
29787
+ source: "unpriced"
29788
+ };
29789
+ }
29790
+ function isUnpriced(account) {
29791
+ return account.source === "unpriced" && account.tokens.total > 0;
29792
+ }
29793
+ var init_accounting = __esm({
29794
+ "../server/src/domains/usage/accounting.ts"() {
29795
+ "use strict";
29796
+ init_model_pricing();
29797
+ }
29798
+ });
29799
+
29800
+ // ../server/src/subagent-cost.ts
29801
+ function estimateSubagentCostUsdMicros(usage) {
29802
+ if (!usage) return void 0;
29803
+ const models = Object.entries(usage.byModel);
29804
+ if (models.length === 0) return void 0;
29805
+ let total = 0;
29806
+ for (const [model, tokens] of models) {
29807
+ const account = accountUsage(model, { ...tokens, costUsdMicros: null });
29808
+ if (!account || account.source !== "estimated") return void 0;
29809
+ total += account.displayUsdMicros;
29810
+ }
29811
+ return total;
29812
+ }
29813
+ function withSubagentCostEstimate(event) {
29814
+ if (event.type !== "item" || event.attrs?.code !== SUBAGENT_TASK_CODE) return event;
29815
+ const task = event.attrs.task;
29816
+ if (!task?.usage || task.estimatedCostUsdMicros !== void 0) return event;
29817
+ const estimatedCostUsdMicros = estimateSubagentCostUsdMicros(task.usage);
29818
+ if (estimatedCostUsdMicros === void 0) return event;
29819
+ return { ...event, attrs: { ...event.attrs, task: { ...task, estimatedCostUsdMicros } } };
29820
+ }
29821
+ var init_subagent_cost = __esm({
29822
+ "../server/src/subagent-cost.ts"() {
29823
+ "use strict";
29824
+ init_src();
29825
+ init_accounting();
29826
+ }
29827
+ });
29828
+
29285
29829
  // ../contract/src/chat-item.ts
29286
29830
  var CHAT_ITEM_TERMINAL_STATUSES, isTerminalChatItemStatus, CHAT_ITEM_META_LEGACY_CONTENT;
29287
29831
  var init_chat_item = __esm({
@@ -29305,8 +29849,9 @@ async function resolveChatItemsTurnId(items, messageId, fallback) {
29305
29849
  const existing = await items.listItemsByMessage(messageId).catch(() => []);
29306
29850
  return existing.length ? existing[existing.length - 1].turnId : fallback;
29307
29851
  }
29308
- function projectsIntoContent(kind, role, origin, telemetryTextInContent) {
29852
+ function projectsIntoContent(kind, role, origin, telemetryTextInContent, attrs = {}) {
29309
29853
  if (kind !== "text" || role !== "assistant") return false;
29854
+ if (attrs.parentToolCallId) return false;
29310
29855
  switch (origin) {
29311
29856
  case "stdout":
29312
29857
  case "live":
@@ -29322,6 +29867,7 @@ var init_chat_item_ledger = __esm({
29322
29867
  "../server/src/chat-item-ledger.ts"() {
29323
29868
  "use strict";
29324
29869
  init_src();
29870
+ init_subagent_cost();
29325
29871
  init_chat_item();
29326
29872
  import_node_crypto15 = require("node:crypto");
29327
29873
  CHAT_ITEMS_DOUBLE_WRITE_ENV = "OASIS_CHAT_ITEMS_DOUBLE_WRITE";
@@ -29422,7 +29968,8 @@ var init_chat_item_ledger = __esm({
29422
29968
  * 应用一条归一层事件。**这是 provider 正文进入系统的唯一入口**(ADR D4(b))——
29423
29969
  * `LiveChatRegistry.applyNormalizedEvent` 与两条恢复路都经它,别再开第二个口子。
29424
29970
  */
29425
- apply(event) {
29971
+ apply(raw) {
29972
+ const event = withSubagentCostEstimate(raw);
29426
29973
  if (event.type === "turn") {
29427
29974
  this.itemKeyMap.clear();
29428
29975
  this.lastTurnId = null;
@@ -29661,7 +30208,7 @@ var init_chat_item_ledger = __esm({
29661
30208
  // ── 内部:建行与应用 op ────────────────────────────────────────────────────
29662
30209
  createFrom(event) {
29663
30210
  const attrs = event.attrs ?? {};
29664
- const inContent = projectsIntoContent(event.kind, event.role, event.origin, this.telemetryTextInContent);
30211
+ const inContent = projectsIntoContent(event.kind, event.role, event.origin, this.telemetryTextInContent, attrs);
29665
30212
  const text5 = event.opShape.op === "append_text" || event.opShape.op === "set_text" ? event.opShape.text : "";
29666
30213
  const status = event.kind === "text" || event.kind === "thinking" ? "streaming" : "completed";
29667
30214
  const id = this.insert({
@@ -29733,25 +30280,33 @@ var init_chat_item_ledger = __esm({
29733
30280
  }
29734
30281
  /** 按 kind 折出落库 payload。**唯一一处** payload 形状定义,别在别处再拼一遍。 */
29735
30282
  payloadOf(row) {
30283
+ const parent = row.attrs.parentToolCallId ? { parentToolCallId: row.attrs.parentToolCallId } : {};
29736
30284
  switch (row.kind) {
29737
30285
  case "text":
29738
30286
  case "thinking":
29739
- return { text: row.text };
30287
+ return { text: row.text, ...parent };
29740
30288
  case "tool_call":
29741
30289
  return {
29742
30290
  toolName: row.attrs.toolName ?? "tool",
29743
30291
  ...row.attrs.toolCallId ? { toolCallId: row.attrs.toolCallId } : {},
29744
- input: row.attrs.input ?? null
30292
+ input: row.attrs.input ?? null,
30293
+ ...parent
29745
30294
  };
29746
30295
  case "tool_result":
29747
30296
  return {
29748
30297
  ...row.attrs.toolCallId ? { toolCallId: row.attrs.toolCallId } : {},
29749
30298
  ...row.attrs.toolName ? { toolName: row.attrs.toolName } : {},
29750
30299
  output: row.attrs.output ?? row.text,
29751
- ...row.attrs.isError === true ? { isError: true } : {}
30300
+ ...row.attrs.isError === true ? { isError: true } : {},
30301
+ ...parent
29752
30302
  };
29753
30303
  case "control":
29754
- return { code: row.attrs.code ?? "control", ...row.attrs.note ? { note: row.attrs.note } : {} };
30304
+ return {
30305
+ code: row.attrs.code ?? "control",
30306
+ ...row.attrs.note ? { note: row.attrs.note } : {},
30307
+ ...row.attrs.code === SUBAGENT_TASK_CODE && row.attrs.toolCallId ? { toolCallId: row.attrs.toolCallId } : {},
30308
+ ...row.attrs.code === SUBAGENT_TASK_CODE && row.attrs.task ? { task: row.attrs.task } : {}
30309
+ };
29755
30310
  }
29756
30311
  }
29757
30312
  insert(input) {
@@ -160517,6 +161072,35 @@ var init_infra_error = __esm({
160517
161072
  }
160518
161073
  });
160519
161074
 
161075
+ // ../server/src/keyed-mutex.ts
161076
+ function createKeyedMutex() {
161077
+ const tails = /* @__PURE__ */ new Map();
161078
+ return {
161079
+ async acquire(key) {
161080
+ const previous3 = tails.get(key) ?? Promise.resolve();
161081
+ let release;
161082
+ const mine = new Promise((resolve10) => {
161083
+ release = resolve10;
161084
+ });
161085
+ const tail = previous3.then(() => mine);
161086
+ tails.set(key, tail);
161087
+ await previous3;
161088
+ let released = false;
161089
+ return () => {
161090
+ if (released) return;
161091
+ released = true;
161092
+ release();
161093
+ if (tails.get(key) === tail) tails.delete(key);
161094
+ };
161095
+ }
161096
+ };
161097
+ }
161098
+ var init_keyed_mutex = __esm({
161099
+ "../server/src/keyed-mutex.ts"() {
161100
+ "use strict";
161101
+ }
161102
+ });
161103
+
160520
161104
  // ../server/src/live-chat.ts
160521
161105
  function normalizedKindToItemType(kind) {
160522
161106
  if (kind === "text") return "message";
@@ -160540,7 +161124,9 @@ function v3StartedPayload(role, attrs, itemType, kind) {
160540
161124
  if (attrs?.input !== void 0) base.input = attrs.input;
160541
161125
  if (attrs?.output !== void 0) base.output = attrs.output;
160542
161126
  if (attrs?.isError !== void 0) base.isError = attrs.isError;
161127
+ if (attrs?.parentToolCallId) base.parentToolCallId = attrs.parentToolCallId;
160543
161128
  if (itemType === "control" && kind === "control" && attrs?.code) base.code = attrs.code;
161129
+ if (itemType === "control" && attrs?.task) base.task = attrs.task;
160544
161130
  return base;
160545
161131
  }
160546
161132
  var DEFAULT_BUFFER_MAX, DEFAULT_GRACE_MS, DEFAULT_LIVE_COALESCE_MS, DEFAULT_LIVE_COALESCE_MAX_CHUNKS, DEFAULT_LIVE_COALESCE_MAX_CHARS, LiveChatRegistry;
@@ -160548,6 +161134,7 @@ var init_live_chat = __esm({
160548
161134
  "../server/src/live-chat.ts"() {
160549
161135
  "use strict";
160550
161136
  init_chat_item_ledger();
161137
+ init_subagent_cost();
160551
161138
  DEFAULT_BUFFER_MAX = 4e3;
160552
161139
  DEFAULT_GRACE_MS = 6e4;
160553
161140
  DEFAULT_LIVE_COALESCE_MS = 24;
@@ -160715,7 +161302,8 @@ var init_live_chat = __esm({
160715
161302
  *
160716
161303
  * 账本坏掉不影响对话;v3 emit 抛异常也不影响 v2 通路与账本落盘——两条独立通路。
160717
161304
  */
160718
- applyNormalizedEvent(turn, event) {
161305
+ applyNormalizedEvent(turn, raw) {
161306
+ const event = withSubagentCostEstimate(raw);
160719
161307
  if (event.type === "item" && event.kind === "control" && event.attrs?.code === "input_consumed") {
160720
161308
  const key = event.attrs.clientMessageKey;
160721
161309
  if (key) turn.inputReceipts.get(key)?.consume();
@@ -203113,6 +203701,15 @@ function toApiError(err) {
203113
203701
  if (isInfrastructureError(err)) return new ApiError(INFRA_ERROR_STATUS, INFRA_ERROR_CODE, message);
203114
203702
  return new ApiError(400, "BAD_REQUEST", message);
203115
203703
  }
203704
+ function workorderApplyTarget(rawBody) {
203705
+ try {
203706
+ const body2 = JSON.parse(rawBody);
203707
+ const briefSpawn = body2.plan?.ops?.find((op) => op.action === "spawn" && op.id === body2.briefArtifactId && op.workspace);
203708
+ return briefSpawn?.workspace ?? body2.plan?.workspace ?? "";
203709
+ } catch {
203710
+ return "";
203711
+ }
203712
+ }
203116
203713
  async function loadWorkorderSnapshot(kernel, workorderId) {
203117
203714
  if (!workorderId) return null;
203118
203715
  const store = kernel.getStore?.();
@@ -204616,6 +205213,7 @@ async function startOasisServer(opts) {
204616
205213
  const stagingWorkspace = /* @__PURE__ */ new Map();
204617
205214
  const discussSessions = opts.discussSessions ?? /* @__PURE__ */ new Map();
204618
205215
  const openingDiscussions = /* @__PURE__ */ new Map();
205216
+ const workorderApplyLocks = createKeyedMutex();
204619
205217
  const reviewWaiters = /* @__PURE__ */ new Map();
204620
205218
  const leftWaiterIds = /* @__PURE__ */ new Map();
204621
205219
  const LEFT_WAITER_IDS_MAX_DRAFTS = 512;
@@ -205908,6 +206506,7 @@ async function startOasisServer(opts) {
205908
206506
  }
205909
206507
  }
205910
206508
  if (url.pathname === "/api/workorder/apply" && req.method === "POST") {
206509
+ const releaseApplyLock = await workorderApplyLocks.acquire(`${currentCompanyId ?? ""}\0${workorderApplyTarget(rawBody)}`);
205911
206510
  try {
205912
206511
  const body2 = JSON.parse(rawBody);
205913
206512
  let targetWs = body2.plan.workspace;
@@ -206066,6 +206665,8 @@ async function startOasisServer(opts) {
206066
206665
  res.writeHead(status, { "content-type": "application/json" }).end(
206067
206666
  JSON.stringify({ error: code2, message: err instanceof Error ? err.message : String(err) })
206068
206667
  );
206668
+ } finally {
206669
+ releaseApplyLock();
206069
206670
  }
206070
206671
  return;
206071
206672
  }
@@ -207495,6 +208096,7 @@ ${composed}`;
207495
208096
  session.onTelemetry?.((event) => {
207496
208097
  if (hasLiveProtocol && event.kind !== "tool_use") return;
207497
208098
  if (event.kind === "message" && sawTextOutput) return;
208099
+ if (typeof event.payload?.parentToolCallId === "string") return;
207498
208100
  if (liveProtocol && live && !hasLiveProtocol) {
207499
208101
  const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
207500
208102
  const itemId = event.kind === "tool_use" ? typeof payload.id === "string" ? payload.id : `${liveTurnId}:tool` : event.kind === "tool_result" ? typeof payload.tool_use_id === "string" ? payload.tool_use_id : `${liveTurnId}:tool` : event.kind === "thought" ? `${liveTurnId}:thinking` : `${liveTurnId}:message`;
@@ -208787,6 +209389,7 @@ var init_server3 = __esm({
208787
209389
  init_company_data_store_router();
208788
209390
  init_router();
208789
209391
  init_infra_error();
209392
+ init_keyed_mutex();
208790
209393
  init_live_chat();
208791
209394
  init_chat_turn_wait();
208792
209395
  init_append_now();
@@ -211943,312 +212546,6 @@ var init_uncategorized_project = __esm({
211943
212546
  }
211944
212547
  });
211945
212548
 
211946
- // ../server/src/domains/actors/model-pricing.ts
211947
- function lookupInMap(map, prefixKeys, candidates) {
211948
- for (const candidate of candidates) {
211949
- const exact = map instanceof Map ? map.get(candidate) : map[candidate];
211950
- if (exact) return exact;
211951
- }
211952
- const prefix = prefixKeys.find((p2) => candidates.some((candidate) => candidate.startsWith(`${p2}-`) || candidate.startsWith(`${p2}:`)));
211953
- if (!prefix) return null;
211954
- return map instanceof Map ? map.get(prefix) ?? null : map[prefix] ?? null;
211955
- }
211956
- function stripBracketTier(model) {
211957
- return model.replace(/\[[^\]]*\]/g, "").trim();
211958
- }
211959
- function modelCandidates(model) {
211960
- const normalized4 = model.trim().toLowerCase();
211961
- if (!normalized4) return [];
211962
- const base = [
211963
- normalized4,
211964
- normalized4.includes(":") ? normalized4.slice(normalized4.lastIndexOf(":") + 1) : "",
211965
- normalized4.includes("/") ? normalized4.slice(normalized4.lastIndexOf("/") + 1) : ""
211966
- ].filter(Boolean);
211967
- const stripped = base.map(stripBracketTier).filter((m2) => m2 && !base.includes(m2));
211968
- return [...base, ...new Set(stripped)];
211969
- }
211970
- function setModelPriceCacheLoader(loader) {
211971
- cacheLoader = loader;
211972
- }
211973
- async function warmModelPriceCache() {
211974
- if (!cacheLoader) return 0;
211975
- try {
211976
- const next = await cacheLoader();
211977
- dbPrices = next;
211978
- dbPrefixKeys = [...next.keys()].sort((a, b2) => b2.length - a.length);
211979
- cacheLoadedAt = Date.now();
211980
- return next.size;
211981
- } catch (err) {
211982
- console.warn(`[model-pricing] \u9884\u70ED\u4EF7\u683C\u7F13\u5B58\u5931\u8D25\uFF0C\u6CBF\u7528\u9759\u6001\u5FEB\u7167\uFF1A${String(err)}`);
211983
- return 0;
211984
- }
211985
- }
211986
- function maybeRefreshCache() {
211987
- if (!cacheLoader) return;
211988
- if (dbPrices && Date.now() - cacheLoadedAt < CACHE_TTL_MS2) return;
211989
- if (cacheRefreshInFlight) return;
211990
- cacheRefreshInFlight = warmModelPriceCache().catch(() => 0).then(() => {
211991
- cacheRefreshInFlight = null;
211992
- });
211993
- }
211994
- function priceForModel(model) {
211995
- if (!model) return null;
211996
- const candidates = modelCandidates(model);
211997
- if (candidates.length === 0) return null;
211998
- maybeRefreshCache();
211999
- if (dbPrices && dbPrices.size > 0) {
212000
- const fromDb = lookupInMap(dbPrices, dbPrefixKeys, candidates);
212001
- if (fromDb) return fromDb;
212002
- }
212003
- return lookupInMap(MODEL_PRICES, STATIC_PREFIX_PRICES, candidates);
212004
- }
212005
- function estimateUsageCostBreakdown(model, usage) {
212006
- const price = priceForModel(model);
212007
- if (!price) return null;
212008
- const inputTokens = Math.max(usage.inputTokens ?? 0, 0);
212009
- const outputTokens = Math.max(usage.outputTokens ?? 0, 0);
212010
- const cacheReadTokens = Math.max(usage.cacheReadTokens ?? 0, 0);
212011
- const cacheCreationTokens = Math.max(usage.cacheCreationTokens ?? 0, 0);
212012
- const cacheReadPrice = price.cachedInputPerMillion ?? price.inputPerMillion * 0.1;
212013
- return {
212014
- currency: price.currency,
212015
- inputMicros: Math.round(inputTokens * price.inputPerMillion / 1e6 * 1e6),
212016
- cacheMicros: Math.round(
212017
- (cacheCreationTokens * price.inputPerMillion + cacheReadTokens * cacheReadPrice) / 1e6 * 1e6
212018
- ),
212019
- outputMicros: Math.round(outputTokens * price.outputPerMillion / 1e6 * 1e6)
212020
- };
212021
- }
212022
- function estimateUsageCost(model, usage) {
212023
- const breakdown = estimateUsageCostBreakdown(model, usage);
212024
- if (!breakdown) return null;
212025
- return {
212026
- currency: breakdown.currency,
212027
- micros: breakdown.inputMicros + breakdown.cacheMicros + breakdown.outputMicros
212028
- };
212029
- }
212030
- function displayCostUsdMicros(model, usage) {
212031
- const stored = Math.max(usage.costUsdMicros ?? 0, 0);
212032
- if (stored > 0) return stored;
212033
- const estimated = estimateUsageCost(model, usage);
212034
- if (!estimated || estimated.micros <= 0) return 0;
212035
- if (estimated.currency === "USD") return estimated.micros;
212036
- return Math.round(estimated.micros / DISPLAY_CNY_PER_USD);
212037
- }
212038
- var FOREIGN_MODEL_MARKUP, usd, cny, MODEL_PRICES, STATIC_PREFIX_PRICES, CACHE_TTL_MS2, dbPrices, dbPrefixKeys, cacheLoadedAt, cacheLoader, cacheRefreshInFlight;
212039
- var init_model_pricing = __esm({
212040
- "../server/src/domains/actors/model-pricing.ts"() {
212041
- "use strict";
212042
- init_src();
212043
- FOREIGN_MODEL_MARKUP = 1.08;
212044
- usd = (inputPerMillion, outputPerMillion, cachedInputPerMillion) => ({
212045
- currency: "USD",
212046
- inputPerMillion: inputPerMillion * FOREIGN_MODEL_MARKUP,
212047
- outputPerMillion: outputPerMillion * FOREIGN_MODEL_MARKUP,
212048
- ...cachedInputPerMillion !== void 0 ? { cachedInputPerMillion: cachedInputPerMillion * FOREIGN_MODEL_MARKUP } : {}
212049
- });
212050
- cny = (inputPerMillion, outputPerMillion, cachedInputPerMillion) => ({
212051
- currency: "CNY",
212052
- inputPerMillion,
212053
- outputPerMillion,
212054
- ...cachedInputPerMillion !== void 0 ? { cachedInputPerMillion } : {}
212055
- });
212056
- MODEL_PRICES = {
212057
- "gpt-5.5": usd(5, 30, 0.5),
212058
- "gpt-5.5-pro": usd(30, 180),
212059
- "chat-latest": usd(5, 30, 0.5),
212060
- "gpt-5.4-mini": usd(0.75, 4.5, 0.075),
212061
- "gpt-5.4-nano": usd(0.2, 1.25, 0.02),
212062
- "gpt-5.4": usd(2.5, 15, 0.25),
212063
- "gpt-5.4-pro": usd(30, 180),
212064
- "gpt-5.3-codex": usd(1.75, 14, 0.175),
212065
- "gpt-5.2": usd(1.75, 14, 0.175),
212066
- "gpt-5.2-chat-latest": usd(1.75, 14, 0.175),
212067
- "gpt-5.2-pro": usd(21, 168),
212068
- "gpt-5.2-codex": usd(1.75, 14, 0.175),
212069
- "gpt-5.1": usd(1.25, 10, 0.125),
212070
- "gpt-5.1-chat-latest": usd(1.25, 10, 0.125),
212071
- "gpt-5.1-codex": usd(1.25, 10, 0.125),
212072
- "gpt-5.1-codex-max": usd(1.25, 10, 0.125),
212073
- "gpt-5.1-codex-mini": usd(0.25, 2, 0.025),
212074
- "gpt-5-pro": usd(15, 120),
212075
- "gpt-5-codex": usd(1.25, 10, 0.125),
212076
- "gpt-5-search-api": usd(1.25, 10, 0.125),
212077
- "gpt-5": usd(1.25, 10, 0.125),
212078
- "gpt-5-mini": usd(0.25, 2, 0.025),
212079
- "gpt-5-nano": usd(0.05, 0.4, 5e-3),
212080
- "gpt-5-chat-latest": usd(1.25, 10, 0.125),
212081
- "gpt-4.1": usd(2, 8, 0.5),
212082
- "gpt-4.1-2025-04-14": usd(2, 8, 0.5),
212083
- "gpt-4.1-mini": usd(0.4, 1.6, 0.1),
212084
- "gpt-4.1-mini-2025-04-14": usd(0.4, 1.6, 0.1),
212085
- "gpt-4.1-nano": usd(0.1, 0.4, 0.025),
212086
- "gpt-4.1-nano-2025-04-14": usd(0.1, 0.4, 0.025),
212087
- "o4-mini": usd(1.1, 4.4, 0.275),
212088
- // o3 / o3-mini 在 codex 静态目录里可选,却一直不在价目快照里(费用记 0)。
212089
- // 这两行按 OpenAI 公开价填,不是智增增快照里的行;供应商价目表同步到时以 DB 为准。
212090
- "o3": usd(2, 8),
212091
- "o3-mini": usd(1.1, 4.4),
212092
- "claude-fable-5-1": usd(10, 50),
212093
- "claude-fable-5": usd(10, 50),
212094
- "claude-mythos-5-1": usd(10, 50),
212095
- "claude-mythos-5": usd(10, 50),
212096
- // claude-opus-5 是 Claude Code 当代默认款,却一直不在这张快照里:查不到价 → 费用记 0
212097
- // (与 2026-09-04 那次 `claude-opus-5[1m]` 记 0 同一类坑)。按 Opus 档 $5/$25 补上。
212098
- "claude-opus-5": usd(5, 25),
212099
- "claude-opus-4-8": usd(5, 25),
212100
- "claude-opus-4-7": usd(5, 25),
212101
- "claude-sonnet-5": usd(2, 10),
212102
- "claude-sonnet-4-6": usd(3, 15),
212103
- "claude-opus-4-6": usd(5, 25),
212104
- "claude-opus-4-5-20251101": usd(5, 25),
212105
- "claude-opus-4-5": usd(5, 25),
212106
- "claude-sonnet-4-5-20250929": usd(3, 15),
212107
- // 短名同样要有:前缀比对是「表里的键是模型名的前缀」,带日期的那行匹配不上不带日期的短名。
212108
- // claude-sonnet-4-5 从 2026-07-04 起就在目录里可选,却一直查不到价(费用记 0),本次一并补。
212109
- "claude-sonnet-4-5": usd(3, 15),
212110
- "claude-opus-4-1-20250805": usd(15, 75),
212111
- "claude-opus-4-20250514": usd(15, 75),
212112
- "claude-sonnet-4-20250514": usd(3, 15),
212113
- "claude-3-7-sonnet-20250219": usd(3, 15),
212114
- "claude-3-5-sonnet-20241022": usd(3, 15),
212115
- "claude-3-5-sonnet-20240620": usd(3, 15),
212116
- "claude-3-sonnet-20240229": usd(3, 15),
212117
- "claude-haiku-4-5-20251001": usd(1, 5),
212118
- // codebuddy 目录用点号写法(claude-sonnet-4.6),与上面的连字符写法是同一批模型,
212119
- // 但字符串对不上就查不到价 → 记 0。同价另起别名行。
212120
- "claude-sonnet-4.6": usd(3, 15),
212121
- "claude-opus-4.7": usd(5, 25),
212122
- "gemini-3.1-pro": usd(2, 12),
212123
- "gemini-3-pro-preview": usd(2, 12),
212124
- // Claude Code 回报的可能是不带日期的短名,短名匹配不上带日期的那行(前缀比对是「表里的键是模型名的前缀」)。
212125
- "claude-haiku-4-5": usd(1, 5),
212126
- "gemini-3.5-flash": usd(1.5, 9),
212127
- "gemini-3.1-pro-preview": usd(2, 12),
212128
- "gemini-3.1-pro-preview-customtools": usd(2, 12),
212129
- "gemini-3.1-flash-lite-preview": usd(0.25, 1.5),
212130
- "gemini-3-flash-preview": usd(0.5, 3),
212131
- "gemini-2.5-pro": usd(1.25, 10),
212132
- "gemini-2.5-flash": usd(0.3, 2.5),
212133
- "gemini-2.5-flash-lite": usd(0.1, 0.4),
212134
- "gemini-2.5-flash-preview-09-2025": usd(0.3, 2.5),
212135
- "gemini-2.0-flash": usd(0.1, 0.4),
212136
- "deepseek-v4-flash": cny(1, 2, 0.02),
212137
- "deepseek-v4-pro": cny(3, 6, 0.025),
212138
- "deepseek-chat": cny(2, 3),
212139
- "deepseek-reasoner": cny(2, 3),
212140
- "deepseek-v3.2": cny(2, 3),
212141
- "deepseek-coder": cny(2, 3),
212142
- "deepseek-r1": cny(4, 16),
212143
- "deepseek-r1-0528": cny(4, 16),
212144
- "deepseek-v3": cny(2, 8),
212145
- "deepseek-v3-0324": cny(2, 8),
212146
- "deepseek-v3.1": cny(4, 12),
212147
- "qwen3-max": cny(6, 24),
212148
- "qwen3-max-preview": cny(6, 24),
212149
- "qwen3-coder-plus": cny(4, 16),
212150
- "qwen3-235b-a22b": cny(4, 40),
212151
- "qwen3-32b": cny(2, 20),
212152
- "qwen3-30b-a3b": cny(1.5, 15),
212153
- "qwen3-14b": cny(1, 10),
212154
- "qwen3-8b": cny(0.5, 5),
212155
- "qwen3-4b": cny(0.3, 3),
212156
- "qwen3-1.7b": cny(0.3, 3),
212157
- "qwen3-0.6b": cny(0.3, 3),
212158
- "qwq-plus": cny(1.6, 4),
212159
- "qwq-plus-latest": cny(1.6, 4),
212160
- "qwq-plus-2025-03-05": cny(1.6, 4),
212161
- "kimi-k2.7-code": cny(6.5, 27),
212162
- "kimi-k2.6": cny(6.5, 27),
212163
- "kimi-k2.5": cny(4, 21),
212164
- "kimi-k2-0905-preview": cny(4, 16),
212165
- "kimi-k2-0711-preview": cny(4, 16),
212166
- "kimi-k2-turbo-preview": cny(8, 58)
212167
- };
212168
- STATIC_PREFIX_PRICES = Object.keys(MODEL_PRICES).sort((a, b2) => b2.length - a.length);
212169
- CACHE_TTL_MS2 = 5 * 6e4;
212170
- dbPrices = null;
212171
- dbPrefixKeys = [];
212172
- cacheLoadedAt = 0;
212173
- cacheLoader = null;
212174
- cacheRefreshInFlight = null;
212175
- }
212176
- });
212177
-
212178
- // ../server/src/domains/usage/accounting.ts
212179
- function nonNegative(v2) {
212180
- return Math.max(v2 ?? 0, 0);
212181
- }
212182
- function resolvedModel(model) {
212183
- const trimmed = model?.trim();
212184
- return trimmed ? trimmed : null;
212185
- }
212186
- function splitByWeight(total, w2) {
212187
- const sum = w2.input + w2.cache + w2.output;
212188
- if (total === 0) return { input: 0, cache: 0, output: 0 };
212189
- if (sum <= 0) return { input: 0, cache: 0, output: total };
212190
- const input = Math.round(total * w2.input / sum);
212191
- const cache = Math.round(total * w2.cache / sum);
212192
- return { input, cache, output: total - input - cache };
212193
- }
212194
- function toDisplayUsd(currency, micros) {
212195
- return currency === "USD" ? micros : Math.round(micros / DISPLAY_CNY_PER_USD);
212196
- }
212197
- function accountUsage(model, usage) {
212198
- const resolved = resolvedModel(model);
212199
- if (!resolved || !usage) return null;
212200
- const input = nonNegative(usage.inputTokens);
212201
- const output = nonNegative(usage.outputTokens);
212202
- const cache = nonNegative(usage.cacheReadTokens) + nonNegative(usage.cacheCreationTokens);
212203
- const tokens = { input, cache, output, total: input + cache + output };
212204
- const estimated = estimateUsageCostBreakdown(resolved, usage);
212205
- const weights = estimated ? { input: estimated.inputMicros, cache: estimated.cacheMicros, output: estimated.outputMicros } : null;
212206
- const reported = Math.max(usage.costUsdMicros ?? 0, 0);
212207
- if (reported > 0) {
212208
- const parts = weights && estimated?.currency === "USD" ? splitByWeight(reported, weights) : { input: 0, cache: 0, output: reported };
212209
- return {
212210
- tokens,
212211
- currency: "USD",
212212
- amountMicros: reported,
212213
- parts,
212214
- displayUsdMicros: reported,
212215
- displayParts: parts,
212216
- source: "reported"
212217
- };
212218
- }
212219
- if (estimated && weights) {
212220
- const amount = weights.input + weights.cache + weights.output;
212221
- const displayUsdMicros = toDisplayUsd(estimated.currency, amount);
212222
- return {
212223
- tokens,
212224
- currency: estimated.currency,
212225
- amountMicros: amount,
212226
- parts: weights,
212227
- displayUsdMicros,
212228
- displayParts: estimated.currency === "USD" ? weights : splitByWeight(displayUsdMicros, weights),
212229
- source: "estimated"
212230
- };
212231
- }
212232
- return {
212233
- tokens,
212234
- currency: "USD",
212235
- amountMicros: 0,
212236
- parts: { input: 0, cache: 0, output: 0 },
212237
- displayUsdMicros: 0,
212238
- displayParts: { input: 0, cache: 0, output: 0 },
212239
- source: "unpriced"
212240
- };
212241
- }
212242
- function isUnpriced(account) {
212243
- return account.source === "unpriced" && account.tokens.total > 0;
212244
- }
212245
- var init_accounting = __esm({
212246
- "../server/src/domains/usage/accounting.ts"() {
212247
- "use strict";
212248
- init_model_pricing();
212249
- }
212250
- });
212251
-
212252
212549
  // ../server/src/domains/actors/stats.ts
212253
212550
  function emptyCostBreakdown() {
212254
212551
  return {
@@ -212404,6 +212701,66 @@ function foldSingleNodeTasks(items) {
212404
212701
  }
212405
212702
  return folded;
212406
212703
  }
212704
+ function basenameOfPath(name) {
212705
+ const i = name.lastIndexOf("/");
212706
+ return i >= 0 ? name.slice(i + 1) : name;
212707
+ }
212708
+ function creatorKindOf(actorId) {
212709
+ if (!actorId) return null;
212710
+ return actorId.startsWith("actor:human:") ? "human" : "agent";
212711
+ }
212712
+ function emitChatFolders(items, chatFiles, resolveName) {
212713
+ const groups = /* @__PURE__ */ new Map();
212714
+ for (const file of chatFiles) {
212715
+ const list2 = groups.get(file.chatSessionId) ?? [];
212716
+ list2.push(file);
212717
+ groups.set(file.chatSessionId, list2);
212718
+ }
212719
+ for (const [sessionId, group] of groups) {
212720
+ const head = group[0];
212721
+ const folderPath = `chat:${sessionId}`;
212722
+ const latest = group.reduce((acc, file) => file.uploadedAt > acc ? file.uploadedAt : acc, group[0].uploadedAt);
212723
+ items.push({
212724
+ kind: "folder",
212725
+ id: `folder:${folderPath}`,
212726
+ name: head.chatTitle,
212727
+ path: folderPath,
212728
+ filePath: null,
212729
+ depth: 0,
212730
+ type: "folder",
212731
+ size: null,
212732
+ taskId: null,
212733
+ taskTitle: CHAT_FOLDER_TASK_LABEL,
212734
+ creatorKind: creatorKindOf(head.chatOwnerId),
212735
+ creatorId: head.chatOwnerId,
212736
+ creatorName: resolveName(head.chatOwnerId),
212737
+ updatedAt: latest,
212738
+ folderKind: "chat",
212739
+ chatSessionId: sessionId
212740
+ });
212741
+ for (const file of group) {
212742
+ items.push({
212743
+ kind: "file",
212744
+ id: file.fileId,
212745
+ name: file.name,
212746
+ path: `${folderPath}/${file.name}`,
212747
+ filePath: null,
212748
+ depth: 1,
212749
+ type: file.contentType ?? inferTypeFromName(basenameOfPath(file.name)),
212750
+ blobRef: file.blobId,
212751
+ contentType: file.contentType ?? null,
212752
+ size: file.size,
212753
+ taskId: null,
212754
+ taskTitle: CHAT_FOLDER_TASK_LABEL,
212755
+ creatorKind: creatorKindOf(file.uploadedBy),
212756
+ creatorId: file.uploadedBy ?? null,
212757
+ creatorName: file.uploadedBy ? resolveName(file.uploadedBy) : null,
212758
+ updatedAt: file.uploadedAt,
212759
+ chatSessionId: sessionId
212760
+ });
212761
+ }
212762
+ }
212763
+ }
212407
212764
  function fillFolderSizes(items) {
212408
212765
  const folders = items.filter((i) => i.kind === "folder" && i.path);
212409
212766
  if (folders.length === 0) return;
@@ -212623,7 +212980,7 @@ function laterOf(a, b2) {
212623
212980
  if (!b2) return a;
212624
212981
  return b2 > a ? b2 : a;
212625
212982
  }
212626
- var DEFAULT_AGENT_ACTOR, MemoryProjectStateStore, MemoryArtifactStateStore, ProjectsService;
212983
+ var DEFAULT_AGENT_ACTOR, CHAT_FOLDER_TASK_LABEL, MemoryProjectStateStore, MemoryArtifactStateStore, ProjectsService;
212627
212984
  var init_service4 = __esm({
212628
212985
  "../server/src/domains/projects/service.ts"() {
212629
212986
  "use strict";
@@ -212635,6 +212992,7 @@ var init_service4 = __esm({
212635
212992
  init_stats();
212636
212993
  init_node_state();
212637
212994
  DEFAULT_AGENT_ACTOR = "actor:agent:system";
212995
+ CHAT_FOLDER_TASK_LABEL = "\u5BF9\u8BDD";
212638
212996
  MemoryProjectStateStore = class {
212639
212997
  projects = /* @__PURE__ */ new Map();
212640
212998
  members = /* @__PURE__ */ new Map();
@@ -213410,12 +213768,12 @@ var init_service4 = __esm({
213410
213768
  ...items.filter((item) => item.blobRef).map((item) => `${item.name}\0${item.blobRef}`),
213411
213769
  ...files.map((file) => `${file.name}\0${file.blobId}`)
213412
213770
  ]);
213413
- if (this.listChatFiles && !isUncategorizedProjectId(projectId2)) {
213771
+ const chatFiles = [];
213772
+ if (this.listChatFiles) {
213414
213773
  for (const file of await this.listChatFiles(projectId2)) {
213415
- const key = `${file.name}\0${file.blobId}`;
213774
+ const key = `${basenameOfPath(file.name)}\0${file.blobId}`;
213416
213775
  if (existingFiles.has(key)) continue;
213417
- existingFiles.add(key);
213418
- files.push(file);
213776
+ chatFiles.push(file);
213419
213777
  }
213420
213778
  }
213421
213779
  for (const file of files) {
@@ -213441,8 +213799,18 @@ var init_service4 = __esm({
213441
213799
  updatedAt: file.uploadedAt
213442
213800
  });
213443
213801
  }
213802
+ emitChatFolders(items, chatFiles, (actorId) => this.resolveActorName(actorId));
213444
213803
  fillFolderSizes(items);
213804
+ const chatFolderTime = new Map(items.filter((item) => item.folderKind === "chat" && item.chatSessionId).map((item) => [item.chatSessionId, item.updatedAt]));
213445
213805
  items.sort((a, b2) => {
213806
+ const chatA = a.chatSessionId ? 1 : 0;
213807
+ const chatB = b2.chatSessionId ? 1 : 0;
213808
+ if (chatA !== chatB) return chatA - chatB;
213809
+ if (a.chatSessionId && b2.chatSessionId && a.chatSessionId !== b2.chatSessionId) {
213810
+ const timeA = chatFolderTime.get(a.chatSessionId) ?? "";
213811
+ const timeB = chatFolderTime.get(b2.chatSessionId) ?? "";
213812
+ return timeB.localeCompare(timeA) || a.chatSessionId.localeCompare(b2.chatSessionId);
213813
+ }
213446
213814
  const pathA = a.path ?? "";
213447
213815
  const pathB = b2.path ?? "";
213448
213816
  if (pathA !== pathB) return pathA.localeCompare(pathB);
@@ -214767,6 +215135,8 @@ var init_dev_store = __esm({
214767
215135
  sessionWorkOrderKinds = /* @__PURE__ */ new Map();
214768
215136
  /** ADR-0166 D6 待发队列(每会话一条,按 seq 升序 = 入队序)。 */
214769
215137
  pending = /* @__PURE__ */ new Map();
215138
+ /** `outputs/` 产出文件的服务端副本表(2026-09-14):sessionId → 路径 → 副本。 */
215139
+ outputCopies = /* @__PURE__ */ new Map();
214770
215140
  pendingSeq = 0;
214771
215141
  async claimPendingInsert(m2) {
214772
215142
  const list2 = this.pending.get(m2.chatSessionId) ?? [];
@@ -214950,6 +215320,7 @@ var init_dev_store = __esm({
214950
215320
  this.messages.delete(id);
214951
215321
  this.sessionWorkOrders.delete(id);
214952
215322
  this.sessionWorkOrderKinds.delete(id);
215323
+ this.outputCopies.delete(id);
214953
215324
  }
214954
215325
  async appendMessage(m2) {
214955
215326
  return (await this.appendMessageOnce(m2)).message;
@@ -215078,6 +215449,27 @@ var init_dev_store = __esm({
215078
215449
  }
215079
215450
  return rows.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.messageId.localeCompare(right.messageId));
215080
215451
  }
215452
+ async readOutputCopies(sessionId) {
215453
+ const hit = this.outputCopies.get(sessionId);
215454
+ return hit ? { ...hit } : null;
215455
+ }
215456
+ async mergeOutputCopies(sessionId, copies) {
215457
+ if (copies.length === 0 || !this.sessions.has(sessionId)) return;
215458
+ const next = { ...this.outputCopies.get(sessionId) ?? {} };
215459
+ for (const copy of copies) next[copy.path] = { ...copy };
215460
+ this.outputCopies.set(sessionId, next);
215461
+ }
215462
+ async listOutputCopies(projectId2) {
215463
+ const rows = [];
215464
+ for (const session of this.sessions.values()) {
215465
+ if (session.visibility === "internal") continue;
215466
+ if ((session.projectId ?? null) !== projectId2) continue;
215467
+ const copies = this.outputCopies.get(session.id);
215468
+ if (!copies || Object.keys(copies).length === 0) continue;
215469
+ rows.push({ sessionId: session.id, ...session.projectId ? { projectId: session.projectId } : {}, copies: { ...copies } });
215470
+ }
215471
+ return rows.sort((left, right) => left.sessionId.localeCompare(right.sessionId));
215472
+ }
215081
215473
  async listRunningAssistantMessages() {
215082
215474
  const out = [];
215083
215475
  for (const list2 of this.messages.values()) {
@@ -216103,6 +216495,148 @@ var init_outputs_scan = __esm({
216103
216495
  }
216104
216496
  });
216105
216497
 
216498
+ // ../server/src/domains/chat-sessions/outputs-copy.ts
216499
+ function isOutputPath(path41) {
216500
+ return path41.startsWith(`${SESSION_OUTPUTS_DIR}/`);
216501
+ }
216502
+ function basenameOf2(path41) {
216503
+ const i = path41.lastIndexOf("/");
216504
+ return i >= 0 ? path41.slice(i + 1) : path41;
216505
+ }
216506
+ function planOutputCopies(fingerprints, copies, limits = OUTPUT_COPY_LIMITS) {
216507
+ const have = copies ?? {};
216508
+ const plan = { toCopy: [], skipped: [] };
216509
+ let totalBytes = 0;
216510
+ for (const path41 of Object.keys(fingerprints).sort((a, b2) => a.localeCompare(b2))) {
216511
+ if (!isOutputPath(path41)) continue;
216512
+ const fp = fingerprints[path41];
216513
+ if (typeof fp.size !== "number" || !Number.isFinite(fp.size) || typeof fp.mtime !== "string" || fp.mtime === "") continue;
216514
+ const copy = have[path41];
216515
+ if (copy && copy.size === fp.size && copy.mtime === fp.mtime) continue;
216516
+ if (fp.size > limits.maxFileBytes) {
216517
+ plan.skipped.push({ path: path41, code: "TOO_LARGE" });
216518
+ continue;
216519
+ }
216520
+ if (plan.toCopy.length >= limits.maxFiles || totalBytes + fp.size > limits.maxTotalBytes) {
216521
+ plan.skipped.push({ path: path41, code: "BUDGET" });
216522
+ continue;
216523
+ }
216524
+ totalBytes += fp.size;
216525
+ plan.toCopy.push({ path: path41, size: fp.size, mtime: fp.mtime });
216526
+ }
216527
+ return plan;
216528
+ }
216529
+ async function copySessionOutputs(deps) {
216530
+ const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
216531
+ const limits = deps.limits ?? OUTPUT_COPY_LIMITS;
216532
+ const result = { copied: [], skipped: [] };
216533
+ let existing;
216534
+ try {
216535
+ existing = await deps.readCopies(deps.sessionId);
216536
+ } catch {
216537
+ return result;
216538
+ }
216539
+ const plan = planOutputCopies(deps.fingerprints, existing, limits);
216540
+ result.skipped.push(...plan.skipped);
216541
+ for (const file of plan.toCopy) {
216542
+ let read;
216543
+ try {
216544
+ read = await deps.bridge.read(
216545
+ deps.loc.nodeId,
216546
+ { workdirKey: deps.loc.workdirKey, runtimeKind: deps.loc.runtimeKind, rtId: deps.loc.rtId, path: file.path },
216547
+ limits.maxFileBytes
216548
+ );
216549
+ } catch {
216550
+ result.skipped.push({ path: file.path, code: "READ_FAILED" });
216551
+ continue;
216552
+ }
216553
+ if (!read.ok) {
216554
+ result.skipped.push({ path: file.path, code: read.code === "TOO_LARGE" ? "TOO_LARGE" : "READ_FAILED" });
216555
+ continue;
216556
+ }
216557
+ const bytes2 = Buffer.from(read.contentBase64, "base64");
216558
+ let blobRef;
216559
+ try {
216560
+ blobRef = await deps.putBlob(new Uint8Array(bytes2));
216561
+ } catch {
216562
+ result.skipped.push({ path: file.path, code: "STORE_FAILED" });
216563
+ continue;
216564
+ }
216565
+ result.copied.push({
216566
+ path: file.path,
216567
+ name: basenameOf2(file.path),
216568
+ blobRef,
216569
+ contentType: read.contentType ? read.contentType : null,
216570
+ size: bytes2.length,
216571
+ // 记**扫描时**的 mtime(不是读的那一刻):下一轮拿同一把节点时钟的指纹来比,相等才算复制过。
216572
+ mtime: file.mtime,
216573
+ copiedAt: now()
216574
+ });
216575
+ }
216576
+ if (result.copied.length > 0) {
216577
+ try {
216578
+ await deps.mergeCopies(deps.sessionId, result.copied);
216579
+ } catch {
216580
+ result.skipped.push(...result.copied.map((copy) => ({ path: copy.path, code: "STORE_FAILED" })));
216581
+ result.copied = [];
216582
+ }
216583
+ }
216584
+ return result;
216585
+ }
216586
+ function createOutputCopyScheduler(deps) {
216587
+ const inFlight = /* @__PURE__ */ new Set();
216588
+ return {
216589
+ schedule(args) {
216590
+ const { store, chatSessionId, companyId } = args;
216591
+ const readCopies = store.readOutputCopies?.bind(store);
216592
+ const mergeCopies = store.mergeOutputCopies?.bind(store);
216593
+ if (!readCopies || !mergeCopies) return Promise.resolve(null);
216594
+ const key = `${companyId ?? ""}|${chatSessionId}`;
216595
+ if (inFlight.has(key)) return Promise.resolve(null);
216596
+ inFlight.add(key);
216597
+ return (async () => {
216598
+ try {
216599
+ const session = await store.getSession(chatSessionId);
216600
+ if (!session || session.visibility === "internal" || session.analyzedRunId) return null;
216601
+ if (deps.isDelegationChild && await deps.isDelegationChild(companyId, chatSessionId).catch(() => false)) return null;
216602
+ const result = await copySessionOutputs({
216603
+ bridge: args.bridge,
216604
+ loc: args.loc,
216605
+ sessionId: chatSessionId,
216606
+ fingerprints: args.fingerprints,
216607
+ readCopies,
216608
+ mergeCopies,
216609
+ putBlob: deps.putBlob,
216610
+ ...deps.limits ? { limits: deps.limits } : {}
216611
+ });
216612
+ if (result.skipped.length > 0) {
216613
+ const head = result.skipped.slice(0, 5).map((s2) => `${s2.path}:${s2.code}`).join(", ");
216614
+ deps.log?.(`[outputs-copy] ${chatSessionId}: \u590D\u5236 ${result.copied.length} \u4E2A\uFF0C\u8DF3\u8FC7 ${result.skipped.length} \u4E2A\uFF08${head}${result.skipped.length > 5 ? " \u2026" : ""}\uFF09`);
216615
+ }
216616
+ return result;
216617
+ } catch (err) {
216618
+ deps.log?.(`[outputs-copy] ${chatSessionId}: ${err instanceof Error ? err.message : String(err)}`);
216619
+ return null;
216620
+ } finally {
216621
+ inFlight.delete(key);
216622
+ }
216623
+ })();
216624
+ }
216625
+ };
216626
+ }
216627
+ var OUTPUT_COPY_LIMITS;
216628
+ var init_outputs_copy = __esm({
216629
+ "../server/src/domains/chat-sessions/outputs-copy.ts"() {
216630
+ "use strict";
216631
+ init_src();
216632
+ OUTPUT_COPY_LIMITS = {
216633
+ maxFileBytes: 2 * 1024 * 1024,
216634
+ maxFiles: 50,
216635
+ maxTotalBytes: 50 * 1024 * 1024
216636
+ };
216637
+ }
216638
+ });
216639
+
216106
216640
  // ../server/src/org-model-backfill.ts
216107
216641
  function resolveDisplayName(existing, humanActor, accountName) {
216108
216642
  const pick3 = existing?.trim() || humanActor?.name?.trim() || accountName?.trim();
@@ -225139,7 +225673,11 @@ function toApiProjectFilesView(view) {
225139
225673
  creator_kind: item.creatorKind,
225140
225674
  creator_id: item.creatorId,
225141
225675
  creator_name: item.creatorName,
225142
- updated_at: item.updatedAt
225676
+ updated_at: item.updatedAt,
225677
+ // 对话文件夹(`"chat"`):前端据此换带对话气泡的文件夹图标。其余文件夹与文件行为 null。
225678
+ folder_kind: item.folderKind ?? null,
225679
+ // 对话文件夹及其子文件所属的对话;前端「所属任务」列的「对话」筛选档靠它。其余行为 null。
225680
+ chat_session_id: item.chatSessionId ?? null
225143
225681
  }))
225144
225682
  };
225145
225683
  }
@@ -229589,37 +230127,93 @@ var init_files_view = __esm({
229589
230127
  });
229590
230128
 
229591
230129
  // ../server/src/domains/projects/chat-files.ts
229592
- async function collectProjectChatFiles(chat, projectId2, meta) {
229593
- if (!chat.listAttachmentMessages) return [];
229594
- const messages = await chat.listAttachmentMessages(projectId2);
229595
- const sessions = new Map(await Promise.all([...new Set(messages.map((m2) => m2.sessionId))].map(async (id) => [id, await chat.getSession(id)])));
229596
- const visible = messages.filter((m2) => {
229597
- const session = sessions.get(m2.sessionId);
229598
- return session?.projectId === projectId2 && session.visibility !== "internal" && !session.analyzedRunId;
229599
- });
229600
- const byMessage = new Map(visible.map((m2) => [m2.messageId, m2]));
229601
- const rows = await collectMessageFiles(visible.map((m2) => ({ ...m2, id: m2.messageId })), {
229602
- meta
229603
- });
229604
- return mergeChatSessionFiles(rows, []).filter((row) => row.blob_ref !== null).map((row) => {
229605
- const message = byMessage.get(row.message_id);
229606
- const session = sessions.get(message.sessionId);
229607
- return {
229608
- projectId: projectId2,
229609
- fileId: `chat:${message.sessionId}:${row.id}`,
229610
- name: row.name,
229611
- blobId: row.blob_ref,
229612
- contentType: row.content_type ?? void 0,
229613
- size: row.size,
229614
- uploadedBy: session.humanActorId,
229615
- uploadedAt: row.updated_at
229616
- };
230130
+ function chatFolderTitle(session) {
230131
+ const title = session.title?.trim();
230132
+ return title ? title : "\u672A\u547D\u540D\u5BF9\u8BDD";
230133
+ }
230134
+ function outputDisplayName(path41) {
230135
+ return path41.startsWith("outputs/") ? path41.slice("outputs/".length) : path41;
230136
+ }
230137
+ async function collectProjectChatFiles(chat, projectId2, meta, options = {}) {
230138
+ const scope = isUncategorizedProjectId(projectId2) ? null : projectId2;
230139
+ const [attachmentMessages, outputSessions] = await Promise.all([
230140
+ chat.listAttachmentMessages ? chat.listAttachmentMessages(scope) : Promise.resolve([]),
230141
+ chat.listOutputCopies ? chat.listOutputCopies(scope) : Promise.resolve([])
230142
+ ]);
230143
+ const sessionIds = [.../* @__PURE__ */ new Set([
230144
+ ...attachmentMessages.map((m2) => m2.sessionId),
230145
+ ...outputSessions.map((s2) => s2.sessionId)
230146
+ ])];
230147
+ const sessions = /* @__PURE__ */ new Map();
230148
+ await Promise.all(sessionIds.map(async (id) => {
230149
+ const session = await chat.getSession(id);
230150
+ if (!session) return;
230151
+ if ((session.projectId ?? null) !== scope) return;
230152
+ if (session.visibility === "internal" || session.analyzedRunId) return;
230153
+ if (options.isDelegationChild && await options.isDelegationChild(id).catch(() => false)) return;
230154
+ sessions.set(id, session);
230155
+ }));
230156
+ const out = [];
230157
+ const base = (session) => ({
230158
+ projectId: projectId2,
230159
+ chatSessionId: session.id,
230160
+ chatTitle: chatFolderTitle(session),
230161
+ chatOwnerId: session.humanActorId
229617
230162
  });
230163
+ const bySession = /* @__PURE__ */ new Map();
230164
+ for (const message of attachmentMessages) {
230165
+ if (!sessions.has(message.sessionId)) continue;
230166
+ const list2 = bySession.get(message.sessionId) ?? [];
230167
+ list2.push(message);
230168
+ bySession.set(message.sessionId, list2);
230169
+ }
230170
+ for (const [sessionId, messages] of bySession) {
230171
+ const session = sessions.get(sessionId);
230172
+ const rows = await collectMessageFiles(messages.map((m2) => ({ ...m2, id: m2.messageId })), { meta });
230173
+ for (const row of mergeChatSessionFiles(rows, [])) {
230174
+ if (row.blob_ref === null) continue;
230175
+ out.push({
230176
+ ...base(session),
230177
+ fileId: `chat:${sessionId}:${row.id}`,
230178
+ name: row.name,
230179
+ blobId: row.blob_ref,
230180
+ ...row.content_type ? { contentType: row.content_type } : {},
230181
+ size: row.size,
230182
+ uploadedBy: session.humanActorId,
230183
+ uploadedAt: row.updated_at,
230184
+ chatSource: "attachment"
230185
+ });
230186
+ }
230187
+ }
230188
+ for (const record8 of outputSessions) {
230189
+ const session = sessions.get(record8.sessionId);
230190
+ if (!session) continue;
230191
+ const seen = new Set(out.filter((file) => file.chatSessionId === session.id).map((file) => `${file.name}\0${file.blobId}`));
230192
+ const copies = record8.copies ?? {};
230193
+ for (const path41 of Object.keys(copies).sort((a, b2) => a.localeCompare(b2))) {
230194
+ const copy = copies[path41];
230195
+ if (!copy?.blobRef) continue;
230196
+ if (seen.has(`${copy.name}\0${copy.blobRef}`)) continue;
230197
+ out.push({
230198
+ ...base(session),
230199
+ fileId: `chat:${session.id}:out:${copy.path}`,
230200
+ name: outputDisplayName(copy.path),
230201
+ blobId: copy.blobRef,
230202
+ ...copy.contentType ? { contentType: copy.contentType } : {},
230203
+ size: copy.size,
230204
+ uploadedBy: session.aiActorId,
230205
+ uploadedAt: Number.isNaN(Date.parse(copy.mtime)) ? copy.copiedAt : new Date(copy.mtime).toISOString(),
230206
+ chatSource: "output"
230207
+ });
230208
+ }
230209
+ }
230210
+ return out;
229618
230211
  }
229619
230212
  var init_chat_files2 = __esm({
229620
230213
  "../server/src/domains/projects/chat-files.ts"() {
229621
230214
  "use strict";
229622
230215
  init_files_view();
230216
+ init_uncategorized_project();
229623
230217
  }
229624
230218
  });
229625
230219
 
@@ -248817,6 +249411,7 @@ var init_postgres_chat_sessions = __esm({
248817
249411
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS model text`);
248818
249412
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS visibility text`);
248819
249413
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS outputs_fingerprints jsonb`);
249414
+ await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS outputs_copies jsonb`);
248820
249415
  await pool.query(`
248821
249416
  WITH ranked AS (
248822
249417
  SELECT id, row_number() OVER (PARTITION BY session_id ORDER BY seq, created_at, id) AS rn
@@ -249064,6 +249659,53 @@ var init_postgres_chat_sessions = __esm({
249064
249659
  [JSON.stringify(fingerprints), sessionId, this.companyId]
249065
249660
  );
249066
249661
  }
249662
+ /**
249663
+ * `outputs/` 产出文件的服务端副本表(2026-09-14)。读:没有副本 → null。
249664
+ */
249665
+ async readOutputCopies(sessionId) {
249666
+ const r = await this.pool.query(
249667
+ `SELECT outputs_copies FROM ${this.s}.chat_sessions WHERE id = $1 AND company_id = $2`,
249668
+ [sessionId, this.companyId]
249669
+ );
249670
+ return r.rows[0]?.outputs_copies ?? null;
249671
+ }
249672
+ /**
249673
+ * 按路径**合并**写入:同一路径新副本覆盖旧副本,其余路径保留。合并在一条 UPDATE 里用 jsonb `||` 完成,
249674
+ * 不是读出来改完再整份写回——两次收尾的复制前后脚落库时,后写的不会把先写的吞掉。
249675
+ */
249676
+ async mergeOutputCopies(sessionId, copies) {
249677
+ if (copies.length === 0) return;
249678
+ const patch = {};
249679
+ for (const copy of copies) patch[copy.path] = copy;
249680
+ await this.pool.query(
249681
+ `UPDATE ${this.s}.chat_sessions
249682
+ SET outputs_copies = COALESCE(outputs_copies, '{}'::jsonb) || $1::jsonb
249683
+ WHERE id = $2 AND company_id = $3`,
249684
+ [JSON.stringify(patch), sessionId, this.companyId]
249685
+ );
249686
+ }
249687
+ /**
249688
+ * 某个范围内有副本的会话。口径同 {@link listAttachmentMessages}:排除内部会话、按公司收口、
249689
+ * `project_id IS NOT DISTINCT FROM $1`(null = 没绑项目的会话)。只取三列,不碰会话其余字段。
249690
+ */
249691
+ async listOutputCopies(projectId2) {
249692
+ const r = await this.pool.query(
249693
+ `SELECT id, project_id, outputs_copies
249694
+ FROM ${this.s}.chat_sessions
249695
+ WHERE outputs_copies IS NOT NULL
249696
+ AND outputs_copies <> '{}'::jsonb
249697
+ AND visibility IS DISTINCT FROM 'internal'
249698
+ AND project_id IS NOT DISTINCT FROM $1
249699
+ AND company_id = $2
249700
+ ORDER BY id`,
249701
+ [projectId2, this.companyId]
249702
+ );
249703
+ return r.rows.map((row) => ({
249704
+ sessionId: row.id,
249705
+ ...row.project_id ? { projectId: row.project_id } : {},
249706
+ copies: row.outputs_copies ?? {}
249707
+ }));
249708
+ }
249067
249709
  /** 删项目时把本公司里绑到它的会话回落成「不指定项目」。会话一条都不删,只清 project_id。 */
249068
249710
  async clearProjectBinding(projectId2) {
249069
249711
  if (!projectId2) return 0;
@@ -259242,6 +259884,7 @@ var init_src11 = __esm({
259242
259884
  init_company_scoped_registry();
259243
259885
  init_session_workdir();
259244
259886
  init_outputs_scan();
259887
+ init_outputs_copy();
259245
259888
  init_org_model_backfill();
259246
259889
  init_types_admin();
259247
259890
  init_roles_admin();
@@ -261050,6 +261693,13 @@ async function startServe(opts) {
261050
261693
  }
261051
261694
  }
261052
261695
  return null;
261696
+ }, {
261697
+ // 委派子会话不进项目(ADR 0555:它的 outputs/ 是交给协调者的)。闭包在请求时才读
261698
+ // `delegationStoreFor`,避开它在下方声明的 TDZ。
261699
+ isDelegationChild: async (sessionId) => {
261700
+ const ledger = await delegationStoreFor(companyId).catch(() => null);
261701
+ return Boolean(ledger && await ledger.getByChildSession(sessionId).catch(() => null));
261702
+ }
261053
261703
  });
261054
261704
  },
261055
261705
  projects: projectStateStore,
@@ -262212,6 +262862,14 @@ async function startServe(opts) {
262212
262862
  delegationStores.set(id, store);
262213
262863
  return store;
262214
262864
  };
262865
+ const outputCopyScheduler = createOutputCopyScheduler({
262866
+ putBlob: (bytes2) => assets.put(bytes2),
262867
+ isDelegationChild: async (companyId, sessionId) => {
262868
+ const ledger = await delegationStoreFor(companyId).catch(() => null);
262869
+ return Boolean(ledger && await ledger.getByChildSession(sessionId).catch(() => null));
262870
+ },
262871
+ log: (message) => console.warn(message)
262872
+ });
262215
262873
  const chatBroadcastMarkers = /* @__PURE__ */ new Map([
262216
262874
  [defaultCompanyId, chatBroadcastMarker]
262217
262875
  ]);
@@ -262943,6 +263601,7 @@ async function startServe(opts) {
262943
263601
  readFingerprints: read,
262944
263602
  writeFingerprints: write
262945
263603
  }).catch(() => null);
263604
+ if (diff && store) void outputCopyScheduler.schedule({ store, bridge, loc: located.loc, chatSessionId, companyId, fingerprints: diff.fingerprints });
262946
263605
  return diff?.changed ?? null;
262947
263606
  },
262948
263607
  delegationWorkdir: createDelegationWorkdirPort({
@@ -273778,7 +274437,7 @@ function shimScript() {
273778
274437
  }
273779
274438
 
273780
274439
  // src/index.ts
273781
- var PKG_VERSION = true ? "2.2.30" : "dev";
274440
+ var PKG_VERSION = true ? "2.2.32" : "dev";
273782
274441
  var LOCAL_BIN = localBin();
273783
274442
  var NPM_PREFIX = npmPrefix();
273784
274443
  var INSTANCE = DEFAULT_INSTANCE;