@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.
- package/CHANGELOG.md +18 -0
- package/lib/client.js +2309 -1127
- package/lib/index.js +572 -36
- package/lib/typert.host.js +249 -65
- package/lib/typert.remote-client.d.ts +4 -1
- package/lib/typert.remote-client.js +208 -53
- package/lib/types/client/InviteDialog.js +11 -4
- package/lib/types/client/RoomAttachments.d.ts +20 -0
- package/lib/types/client/RoomAttachments.js +36 -0
- package/lib/types/client/RoomComposer.d.ts +6 -12
- package/lib/types/client/RoomComposer.js +116 -12
- package/lib/types/client/RoomQuestionView.d.ts +10 -2
- package/lib/types/client/RoomQuestionView.js +18 -5
- package/lib/types/client/RoomRunView.d.ts +1 -1
- package/lib/types/client/RoomRunView.js +15 -5
- package/lib/types/client/RoomSpeechView.d.ts +24 -1
- package/lib/types/client/RoomSpeechView.js +39 -3
- package/lib/types/client/RoomStatsLine.d.ts +3 -1
- package/lib/types/client/RoomStatsLine.js +3 -1
- package/lib/types/client/RoomTaskCapsule.d.ts +6 -1
- package/lib/types/client/RoomTaskCapsule.js +2 -2
- package/lib/types/client/TasksView.js +1 -1
- package/lib/types/client/TeamView.js +1 -1
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.js +94 -15
- package/lib/types/client/locales.d.ts +44 -0
- package/lib/types/client/locales.js +44 -0
- package/lib/types/client/nodes.d.ts +22 -3
- package/lib/types/client/nodes.js +28 -5
- package/lib/types/client/path-mentions.d.ts +34 -0
- package/lib/types/client/path-mentions.js +39 -0
- package/lib/types/client/slots.d.ts +51 -1
- package/lib/types/dispatch.d.ts +60 -0
- package/lib/types/dispatch.js +136 -1
- package/lib/types/index.d.ts +69 -1
- package/lib/types/index.js +368 -20
- package/lib/types/jobs.js +7 -3
- package/lib/types/journal.d.ts +1 -1
- package/lib/types/journal.js +18 -2
- package/lib/types/types.d.ts +77 -0
- package/package.json +10 -6
- package/lib/journal-B4FOGO6S.js +0 -482
- package/lib/journal-BWL3-i0s.js +0 -462
- package/lib/journal-Rid9AAnD.js +0 -485
package/lib/types/index.js
CHANGED
|
@@ -32,6 +32,58 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
32
32
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
33
|
done = true;
|
|
34
34
|
};
|
|
35
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
36
|
+
if (value !== null && value !== void 0) {
|
|
37
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
38
|
+
var dispose, inner;
|
|
39
|
+
if (async) {
|
|
40
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
41
|
+
dispose = value[Symbol.asyncDispose];
|
|
42
|
+
}
|
|
43
|
+
if (dispose === void 0) {
|
|
44
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
45
|
+
dispose = value[Symbol.dispose];
|
|
46
|
+
if (async) inner = dispose;
|
|
47
|
+
}
|
|
48
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
49
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
50
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
51
|
+
}
|
|
52
|
+
else if (async) {
|
|
53
|
+
env.stack.push({ async: true });
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
58
|
+
return function (env) {
|
|
59
|
+
function fail(e) {
|
|
60
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
61
|
+
env.hasError = true;
|
|
62
|
+
}
|
|
63
|
+
var r, s = 0;
|
|
64
|
+
function next() {
|
|
65
|
+
while (r = env.stack.pop()) {
|
|
66
|
+
try {
|
|
67
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
68
|
+
if (r.dispose) {
|
|
69
|
+
var result = r.dispose.call(r.value);
|
|
70
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
71
|
+
}
|
|
72
|
+
else s |= 1;
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
fail(e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
79
|
+
if (env.hasError) throw env.error;
|
|
80
|
+
}
|
|
81
|
+
return next();
|
|
82
|
+
};
|
|
83
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
84
|
+
var e = new Error(message);
|
|
85
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
86
|
+
});
|
|
35
87
|
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
36
88
|
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
37
89
|
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
@@ -87,7 +139,7 @@ import { composeRoomAgent, deriveSessionPreset, probeAgentPresets, inspectCold,
|
|
|
87
139
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
88
140
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
89
141
|
import { probeLocalAgent, probeLocalAgentRoster } from './adapter.js';
|
|
90
|
-
import { DispatchEngine, heldReportsText, isRoomSource, roomSource, runChildSessionId, runChildTurn } from './dispatch.js';
|
|
142
|
+
import { BEFORE_TAIL, DispatchEngine, MERGED_BACKLOG_TEXT, roundTextSoFar, MOVED_ON_TEXT, floatRunningCards, heldReportsText, isRoomSource, roomSource, runChildSessionId, runChildTurn } from './dispatch.js';
|
|
91
143
|
import { coordinatorMember, DEFAULT_ROOM_SETTINGS, memberId, isRoomLog, MAIN_AGENT_MEMBER, parseMentions, replay, ROOM_EVENT_TYPES, ROOM_ROLE_MAX } from './journal.js';
|
|
92
144
|
import { messageId, parseMessageRef, releaseIntent, SELF_TASK, sendOrHold, workingHop } from './messaging.js';
|
|
93
145
|
import { isHumanMessage, linkedTask, taskLinks } from './deliveries.js';
|
|
@@ -155,6 +207,43 @@ export function parseQuestions(raw) {
|
|
|
155
207
|
return 'Question ids must be unique';
|
|
156
208
|
return questions;
|
|
157
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* The message a late answer reaches its member as: each question with the
|
|
212
|
+
* human's pick, so the member can go on (or correct what it assumed).
|
|
213
|
+
* @param questions - the card's questions.
|
|
214
|
+
* @param answers - the human's answers, one per question.
|
|
215
|
+
* @returns the dispatch text.
|
|
216
|
+
*/
|
|
217
|
+
export function lateAnswerText(questions, answers) {
|
|
218
|
+
const lines = questions.map((question) => {
|
|
219
|
+
const answer = answers.find(entry => entry.id === question.id);
|
|
220
|
+
const picked = [...answer?.selected ?? [], ...answer?.custom === undefined ? [] : [answer.custom]].join(';');
|
|
221
|
+
return `- ${question.header === undefined ? '' : `${question.header}:`}${question.question} → ${picked === '' ? '(没选)' : picked}`;
|
|
222
|
+
});
|
|
223
|
+
return [
|
|
224
|
+
'你之前用 ask_user_question 问的问题没等到回答就过期了,人现在补答了:',
|
|
225
|
+
...lines,
|
|
226
|
+
'',
|
|
227
|
+
'按这些答案继续。如果你已经按自己的假设往下做了,对照答案调整,并说一下改了什么。',
|
|
228
|
+
].join('\n');
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The dispatch text with the message's attachments appended as local paths:
|
|
232
|
+
* every member — CLI or native — opens them with its own read tool, so one
|
|
233
|
+
* form reaches all of them.
|
|
234
|
+
* @param text - the dispatched text.
|
|
235
|
+
* @param attachments - the admitted attachments.
|
|
236
|
+
* @returns the text members receive.
|
|
237
|
+
*/
|
|
238
|
+
export function withAttachmentPaths(text, attachments) {
|
|
239
|
+
if (attachments === undefined || attachments.length === 0)
|
|
240
|
+
return text;
|
|
241
|
+
const lines = attachments.map((entry) => {
|
|
242
|
+
const label = `${entry.kind === 'image' ? '图片' : '文件'}${entry.name === undefined ? '' : ` ${entry.name}`}`;
|
|
243
|
+
return `- ${label}:${entry.path ?? '(宿主没有给出本地路径,打不开就告诉人)'}`;
|
|
244
|
+
});
|
|
245
|
+
return [text, '', '人随消息附了这些附件,用你自己的读取工具打开(图片用能看图的读取工具):', ...lines].join('\n');
|
|
246
|
+
}
|
|
158
247
|
function messageReceipt(receipt, kind) {
|
|
159
248
|
if (receipt.held !== undefined) {
|
|
160
249
|
const why = receipt.held === 'paused' ? '房间已暂停,恢复后自动投递'
|
|
@@ -205,6 +294,7 @@ export function handoffText(_events, state, sessionId, from, to) {
|
|
|
205
294
|
let RoomService = (() => {
|
|
206
295
|
let _classSuper = TypertRemoteService;
|
|
207
296
|
let _instanceExtraInitializers = [];
|
|
297
|
+
let _pendingCards_decorators;
|
|
208
298
|
let _isRoom_decorators;
|
|
209
299
|
let _listProviders_decorators;
|
|
210
300
|
let _setTaskHint_decorators;
|
|
@@ -234,6 +324,7 @@ let RoomService = (() => {
|
|
|
234
324
|
return class RoomService extends _classSuper {
|
|
235
325
|
static {
|
|
236
326
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
327
|
+
_pendingCards_decorators = [Remote('pendingCards')];
|
|
237
328
|
_isRoom_decorators = [Remote('isRoom')];
|
|
238
329
|
_listProviders_decorators = [Remote('listProviders')];
|
|
239
330
|
_setTaskHint_decorators = [Remote('setTaskHint')];
|
|
@@ -260,6 +351,7 @@ let RoomService = (() => {
|
|
|
260
351
|
_setGoal_decorators = [Remote('setGoal')];
|
|
261
352
|
_cancel_decorators = [Remote('cancel')];
|
|
262
353
|
_cancelChild_decorators = [Remote('cancelChild')];
|
|
354
|
+
__esDecorate(this, null, _pendingCards_decorators, { kind: "method", name: "pendingCards", static: false, private: false, access: { has: obj => "pendingCards" in obj, get: obj => obj.pendingCards }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
263
355
|
__esDecorate(this, null, _isRoom_decorators, { kind: "method", name: "isRoom", static: false, private: false, access: { has: obj => "isRoom" in obj, get: obj => obj.isRoom }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
264
356
|
__esDecorate(this, null, _listProviders_decorators, { kind: "method", name: "listProviders", static: false, private: false, access: { has: obj => "listProviders" in obj, get: obj => obj.listProviders }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
265
357
|
__esDecorate(this, null, _setTaskHint_decorators, { kind: "method", name: "setTaskHint", static: false, private: false, access: { has: obj => "setTaskHint" in obj, get: obj => obj.setTaskHint }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -421,6 +513,15 @@ let RoomService = (() => {
|
|
|
421
513
|
}
|
|
422
514
|
return { kind: 'enter', messages: [] };
|
|
423
515
|
});
|
|
516
|
+
// The session list's status (the host shows a session running only while
|
|
517
|
+
// its own agent runs): a room is running while any member runs, and its
|
|
518
|
+
// waiting question/approval cards are tracked for the browser.
|
|
519
|
+
ctx.on('session/event', (session, event) => { this.observeActivity(session, event); });
|
|
520
|
+
ctx.on('agent/status', ({ agent, status }) => {
|
|
521
|
+
// The room's own agent settling must not hide members still at work.
|
|
522
|
+
if (status !== 'running' && (this.activeRuns.get(String(agent.id))?.size ?? 0) > 0)
|
|
523
|
+
this.emitStatus(String(agent.id), true);
|
|
524
|
+
});
|
|
424
525
|
// Cross-session help (help.ts): a help message reaching a helper's turn
|
|
425
526
|
// equips that agent with the helper tools and marks the thread running;
|
|
426
527
|
// an agent whose log already holds help requests (a restart dropped its
|
|
@@ -541,6 +642,95 @@ let RoomService = (() => {
|
|
|
541
642
|
* @param request - session identity.
|
|
542
643
|
* @returns true when the session carries the marker (false for unknown sessions too).
|
|
543
644
|
*/
|
|
645
|
+
/** Run ids of the members working in each room (the session list's 运行中). */
|
|
646
|
+
activeRuns = new Map();
|
|
647
|
+
/** Question/approval cards waiting for the human, per room. */
|
|
648
|
+
waitingCards = new Map();
|
|
649
|
+
/**
|
|
650
|
+
* Follow a room's run and card edges as they are journaled: a room turns
|
|
651
|
+
* running when its first member starts and settles when its last one ends
|
|
652
|
+
* (the host's `api-session/status`, so the list shows 运行中 and then
|
|
653
|
+
* 已完成), and its waiting cards are kept for {@link pendingCards}.
|
|
654
|
+
* @param session - the session the event landed in.
|
|
655
|
+
* @param event - the journaled event.
|
|
656
|
+
*/
|
|
657
|
+
observeActivity(session, event) {
|
|
658
|
+
const id = String(session.id);
|
|
659
|
+
switch (event.type) {
|
|
660
|
+
case 'room/run-state': {
|
|
661
|
+
const runs = this.activeRuns.get(id) ?? new Set();
|
|
662
|
+
const before = runs.size;
|
|
663
|
+
const key = event.data.runId ?? `${event.data.member}:${event.data.startedAt}`;
|
|
664
|
+
if (event.data.state === 'running')
|
|
665
|
+
runs.add(key);
|
|
666
|
+
else
|
|
667
|
+
runs.delete(key);
|
|
668
|
+
if (runs.size === 0)
|
|
669
|
+
this.activeRuns.delete(id);
|
|
670
|
+
else
|
|
671
|
+
this.activeRuns.set(id, runs);
|
|
672
|
+
// A member whose round ended is no longer waiting on its cards: its
|
|
673
|
+
// bridge call is gone (a CLI's own MCP timeout can end it early, and
|
|
674
|
+
// the round then goes on without the answer). Expire them, so an
|
|
675
|
+
// answer given now reaches the member as a new message.
|
|
676
|
+
if (event.data.state !== 'running')
|
|
677
|
+
this.expireCardsOf(id, event.data.member);
|
|
678
|
+
if (before === 0 && runs.size > 0)
|
|
679
|
+
this.emitStatus(id, true);
|
|
680
|
+
// The room's own agent still running keeps the row running.
|
|
681
|
+
else if (before > 0 && runs.size === 0 && this.ctx.agents.get(session.id)?.status !== 'running')
|
|
682
|
+
this.emitStatus(id, false);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
case 'room/question':
|
|
686
|
+
case 'room/approval': {
|
|
687
|
+
const cards = this.waitingCards.get(id) ?? new Map();
|
|
688
|
+
cards.set(event.data.id, { kind: event.type === 'room/question' ? 'question' : 'approval', member: event.data.member });
|
|
689
|
+
this.waitingCards.set(id, cards);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
case 'room/question-resolved':
|
|
693
|
+
case 'room/approval-resolved': {
|
|
694
|
+
const cards = this.waitingCards.get(id);
|
|
695
|
+
cards?.delete(event.data.id);
|
|
696
|
+
if (cards?.size === 0)
|
|
697
|
+
this.waitingCards.delete(id);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
default:
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* End the waits behind one member's open cards (its round settled): the
|
|
705
|
+
* question/approval calls resolve as unanswered, which journals them
|
|
706
|
+
* expired — still answerable, by a new message (answerExpiredQuestion).
|
|
707
|
+
* @param roomId - the room session.
|
|
708
|
+
* @param member - the member whose round ended.
|
|
709
|
+
*/
|
|
710
|
+
expireCardsOf(roomId, member) {
|
|
711
|
+
for (const [cardId, card] of this.waitingCards.get(roomId) ?? []) {
|
|
712
|
+
if (card.member !== member)
|
|
713
|
+
continue;
|
|
714
|
+
if (card.kind === 'question')
|
|
715
|
+
this.questionWaiters.get(cardId)?.(undefined);
|
|
716
|
+
else
|
|
717
|
+
this.approvalWaiters.get(cardId)?.('cancelled');
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/** Broadcast a room's running flip on the host's session-status channel. */
|
|
721
|
+
emitStatus(sessionId, running) {
|
|
722
|
+
this.ctx.emit('api-session/status', SessionId(sessionId), running);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* The question/approval cards waiting for the human across live rooms —
|
|
726
|
+
* the browser publishes them as the session list's 等你处理 status.
|
|
727
|
+
* @param request - optionally one room only.
|
|
728
|
+
* @returns the waiting cards.
|
|
729
|
+
*/
|
|
730
|
+
async pendingCards(request) {
|
|
731
|
+
const rooms = request.sessionId === undefined ? [...this.waitingCards.keys()] : [String(request.sessionId)];
|
|
732
|
+
return rooms.flatMap(sessionId => [...this.waitingCards.get(sessionId) ?? []].map(([id, card]) => ({ sessionId, id, kind: card.kind })));
|
|
733
|
+
}
|
|
544
734
|
async isRoom(request) {
|
|
545
735
|
const session = this.ctx.sessions.get(request.sessionId);
|
|
546
736
|
if (session !== undefined)
|
|
@@ -800,14 +990,14 @@ let RoomService = (() => {
|
|
|
800
990
|
return { ok: false, code: 'unknown-targets' };
|
|
801
991
|
const body = parsed.targets.length > 0 ? parsed.text : text;
|
|
802
992
|
const filed = await this.fileHumanMessage(session, edit.data.id, text, task);
|
|
803
|
-
this.moveOnFromLostRounds(session, targets);
|
|
993
|
+
const carry = this.moveOnFromLostRounds(session, targets);
|
|
804
994
|
const dispatch = session.append('room/dispatch', {
|
|
805
995
|
id: requestId, targets, text: body, origin: 'human', task: filed.task,
|
|
806
996
|
targetIds: targets.map(name => memberId(session.snapshotEvents(), current.members.find(member => member.name === name))),
|
|
807
997
|
});
|
|
808
998
|
await this.ctx.sessions.flush(session);
|
|
809
999
|
for (const target of targets)
|
|
810
|
-
this.engine.dispatch(session, target, body, { dispatchSeq: dispatch.seq });
|
|
1000
|
+
this.engine.dispatch(session, target, carry(target, body), { dispatchSeq: dispatch.seq });
|
|
811
1001
|
return { ok: true };
|
|
812
1002
|
}
|
|
813
1003
|
/**
|
|
@@ -816,10 +1006,12 @@ let RoomService = (() => {
|
|
|
816
1006
|
* message runs instead of queueing behind a reconciliation. Formal-goal
|
|
817
1007
|
* attempts keep their explicit reconciliation.
|
|
818
1008
|
*/
|
|
819
|
-
moveOnFromLostRounds(session, targets, why =
|
|
1009
|
+
moveOnFromLostRounds(session, targets, why = MOVED_ON_TEXT) {
|
|
820
1010
|
const events = session.snapshotEvents();
|
|
821
1011
|
const state = replay(events);
|
|
822
|
-
const
|
|
1012
|
+
const members = state.members.filter(member => targets.includes(member.name));
|
|
1013
|
+
const ids = new Set(members.map(member => memberId(events, member)));
|
|
1014
|
+
const backlog = new Map();
|
|
823
1015
|
for (const row of state.deliveries ?? []) {
|
|
824
1016
|
if (row.status !== 'uncertain' || row.plan?.taskId !== undefined || !ids.has(row.memberId))
|
|
825
1017
|
continue;
|
|
@@ -827,7 +1019,24 @@ let RoomService = (() => {
|
|
|
827
1019
|
const run = state.runs.find(entry => entry.runId === row.id);
|
|
828
1020
|
if (run !== undefined)
|
|
829
1021
|
session.append('room/run-state', { member: run.member, runId: row.id, startedAt: run.startedAt, state: 'cancelled', ...this.terminalRunStateFields(session, state, run.member) });
|
|
1022
|
+
// The messages that queued behind the lost round never ran (the round
|
|
1023
|
+
// blocked them, the restart lost the queue): they ride along with this
|
|
1024
|
+
// message, one round, instead of replaying stale rounds one by one.
|
|
1025
|
+
const name = members.find(member => memberId(events, member) === row.memberId)?.name;
|
|
1026
|
+
if (name === undefined)
|
|
1027
|
+
continue;
|
|
1028
|
+
for (const queued of state.deliveries ?? []) {
|
|
1029
|
+
if (queued.status !== 'queued' || queued.memberId !== row.memberId || queued.dispatchSeq <= row.dispatchSeq || queued.plan?.taskId !== undefined)
|
|
1030
|
+
continue;
|
|
1031
|
+
session.append('room/delivery-state', { id: queued.id, dispatchSeq: queued.dispatchSeq, memberId: queued.memberId, state: 'done', text: MERGED_BACKLOG_TEXT });
|
|
1032
|
+
const from = queued.from === undefined ? undefined : state.members.find(member => memberId(events, member) === queued.from)?.name;
|
|
1033
|
+
backlog.set(name, [...backlog.get(name) ?? [], `- ${from === undefined ? '' : `来自 ${from}:`}${queued.text}`]);
|
|
1034
|
+
}
|
|
830
1035
|
}
|
|
1036
|
+
return (member, text) => {
|
|
1037
|
+
const lines = backlog.get(member);
|
|
1038
|
+
return lines === undefined ? text : [text, '', '(你被重启打断期间,房间里还有这些消息排着没送到,一并给你:', ...lines, ')'].join('\n');
|
|
1039
|
+
};
|
|
831
1040
|
}
|
|
832
1041
|
/**
|
|
833
1042
|
* Where a member's next send belongs: the task and delivery it is working
|
|
@@ -891,6 +1100,12 @@ let RoomService = (() => {
|
|
|
891
1100
|
const loaded = await this.loadCold(request.sessionId);
|
|
892
1101
|
if (!loaded.ok)
|
|
893
1102
|
return { ok: false, error: loaded.error };
|
|
1103
|
+
// A held report's wake timer lives in the process that held it: after a
|
|
1104
|
+
// restart nothing would ever hand it over. A cold room holding one is
|
|
1105
|
+
// brought live, whose recovery hands every untimed hold over.
|
|
1106
|
+
if (this.ctx.sessions.get(request.sessionId) === undefined && (loaded.state.deliveries ?? []).some(row => row.status === 'held')) {
|
|
1107
|
+
void this.ensureLive(request.sessionId).catch((error) => { this.ctx.logger.warn(`room: reviving held reports failed: ${String(error)}`); });
|
|
1108
|
+
}
|
|
894
1109
|
return { ok: true, value: { ...this.engine.view(request.sessionId, loaded.state), ...loaded.state.plan === undefined ? {} : { plan: this.plans.view(request.sessionId, loaded.state.plan) } } };
|
|
895
1110
|
}
|
|
896
1111
|
/**
|
|
@@ -1358,9 +1573,13 @@ let RoomService = (() => {
|
|
|
1358
1573
|
const loaded = await this.ensureLive(request.sessionId);
|
|
1359
1574
|
if (!loaded.ok)
|
|
1360
1575
|
return { ok: false, error: loaded.error };
|
|
1361
|
-
|
|
1576
|
+
const attached = request.attachments ?? [];
|
|
1577
|
+
if (request.text.trim() === '' && attached.length === 0)
|
|
1362
1578
|
return { ok: false, error: { code: 'empty-text' } };
|
|
1363
|
-
const requestSignature = createHash('sha256').update(JSON.stringify({
|
|
1579
|
+
const requestSignature = createHash('sha256').update(JSON.stringify({
|
|
1580
|
+
text: request.text.trim(), targets: [...new Set(request.targets ?? [])].sort(),
|
|
1581
|
+
...attached.length === 0 ? {} : { attachments: attached.map(entry => entry.type === 'file' ? entry.receiptId : `${entry.mediaType}:${entry.data.length}`) },
|
|
1582
|
+
})).digest('hex');
|
|
1364
1583
|
if (request.requestId !== undefined) {
|
|
1365
1584
|
if (request.requestId.trim() === '' || request.requestId.length > 128)
|
|
1366
1585
|
return { ok: false, error: { code: 'request-conflict' } };
|
|
@@ -1378,7 +1597,7 @@ let RoomService = (() => {
|
|
|
1378
1597
|
for (const target of original.targetIds ?? []) {
|
|
1379
1598
|
const member = replay(loaded.session.snapshotEvents()).members.find(member => memberId(loaded.session.snapshotEvents(), member) === target);
|
|
1380
1599
|
if (member !== undefined)
|
|
1381
|
-
this.engine.dispatch(loaded.session, member.name, original.text, { dispatchSeq: previous.seq, targetId: target });
|
|
1600
|
+
this.engine.dispatch(loaded.session, member.name, withAttachmentPaths(original.text, original.attachments), { dispatchSeq: previous.seq, targetId: target });
|
|
1382
1601
|
}
|
|
1383
1602
|
return { ok: true, value: { parsed: { targets: original.targets, text: original.text }, seq: previous.seq } };
|
|
1384
1603
|
}
|
|
@@ -1401,9 +1620,10 @@ let RoomService = (() => {
|
|
|
1401
1620
|
// The human pointing at a session outside the room is a help request: say
|
|
1402
1621
|
// so every time (a member's briefing reaches it once, on its first round).
|
|
1403
1622
|
const outside = [...said.matchAll(/@\[((?:\\.|[^\\\]\n])+)\]\((dsh-session:[^)\s]+)\)/gu)];
|
|
1404
|
-
|
|
1623
|
+
// An attachment-only message still dispatches: its words are the attachments.
|
|
1624
|
+
const text = said === '' && attached.length > 0 ? '(见附件)' : said;
|
|
1405
1625
|
// The hint rides the member's prompt only; the journal keeps the human's words.
|
|
1406
|
-
const prompt = outside.length === 0 ?
|
|
1626
|
+
const prompt = outside.length === 0 ? text : [
|
|
1407
1627
|
said,
|
|
1408
1628
|
'',
|
|
1409
1629
|
`(人点名了房间外的会话:${outside.map(match => `「${match[1].replace(/\\(.)/gu, '$1')}」`).join('、')}。用 room_help 去问:session 参数原样传那个 @[…](dsh-session:…) 引用;标签是「成员(房间)」时会交给那个成员。不要转给别的成员代问。)`,
|
|
@@ -1414,23 +1634,103 @@ let RoomService = (() => {
|
|
|
1414
1634
|
const unknown = targets.filter(target => !roster.has(target));
|
|
1415
1635
|
if (unknown.length > 0)
|
|
1416
1636
|
return { ok: false, error: { code: 'unknown-targets', names: unknown } };
|
|
1637
|
+
const admitted = await this.admitAttachments(request.sessionId, attached);
|
|
1638
|
+
if (!admitted.ok)
|
|
1639
|
+
return { ok: false, error: admitted.error };
|
|
1417
1640
|
const human = createUserMessage({
|
|
1418
|
-
content: [{ type: 'text', text: request.text.trim() }],
|
|
1641
|
+
content: [...admitted.blocks, ...request.text.trim() === '' ? [] : [{ type: 'text', text: request.text.trim() }]],
|
|
1419
1642
|
source: { kind: 'user' },
|
|
1420
1643
|
});
|
|
1421
|
-
|
|
1644
|
+
{
|
|
1645
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
1646
|
+
try {
|
|
1647
|
+
// Uploaded files are bound to this message once it is in the log (an unbound receipt is reclaimed).
|
|
1648
|
+
const binding = __addDisposableResource(env_1, admitted.bind(request.requestId ?? String(human.id)), false);
|
|
1649
|
+
loaded.session.append('user/message', human, { surfaceOp: 'append' });
|
|
1650
|
+
binding?.commit();
|
|
1651
|
+
}
|
|
1652
|
+
catch (e_1) {
|
|
1653
|
+
env_1.error = e_1;
|
|
1654
|
+
env_1.hasError = true;
|
|
1655
|
+
}
|
|
1656
|
+
finally {
|
|
1657
|
+
__disposeResources(env_1);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
floatRunningCards(loaded.session);
|
|
1422
1661
|
const filed = await this.fileHumanMessage(loaded.session, human.id, request.text.trim(), request.task ?? this.consumeTaskHint(request.sessionId));
|
|
1423
|
-
this.moveOnFromLostRounds(loaded.session, targets);
|
|
1662
|
+
const carry = this.moveOnFromLostRounds(loaded.session, targets);
|
|
1424
1663
|
const dispatch = loaded.session.append('room/dispatch', {
|
|
1425
1664
|
id: request.requestId ?? randomUUID(), requestSignature, targets, text, origin: 'human', task: filed.task,
|
|
1665
|
+
...admitted.records.length === 0 ? {} : { attachments: admitted.records },
|
|
1426
1666
|
targetIds: targets.map(name => memberId(loaded.session.snapshotEvents(), loaded.state.members.find(member => member.name === name))),
|
|
1427
1667
|
});
|
|
1428
1668
|
await this.ctx.sessions.flush(loaded.session);
|
|
1429
1669
|
for (const target of targets) {
|
|
1430
|
-
this.engine.dispatch(loaded.session, target, prompt, { dispatchSeq: dispatch.seq });
|
|
1670
|
+
this.engine.dispatch(loaded.session, target, carry(target, withAttachmentPaths(prompt, admitted.records)), { dispatchSeq: dispatch.seq });
|
|
1431
1671
|
}
|
|
1432
1672
|
return { ok: true, value: { parsed: { targets, text }, seq: dispatch.seq } };
|
|
1433
1673
|
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Admit a human message's composer attachments the way the host's own
|
|
1676
|
+
* prompt path does: file receipts resolve against this room session's
|
|
1677
|
+
* uploads, images are stored, and every attachment becomes a durable
|
|
1678
|
+
* content block for the message — plus the local path members read it by.
|
|
1679
|
+
* @param sessionId - the room session (its agent owns the uploads).
|
|
1680
|
+
* @param inputs - the wire attachments.
|
|
1681
|
+
* @returns the content blocks, the dispatch records, and the receipt binder; or a refusal.
|
|
1682
|
+
*/
|
|
1683
|
+
async admitAttachments(sessionId, inputs) {
|
|
1684
|
+
if (inputs.length === 0)
|
|
1685
|
+
return { ok: true, blocks: [], records: [], bind: () => undefined };
|
|
1686
|
+
const probe = this.ctx.get;
|
|
1687
|
+
const services = {
|
|
1688
|
+
attachments: probe('attachments'),
|
|
1689
|
+
fileUploads: probe('fileUploads'),
|
|
1690
|
+
};
|
|
1691
|
+
const store = services.attachments;
|
|
1692
|
+
// The uploads are staged on the room session's agent; a cold room wakes it.
|
|
1693
|
+
let agent = this.ctx.agents.get(sessionId);
|
|
1694
|
+
if (agent === undefined) {
|
|
1695
|
+
const controller = this.ctx.get('sessionController');
|
|
1696
|
+
const resolved = await controller?.resolveAgent?.(sessionId).catch(() => undefined);
|
|
1697
|
+
if (resolved !== undefined && 'agent' in resolved)
|
|
1698
|
+
agent = resolved.agent;
|
|
1699
|
+
}
|
|
1700
|
+
if (store === undefined || agent === undefined)
|
|
1701
|
+
return { ok: false, error: { code: 'attachment-invalid', message: 'attachments are unavailable on this host' } };
|
|
1702
|
+
const receipts = [];
|
|
1703
|
+
const parts = [];
|
|
1704
|
+
for (const input of inputs) {
|
|
1705
|
+
if (input.type === 'image') {
|
|
1706
|
+
parts.push({ type: 'image', data: input.data, mediaType: input.mediaType, ...input.name === undefined ? {} : { name: input.name } });
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
const ref = services.fileUploads?.resolve(agent, input.receiptId);
|
|
1710
|
+
if (ref === undefined)
|
|
1711
|
+
return { ok: false, error: { code: 'attachment-invalid', message: 'a file was not uploaded to this room' } };
|
|
1712
|
+
receipts.push(input.receiptId);
|
|
1713
|
+
parts.push({ type: 'file', attachment: ref });
|
|
1714
|
+
}
|
|
1715
|
+
let blocks;
|
|
1716
|
+
try {
|
|
1717
|
+
blocks = await store.admitPromptContent(parts);
|
|
1718
|
+
}
|
|
1719
|
+
catch (error) {
|
|
1720
|
+
return { ok: false, error: { code: 'attachment-invalid', message: error instanceof Error ? error.message : String(error) } };
|
|
1721
|
+
}
|
|
1722
|
+
const records = blocks.flatMap((block) => {
|
|
1723
|
+
if (block.type !== 'image' && block.type !== 'file')
|
|
1724
|
+
return [];
|
|
1725
|
+
const path = block.type === 'image' ? store.imageHostPath?.(block.attachment) : store.fileHostPath?.(block.attachment);
|
|
1726
|
+
return [{ kind: block.type, ...block.attachment?.name === undefined ? {} : { name: block.attachment.name }, ...path === undefined ? {} : { path } }];
|
|
1727
|
+
});
|
|
1728
|
+
const uploads = services.fileUploads;
|
|
1729
|
+
return {
|
|
1730
|
+
ok: true, blocks, records,
|
|
1731
|
+
bind: requestId => receipts.length === 0 || uploads === undefined ? undefined : uploads.bindPrompt(agent, receipts, requestId),
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1434
1734
|
/**
|
|
1435
1735
|
* Send one message from a member to another (host-only — the `room_message`
|
|
1436
1736
|
* tool and the bridge's `member_message`/`room_message` land here; the
|
|
@@ -1634,7 +1934,7 @@ let RoomService = (() => {
|
|
|
1634
1934
|
id: 'help', header: '求助',
|
|
1635
1935
|
question: `${asker.name} 想向会话「${target.title}」求助:${text.length > 80 ? `${text.slice(0, 80)}…` : text}`,
|
|
1636
1936
|
options: [{ label: '允许' }, { label: '不允许' }],
|
|
1637
|
-
}], 10 * 60_000);
|
|
1937
|
+
}], 10 * 60_000, true);
|
|
1638
1938
|
if (answers?.[0]?.selected[0] !== '允许')
|
|
1639
1939
|
return `The human did not allow asking "${target.title}". Do not retry unless they @ that session in the room.`;
|
|
1640
1940
|
}
|
|
@@ -1673,7 +1973,7 @@ let RoomService = (() => {
|
|
|
1673
1973
|
// A round a restart left uncertain blocks the member's later work until a
|
|
1674
1974
|
// human reconciles it — silently, for a request from outside. Move on
|
|
1675
1975
|
// from it, as a human message to that member does.
|
|
1676
|
-
this.moveOnFromLostRounds(other.session, [member.name], '另一个协作间来求助;被重启打断的这一轮按没做完处理');
|
|
1976
|
+
const carry = this.moveOnFromLostRounds(other.session, [member.name], '另一个协作间来求助;被重启打断的这一轮按没做完处理');
|
|
1677
1977
|
const sent = sendOrHold(other.session, {
|
|
1678
1978
|
to: member, text, fromName: `${help.fromName}(${origin.roomTitle})`,
|
|
1679
1979
|
intent: { origin: 'help', hop: 1, kind: 'task', help: origin, ...op === 'request' ? { task: SELF_TASK } : {} },
|
|
@@ -1683,7 +1983,7 @@ let RoomService = (() => {
|
|
|
1683
1983
|
room.append('room/help-state', { help: help.id, state: sent.delivered ? 'running' : 'queued' });
|
|
1684
1984
|
await this.ctx.sessions.flush(room);
|
|
1685
1985
|
if (sent.delivered)
|
|
1686
|
-
this.engine.dispatch(other.session, member.name, text, { dispatchSeq: sent.seq });
|
|
1986
|
+
this.engine.dispatch(other.session, member.name, carry(member.name, text), { dispatchSeq: sent.seq });
|
|
1687
1987
|
const who = `${member.name} in the room "${help.target.title}"`;
|
|
1688
1988
|
return `${sent.delivered ? `Sent to ${who}.` : `Sent to ${who}; that room is paused, so the request waits until it resumes.`}`
|
|
1689
1989
|
+ ` Help id: ${help.id}. The reply reaches you on its own — end your turn now; do not poll.`;
|
|
@@ -2003,13 +2303,23 @@ let RoomService = (() => {
|
|
|
2003
2303
|
* @param timeoutMs - how long the call waits before it gives up.
|
|
2004
2304
|
* @returns the answers, or undefined when nobody answered.
|
|
2005
2305
|
*/
|
|
2006
|
-
async askQuestions(session, member, questions, timeoutMs) {
|
|
2306
|
+
async askQuestions(session, member, questions, timeoutMs, consent = false) {
|
|
2007
2307
|
const id = randomUUID();
|
|
2008
2308
|
const answered = new Promise((resolve) => {
|
|
2009
2309
|
this.questionWaiters.set(id, resolve);
|
|
2010
2310
|
setTimeout(() => { resolve(undefined); }, timeoutMs).unref?.();
|
|
2011
2311
|
});
|
|
2012
|
-
|
|
2312
|
+
// Place the question inside the asker's round: its run, and the tail of
|
|
2313
|
+
// what the round had said so far (the reply splits there when it lands).
|
|
2314
|
+
const run = replay(session.snapshotEvents()).runs.find(entry => entry.member === member.name && entry.state === 'running');
|
|
2315
|
+
const child = member.childSessionId === undefined ? undefined : this.ctx.sessions.get(member.childSessionId);
|
|
2316
|
+
const before = child === undefined ? '' : roundTextSoFar(child.snapshotEvents()).slice(-BEFORE_TAIL);
|
|
2317
|
+
session.append('room/question', {
|
|
2318
|
+
id, member: member.name, memberId: memberId(session.snapshotEvents(), member), questions,
|
|
2319
|
+
...consent ? { consent: true } : {},
|
|
2320
|
+
...run?.runId === undefined ? {} : { runId: run.runId },
|
|
2321
|
+
...before.trim() === '' ? {} : { before },
|
|
2322
|
+
});
|
|
2013
2323
|
await this.ctx.sessions.flush(session);
|
|
2014
2324
|
const answers = await answered;
|
|
2015
2325
|
this.questionWaiters.delete(id);
|
|
@@ -2031,7 +2341,7 @@ let RoomService = (() => {
|
|
|
2031
2341
|
const waiter = this.questionWaiters.get(request.id);
|
|
2032
2342
|
const pending = loaded.state.questions?.find(entry => entry.id === request.id);
|
|
2033
2343
|
if (waiter === undefined || pending === undefined)
|
|
2034
|
-
return
|
|
2344
|
+
return this.answerExpiredQuestion(loaded.session, request);
|
|
2035
2345
|
// Keep only answers to this card's own questions, in the shape the asker expects.
|
|
2036
2346
|
const answers = pending.questions.map((question) => {
|
|
2037
2347
|
const given = request.answers.find(answer => answer.id === question.id);
|
|
@@ -2043,6 +2353,44 @@ let RoomService = (() => {
|
|
|
2043
2353
|
waiter(answers);
|
|
2044
2354
|
return { ok: true };
|
|
2045
2355
|
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Answer a card whose waiting call is gone (it timed out, or a restart
|
|
2358
|
+
* ended the member's process): the card records the answers, and they
|
|
2359
|
+
* reach the member as a new message that wakes it with its own context —
|
|
2360
|
+
* the member asked so it could go on, so a late answer is still worth it.
|
|
2361
|
+
* A consent gate is never answered late: its call already decided.
|
|
2362
|
+
* @param session - the live room session.
|
|
2363
|
+
* @param request - card id and the human's answers.
|
|
2364
|
+
* @returns ok, or `question-not-pending` when the card is not an expired, unanswered question.
|
|
2365
|
+
*/
|
|
2366
|
+
async answerExpiredQuestion(session, request) {
|
|
2367
|
+
const events = session.snapshotEvents();
|
|
2368
|
+
const asked = events.find(event => event.type === 'room/question' && event.data.id === request.id);
|
|
2369
|
+
if (asked?.type !== 'room/question' || asked.data.consent === true)
|
|
2370
|
+
return { ok: false, error: { code: 'question-not-pending' } };
|
|
2371
|
+
const outcomes = events.filter(event => event.type === 'room/question-resolved' && event.data.id === request.id);
|
|
2372
|
+
// Expired, or still open with no call behind it (a restart this room has not swept yet).
|
|
2373
|
+
const last = outcomes.at(-1);
|
|
2374
|
+
if (last !== undefined && (last.type !== 'room/question-resolved' || last.data.outcome !== 'cancelled'))
|
|
2375
|
+
return { ok: false, error: { code: 'question-not-pending' } };
|
|
2376
|
+
const member = replay(events).members.find(entry => memberId(events, entry) === asked.data.memberId);
|
|
2377
|
+
if (member === undefined)
|
|
2378
|
+
return { ok: false, error: { code: 'question-not-pending' } };
|
|
2379
|
+
const answers = asked.data.questions.map((question) => {
|
|
2380
|
+
const given = request.answers.find(answer => answer.id === question.id);
|
|
2381
|
+
const custom = given?.custom?.trim();
|
|
2382
|
+
return { id: question.id, selected: [...given?.selected ?? []].filter(label => typeof label === 'string'), ...custom === undefined || custom === '' ? {} : { custom } };
|
|
2383
|
+
});
|
|
2384
|
+
session.append('room/question-resolved', { id: request.id, outcome: 'answered', answers, late: true });
|
|
2385
|
+
const text = lateAnswerText(asked.data.questions, answers);
|
|
2386
|
+
const carry = this.moveOnFromLostRounds(session, [member.name]);
|
|
2387
|
+
const dispatch = session.append('room/dispatch', {
|
|
2388
|
+
id: randomUUID(), targets: [member.name], targetIds: [memberId(events, member)], text, origin: 'human',
|
|
2389
|
+
});
|
|
2390
|
+
await this.ctx.sessions.flush(session);
|
|
2391
|
+
this.engine.dispatch(session, member.name, carry(member.name, text), { dispatchSeq: dispatch.seq });
|
|
2392
|
+
return { ok: true };
|
|
2393
|
+
}
|
|
2046
2394
|
/** Shared backend for external MCP room tools. Not exposed as a browser Remote. */
|
|
2047
2395
|
async receiveMemberCommand(actor, command) {
|
|
2048
2396
|
try {
|
package/lib/types/jobs.js
CHANGED
|
@@ -22,8 +22,12 @@ export function replayJobs(events, deliveries) {
|
|
|
22
22
|
}
|
|
23
23
|
else if (event.type === 'room/job-stopped')
|
|
24
24
|
stopped.add(event.data.task);
|
|
25
|
-
else if (event.type === 'room/dispatch' && event.data.id !== undefined)
|
|
26
|
-
|
|
25
|
+
else if (event.type === 'room/dispatch' && event.data.id !== undefined) {
|
|
26
|
+
// A task rooted in a member's dispatch (the coordinator delegating) is
|
|
27
|
+
// that member's words, not the human's.
|
|
28
|
+
const from = event.data.origin !== 'human' && event.data.from !== undefined ? event.data.from : undefined;
|
|
29
|
+
dispatchById.set(event.data.id, { time: event.time, text: event.data.text, ...from === undefined ? {} : { from } });
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
const byTask = new Map();
|
|
29
33
|
for (const row of deliveries) {
|
|
@@ -55,7 +59,7 @@ export function replayJobs(events, deliveries) {
|
|
|
55
59
|
id, number: index + 1,
|
|
56
60
|
// Leading @-mentions address members; the request is what follows.
|
|
57
61
|
title: (root.text.replace(/^(?:@\S+\s+)+/, '').split('\n', 1)[0] ?? '').slice(0, 120),
|
|
58
|
-
messages: [{ id, time: root.time, text: root.text }, ...follow].sort((a, b) => a.time - b.time),
|
|
62
|
+
messages: [{ id, time: root.time, text: root.text, ...root.from === undefined ? {} : { from: root.from } }, ...follow].sort((a, b) => a.time - b.time),
|
|
59
63
|
status, startedAt: root.time,
|
|
60
64
|
...open ? {} : { endedAt: ended },
|
|
61
65
|
deliveries: rows.map(row => row.id),
|
package/lib/types/journal.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare const ROOM_ROLE_MAX = 30;
|
|
|
20
20
|
* in-place add is that surface's temporary form — migrate to the official
|
|
21
21
|
* one when it lands upstream.
|
|
22
22
|
*/
|
|
23
|
-
export declare const ROOM_EVENT_TYPES: readonly ['room/created', 'room/plan-state', 'room/coordinator', 'room/delivery-state', 'room/member-added', 'room/member-updated', 'room/member-removed', 'room/dispatch', 'room/speech', 'room/run-state', 'room/execution-metadata', 'room/relay', 'room/relay-resolved', 'room/approval', 'room/approval-resolved', 'room/question', 'room/question-resolved', 'room/task-added', 'room/task-updated', 'room/task-edited', 'room/goal', 'room/settings', 'room/report-gate', 'room/decision', 'room/task-link', 'room/job-stopped', 'room/help', 'room/help-message', 'room/help-state', 'room/help-in', 'room/help-link', 'room/help-unlink', 'room/help-nudge-dismissed'];
|
|
23
|
+
export declare const ROOM_EVENT_TYPES: readonly ['room/created', 'room/plan-state', 'room/coordinator', 'room/delivery-state', 'room/member-added', 'room/member-updated', 'room/member-removed', 'room/dispatch', 'room/speech', 'room/run-state', 'room/run-moved', 'room/execution-metadata', 'room/relay', 'room/relay-resolved', 'room/approval', 'room/approval-resolved', 'room/question', 'room/question-resolved', 'room/task-added', 'room/task-updated', 'room/task-edited', 'room/goal', 'room/settings', 'room/report-gate', 'room/decision', 'room/task-link', 'room/job-stopped', 'room/help', 'room/help-message', 'room/help-state', 'room/help-in', 'room/help-link', 'room/help-unlink', 'room/help-nudge-dismissed'];
|
|
24
24
|
/** Collaboration settings of a room that never changed them. */
|
|
25
25
|
export declare const DEFAULT_ROOM_SETTINGS: RoomSettings;
|
|
26
26
|
/** Whether an event log carries the room identity marker. */
|