@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.
- package/CHANGELOG.md +13 -0
- package/lib/client.js +2256 -1074
- package/lib/index.js +548 -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 +50 -0
- package/lib/types/dispatch.js +115 -1
- package/lib/types/index.d.ts +69 -1
- package/lib/types/index.js +362 -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
|
|
@@ -1364,9 +1573,13 @@ let RoomService = (() => {
|
|
|
1364
1573
|
const loaded = await this.ensureLive(request.sessionId);
|
|
1365
1574
|
if (!loaded.ok)
|
|
1366
1575
|
return { ok: false, error: loaded.error };
|
|
1367
|
-
|
|
1576
|
+
const attached = request.attachments ?? [];
|
|
1577
|
+
if (request.text.trim() === '' && attached.length === 0)
|
|
1368
1578
|
return { ok: false, error: { code: 'empty-text' } };
|
|
1369
|
-
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');
|
|
1370
1583
|
if (request.requestId !== undefined) {
|
|
1371
1584
|
if (request.requestId.trim() === '' || request.requestId.length > 128)
|
|
1372
1585
|
return { ok: false, error: { code: 'request-conflict' } };
|
|
@@ -1384,7 +1597,7 @@ let RoomService = (() => {
|
|
|
1384
1597
|
for (const target of original.targetIds ?? []) {
|
|
1385
1598
|
const member = replay(loaded.session.snapshotEvents()).members.find(member => memberId(loaded.session.snapshotEvents(), member) === target);
|
|
1386
1599
|
if (member !== undefined)
|
|
1387
|
-
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 });
|
|
1388
1601
|
}
|
|
1389
1602
|
return { ok: true, value: { parsed: { targets: original.targets, text: original.text }, seq: previous.seq } };
|
|
1390
1603
|
}
|
|
@@ -1407,9 +1620,10 @@ let RoomService = (() => {
|
|
|
1407
1620
|
// The human pointing at a session outside the room is a help request: say
|
|
1408
1621
|
// so every time (a member's briefing reaches it once, on its first round).
|
|
1409
1622
|
const outside = [...said.matchAll(/@\[((?:\\.|[^\\\]\n])+)\]\((dsh-session:[^)\s]+)\)/gu)];
|
|
1410
|
-
|
|
1623
|
+
// An attachment-only message still dispatches: its words are the attachments.
|
|
1624
|
+
const text = said === '' && attached.length > 0 ? '(见附件)' : said;
|
|
1411
1625
|
// The hint rides the member's prompt only; the journal keeps the human's words.
|
|
1412
|
-
const prompt = outside.length === 0 ?
|
|
1626
|
+
const prompt = outside.length === 0 ? text : [
|
|
1413
1627
|
said,
|
|
1414
1628
|
'',
|
|
1415
1629
|
`(人点名了房间外的会话:${outside.map(match => `「${match[1].replace(/\\(.)/gu, '$1')}」`).join('、')}。用 room_help 去问:session 参数原样传那个 @[…](dsh-session:…) 引用;标签是「成员(房间)」时会交给那个成员。不要转给别的成员代问。)`,
|
|
@@ -1420,23 +1634,103 @@ let RoomService = (() => {
|
|
|
1420
1634
|
const unknown = targets.filter(target => !roster.has(target));
|
|
1421
1635
|
if (unknown.length > 0)
|
|
1422
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 };
|
|
1423
1640
|
const human = createUserMessage({
|
|
1424
|
-
content: [{ type: 'text', text: request.text.trim() }],
|
|
1641
|
+
content: [...admitted.blocks, ...request.text.trim() === '' ? [] : [{ type: 'text', text: request.text.trim() }]],
|
|
1425
1642
|
source: { kind: 'user' },
|
|
1426
1643
|
});
|
|
1427
|
-
|
|
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);
|
|
1428
1661
|
const filed = await this.fileHumanMessage(loaded.session, human.id, request.text.trim(), request.task ?? this.consumeTaskHint(request.sessionId));
|
|
1429
|
-
this.moveOnFromLostRounds(loaded.session, targets);
|
|
1662
|
+
const carry = this.moveOnFromLostRounds(loaded.session, targets);
|
|
1430
1663
|
const dispatch = loaded.session.append('room/dispatch', {
|
|
1431
1664
|
id: request.requestId ?? randomUUID(), requestSignature, targets, text, origin: 'human', task: filed.task,
|
|
1665
|
+
...admitted.records.length === 0 ? {} : { attachments: admitted.records },
|
|
1432
1666
|
targetIds: targets.map(name => memberId(loaded.session.snapshotEvents(), loaded.state.members.find(member => member.name === name))),
|
|
1433
1667
|
});
|
|
1434
1668
|
await this.ctx.sessions.flush(loaded.session);
|
|
1435
1669
|
for (const target of targets) {
|
|
1436
|
-
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 });
|
|
1437
1671
|
}
|
|
1438
1672
|
return { ok: true, value: { parsed: { targets, text }, seq: dispatch.seq } };
|
|
1439
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
|
+
}
|
|
1440
1734
|
/**
|
|
1441
1735
|
* Send one message from a member to another (host-only — the `room_message`
|
|
1442
1736
|
* tool and the bridge's `member_message`/`room_message` land here; the
|
|
@@ -1640,7 +1934,7 @@ let RoomService = (() => {
|
|
|
1640
1934
|
id: 'help', header: '求助',
|
|
1641
1935
|
question: `${asker.name} 想向会话「${target.title}」求助:${text.length > 80 ? `${text.slice(0, 80)}…` : text}`,
|
|
1642
1936
|
options: [{ label: '允许' }, { label: '不允许' }],
|
|
1643
|
-
}], 10 * 60_000);
|
|
1937
|
+
}], 10 * 60_000, true);
|
|
1644
1938
|
if (answers?.[0]?.selected[0] !== '允许')
|
|
1645
1939
|
return `The human did not allow asking "${target.title}". Do not retry unless they @ that session in the room.`;
|
|
1646
1940
|
}
|
|
@@ -1679,7 +1973,7 @@ let RoomService = (() => {
|
|
|
1679
1973
|
// A round a restart left uncertain blocks the member's later work until a
|
|
1680
1974
|
// human reconciles it — silently, for a request from outside. Move on
|
|
1681
1975
|
// from it, as a human message to that member does.
|
|
1682
|
-
this.moveOnFromLostRounds(other.session, [member.name], '另一个协作间来求助;被重启打断的这一轮按没做完处理');
|
|
1976
|
+
const carry = this.moveOnFromLostRounds(other.session, [member.name], '另一个协作间来求助;被重启打断的这一轮按没做完处理');
|
|
1683
1977
|
const sent = sendOrHold(other.session, {
|
|
1684
1978
|
to: member, text, fromName: `${help.fromName}(${origin.roomTitle})`,
|
|
1685
1979
|
intent: { origin: 'help', hop: 1, kind: 'task', help: origin, ...op === 'request' ? { task: SELF_TASK } : {} },
|
|
@@ -1689,7 +1983,7 @@ let RoomService = (() => {
|
|
|
1689
1983
|
room.append('room/help-state', { help: help.id, state: sent.delivered ? 'running' : 'queued' });
|
|
1690
1984
|
await this.ctx.sessions.flush(room);
|
|
1691
1985
|
if (sent.delivered)
|
|
1692
|
-
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 });
|
|
1693
1987
|
const who = `${member.name} in the room "${help.target.title}"`;
|
|
1694
1988
|
return `${sent.delivered ? `Sent to ${who}.` : `Sent to ${who}; that room is paused, so the request waits until it resumes.`}`
|
|
1695
1989
|
+ ` Help id: ${help.id}. The reply reaches you on its own — end your turn now; do not poll.`;
|
|
@@ -2009,13 +2303,23 @@ let RoomService = (() => {
|
|
|
2009
2303
|
* @param timeoutMs - how long the call waits before it gives up.
|
|
2010
2304
|
* @returns the answers, or undefined when nobody answered.
|
|
2011
2305
|
*/
|
|
2012
|
-
async askQuestions(session, member, questions, timeoutMs) {
|
|
2306
|
+
async askQuestions(session, member, questions, timeoutMs, consent = false) {
|
|
2013
2307
|
const id = randomUUID();
|
|
2014
2308
|
const answered = new Promise((resolve) => {
|
|
2015
2309
|
this.questionWaiters.set(id, resolve);
|
|
2016
2310
|
setTimeout(() => { resolve(undefined); }, timeoutMs).unref?.();
|
|
2017
2311
|
});
|
|
2018
|
-
|
|
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
|
+
});
|
|
2019
2323
|
await this.ctx.sessions.flush(session);
|
|
2020
2324
|
const answers = await answered;
|
|
2021
2325
|
this.questionWaiters.delete(id);
|
|
@@ -2037,7 +2341,7 @@ let RoomService = (() => {
|
|
|
2037
2341
|
const waiter = this.questionWaiters.get(request.id);
|
|
2038
2342
|
const pending = loaded.state.questions?.find(entry => entry.id === request.id);
|
|
2039
2343
|
if (waiter === undefined || pending === undefined)
|
|
2040
|
-
return
|
|
2344
|
+
return this.answerExpiredQuestion(loaded.session, request);
|
|
2041
2345
|
// Keep only answers to this card's own questions, in the shape the asker expects.
|
|
2042
2346
|
const answers = pending.questions.map((question) => {
|
|
2043
2347
|
const given = request.answers.find(answer => answer.id === question.id);
|
|
@@ -2049,6 +2353,44 @@ let RoomService = (() => {
|
|
|
2049
2353
|
waiter(answers);
|
|
2050
2354
|
return { ok: true };
|
|
2051
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
|
+
}
|
|
2052
2394
|
/** Shared backend for external MCP room tools. Not exposed as a browser Remote. */
|
|
2053
2395
|
async receiveMemberCommand(actor, command) {
|
|
2054
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. */
|
package/lib/types/journal.js
CHANGED
|
@@ -44,6 +44,7 @@ export const ROOM_EVENT_TYPES = [
|
|
|
44
44
|
'room/dispatch',
|
|
45
45
|
'room/speech',
|
|
46
46
|
'room/run-state',
|
|
47
|
+
'room/run-moved',
|
|
47
48
|
'room/execution-metadata',
|
|
48
49
|
'room/relay',
|
|
49
50
|
'room/relay-resolved',
|
|
@@ -117,6 +118,7 @@ export function replay(events) {
|
|
|
117
118
|
const relays = [];
|
|
118
119
|
const approvals = new Map();
|
|
119
120
|
const questions = new Map();
|
|
121
|
+
const asks = new Map();
|
|
120
122
|
const relayById = new Map();
|
|
121
123
|
const tasks = [];
|
|
122
124
|
const taskById = new Map();
|
|
@@ -269,10 +271,24 @@ export function replay(events) {
|
|
|
269
271
|
break;
|
|
270
272
|
case 'room/question':
|
|
271
273
|
questions.set(event.data.id, { id: event.data.id, member: event.data.member, questions: event.data.questions, time: event.time });
|
|
274
|
+
asks.set(event.data.id, {
|
|
275
|
+
id: event.data.id, member: event.data.member, questions: event.data.questions, state: 'pending', time: event.time,
|
|
276
|
+
...event.data.runId === undefined ? {} : { runId: event.data.runId },
|
|
277
|
+
...event.data.consent === true ? { consent: true } : {},
|
|
278
|
+
});
|
|
272
279
|
break;
|
|
273
|
-
case 'room/question-resolved':
|
|
280
|
+
case 'room/question-resolved': {
|
|
274
281
|
questions.delete(event.data.id);
|
|
282
|
+
const ask = asks.get(event.data.id);
|
|
283
|
+
if (ask !== undefined) {
|
|
284
|
+
asks.set(ask.id, {
|
|
285
|
+
...ask, state: event.data.outcome,
|
|
286
|
+
...event.data.answers === undefined ? {} : { answers: event.data.answers },
|
|
287
|
+
...event.data.late === true ? { late: true } : {},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
275
290
|
break;
|
|
291
|
+
}
|
|
276
292
|
case 'room/approval-resolved':
|
|
277
293
|
approvals.delete(event.data.id);
|
|
278
294
|
break;
|
|
@@ -395,7 +411,7 @@ export function replay(events) {
|
|
|
395
411
|
return {
|
|
396
412
|
...helps.length === 0 ? {} : { helps },
|
|
397
413
|
...links.linked.length === 0 ? {} : { linked: links.linked },
|
|
398
|
-
...links.nudgesDismissed.length === 0 ? {} : { helpNudgesDismissed: links.nudgesDismissed }, members, relays, ...approvals.size === 0 ? {} : { approvals: [...approvals.values()] }, ...questions.size === 0 ? {} : { questions: [...questions.values()] }, tasks, ...executions.size === 0 ? {} : { executions: [...executions.values()] }, ...plan === undefined ? {} : { plan }, ...deliveries.length === 0 ? {} : { deliveries }, runs: [...runs.values()], ...coordinator === undefined ? {} : { coordinator }, ...goal === undefined ? {} : { goal }, ...latestReplySeq === undefined ? {} : { latestReplySeq }, ...latestSpeechSeq === undefined ? {} : { latestSpeechSeq }, settings, ...jobs.length === 0 ? {} : { jobs }
|
|
414
|
+
...links.nudgesDismissed.length === 0 ? {} : { helpNudgesDismissed: links.nudgesDismissed }, members, relays, ...approvals.size === 0 ? {} : { approvals: [...approvals.values()] }, ...questions.size === 0 ? {} : { questions: [...questions.values()] }, ...asks.size === 0 ? {} : { askLog: [...asks.values()] }, tasks, ...executions.size === 0 ? {} : { executions: [...executions.values()] }, ...plan === undefined ? {} : { plan }, ...deliveries.length === 0 ? {} : { deliveries }, runs: [...runs.values()], ...coordinator === undefined ? {} : { coordinator }, ...goal === undefined ? {} : { goal }, ...latestReplySeq === undefined ? {} : { latestReplySeq }, ...latestSpeechSeq === undefined ? {} : { latestSpeechSeq }, settings, ...jobs.length === 0 ? {} : { jobs }
|
|
399
415
|
};
|
|
400
416
|
}
|
|
401
417
|
/**
|