@khorsheed/dsh-room 0.2.3 → 0.2.5

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 (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/client.js +2309 -1127
  3. package/lib/index.js +572 -36
  4. package/lib/typert.host.js +249 -65
  5. package/lib/typert.remote-client.d.ts +4 -1
  6. package/lib/typert.remote-client.js +208 -53
  7. package/lib/types/client/InviteDialog.js +11 -4
  8. package/lib/types/client/RoomAttachments.d.ts +20 -0
  9. package/lib/types/client/RoomAttachments.js +36 -0
  10. package/lib/types/client/RoomComposer.d.ts +6 -12
  11. package/lib/types/client/RoomComposer.js +116 -12
  12. package/lib/types/client/RoomQuestionView.d.ts +10 -2
  13. package/lib/types/client/RoomQuestionView.js +18 -5
  14. package/lib/types/client/RoomRunView.d.ts +1 -1
  15. package/lib/types/client/RoomRunView.js +15 -5
  16. package/lib/types/client/RoomSpeechView.d.ts +24 -1
  17. package/lib/types/client/RoomSpeechView.js +39 -3
  18. package/lib/types/client/RoomStatsLine.d.ts +3 -1
  19. package/lib/types/client/RoomStatsLine.js +3 -1
  20. package/lib/types/client/RoomTaskCapsule.d.ts +6 -1
  21. package/lib/types/client/RoomTaskCapsule.js +2 -2
  22. package/lib/types/client/TasksView.js +1 -1
  23. package/lib/types/client/TeamView.js +1 -1
  24. package/lib/types/client/icons.d.ts +2 -1
  25. package/lib/types/client/icons.js +1 -0
  26. package/lib/types/client/index.js +94 -15
  27. package/lib/types/client/locales.d.ts +44 -0
  28. package/lib/types/client/locales.js +44 -0
  29. package/lib/types/client/nodes.d.ts +22 -3
  30. package/lib/types/client/nodes.js +28 -5
  31. package/lib/types/client/path-mentions.d.ts +34 -0
  32. package/lib/types/client/path-mentions.js +39 -0
  33. package/lib/types/client/slots.d.ts +51 -1
  34. package/lib/types/dispatch.d.ts +60 -0
  35. package/lib/types/dispatch.js +136 -1
  36. package/lib/types/index.d.ts +69 -1
  37. package/lib/types/index.js +368 -20
  38. package/lib/types/jobs.js +7 -3
  39. package/lib/types/journal.d.ts +1 -1
  40. package/lib/types/journal.js +18 -2
  41. package/lib/types/types.d.ts +77 -0
  42. package/package.json +10 -6
  43. package/lib/journal-B4FOGO6S.js +0 -482
  44. package/lib/journal-BWL3-i0s.js +0 -462
  45. package/lib/journal-Rid9AAnD.js +0 -485
package/lib/index.js CHANGED
@@ -700,10 +700,14 @@ function replayJobs(events, deliveries) {
700
700
  ...event.data.probability === void 0 ? {} : { probability: event.data.probability }
701
701
  });
702
702
  else if (event.type === "room/job-stopped") stopped.add(event.data.task);
703
- else if (event.type === "room/dispatch" && event.data.id !== void 0) dispatchById.set(event.data.id, {
704
- time: event.time,
705
- text: event.data.text
706
- });
703
+ else if (event.type === "room/dispatch" && event.data.id !== void 0) {
704
+ const from = event.data.origin !== "human" && event.data.from !== void 0 ? event.data.from : void 0;
705
+ dispatchById.set(event.data.id, {
706
+ time: event.time,
707
+ text: event.data.text,
708
+ ...from === void 0 ? {} : { from }
709
+ });
710
+ }
707
711
  const byTask = /* @__PURE__ */ new Map();
708
712
  for (const row of deliveries) {
709
713
  if (row.task === void 0) continue;
@@ -735,7 +739,8 @@ function replayJobs(events, deliveries) {
735
739
  messages: [{
736
740
  id,
737
741
  time: root.time,
738
- text: root.text
742
+ text: root.text,
743
+ ...root.from === void 0 ? {} : { from: root.from }
739
744
  }, ...follow].sort((a, b) => a.time - b.time),
740
745
  status,
741
746
  startedAt: root.time,
@@ -856,6 +861,7 @@ const ROOM_EVENT_TYPES = [
856
861
  "room/dispatch",
857
862
  "room/speech",
858
863
  "room/run-state",
864
+ "room/run-moved",
859
865
  "room/execution-metadata",
860
866
  "room/relay",
861
867
  "room/relay-resolved",
@@ -937,6 +943,7 @@ function replay(events) {
937
943
  const relays = [];
938
944
  const approvals = /* @__PURE__ */ new Map();
939
945
  const questions = /* @__PURE__ */ new Map();
946
+ const asks = /* @__PURE__ */ new Map();
940
947
  const relayById = /* @__PURE__ */ new Map();
941
948
  const tasks = [];
942
949
  const taskById = /* @__PURE__ */ new Map();
@@ -1090,10 +1097,27 @@ function replay(events) {
1090
1097
  questions: event.data.questions,
1091
1098
  time: event.time
1092
1099
  });
1100
+ asks.set(event.data.id, {
1101
+ id: event.data.id,
1102
+ member: event.data.member,
1103
+ questions: event.data.questions,
1104
+ state: "pending",
1105
+ time: event.time,
1106
+ ...event.data.runId === void 0 ? {} : { runId: event.data.runId },
1107
+ ...event.data.consent === true ? { consent: true } : {}
1108
+ });
1093
1109
  break;
1094
- case "room/question-resolved":
1110
+ case "room/question-resolved": {
1095
1111
  questions.delete(event.data.id);
1112
+ const ask = asks.get(event.data.id);
1113
+ if (ask !== void 0) asks.set(ask.id, {
1114
+ ...ask,
1115
+ state: event.data.outcome,
1116
+ ...event.data.answers === void 0 ? {} : { answers: event.data.answers },
1117
+ ...event.data.late === true ? { late: true } : {}
1118
+ });
1096
1119
  break;
1120
+ }
1097
1121
  case "room/approval-resolved":
1098
1122
  approvals.delete(event.data.id);
1099
1123
  break;
@@ -1216,6 +1240,7 @@ function replay(events) {
1216
1240
  relays,
1217
1241
  ...approvals.size === 0 ? {} : { approvals: [...approvals.values()] },
1218
1242
  ...questions.size === 0 ? {} : { questions: [...questions.values()] },
1243
+ ...asks.size === 0 ? {} : { askLog: [...asks.values()] },
1219
1244
  tasks,
1220
1245
  ...executions.size === 0 ? {} : { executions: [...executions.values()] },
1221
1246
  ...plan === void 0 ? {} : { plan },
@@ -1948,11 +1973,89 @@ function roomSource() {
1948
1973
  return Object.freeze({ kind: ROOM_KIND });
1949
1974
  }
1950
1975
  /**
1951
- * A run-state edge's child-session identity: the main agent runs on the room
1952
- * itself, external members on their child session. Optional — a fresh CLI
1953
- * member's early running edge predates the child handle's publication (the
1954
- * terminal edge and the journal backfill cover it).
1976
+ * The text a member's current round has said so far in its own session: the
1977
+ * assistant text blocks since the latest turn start, in order.
1978
+ * @param events - the member's child session log.
1979
+ * @returns the round's text so far.
1955
1980
  */
1981
+ function roundTextSoFar(events) {
1982
+ let start = -1;
1983
+ for (let index = events.length - 1; index >= 0; index -= 1) if (events[index].type === "turn/start") {
1984
+ start = index;
1985
+ break;
1986
+ }
1987
+ const parts = [];
1988
+ for (const event of events.slice(start + 1)) {
1989
+ if (event.type !== "assistant/message") continue;
1990
+ const content = event.data.message?.content ?? [];
1991
+ for (const block of content) if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
1992
+ }
1993
+ return parts.join("");
1994
+ }
1995
+ /**
1996
+ * Where each question a round asked sits in its final reply: just after the
1997
+ * text the round had said when it asked (its recorded tail, shortened until
1998
+ * found). Asked before saying anything → 0; tail not found → -1 (shown
1999
+ * first, marked as asked mid-round). Offsets never go backwards.
2000
+ * @param events - the room log.
2001
+ * @param runId - the round.
2002
+ * @param text - the round's reply.
2003
+ * @returns the question ids with their offsets.
2004
+ */
2005
+ function askOffsets(events, runId, text) {
2006
+ const out = [];
2007
+ let floor = 0;
2008
+ for (const event of events) {
2009
+ if (event.type !== "room/question" || event.data.runId !== runId) continue;
2010
+ const tail = (event.data.before ?? "").trimEnd();
2011
+ let at = tail === "" ? 0 : -1;
2012
+ for (const size of [
2013
+ 80,
2014
+ 40,
2015
+ 20
2016
+ ]) {
2017
+ if (tail === "") break;
2018
+ const probe = tail.slice(-size);
2019
+ if (probe.length < Math.min(size, 8)) break;
2020
+ const found = text.indexOf(probe, floor);
2021
+ if (found >= 0) {
2022
+ at = found + probe.length;
2023
+ break;
2024
+ }
2025
+ }
2026
+ if (at > 0) floor = at;
2027
+ out.push({
2028
+ id: event.data.id,
2029
+ at
2030
+ });
2031
+ }
2032
+ return out;
2033
+ }
2034
+ /** Why a human message's move-on closes a restart-lost round (index.ts moveOnFromLostRounds). */
2035
+ const MOVED_ON_TEXT = "你发了新消息;被重启打断的这一轮按没做完处理";
2036
+ /** A message that queued behind a lost round and rode along with the move-on message. */
2037
+ const MERGED_BACKLOG_TEXT = "已并入下一条消息一起送达";
2038
+ /** A message that queued behind a lost round an earlier move-on skipped: closed, not replayed. */
2039
+ const SUPERSEDED_BACKLOG_TEXT = "被重启打断前排队,已被后续消息取代";
2040
+ /**
2041
+ * Whether a queued delivery sat behind a restart-lost round of the same
2042
+ * member that a human message has since moved on from (its dispatch lies
2043
+ * between the lost round and the move-on edge).
2044
+ * @param events - the room journal.
2045
+ * @param targetId - the member id.
2046
+ * @param dispatchSeq - the queued delivery's dispatch seq.
2047
+ * @returns whether the move-on superseded it.
2048
+ */
2049
+ function supersededByMoveOn(events, targetId, dispatchSeq) {
2050
+ return events.some((event) => event.type === "room/delivery-state" && event.data.memberId === targetId && event.data.state === "cancelled" && event.data.text === "你发了新消息;被重启打断的这一轮按没做完处理" && event.data.dispatchSeq < dispatchSeq && dispatchSeq < event.seq);
2051
+ }
2052
+ function floatRunningCards(room, except) {
2053
+ const running = replay(room.snapshotEvents()).runs.filter((run) => run.state === "running" && run.member !== except).sort((left, right) => left.startedAt - right.startedAt);
2054
+ for (const run of running) room.append("room/run-moved", {
2055
+ member: run.member,
2056
+ startedAt: run.startedAt
2057
+ });
2058
+ }
1956
2059
  function runChildSessionId(roomId, member) {
1957
2060
  if (member?.kind === "main-agent") return roomId;
1958
2061
  return member?.childSessionId;
@@ -2272,6 +2375,17 @@ var DispatchEngine = class {
2272
2375
  } else await this.reportOutcome(room, dispatch.seq, id, targetId, last.data.state, last.data.text ?? last.data.error ?? "");
2273
2376
  continue;
2274
2377
  }
2378
+ if (supersededByMoveOn(events, targetId, dispatch.seq)) {
2379
+ room.append("room/delivery-state", {
2380
+ id,
2381
+ dispatchSeq: dispatch.seq,
2382
+ memberId: targetId,
2383
+ state: "cancelled",
2384
+ text: SUPERSEDED_BACKLOG_TEXT
2385
+ });
2386
+ await this.ctx.sessions.flush(room);
2387
+ continue;
2388
+ }
2275
2389
  const target = state.members.find((member) => memberId(events, member) === targetId);
2276
2390
  if (target !== void 0 && !state.deliveries?.some((row) => row.memberId === targetId && (row.status === "running" || row.status === "uncertain"))) this.dispatch(room, target.name, dispatch.data.text, {
2277
2391
  dispatchSeq: dispatch.seq,
@@ -2351,7 +2465,28 @@ var DispatchEngine = class {
2351
2465
  * coordinator for plan work, else the dispatch's `replyTo`), at most once
2352
2466
  * per delivery. Public so a removal can settle the waits it breaks.
2353
2467
  */
2468
+ /** Report-gate decisions per recipient, one at a time (see reportOutcome). */
2469
+ gateQueues = /* @__PURE__ */ new Map();
2470
+ /**
2471
+ * Gate one arrived report for the member waiting on it. Decisions for the
2472
+ * same recipient run one at a time: a decision-model judgement takes a
2473
+ * second or two, and a later report deciding meanwhile (the 「最后一个结果」
2474
+ * rule) would hand over its batch without the one still being judged —
2475
+ * which then got held behind a round that had already answered.
2476
+ */
2354
2477
  async reportOutcome(room, dispatchSeq, deliveryId, fromId, outcome, text) {
2478
+ const source = room.snapshotEvents().find((event) => event.seq === dispatchSeq);
2479
+ const key = `${room.id} ${source?.type === "room/dispatch" ? source.data.replyTo ?? "" : ""}`;
2480
+ const next = (this.gateQueues.get(key) ?? Promise.resolve()).catch(() => {}).then(() => this.gateReport(room, dispatchSeq, deliveryId, fromId, outcome, text));
2481
+ this.gateQueues.set(key, next);
2482
+ next.then(() => {
2483
+ if (this.gateQueues.get(key) === next) this.gateQueues.delete(key);
2484
+ }, () => {
2485
+ if (this.gateQueues.get(key) === next) this.gateQueues.delete(key);
2486
+ });
2487
+ return next;
2488
+ }
2489
+ async gateReport(room, dispatchSeq, deliveryId, fromId, outcome, text) {
2355
2490
  const events = room.snapshotEvents();
2356
2491
  const source = events.find((event) => event.seq === dispatchSeq);
2357
2492
  if (source?.type !== "room/dispatch" || source.data.origin !== "coordinator" && source.data.origin !== "member" || source.data.replyTo === void 0) return;
@@ -2649,6 +2784,7 @@ var DispatchEngine = class {
2649
2784
  ...lastAssistantMessageId(agent.session, before) === void 0 ? {} : { messageId: lastAssistantMessageId(agent.session, before) },
2650
2785
  ...reportedTo === void 0 ? {} : { reportedTo }
2651
2786
  });
2787
+ floatRunningCards(room, replay(room.snapshotEvents()).members.find((entry) => entry.id === member.id)?.name ?? member.name);
2652
2788
  await this.ctx.sessions.flush(room);
2653
2789
  return {
2654
2790
  state: "done",
@@ -2750,16 +2886,22 @@ var DispatchEngine = class {
2750
2886
  const interrupted = result.stopReason === "completed" ? void 0 : result.stopReason === "aborted" ? "cancelled" : "failed";
2751
2887
  const reportedTo = dispatchSeq === void 0 ? void 0 : reportRecipient(room.snapshotEvents(), dispatchSeq)?.name;
2752
2888
  const closingMessageId = lastAssistantMessageId(this.ctx.sessions.get(run.id));
2753
- if ((output !== "" || interrupted === void 0 ? room.append("room/speech", {
2889
+ const asks = askOffsets(room.snapshotEvents(), runId, output);
2890
+ const speech = output !== "" || interrupted === void 0 ? room.append("room/speech", {
2754
2891
  member: replay(room.snapshotEvents()).members.find((entry) => entry.id === member.id)?.name ?? member.name,
2755
2892
  text: output,
2893
+ ...asks.length === 0 ? {} : { asks },
2756
2894
  childSessionId: run.id,
2757
2895
  runId,
2758
2896
  ...closingMessageId === void 0 ? {} : { messageId: closingMessageId },
2759
2897
  durationMs: Date.now() - startedAt,
2760
2898
  ...interrupted === void 0 ? {} : { interrupted },
2761
2899
  ...reportedTo === void 0 ? {} : { reportedTo }
2762
- }) : void 0) !== void 0) await this.ctx.sessions.flush(room);
2900
+ }) : void 0;
2901
+ if (speech !== void 0) {
2902
+ floatRunningCards(room, speech.data.member);
2903
+ await this.ctx.sessions.flush(room);
2904
+ }
2763
2905
  if (result.stopReason === "completed") {
2764
2906
  await this.ctx.sessions.flush(room);
2765
2907
  return {
@@ -3426,6 +3568,64 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
3426
3568
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
3427
3569
  done = true;
3428
3570
  };
3571
+ var __addDisposableResource = function(env, value, async) {
3572
+ if (value !== null && value !== void 0) {
3573
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
3574
+ var dispose, inner;
3575
+ if (async) {
3576
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
3577
+ dispose = value[Symbol.asyncDispose];
3578
+ }
3579
+ if (dispose === void 0) {
3580
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
3581
+ dispose = value[Symbol.dispose];
3582
+ if (async) inner = dispose;
3583
+ }
3584
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
3585
+ if (inner) dispose = function() {
3586
+ try {
3587
+ inner.call(this);
3588
+ } catch (e) {
3589
+ return Promise.reject(e);
3590
+ }
3591
+ };
3592
+ env.stack.push({
3593
+ value,
3594
+ dispose,
3595
+ async
3596
+ });
3597
+ } else if (async) env.stack.push({ async: true });
3598
+ return value;
3599
+ };
3600
+ var __disposeResources = (function(SuppressedError) {
3601
+ return function(env) {
3602
+ function fail(e) {
3603
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
3604
+ env.hasError = true;
3605
+ }
3606
+ var r, s = 0;
3607
+ function next() {
3608
+ while (r = env.stack.pop()) try {
3609
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
3610
+ if (r.dispose) {
3611
+ var result = r.dispose.call(r.value);
3612
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
3613
+ fail(e);
3614
+ return next();
3615
+ });
3616
+ } else s |= 1;
3617
+ } catch (e) {
3618
+ fail(e);
3619
+ }
3620
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
3621
+ if (env.hasError) throw env.error;
3622
+ }
3623
+ return next();
3624
+ };
3625
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
3626
+ var e = new Error(message);
3627
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3628
+ });
3429
3629
  var __rewriteRelativeImportExtension = function(path, preserveJsx) {
3430
3630
  if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
3431
3631
  return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
@@ -3497,6 +3697,44 @@ function parseQuestions(raw) {
3497
3697
  if (new Set(questions.map((question) => question.id)).size !== questions.length) return "Question ids must be unique";
3498
3698
  return questions;
3499
3699
  }
3700
+ /**
3701
+ * The message a late answer reaches its member as: each question with the
3702
+ * human's pick, so the member can go on (or correct what it assumed).
3703
+ * @param questions - the card's questions.
3704
+ * @param answers - the human's answers, one per question.
3705
+ * @returns the dispatch text.
3706
+ */
3707
+ function lateAnswerText(questions, answers) {
3708
+ return [
3709
+ "你之前用 ask_user_question 问的问题没等到回答就过期了,人现在补答了:",
3710
+ ...questions.map((question) => {
3711
+ const answer = answers.find((entry) => entry.id === question.id);
3712
+ const picked = [...answer?.selected ?? [], ...answer?.custom === void 0 ? [] : [answer.custom]].join(";");
3713
+ return `- ${question.header === void 0 ? "" : `${question.header}:`}${question.question} → ${picked === "" ? "(没选)" : picked}`;
3714
+ }),
3715
+ "",
3716
+ "按这些答案继续。如果你已经按自己的假设往下做了,对照答案调整,并说一下改了什么。"
3717
+ ].join("\n");
3718
+ }
3719
+ /**
3720
+ * The dispatch text with the message's attachments appended as local paths:
3721
+ * every member — CLI or native — opens them with its own read tool, so one
3722
+ * form reaches all of them.
3723
+ * @param text - the dispatched text.
3724
+ * @param attachments - the admitted attachments.
3725
+ * @returns the text members receive.
3726
+ */
3727
+ function withAttachmentPaths(text, attachments) {
3728
+ if (attachments === void 0 || attachments.length === 0) return text;
3729
+ return [
3730
+ text,
3731
+ "",
3732
+ "人随消息附了这些附件,用你自己的读取工具打开(图片用能看图的读取工具):",
3733
+ ...attachments.map((entry) => {
3734
+ return `- ${`${entry.kind === "image" ? "图片" : "文件"}${entry.name === void 0 ? "" : ` ${entry.name}`}`}:${entry.path ?? "(宿主没有给出本地路径,打不开就告诉人)"}`;
3735
+ })
3736
+ ].join("\n");
3737
+ }
3500
3738
  function messageReceipt(receipt, kind) {
3501
3739
  if (receipt.held !== void 0) return `pending-confirm (${receipt.held === "paused" ? "房间已暂停,恢复后自动投递" : receipt.held === "hop-limit" ? "这条消息链已到跳数上限,等房间主人决定是否继续" : "房间设置为成员间消息需房间主人确认"})`;
3502
3740
  return kind === "notify" ? `sent #${receipt.messageId}(知会,不会回报)` : `sent #${receipt.messageId}(${receipt.member} 做完后结果会自动回报给你;不要 sleep 等待,也不要用 room_read 轮询)`;
@@ -3540,6 +3778,7 @@ function handoffText(_events, state, sessionId, from, to) {
3540
3778
  let RoomService = (() => {
3541
3779
  let _classSuper = TypertRemoteService;
3542
3780
  let _instanceExtraInitializers = [];
3781
+ let _pendingCards_decorators;
3543
3782
  let _isRoom_decorators;
3544
3783
  let _listProviders_decorators;
3545
3784
  let _setTaskHint_decorators;
@@ -3569,6 +3808,7 @@ let RoomService = (() => {
3569
3808
  return class RoomService extends _classSuper {
3570
3809
  static {
3571
3810
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
3811
+ _pendingCards_decorators = [Remote("pendingCards")];
3572
3812
  _isRoom_decorators = [Remote("isRoom")];
3573
3813
  _listProviders_decorators = [Remote("listProviders")];
3574
3814
  _setTaskHint_decorators = [Remote("setTaskHint")];
@@ -3595,6 +3835,17 @@ let RoomService = (() => {
3595
3835
  _setGoal_decorators = [Remote("setGoal")];
3596
3836
  _cancel_decorators = [Remote("cancel")];
3597
3837
  _cancelChild_decorators = [Remote("cancelChild")];
3838
+ __esDecorate(this, null, _pendingCards_decorators, {
3839
+ kind: "method",
3840
+ name: "pendingCards",
3841
+ static: false,
3842
+ private: false,
3843
+ access: {
3844
+ has: (obj) => "pendingCards" in obj,
3845
+ get: (obj) => obj.pendingCards
3846
+ },
3847
+ metadata: _metadata
3848
+ }, null, _instanceExtraInitializers);
3598
3849
  __esDecorate(this, null, _isRoom_decorators, {
3599
3850
  kind: "method",
3600
3851
  name: "isRoom",
@@ -4014,6 +4265,12 @@ let RoomService = (() => {
4014
4265
  messages: []
4015
4266
  };
4016
4267
  });
4268
+ ctx.on("session/event", (session, event) => {
4269
+ this.observeActivity(session, event);
4270
+ });
4271
+ ctx.on("agent/status", ({ agent, status }) => {
4272
+ if (status !== "running" && (this.activeRuns.get(String(agent.id))?.size ?? 0) > 0) this.emitStatus(String(agent.id), true);
4273
+ });
4017
4274
  const scanned = /* @__PURE__ */ new WeakSet();
4018
4275
  ctx.on("agent/pre-step", async ({ agent, step }, next) => {
4019
4276
  const decision = await next();
@@ -4141,6 +4398,84 @@ let RoomService = (() => {
4141
4398
  * @param request - session identity.
4142
4399
  * @returns true when the session carries the marker (false for unknown sessions too).
4143
4400
  */
4401
+ /** Run ids of the members working in each room (the session list's 运行中). */
4402
+ activeRuns = /* @__PURE__ */ new Map();
4403
+ /** Question/approval cards waiting for the human, per room. */
4404
+ waitingCards = /* @__PURE__ */ new Map();
4405
+ /**
4406
+ * Follow a room's run and card edges as they are journaled: a room turns
4407
+ * running when its first member starts and settles when its last one ends
4408
+ * (the host's `api-session/status`, so the list shows 运行中 and then
4409
+ * 已完成), and its waiting cards are kept for {@link pendingCards}.
4410
+ * @param session - the session the event landed in.
4411
+ * @param event - the journaled event.
4412
+ */
4413
+ observeActivity(session, event) {
4414
+ const id = String(session.id);
4415
+ switch (event.type) {
4416
+ case "room/run-state": {
4417
+ const runs = this.activeRuns.get(id) ?? /* @__PURE__ */ new Set();
4418
+ const before = runs.size;
4419
+ const key = event.data.runId ?? `${event.data.member}:${event.data.startedAt}`;
4420
+ if (event.data.state === "running") runs.add(key);
4421
+ else runs.delete(key);
4422
+ if (runs.size === 0) this.activeRuns.delete(id);
4423
+ else this.activeRuns.set(id, runs);
4424
+ if (event.data.state !== "running") this.expireCardsOf(id, event.data.member);
4425
+ if (before === 0 && runs.size > 0) this.emitStatus(id, true);
4426
+ else if (before > 0 && runs.size === 0 && this.ctx.agents.get(session.id)?.status !== "running") this.emitStatus(id, false);
4427
+ return;
4428
+ }
4429
+ case "room/question":
4430
+ case "room/approval": {
4431
+ const cards = this.waitingCards.get(id) ?? /* @__PURE__ */ new Map();
4432
+ cards.set(event.data.id, {
4433
+ kind: event.type === "room/question" ? "question" : "approval",
4434
+ member: event.data.member
4435
+ });
4436
+ this.waitingCards.set(id, cards);
4437
+ return;
4438
+ }
4439
+ case "room/question-resolved":
4440
+ case "room/approval-resolved": {
4441
+ const cards = this.waitingCards.get(id);
4442
+ cards?.delete(event.data.id);
4443
+ if (cards?.size === 0) this.waitingCards.delete(id);
4444
+ return;
4445
+ }
4446
+ }
4447
+ }
4448
+ /**
4449
+ * End the waits behind one member's open cards (its round settled): the
4450
+ * question/approval calls resolve as unanswered, which journals them
4451
+ * expired — still answerable, by a new message (answerExpiredQuestion).
4452
+ * @param roomId - the room session.
4453
+ * @param member - the member whose round ended.
4454
+ */
4455
+ expireCardsOf(roomId, member) {
4456
+ for (const [cardId, card] of this.waitingCards.get(roomId) ?? []) {
4457
+ if (card.member !== member) continue;
4458
+ if (card.kind === "question") this.questionWaiters.get(cardId)?.(void 0);
4459
+ else this.approvalWaiters.get(cardId)?.("cancelled");
4460
+ }
4461
+ }
4462
+ /** Broadcast a room's running flip on the host's session-status channel. */
4463
+ emitStatus(sessionId, running) {
4464
+ this.ctx.emit("api-session/status", SessionId(sessionId), running);
4465
+ }
4466
+ /**
4467
+ * The question/approval cards waiting for the human across live rooms —
4468
+ * the browser publishes them as the session list's 等你处理 status.
4469
+ * @param request - optionally one room only.
4470
+ * @returns the waiting cards.
4471
+ */
4472
+ async pendingCards(request) {
4473
+ return (request.sessionId === void 0 ? [...this.waitingCards.keys()] : [String(request.sessionId)]).flatMap((sessionId) => [...this.waitingCards.get(sessionId) ?? []].map(([id, card]) => ({
4474
+ sessionId,
4475
+ id,
4476
+ kind: card.kind
4477
+ })));
4478
+ }
4144
4479
  async isRoom(request) {
4145
4480
  const session = this.ctx.sessions.get(request.sessionId);
4146
4481
  if (session !== void 0) return isRoomLog(session.snapshotEvents());
@@ -4463,7 +4798,7 @@ let RoomService = (() => {
4463
4798
  };
4464
4799
  const body = parsed.targets.length > 0 ? parsed.text : text;
4465
4800
  const filed = await this.fileHumanMessage(session, edit.data.id, text, task);
4466
- this.moveOnFromLostRounds(session, targets);
4801
+ const carry = this.moveOnFromLostRounds(session, targets);
4467
4802
  const dispatch = session.append("room/dispatch", {
4468
4803
  id: requestId,
4469
4804
  targets,
@@ -4473,7 +4808,7 @@ let RoomService = (() => {
4473
4808
  targetIds: targets.map((name) => memberId(session.snapshotEvents(), current.members.find((member) => member.name === name)))
4474
4809
  });
4475
4810
  await this.ctx.sessions.flush(session);
4476
- for (const target of targets) this.engine.dispatch(session, target, body, { dispatchSeq: dispatch.seq });
4811
+ for (const target of targets) this.engine.dispatch(session, target, carry(target, body), { dispatchSeq: dispatch.seq });
4477
4812
  return { ok: true };
4478
4813
  }
4479
4814
  /**
@@ -4482,10 +4817,12 @@ let RoomService = (() => {
4482
4817
  * message runs instead of queueing behind a reconciliation. Formal-goal
4483
4818
  * attempts keep their explicit reconciliation.
4484
4819
  */
4485
- moveOnFromLostRounds(session, targets, why = "你发了新消息;被重启打断的这一轮按没做完处理") {
4820
+ moveOnFromLostRounds(session, targets, why = MOVED_ON_TEXT) {
4486
4821
  const events = session.snapshotEvents();
4487
4822
  const state = replay(events);
4488
- const ids = new Set(state.members.filter((member) => targets.includes(member.name)).map((member) => memberId(events, member)));
4823
+ const members = state.members.filter((member) => targets.includes(member.name));
4824
+ const ids = new Set(members.map((member) => memberId(events, member)));
4825
+ const backlog = /* @__PURE__ */ new Map();
4489
4826
  for (const row of state.deliveries ?? []) {
4490
4827
  if (row.status !== "uncertain" || row.plan?.taskId !== void 0 || !ids.has(row.memberId)) continue;
4491
4828
  session.append("room/delivery-state", {
@@ -4503,7 +4840,31 @@ let RoomService = (() => {
4503
4840
  state: "cancelled",
4504
4841
  ...this.terminalRunStateFields(session, state, run.member)
4505
4842
  });
4843
+ const name = members.find((member) => memberId(events, member) === row.memberId)?.name;
4844
+ if (name === void 0) continue;
4845
+ for (const queued of state.deliveries ?? []) {
4846
+ if (queued.status !== "queued" || queued.memberId !== row.memberId || queued.dispatchSeq <= row.dispatchSeq || queued.plan?.taskId !== void 0) continue;
4847
+ session.append("room/delivery-state", {
4848
+ id: queued.id,
4849
+ dispatchSeq: queued.dispatchSeq,
4850
+ memberId: queued.memberId,
4851
+ state: "done",
4852
+ text: MERGED_BACKLOG_TEXT
4853
+ });
4854
+ const from = queued.from === void 0 ? void 0 : state.members.find((member) => memberId(events, member) === queued.from)?.name;
4855
+ backlog.set(name, [...backlog.get(name) ?? [], `- ${from === void 0 ? "" : `来自 ${from}:`}${queued.text}`]);
4856
+ }
4506
4857
  }
4858
+ return (member, text) => {
4859
+ const lines = backlog.get(member);
4860
+ return lines === void 0 ? text : [
4861
+ text,
4862
+ "",
4863
+ "(你被重启打断期间,房间里还有这些消息排着没送到,一并给你:",
4864
+ ...lines,
4865
+ ")"
4866
+ ].join("\n");
4867
+ };
4507
4868
  }
4508
4869
  /**
4509
4870
  * Where a member's next send belongs: the task and delivery it is working
@@ -4576,6 +4937,9 @@ let RoomService = (() => {
4576
4937
  ok: false,
4577
4938
  error: loaded.error
4578
4939
  };
4940
+ if (this.ctx.sessions.get(request.sessionId) === void 0 && (loaded.state.deliveries ?? []).some((row) => row.status === "held")) this.ensureLive(request.sessionId).catch((error) => {
4941
+ this.ctx.logger.warn(`room: reviving held reports failed: ${String(error)}`);
4942
+ });
4579
4943
  return {
4580
4944
  ok: true,
4581
4945
  value: {
@@ -5226,13 +5590,15 @@ let RoomService = (() => {
5226
5590
  ok: false,
5227
5591
  error: loaded.error
5228
5592
  };
5229
- if (request.text.trim() === "") return {
5593
+ const attached = request.attachments ?? [];
5594
+ if (request.text.trim() === "" && attached.length === 0) return {
5230
5595
  ok: false,
5231
5596
  error: { code: "empty-text" }
5232
5597
  };
5233
5598
  const requestSignature = createHash("sha256").update(JSON.stringify({
5234
5599
  text: request.text.trim(),
5235
- targets: [...new Set(request.targets ?? [])].sort()
5600
+ targets: [...new Set(request.targets ?? [])].sort(),
5601
+ ...attached.length === 0 ? {} : { attachments: attached.map((entry) => entry.type === "file" ? entry.receiptId : `${entry.mediaType}:${entry.data.length}`) }
5236
5602
  })).digest("hex");
5237
5603
  if (request.requestId !== void 0) {
5238
5604
  if (request.requestId.trim() === "" || request.requestId.length > 128) return {
@@ -5252,7 +5618,7 @@ let RoomService = (() => {
5252
5618
  await this.ctx.sessions.flush(loaded.session);
5253
5619
  for (const target of original.targetIds ?? []) {
5254
5620
  const member = replay(loaded.session.snapshotEvents()).members.find((member) => memberId(loaded.session.snapshotEvents(), member) === target);
5255
- if (member !== void 0) this.engine.dispatch(loaded.session, member.name, original.text, {
5621
+ if (member !== void 0) this.engine.dispatch(loaded.session, member.name, withAttachmentPaths(original.text, original.attachments), {
5256
5622
  dispatchSeq: previous.seq,
5257
5623
  targetId: target
5258
5624
  });
@@ -5285,8 +5651,8 @@ let RoomService = (() => {
5285
5651
  }
5286
5652
  const said = parsed.targets.length > 0 ? parsed.text : request.text.trim();
5287
5653
  const outside = [...said.matchAll(/@\[((?:\\.|[^\\\]\n])+)\]\((dsh-session:[^)\s]+)\)/gu)];
5288
- const text = said;
5289
- const prompt = outside.length === 0 ? said : [
5654
+ const text = said === "" && attached.length > 0 ? "(见附件)" : said;
5655
+ const prompt = outside.length === 0 ? text : [
5290
5656
  said,
5291
5657
  "",
5292
5658
  `(人点名了房间外的会话:${outside.map((match) => `「${match[1].replace(/\\(.)/gu, "$1")}」`).join("、")}。用 room_help 去问:session 参数原样传那个 @[…](dsh-session:…) 引用;标签是「成员(房间)」时会交给那个成员。不要转给别的成员代问。)`
@@ -5304,16 +5670,38 @@ let RoomService = (() => {
5304
5670
  names: unknown
5305
5671
  }
5306
5672
  };
5673
+ const admitted = await this.admitAttachments(request.sessionId, attached);
5674
+ if (!admitted.ok) return {
5675
+ ok: false,
5676
+ error: admitted.error
5677
+ };
5307
5678
  const human = createUserMessage({
5308
- content: [{
5679
+ content: [...admitted.blocks, ...request.text.trim() === "" ? [] : [{
5309
5680
  type: "text",
5310
5681
  text: request.text.trim()
5311
- }],
5682
+ }]],
5312
5683
  source: { kind: "user" }
5313
5684
  });
5314
- loaded.session.append("user/message", human, { surfaceOp: "append" });
5685
+ {
5686
+ const env_1 = {
5687
+ stack: [],
5688
+ error: void 0,
5689
+ hasError: false
5690
+ };
5691
+ try {
5692
+ const binding = __addDisposableResource(env_1, admitted.bind(request.requestId ?? String(human.id)), false);
5693
+ loaded.session.append("user/message", human, { surfaceOp: "append" });
5694
+ binding?.commit();
5695
+ } catch (e_1) {
5696
+ env_1.error = e_1;
5697
+ env_1.hasError = true;
5698
+ } finally {
5699
+ __disposeResources(env_1);
5700
+ }
5701
+ }
5702
+ floatRunningCards(loaded.session);
5315
5703
  const filed = await this.fileHumanMessage(loaded.session, human.id, request.text.trim(), request.task ?? this.consumeTaskHint(request.sessionId));
5316
- this.moveOnFromLostRounds(loaded.session, targets);
5704
+ const carry = this.moveOnFromLostRounds(loaded.session, targets);
5317
5705
  const dispatch = loaded.session.append("room/dispatch", {
5318
5706
  id: request.requestId ?? randomUUID(),
5319
5707
  requestSignature,
@@ -5321,10 +5709,11 @@ let RoomService = (() => {
5321
5709
  text,
5322
5710
  origin: "human",
5323
5711
  task: filed.task,
5712
+ ...admitted.records.length === 0 ? {} : { attachments: admitted.records },
5324
5713
  targetIds: targets.map((name) => memberId(loaded.session.snapshotEvents(), loaded.state.members.find((member) => member.name === name)))
5325
5714
  });
5326
5715
  await this.ctx.sessions.flush(loaded.session);
5327
- for (const target of targets) this.engine.dispatch(loaded.session, target, prompt, { dispatchSeq: dispatch.seq });
5716
+ for (const target of targets) this.engine.dispatch(loaded.session, target, carry(target, withAttachmentPaths(prompt, admitted.records)), { dispatchSeq: dispatch.seq });
5328
5717
  return {
5329
5718
  ok: true,
5330
5719
  value: {
@@ -5337,6 +5726,95 @@ let RoomService = (() => {
5337
5726
  };
5338
5727
  }
5339
5728
  /**
5729
+ * Admit a human message's composer attachments the way the host's own
5730
+ * prompt path does: file receipts resolve against this room session's
5731
+ * uploads, images are stored, and every attachment becomes a durable
5732
+ * content block for the message — plus the local path members read it by.
5733
+ * @param sessionId - the room session (its agent owns the uploads).
5734
+ * @param inputs - the wire attachments.
5735
+ * @returns the content blocks, the dispatch records, and the receipt binder; or a refusal.
5736
+ */
5737
+ async admitAttachments(sessionId, inputs) {
5738
+ if (inputs.length === 0) return {
5739
+ ok: true,
5740
+ blocks: [],
5741
+ records: [],
5742
+ bind: () => void 0
5743
+ };
5744
+ const probe = this.ctx.get;
5745
+ const services = {
5746
+ attachments: probe("attachments"),
5747
+ fileUploads: probe("fileUploads")
5748
+ };
5749
+ const store = services.attachments;
5750
+ let agent = this.ctx.agents.get(sessionId);
5751
+ if (agent === void 0) {
5752
+ const resolved = await this.ctx.get("sessionController")?.resolveAgent?.(sessionId).catch(() => void 0);
5753
+ if (resolved !== void 0 && "agent" in resolved) agent = resolved.agent;
5754
+ }
5755
+ if (store === void 0 || agent === void 0) return {
5756
+ ok: false,
5757
+ error: {
5758
+ code: "attachment-invalid",
5759
+ message: "attachments are unavailable on this host"
5760
+ }
5761
+ };
5762
+ const receipts = [];
5763
+ const parts = [];
5764
+ for (const input of inputs) {
5765
+ if (input.type === "image") {
5766
+ parts.push({
5767
+ type: "image",
5768
+ data: input.data,
5769
+ mediaType: input.mediaType,
5770
+ ...input.name === void 0 ? {} : { name: input.name }
5771
+ });
5772
+ continue;
5773
+ }
5774
+ const ref = services.fileUploads?.resolve(agent, input.receiptId);
5775
+ if (ref === void 0) return {
5776
+ ok: false,
5777
+ error: {
5778
+ code: "attachment-invalid",
5779
+ message: "a file was not uploaded to this room"
5780
+ }
5781
+ };
5782
+ receipts.push(input.receiptId);
5783
+ parts.push({
5784
+ type: "file",
5785
+ attachment: ref
5786
+ });
5787
+ }
5788
+ let blocks;
5789
+ try {
5790
+ blocks = await store.admitPromptContent(parts);
5791
+ } catch (error) {
5792
+ return {
5793
+ ok: false,
5794
+ error: {
5795
+ code: "attachment-invalid",
5796
+ message: error instanceof Error ? error.message : String(error)
5797
+ }
5798
+ };
5799
+ }
5800
+ const records = blocks.flatMap((block) => {
5801
+ if (block.type !== "image" && block.type !== "file") return [];
5802
+ const path = block.type === "image" ? store.imageHostPath?.(block.attachment) : store.fileHostPath?.(block.attachment);
5803
+ return [{
5804
+ kind: block.type,
5805
+ ...block.attachment?.name === void 0 ? {} : { name: block.attachment.name },
5806
+ ...path === void 0 ? {} : { path }
5807
+ }];
5808
+ });
5809
+ const uploads = services.fileUploads;
5810
+ return {
5811
+ ok: true,
5812
+ blocks,
5813
+ records,
5814
+ bind: (requestId) => receipts.length === 0 || uploads === void 0 ? void 0 : uploads.bindPrompt(agent, receipts, requestId)
5815
+ };
5816
+ }
5817
+ /**
5340
5818
  * Send one message from a member to another (host-only — the `room_message`
5341
5819
  * tool and the bridge's `member_message`/`room_message` land here; the
5342
5820
  * human's path is postMessage). Any member may message any other: the
@@ -5538,7 +6016,7 @@ let RoomService = (() => {
5538
6016
  header: "求助",
5539
6017
  question: `${asker.name} 想向会话「${target.title}」求助:${text.length > 80 ? `${text.slice(0, 80)}…` : text}`,
5540
6018
  options: [{ label: "允许" }, { label: "不允许" }]
5541
- }], 6e5))?.[0]?.selected[0] !== "允许") return `The human did not allow asking "${target.title}". Do not retry unless they @ that session in the room.`;
6019
+ }], 6e5, true))?.[0]?.selected[0] !== "允许") return `The human did not allow asking "${target.title}". Do not retry unless they @ that session in the room.`;
5542
6020
  }
5543
6021
  const help = {
5544
6022
  id: randomUUID().slice(0, 8),
@@ -5583,7 +6061,7 @@ let RoomService = (() => {
5583
6061
  roomTitle: this.roomTitle(room),
5584
6062
  from: help.fromName
5585
6063
  };
5586
- this.moveOnFromLostRounds(other.session, [member.name], "另一个协作间来求助;被重启打断的这一轮按没做完处理");
6064
+ const carry = this.moveOnFromLostRounds(other.session, [member.name], "另一个协作间来求助;被重启打断的这一轮按没做完处理");
5587
6065
  const sent = sendOrHold(other.session, {
5588
6066
  to: member,
5589
6067
  text,
@@ -5609,7 +6087,7 @@ let RoomService = (() => {
5609
6087
  state: sent.delivered ? "running" : "queued"
5610
6088
  });
5611
6089
  await this.ctx.sessions.flush(room);
5612
- if (sent.delivered) this.engine.dispatch(other.session, member.name, text, { dispatchSeq: sent.seq });
6090
+ if (sent.delivered) this.engine.dispatch(other.session, member.name, carry(member.name, text), { dispatchSeq: sent.seq });
5613
6091
  const who = `${member.name} in the room "${help.target.title}"`;
5614
6092
  return `${sent.delivered ? `Sent to ${who}.` : `Sent to ${who}; that room is paused, so the request waits until it resumes.`} Help id: ${help.id}. The reply reaches you on its own — end your turn now; do not poll.`;
5615
6093
  }
@@ -5993,7 +6471,7 @@ let RoomService = (() => {
5993
6471
  * @param timeoutMs - how long the call waits before it gives up.
5994
6472
  * @returns the answers, or undefined when nobody answered.
5995
6473
  */
5996
- async askQuestions(session, member, questions, timeoutMs) {
6474
+ async askQuestions(session, member, questions, timeoutMs, consent = false) {
5997
6475
  const id = randomUUID();
5998
6476
  const answered = new Promise((resolve) => {
5999
6477
  this.questionWaiters.set(id, resolve);
@@ -6001,11 +6479,17 @@ let RoomService = (() => {
6001
6479
  resolve(void 0);
6002
6480
  }, timeoutMs).unref?.();
6003
6481
  });
6482
+ const run = replay(session.snapshotEvents()).runs.find((entry) => entry.member === member.name && entry.state === "running");
6483
+ const child = member.childSessionId === void 0 ? void 0 : this.ctx.sessions.get(member.childSessionId);
6484
+ const before = child === void 0 ? "" : roundTextSoFar(child.snapshotEvents()).slice(-160);
6004
6485
  session.append("room/question", {
6005
6486
  id,
6006
6487
  member: member.name,
6007
6488
  memberId: memberId(session.snapshotEvents(), member),
6008
- questions
6489
+ questions,
6490
+ ...consent ? { consent: true } : {},
6491
+ ...run?.runId === void 0 ? {} : { runId: run.runId },
6492
+ ...before.trim() === "" ? {} : { before }
6009
6493
  });
6010
6494
  await this.ctx.sessions.flush(session);
6011
6495
  const answers = await answered;
@@ -6032,10 +6516,7 @@ let RoomService = (() => {
6032
6516
  };
6033
6517
  const waiter = this.questionWaiters.get(request.id);
6034
6518
  const pending = loaded.state.questions?.find((entry) => entry.id === request.id);
6035
- if (waiter === void 0 || pending === void 0) return {
6036
- ok: false,
6037
- error: { code: "question-not-pending" }
6038
- };
6519
+ if (waiter === void 0 || pending === void 0) return this.answerExpiredQuestion(loaded.session, request);
6039
6520
  const answers = pending.questions.map((question) => {
6040
6521
  const given = request.answers.find((answer) => answer.id === question.id);
6041
6522
  const custom = given?.custom?.trim();
@@ -6054,6 +6535,61 @@ let RoomService = (() => {
6054
6535
  waiter(answers);
6055
6536
  return { ok: true };
6056
6537
  }
6538
+ /**
6539
+ * Answer a card whose waiting call is gone (it timed out, or a restart
6540
+ * ended the member's process): the card records the answers, and they
6541
+ * reach the member as a new message that wakes it with its own context —
6542
+ * the member asked so it could go on, so a late answer is still worth it.
6543
+ * A consent gate is never answered late: its call already decided.
6544
+ * @param session - the live room session.
6545
+ * @param request - card id and the human's answers.
6546
+ * @returns ok, or `question-not-pending` when the card is not an expired, unanswered question.
6547
+ */
6548
+ async answerExpiredQuestion(session, request) {
6549
+ const events = session.snapshotEvents();
6550
+ const asked = events.find((event) => event.type === "room/question" && event.data.id === request.id);
6551
+ if (asked?.type !== "room/question" || asked.data.consent === true) return {
6552
+ ok: false,
6553
+ error: { code: "question-not-pending" }
6554
+ };
6555
+ const last = events.filter((event) => event.type === "room/question-resolved" && event.data.id === request.id).at(-1);
6556
+ if (last !== void 0 && (last.type !== "room/question-resolved" || last.data.outcome !== "cancelled")) return {
6557
+ ok: false,
6558
+ error: { code: "question-not-pending" }
6559
+ };
6560
+ const member = replay(events).members.find((entry) => memberId(events, entry) === asked.data.memberId);
6561
+ if (member === void 0) return {
6562
+ ok: false,
6563
+ error: { code: "question-not-pending" }
6564
+ };
6565
+ const answers = asked.data.questions.map((question) => {
6566
+ const given = request.answers.find((answer) => answer.id === question.id);
6567
+ const custom = given?.custom?.trim();
6568
+ return {
6569
+ id: question.id,
6570
+ selected: [...given?.selected ?? []].filter((label) => typeof label === "string"),
6571
+ ...custom === void 0 || custom === "" ? {} : { custom }
6572
+ };
6573
+ });
6574
+ session.append("room/question-resolved", {
6575
+ id: request.id,
6576
+ outcome: "answered",
6577
+ answers,
6578
+ late: true
6579
+ });
6580
+ const text = lateAnswerText(asked.data.questions, answers);
6581
+ const carry = this.moveOnFromLostRounds(session, [member.name]);
6582
+ const dispatch = session.append("room/dispatch", {
6583
+ id: randomUUID(),
6584
+ targets: [member.name],
6585
+ targetIds: [memberId(events, member)],
6586
+ text,
6587
+ origin: "human"
6588
+ });
6589
+ await this.ctx.sessions.flush(session);
6590
+ this.engine.dispatch(session, member.name, carry(member.name, text), { dispatchSeq: dispatch.seq });
6591
+ return { ok: true };
6592
+ }
6057
6593
  /** Shared backend for external MCP room tools. Not exposed as a browser Remote. */
6058
6594
  async receiveMemberCommand(actor, command) {
6059
6595
  try {
@@ -6619,4 +7155,4 @@ let RoomService = (() => {
6619
7155
  };
6620
7156
  })();
6621
7157
  //#endregion
6622
- export { MAIN_AGENT_MEMBER, ROOM_EVENT_TYPES, RoomService, RoomService as default, handoffText, parseQuestions };
7158
+ export { MAIN_AGENT_MEMBER, ROOM_EVENT_TYPES, RoomService, RoomService as default, handoffText, lateAnswerText, parseQuestions, withAttachmentPaths };