@khorsheed/dsh-room 0.2.4 → 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 +13 -0
  2. package/lib/client.js +2256 -1074
  3. package/lib/index.js +548 -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 +50 -0
  35. package/lib/types/dispatch.js +115 -1
  36. package/lib/types/index.d.ts +69 -1
  37. package/lib/types/index.js +362 -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.
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.
1955
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,
@@ -2670,6 +2784,7 @@ var DispatchEngine = class {
2670
2784
  ...lastAssistantMessageId(agent.session, before) === void 0 ? {} : { messageId: lastAssistantMessageId(agent.session, before) },
2671
2785
  ...reportedTo === void 0 ? {} : { reportedTo }
2672
2786
  });
2787
+ floatRunningCards(room, replay(room.snapshotEvents()).members.find((entry) => entry.id === member.id)?.name ?? member.name);
2673
2788
  await this.ctx.sessions.flush(room);
2674
2789
  return {
2675
2790
  state: "done",
@@ -2771,16 +2886,22 @@ var DispatchEngine = class {
2771
2886
  const interrupted = result.stopReason === "completed" ? void 0 : result.stopReason === "aborted" ? "cancelled" : "failed";
2772
2887
  const reportedTo = dispatchSeq === void 0 ? void 0 : reportRecipient(room.snapshotEvents(), dispatchSeq)?.name;
2773
2888
  const closingMessageId = lastAssistantMessageId(this.ctx.sessions.get(run.id));
2774
- 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", {
2775
2891
  member: replay(room.snapshotEvents()).members.find((entry) => entry.id === member.id)?.name ?? member.name,
2776
2892
  text: output,
2893
+ ...asks.length === 0 ? {} : { asks },
2777
2894
  childSessionId: run.id,
2778
2895
  runId,
2779
2896
  ...closingMessageId === void 0 ? {} : { messageId: closingMessageId },
2780
2897
  durationMs: Date.now() - startedAt,
2781
2898
  ...interrupted === void 0 ? {} : { interrupted },
2782
2899
  ...reportedTo === void 0 ? {} : { reportedTo }
2783
- }) : 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
+ }
2784
2905
  if (result.stopReason === "completed") {
2785
2906
  await this.ctx.sessions.flush(room);
2786
2907
  return {
@@ -3447,6 +3568,64 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
3447
3568
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
3448
3569
  done = true;
3449
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
+ });
3450
3629
  var __rewriteRelativeImportExtension = function(path, preserveJsx) {
3451
3630
  if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
3452
3631
  return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
@@ -3518,6 +3697,44 @@ function parseQuestions(raw) {
3518
3697
  if (new Set(questions.map((question) => question.id)).size !== questions.length) return "Question ids must be unique";
3519
3698
  return questions;
3520
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
+ }
3521
3738
  function messageReceipt(receipt, kind) {
3522
3739
  if (receipt.held !== void 0) return `pending-confirm (${receipt.held === "paused" ? "房间已暂停,恢复后自动投递" : receipt.held === "hop-limit" ? "这条消息链已到跳数上限,等房间主人决定是否继续" : "房间设置为成员间消息需房间主人确认"})`;
3523
3740
  return kind === "notify" ? `sent #${receipt.messageId}(知会,不会回报)` : `sent #${receipt.messageId}(${receipt.member} 做完后结果会自动回报给你;不要 sleep 等待,也不要用 room_read 轮询)`;
@@ -3561,6 +3778,7 @@ function handoffText(_events, state, sessionId, from, to) {
3561
3778
  let RoomService = (() => {
3562
3779
  let _classSuper = TypertRemoteService;
3563
3780
  let _instanceExtraInitializers = [];
3781
+ let _pendingCards_decorators;
3564
3782
  let _isRoom_decorators;
3565
3783
  let _listProviders_decorators;
3566
3784
  let _setTaskHint_decorators;
@@ -3590,6 +3808,7 @@ let RoomService = (() => {
3590
3808
  return class RoomService extends _classSuper {
3591
3809
  static {
3592
3810
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
3811
+ _pendingCards_decorators = [Remote("pendingCards")];
3593
3812
  _isRoom_decorators = [Remote("isRoom")];
3594
3813
  _listProviders_decorators = [Remote("listProviders")];
3595
3814
  _setTaskHint_decorators = [Remote("setTaskHint")];
@@ -3616,6 +3835,17 @@ let RoomService = (() => {
3616
3835
  _setGoal_decorators = [Remote("setGoal")];
3617
3836
  _cancel_decorators = [Remote("cancel")];
3618
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);
3619
3849
  __esDecorate(this, null, _isRoom_decorators, {
3620
3850
  kind: "method",
3621
3851
  name: "isRoom",
@@ -4035,6 +4265,12 @@ let RoomService = (() => {
4035
4265
  messages: []
4036
4266
  };
4037
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
+ });
4038
4274
  const scanned = /* @__PURE__ */ new WeakSet();
4039
4275
  ctx.on("agent/pre-step", async ({ agent, step }, next) => {
4040
4276
  const decision = await next();
@@ -4162,6 +4398,84 @@ let RoomService = (() => {
4162
4398
  * @param request - session identity.
4163
4399
  * @returns true when the session carries the marker (false for unknown sessions too).
4164
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
+ }
4165
4479
  async isRoom(request) {
4166
4480
  const session = this.ctx.sessions.get(request.sessionId);
4167
4481
  if (session !== void 0) return isRoomLog(session.snapshotEvents());
@@ -4484,7 +4798,7 @@ let RoomService = (() => {
4484
4798
  };
4485
4799
  const body = parsed.targets.length > 0 ? parsed.text : text;
4486
4800
  const filed = await this.fileHumanMessage(session, edit.data.id, text, task);
4487
- this.moveOnFromLostRounds(session, targets);
4801
+ const carry = this.moveOnFromLostRounds(session, targets);
4488
4802
  const dispatch = session.append("room/dispatch", {
4489
4803
  id: requestId,
4490
4804
  targets,
@@ -4494,7 +4808,7 @@ let RoomService = (() => {
4494
4808
  targetIds: targets.map((name) => memberId(session.snapshotEvents(), current.members.find((member) => member.name === name)))
4495
4809
  });
4496
4810
  await this.ctx.sessions.flush(session);
4497
- 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 });
4498
4812
  return { ok: true };
4499
4813
  }
4500
4814
  /**
@@ -4503,10 +4817,12 @@ let RoomService = (() => {
4503
4817
  * message runs instead of queueing behind a reconciliation. Formal-goal
4504
4818
  * attempts keep their explicit reconciliation.
4505
4819
  */
4506
- moveOnFromLostRounds(session, targets, why = "你发了新消息;被重启打断的这一轮按没做完处理") {
4820
+ moveOnFromLostRounds(session, targets, why = MOVED_ON_TEXT) {
4507
4821
  const events = session.snapshotEvents();
4508
4822
  const state = replay(events);
4509
- 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();
4510
4826
  for (const row of state.deliveries ?? []) {
4511
4827
  if (row.status !== "uncertain" || row.plan?.taskId !== void 0 || !ids.has(row.memberId)) continue;
4512
4828
  session.append("room/delivery-state", {
@@ -4524,7 +4840,31 @@ let RoomService = (() => {
4524
4840
  state: "cancelled",
4525
4841
  ...this.terminalRunStateFields(session, state, run.member)
4526
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
+ }
4527
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
+ };
4528
4868
  }
4529
4869
  /**
4530
4870
  * Where a member's next send belongs: the task and delivery it is working
@@ -5250,13 +5590,15 @@ let RoomService = (() => {
5250
5590
  ok: false,
5251
5591
  error: loaded.error
5252
5592
  };
5253
- if (request.text.trim() === "") return {
5593
+ const attached = request.attachments ?? [];
5594
+ if (request.text.trim() === "" && attached.length === 0) return {
5254
5595
  ok: false,
5255
5596
  error: { code: "empty-text" }
5256
5597
  };
5257
5598
  const requestSignature = createHash("sha256").update(JSON.stringify({
5258
5599
  text: request.text.trim(),
5259
- 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}`) }
5260
5602
  })).digest("hex");
5261
5603
  if (request.requestId !== void 0) {
5262
5604
  if (request.requestId.trim() === "" || request.requestId.length > 128) return {
@@ -5276,7 +5618,7 @@ let RoomService = (() => {
5276
5618
  await this.ctx.sessions.flush(loaded.session);
5277
5619
  for (const target of original.targetIds ?? []) {
5278
5620
  const member = replay(loaded.session.snapshotEvents()).members.find((member) => memberId(loaded.session.snapshotEvents(), member) === target);
5279
- 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), {
5280
5622
  dispatchSeq: previous.seq,
5281
5623
  targetId: target
5282
5624
  });
@@ -5309,8 +5651,8 @@ let RoomService = (() => {
5309
5651
  }
5310
5652
  const said = parsed.targets.length > 0 ? parsed.text : request.text.trim();
5311
5653
  const outside = [...said.matchAll(/@\[((?:\\.|[^\\\]\n])+)\]\((dsh-session:[^)\s]+)\)/gu)];
5312
- const text = said;
5313
- const prompt = outside.length === 0 ? said : [
5654
+ const text = said === "" && attached.length > 0 ? "(见附件)" : said;
5655
+ const prompt = outside.length === 0 ? text : [
5314
5656
  said,
5315
5657
  "",
5316
5658
  `(人点名了房间外的会话:${outside.map((match) => `「${match[1].replace(/\\(.)/gu, "$1")}」`).join("、")}。用 room_help 去问:session 参数原样传那个 @[…](dsh-session:…) 引用;标签是「成员(房间)」时会交给那个成员。不要转给别的成员代问。)`
@@ -5328,16 +5670,38 @@ let RoomService = (() => {
5328
5670
  names: unknown
5329
5671
  }
5330
5672
  };
5673
+ const admitted = await this.admitAttachments(request.sessionId, attached);
5674
+ if (!admitted.ok) return {
5675
+ ok: false,
5676
+ error: admitted.error
5677
+ };
5331
5678
  const human = createUserMessage({
5332
- content: [{
5679
+ content: [...admitted.blocks, ...request.text.trim() === "" ? [] : [{
5333
5680
  type: "text",
5334
5681
  text: request.text.trim()
5335
- }],
5682
+ }]],
5336
5683
  source: { kind: "user" }
5337
5684
  });
5338
- 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);
5339
5703
  const filed = await this.fileHumanMessage(loaded.session, human.id, request.text.trim(), request.task ?? this.consumeTaskHint(request.sessionId));
5340
- this.moveOnFromLostRounds(loaded.session, targets);
5704
+ const carry = this.moveOnFromLostRounds(loaded.session, targets);
5341
5705
  const dispatch = loaded.session.append("room/dispatch", {
5342
5706
  id: request.requestId ?? randomUUID(),
5343
5707
  requestSignature,
@@ -5345,10 +5709,11 @@ let RoomService = (() => {
5345
5709
  text,
5346
5710
  origin: "human",
5347
5711
  task: filed.task,
5712
+ ...admitted.records.length === 0 ? {} : { attachments: admitted.records },
5348
5713
  targetIds: targets.map((name) => memberId(loaded.session.snapshotEvents(), loaded.state.members.find((member) => member.name === name)))
5349
5714
  });
5350
5715
  await this.ctx.sessions.flush(loaded.session);
5351
- 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 });
5352
5717
  return {
5353
5718
  ok: true,
5354
5719
  value: {
@@ -5361,6 +5726,95 @@ let RoomService = (() => {
5361
5726
  };
5362
5727
  }
5363
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
+ /**
5364
5818
  * Send one message from a member to another (host-only — the `room_message`
5365
5819
  * tool and the bridge's `member_message`/`room_message` land here; the
5366
5820
  * human's path is postMessage). Any member may message any other: the
@@ -5562,7 +6016,7 @@ let RoomService = (() => {
5562
6016
  header: "求助",
5563
6017
  question: `${asker.name} 想向会话「${target.title}」求助:${text.length > 80 ? `${text.slice(0, 80)}…` : text}`,
5564
6018
  options: [{ label: "允许" }, { label: "不允许" }]
5565
- }], 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.`;
5566
6020
  }
5567
6021
  const help = {
5568
6022
  id: randomUUID().slice(0, 8),
@@ -5607,7 +6061,7 @@ let RoomService = (() => {
5607
6061
  roomTitle: this.roomTitle(room),
5608
6062
  from: help.fromName
5609
6063
  };
5610
- this.moveOnFromLostRounds(other.session, [member.name], "另一个协作间来求助;被重启打断的这一轮按没做完处理");
6064
+ const carry = this.moveOnFromLostRounds(other.session, [member.name], "另一个协作间来求助;被重启打断的这一轮按没做完处理");
5611
6065
  const sent = sendOrHold(other.session, {
5612
6066
  to: member,
5613
6067
  text,
@@ -5633,7 +6087,7 @@ let RoomService = (() => {
5633
6087
  state: sent.delivered ? "running" : "queued"
5634
6088
  });
5635
6089
  await this.ctx.sessions.flush(room);
5636
- 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 });
5637
6091
  const who = `${member.name} in the room "${help.target.title}"`;
5638
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.`;
5639
6093
  }
@@ -6017,7 +6471,7 @@ let RoomService = (() => {
6017
6471
  * @param timeoutMs - how long the call waits before it gives up.
6018
6472
  * @returns the answers, or undefined when nobody answered.
6019
6473
  */
6020
- async askQuestions(session, member, questions, timeoutMs) {
6474
+ async askQuestions(session, member, questions, timeoutMs, consent = false) {
6021
6475
  const id = randomUUID();
6022
6476
  const answered = new Promise((resolve) => {
6023
6477
  this.questionWaiters.set(id, resolve);
@@ -6025,11 +6479,17 @@ let RoomService = (() => {
6025
6479
  resolve(void 0);
6026
6480
  }, timeoutMs).unref?.();
6027
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);
6028
6485
  session.append("room/question", {
6029
6486
  id,
6030
6487
  member: member.name,
6031
6488
  memberId: memberId(session.snapshotEvents(), member),
6032
- questions
6489
+ questions,
6490
+ ...consent ? { consent: true } : {},
6491
+ ...run?.runId === void 0 ? {} : { runId: run.runId },
6492
+ ...before.trim() === "" ? {} : { before }
6033
6493
  });
6034
6494
  await this.ctx.sessions.flush(session);
6035
6495
  const answers = await answered;
@@ -6056,10 +6516,7 @@ let RoomService = (() => {
6056
6516
  };
6057
6517
  const waiter = this.questionWaiters.get(request.id);
6058
6518
  const pending = loaded.state.questions?.find((entry) => entry.id === request.id);
6059
- if (waiter === void 0 || pending === void 0) return {
6060
- ok: false,
6061
- error: { code: "question-not-pending" }
6062
- };
6519
+ if (waiter === void 0 || pending === void 0) return this.answerExpiredQuestion(loaded.session, request);
6063
6520
  const answers = pending.questions.map((question) => {
6064
6521
  const given = request.answers.find((answer) => answer.id === question.id);
6065
6522
  const custom = given?.custom?.trim();
@@ -6078,6 +6535,61 @@ let RoomService = (() => {
6078
6535
  waiter(answers);
6079
6536
  return { ok: true };
6080
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
+ }
6081
6593
  /** Shared backend for external MCP room tools. Not exposed as a browser Remote. */
6082
6594
  async receiveMemberCommand(actor, command) {
6083
6595
  try {
@@ -6643,4 +7155,4 @@ let RoomService = (() => {
6643
7155
  };
6644
7156
  })();
6645
7157
  //#endregion
6646
- 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 };