oasis_test_v2 2.2.19 → 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 +539 -102
  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
  ``,
@@ -24897,6 +24931,8 @@ async function runProtocolSession(job, cfg) {
24897
24931
  const transcript = openTranscript(transcriptRef);
24898
24932
  const outputCbs = [];
24899
24933
  const telemetryCbs = [];
24934
+ const liveCbs = [];
24935
+ const appendByKey = /* @__PURE__ */ new Map();
24900
24936
  const exitCbs = [];
24901
24937
  const state = cfg.createState();
24902
24938
  const pending = /* @__PURE__ */ new Map();
@@ -24932,7 +24968,7 @@ async function runProtocolSession(job, cfg) {
24932
24968
  pending.clear();
24933
24969
  };
24934
24970
  const send = (command) => {
24935
- 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"));
24936
24972
  const idValue = command.id ?? `oasis_${++nextRequestId}`;
24937
24973
  const message = { ...command, id: idValue };
24938
24974
  const key = commandId(idValue);
@@ -24952,11 +24988,12 @@ async function runProtocolSession(job, cfg) {
24952
24988
  if (isResponse(message) && responseId !== void 0 && pending.has(responseId)) {
24953
24989
  const waiter = pending.get(responseId);
24954
24990
  pending.delete(responseId);
24955
- if (message.type === "response" && message.success === false) waiter.reject(new Error(String(message.error ?? "runtime rejected command")));
24956
- 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)));
24957
24993
  else waiter.resolve(message.result ?? message);
24958
24994
  return;
24959
24995
  }
24996
+ for (const event of cfg.liveEvents?.(message, state) ?? []) for (const cb of liveCbs) cb(event);
24960
24997
  for (const line of cfg.normalize(message, state)) {
24961
24998
  if (line.outputText) for (const cb of outputCbs) cb(line.outputText);
24962
24999
  const event = { kind: line.kind, payload: line.payload, seq: ++eventSeq, ts: (/* @__PURE__ */ new Date()).toISOString() };
@@ -25048,26 +25085,34 @@ async function runProtocolSession(job, cfg) {
25048
25085
  onTelemetry(cb) {
25049
25086
  telemetryCbs.push(cb);
25050
25087
  },
25088
+ onLiveEvent(cb) {
25089
+ liveCbs.push(cb);
25090
+ },
25051
25091
  get canAppendInput() {
25052
25092
  return !closing && started && !exited;
25053
25093
  },
25054
25094
  appendInput(input) {
25055
- if (input.requireConsumptionReceipt) return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
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" });
25056
25098
  appendChain = appendChain.then(async () => {
25057
25099
  if (exited || closing) return { accepted: false, reason: "session-closing" };
25058
25100
  const files = materializeFiles(dir, input.files, input.binaryFiles);
25059
25101
  try {
25060
- const result = await cfg.append(input, state, send);
25061
- return { ...result, ...files.written.length || files.failed.length ? { files } : {} };
25062
- } catch {
25063
- 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 } : {} };
25064
25107
  }
25065
25108
  });
25066
- return appendChain;
25109
+ const result = appendChain;
25110
+ if (key) appendByKey.set(key, result);
25111
+ return result;
25067
25112
  }
25068
25113
  };
25069
25114
  }
25070
- var import_node_child_process6, import_node_crypto10, path9;
25115
+ var import_node_child_process6, import_node_crypto10, path9, ProtocolCommandRejected, ProtocolSessionClosing;
25071
25116
  var init_protocol_session = __esm({
25072
25117
  "../adapters/src/_core/protocol-session.ts"() {
25073
25118
  "use strict";
@@ -25083,6 +25128,10 @@ var init_protocol_session = __esm({
25083
25128
  init_transcript();
25084
25129
  init_filter_extra_args();
25085
25130
  init_root_bypass();
25131
+ ProtocolCommandRejected = class extends Error {
25132
+ };
25133
+ ProtocolSessionClosing = class extends Error {
25134
+ };
25086
25135
  }
25087
25136
  });
25088
25137
 
@@ -25595,11 +25644,13 @@ var init_codex = __esm({
25595
25644
  workRoot: this.opts.workRoot,
25596
25645
  env: this.opts.env,
25597
25646
  runtimeKind: "codex",
25647
+ supportsInputConsumptionReceipts: true,
25598
25648
  syncSkills: this.opts.syncSkills,
25599
25649
  createState: () => ({
25600
25650
  threadId: void 0,
25601
25651
  turnId: void 0,
25602
25652
  active: false,
25653
+ pendingInputs: /* @__PURE__ */ new Set(),
25603
25654
  codex: createCodexNormalizeState(),
25604
25655
  // 本次 run 的用量:逐轮累加 `thread/tokenUsage/updated` 的 `last`(见该解析函数注释)。
25605
25656
  usage: { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 },
@@ -25643,6 +25694,34 @@ ${task}` : task;
25643
25694
  if (typeof turnId === "string" && turnId) state.turnId = turnId;
25644
25695
  if (!state.turnId) throw new Error("codex app-server returned no turn id");
25645
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
+ },
25646
25725
  normalize(message, state) {
25647
25726
  const params = message.params;
25648
25727
  const method = typeof message.method === "string" ? message.method : "";
@@ -25701,7 +25780,7 @@ ${task}` : task;
25701
25780
  };
25702
25781
  },
25703
25782
  async append(input, state, send) {
25704
- if (!state.threadId || !state.turnId) return { accepted: false, reason: "rejected" };
25783
+ if (!state.threadId || !state.turnId || !state.active) return { accepted: false, reason: "session-closing" };
25705
25784
  if (input.interrupt) {
25706
25785
  await send({ jsonrpc: "2.0", method: "turn/interrupt", params: { threadId: state.threadId, turnId: state.turnId } });
25707
25786
  const next = await send({ jsonrpc: "2.0", method: "turn/start", params: { threadId: state.threadId, input: [{ type: "text", text: input.text }] } });
@@ -25710,13 +25789,22 @@ ${task}` : task;
25710
25789
  state.active = true;
25711
25790
  return { accepted: true, interrupt: "unknown" };
25712
25791
  }
25713
- await send({ jsonrpc: "2.0", method: "turn/steer", params: {
25714
- threadId: state.threadId,
25715
- expectedTurnId: state.turnId,
25716
- ...input.clientMessageKey ? { clientUserMessageId: input.clientMessageKey } : {},
25717
- input: [{ type: "text", text: input.text }]
25718
- } });
25719
- 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" };
25720
25808
  }
25721
25809
  });
25722
25810
  }
@@ -26592,6 +26680,7 @@ async function runACPSession(job, cfg) {
26592
26680
  const transcriptOut = openTranscript(transcriptRef);
26593
26681
  const outputCbs = [];
26594
26682
  const telemetryCbs = [];
26683
+ const liveCbs = [];
26595
26684
  const exitCbs = [];
26596
26685
  let exited;
26597
26686
  let killReason = null;
@@ -26670,6 +26759,34 @@ ${note}` : note };
26670
26759
  });
26671
26760
  let lastRpcError;
26672
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
+ };
26673
26790
  const client = new ACPClient(
26674
26791
  (line) => {
26675
26792
  try {
@@ -26678,9 +26795,11 @@ ${note}` : note };
26678
26795
  }
26679
26796
  },
26680
26797
  (text5) => {
26798
+ confirmAppendBatch();
26681
26799
  for (const cb of outputCbs) cb(text5);
26682
26800
  },
26683
26801
  (e) => {
26802
+ if (["message", "thought", "tool_use", "tool_result"].includes(e.kind)) confirmAppendBatch();
26684
26803
  for (const cb of telemetryCbs) cb(e);
26685
26804
  },
26686
26805
  // runtime 自己用 stopReason=cancelled 收尾时也保留取消语义;此前误记成 error,
@@ -26814,9 +26933,11 @@ ${task}` : task;
26814
26933
  promptResult = await sendPrompt(userText);
26815
26934
  }
26816
26935
  while (!killReason && appendQueue.length > 0) {
26936
+ emitBoundary();
26817
26937
  inflightAppendBatch = appendQueue.splice(0);
26818
26938
  const merged = inflightAppendBatch.map((i) => i.text).filter(Boolean).join("\n\n");
26819
26939
  promptResult = await sendPrompt(merged);
26940
+ confirmAppendBatch();
26820
26941
  inflightAppendBatch = [];
26821
26942
  }
26822
26943
  closingForExit = true;
@@ -26855,6 +26976,9 @@ ${task}` : task;
26855
26976
  onTelemetry(cb) {
26856
26977
  telemetryCbs.push(cb);
26857
26978
  },
26979
+ onLiveEvent(cb) {
26980
+ liveCbs.push(cb);
26981
+ },
26858
26982
  // ADR-0093 对话追加:入队一条 user 输入,drain 循环会把它发成同 session 的下一回合。
26859
26983
  // 已收尾/已退出则拒(session-closing),上层据此降级 kill+resume。
26860
26984
  get canAppendInput() {
@@ -26878,12 +27002,15 @@ ${task}` : task;
26878
27002
  }
26879
27003
  };
26880
27004
  function doAppend(input) {
26881
- if (input.requireConsumptionReceipt) return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
26882
27005
  if (exited || closingForExit) return Promise.resolve({ accepted: false, reason: "session-closing" });
26883
27006
  const report = materializeFiles(dir, input.files, input.binaryFiles);
26884
27007
  const queued = report.failed.length ? { ...input, text: appendMaterializeWarning(input.text, report) } : input;
26885
27008
  appendQueue.push(queued);
26886
- 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
+ });
26887
27014
  if (!input.interrupt) return Promise.resolve(withFiles({ accepted: true, interrupt: "queued_at_boundary" }));
26888
27015
  const run = appendChain.then(async () => {
26889
27016
  const pending = inflightStopReason;
@@ -27637,12 +27764,13 @@ var init_pi = __esm({
27637
27764
  return ["--mode", "rpc", "--session", sessionFile, ...model ? ["--model", model] : []];
27638
27765
  },
27639
27766
  runtimeKind: "pi",
27767
+ supportsInputConsumptionReceipts: true,
27640
27768
  workRoot: opts.workRoot,
27641
27769
  cleanupWorkdir: opts.cleanupWorkdir,
27642
27770
  env: opts.env,
27643
27771
  extraArgs: opts.extraArgs,
27644
27772
  blockedFlags: PI_BLOCKED_FLAGS,
27645
- createState: () => ({}),
27773
+ createState: () => ({ pendingInputs: [] }),
27646
27774
  initialCommand(job2, _dir, task) {
27647
27775
  const prompt2 = job2.systemPrompt ? `${job2.systemPrompt}
27648
27776
 
@@ -27659,6 +27787,28 @@ ${task}` : task;
27659
27787
  } catch {
27660
27788
  }
27661
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
+ },
27662
27812
  normalize(message) {
27663
27813
  return normalizePi(JSON.stringify(message));
27664
27814
  },
@@ -27668,14 +27818,21 @@ ${task}` : task;
27668
27818
  runtimeSessionId(state) {
27669
27819
  return state.sessionId;
27670
27820
  },
27671
- async append(input, _state, send) {
27672
- if (input.interrupt) {
27673
- 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
+ }
27674
27830
  await send({ type: "steer", message: input.text });
27675
- 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;
27676
27835
  }
27677
- await send({ type: "steer", message: input.text });
27678
- return { accepted: true };
27679
27836
  }
27680
27837
  });
27681
27838
  }
@@ -28485,10 +28642,13 @@ var init_normalizer = __esm({
28485
28642
  proseClaim = /* @__PURE__ */ new Map();
28486
28643
  /** 本 turn 内已经见过的 item 键——认领闸只拦「新起一段」,不拦已知 item 的后续 op。 */
28487
28644
  claimedKeys = /* @__PURE__ */ new Set();
28645
+ closedKeys = /* @__PURE__ */ new Set();
28488
28646
  /** 见过 stdout 文本没有——见过就压掉 telemetry `message`(与 server.ts 原口径同款去重)。 */
28489
28647
  sawTextOutput = false;
28490
28648
  /** 本 turn 产出过 item op 没有。收尾时用来决定要不要发 turn 信号(见 turnFinished)。 */
28491
28649
  producedAny = false;
28650
+ /** Receipts wait for a whole output block; they never split its bytes. */
28651
+ pendingInputs = /* @__PURE__ */ new Map();
28492
28652
  constructor(opts) {
28493
28653
  this.providerName = opts.providerName;
28494
28654
  this.hasLiveProtocol = opts.hasLiveProtocol === true;
@@ -28532,20 +28692,17 @@ var init_normalizer = __esm({
28532
28692
  }
28533
28693
  /** 结构化实时事件(原生 Live Protocol v2)。 */
28534
28694
  onLiveEvent(event) {
28535
- this.turnId = event.turnId || this.turnId;
28536
28695
  const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
28537
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") {
28538
28702
  this.closeOpen("completed");
28539
- this.emitItem({
28540
- key: providerItemKey(this.providerName, event.itemId),
28541
- kind: "control",
28542
- role: "system",
28543
- origin: "live",
28544
- opShape: { op: "set_status", status: "completed" },
28545
- attrs: { code: "input_consumed", clientMessageKey: payload.clientMessageKey }
28546
- });
28547
28703
  return;
28548
28704
  }
28705
+ this.turnId = event.turnId || this.turnId;
28549
28706
  if (event.operation === "turn_completed" || event.operation === "turn_failed") {
28550
28707
  this.turnFinished(event.operation === "turn_failed" ? "failed" : "completed");
28551
28708
  return;
@@ -28561,13 +28718,12 @@ var init_normalizer = __esm({
28561
28718
  }
28562
28719
  return;
28563
28720
  }
28564
- if (event.operation === "completed" && (event.itemType === "message" || event.itemType === "thinking") && typeof payload.text === "string") {
28565
- this.snapshotProse(
28566
- event.itemType === "message" ? "text" : "thinking",
28567
- payload.text,
28568
- "live",
28569
- providerItemKey(this.providerName, event.itemId)
28570
- );
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");
28571
28727
  return;
28572
28728
  }
28573
28729
  if (event.operation === "started" && event.itemType === "tool") {
@@ -28592,6 +28748,7 @@ var init_normalizer = __esm({
28592
28748
  this.fallbackIndex.clear();
28593
28749
  this.proseClaim.clear();
28594
28750
  this.claimedKeys.clear();
28751
+ this.closedKeys.clear();
28595
28752
  this.sawTextOutput = false;
28596
28753
  this.producedAny = false;
28597
28754
  if (!produced) return;
@@ -28615,6 +28772,10 @@ var init_normalizer = __esm({
28615
28772
  if (!known && !this.claim(kind, origin)) return;
28616
28773
  const key = explicitKey ?? (this.open && this.open.kind === kind ? this.open.key : this.nextFallbackKey(kind));
28617
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
+ }
28618
28779
  if (!this.open || this.open.key !== key) {
28619
28780
  this.closeOpen("completed");
28620
28781
  this.open = { key, kind, origin };
@@ -28631,6 +28792,10 @@ var init_normalizer = __esm({
28631
28792
  if (!known && !this.claim(kind, origin)) return;
28632
28793
  const key = explicitKey ?? (this.open && this.open.kind === kind ? this.open.key : this.nextFallbackKey(kind));
28633
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
+ }
28634
28799
  if (!this.open || this.open.key !== key) {
28635
28800
  this.closeOpen("completed");
28636
28801
  this.open = { key, kind, origin };
@@ -28649,8 +28814,23 @@ var init_normalizer = __esm({
28649
28814
  closeOpen(status) {
28650
28815
  const open3 = this.open;
28651
28816
  this.open = null;
28652
- if (!open3) return;
28653
- 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
+ }
28654
28834
  }
28655
28835
  toolCall(origin, id, name, input) {
28656
28836
  this.closeOpen("completed");
@@ -28870,7 +29050,7 @@ var init_chat_item_ledger = __esm({
28870
29050
  telemetryTextInContent;
28871
29051
  /** 本段的「无输出」注解已经提前落过(`closeSegmentBeforeUserInput`),`beginSegment` 别再落一条。 */
28872
29052
  segmentEndEmitted = false;
28873
- /** **不变量 1**:`providerItemKey → itemId`,本账本唯一持有。切段/收尾清空。 */
29053
+ /** **不变量 1**:`providerItemKey → itemId`,本账本唯一持有。仅 runtime turn 结束/收尾清空,用户插入保留。 */
28874
29054
  itemKeyMap = /* @__PURE__ */ new Map();
28875
29055
  /** 内存账本:itemId → 行。插入顺序即 seq 顺序。 */
28876
29056
  rows = /* @__PURE__ */ new Map();
@@ -29049,7 +29229,7 @@ var init_chat_item_ledger = __esm({
29049
29229
  }
29050
29230
  });
29051
29231
  const row = this.rows.get(id);
29052
- 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 };
29053
29233
  }
29054
29234
  /**
29055
29235
  * 回灌重启前已经落库的那段正文(`chat-recovery` 重挂 / 迟到对账用)。
@@ -29084,7 +29264,7 @@ var init_chat_item_ledger = __esm({
29084
29264
  // ── 段与归属 ──────────────────────────────────────────────────────────────
29085
29265
  /**
29086
29266
  * 切段(人中途插入):此刻起建的 item 属于**下一条** assistant 行。
29087
- * 映射一并清空——新段是新行,复用旧 itemId 会把新文本写进上一条消息。
29267
+ * 已有 provider item 保留身份与位置;新 item 才归下一段,迟到快照不能复制旧块。
29088
29268
  */
29089
29269
  beginSegment() {
29090
29270
  if (this.segmentEndEmitted) {
@@ -29093,8 +29273,6 @@ var init_chat_item_ledger = __esm({
29093
29273
  this.closeStreamingRows("completed");
29094
29274
  if (this.segmentItemCount === 0 && this.messageId) this.insertSegmentEnd();
29095
29275
  }
29096
- this.itemKeyMap.clear();
29097
- this.lastTurnId = null;
29098
29276
  this.messageId = null;
29099
29277
  this.segment += 1;
29100
29278
  this.segmentItemCount = 0;
@@ -29118,7 +29296,7 @@ var init_chat_item_ledger = __esm({
29118
29296
  const id = this.insertSegmentEnd();
29119
29297
  this.segmentEndEmitted = true;
29120
29298
  const row = this.rows.get(id);
29121
- 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 };
29122
29300
  }
29123
29301
  /** 零输出段兜底(S1 评审 B1):那条 assistant 行库里已经在了,一条 item 都没有会被对账抓成假红。 */
29124
29302
  insertSegmentEnd() {
@@ -29137,6 +29315,9 @@ var init_chat_item_ledger = __esm({
29137
29315
  /** 行落库了,把本段还挂着 NULL 的 assistant item 认领回去,并让后续 item 直接带上它。 */
29138
29316
  bindMessage(messageId) {
29139
29317
  this.messageId = messageId;
29318
+ for (const row of this.rows.values()) {
29319
+ if (row.role === "assistant" && row.messageId === null) row.messageId = messageId;
29320
+ }
29140
29321
  if (!this.persistEnabled) return;
29141
29322
  this.enqueue(async () => {
29142
29323
  await this.deps.items.bindMessage(this.deps.sessionId, this.deps.turnId, messageId);
@@ -29196,7 +29377,8 @@ var init_chat_item_ledger = __esm({
29196
29377
  itemId: id,
29197
29378
  version: row.wireVersion,
29198
29379
  startedVersion: row.startedVersion,
29199
- ord: row.ord
29380
+ ord: row.ord,
29381
+ messageId: row.messageId
29200
29382
  };
29201
29383
  if (event.opShape.op === "set_status") {
29202
29384
  const followUp = this.applyOpTo(row, event);
@@ -29232,7 +29414,7 @@ var init_chat_item_ledger = __esm({
29232
29414
  }
29233
29415
  /** 这一次写的回执。`persist` 已经在里面走过钟了,这里只是把读数带出去给 wire。 */
29234
29416
  outcomeOf(row) {
29235
- return { itemId: row.id, version: row.wireVersion, ord: row.ord };
29417
+ return { itemId: row.id, version: row.wireVersion, ord: row.ord, messageId: row.messageId };
29236
29418
  }
29237
29419
  /** 关掉所有还 streaming 的行(收尾/切段)。 */
29238
29420
  closeStreamingRows(status) {
@@ -29284,13 +29466,16 @@ var init_chat_item_ledger = __esm({
29284
29466
  origin: input.origin,
29285
29467
  inContent: input.inContent,
29286
29468
  persist: input.persist,
29287
- segment: this.segment
29469
+ segment: this.segment,
29470
+ /* 归属在**建行那一刻**定死:链是异步的,等它执行时 this.messageId 可能已被切段改了。
29471
+ 记在行上而不是只记在入库那一步——wire 帧也要带它(2026-09-11),两边必须同一个值。 */
29472
+ messageId: input.forceNullMessage ? null : this.messageId
29288
29473
  };
29289
29474
  this.rows.set(id, row);
29290
29475
  this.createdCount += 1;
29291
29476
  if (!input.forceNullMessage) this.segmentItemCount += 1;
29292
29477
  if (!input.persist || !this.persistEnabled) return id;
29293
- const messageId = input.forceNullMessage ? null : this.messageId;
29478
+ const messageId = row.messageId;
29294
29479
  const createdAt = this.now();
29295
29480
  const payload = input.payload ?? this.payloadOf(row);
29296
29481
  const metadata = input.metadata ?? { [CHAT_ITEM_META_LEGACY_CONTENT]: input.inContent };
@@ -29473,6 +29658,7 @@ async function openAssistantRunningRow(deps) {
29473
29658
  async function finalizeAssistantRow(deps) {
29474
29659
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
29475
29660
  const finishedAt = now();
29661
+ const parts = await appendOutputsPart(deps.parts, deps.outputsPart);
29476
29662
  if (deps.assistantMsgId) {
29477
29663
  if (typeof deps.chatStore.updateMessage !== "function") return;
29478
29664
  try {
@@ -29480,7 +29666,7 @@ async function finalizeAssistantRow(deps) {
29480
29666
  content: deps.content,
29481
29667
  status: deps.status,
29482
29668
  completedAt: finishedAt,
29483
- ...deps.parts.length ? { parts: deps.parts } : {},
29669
+ ...parts.length ? { parts } : {},
29484
29670
  ...deps.runId ? { runId: deps.runId } : {},
29485
29671
  ...deps.turnId ? { turnId: deps.turnId } : {}
29486
29672
  });
@@ -29488,7 +29674,7 @@ async function finalizeAssistantRow(deps) {
29488
29674
  }
29489
29675
  return;
29490
29676
  }
29491
- if (!deps.content && !deps.runId && deps.parts.length === 0) return;
29677
+ if (!deps.content && !deps.runId && parts.length === 0) return;
29492
29678
  if (typeof deps.chatStore.appendMessage !== "function") return;
29493
29679
  try {
29494
29680
  await deps.chatStore.appendMessage({
@@ -29501,11 +29687,18 @@ async function finalizeAssistantRow(deps) {
29501
29687
  status: deps.status,
29502
29688
  ...deps.runId ? { runId: deps.runId } : {},
29503
29689
  ...deps.turnId ? { turnId: deps.turnId } : {},
29504
- ...deps.parts.length ? { parts: deps.parts } : {}
29690
+ ...parts.length ? { parts } : {}
29505
29691
  });
29506
29692
  } catch {
29507
29693
  }
29508
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
+ }
29509
29702
  function runBackgroundAssistantChatTurn(deps) {
29510
29703
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
29511
29704
  const log3 = deps.log ?? ((m2) => console.warn(m2));
@@ -29674,6 +29867,7 @@ function runBackgroundAssistantChatTurn(deps) {
29674
29867
  parts,
29675
29868
  status,
29676
29869
  ...runId ? { runId } : {},
29870
+ ...deps.outputsPart ? { outputsPart: deps.outputsPart } : {},
29677
29871
  now
29678
29872
  });
29679
29873
  if (typeof deps.chatStore.updateSession === "function") {
@@ -51546,7 +51740,7 @@ var require_websocket = __commonJS({
51546
51740
  var http2 = require("http");
51547
51741
  var net = require("net");
51548
51742
  var tls = require("tls");
51549
- var { randomBytes: randomBytes13, createHash: createHash38 } = require("crypto");
51743
+ var { randomBytes: randomBytes13, createHash: createHash39 } = require("crypto");
51550
51744
  var { Duplex, Readable } = require("stream");
51551
51745
  var { URL: URL2 } = require("url");
51552
51746
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -52214,7 +52408,7 @@ var require_websocket = __commonJS({
52214
52408
  abortHandshake(websocket, socket, "Invalid Upgrade header");
52215
52409
  return;
52216
52410
  }
52217
- const digest = createHash38("sha1").update(key + GUID).digest("base64");
52411
+ const digest = createHash39("sha1").update(key + GUID).digest("base64");
52218
52412
  if (res.headers["sec-websocket-accept"] !== digest) {
52219
52413
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
52220
52414
  return;
@@ -52583,7 +52777,7 @@ var require_websocket_server = __commonJS({
52583
52777
  var EventEmitter = require("events");
52584
52778
  var http2 = require("http");
52585
52779
  var { Duplex } = require("stream");
52586
- var { createHash: createHash38 } = require("crypto");
52780
+ var { createHash: createHash39 } = require("crypto");
52587
52781
  var extension3 = require_extension();
52588
52782
  var PerMessageDeflate2 = require_permessage_deflate();
52589
52783
  var subprotocol2 = require_subprotocol();
@@ -52890,7 +53084,7 @@ var require_websocket_server = __commonJS({
52890
53084
  );
52891
53085
  }
52892
53086
  if (this._state > RUNNING) return abortHandshake(socket, 503);
52893
- const digest = createHash38("sha1").update(key + GUID).digest("base64");
53087
+ const digest = createHash39("sha1").update(key + GUID).digest("base64");
52894
53088
  const headers = [
52895
53089
  "HTTP/1.1 101 Switching Protocols",
52896
53090
  "Upgrade: websocket",
@@ -160267,6 +160461,7 @@ var init_live_chat = __esm({
160267
160461
  operation: "started",
160268
160462
  itemVersion: outcome.startedVersion,
160269
160463
  ...ord !== void 0 ? { ord } : {},
160464
+ ...outcome.messageId !== null ? { messageId: outcome.messageId } : {},
160270
160465
  offset: 0,
160271
160466
  payload: v3StartedPayload(event.role, event.attrs, itemType, event.kind)
160272
160467
  };
@@ -160300,6 +160495,11 @@ var init_live_chat = __esm({
160300
160495
  // 账本这一次写用掉的那一格钟——**库里那一行的 version 就是它**,快照与增量流同一套版本空间。
160301
160496
  itemVersion: outcome.version,
160302
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 } : {},
160303
160503
  ...offset !== void 0 ? { offset } : {},
160304
160504
  ...payload !== void 0 ? { payload } : {}
160305
160505
  };
@@ -161065,15 +161265,29 @@ var init_chat_attachment_files = __esm({
161065
161265
  async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, attachments, retainLocallyOnFailure = false) {
161066
161266
  const newId = deps.newId ?? import_node_crypto20.randomUUID;
161067
161267
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
161068
- const submitted = await deps.store.enqueuePendingMessage({
161069
- 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(),
161070
161273
  chatSessionId,
161071
161274
  text: text5,
161072
161275
  ...attachments?.length ? { attachments } : {},
161073
161276
  ...clientKey ? { clientKey } : {},
161074
161277
  createdAt: now()
161075
161278
  });
161076
- 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
+ }
161077
161291
  const prepared = await buildChatAttachmentFiles(
161078
161292
  batch.flatMap((m2) => m2.attachments ?? []),
161079
161293
  deps.readBlob ?? (async (ref2) => {
@@ -161089,7 +161303,6 @@ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, atta
161089
161303
  })));
161090
161304
  const batchKey = `append:${chatSessionId}:${batch.map((m2) => m2.id).join(",")}`;
161091
161305
  const ids2 = batch.map((m2) => m2.id);
161092
- await deps.store.markPendingMessages?.(chatSessionId, ids2, "delivering").catch(() => void 0);
161093
161306
  const clientKeys = batch.map((m2) => m2.clientKey).filter((k2) => !!k2);
161094
161307
  const res = await deps.deliver(merged, batchKey, {
161095
161308
  ...clientKeys.length ? { clientKeys } : {},
@@ -161102,7 +161315,7 @@ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, atta
161102
161315
  const nextState = res.accepted ? "delivered" : DEFINITELY_NOT_DELIVERED.has(res.reason ?? "") ? "queued" : "unconfirmed";
161103
161316
  const returnToClient = retainLocallyOnFailure && !!clientKey && nextState === "queued";
161104
161317
  if (returnToClient && deps.store.markPendingMessages) {
161105
- await deps.store.markPendingMessages(chatSessionId, [submitted.id], "cancelled", "client-retained");
161318
+ await deps.store.markPendingMessages(chatSessionId, [submitted.id], "cancelled", `client-retained:${res.reason}`);
161106
161319
  await deps.store.markPendingMessages(chatSessionId, ids2.filter((id) => id !== submitted.id), nextState, res.reason);
161107
161320
  } else {
161108
161321
  const transition = deps.store.markPendingMessages?.(chatSessionId, ids2, nextState, res.reason);
@@ -195758,7 +195971,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195758
195971
  out.listDirCode = located.code;
195759
195972
  if (located.location) out.location = located.location;
195760
195973
  out.skipped.push({
195761
- source: `${DELEGATION_OUTPUTS_DIR}/`,
195974
+ source: `${SESSION_OUTPUTS_DIR}/`,
195762
195975
  code: located.code,
195763
195976
  message: located.message ?? locationFailureMessage(located.code)
195764
195977
  });
@@ -195766,7 +195979,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195766
195979
  }
195767
195980
  out.location = located.location;
195768
195981
  }
195769
- const listing = await port.listDir(child, DELEGATION_OUTPUTS_DIR).catch((err) => ({
195982
+ const listing = await port.listDir(child, SESSION_OUTPUTS_DIR).catch((err) => ({
195770
195983
  ok: false,
195771
195984
  code: "INTERNAL",
195772
195985
  message: err instanceof Error ? err.message : String(err)
@@ -195776,11 +195989,11 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195776
195989
  if (listing.code === "NOT_FOUND") return out;
195777
195990
  const code2 = listing.code;
195778
195991
  const message = isLocationFailureCode(code2) ? listing.message ?? locationFailureMessage(code2) : listing.message ?? code2;
195779
- out.skipped.push({ source: `${DELEGATION_OUTPUTS_DIR}/`, code: code2, message });
195992
+ out.skipped.push({ source: `${SESSION_OUTPUTS_DIR}/`, code: code2, message });
195780
195993
  return out;
195781
195994
  }
195782
195995
  out.listDirCode = "OK";
195783
- const queue = [DELEGATION_OUTPUTS_DIR];
195996
+ const queue = [SESSION_OUTPUTS_DIR];
195784
195997
  const files = [];
195785
195998
  let guard = 0;
195786
195999
  let hitFileCap = false;
@@ -195791,7 +196004,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195791
196004
  }
195792
196005
  guard += 1;
195793
196006
  const dir = queue.shift();
195794
- 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" }));
195795
196008
  if (!level.ok) {
195796
196009
  out.skipped.push({ source: `${dir}/`, code: level.code, message: level.code });
195797
196010
  continue;
@@ -195817,7 +196030,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195817
196030
  });
195818
196031
  }
195819
196032
  for (const file of files) {
195820
- const relative5 = normalizeRelPath(file.rel.slice(DELEGATION_OUTPUTS_DIR.length + 1));
196033
+ const relative5 = normalizeRelPath(file.rel.slice(SESSION_OUTPUTS_DIR.length + 1));
195821
196034
  if (!relative5) {
195822
196035
  out.skipped.push({ source: file.rel, code: "PATH_REJECTED", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF0C\u6CA1\u642C" });
195823
196036
  continue;
@@ -204058,7 +204271,7 @@ async function startOasisServer(opts) {
204058
204271
  const leftWaiterIds = /* @__PURE__ */ new Map();
204059
204272
  const LEFT_WAITER_IDS_MAX_DRAFTS = 512;
204060
204273
  const terminalReviews = /* @__PURE__ */ new Map();
204061
- const notifyReviewResolved = (draftId, r, resume, storeFor) => {
204274
+ const notifyReviewResolved = (draftId, r, resume, storeFor, companyId) => {
204062
204275
  if (terminalReviews.has(draftId)) return;
204063
204276
  terminalReviews.set(draftId, r);
204064
204277
  if (terminalReviews.size > 512) {
@@ -204078,7 +204291,7 @@ async function startOasisServer(opts) {
204078
204291
  leftWaiterIds.delete(draftId);
204079
204292
  if (resume?.origin) {
204080
204293
  const needsAgentDelivery = resume.kind === "command" && !hadWaiters;
204081
- void settleAuthorizationStatusLine(resume.origin, r, resume, draftId, needsAgentDelivery, storeFor);
204294
+ void settleAuthorizationStatusLine(resume.origin, r, resume, draftId, needsAgentDelivery, storeFor, companyId);
204082
204295
  }
204083
204296
  };
204084
204297
  const resolveEngine = opts.resolveEngine ?? (async () => ({ kernel: opts.kernel, oplog: opts.oplog, blobs: opts.blobs, ...opts.registry ? { registry: opts.registry } : {} }));
@@ -204156,6 +204369,14 @@ async function startOasisServer(opts) {
204156
204369
  }
204157
204370
  return null;
204158
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
+ };
204159
204380
  async function anchorMessageIdFor(origin, storeFor) {
204160
204381
  if (!origin) return void 0;
204161
204382
  const live = liveChat.messageFor(origin);
@@ -204220,7 +204441,7 @@ async function startOasisServer(opts) {
204220
204441
  const label = ctx.effectLabel || ctx.command || "";
204221
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`;
204222
204443
  };
204223
- const settleAuthorizationStatusLine = async (origin, resolution, ctx, draftId, needsAgentDelivery, storeFor) => {
204444
+ const settleAuthorizationStatusLine = async (origin, resolution, ctx, draftId, needsAgentDelivery, storeFor, companyId) => {
204224
204445
  const store = storeFor ? await storeFor().catch(() => null) : opts.chatSession;
204225
204446
  if (!store) return;
204226
204447
  const session = await store.getSession(origin).catch(() => null);
@@ -204293,10 +204514,12 @@ async function startOasisServer(opts) {
204293
204514
  ...dispatched.supportsLiveProtocol !== void 0 ? { supportsLiveProtocol: dispatched.supportsLiveProtocol } : {},
204294
204515
  done: dispatched.done
204295
204516
  };
204517
+ const resumeOutputsPart = outputsPartFor(origin, companyId);
204296
204518
  runBackgroundAssistantChatTurn({
204297
204519
  chatStore: store,
204298
204520
  chatSessionId: origin,
204299
- session: bgSession
204521
+ session: bgSession,
204522
+ ...resumeOutputsPart ? { outputsPart: resumeOutputsPart } : {}
204300
204523
  });
204301
204524
  await markDelivered();
204302
204525
  };
@@ -204552,10 +204775,12 @@ async function startOasisServer(opts) {
204552
204775
  ...session.supportsLiveProtocol !== void 0 ? { supportsLiveProtocol: session.supportsLiveProtocol } : {},
204553
204776
  ...session.done ? { done: session.done } : {}
204554
204777
  };
204778
+ const broadcastOutputsPart = outputsPartFor(sessionId, companyId);
204555
204779
  runBackgroundAssistantChatTurn({
204556
204780
  chatStore: store,
204557
204781
  chatSessionId: sessionId,
204558
204782
  session: bgSession,
204783
+ ...broadcastOutputsPart ? { outputsPart: broadcastOutputsPart } : {},
204559
204784
  liveChat,
204560
204785
  liveHandle: {
204561
204786
  runtimeSessionId: session.id,
@@ -205533,7 +205758,8 @@ async function startOasisServer(opts) {
205533
205758
  draftId,
205534
205759
  { status: "approved", command, effectLabel: review.command.effectLabel, authorizedBy: actor, receipt },
205535
205760
  { origin: review.origin, kind: "command", authorizedBy: actor, effectLabel: review.command.effectLabel, command },
205536
- chatStoreNowOrNull
205761
+ chatStoreNowOrNull,
205762
+ currentCompanyId
205537
205763
  );
205538
205764
  res.writeHead(200, { "content-type": "application/json" }).end(
205539
205765
  JSON.stringify({ applied: 1, command, artifactId, draftId, draftedBy: review.proposedBy, authorizedBy: actor })
@@ -206721,6 +206947,7 @@ ${composed}`;
206721
206947
  await checkpointChain.catch(() => void 0);
206722
206948
  await itemLedger.drain();
206723
206949
  const projectedContent = itemLedger.projectContent();
206950
+ const chatOutputsPart = outputsPartFor(persistTarget.id, currentCompanyId);
206724
206951
  await finalizeAssistantRow({
206725
206952
  chatStore,
206726
206953
  chatSessionId: persistTarget.id,
@@ -206730,6 +206957,7 @@ ${composed}`;
206730
206957
  status,
206731
206958
  ...session.runId ? { runId: session.runId } : {},
206732
206959
  ...heldTurn ? { turnId: heldTurn.id } : {},
206960
+ ...chatOutputsPart ? { outputsPart: chatOutputsPart } : {},
206733
206961
  now: () => finishedAt
206734
206962
  });
206735
206963
  await heldTurn?.settle({
@@ -213729,6 +213957,17 @@ var init_dev_store = __esm({
213729
213957
  /** ADR-0166 D6 待发队列(每会话一条,按 seq 升序 = 入队序)。 */
213730
213958
  pending = /* @__PURE__ */ new Map();
213731
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
+ }
213732
213971
  async enqueuePendingMessage(m2) {
213733
213972
  const list2 = this.pending.get(m2.chatSessionId) ?? [];
213734
213973
  if (m2.clientKey) {
@@ -214096,6 +214335,11 @@ var init_dev_store = __esm({
214096
214335
  return r;
214097
214336
  }
214098
214337
  // 待发队列也要落盘——它存在的全部理由就是"别只活在一处内存里"(ADR-0166 D6)。
214338
+ async claimPendingInsert(m2) {
214339
+ const result = await super.claimPendingInsert(m2);
214340
+ this.save();
214341
+ return result;
214342
+ }
214099
214343
  async enqueuePendingMessage(m2) {
214100
214344
  const record8 = await super.enqueuePendingMessage(m2);
214101
214345
  this.save();
@@ -214943,6 +215187,74 @@ var init_company_scoped_registry = __esm({
214943
215187
  }
214944
215188
  });
214945
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
+
214946
215258
  // ../server/src/org-model-backfill.ts
214947
215259
  function resolveDisplayName(existing, humanActor, accountName) {
214948
215260
  const pick3 = existing?.trim() || humanActor?.name?.trim() || accountName?.trim();
@@ -221367,11 +221679,11 @@ function aggregateUsageByActor(runs) {
221367
221679
  function weekWindow(now = /* @__PURE__ */ new Date()) {
221368
221680
  return { from: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3).toISOString(), to: now.toISOString() };
221369
221681
  }
221370
- async function listWeeklyUsageByActor(trace) {
221682
+ async function listWeeklyUsageByActor(trace, artifactIds) {
221371
221683
  if (!trace?.listUsageRuns) return null;
221372
221684
  const { from, to } = weekWindow();
221373
221685
  try {
221374
- return aggregateUsageByActor(await trace.listUsageRuns({ from, to }));
221686
+ return aggregateUsageByActor(await trace.listUsageRuns({ from, to, ...artifactIds ? { artifactIds: [...artifactIds] } : {} }));
221375
221687
  } catch {
221376
221688
  return null;
221377
221689
  }
@@ -221973,9 +222285,15 @@ function actorsDomain(opts) {
221973
222285
  const revealLimiter = new RevealRateLimiter(revealLimits.perSession, revealLimits.perSessionPerKey);
221974
222286
  const consoleRevealLimits = opts.credentialRevealLimits?.console ?? { perSession: 60, perSessionPerKey: 10 };
221975
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
+ }
221976
222293
  return (router) => {
221977
222294
  router.get("/api/actors", async (req) => {
221978
- const { service } = await resolveCtx(req.auth.companyId);
222295
+ const ctx = await resolveCtx(req.auth.companyId);
222296
+ const { service } = ctx;
221979
222297
  const filter = {};
221980
222298
  const kind = req.query.get("kind");
221981
222299
  const teamId = req.query.get("team_id");
@@ -221987,9 +222305,10 @@ function actorsDomain(opts) {
221987
222305
  const includeDeleted = req.query.get("include_deleted") === "1";
221988
222306
  const items = status || includeDeleted ? all2 : all2.filter((a) => a.kind !== "agent" || !isDeletedActor(a));
221989
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 }));
221990
222309
  const [configs, usageByActor] = await Promise.all([
221991
222310
  service.latestConfigs(agentIds),
221992
- listWeeklyUsageByActor(opts.trace?.())
222311
+ listWeeklyUsageByActor(usageSource.usage, usageSource.artifactIds)
221993
222312
  ]);
221994
222313
  return {
221995
222314
  status: 200,
@@ -222665,9 +222984,8 @@ function actorsDomain(opts) {
222665
222984
  return {
222666
222985
  status: 200,
222667
222986
  body: await actorStats(req.params.id, {
222668
- usage: req.auth.companyId && !model ? void 0 : opts.trace?.(),
222669
- model,
222670
- ...req.auth.companyId ? { artifactIds: new Set(model?.artifacts.keys() ?? []) } : {}
222987
+ ...await usageFor(req.auth.companyId, ctx),
222988
+ model
222671
222989
  })
222672
222990
  };
222673
222991
  });
@@ -223251,7 +223569,7 @@ function createActorsDomain(opts) {
223251
223569
  return {
223252
223570
  service: defaultCtx.service,
223253
223571
  resolveCtx,
223254
- 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 } : {} })
223255
223573
  };
223256
223574
  }
223257
223575
  var import_node_crypto49;
@@ -228494,7 +228812,7 @@ function reasonFromWorkdirCode(code2) {
228494
228812
  return "ERROR";
228495
228813
  }
228496
228814
  }
228497
- function joinRel(dir, name) {
228815
+ function joinRel2(dir, name) {
228498
228816
  return dir ? `${dir}/${name}` : name;
228499
228817
  }
228500
228818
  async function collectWorkdirFiles(bridge, loc, now = Date.now) {
@@ -228527,14 +228845,14 @@ async function collectWorkdirFiles(bridge, loc, now = Date.now) {
228527
228845
  if (outcome.truncated) truncated = true;
228528
228846
  for (const entry of outcome.entries) {
228529
228847
  if (entry.type === "dir") {
228530
- nextDirs.push(joinRel(dir, entry.name));
228848
+ nextDirs.push(joinRel2(dir, entry.name));
228531
228849
  continue;
228532
228850
  }
228533
228851
  if (items.length >= WORKDIR_WALK_MAX_ENTRIES) {
228534
228852
  truncated = true;
228535
228853
  continue;
228536
228854
  }
228537
- const rel = joinRel(dir, entry.name);
228855
+ const rel = joinRel2(dir, entry.name);
228538
228856
  items.push({
228539
228857
  id: `workdir:${rel}`,
228540
228858
  name: entry.name,
@@ -231227,6 +231545,9 @@ function nodesDomain(deps) {
231227
231545
  const rts = runtimesByNode.get(n.id) ?? [];
231228
231546
  const daemonVersion = daemonVersionById.get(n.id);
231229
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;
231230
231551
  const activeRunCount = deps.activeRunCountOf?.(n.id) ?? 0;
231231
231552
  const updateStatus = reconcileNodeUpdate(n.id, daemonVersion, activeRunCount, healthNow);
231232
231553
  return {
@@ -231240,10 +231561,10 @@ function nodesDomain(deps) {
231240
231561
  ...sourceIpById.get(n.id) ? { sourceIp: sourceIpById.get(n.id) } : {},
231241
231562
  ...n.name ? { name: n.name } : {},
231242
231563
  ...updateStatus ? { updateStatus } : {},
231243
- ...connection ? {
231564
+ ...lastDisconnectedAt || lastReconnectedAt ? {
231244
231565
  connection: {
231245
- ...connection.lastDisconnectedAt ? { lastDisconnectedAt: new Date(connection.lastDisconnectedAt).toISOString() } : {},
231246
- ...connection.lastReconnectedAt ? { lastReconnectedAt: new Date(connection.lastReconnectedAt).toISOString() } : {}
231566
+ ...lastDisconnectedAt ? { lastDisconnectedAt } : {},
231567
+ ...lastReconnectedAt ? { lastReconnectedAt } : {}
231247
231568
  }
231248
231569
  } : {},
231249
231570
  ...deps.nodeHealth ? {
@@ -247647,6 +247968,7 @@ var init_postgres_chat_sessions = __esm({
247647
247968
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS escalation_id text`);
247648
247969
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS model text`);
247649
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`);
247650
247972
  await pool.query(`
247651
247973
  WITH ranked AS (
247652
247974
  SELECT id, row_number() OVER (PARTITION BY session_id ORDER BY seq, created_at, id) AS rn
@@ -247877,6 +248199,23 @@ var init_postgres_chat_sessions = __esm({
247877
248199
  args
247878
248200
  );
247879
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
+ }
247880
248219
  /** 删项目时把本公司里绑到它的会话回落成「不指定项目」。会话一条都不删,只清 project_id。 */
247881
248220
  async clearProjectBinding(projectId2) {
247882
248221
  if (!projectId2) return 0;
@@ -248279,6 +248618,42 @@ var init_postgres_chat_sessions = __esm({
248279
248618
  return r.rows.map(rowToMessage);
248280
248619
  }
248281
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
+ }
248282
248657
  async enqueuePendingMessage(m2) {
248283
248658
  await this.assertSessionInScope(m2.chatSessionId);
248284
248659
  const initialState = m2.state;
@@ -253695,9 +254070,19 @@ var init_cutover_aware_trace = __esm({
253695
254070
  constructor(o) {
253696
254071
  this.o = o;
253697
254072
  }
253698
- 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() {
253699
254076
  const row = await this.o.cutover.get(this.o.companyId, "trace");
253700
- 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;
253701
254086
  }
253702
254087
  async pickWritable() {
253703
254088
  const row = await this.o.cutover.get(this.o.companyId, "trace");
@@ -257981,6 +258366,7 @@ var init_src11 = __esm({
257981
258366
  init_tenancy_move_verify();
257982
258367
  init_company_scoped_registry();
257983
258368
  init_session_workdir();
258369
+ init_outputs_scan();
257984
258370
  init_org_model_backfill();
257985
258371
  init_types_admin();
257986
258372
  init_roles_admin();
@@ -258390,6 +258776,13 @@ init_src11();
258390
258776
  init_src();
258391
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";
258392
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");
258393
258786
  function buildChatTaskHeader(input) {
258394
258787
  if (input.kind === "isolated-knowledge") {
258395
258788
  return {
@@ -258410,7 +258803,9 @@ function buildChatTaskHeader(input) {
258410
258803
  `your_actor_id: ${input.actorId}`,
258411
258804
  `workspace: ${input.workspace}`,
258412
258805
  "",
258413
- CONVERSATIONAL_MANAGER_BODY
258806
+ CONVERSATIONAL_MANAGER_BODY,
258807
+ "",
258808
+ OUTPUTS_CONVENTION
258414
258809
  ]
258415
258810
  };
258416
258811
  }
@@ -258422,7 +258817,9 @@ function buildChatTaskHeader(input) {
258422
258817
  input.sessionLine,
258423
258818
  `your_actor_id: ${input.actorId}`,
258424
258819
  "",
258425
- 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]
258426
258823
  ],
258427
258824
  ...routed ? { routingSystemPrompt: EXECUTION_ROUTING_BODY } : {}
258428
258825
  };
@@ -259597,6 +259994,12 @@ async function startServe(opts) {
259597
259994
  kernel: defaultCompanyKernel,
259598
259995
  blobs: assets,
259599
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
+ },
259600
260003
  audit: registryAudit,
259601
260004
  // ADR 凭据保险库 §4.5/§4.7 / ADR 0125:reveal 判权的项目解析 + 凭据读取审计读写。
259602
260005
  resolveDispatchScope: resolveDispatchScope2,
@@ -261615,6 +262018,39 @@ async function startServe(opts) {
261615
262018
  // 三个方法的组合规则住在 `createDelegationWorkdirPort`(`@oasis/server`)——评审
261616
262019
  // wo:acfee725 blocker ② 回归防线:**serve.ts 与单测走同一处**,那样「装配层退回全局 binding」
261617
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
+ },
261618
262054
  delegationWorkdir: createDelegationWorkdirPort({
261619
262055
  locator: delegationWorkdirLocator,
261620
262056
  // 桥在 hub 建好后(本函数尾部)才赋值 → getter 每次调用时读最新值;早于此处的构造时 `workdirBridge`
@@ -265300,6 +265736,7 @@ async function runSession(dispatchId, job, deps) {
265300
265736
  const cancelledBeforeStart = deps.wasCancelled?.() ?? false;
265301
265737
  const normalizer = new ProviderStreamNormalizer({
265302
265738
  providerName: job.binding?.runtimeKind ?? "runtime",
265739
+ hasLiveProtocol: handle.supportsLiveProtocol === true,
265303
265740
  fallbackTurnId: `oasis-turn:${handle.id}`,
265304
265741
  emit: (event) => deps.sink.stream({ type: "session_normalized_event", dispatchId, event })
265305
265742
  });
@@ -272392,7 +272829,7 @@ function shimScript() {
272392
272829
  }
272393
272830
 
272394
272831
  // src/index.ts
272395
- var PKG_VERSION = true ? "2.2.19" : "dev";
272832
+ var PKG_VERSION = true ? "2.2.20" : "dev";
272396
272833
  var LOCAL_BIN = localBin();
272397
272834
  var NPM_PREFIX = npmPrefix();
272398
272835
  var INSTANCE = DEFAULT_INSTANCE;