oasis_test_v2 2.2.18 → 2.2.20

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 +767 -122
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -243,7 +243,7 @@ function rankMemories(items, terms) {
243
243
  (a, b2) => b2.score - a.score || b2.accessedAt.localeCompare(a.accessedAt) || a.memId.localeCompare(b2.memId)
244
244
  );
245
245
  }
246
- var ActorMemoryNotFoundError, ActorMemoryVersionConflictError, MEMORY_CONTENT_MAX_CHARS, MEMORY_KEYWORDS_MAX, MEMORY_KEYWORD_MAX_CHARS, MEMORY_PER_ACTOR_MAX, MEMORY_DIGEST_CHARS, MEMORY_INJECT_MAX, MEMORY_KEYWORD_WEIGHT, MEMORY_QUERY_TERMS_MAX, MEMORY_READ_MAX_IDS, MEMORY_NGRAM_SCAN_MAX_CHARS, CJK_RE, ASCII_WORD_RE;
246
+ var ActorMemoryNotFoundError, ActorMemoryVersionConflictError, MEMORY_CONTENT_MAX_CHARS, MEMORY_KEYWORDS_MAX, MEMORY_KEYWORD_MAX_CHARS, MEMORY_PER_ACTOR_MAX, MEMORY_DIGEST_CHARS, MEMORY_INJECT_DIGEST_CHARS, MEMORY_INJECT_MAX, MEMORY_KEYWORD_WEIGHT, MEMORY_QUERY_TERMS_MAX, MEMORY_READ_MAX_IDS, MEMORY_NGRAM_SCAN_MAX_CHARS, CJK_RE, ASCII_WORD_RE;
247
247
  var init_actor_memory = __esm({
248
248
  "../contract/src/actor-memory.ts"() {
249
249
  "use strict";
@@ -268,7 +268,8 @@ var init_actor_memory = __esm({
268
268
  MEMORY_KEYWORDS_MAX = 16;
269
269
  MEMORY_KEYWORD_MAX_CHARS = 32;
270
270
  MEMORY_PER_ACTOR_MAX = 500;
271
- MEMORY_DIGEST_CHARS = 80;
271
+ MEMORY_DIGEST_CHARS = 140;
272
+ MEMORY_INJECT_DIGEST_CHARS = 80;
272
273
  MEMORY_INJECT_MAX = 30;
273
274
  MEMORY_KEYWORD_WEIGHT = 3;
274
275
  MEMORY_QUERY_TERMS_MAX = 32;
@@ -1219,6 +1220,35 @@ var init_chat = __esm({
1219
1220
  }
1220
1221
  });
1221
1222
 
1223
+ // ../contract/src/session-outputs.ts
1224
+ function diffSessionOutputs(scanned, previous3) {
1225
+ const prev = previous3 ?? {};
1226
+ const changed = [];
1227
+ const fingerprints = {};
1228
+ for (const entry of [...scanned].sort((a, b2) => a.path.localeCompare(b2.path))) {
1229
+ const { path: path41, size, mtime } = entry;
1230
+ if (!path41) continue;
1231
+ const usable = typeof size === "number" && Number.isFinite(size) && typeof mtime === "string" && mtime !== "";
1232
+ if (usable) fingerprints[path41] = { size, mtime };
1233
+ const before = prev[path41];
1234
+ const unchanged = usable && before !== void 0 && before.size === size && before.mtime === mtime;
1235
+ if (unchanged) continue;
1236
+ changed.push({ path: path41, name: basenameOf(path41), size: size ?? null, mtime: mtime ?? null });
1237
+ }
1238
+ return { changed, fingerprints };
1239
+ }
1240
+ function basenameOf(path41) {
1241
+ const i = path41.lastIndexOf("/");
1242
+ return i >= 0 ? path41.slice(i + 1) : path41;
1243
+ }
1244
+ var SESSION_OUTPUTS_DIR;
1245
+ var init_session_outputs = __esm({
1246
+ "../contract/src/session-outputs.ts"() {
1247
+ "use strict";
1248
+ SESSION_OUTPUTS_DIR = "outputs";
1249
+ }
1250
+ });
1251
+
1222
1252
  // ../contract/src/chat-session.ts
1223
1253
  function supportsChatModelOverride(runtimeKind) {
1224
1254
  return !!runtimeKind && CHAT_MODEL_OVERRIDE_RUNTIMES.includes(runtimeKind);
@@ -1409,10 +1439,11 @@ function touchAnchorMessageId(messageId) {
1409
1439
  const hash2 = messageId.indexOf("#");
1410
1440
  return hash2 >= 0 ? messageId.slice(0, hash2) : messageId;
1411
1441
  }
1412
- var DelegationLabelConflictError, DELEGATION_FILE_LIMITS, DELEGATION_INBOUND_DIR, DELEGATION_OUTBOUND_DIR, DELEGATION_OUTPUTS_DIR, DELEGATION_RETURN_RECENT_ROUNDS;
1442
+ var DelegationLabelConflictError, DELEGATION_FILE_LIMITS, DELEGATION_INBOUND_DIR, DELEGATION_OUTBOUND_DIR, DELEGATION_RETURN_RECENT_ROUNDS;
1413
1443
  var init_delegation = __esm({
1414
1444
  "../contract/src/delegation.ts"() {
1415
1445
  "use strict";
1446
+ init_session_outputs();
1416
1447
  DelegationLabelConflictError = class extends Error {
1417
1448
  constructor(parentSessionId, label) {
1418
1449
  super(`delegation label conflict in parent session: parent=${parentSessionId} label=${label ?? "<null>"}`);
@@ -1431,7 +1462,6 @@ var init_delegation = __esm({
1431
1462
  };
1432
1463
  DELEGATION_INBOUND_DIR = "inputs/attachments";
1433
1464
  DELEGATION_OUTBOUND_DIR = "delegations";
1434
- DELEGATION_OUTPUTS_DIR = "outputs";
1435
1465
  DELEGATION_RETURN_RECENT_ROUNDS = 3;
1436
1466
  }
1437
1467
  });
@@ -1452,6 +1482,8 @@ var init_delegation_prompts = __esm({
1452
1482
  "## \u5DE5\u4F5C\u533A\u7EA6\u5B9A",
1453
1483
  "",
1454
1484
  "- **\u4EA4\u4ED8\u7269\u653E `outputs/`**\uFF1A\u4F60\u8981\u4EA4\u56DE\u53BB\u7684\u6587\u4EF6\u653E\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E0B\uFF0C\u4FDD\u7559\u4F60\u81EA\u5DF1\u7684\u5B50\u76EE\u5F55\u7ED3\u6784\u3002",
1485
+ " \u8FD9\u91CC\u7684\u6587\u4EF6\u65E2\u4F1A\u4EA4\u56DE\u534F\u8C03\u8005\uFF0C\u4E5F\u4F1A\u5728**\u4F60\u8FD9\u6761\u5B50\u4F1A\u8BDD\u7684\u9875\u9762\u4E0A**\u663E\u793A\u6210\u53EF\u70B9\u5F00\u7684\u6587\u4EF6\u5361\u7247\uFF1B",
1486
+ " \u4E2D\u95F4\u4EA7\u7269\u548C\u8349\u7A3F\u522B\u653E\u8FD9\u513F\u3002",
1455
1487
  `- \`${PARENT_CONVERSATION_PATH}\` \u5982\u679C\u5B58\u5728\uFF0C\u662F\u4E3B\u4F1A\u8BDD\u7684\u5BF9\u8BDD\u5FEB\u7167\uFF0C\u53EF\u4EE5\u67E5\u9605\u3002`,
1456
1488
  " **\u5B83\u4E0D\u4FDD\u8BC1\u5B58\u5728**\uFF08\u4E3B\u4F1A\u8BDD\u6CA1\u6709\u53EF\u8F6C\u50A8\u7684\u5386\u53F2\u65F6\u5C31\u6CA1\u6709\u8FD9\u4E2A\u6587\u4EF6\uFF09\uFF0C\u522B\u628A\u5B83\u5F53\u524D\u63D0\u3002",
1457
1489
  "- \u534F\u8C03\u8005\u968F\u59D4\u6D3E\u5E26\u8FC7\u6765\u7684\u6587\u4EF6\u5728\u4F60\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0B\uFF0C\u4EFB\u52A1\u4E66\u91CC\u4F1A\u70B9\u540D\u3002",
@@ -1658,6 +1690,7 @@ var init_src = __esm({
1658
1690
  init_playbook();
1659
1691
  init_experiments();
1660
1692
  init_chat();
1693
+ init_session_outputs();
1661
1694
  init_chat_session();
1662
1695
  init_chat_session();
1663
1696
  init_live_protocol();
@@ -14320,7 +14353,8 @@ function memoryIndexLines(entries) {
14320
14353
  const rows = entries.slice(0, MEMORY_INJECT_MAX).map((e) => {
14321
14354
  const tag = e.projectId === null ? "[actor]" : "[project]";
14322
14355
  const kw = e.keywords.length > 0 ? ` {${e.keywords.join(",")}}` : "";
14323
- return `- \`${e.memId}\` ${tag}${kw} ${e.digest}`;
14356
+ const preview2 = e.digest.length > MEMORY_INJECT_DIGEST_CHARS ? `${e.digest.slice(0, MEMORY_INJECT_DIGEST_CHARS)}\u2026` : e.digest;
14357
+ return `- \`${e.memId}\` ${tag}${kw} ${preview2}`;
14324
14358
  });
14325
14359
  return [
14326
14360
  ``,
@@ -22923,7 +22957,7 @@ function normalizeClaudeStreamLine(line) {
22923
22957
  for (const block of content3) {
22924
22958
  switch (block["type"]) {
22925
22959
  case "text":
22926
- out.push({ kind: "message", payload: { text: block["text"] } });
22960
+ if (type === "assistant") out.push({ kind: "message", payload: { text: block["text"] } });
22927
22961
  break;
22928
22962
  case "thinking":
22929
22963
  out.push({ kind: "thought", payload: { text: block["thinking"] } });
@@ -23123,6 +23157,8 @@ var init_claude_code = __esm({
23123
23157
  // 同上
23124
23158
  ["--verbose", "boolean"],
23125
23159
  // 与 --output-format stream-json 配对;单独无意义、成对是重复
23160
+ ["--replay-user-messages", "boolean"],
23161
+ // Input consumption receipts are adapter-owned.
23126
23162
  ["--include-partial-messages", "boolean"]
23127
23163
  // 同上
23128
23164
  ]);
@@ -23211,7 +23247,7 @@ var init_claude_code = __esm({
23211
23247
  // 据此喂 onOutput 实现 chat 逐字流式(同时 telemetry 仍由整条 assistant 消息归一)。
23212
23248
  ...this.opts.streamJson ? ["--output-format", "stream-json", "--verbose", "--include-partial-messages"] : [],
23213
23249
  // ADR-0093:stream-json 输入模式——让 claude 逐条从 stdin 读 user 消息(支持多轮追加)。
23214
- ...appendMode ? ["--input-format", "stream-json"] : [],
23250
+ ...appendMode ? ["--input-format", "stream-json", "--replay-user-messages"] : [],
23215
23251
  ...job.model ?? this.opts.model ? ["--model", job.model ?? this.opts.model] : [],
23216
23252
  // ADR「agent CLI 启动参数员工级可配置化」:员工配置 job.extraArgs(在前)+ opts.extraArgs(静态默认,在后),
23217
23253
  // 过 CLAUDE_BLOCKED_FLAGS 拦截打破协议契约的 flag(剔除 + warn),追加到 argv 末尾。
@@ -23279,10 +23315,11 @@ var init_claude_code = __esm({
23279
23315
  pendingClose = null;
23280
23316
  }
23281
23317
  };
23282
- const writeUserMessage = (text5) => {
23318
+ const pendingInputReceipts = /* @__PURE__ */ new Map();
23319
+ const writeUserMessage = (text5, uuid2) => {
23283
23320
  try {
23284
23321
  child.stdin.write(
23285
- JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: text5 }] } }) + "\n"
23322
+ JSON.stringify({ type: "user", ...uuid2 ? { uuid: uuid2 } : {}, message: { role: "user", content: [{ type: "text", text: text5 }] } }) + "\n"
23286
23323
  );
23287
23324
  writes++;
23288
23325
  cancelPendingClose();
@@ -23357,6 +23394,22 @@ var init_claude_code = __esm({
23357
23394
  out.write(line + "\n");
23358
23395
  try {
23359
23396
  const p2 = JSON.parse(line);
23397
+ if (p2["type"] === "user" && p2["isReplay"] === true && !p2["parent_tool_use_id"] && typeof p2["uuid"] === "string") {
23398
+ const key = pendingInputReceipts.get(p2["uuid"]);
23399
+ if (key) {
23400
+ pendingInputReceipts.delete(p2["uuid"]);
23401
+ diag("input_consumed", { key, uuid: p2["uuid"] });
23402
+ for (const cb of liveEventCbs) cb({
23403
+ protocolVersion: 2,
23404
+ streamId: `claude-live:${id}`,
23405
+ turnId: id,
23406
+ itemId: `input:${p2["uuid"]}`,
23407
+ itemType: "control",
23408
+ operation: "completed",
23409
+ payload: { code: "input_consumed", clientMessageKey: key }
23410
+ });
23411
+ }
23412
+ }
23360
23413
  if (p2["type"] === "result") {
23361
23414
  lastResult = p2;
23362
23415
  results++;
@@ -23532,6 +23585,9 @@ var init_claude_code = __esm({
23532
23585
  }
23533
23586
  };
23534
23587
  function doAppend(input) {
23588
+ if (input.requireConsumptionReceipt && (!input.clientMessageKey || !appendMode)) {
23589
+ return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
23590
+ }
23535
23591
  if (!appendMode) {
23536
23592
  diag("append_rejected", { reason: "unsupported", writes, results });
23537
23593
  return Promise.resolve({ accepted: false, reason: "unsupported", ...input.interrupt ? { interrupt: "unsupported" } : {} });
@@ -23545,9 +23601,12 @@ var init_claude_code = __esm({
23545
23601
  const outgoing = report.failed.length ? appendMaterializeWarning(input.text, report) : input.text;
23546
23602
  if (input.interrupt) writeInterrupt();
23547
23603
  diag("append_accepted", { writes, results, interrupt: !!input.interrupt, chars: input.text.length });
23548
- const ok3 = writeUserMessage(outgoing);
23604
+ const receiptId = input.requireConsumptionReceipt ? (0, import_node_crypto8.randomUUID)() : void 0;
23605
+ if (receiptId) pendingInputReceipts.set(receiptId, input.clientMessageKey);
23606
+ const ok3 = writeUserMessage(outgoing, receiptId);
23607
+ if (!ok3 && receiptId) pendingInputReceipts.delete(receiptId);
23549
23608
  if (!ok3) return Promise.resolve({ accepted: false, reason: "rejected", ...hasFiles ? { files: report } : {}, ...input.interrupt ? { interrupt: "unknown" } : {} });
23550
- return Promise.resolve({ accepted: true, ...hasFiles ? { files: report } : {}, interrupt: input.interrupt ? "interrupted" : "queued_at_boundary" });
23609
+ return Promise.resolve({ accepted: true, ...receiptId ? { consumption: "pending" } : {}, ...hasFiles ? { files: report } : {}, interrupt: input.interrupt ? "interrupted" : "queued_at_boundary" });
23551
23610
  }
23552
23611
  }
23553
23612
  };
@@ -24872,6 +24931,8 @@ async function runProtocolSession(job, cfg) {
24872
24931
  const transcript = openTranscript(transcriptRef);
24873
24932
  const outputCbs = [];
24874
24933
  const telemetryCbs = [];
24934
+ const liveCbs = [];
24935
+ const appendByKey = /* @__PURE__ */ new Map();
24875
24936
  const exitCbs = [];
24876
24937
  const state = cfg.createState();
24877
24938
  const pending = /* @__PURE__ */ new Map();
@@ -24907,7 +24968,7 @@ async function runProtocolSession(job, cfg) {
24907
24968
  pending.clear();
24908
24969
  };
24909
24970
  const send = (command) => {
24910
- if (closing || child.stdin.destroyed || !child.stdin.writable) return Promise.reject(new Error("session stdin is not writable"));
24971
+ if (closing || child.stdin.destroyed || !child.stdin.writable) return Promise.reject(new ProtocolSessionClosing("session stdin is not writable"));
24911
24972
  const idValue = command.id ?? `oasis_${++nextRequestId}`;
24912
24973
  const message = { ...command, id: idValue };
24913
24974
  const key = commandId(idValue);
@@ -24927,11 +24988,12 @@ async function runProtocolSession(job, cfg) {
24927
24988
  if (isResponse(message) && responseId !== void 0 && pending.has(responseId)) {
24928
24989
  const waiter = pending.get(responseId);
24929
24990
  pending.delete(responseId);
24930
- if (message.type === "response" && message.success === false) waiter.reject(new Error(String(message.error ?? "runtime rejected command")));
24931
- else if (Object.prototype.hasOwnProperty.call(message, "error")) waiter.reject(new Error(String(message.error?.message ?? message.error)));
24991
+ if (message.type === "response" && message.success === false) waiter.reject(new ProtocolCommandRejected(String(message.error ?? "runtime rejected command")));
24992
+ else if (Object.prototype.hasOwnProperty.call(message, "error")) waiter.reject(new ProtocolCommandRejected(String(message.error?.message ?? message.error)));
24932
24993
  else waiter.resolve(message.result ?? message);
24933
24994
  return;
24934
24995
  }
24996
+ for (const event of cfg.liveEvents?.(message, state) ?? []) for (const cb of liveCbs) cb(event);
24935
24997
  for (const line of cfg.normalize(message, state)) {
24936
24998
  if (line.outputText) for (const cb of outputCbs) cb(line.outputText);
24937
24999
  const event = { kind: line.kind, payload: line.payload, seq: ++eventSeq, ts: (/* @__PURE__ */ new Date()).toISOString() };
@@ -25023,25 +25085,34 @@ async function runProtocolSession(job, cfg) {
25023
25085
  onTelemetry(cb) {
25024
25086
  telemetryCbs.push(cb);
25025
25087
  },
25088
+ onLiveEvent(cb) {
25089
+ liveCbs.push(cb);
25090
+ },
25026
25091
  get canAppendInput() {
25027
25092
  return !closing && started && !exited;
25028
25093
  },
25029
25094
  appendInput(input) {
25095
+ const key = input.clientMessageKey;
25096
+ if (key && appendByKey.has(key)) return appendByKey.get(key);
25097
+ if (input.requireConsumptionReceipt && !cfg.supportsInputConsumptionReceipts) return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
25030
25098
  appendChain = appendChain.then(async () => {
25031
25099
  if (exited || closing) return { accepted: false, reason: "session-closing" };
25032
25100
  const files = materializeFiles(dir, input.files, input.binaryFiles);
25033
25101
  try {
25034
- const result = await cfg.append(input, state, send);
25035
- return { ...result, ...files.written.length || files.failed.length ? { files } : {} };
25036
- } catch {
25037
- return { accepted: false, reason: "rejected", ...files.written.length || files.failed.length ? { files } : {} };
25102
+ const result2 = await cfg.append(input, state, send);
25103
+ return { ...result2, ...files.written.length || files.failed.length ? { files } : {} };
25104
+ } catch (error2) {
25105
+ const reason = error2 instanceof ProtocolSessionClosing ? "session-closing" : error2 instanceof ProtocolCommandRejected ? "rejected" : "unconfirmed";
25106
+ return { accepted: false, reason, ...files.written.length || files.failed.length ? { files } : {} };
25038
25107
  }
25039
25108
  });
25040
- return appendChain;
25109
+ const result = appendChain;
25110
+ if (key) appendByKey.set(key, result);
25111
+ return result;
25041
25112
  }
25042
25113
  };
25043
25114
  }
25044
- var import_node_child_process6, import_node_crypto10, path9;
25115
+ var import_node_child_process6, import_node_crypto10, path9, ProtocolCommandRejected, ProtocolSessionClosing;
25045
25116
  var init_protocol_session = __esm({
25046
25117
  "../adapters/src/_core/protocol-session.ts"() {
25047
25118
  "use strict";
@@ -25057,6 +25128,10 @@ var init_protocol_session = __esm({
25057
25128
  init_transcript();
25058
25129
  init_filter_extra_args();
25059
25130
  init_root_bypass();
25131
+ ProtocolCommandRejected = class extends Error {
25132
+ };
25133
+ ProtocolSessionClosing = class extends Error {
25134
+ };
25060
25135
  }
25061
25136
  });
25062
25137
 
@@ -25569,11 +25644,13 @@ var init_codex = __esm({
25569
25644
  workRoot: this.opts.workRoot,
25570
25645
  env: this.opts.env,
25571
25646
  runtimeKind: "codex",
25647
+ supportsInputConsumptionReceipts: true,
25572
25648
  syncSkills: this.opts.syncSkills,
25573
25649
  createState: () => ({
25574
25650
  threadId: void 0,
25575
25651
  turnId: void 0,
25576
25652
  active: false,
25653
+ pendingInputs: /* @__PURE__ */ new Set(),
25577
25654
  codex: createCodexNormalizeState(),
25578
25655
  // 本次 run 的用量:逐轮累加 `thread/tokenUsage/updated` 的 `last`(见该解析函数注释)。
25579
25656
  usage: { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 },
@@ -25617,6 +25694,34 @@ ${task}` : task;
25617
25694
  if (typeof turnId === "string" && turnId) state.turnId = turnId;
25618
25695
  if (!state.turnId) throw new Error("codex app-server returned no turn id");
25619
25696
  },
25697
+ liveEvents(message, state) {
25698
+ const params = message.params;
25699
+ const method = message.method;
25700
+ const turnId = typeof params?.turnId === "string" ? params.turnId : state.turnId ?? "codex-pending";
25701
+ const base = { protocolVersion: 2, streamId: `codex:${state.threadId}`, turnId };
25702
+ if (method === "item/agentMessage/delta" && typeof params?.itemId === "string" && typeof params.delta === "string") {
25703
+ return [{ ...base, itemId: params.itemId, itemType: "message", operation: "append_text", payload: { text: params.delta } }];
25704
+ }
25705
+ if (method === "item/started" || method === "item/completed") {
25706
+ const item = params?.item;
25707
+ if (item?.type === "userMessage" && typeof item.clientId === "string" && state.pendingInputs.delete(item.clientId)) {
25708
+ return [{
25709
+ ...base,
25710
+ itemId: `input:${item.clientId}`,
25711
+ itemType: "control",
25712
+ operation: "completed",
25713
+ payload: { code: "input_consumed", clientMessageKey: item.clientId }
25714
+ }];
25715
+ }
25716
+ if (method === "item/completed" && item?.type === "agentMessage" && typeof item.id === "string") {
25717
+ return [{ ...base, itemId: item.id, itemType: "message", operation: "completed", payload: { text: typeof item.text === "string" ? item.text : "" } }];
25718
+ }
25719
+ }
25720
+ if (method === "turn/completed") {
25721
+ return [{ ...base, itemId: "turn-end", itemType: "control", operation: "completed", payload: { code: "output_block_completed" } }];
25722
+ }
25723
+ return [];
25724
+ },
25620
25725
  normalize(message, state) {
25621
25726
  const params = message.params;
25622
25727
  const method = typeof message.method === "string" ? message.method : "";
@@ -25675,7 +25780,7 @@ ${task}` : task;
25675
25780
  };
25676
25781
  },
25677
25782
  async append(input, state, send) {
25678
- if (!state.threadId || !state.turnId) return { accepted: false, reason: "rejected" };
25783
+ if (!state.threadId || !state.turnId || !state.active) return { accepted: false, reason: "session-closing" };
25679
25784
  if (input.interrupt) {
25680
25785
  await send({ jsonrpc: "2.0", method: "turn/interrupt", params: { threadId: state.threadId, turnId: state.turnId } });
25681
25786
  const next = await send({ jsonrpc: "2.0", method: "turn/start", params: { threadId: state.threadId, input: [{ type: "text", text: input.text }] } });
@@ -25684,13 +25789,22 @@ ${task}` : task;
25684
25789
  state.active = true;
25685
25790
  return { accepted: true, interrupt: "unknown" };
25686
25791
  }
25687
- await send({ jsonrpc: "2.0", method: "turn/steer", params: {
25688
- threadId: state.threadId,
25689
- expectedTurnId: state.turnId,
25690
- ...input.clientMessageKey ? { clientUserMessageId: input.clientMessageKey } : {},
25691
- input: [{ type: "text", text: input.text }]
25692
- } });
25693
- return { accepted: true };
25792
+ if (input.clientMessageKey) state.pendingInputs.add(input.clientMessageKey);
25793
+ try {
25794
+ await send({ jsonrpc: "2.0", method: "turn/steer", params: {
25795
+ threadId: state.threadId,
25796
+ expectedTurnId: state.turnId,
25797
+ ...input.clientMessageKey ? { clientUserMessageId: input.clientMessageKey } : {},
25798
+ input: [{ type: "text", text: input.text }]
25799
+ } });
25800
+ } catch (error2) {
25801
+ if (input.clientMessageKey) state.pendingInputs.delete(input.clientMessageKey);
25802
+ if (error2 instanceof ProtocolCommandRejected && /no active turn|not active|expected.*turn|turn.*mismatch/i.test(error2.message)) {
25803
+ return { accepted: false, reason: "session-closing" };
25804
+ }
25805
+ throw error2;
25806
+ }
25807
+ return { accepted: true, consumption: "pending" };
25694
25808
  }
25695
25809
  });
25696
25810
  }
@@ -26566,6 +26680,7 @@ async function runACPSession(job, cfg) {
26566
26680
  const transcriptOut = openTranscript(transcriptRef);
26567
26681
  const outputCbs = [];
26568
26682
  const telemetryCbs = [];
26683
+ const liveCbs = [];
26569
26684
  const exitCbs = [];
26570
26685
  let exited;
26571
26686
  let killReason = null;
@@ -26644,6 +26759,34 @@ ${note}` : note };
26644
26759
  });
26645
26760
  let lastRpcError;
26646
26761
  let streamingCurrentTurn = false;
26762
+ const emitBoundary = () => {
26763
+ for (const cb of liveCbs) cb({
26764
+ protocolVersion: 2,
26765
+ streamId: id,
26766
+ turnId: id,
26767
+ itemId: "prompt-boundary",
26768
+ itemType: "control",
26769
+ operation: "completed",
26770
+ payload: { code: "output_block_completed" }
26771
+ });
26772
+ };
26773
+ const confirmedInputs = /* @__PURE__ */ new Set();
26774
+ const confirmAppendBatch = () => {
26775
+ for (const input of inflightAppendBatch) {
26776
+ const key = input.clientMessageKey;
26777
+ if (!key || confirmedInputs.has(key)) continue;
26778
+ confirmedInputs.add(key);
26779
+ for (const cb of liveCbs) cb({
26780
+ protocolVersion: 2,
26781
+ streamId: id,
26782
+ turnId: id,
26783
+ itemId: `input:${key}`,
26784
+ itemType: "control",
26785
+ operation: "completed",
26786
+ payload: { code: "input_consumed", clientMessageKey: key }
26787
+ });
26788
+ }
26789
+ };
26647
26790
  const client = new ACPClient(
26648
26791
  (line) => {
26649
26792
  try {
@@ -26652,9 +26795,11 @@ ${note}` : note };
26652
26795
  }
26653
26796
  },
26654
26797
  (text5) => {
26798
+ confirmAppendBatch();
26655
26799
  for (const cb of outputCbs) cb(text5);
26656
26800
  },
26657
26801
  (e) => {
26802
+ if (["message", "thought", "tool_use", "tool_result"].includes(e.kind)) confirmAppendBatch();
26658
26803
  for (const cb of telemetryCbs) cb(e);
26659
26804
  },
26660
26805
  // runtime 自己用 stopReason=cancelled 收尾时也保留取消语义;此前误记成 error,
@@ -26788,9 +26933,11 @@ ${task}` : task;
26788
26933
  promptResult = await sendPrompt(userText);
26789
26934
  }
26790
26935
  while (!killReason && appendQueue.length > 0) {
26936
+ emitBoundary();
26791
26937
  inflightAppendBatch = appendQueue.splice(0);
26792
26938
  const merged = inflightAppendBatch.map((i) => i.text).filter(Boolean).join("\n\n");
26793
26939
  promptResult = await sendPrompt(merged);
26940
+ confirmAppendBatch();
26794
26941
  inflightAppendBatch = [];
26795
26942
  }
26796
26943
  closingForExit = true;
@@ -26829,6 +26976,9 @@ ${task}` : task;
26829
26976
  onTelemetry(cb) {
26830
26977
  telemetryCbs.push(cb);
26831
26978
  },
26979
+ onLiveEvent(cb) {
26980
+ liveCbs.push(cb);
26981
+ },
26832
26982
  // ADR-0093 对话追加:入队一条 user 输入,drain 循环会把它发成同 session 的下一回合。
26833
26983
  // 已收尾/已退出则拒(session-closing),上层据此降级 kill+resume。
26834
26984
  get canAppendInput() {
@@ -26856,7 +27006,11 @@ ${task}` : task;
26856
27006
  const report = materializeFiles(dir, input.files, input.binaryFiles);
26857
27007
  const queued = report.failed.length ? { ...input, text: appendMaterializeWarning(input.text, report) } : input;
26858
27008
  appendQueue.push(queued);
26859
- const withFiles = (r) => report.written.length || report.failed.length ? { ...r, files: report } : r;
27009
+ const withFiles = (r) => ({
27010
+ ...r,
27011
+ ...input.requireConsumptionReceipt && r.accepted ? { consumption: "pending" } : {},
27012
+ ...report.written.length || report.failed.length ? { files: report } : {}
27013
+ });
26860
27014
  if (!input.interrupt) return Promise.resolve(withFiles({ accepted: true, interrupt: "queued_at_boundary" }));
26861
27015
  const run = appendChain.then(async () => {
26862
27016
  const pending = inflightStopReason;
@@ -27610,12 +27764,13 @@ var init_pi = __esm({
27610
27764
  return ["--mode", "rpc", "--session", sessionFile, ...model ? ["--model", model] : []];
27611
27765
  },
27612
27766
  runtimeKind: "pi",
27767
+ supportsInputConsumptionReceipts: true,
27613
27768
  workRoot: opts.workRoot,
27614
27769
  cleanupWorkdir: opts.cleanupWorkdir,
27615
27770
  env: opts.env,
27616
27771
  extraArgs: opts.extraArgs,
27617
27772
  blockedFlags: PI_BLOCKED_FLAGS,
27618
- createState: () => ({}),
27773
+ createState: () => ({ pendingInputs: [] }),
27619
27774
  initialCommand(job2, _dir, task) {
27620
27775
  const prompt2 = job2.systemPrompt ? `${job2.systemPrompt}
27621
27776
 
@@ -27632,6 +27787,28 @@ ${task}` : task;
27632
27787
  } catch {
27633
27788
  }
27634
27789
  },
27790
+ liveEvents(message, state) {
27791
+ const base = { protocolVersion: 2, streamId: "pi-rpc", turnId: "pi-rpc" };
27792
+ const raw = message.message;
27793
+ if (message.type === "message_end" && raw?.role === "assistant") {
27794
+ return [{ ...base, itemId: "message-end", itemType: "control", operation: "completed", payload: { code: "output_block_completed" } }];
27795
+ }
27796
+ if (message.type === "message_start" && raw?.role === "user") {
27797
+ const text5 = typeof raw.content === "string" ? raw.content : Array.isArray(raw.content) ? raw.content.filter((p2) => p2.type === "text").map((p2) => p2.text).join("") : void 0;
27798
+ const pending = state.pendingInputs[0];
27799
+ if (pending && text5 === pending.text) {
27800
+ state.pendingInputs.shift();
27801
+ return [{
27802
+ ...base,
27803
+ itemId: `input:${pending.key}`,
27804
+ itemType: "control",
27805
+ operation: "completed",
27806
+ payload: { code: "input_consumed", clientMessageKey: pending.key }
27807
+ }];
27808
+ }
27809
+ }
27810
+ return [];
27811
+ },
27635
27812
  normalize(message) {
27636
27813
  return normalizePi(JSON.stringify(message));
27637
27814
  },
@@ -27641,14 +27818,21 @@ ${task}` : task;
27641
27818
  runtimeSessionId(state) {
27642
27819
  return state.sessionId;
27643
27820
  },
27644
- async append(input, _state, send) {
27645
- if (input.interrupt) {
27646
- await send({ type: "abort" });
27821
+ async append(input, state, send) {
27822
+ const pending = input.clientMessageKey ? { key: input.clientMessageKey, text: input.text } : void 0;
27823
+ if (pending) state.pendingInputs.push(pending);
27824
+ try {
27825
+ if (input.interrupt) {
27826
+ await send({ type: "abort" });
27827
+ await send({ type: "steer", message: input.text });
27828
+ return { accepted: true, interrupt: "unknown" };
27829
+ }
27647
27830
  await send({ type: "steer", message: input.text });
27648
- return { accepted: true, interrupt: "unknown" };
27831
+ return { accepted: true, consumption: "pending" };
27832
+ } catch (error2) {
27833
+ if (pending) state.pendingInputs = state.pendingInputs.filter((p2) => p2 !== pending);
27834
+ throw error2;
27649
27835
  }
27650
- await send({ type: "steer", message: input.text });
27651
- return { accepted: true };
27652
27836
  }
27653
27837
  });
27654
27838
  }
@@ -28458,10 +28642,13 @@ var init_normalizer = __esm({
28458
28642
  proseClaim = /* @__PURE__ */ new Map();
28459
28643
  /** 本 turn 内已经见过的 item 键——认领闸只拦「新起一段」,不拦已知 item 的后续 op。 */
28460
28644
  claimedKeys = /* @__PURE__ */ new Set();
28645
+ closedKeys = /* @__PURE__ */ new Set();
28461
28646
  /** 见过 stdout 文本没有——见过就压掉 telemetry `message`(与 server.ts 原口径同款去重)。 */
28462
28647
  sawTextOutput = false;
28463
28648
  /** 本 turn 产出过 item op 没有。收尾时用来决定要不要发 turn 信号(见 turnFinished)。 */
28464
28649
  producedAny = false;
28650
+ /** Receipts wait for a whole output block; they never split its bytes. */
28651
+ pendingInputs = /* @__PURE__ */ new Map();
28465
28652
  constructor(opts) {
28466
28653
  this.providerName = opts.providerName;
28467
28654
  this.hasLiveProtocol = opts.hasLiveProtocol === true;
@@ -28505,8 +28692,17 @@ var init_normalizer = __esm({
28505
28692
  }
28506
28693
  /** 结构化实时事件(原生 Live Protocol v2)。 */
28507
28694
  onLiveEvent(event) {
28508
- this.turnId = event.turnId || this.turnId;
28509
28695
  const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
28696
+ if (event.itemType === "control" && payload.code === "input_consumed" && typeof payload.clientMessageKey === "string") {
28697
+ this.pendingInputs.set(payload.clientMessageKey, event.itemId);
28698
+ if (!this.open) this.flushInputs();
28699
+ return;
28700
+ }
28701
+ if (event.itemType === "control" && payload.code === "output_block_completed") {
28702
+ this.closeOpen("completed");
28703
+ return;
28704
+ }
28705
+ this.turnId = event.turnId || this.turnId;
28510
28706
  if (event.operation === "turn_completed" || event.operation === "turn_failed") {
28511
28707
  this.turnFinished(event.operation === "turn_failed" ? "failed" : "completed");
28512
28708
  return;
@@ -28522,13 +28718,12 @@ var init_normalizer = __esm({
28522
28718
  }
28523
28719
  return;
28524
28720
  }
28525
- if (event.operation === "completed" && (event.itemType === "message" || event.itemType === "thinking") && typeof payload.text === "string") {
28526
- this.snapshotProse(
28527
- event.itemType === "message" ? "text" : "thinking",
28528
- payload.text,
28529
- "live",
28530
- providerItemKey(this.providerName, event.itemId)
28531
- );
28721
+ if (event.operation === "completed" && (event.itemType === "message" || event.itemType === "thinking")) {
28722
+ const key = providerItemKey(this.providerName, event.itemId);
28723
+ if (typeof payload.text === "string") {
28724
+ this.snapshotProse(event.itemType === "message" ? "text" : "thinking", payload.text, "live", key);
28725
+ }
28726
+ if (this.open?.key === key) this.closeOpen("completed");
28532
28727
  return;
28533
28728
  }
28534
28729
  if (event.operation === "started" && event.itemType === "tool") {
@@ -28553,6 +28748,7 @@ var init_normalizer = __esm({
28553
28748
  this.fallbackIndex.clear();
28554
28749
  this.proseClaim.clear();
28555
28750
  this.claimedKeys.clear();
28751
+ this.closedKeys.clear();
28556
28752
  this.sawTextOutput = false;
28557
28753
  this.producedAny = false;
28558
28754
  if (!produced) return;
@@ -28576,6 +28772,10 @@ var init_normalizer = __esm({
28576
28772
  if (!known && !this.claim(kind, origin)) return;
28577
28773
  const key = explicitKey ?? (this.open && this.open.kind === kind ? this.open.key : this.nextFallbackKey(kind));
28578
28774
  this.claimedKeys.add(key);
28775
+ if (known && this.closedKeys.has(key)) {
28776
+ this.emitItem({ key, kind, role: "assistant", origin, opShape: { op: "append_text", text: text5 } });
28777
+ return;
28778
+ }
28579
28779
  if (!this.open || this.open.key !== key) {
28580
28780
  this.closeOpen("completed");
28581
28781
  this.open = { key, kind, origin };
@@ -28592,6 +28792,10 @@ var init_normalizer = __esm({
28592
28792
  if (!known && !this.claim(kind, origin)) return;
28593
28793
  const key = explicitKey ?? (this.open && this.open.kind === kind ? this.open.key : this.nextFallbackKey(kind));
28594
28794
  this.claimedKeys.add(key);
28795
+ if (known && this.closedKeys.has(key)) {
28796
+ this.emitItem({ key, kind, role: "assistant", origin, opShape: { op: "set_text", text: text5 } });
28797
+ return;
28798
+ }
28595
28799
  if (!this.open || this.open.key !== key) {
28596
28800
  this.closeOpen("completed");
28597
28801
  this.open = { key, kind, origin };
@@ -28610,8 +28814,23 @@ var init_normalizer = __esm({
28610
28814
  closeOpen(status) {
28611
28815
  const open3 = this.open;
28612
28816
  this.open = null;
28613
- if (!open3) return;
28614
- this.emitItem({ key: open3.key, kind: open3.kind, role: "assistant", origin: open3.origin, opShape: { op: "set_status", status } });
28817
+ if (open3) this.closedKeys.add(open3.key);
28818
+ if (open3) this.emitItem({ key: open3.key, kind: open3.kind, role: "assistant", origin: open3.origin, opShape: { op: "set_status", status } });
28819
+ this.flushInputs();
28820
+ }
28821
+ flushInputs() {
28822
+ const inputs = [...this.pendingInputs];
28823
+ this.pendingInputs.clear();
28824
+ for (const [clientMessageKey, id] of inputs) {
28825
+ this.emitItem({
28826
+ key: providerItemKey(this.providerName, id),
28827
+ kind: "control",
28828
+ role: "system",
28829
+ origin: "live",
28830
+ opShape: { op: "set_status", status: "completed" },
28831
+ attrs: { code: "input_consumed", clientMessageKey }
28832
+ });
28833
+ }
28615
28834
  }
28616
28835
  toolCall(origin, id, name, input) {
28617
28836
  this.closeOpen("completed");
@@ -28831,7 +29050,7 @@ var init_chat_item_ledger = __esm({
28831
29050
  telemetryTextInContent;
28832
29051
  /** 本段的「无输出」注解已经提前落过(`closeSegmentBeforeUserInput`),`beginSegment` 别再落一条。 */
28833
29052
  segmentEndEmitted = false;
28834
- /** **不变量 1**:`providerItemKey → itemId`,本账本唯一持有。切段/收尾清空。 */
29053
+ /** **不变量 1**:`providerItemKey → itemId`,本账本唯一持有。仅 runtime turn 结束/收尾清空,用户插入保留。 */
28835
29054
  itemKeyMap = /* @__PURE__ */ new Map();
28836
29055
  /** 内存账本:itemId → 行。插入顺序即 seq 顺序。 */
28837
29056
  rows = /* @__PURE__ */ new Map();
@@ -28922,7 +29141,7 @@ var init_chat_item_ledger = __esm({
28922
29141
  const payload = { role: "user", text: text5 };
28923
29142
  if (attachments?.length) payload.attachments = attachments;
28924
29143
  if (clientKeys?.length) payload.clientSubmitIds = clientKeys;
28925
- this.insert({
29144
+ return this.insert({
28926
29145
  kind: "text",
28927
29146
  role: "user",
28928
29147
  status: "completed",
@@ -29010,7 +29229,7 @@ var init_chat_item_ledger = __esm({
29010
29229
  }
29011
29230
  });
29012
29231
  const row = this.rows.get(id);
29013
- return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord };
29232
+ return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord, messageId: row.messageId };
29014
29233
  }
29015
29234
  /**
29016
29235
  * 回灌重启前已经落库的那段正文(`chat-recovery` 重挂 / 迟到对账用)。
@@ -29045,7 +29264,7 @@ var init_chat_item_ledger = __esm({
29045
29264
  // ── 段与归属 ──────────────────────────────────────────────────────────────
29046
29265
  /**
29047
29266
  * 切段(人中途插入):此刻起建的 item 属于**下一条** assistant 行。
29048
- * 映射一并清空——新段是新行,复用旧 itemId 会把新文本写进上一条消息。
29267
+ * 已有 provider item 保留身份与位置;新 item 才归下一段,迟到快照不能复制旧块。
29049
29268
  */
29050
29269
  beginSegment() {
29051
29270
  if (this.segmentEndEmitted) {
@@ -29054,8 +29273,6 @@ var init_chat_item_ledger = __esm({
29054
29273
  this.closeStreamingRows("completed");
29055
29274
  if (this.segmentItemCount === 0 && this.messageId) this.insertSegmentEnd();
29056
29275
  }
29057
- this.itemKeyMap.clear();
29058
- this.lastTurnId = null;
29059
29276
  this.messageId = null;
29060
29277
  this.segment += 1;
29061
29278
  this.segmentItemCount = 0;
@@ -29072,14 +29289,14 @@ var init_chat_item_ledger = __esm({
29072
29289
  * 返回落下的那一行(`null` = 本段有输出、或还没有 assistant 行可归属,不需要注解),
29073
29290
  * 调用方据它把这条推上 v3 流——不推的话它只在下一次快照才出现。
29074
29291
  */
29075
- closeSegmentBeforeUserInput() {
29292
+ closeSegmentBeforeUserInput(allowUnboundSegment = false) {
29076
29293
  this.closeStreamingRows("completed");
29077
29294
  if (this.segmentEndEmitted) return null;
29078
- if (this.segmentItemCount !== 0 || !this.messageId) return null;
29295
+ if (this.segmentItemCount !== 0 || !this.messageId && !allowUnboundSegment) return null;
29079
29296
  const id = this.insertSegmentEnd();
29080
29297
  this.segmentEndEmitted = true;
29081
29298
  const row = this.rows.get(id);
29082
- return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord };
29299
+ return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord, messageId: row.messageId };
29083
29300
  }
29084
29301
  /** 零输出段兜底(S1 评审 B1):那条 assistant 行库里已经在了,一条 item 都没有会被对账抓成假红。 */
29085
29302
  insertSegmentEnd() {
@@ -29098,6 +29315,9 @@ var init_chat_item_ledger = __esm({
29098
29315
  /** 行落库了,把本段还挂着 NULL 的 assistant item 认领回去,并让后续 item 直接带上它。 */
29099
29316
  bindMessage(messageId) {
29100
29317
  this.messageId = messageId;
29318
+ for (const row of this.rows.values()) {
29319
+ if (row.role === "assistant" && row.messageId === null) row.messageId = messageId;
29320
+ }
29101
29321
  if (!this.persistEnabled) return;
29102
29322
  this.enqueue(async () => {
29103
29323
  await this.deps.items.bindMessage(this.deps.sessionId, this.deps.turnId, messageId);
@@ -29157,7 +29377,8 @@ var init_chat_item_ledger = __esm({
29157
29377
  itemId: id,
29158
29378
  version: row.wireVersion,
29159
29379
  startedVersion: row.startedVersion,
29160
- ord: row.ord
29380
+ ord: row.ord,
29381
+ messageId: row.messageId
29161
29382
  };
29162
29383
  if (event.opShape.op === "set_status") {
29163
29384
  const followUp = this.applyOpTo(row, event);
@@ -29193,7 +29414,7 @@ var init_chat_item_ledger = __esm({
29193
29414
  }
29194
29415
  /** 这一次写的回执。`persist` 已经在里面走过钟了,这里只是把读数带出去给 wire。 */
29195
29416
  outcomeOf(row) {
29196
- return { itemId: row.id, version: row.wireVersion, ord: row.ord };
29417
+ return { itemId: row.id, version: row.wireVersion, ord: row.ord, messageId: row.messageId };
29197
29418
  }
29198
29419
  /** 关掉所有还 streaming 的行(收尾/切段)。 */
29199
29420
  closeStreamingRows(status) {
@@ -29245,13 +29466,16 @@ var init_chat_item_ledger = __esm({
29245
29466
  origin: input.origin,
29246
29467
  inContent: input.inContent,
29247
29468
  persist: input.persist,
29248
- segment: this.segment
29469
+ segment: this.segment,
29470
+ /* 归属在**建行那一刻**定死:链是异步的,等它执行时 this.messageId 可能已被切段改了。
29471
+ 记在行上而不是只记在入库那一步——wire 帧也要带它(2026-09-11),两边必须同一个值。 */
29472
+ messageId: input.forceNullMessage ? null : this.messageId
29249
29473
  };
29250
29474
  this.rows.set(id, row);
29251
29475
  this.createdCount += 1;
29252
29476
  if (!input.forceNullMessage) this.segmentItemCount += 1;
29253
29477
  if (!input.persist || !this.persistEnabled) return id;
29254
- const messageId = input.forceNullMessage ? null : this.messageId;
29478
+ const messageId = row.messageId;
29255
29479
  const createdAt = this.now();
29256
29480
  const payload = input.payload ?? this.payloadOf(row);
29257
29481
  const metadata = input.metadata ?? { [CHAT_ITEM_META_LEGACY_CONTENT]: input.inContent };
@@ -29434,6 +29658,7 @@ async function openAssistantRunningRow(deps) {
29434
29658
  async function finalizeAssistantRow(deps) {
29435
29659
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
29436
29660
  const finishedAt = now();
29661
+ const parts = await appendOutputsPart(deps.parts, deps.outputsPart);
29437
29662
  if (deps.assistantMsgId) {
29438
29663
  if (typeof deps.chatStore.updateMessage !== "function") return;
29439
29664
  try {
@@ -29441,7 +29666,7 @@ async function finalizeAssistantRow(deps) {
29441
29666
  content: deps.content,
29442
29667
  status: deps.status,
29443
29668
  completedAt: finishedAt,
29444
- ...deps.parts.length ? { parts: deps.parts } : {},
29669
+ ...parts.length ? { parts } : {},
29445
29670
  ...deps.runId ? { runId: deps.runId } : {},
29446
29671
  ...deps.turnId ? { turnId: deps.turnId } : {}
29447
29672
  });
@@ -29449,7 +29674,7 @@ async function finalizeAssistantRow(deps) {
29449
29674
  }
29450
29675
  return;
29451
29676
  }
29452
- if (!deps.content && !deps.runId && deps.parts.length === 0) return;
29677
+ if (!deps.content && !deps.runId && parts.length === 0) return;
29453
29678
  if (typeof deps.chatStore.appendMessage !== "function") return;
29454
29679
  try {
29455
29680
  await deps.chatStore.appendMessage({
@@ -29462,11 +29687,18 @@ async function finalizeAssistantRow(deps) {
29462
29687
  status: deps.status,
29463
29688
  ...deps.runId ? { runId: deps.runId } : {},
29464
29689
  ...deps.turnId ? { turnId: deps.turnId } : {},
29465
- ...deps.parts.length ? { parts: deps.parts } : {}
29690
+ ...parts.length ? { parts } : {}
29466
29691
  });
29467
29692
  } catch {
29468
29693
  }
29469
29694
  }
29695
+ async function appendOutputsPart(parts, provider) {
29696
+ if (!provider) return parts;
29697
+ const part = await provider().catch(() => null);
29698
+ if (!part || !part.outputs?.length) return parts;
29699
+ const maxSeq = parts.reduce((m2, p2) => typeof p2.seq === "number" && p2.seq > m2 ? p2.seq : m2, 0);
29700
+ return [...parts, { ...part, seq: maxSeq + 1 }];
29701
+ }
29470
29702
  function runBackgroundAssistantChatTurn(deps) {
29471
29703
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
29472
29704
  const log3 = deps.log ?? ((m2) => console.warn(m2));
@@ -29635,6 +29867,7 @@ function runBackgroundAssistantChatTurn(deps) {
29635
29867
  parts,
29636
29868
  status,
29637
29869
  ...runId ? { runId } : {},
29870
+ ...deps.outputsPart ? { outputsPart: deps.outputsPart } : {},
29638
29871
  now
29639
29872
  });
29640
29873
  if (typeof deps.chatStore.updateSession === "function") {
@@ -51507,7 +51740,7 @@ var require_websocket = __commonJS({
51507
51740
  var http2 = require("http");
51508
51741
  var net = require("net");
51509
51742
  var tls = require("tls");
51510
- var { randomBytes: randomBytes13, createHash: createHash38 } = require("crypto");
51743
+ var { randomBytes: randomBytes13, createHash: createHash39 } = require("crypto");
51511
51744
  var { Duplex, Readable } = require("stream");
51512
51745
  var { URL: URL2 } = require("url");
51513
51746
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -52175,7 +52408,7 @@ var require_websocket = __commonJS({
52175
52408
  abortHandshake(websocket, socket, "Invalid Upgrade header");
52176
52409
  return;
52177
52410
  }
52178
- const digest = createHash38("sha1").update(key + GUID).digest("base64");
52411
+ const digest = createHash39("sha1").update(key + GUID).digest("base64");
52179
52412
  if (res.headers["sec-websocket-accept"] !== digest) {
52180
52413
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
52181
52414
  return;
@@ -52544,7 +52777,7 @@ var require_websocket_server = __commonJS({
52544
52777
  var EventEmitter = require("events");
52545
52778
  var http2 = require("http");
52546
52779
  var { Duplex } = require("stream");
52547
- var { createHash: createHash38 } = require("crypto");
52780
+ var { createHash: createHash39 } = require("crypto");
52548
52781
  var extension3 = require_extension();
52549
52782
  var PerMessageDeflate2 = require_permessage_deflate();
52550
52783
  var subprotocol2 = require_subprotocol();
@@ -52851,7 +53084,7 @@ var require_websocket_server = __commonJS({
52851
53084
  );
52852
53085
  }
52853
53086
  if (this._state > RUNNING) return abortHandshake(socket, 503);
52854
- const digest = createHash38("sha1").update(key + GUID).digest("base64");
53087
+ const digest = createHash39("sha1").update(key + GUID).digest("base64");
52855
53088
  const headers = [
52856
53089
  "HTTP/1.1 101 Switching Protocols",
52857
53090
  "Upgrade: websocket",
@@ -160057,7 +160290,8 @@ var init_live_chat = __esm({
160057
160290
  v3Seq: 0,
160058
160291
  v3Buffer: [],
160059
160292
  v3Subscribers: prev?.recovering ? prev.v3Subscribers : /* @__PURE__ */ new Set(),
160060
- v3Items: /* @__PURE__ */ new Map()
160293
+ v3Items: /* @__PURE__ */ new Map(),
160294
+ inputReceipts: /* @__PURE__ */ new Map()
160061
160295
  };
160062
160296
  this.turns.set(chatSessionId, turn);
160063
160297
  this.flushPendingUserItems(chatSessionId);
@@ -160111,6 +160345,8 @@ var init_live_chat = __esm({
160111
160345
  };
160112
160346
  },
160113
160347
  finish: (status) => {
160348
+ for (const receipt of turn.inputReceipts.values()) receipt.abandon();
160349
+ turn.inputReceipts.clear();
160114
160350
  try {
160115
160351
  turn.items?.finish(status === "error" ? "failed" : "completed");
160116
160352
  } catch {
@@ -160172,6 +160408,11 @@ var init_live_chat = __esm({
160172
160408
  * 账本坏掉不影响对话;v3 emit 抛异常也不影响 v2 通路与账本落盘——两条独立通路。
160173
160409
  */
160174
160410
  applyNormalizedEvent(turn, event) {
160411
+ if (event.type === "item" && event.kind === "control" && event.attrs?.code === "input_consumed") {
160412
+ const key = event.attrs.clientMessageKey;
160413
+ if (key) turn.inputReceipts.get(key)?.consume();
160414
+ return;
160415
+ }
160175
160416
  let outcome = null;
160176
160417
  try {
160177
160418
  outcome = turn.items?.apply(event) ?? null;
@@ -160220,6 +160461,7 @@ var init_live_chat = __esm({
160220
160461
  operation: "started",
160221
160462
  itemVersion: outcome.startedVersion,
160222
160463
  ...ord !== void 0 ? { ord } : {},
160464
+ ...outcome.messageId !== null ? { messageId: outcome.messageId } : {},
160223
160465
  offset: 0,
160224
160466
  payload: v3StartedPayload(event.role, event.attrs, itemType, event.kind)
160225
160467
  };
@@ -160253,6 +160495,11 @@ var init_live_chat = __esm({
160253
160495
  // 账本这一次写用掉的那一格钟——**库里那一行的 version 就是它**,快照与增量流同一套版本空间。
160254
160496
  itemVersion: outcome.version,
160255
160497
  ...ord !== void 0 ? { ord } : {},
160498
+ /* 归属:账本写进 `chat_items.message_id` 的那个值,原样带出去(2026-09-11)。
160499
+ 此前只有快照带它,于是「面板挂载之后才生出来的 item」在客户端永远没有归属——
160500
+ 右侧子会话面板靠它把 DB 详情与实时桶合成一条,对不上号就把同一轮画两遍。
160501
+ `null` 不上 wire(省字节):客户端合并层本就按「帧不带 ⇒ 保留已有」处理。 */
160502
+ ...outcome.messageId !== null ? { messageId: outcome.messageId } : {},
160256
160503
  ...offset !== void 0 ? { offset } : {},
160257
160504
  ...payload !== void 0 ? { payload } : {}
160258
160505
  };
@@ -160695,7 +160942,7 @@ var init_live_chat = __esm({
160695
160942
  if (turn.recovering) return say({ accepted: false, reason: "recovering" }, { recovering: true });
160696
160943
  const fn = turn.handle.appendInput;
160697
160944
  if (typeof fn !== "function") return say({ accepted: false, reason: "unsupported" });
160698
- const pendingItemId = !opts?.system && opts?.clientMessageKey ? `oasis-user:${opts.clientMessageKey}` : void 0;
160945
+ const pendingItemId = !opts?.requireConsumptionReceipt && !opts?.system && opts?.clientMessageKey ? `oasis-user:${opts.clientMessageKey}` : void 0;
160699
160946
  const pendingAttachments = opts?.attachments?.map((a) => ({
160700
160947
  name: a.name,
160701
160948
  ...a.blobRef ? { blobRef: a.blobRef } : {},
@@ -160726,17 +160973,75 @@ var init_live_chat = __esm({
160726
160973
  payload: { role: "user" }
160727
160974
  });
160728
160975
  };
160976
+ let confirmed2 = false;
160977
+ let consumedItemId;
160978
+ let receiptPromise;
160979
+ const receiptKey = opts?.requireConsumptionReceipt && !opts.system ? opts.clientMessageKey : void 0;
160980
+ if (opts?.requireConsumptionReceipt && !opts.system && !receiptKey) {
160981
+ return say({ accepted: false, reason: "consumption-unsupported" });
160982
+ }
160983
+ if (receiptKey) {
160984
+ const existing = turn.inputReceipts.get(receiptKey);
160985
+ if (existing) {
160986
+ const consumed = await existing.result;
160987
+ await turn.items?.drain();
160988
+ return consumed && existing.itemId && turn.items?.ordFor(existing.itemId) !== void 0 ? { accepted: true } : { accepted: false, reason: "unconfirmed" };
160989
+ }
160990
+ let resolveReceipt;
160991
+ receiptPromise = new Promise((resolve10) => {
160992
+ resolveReceipt = resolve10;
160993
+ });
160994
+ turn.inputReceipts.set(receiptKey, {
160995
+ get itemId() {
160996
+ return consumedItemId;
160997
+ },
160998
+ result: receiptPromise,
160999
+ consume: () => {
161000
+ if (confirmed2 || turn.status !== "running") return;
161001
+ confirmed2 = true;
161002
+ consumedItemId = this.commitUserInput(turn, text5, opts);
161003
+ resolveReceipt(true);
161004
+ },
161005
+ abandon: () => {
161006
+ resolveReceipt(false);
161007
+ }
161008
+ });
161009
+ }
161010
+ const clearReceipt = () => {
161011
+ if (!receiptKey) return;
161012
+ turn.inputReceipts.get(receiptKey)?.abandon();
161013
+ turn.inputReceipts.delete(receiptKey);
161014
+ };
160729
161015
  let res;
160730
161016
  try {
160731
161017
  res = await fn.call(turn.handle, {
160732
161018
  text: text5,
161019
+ ...receiptKey ? { requireConsumptionReceipt: true } : {},
160733
161020
  ...opts?.clientMessageKey ? { clientMessageKey: opts.clientMessageKey } : {},
160734
161021
  ...opts?.files && Object.keys(opts.files).length ? { files: opts.files } : {},
160735
161022
  ...opts?.binaryFiles && Object.keys(opts.binaryFiles).length ? { binaryFiles: opts.binaryFiles } : {}
160736
161023
  });
160737
161024
  } catch (e) {
161025
+ if (!receiptKey) clearReceipt();
160738
161026
  failPending();
160739
- return say({ accepted: false, reason: "rejected" }, { err: String(e) });
161027
+ if (confirmed2) await turn.items?.drain();
161028
+ confirmed2 = confirmed2 && !!consumedItemId && turn.items?.ordFor(consumedItemId) !== void 0;
161029
+ return say({ accepted: confirmed2, ...!confirmed2 ? { reason: receiptKey ? "unconfirmed" : "rejected" } : {} }, { err: String(e) });
161030
+ }
161031
+ if (receiptPromise) {
161032
+ const notSent = ["unsupported", "consumption-unsupported", "session-closing", "no-live-turn", "recovering"];
161033
+ if (!confirmed2 && !res.accepted && notSent.includes(res.reason ?? "")) {
161034
+ clearReceipt();
161035
+ failPending();
161036
+ return say(res);
161037
+ }
161038
+ if (!await receiptPromise) {
161039
+ clearReceipt();
161040
+ failPending();
161041
+ return say({ accepted: false, reason: "unconfirmed" });
161042
+ }
161043
+ await turn.items?.drain();
161044
+ return say(!consumedItemId || turn.items?.ordFor(consumedItemId) === void 0 ? { accepted: false, reason: "unconfirmed" } : { accepted: true });
160740
161045
  }
160741
161046
  if (!res.accepted) {
160742
161047
  failPending();
@@ -160747,6 +161052,10 @@ var init_live_chat = __esm({
160747
161052
  failPending();
160748
161053
  return say({ accepted: false, reason: "turn-finished" }, { turnStatus: turn.status, replaced: this.turns.get(chatSessionId) !== turn });
160749
161054
  }
161055
+ this.commitUserInput(turn, text5, opts);
161056
+ return { accepted: true };
161057
+ }
161058
+ commitUserInput(turn, text5, opts) {
160750
161059
  const shown = opts?.displayText ?? text5;
160751
161060
  const userPart = {
160752
161061
  type: "user",
@@ -160755,7 +161064,7 @@ var init_live_chat = __esm({
160755
161064
  };
160756
161065
  const segmentEnd = (() => {
160757
161066
  try {
160758
- return turn.items?.closeSegmentBeforeUserInput() ?? null;
161067
+ return turn.items?.closeSegmentBeforeUserInput(!!opts?.requireConsumptionReceipt && !!turn.onUserInput) ?? null;
160759
161068
  } catch {
160760
161069
  return null;
160761
161070
  }
@@ -160771,8 +161080,9 @@ var init_live_chat = __esm({
160771
161080
  payload: { code: "segment_end", note: SEGMENT_END_NOTE }
160772
161081
  });
160773
161082
  }
161083
+ let userItemId;
160774
161084
  try {
160775
- turn.items?.recordUserInput(shown, opts?.attachments, opts?.clientKeys);
161085
+ userItemId = turn.items?.recordUserInput(shown, opts?.attachments, opts?.clientKeys);
160776
161086
  } catch {
160777
161087
  }
160778
161088
  const withSeq = { ...userPart, seq: ++turn.seq };
@@ -160806,7 +161116,7 @@ var init_live_chat = __esm({
160806
161116
  turn.onUserInput?.(shown, opts?.attachments);
160807
161117
  } catch {
160808
161118
  }
160809
- return { accepted: true };
161119
+ return userItemId;
160810
161120
  }
160811
161121
  /** 反查该会话当前驻留那一轮的 runId(无进行中/grace 轮,或该轮无 runId 时 undefined)。
160812
161122
  * 建单草案 submit 发生在这一轮内:用它给草案盖上「产出轮次」标记,供 friday 重进对话页把每版
@@ -160952,18 +161262,32 @@ var init_chat_attachment_files = __esm({
160952
161262
  });
160953
161263
 
160954
161264
  // ../server/src/domains/chat-sessions/append-now.ts
160955
- async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, attachments) {
161265
+ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, attachments, retainLocallyOnFailure = false) {
160956
161266
  const newId = deps.newId ?? import_node_crypto20.randomUUID;
160957
161267
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
160958
- await deps.store.enqueuePendingMessage({
160959
- id: newId(),
161268
+ if (!deps.store.claimPendingInsert) {
161269
+ return { accepted: false, reason: "queue-unavailable", queued: false, delivered: "", batch: [] };
161270
+ }
161271
+ const { submitted, batch } = await deps.store.claimPendingInsert({
161272
+ id: clientKey ? `append-request:${(0, import_node_crypto20.createHash)("sha256").update(JSON.stringify([chatSessionId, clientKey])).digest("hex")}` : newId(),
160960
161273
  chatSessionId,
160961
161274
  text: text5,
160962
161275
  ...attachments?.length ? { attachments } : {},
160963
161276
  ...clientKey ? { clientKey } : {},
160964
161277
  createdAt: now()
160965
161278
  });
160966
- const batch = await deps.store.listPendingMessages(chatSessionId);
161279
+ if (!batch.length) {
161280
+ const state = submitted.state ?? "queued";
161281
+ const returned = state === "cancelled" && submitted.stateReason?.startsWith("client-retained:");
161282
+ const reason = returned ? submitted.stateReason.slice("client-retained:".length) : "unconfirmed";
161283
+ return {
161284
+ accepted: state === "delivered",
161285
+ ...state !== "delivered" ? { reason } : {},
161286
+ queued: !returned,
161287
+ delivered: "",
161288
+ batch: [submitted]
161289
+ };
161290
+ }
160967
161291
  const prepared = await buildChatAttachmentFiles(
160968
161292
  batch.flatMap((m2) => m2.attachments ?? []),
160969
161293
  deps.readBlob ?? (async (ref2) => {
@@ -160979,7 +161303,6 @@ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, atta
160979
161303
  })));
160980
161304
  const batchKey = `append:${chatSessionId}:${batch.map((m2) => m2.id).join(",")}`;
160981
161305
  const ids2 = batch.map((m2) => m2.id);
160982
- await deps.store.markPendingMessages?.(chatSessionId, ids2, "delivering").catch(() => void 0);
160983
161306
  const clientKeys = batch.map((m2) => m2.clientKey).filter((k2) => !!k2);
160984
161307
  const res = await deps.deliver(merged, batchKey, {
160985
161308
  ...clientKeys.length ? { clientKeys } : {},
@@ -160988,10 +161311,19 @@ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, atta
160988
161311
  ...displayText !== merged ? { displayText } : {},
160989
161312
  ...attachmentRefs.length ? { attachments: attachmentRefs } : {}
160990
161313
  });
160991
- const DEFINITELY_NOT_DELIVERED = /* @__PURE__ */ new Set(["unsupported", "no-live-turn", "session-closing", "turn-finished", "recovering"]);
161314
+ const DEFINITELY_NOT_DELIVERED = /* @__PURE__ */ new Set(["unsupported", "no-live-turn", "session-closing", "turn-finished", "recovering", "consumption-unsupported"]);
160992
161315
  const nextState = res.accepted ? "delivered" : DEFINITELY_NOT_DELIVERED.has(res.reason ?? "") ? "queued" : "unconfirmed";
160993
- await deps.store.markPendingMessages?.(chatSessionId, ids2, nextState, res.reason).catch(() => void 0);
160994
- return { ...res, queued: true, delivered: merged, batch, ...prepared.paths.length ? { attachmentPaths: prepared.paths } : {} };
161316
+ const returnToClient = retainLocallyOnFailure && !!clientKey && nextState === "queued";
161317
+ if (returnToClient && deps.store.markPendingMessages) {
161318
+ await deps.store.markPendingMessages(chatSessionId, [submitted.id], "cancelled", `client-retained:${res.reason}`);
161319
+ await deps.store.markPendingMessages(chatSessionId, ids2.filter((id) => id !== submitted.id), nextState, res.reason);
161320
+ } else {
161321
+ const transition = deps.store.markPendingMessages?.(chatSessionId, ids2, nextState, res.reason);
161322
+ if (retainLocallyOnFailure) await transition;
161323
+ else await transition?.catch(() => void 0);
161324
+ if (returnToClient) await deps.store.removePendingMessage(chatSessionId, submitted.id);
161325
+ }
161326
+ return { ...res, queued: !returnToClient, delivered: merged, batch, ...prepared.paths.length ? { attachmentPaths: prepared.paths } : {} };
160995
161327
  }
160996
161328
  var import_node_crypto20;
160997
161329
  var init_append_now = __esm({
@@ -195639,7 +195971,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195639
195971
  out.listDirCode = located.code;
195640
195972
  if (located.location) out.location = located.location;
195641
195973
  out.skipped.push({
195642
- source: `${DELEGATION_OUTPUTS_DIR}/`,
195974
+ source: `${SESSION_OUTPUTS_DIR}/`,
195643
195975
  code: located.code,
195644
195976
  message: located.message ?? locationFailureMessage(located.code)
195645
195977
  });
@@ -195647,7 +195979,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195647
195979
  }
195648
195980
  out.location = located.location;
195649
195981
  }
195650
- const listing = await port.listDir(child, DELEGATION_OUTPUTS_DIR).catch((err) => ({
195982
+ const listing = await port.listDir(child, SESSION_OUTPUTS_DIR).catch((err) => ({
195651
195983
  ok: false,
195652
195984
  code: "INTERNAL",
195653
195985
  message: err instanceof Error ? err.message : String(err)
@@ -195657,11 +195989,11 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195657
195989
  if (listing.code === "NOT_FOUND") return out;
195658
195990
  const code2 = listing.code;
195659
195991
  const message = isLocationFailureCode(code2) ? listing.message ?? locationFailureMessage(code2) : listing.message ?? code2;
195660
- out.skipped.push({ source: `${DELEGATION_OUTPUTS_DIR}/`, code: code2, message });
195992
+ out.skipped.push({ source: `${SESSION_OUTPUTS_DIR}/`, code: code2, message });
195661
195993
  return out;
195662
195994
  }
195663
195995
  out.listDirCode = "OK";
195664
- const queue = [DELEGATION_OUTPUTS_DIR];
195996
+ const queue = [SESSION_OUTPUTS_DIR];
195665
195997
  const files = [];
195666
195998
  let guard = 0;
195667
195999
  let hitFileCap = false;
@@ -195672,7 +196004,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195672
196004
  }
195673
196005
  guard += 1;
195674
196006
  const dir = queue.shift();
195675
- const level = dir === DELEGATION_OUTPUTS_DIR ? listing : await port.listDir(child, dir).catch(() => ({ ok: false, code: "INTERNAL" }));
196007
+ const level = dir === SESSION_OUTPUTS_DIR ? listing : await port.listDir(child, dir).catch(() => ({ ok: false, code: "INTERNAL" }));
195676
196008
  if (!level.ok) {
195677
196009
  out.skipped.push({ source: `${dir}/`, code: level.code, message: level.code });
195678
196010
  continue;
@@ -195698,7 +196030,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195698
196030
  });
195699
196031
  }
195700
196032
  for (const file of files) {
195701
- const relative5 = normalizeRelPath(file.rel.slice(DELEGATION_OUTPUTS_DIR.length + 1));
196033
+ const relative5 = normalizeRelPath(file.rel.slice(SESSION_OUTPUTS_DIR.length + 1));
195702
196034
  if (!relative5) {
195703
196035
  out.skipped.push({ source: file.rel, code: "PATH_REJECTED", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF0C\u6CA1\u642C" });
195704
196036
  continue;
@@ -203939,7 +204271,7 @@ async function startOasisServer(opts) {
203939
204271
  const leftWaiterIds = /* @__PURE__ */ new Map();
203940
204272
  const LEFT_WAITER_IDS_MAX_DRAFTS = 512;
203941
204273
  const terminalReviews = /* @__PURE__ */ new Map();
203942
- const notifyReviewResolved = (draftId, r, resume, storeFor) => {
204274
+ const notifyReviewResolved = (draftId, r, resume, storeFor, companyId) => {
203943
204275
  if (terminalReviews.has(draftId)) return;
203944
204276
  terminalReviews.set(draftId, r);
203945
204277
  if (terminalReviews.size > 512) {
@@ -203959,7 +204291,7 @@ async function startOasisServer(opts) {
203959
204291
  leftWaiterIds.delete(draftId);
203960
204292
  if (resume?.origin) {
203961
204293
  const needsAgentDelivery = resume.kind === "command" && !hadWaiters;
203962
- void settleAuthorizationStatusLine(resume.origin, r, resume, draftId, needsAgentDelivery, storeFor);
204294
+ void settleAuthorizationStatusLine(resume.origin, r, resume, draftId, needsAgentDelivery, storeFor, companyId);
203963
204295
  }
203964
204296
  };
203965
204297
  const resolveEngine = opts.resolveEngine ?? (async () => ({ kernel: opts.kernel, oplog: opts.oplog, blobs: opts.blobs, ...opts.registry ? { registry: opts.registry } : {} }));
@@ -204037,6 +204369,14 @@ async function startOasisServer(opts) {
204037
204369
  }
204038
204370
  return null;
204039
204371
  };
204372
+ const outputsPartFor = (chatSessionId, companyId) => {
204373
+ const scan2 = opts.sessionOutputsScan;
204374
+ if (!scan2) return void 0;
204375
+ return async () => {
204376
+ const files = await scan2({ chatSessionId, ...companyId !== void 0 ? { companyId } : {} });
204377
+ return files && files.length > 0 ? { type: "outputs", seq: 0, outputs: files } : null;
204378
+ };
204379
+ };
204040
204380
  async function anchorMessageIdFor(origin, storeFor) {
204041
204381
  if (!origin) return void 0;
204042
204382
  const live = liveChat.messageFor(origin);
@@ -204053,7 +204393,7 @@ async function startOasisServer(opts) {
204053
204393
  }
204054
204394
  return void 0;
204055
204395
  }
204056
- const appendNowThroughQueue2 = async (sid, text5, clientKey, attachments, perCompanyStore) => {
204396
+ const appendNowThroughQueue2 = async (sid, text5, clientKey, attachments, perCompanyStore, retainLocallyOnFailure = false) => {
204057
204397
  if (perCompanyStore === null) {
204058
204398
  throw new Error(
204059
204399
  `appendNowThroughQueue: per-company chat store unresolved (sid=${sid})`
@@ -204067,7 +204407,7 @@ async function startOasisServer(opts) {
204067
204407
  store,
204068
204408
  // ADR-0166 D9.1:附件与正文**同一次**送出——拆开就等于让 agent 拿着「见附件」而
204069
204409
  // 附件不存在的描述去干活。`files` 空时这次调用与从前逐字相同。
204070
- deliver: (merged, clientMessageKey, extra) => liveChat.append(sid, merged, { clientMessageKey, ...extra }),
204410
+ deliver: (merged, clientMessageKey, extra) => liveChat.append(sid, merged, { clientMessageKey, ...extra, requireConsumptionReceipt: true }),
204071
204411
  // **先查资产柜再查事实柜**——chat 附件是 `uploadChatAttachment` 存进**资产柜**的
204072
204412
  // (serve 那侧 `assets.put`),两个柜是**物理隔离**的(「资产柜与事实柜分开」)。
204073
204413
  // 只查 `opts.blobs` 必然取不到,于是每张图都被报成「附件没能取回来」。
@@ -204082,8 +204422,8 @@ async function startOasisServer(opts) {
204082
204422
  }
204083
204423
  throw new Error(`blob ${blobRef} \u4E24\u4E2A\u67DC\u91CC\u90FD\u6CA1\u6709`);
204084
204424
  }
204085
- }, clientKey, attachments);
204086
- return { accepted: r.accepted, ...r.reason ? { reason: r.reason } : {}, queued: true };
204425
+ }, clientKey, attachments, retainLocallyOnFailure);
204426
+ return { accepted: r.accepted, ...r.reason ? { reason: r.reason } : {}, queued: r.queued };
204087
204427
  };
204088
204428
  const GRAPH_EDIT_SUMMARY_MAX = 60;
204089
204429
  const truncateSummary = (s2) => {
@@ -204101,7 +204441,7 @@ async function startOasisServer(opts) {
204101
204441
  const label = ctx.effectLabel || ctx.command || "";
204102
204442
  return approved ? `\u547D\u4EE4\u5DF2\u7531 ${by} \u6279\u51C6\u5E76\u4EE5\u5176\u540D\u4E49\u6267\u884C${label ? `\uFF1A${label}` : ""}\u3002` : `\u547D\u4EE4\u5DF2\u88AB ${by} \u9A73\u56DE${reason ? `\uFF1A${reason}` : ""}\u3002`;
204103
204443
  };
204104
- const settleAuthorizationStatusLine = async (origin, resolution, ctx, draftId, needsAgentDelivery, storeFor) => {
204444
+ const settleAuthorizationStatusLine = async (origin, resolution, ctx, draftId, needsAgentDelivery, storeFor, companyId) => {
204105
204445
  const store = storeFor ? await storeFor().catch(() => null) : opts.chatSession;
204106
204446
  if (!store) return;
204107
204447
  const session = await store.getSession(origin).catch(() => null);
@@ -204174,10 +204514,12 @@ async function startOasisServer(opts) {
204174
204514
  ...dispatched.supportsLiveProtocol !== void 0 ? { supportsLiveProtocol: dispatched.supportsLiveProtocol } : {},
204175
204515
  done: dispatched.done
204176
204516
  };
204517
+ const resumeOutputsPart = outputsPartFor(origin, companyId);
204177
204518
  runBackgroundAssistantChatTurn({
204178
204519
  chatStore: store,
204179
204520
  chatSessionId: origin,
204180
- session: bgSession
204521
+ session: bgSession,
204522
+ ...resumeOutputsPart ? { outputsPart: resumeOutputsPart } : {}
204181
204523
  });
204182
204524
  await markDelivered();
204183
204525
  };
@@ -204433,10 +204775,12 @@ async function startOasisServer(opts) {
204433
204775
  ...session.supportsLiveProtocol !== void 0 ? { supportsLiveProtocol: session.supportsLiveProtocol } : {},
204434
204776
  ...session.done ? { done: session.done } : {}
204435
204777
  };
204778
+ const broadcastOutputsPart = outputsPartFor(sessionId, companyId);
204436
204779
  runBackgroundAssistantChatTurn({
204437
204780
  chatStore: store,
204438
204781
  chatSessionId: sessionId,
204439
204782
  session: bgSession,
204783
+ ...broadcastOutputsPart ? { outputsPart: broadcastOutputsPart } : {},
204440
204784
  liveChat,
204441
204785
  liveHandle: {
204442
204786
  runtimeSessionId: session.id,
@@ -205414,7 +205758,8 @@ async function startOasisServer(opts) {
205414
205758
  draftId,
205415
205759
  { status: "approved", command, effectLabel: review.command.effectLabel, authorizedBy: actor, receipt },
205416
205760
  { origin: review.origin, kind: "command", authorizedBy: actor, effectLabel: review.command.effectLabel, command },
205417
- chatStoreNowOrNull
205761
+ chatStoreNowOrNull,
205762
+ currentCompanyId
205418
205763
  );
205419
205764
  res.writeHead(200, { "content-type": "application/json" }).end(
205420
205765
  JSON.stringify({ applied: 1, command, artifactId, draftId, draftedBy: review.proposedBy, authorizedBy: actor })
@@ -206240,9 +206585,11 @@ async function startOasisServer(opts) {
206240
206585
  let text5 = "";
206241
206586
  let clientKey;
206242
206587
  let insertAttachments = [];
206588
+ let retainLocallyOnFailure = false;
206243
206589
  try {
206244
206590
  const b2 = JSON.parse(rawBody);
206245
206591
  text5 = String(b2.text ?? "").trim();
206592
+ retainLocallyOnFailure = b2.retainLocallyOnFailure === true;
206246
206593
  if (typeof b2.clientKey === "string" && b2.clientKey) clientKey = b2.clientKey;
206247
206594
  insertAttachments = (Array.isArray(b2.attachments) ? b2.attachments : []).filter((a) => !!a && typeof a === "object" && typeof a["name"] === "string" && (typeof a["text"] === "string" || typeof a["blobRef"] === "string")).map((a) => ({
206248
206595
  name: a["name"],
@@ -206256,7 +206603,7 @@ async function startOasisServer(opts) {
206256
206603
  res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "EMPTY_TEXT" }));
206257
206604
  return;
206258
206605
  }
206259
- const result = await appendNowThroughQueue2(sid, text5, clientKey, insertAttachments, perCompanyStore);
206606
+ const result = await appendNowThroughQueue2(sid, text5, clientKey, insertAttachments, perCompanyStore, retainLocallyOnFailure);
206260
206607
  res.writeHead(result.accepted ? 200 : 409, { "content-type": "application/json" }).end(JSON.stringify(result));
206261
206608
  return;
206262
206609
  }
@@ -206600,6 +206947,7 @@ ${composed}`;
206600
206947
  await checkpointChain.catch(() => void 0);
206601
206948
  await itemLedger.drain();
206602
206949
  const projectedContent = itemLedger.projectContent();
206950
+ const chatOutputsPart = outputsPartFor(persistTarget.id, currentCompanyId);
206603
206951
  await finalizeAssistantRow({
206604
206952
  chatStore,
206605
206953
  chatSessionId: persistTarget.id,
@@ -206609,6 +206957,7 @@ ${composed}`;
206609
206957
  status,
206610
206958
  ...session.runId ? { runId: session.runId } : {},
206611
206959
  ...heldTurn ? { turnId: heldTurn.id } : {},
206960
+ ...chatOutputsPart ? { outputsPart: chatOutputsPart } : {},
206612
206961
  now: () => finishedAt
206613
206962
  });
206614
206963
  await heldTurn?.settle({
@@ -213608,6 +213957,17 @@ var init_dev_store = __esm({
213608
213957
  /** ADR-0166 D6 待发队列(每会话一条,按 seq 升序 = 入队序)。 */
213609
213958
  pending = /* @__PURE__ */ new Map();
213610
213959
  pendingSeq = 0;
213960
+ async claimPendingInsert(m2) {
213961
+ const list2 = this.pending.get(m2.chatSessionId) ?? [];
213962
+ const prior = list2.find((x2) => x2.id === m2.id) ?? list2.find((x2) => m2.clientKey && x2.clientKey === m2.clientKey && (x2.state ?? "queued") === "queued");
213963
+ if (prior) return { submitted: prior, batch: [] };
213964
+ const submitted = { ...m2, state: "queued", stateAt: m2.createdAt, seq: ++this.pendingSeq };
213965
+ const all2 = [...list2, submitted];
213966
+ const batch = all2.filter((x2) => (x2.state ?? "queued") === "queued");
213967
+ this.pending.set(m2.chatSessionId, all2);
213968
+ this.setState(m2.chatSessionId, new Set(batch.map((x2) => x2.id)), "delivering", "append-now");
213969
+ return { submitted, batch };
213970
+ }
213611
213971
  async enqueuePendingMessage(m2) {
213612
213972
  const list2 = this.pending.get(m2.chatSessionId) ?? [];
213613
213973
  if (m2.clientKey) {
@@ -213975,6 +214335,11 @@ var init_dev_store = __esm({
213975
214335
  return r;
213976
214336
  }
213977
214337
  // 待发队列也要落盘——它存在的全部理由就是"别只活在一处内存里"(ADR-0166 D6)。
214338
+ async claimPendingInsert(m2) {
214339
+ const result = await super.claimPendingInsert(m2);
214340
+ this.save();
214341
+ return result;
214342
+ }
213978
214343
  async enqueuePendingMessage(m2) {
213979
214344
  const record8 = await super.enqueuePendingMessage(m2);
213980
214345
  this.save();
@@ -214822,6 +215187,74 @@ var init_company_scoped_registry = __esm({
214822
215187
  }
214823
215188
  });
214824
215189
 
215190
+ // ../server/src/domains/chat-sessions/outputs-scan.ts
215191
+ function joinRel(dir, name) {
215192
+ return dir ? `${dir}/${name}` : name;
215193
+ }
215194
+ async function scanSessionOutputs(bridge, loc, now = Date.now) {
215195
+ const deadline = now() + BUDGET_MS;
215196
+ const out = [];
215197
+ let listCalls = 0;
215198
+ const listDir = (path41) => {
215199
+ listCalls += 1;
215200
+ return bridge.list(loc.nodeId, {
215201
+ workdirKey: loc.workdirKey,
215202
+ runtimeKind: loc.runtimeKind,
215203
+ rtId: loc.rtId,
215204
+ path: path41
215205
+ });
215206
+ };
215207
+ const root = await listDir(SESSION_OUTPUTS_DIR).catch(() => null);
215208
+ if (!root) return null;
215209
+ if (!root.ok) {
215210
+ return root.code === "NOT_FOUND" ? [] : null;
215211
+ }
215212
+ let level = [{ path: SESSION_OUTPUTS_DIR, outcome: root }];
215213
+ for (let depth = 0; depth <= MAX_DEPTH; depth += 1) {
215214
+ const nextDirs = [];
215215
+ for (const { path: dir, outcome } of level) {
215216
+ if (!outcome.ok) continue;
215217
+ for (const entry of outcome.entries) {
215218
+ if (entry.type === "dir") {
215219
+ nextDirs.push(joinRel(dir, entry.name));
215220
+ continue;
215221
+ }
215222
+ if (out.length >= MAX_ENTRIES) continue;
215223
+ out.push({ path: joinRel(dir, entry.name), size: entry.size, mtime: entry.mtime });
215224
+ }
215225
+ }
215226
+ if (nextDirs.length === 0 || depth === MAX_DEPTH) break;
215227
+ if (out.length >= MAX_ENTRIES || listCalls >= MAX_LIST_CALLS || now() >= deadline) break;
215228
+ const batch = [];
215229
+ for (const dir of nextDirs) {
215230
+ if (listCalls >= MAX_LIST_CALLS || now() >= deadline) break;
215231
+ const outcome = await listDir(dir).catch(() => null);
215232
+ if (outcome) batch.push({ path: dir, outcome });
215233
+ }
215234
+ level = batch;
215235
+ }
215236
+ return out;
215237
+ }
215238
+ async function settleSessionOutputs(deps) {
215239
+ const scanned = await scanSessionOutputs(deps.bridge, deps.loc, deps.now);
215240
+ if (scanned === null) return null;
215241
+ const previous3 = await deps.readFingerprints(deps.sessionId).catch(() => null);
215242
+ const diff = diffSessionOutputs(scanned, previous3 ?? void 0);
215243
+ await deps.writeFingerprints(deps.sessionId, diff.fingerprints).catch(() => void 0);
215244
+ return diff;
215245
+ }
215246
+ var MAX_DEPTH, MAX_ENTRIES, MAX_LIST_CALLS, BUDGET_MS;
215247
+ var init_outputs_scan = __esm({
215248
+ "../server/src/domains/chat-sessions/outputs-scan.ts"() {
215249
+ "use strict";
215250
+ init_src();
215251
+ MAX_DEPTH = 3;
215252
+ MAX_ENTRIES = 100;
215253
+ MAX_LIST_CALLS = 12;
215254
+ BUDGET_MS = 5e3;
215255
+ }
215256
+ });
215257
+
214825
215258
  // ../server/src/org-model-backfill.ts
214826
215259
  function resolveDisplayName(existing, humanActor, accountName) {
214827
215260
  const pick3 = existing?.trim() || humanActor?.name?.trim() || accountName?.trim();
@@ -218446,6 +218879,10 @@ var init_daemon_hub = __esm({
218446
218879
  this.onHandshakeRejected?.(rejection);
218447
218880
  cb(false, code2, message);
218448
218881
  }
218882
+ /** Ask the running session: an upgrade leaves existing sessions alive. */
218883
+ supportsInputConsumptionReceipts(dispatchId) {
218884
+ return this.hasSession(dispatchId) && this.sessionMeta.get(dispatchId)?.supportsInputConsumptionReceipts === true;
218885
+ }
218449
218886
  dispatch(daemonId, msg) {
218450
218887
  const daemon = this.daemons.get(daemonId);
218451
218888
  if (!daemon) return false;
@@ -218515,6 +218952,7 @@ var init_daemon_hub = __esm({
218515
218952
  nodeId,
218516
218953
  sessionId: msg.sessionId,
218517
218954
  connectedAt: Date.now(),
218955
+ supportsInputConsumptionReceipts: msg.supportsInputConsumptionReceipts === true,
218518
218956
  ...msg.jobArtifactId ? { jobArtifactId: msg.jobArtifactId } : {}
218519
218957
  });
218520
218958
  for (const l of this.sessionConnectListeners) l({ dispatchId, sessionId: msg.sessionId, nodeId });
@@ -221241,11 +221679,11 @@ function aggregateUsageByActor(runs) {
221241
221679
  function weekWindow(now = /* @__PURE__ */ new Date()) {
221242
221680
  return { from: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3).toISOString(), to: now.toISOString() };
221243
221681
  }
221244
- async function listWeeklyUsageByActor(trace) {
221682
+ async function listWeeklyUsageByActor(trace, artifactIds) {
221245
221683
  if (!trace?.listUsageRuns) return null;
221246
221684
  const { from, to } = weekWindow();
221247
221685
  try {
221248
- return aggregateUsageByActor(await trace.listUsageRuns({ from, to }));
221686
+ return aggregateUsageByActor(await trace.listUsageRuns({ from, to, ...artifactIds ? { artifactIds: [...artifactIds] } : {} }));
221249
221687
  } catch {
221250
221688
  return null;
221251
221689
  }
@@ -221847,9 +222285,15 @@ function actorsDomain(opts) {
221847
222285
  const revealLimiter = new RevealRateLimiter(revealLimits.perSession, revealLimits.perSessionPerKey);
221848
222286
  const consoleRevealLimits = opts.credentialRevealLimits?.console ?? { perSession: 60, perSessionPerKey: 10 };
221849
222287
  const consoleRevealLimiter = new RevealRateLimiter(consoleRevealLimits.perSession, consoleRevealLimits.perSessionPerKey);
222288
+ async function usageFor(companyId, ctx) {
222289
+ const source = opts.resolveTrace ? await opts.resolveTrace(companyId) : { store: opts.trace?.(), companyScoped: false };
222290
+ const artifactIds = companyId && !source?.companyScoped ? new Set(ctx.getModel?.().artifacts.keys() ?? []) : void 0;
222291
+ return { usage: source?.store, ...artifactIds ? { artifactIds } : {} };
222292
+ }
221850
222293
  return (router) => {
221851
222294
  router.get("/api/actors", async (req) => {
221852
- const { service } = await resolveCtx(req.auth.companyId);
222295
+ const ctx = await resolveCtx(req.auth.companyId);
222296
+ const { service } = ctx;
221853
222297
  const filter = {};
221854
222298
  const kind = req.query.get("kind");
221855
222299
  const teamId = req.query.get("team_id");
@@ -221861,9 +222305,10 @@ function actorsDomain(opts) {
221861
222305
  const includeDeleted = req.query.get("include_deleted") === "1";
221862
222306
  const items = status || includeDeleted ? all2 : all2.filter((a) => a.kind !== "agent" || !isDeletedActor(a));
221863
222307
  const agentIds = items.filter((a) => a.kind === "agent").map((a) => a.id);
222308
+ const usageSource = await usageFor(req.auth.companyId, ctx).catch(() => ({ usage: void 0, artifactIds: void 0 }));
221864
222309
  const [configs, usageByActor] = await Promise.all([
221865
222310
  service.latestConfigs(agentIds),
221866
- listWeeklyUsageByActor(opts.trace?.())
222311
+ listWeeklyUsageByActor(usageSource.usage, usageSource.artifactIds)
221867
222312
  ]);
221868
222313
  return {
221869
222314
  status: 200,
@@ -222539,9 +222984,8 @@ function actorsDomain(opts) {
222539
222984
  return {
222540
222985
  status: 200,
222541
222986
  body: await actorStats(req.params.id, {
222542
- usage: req.auth.companyId && !model ? void 0 : opts.trace?.(),
222543
- model,
222544
- ...req.auth.companyId ? { artifactIds: new Set(model?.artifacts.keys() ?? []) } : {}
222987
+ ...await usageFor(req.auth.companyId, ctx),
222988
+ model
222545
222989
  })
222546
222990
  };
222547
222991
  });
@@ -223125,7 +223569,7 @@ function createActorsDomain(opts) {
223125
223569
  return {
223126
223570
  service: defaultCtx.service,
223127
223571
  resolveCtx,
223128
- register: actorsDomain({ resolveCtx, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.isCompanyManager ? { isCompanyManager: opts.isCompanyManager } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
223572
+ register: actorsDomain({ resolveCtx, ...opts.resolveTrace ? { resolveTrace: opts.resolveTrace } : {}, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.isCompanyManager ? { isCompanyManager: opts.isCompanyManager } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
223129
223573
  };
223130
223574
  }
223131
223575
  var import_node_crypto49;
@@ -223300,6 +223744,16 @@ var init_assistant_tier_dispatch = __esm({
223300
223744
  // ../server/src/domains/projects/routes.ts
223301
223745
  function projectsDomain(opts) {
223302
223746
  const svc = async (req) => opts.resolveService ? opts.resolveService(req.auth.companyId) : opts.service;
223747
+ const requireProjectManagePermission = async (req, projectId2, action) => {
223748
+ if (!opts.resolveMemberRole) return;
223749
+ if (isUncategorizedProjectId(projectId2)) return;
223750
+ const project = await (await svc(req)).getProject(projectId2);
223751
+ if (!project) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u9879\u76EE\uFF1A${projectId2}`);
223752
+ if (project.createdBy && project.createdBy.id === req.auth.actor) return;
223753
+ const role = await opts.resolveMemberRole(req.auth.companyId, req.auth.actor);
223754
+ if (role === "owner" || role === "admin") return;
223755
+ throw new ApiError(403, action.code, `\u4F60\u6CA1\u6709${action.verb}\u8BE5\u9879\u76EE\u7684\u6743\u9650\uFF0C\u8BF7\u8054\u7CFB\u7EC4\u7EC7\u7BA1\u7406\u5458`);
223756
+ };
223303
223757
  return (router) => {
223304
223758
  router.post("/api/projects", async (req) => {
223305
223759
  const body2 = req.body;
@@ -223341,6 +223795,7 @@ function projectsDomain(opts) {
223341
223795
  return { status: 200, body: toApiProject(project) };
223342
223796
  });
223343
223797
  router.patch("/api/projects/:id", async (req) => {
223798
+ await requireProjectManagePermission(req, req.params.id, { verb: "\u7F16\u8F91", code: "PROJECT_EDIT_FORBIDDEN" });
223344
223799
  const body2 = req.body ?? {};
223345
223800
  for (const immutable of ["id", "project_id", "projectId", "createdBy", "created_by", "ownerId", "owner_id"]) {
223346
223801
  if (Object.prototype.hasOwnProperty.call(body2, immutable)) {
@@ -223360,6 +223815,7 @@ function projectsDomain(opts) {
223360
223815
  }
223361
223816
  });
223362
223817
  router.delete("/api/projects/:id", async (req) => {
223818
+ await requireProjectManagePermission(req, req.params.id, { verb: "\u5220\u9664", code: "PROJECT_DELETE_FORBIDDEN" });
223363
223819
  try {
223364
223820
  const result = await (await svc(req)).deleteProject(req.params.id);
223365
223821
  return {
@@ -228356,7 +228812,7 @@ function reasonFromWorkdirCode(code2) {
228356
228812
  return "ERROR";
228357
228813
  }
228358
228814
  }
228359
- function joinRel(dir, name) {
228815
+ function joinRel2(dir, name) {
228360
228816
  return dir ? `${dir}/${name}` : name;
228361
228817
  }
228362
228818
  async function collectWorkdirFiles(bridge, loc, now = Date.now) {
@@ -228389,14 +228845,14 @@ async function collectWorkdirFiles(bridge, loc, now = Date.now) {
228389
228845
  if (outcome.truncated) truncated = true;
228390
228846
  for (const entry of outcome.entries) {
228391
228847
  if (entry.type === "dir") {
228392
- nextDirs.push(joinRel(dir, entry.name));
228848
+ nextDirs.push(joinRel2(dir, entry.name));
228393
228849
  continue;
228394
228850
  }
228395
228851
  if (items.length >= WORKDIR_WALK_MAX_ENTRIES) {
228396
228852
  truncated = true;
228397
228853
  continue;
228398
228854
  }
228399
- const rel = joinRel(dir, entry.name);
228855
+ const rel = joinRel2(dir, entry.name);
228400
228856
  items.push({
228401
228857
  id: `workdir:${rel}`,
228402
228858
  name: entry.name,
@@ -228641,7 +229097,8 @@ function createProjectsDomain(opts) {
228641
229097
  register: projectsDomain({
228642
229098
  service,
228643
229099
  ...opts.resolveCompany ? { resolveService } : {},
228644
- ...opts.defaultBranchResolver ? { defaultBranchResolver: opts.defaultBranchResolver } : {}
229100
+ ...opts.defaultBranchResolver ? { defaultBranchResolver: opts.defaultBranchResolver } : {},
229101
+ ...opts.resolveMemberRole ? { resolveMemberRole: opts.resolveMemberRole } : {}
228645
229102
  }),
228646
229103
  ...opts.resolveCompany ? { resolveService } : {}
228647
229104
  };
@@ -231088,6 +231545,9 @@ function nodesDomain(deps) {
231088
231545
  const rts = runtimesByNode.get(n.id) ?? [];
231089
231546
  const daemonVersion = daemonVersionById.get(n.id);
231090
231547
  const connection = connectionById.get(n.id);
231548
+ const nodeOffline = !onlineIds.has(n.id);
231549
+ const lastDisconnectedAt = connection?.lastDisconnectedAt !== void 0 ? new Date(connection.lastDisconnectedAt).toISOString() : nodeOffline ? n.lastSeenAt : void 0;
231550
+ const lastReconnectedAt = connection?.lastReconnectedAt !== void 0 ? new Date(connection.lastReconnectedAt).toISOString() : void 0;
231091
231551
  const activeRunCount = deps.activeRunCountOf?.(n.id) ?? 0;
231092
231552
  const updateStatus = reconcileNodeUpdate(n.id, daemonVersion, activeRunCount, healthNow);
231093
231553
  return {
@@ -231101,10 +231561,10 @@ function nodesDomain(deps) {
231101
231561
  ...sourceIpById.get(n.id) ? { sourceIp: sourceIpById.get(n.id) } : {},
231102
231562
  ...n.name ? { name: n.name } : {},
231103
231563
  ...updateStatus ? { updateStatus } : {},
231104
- ...connection ? {
231564
+ ...lastDisconnectedAt || lastReconnectedAt ? {
231105
231565
  connection: {
231106
- ...connection.lastDisconnectedAt ? { lastDisconnectedAt: new Date(connection.lastDisconnectedAt).toISOString() } : {},
231107
- ...connection.lastReconnectedAt ? { lastReconnectedAt: new Date(connection.lastReconnectedAt).toISOString() } : {}
231566
+ ...lastDisconnectedAt ? { lastDisconnectedAt } : {},
231567
+ ...lastReconnectedAt ? { lastReconnectedAt } : {}
231108
231568
  }
231109
231569
  } : {},
231110
231570
  ...deps.nodeHealth ? {
@@ -239294,6 +239754,9 @@ function routeToSession(hub, nodeId, dispatchId, msg) {
239294
239754
  function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
239295
239755
  const unknownIfInterrupting = input.interrupt ? { interrupt: "unknown" } : {};
239296
239756
  if (entry.exited) return Promise.resolve({ accepted: false, reason: "session-closing", ...unknownIfInterrupting });
239757
+ if (input.requireConsumptionReceipt && !hub.supportsInputConsumptionReceipts?.(dispatchId)) {
239758
+ return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
239759
+ }
239297
239760
  const requestId = (0, import_node_crypto65.randomUUID)();
239298
239761
  const sent = routeToSession(hub, nodeId, dispatchId, { type: "append_input", dispatchId, input, requestId });
239299
239762
  if (!sent) return Promise.resolve({ accepted: false, reason: "session-closing", ...unknownIfInterrupting });
@@ -239301,10 +239764,10 @@ function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
239301
239764
  const timer = setTimeout(() => {
239302
239765
  const i = entry.appendWaiters.findIndex((w2) => w2.timer === timer);
239303
239766
  if (i >= 0) entry.appendWaiters.splice(i, 1);
239304
- resolve10({ accepted: false, reason: "unsupported", ...unknownIfInterrupting });
239767
+ resolve10({ accepted: false, reason: input.requireConsumptionReceipt ? "unconfirmed" : "unsupported", ...unknownIfInterrupting });
239305
239768
  }, timeoutMs);
239306
239769
  timer.unref?.();
239307
- entry.appendWaiters.push({ requestId, resolve: resolve10, timer });
239770
+ entry.appendWaiters.push({ requestId, requireConsumptionReceipt: input.requireConsumptionReceipt, resolve: resolve10, timer });
239308
239771
  });
239309
239772
  }
239310
239773
  var import_node_crypto65, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
@@ -239451,7 +239914,7 @@ var init_daemon_adapter = __esm({
239451
239914
  entry.exited = info;
239452
239915
  for (const w2 of entry.appendWaiters.splice(0)) {
239453
239916
  clearTimeout(w2.timer);
239454
- w2.resolve({ accepted: false, reason: "session-closing" });
239917
+ w2.resolve({ accepted: false, reason: w2.requireConsumptionReceipt ? "unconfirmed" : "session-closing" });
239455
239918
  }
239456
239919
  if (info.reason === "server-unreachable") this.health?.recordUnreachable(entry.nodeId, info.reason);
239457
239920
  else this.health?.recordReachable(entry.nodeId);
@@ -247505,6 +247968,7 @@ var init_postgres_chat_sessions = __esm({
247505
247968
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS escalation_id text`);
247506
247969
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS model text`);
247507
247970
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS visibility text`);
247971
+ await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS outputs_fingerprints jsonb`);
247508
247972
  await pool.query(`
247509
247973
  WITH ranked AS (
247510
247974
  SELECT id, row_number() OVER (PARTITION BY session_id ORDER BY seq, created_at, id) AS rn
@@ -247735,6 +248199,23 @@ var init_postgres_chat_sessions = __esm({
247735
248199
  args
247736
248200
  );
247737
248201
  }
248202
+ /**
248203
+ * `outputs/` 跨轮指纹(2026-09-11 产出卡)。整表覆盖,不做增量合并——判据的真源永远是
248204
+ * 最近一次扫描看到的现场;合并会让「文件被删掉」这件事永远留在表里。
248205
+ */
248206
+ async readOutputFingerprints(sessionId) {
248207
+ const r = await this.pool.query(
248208
+ `SELECT outputs_fingerprints FROM ${this.s}.chat_sessions WHERE id = $1 AND company_id = $2`,
248209
+ [sessionId, this.companyId]
248210
+ );
248211
+ return r.rows[0]?.outputs_fingerprints ?? null;
248212
+ }
248213
+ async writeOutputFingerprints(sessionId, fingerprints) {
248214
+ await this.pool.query(
248215
+ `UPDATE ${this.s}.chat_sessions SET outputs_fingerprints = $1::jsonb WHERE id = $2 AND company_id = $3`,
248216
+ [JSON.stringify(fingerprints), sessionId, this.companyId]
248217
+ );
248218
+ }
247738
248219
  /** 删项目时把本公司里绑到它的会话回落成「不指定项目」。会话一条都不删,只清 project_id。 */
247739
248220
  async clearProjectBinding(projectId2) {
247740
248221
  if (!projectId2) return 0;
@@ -248137,6 +248618,42 @@ var init_postgres_chat_sessions = __esm({
248137
248618
  return r.rows.map(rowToMessage);
248138
248619
  }
248139
248620
  // ── ADR-0166 D6 待发队列 ─────────────────────────────────────────────────
248621
+ async claimPendingInsert(m2) {
248622
+ const client = await this.pool.connect();
248623
+ try {
248624
+ await client.query("BEGIN");
248625
+ const scope = await client.query(`SELECT id FROM ${this.s}.chat_sessions WHERE id=$1 AND company_id=$2 FOR UPDATE`, [m2.chatSessionId, this.companyId]);
248626
+ if (!scope.rows.length) throw new ChatCompanyScopeError(this.companyId, m2.chatSessionId);
248627
+ const ins = await client.query(
248628
+ `INSERT INTO ${this.s}.chat_pending_messages (id, chat_session_id, text, attachments, client_key, created_at, flavour)
248629
+ VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT DO NOTHING RETURNING *`,
248630
+ [m2.id, m2.chatSessionId, m2.text, m2.attachments ? JSON.stringify(m2.attachments) : null, m2.clientKey ?? null, m2.createdAt, m2.flavour ?? "user"]
248631
+ );
248632
+ if (!ins.rows.length) {
248633
+ const prior = await client.query(
248634
+ `SELECT * FROM ${this.s}.chat_pending_messages WHERE chat_session_id=$1
248635
+ AND (id=$2 OR (client_key=$3 AND state='queued')) ORDER BY (id=$2) DESC LIMIT 1`,
248636
+ [m2.chatSessionId, m2.id, m2.clientKey ?? null]
248637
+ );
248638
+ if (!prior.rows[0]) throw new Error("claimPendingInsert: conflicting submission unavailable");
248639
+ await client.query("COMMIT");
248640
+ return { submitted: rowToPending(prior.rows[0]), batch: [] };
248641
+ }
248642
+ const claimed = await client.query(
248643
+ `UPDATE ${this.s}.chat_pending_messages SET state='delivering', state_at=now(), state_reason='append-now'
248644
+ WHERE chat_session_id=$1 AND state='queued' RETURNING *`,
248645
+ [m2.chatSessionId]
248646
+ );
248647
+ await client.query("COMMIT");
248648
+ return { submitted: rowToPending(ins.rows[0]), batch: claimed.rows.map(rowToPending).sort((a, b2) => a.seq - b2.seq) };
248649
+ } catch (error2) {
248650
+ await client.query("ROLLBACK").catch(() => {
248651
+ });
248652
+ throw error2;
248653
+ } finally {
248654
+ client.release();
248655
+ }
248656
+ }
248140
248657
  async enqueuePendingMessage(m2) {
248141
248658
  await this.assertSessionInScope(m2.chatSessionId);
248142
248659
  const initialState = m2.state;
@@ -253339,6 +253856,17 @@ var init_cutover_aware_dispatches = __esm({
253339
253856
  if (row.state === "frozen") throw new BundleFrozenError("dispatch");
253340
253857
  return this.o.cutover.readsCompanySchema(row) ? this.o.owned : this.o.legacy;
253341
253858
  }
253859
+ /**
253860
+ * 这家公司的派发台账当前**物理落在哪**:切走了就是它自己的引擎 schema,没切走就是那个
253861
+ * 所有未切公司共用的 legacy 台账。
253862
+ *
253863
+ * 给 {@link FanOutDispatchLedger} 去重用。判据**不在调用方重算**——就是 {@link pick}
253864
+ * 那一条(`cutover.get` + `readsCompanySchema`)。与 `CutoverAwareTraceStore.storageKey` 同构。
253865
+ */
253866
+ async storageKey() {
253867
+ const row = await this.o.cutover.get(this.o.companyId, "dispatch");
253868
+ return this.o.cutover.readsCompanySchema(row) ? `owned:${this.o.companyId}` : "legacy";
253869
+ }
253342
253870
  async insertOpen(rec) {
253343
253871
  return (await this.pickWritable()).insertOpen(rec);
253344
253872
  }
@@ -253374,11 +253902,48 @@ var init_cutover_aware_dispatches = __esm({
253374
253902
  constructor(o) {
253375
253903
  this.o = o;
253376
253904
  }
253377
- async each(op, fn) {
253905
+ /**
253906
+ * 本次要作用的物理落点清单——**同一个台账只出现一次**。
253907
+ *
253908
+ * 为什么必须去重:`dispatch` 是按 (公司, bundle) 逐个切的,没切走的公司**全部共用同一个
253909
+ * legacy 台账**(读写同一张 `public.dispatches`)。此前 `each` 直接按公司逐家来,于是同一张
253910
+ * 表被访问 N 次(N = 未切走的公司数):
253911
+ *
253912
+ * - **纯读直接出重复行**(`listOpen` / `listByWorkorder` / `findOpenByTarget`)。
253913
+ * 2026-09-11 v2 生产实证(`ws:wo-9f4652cd`):35 家公司、其中 33 家 dispatch 未切走,
253914
+ * `listByWorkorder` 返回 33 份同一条派发,动态卡「执行轨迹」抽屉里同一轮 attempt
253915
+ * 就出现 33 次(`attempts` 由它喂,见 server 的 `activity-trace.ts`)。
253916
+ * - 写侧**没有被放大**,但原因是运气好而不是设计:`markStarted` / `close` 是同值 UPDATE;
253917
+ * `closeStaleNeverStarted` / `closeOpenByTarget` 的 WHERE 带 `ended_at IS NULL`,
253918
+ * 第一遍改完第二遍就匹配不上,返回 0,`reduce` 出来仍是真实值。
253919
+ * - 无论读写,同一条 SQL 都实打实打了 N 遍。
253920
+ *
253921
+ * 去重键由台账自己报,不在这里按 cutover 状态重算(见 `storageKey` 的注释)。也不能按
253922
+ * `ledgerFor` 返回的对象做身份去重:每家各有一个 `CutoverAwareDispatchStore` 包装实例,
253923
+ * N 个不同对象包着同一个 legacy 台账。
253924
+ */
253925
+ async targets(op) {
253378
253926
  const out = [];
253927
+ const seen = /* @__PURE__ */ new Set();
253379
253928
  for (const cid of await this.o.listCompanies()) {
253380
253929
  try {
253381
- out.push(await fn(await this.o.ledgerFor(cid)));
253930
+ if (this.o.storageKeyFor) {
253931
+ const key = await this.o.storageKeyFor(cid);
253932
+ if (seen.has(key)) continue;
253933
+ seen.add(key);
253934
+ }
253935
+ out.push({ cid, ledger: await this.o.ledgerFor(cid) });
253936
+ } catch (err) {
253937
+ this.o.onError?.(cid, op, err);
253938
+ }
253939
+ }
253940
+ return out;
253941
+ }
253942
+ async each(op, fn) {
253943
+ const out = [];
253944
+ for (const { cid, ledger } of await this.targets(op)) {
253945
+ try {
253946
+ out.push(await fn(ledger));
253382
253947
  } catch (err) {
253383
253948
  this.o.onError?.(cid, op, err);
253384
253949
  }
@@ -253505,9 +254070,19 @@ var init_cutover_aware_trace = __esm({
253505
254070
  constructor(o) {
253506
254071
  this.o = o;
253507
254072
  }
253508
- async pick() {
254073
+ /** Resolve store and isolation together, from one cutover snapshot. Read-only consumers
254074
+ * must not infer company isolation merely from having a companyId. */
254075
+ async readTarget() {
253509
254076
  const row = await this.o.cutover.get(this.o.companyId, "trace");
253510
- return this.o.cutover.readsCompanySchema(row) ? this.o.owned : this.o.legacy;
254077
+ const companyScoped = this.o.cutover.readsCompanySchema(row);
254078
+ return { store: companyScoped ? this.o.owned : this.o.legacy, companyScoped };
254079
+ }
254080
+ /** Expose only usage reads: callers cannot bypass the frozen-write fence. */
254081
+ async resolveUsageReadTarget() {
254082
+ return this.readTarget();
254083
+ }
254084
+ async pick() {
254085
+ return (await this.readTarget()).store;
253511
254086
  }
253512
254087
  async pickWritable() {
253513
254088
  const row = await this.o.cutover.get(this.o.companyId, "trace");
@@ -257791,6 +258366,7 @@ var init_src11 = __esm({
257791
258366
  init_tenancy_move_verify();
257792
258367
  init_company_scoped_registry();
257793
258368
  init_session_workdir();
258369
+ init_outputs_scan();
257794
258370
  init_org_model_backfill();
257795
258371
  init_types_admin();
257796
258372
  init_roles_admin();
@@ -258200,6 +258776,13 @@ init_src11();
258200
258776
  init_src();
258201
258777
  var BRAINSTORM_LEGACY_LINE = "\u8BF7\u76F4\u63A5\u56DE\u7B54\u7528\u6237\u95EE\u9898\u3002\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5199\u5165\u7CFB\u7EDF\uFF0C\u5426\u5219\u4E0D\u8981\u521B\u5EFA\u6216\u4FEE\u6539 artifact\u3002";
258202
258778
  var BRAINSTORM_ARTIFACT_GUARD = "\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5199\u5165\u7CFB\u7EDF\uFF0C\u5426\u5219\u4E0D\u8981\u521B\u5EFA\u6216\u4FEE\u6539 artifact\u3002";
258779
+ var OUTPUTS_CONVENTION = [
258780
+ "## \u4EA7\u51FA\u653E\u54EA",
258781
+ "",
258782
+ "\u8981\u7ED9\u7528\u6237\u770B\u7684\u6210\u54C1\u6587\u4EF6\uFF0C**\u653E\u8FDB\u5DE5\u4F5C\u76EE\u5F55\u4E0B\u7684 `outputs/`**\uFF08\u53EF\u4EE5\u6709\u5B50\u76EE\u5F55\uFF0C\u7ED3\u6784\u81EA\u5DF1\u5B9A\uFF09\u3002",
258783
+ "\u53EA\u6709\u8FD9\u4E2A\u76EE\u5F55\u91CC\u7684\u6587\u4EF6\u4F1A\u5728\u5BF9\u8BDD\u91CC\u663E\u793A\u6210\u53EF\u70B9\u5F00\u7684\u6587\u4EF6\u5361\u7247\uFF1B\u5199\u5728\u522B\u5904\u7684\u7528\u6237\u770B\u4E0D\u89C1\u3002",
258784
+ "\u4E2D\u95F4\u4EA7\u7269\u3001\u4E34\u65F6\u811A\u672C\u3001\u8349\u7A3F\u4E0D\u8981\u653E\u8FD9\u91CC\u2014\u2014\u653E\u8FDB\u53BB\u5C31\u4F1A\u6446\u5230\u7528\u6237\u9762\u524D\u3002"
258785
+ ].join("\n");
258203
258786
  function buildChatTaskHeader(input) {
258204
258787
  if (input.kind === "isolated-knowledge") {
258205
258788
  return {
@@ -258220,7 +258803,9 @@ function buildChatTaskHeader(input) {
258220
258803
  `your_actor_id: ${input.actorId}`,
258221
258804
  `workspace: ${input.workspace}`,
258222
258805
  "",
258223
- CONVERSATIONAL_MANAGER_BODY
258806
+ CONVERSATIONAL_MANAGER_BODY,
258807
+ "",
258808
+ OUTPUTS_CONVENTION
258224
258809
  ]
258225
258810
  };
258226
258811
  }
@@ -258232,7 +258817,9 @@ function buildChatTaskHeader(input) {
258232
258817
  input.sessionLine,
258233
258818
  `your_actor_id: ${input.actorId}`,
258234
258819
  "",
258235
- routed ? BRAINSTORM_ARTIFACT_GUARD : BRAINSTORM_LEGACY_LINE
258820
+ routed ? BRAINSTORM_ARTIFACT_GUARD : BRAINSTORM_LEGACY_LINE,
258821
+ // 子会话那一段由 EXPERT_DELEGATION_SYSTEM_PROMPT 讲,这里不重复(见 OUTPUTS_CONVENTION 注释)。
258822
+ ...input.delegatedChildTurn ? [] : ["", OUTPUTS_CONVENTION]
258236
258823
  ],
258237
258824
  ...routed ? { routingSystemPrompt: EXECUTION_ROUTING_BODY } : {}
258238
258825
  };
@@ -259407,6 +259994,12 @@ async function startServe(opts) {
259407
259994
  kernel: defaultCompanyKernel,
259408
259995
  blobs: assets,
259409
259996
  trace: () => traceStoreForActors,
259997
+ // Same current-company ledger as chat/dispatch writes (PR #1223). Resolve the
259998
+ // physical read target and its isolation together; never treat legacy as owned.
259999
+ resolveTrace: async (cid) => {
260000
+ const store = await traceStoreFor(cid);
260001
+ return store instanceof CutoverAwareTraceStore ? store.resolveUsageReadTarget() : { store, companyScoped: !pgPool };
260002
+ },
259410
260003
  audit: registryAudit,
259411
260004
  // ADR 凭据保险库 §4.5/§4.7 / ADR 0125:reveal 判权的项目解析 + 凭据读取审计读写。
259412
260005
  resolveDispatchScope: resolveDispatchScope2,
@@ -259595,6 +260188,14 @@ async function startServe(opts) {
259595
260188
  actorDirectory: registryStore,
259596
260189
  // 与 collab 同源;闭包避开下方 listCompanyMembers 声明的 TDZ,实际请求时才读取。
259597
260190
  listMembers: (companyId) => listCompanyMembers(companyId),
260191
+ // 改/删项目的门禁判据之一(另一半是项目创建人)——见 domains/projects/routes.ts 的
260192
+ // `requireProjectManagePermission`。同样用闭包避开 listCompanyMembers 的 TDZ。
260193
+ // 成员表按 company_id 有索引、量级几十行,与 `listMembers` 同一次读的成本量级;
260194
+ // 只在 PATCH/DELETE 这两个低频写端点上调,不在任何读热路径上。
260195
+ resolveMemberRole: async (companyId, actorId) => {
260196
+ const members = await listCompanyMembers(companyId);
260197
+ return members.find((m2) => m2.actorId === actorId)?.role;
260198
+ },
259598
260199
  resolveActorContext: (actorId) => buildActorContext(actors.service, actorId),
259599
260200
  defaultBranchResolver,
259600
260201
  // 展示:GET content-locations 给 git 仓补 resolvedDefaultBranch
@@ -260611,6 +261212,15 @@ async function startServe(opts) {
260611
261212
  // 只扫 active 的公司:停用公司的台账不该再被兜底扫改写(与本文件其它按公司扇出的地方同口径)。
260612
261213
  listCompanies: async () => (await controlPlaneStore.listCompanies()).filter((c) => c.status === "active").map((c) => c.id),
260613
261214
  ledgerFor: (cid) => dispatchLedgerFor(cid),
261215
+ /**
261216
+ * 去重键:`dispatch` 未切走的公司全部共用同一张 legacy 台账,扇出只该作用一次。
261217
+ * 由台账自己报落点,这里不按 cutover 状态重算(见 FanOutDispatchLedger.targets 的注释)。
261218
+ * 非 PG / 非 cutover-aware 的路径没有这个方法 ⇒ 退回按公司逐家来,行为不变。
261219
+ */
261220
+ storageKeyFor: async (cid) => {
261221
+ const l = await dispatchLedgerFor(cid);
261222
+ return typeof l.storageKey === "function" ? l.storageKey() : cid;
261223
+ },
260614
261224
  onError: (cid, op, err) => console.warn(`[dispatch-ledger] ${cid} \u7684 ${op} \u5931\u8D25\uFF08\u5176\u4F59\u516C\u53F8\u7EE7\u7EED\uFF09\uFF1A${String(err)}`)
260615
261225
  }) : dispatchLedger;
260616
261226
  const companyRegistryFor = async (companyId) => pgPool ? (await openCompanyPlane(companyId)).overlay : scopedRegistryFor(companyId);
@@ -261408,6 +262018,39 @@ async function startServe(opts) {
261408
262018
  // 三个方法的组合规则住在 `createDelegationWorkdirPort`(`@oasis/server`)——评审
261409
262019
  // wo:acfee725 blocker ② 回归防线:**serve.ts 与单测走同一处**,那样「装配层退回全局 binding」
261410
262020
  // 这类事故在单测里就能被复现。绝不许在这里再写一份。
262021
+ /**
262022
+ * 会话产出扫描(2026-09-11 产出卡):轮次收尾扫这条会话的 `outputs/`,交出「这一轮变了哪些」。
262023
+ *
262024
+ * 复用 `delegationWorkdirLocator`(与派发、回流、chat 域**同一个 binding resolver 实例**)——
262025
+ * 委派回程读的就是子会话的 `outputs/`,主会话这条只是把同一套读口用到了自己身上。
262026
+ *
262027
+ * 三处 fail-closed,都返回 `null`(= 这一轮没有产出卡),一处都不猜:
262028
+ * · 桥还没建好 / 会话没绑定 / 节点离线 → 拿不到工作区;
262029
+ * · store 没有指纹方法(老 store / 文件态装配)→ 没地方记「上一轮长什么样」,
262030
+ * 硬扫会把整个 `outputs/` 每轮都当成新增,一口气刷几十张卡;
262031
+ * · 解析不出公司 → **绝不回落默认公司**(否则会去别家公司的库里读写指纹)。
262032
+ */
262033
+ sessionOutputsScan: async ({ chatSessionId, companyId }) => {
262034
+ const bridge = workdirBridge;
262035
+ if (!bridge) return null;
262036
+ const store = chatStoreRouter ? await chatStoreFor(companyId).catch(() => null) : chatSessionStore;
262037
+ const read = store?.readOutputFingerprints?.bind(store);
262038
+ const write = store?.writeOutputFingerprints?.bind(store);
262039
+ if (!read || !write) return null;
262040
+ const located = await locateSessionWorkdir(delegationWorkdirLocator, {
262041
+ chatSessionId,
262042
+ ...companyId !== void 0 ? { companyId } : {}
262043
+ });
262044
+ if (!located.ok) return null;
262045
+ const diff = await settleSessionOutputs({
262046
+ bridge,
262047
+ loc: located.loc,
262048
+ sessionId: chatSessionId,
262049
+ readFingerprints: read,
262050
+ writeFingerprints: write
262051
+ }).catch(() => null);
262052
+ return diff?.changed ?? null;
262053
+ },
261411
262054
  delegationWorkdir: createDelegationWorkdirPort({
261412
262055
  locator: delegationWorkdirLocator,
261413
262056
  // 桥在 hub 建好后(本函数尾部)才赋值 → getter 每次调用时读最新值;早于此处的构造时 `workdirBridge`
@@ -265093,6 +265736,7 @@ async function runSession(dispatchId, job, deps) {
265093
265736
  const cancelledBeforeStart = deps.wasCancelled?.() ?? false;
265094
265737
  const normalizer = new ProviderStreamNormalizer({
265095
265738
  providerName: job.binding?.runtimeKind ?? "runtime",
265739
+ hasLiveProtocol: handle.supportsLiveProtocol === true,
265096
265740
  fallbackTurnId: `oasis-turn:${handle.id}`,
265097
265741
  emit: (event) => deps.sink.stream({ type: "session_normalized_event", dispatchId, event })
265098
265742
  });
@@ -265262,7 +265906,8 @@ var SessionProcessState = class {
265262
265906
  // 只报 dispatchId 的话 serve 重启后它认不出这是条活会话,会当成野进程 evict 掉。
265263
265907
  ...this.spec.job.artifactId ? { jobArtifactId: this.spec.job.artifactId } : {},
265264
265908
  // ADR 0300 D2:自报支持帧号;新服务端据此回带 ackSeq 的 hello-ack,届时才把 serverSupportsDedup 置真。
265265
- supportsFrameSeq: true
265909
+ supportsFrameSeq: true,
265910
+ supportsInputConsumptionReceipts: true
265266
265911
  });
265267
265912
  const toResend = this.serverSupportsDedup ? this.outbox : this.outbox.filter(isKeyFrame);
265268
265913
  for (const m2 of toResend) this.rawSend(m2);
@@ -272184,7 +272829,7 @@ function shimScript() {
272184
272829
  }
272185
272830
 
272186
272831
  // src/index.ts
272187
- var PKG_VERSION = true ? "2.2.18" : "dev";
272832
+ var PKG_VERSION = true ? "2.2.20" : "dev";
272188
272833
  var LOCAL_BIN = localBin();
272189
272834
  var NPM_PREFIX = npmPrefix();
272190
272835
  var INSTANCE = DEFAULT_INSTANCE;