@yeaft/webchat-agent 1.0.640 → 1.0.642
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/connection/message-router.js +4 -1
- package/index.js +1 -1
- package/local-runtime/server/handlers/agent-output.js +7 -5
- package/local-runtime/server/handlers/client-conversation.js +47 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +158 -119
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +272 -24
- package/yeaft/conversation/session-history.js +336 -0
- package/yeaft/engine.js +88 -573
- package/yeaft/history-window.js +132 -5
- package/yeaft/routing/router.js +4 -0
- package/yeaft/sessions/index.js +1 -0
- package/yeaft/sessions/session-crud.js +68 -3
- package/yeaft/tools/agent.js +4 -1
- package/yeaft/web-bridge.js +255 -54
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session 历史复制/合并的纯变换。持久 ID 只在所属 stream 内有意义;
|
|
3
|
+
* origin 跨 fork/merge 保留,绝不把相同提问文本当作共同历史。
|
|
4
|
+
*/
|
|
5
|
+
import { isVisibleConversationRow } from './internal-control.js';
|
|
6
|
+
|
|
7
|
+
/** Create a history-boundary error with a stable machine-readable code. */
|
|
8
|
+
export function historyError(code, message) {
|
|
9
|
+
return Object.assign(new Error(message), { code });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Whether a durable row is a human request rather than protocol/control input. */
|
|
13
|
+
export function isHumanRequest(row) {
|
|
14
|
+
return row?.role === 'user' && isVisibleConversationRow(row);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Preserve fork lineage, or establish an origin in the source stream's lifetime. */
|
|
18
|
+
export function messageOrigin(row, sessionId, streamId) {
|
|
19
|
+
const origin = row?.origin;
|
|
20
|
+
if (origin && typeof origin.streamId === 'string' && origin.streamId
|
|
21
|
+
&& typeof origin.messageId === 'string' && origin.messageId) return { ...origin };
|
|
22
|
+
return { sessionId, streamId, messageId: row.id, ...(row.turnId ? { turnId: row.turnId } : {}) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Cross-Session rows are historical projections, never native continuations.
|
|
27
|
+
* Signatures/encrypted reasoning are bound to the original owner and exact
|
|
28
|
+
* tool IDs. Keep the source transcript (linked by origin) as provenance, not
|
|
29
|
+
* an opaque payload in the new Session. Reconstruct only canonical text/tools.
|
|
30
|
+
* This deliberately does not relax replayProviderState's owner/digest fences.
|
|
31
|
+
*/
|
|
32
|
+
export function historicalReplayRow(row) {
|
|
33
|
+
const { providerState, thinkingBlocks, ...history } = row;
|
|
34
|
+
return history;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function originKey(origin) {
|
|
38
|
+
return JSON.stringify([origin.streamId, origin.messageId]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stableJson(value) {
|
|
42
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
43
|
+
if (value && typeof value === 'object') {
|
|
44
|
+
return `{${Object.keys(value).sort().filter(key => value[key] !== undefined)
|
|
45
|
+
.map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sentAt(row) {
|
|
51
|
+
return row.time ?? row.timestamp ?? row.ts ?? '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toolScope(row, callId) {
|
|
55
|
+
return JSON.stringify([row.turnId || '', row.speakerVpId || row.vpId || '', row.threadId || 'main', callId]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ambiguous(message) {
|
|
59
|
+
throw historyError('ambiguous_merge_history', message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 原始 transcript 不被 sanitize(sanitize 会丢数据)。返回每个结果对应的
|
|
63
|
+
// assistant 调用位置,允许不同 VP/turn 重用同一个 provider tool-call ID。
|
|
64
|
+
function toolLinks(rows) {
|
|
65
|
+
const calls = new Map();
|
|
66
|
+
const results = new Map();
|
|
67
|
+
const paired = new Set();
|
|
68
|
+
for (const [index, row] of rows.entries()) {
|
|
69
|
+
for (const [callIndex, call] of (row.toolCalls || []).entries()) {
|
|
70
|
+
const key = toolScope(row, call.id);
|
|
71
|
+
if (!call.id || calls.has(key)) ambiguous('Duplicate tool call identity within a request');
|
|
72
|
+
calls.set(key, { index, callIndex });
|
|
73
|
+
}
|
|
74
|
+
if (row.role !== 'tool') continue;
|
|
75
|
+
const key = toolScope(row, row.toolCallId);
|
|
76
|
+
let call = calls.get(key);
|
|
77
|
+
// Some legacy tool rows lack speaker/turn metadata. Only accept an
|
|
78
|
+
// unambiguous preceding call, never a globally colliding bare tool id.
|
|
79
|
+
if (!call) {
|
|
80
|
+
const candidates = [...calls.values()].filter(candidate => {
|
|
81
|
+
const owner = rows[candidate.index];
|
|
82
|
+
return owner.toolCalls[candidate.callIndex].id === row.toolCallId
|
|
83
|
+
&& (!row.turnId || owner.turnId === row.turnId)
|
|
84
|
+
&& (!(row.speakerVpId || row.vpId) || (owner.speakerVpId || owner.vpId) === (row.speakerVpId || row.vpId))
|
|
85
|
+
&& (!row.threadId || (owner.threadId || 'main') === row.threadId);
|
|
86
|
+
});
|
|
87
|
+
if (candidates.length === 1) [call] = candidates;
|
|
88
|
+
}
|
|
89
|
+
if (!call || paired.has(call)) ambiguous('Unpaired or ambiguous tool result within a request');
|
|
90
|
+
results.set(index, call);
|
|
91
|
+
paired.add(call);
|
|
92
|
+
}
|
|
93
|
+
if ([...calls.values()].some(call => !paired.has(call))) ambiguous('Incomplete tool history within a request');
|
|
94
|
+
return { calls, results };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function requestTurns(input) {
|
|
98
|
+
const { rows, sessionId, streamId } = input;
|
|
99
|
+
if (new Set(rows.map(row => row.id)).size !== rows.length) ambiguous('Duplicate persisted identity within one Session');
|
|
100
|
+
const turns = [];
|
|
101
|
+
const byRow = new Map();
|
|
102
|
+
const byClient = new Map();
|
|
103
|
+
for (const [index, row] of rows.entries()) {
|
|
104
|
+
if (!isHumanRequest(row)) continue;
|
|
105
|
+
const candidates = row.clientMessageId ? (byClient.get(row.clientMessageId) || []) : [];
|
|
106
|
+
// Historical multi-VP prompt variants share a client identity and source
|
|
107
|
+
// stream. Distinct origins in a merged Session must remain distinct even
|
|
108
|
+
// if two clients happened to reuse the same clientMessageId.
|
|
109
|
+
const sameStream = candidate => !candidate.user.origin || !row.origin
|
|
110
|
+
|| candidate.user.origin.streamId === row.origin.streamId;
|
|
111
|
+
if (candidates.some(candidate => sameStream(candidate) && sentAt(candidate.user) !== sentAt(row))) {
|
|
112
|
+
ambiguous('Reused clientMessageId within one Session');
|
|
113
|
+
}
|
|
114
|
+
let turn = candidates.find(candidate => sameStream(candidate) && sentAt(candidate.user) === sentAt(row));
|
|
115
|
+
if (!turn) {
|
|
116
|
+
turn = { user: row, rows: [], origins: new Set(), explicitOrigins: false, index, input };
|
|
117
|
+
turns.push(turn);
|
|
118
|
+
if (row.clientMessageId) byClient.set(row.clientMessageId, [...candidates, turn]);
|
|
119
|
+
}
|
|
120
|
+
byRow.set(row.id, turn);
|
|
121
|
+
turn.origins.add(originKey(messageOrigin(row, sessionId, streamId)));
|
|
122
|
+
turn.explicitOrigins ||= !!row.origin;
|
|
123
|
+
}
|
|
124
|
+
if (!turns.length) {
|
|
125
|
+
if (rows.length) ambiguous('History without a durable human request cannot be merged');
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const records = new Map(rows.map(row => [row.id, row]));
|
|
129
|
+
function causalTurn(row, seen = new Set()) {
|
|
130
|
+
if (!row?.causalRootId || seen.has(row.id)) return null;
|
|
131
|
+
seen.add(row.id);
|
|
132
|
+
const direct = byRow.get(row.causalRootId);
|
|
133
|
+
if (direct) return direct;
|
|
134
|
+
const candidates = byClient.get(row.causalRootId) || [];
|
|
135
|
+
if (candidates.length > 1) ambiguous('Ambiguous client causal reference');
|
|
136
|
+
return candidates[0] || causalTurn(records.get(row.causalRootId), seen);
|
|
137
|
+
}
|
|
138
|
+
const byVpTurn = new Map();
|
|
139
|
+
const vpTurnKey = row => row.turnId ? JSON.stringify([row.speakerVpId || row.vpId || '', row.turnId]) : null;
|
|
140
|
+
// Learn explicit causal ownership before assigning interleaved legacy tails.
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
const turn = causalTurn(row);
|
|
143
|
+
const key = vpTurnKey(row);
|
|
144
|
+
if (!turn || !key) continue;
|
|
145
|
+
if (byVpTurn.has(key) && byVpTurn.get(key) !== turn) ambiguous('A VP turn belongs to multiple human requests');
|
|
146
|
+
byVpTurn.set(key, turn);
|
|
147
|
+
}
|
|
148
|
+
let current = null;
|
|
149
|
+
for (const row of rows) {
|
|
150
|
+
if (isHumanRequest(row)) current = byRow.get(row.id);
|
|
151
|
+
const key = vpTurnKey(row);
|
|
152
|
+
const turn = byRow.get(row.id) || causalTurn(row) || byVpTurn.get(key) || current;
|
|
153
|
+
if (!turn) {
|
|
154
|
+
if (isVisibleConversationRow(row) && row.role !== 'system') ambiguous('History begins inside a human request');
|
|
155
|
+
turns[0].rows.push(row);
|
|
156
|
+
byRow.set(row.id, turns[0]);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (key) {
|
|
160
|
+
if (byVpTurn.has(key) && byVpTurn.get(key) !== turn) ambiguous('Ambiguous interleaved VP history');
|
|
161
|
+
byVpTurn.set(key, turn);
|
|
162
|
+
}
|
|
163
|
+
turn.rows.push(row);
|
|
164
|
+
byRow.set(row.id, turn);
|
|
165
|
+
}
|
|
166
|
+
for (const turn of turns) {
|
|
167
|
+
turn.links = toolLinks(turn.rows);
|
|
168
|
+
turn.vpTurns = new Set(turn.rows.filter(row => row.role === 'assistant' && row.turnId)
|
|
169
|
+
.map(row => JSON.stringify([row.speakerVpId || row.vpId || '', row.origin?.turnId || row.turnId])));
|
|
170
|
+
// Legacy forks preserved these identities, even though persisted row IDs
|
|
171
|
+
// were remapped. Require sent-time evidence too; no text-only matching.
|
|
172
|
+
turn.legacyKey = turn.user.clientMessageId && sentAt(turn.user)
|
|
173
|
+
? stableJson(['client', turn.user.clientMessageId, sentAt(turn.user)])
|
|
174
|
+
: (turn.vpTurns.size && sentAt(turn.user)
|
|
175
|
+
? stableJson(['turns', [...turn.vpTurns].sort(), sentAt(turn.user)]) : null);
|
|
176
|
+
}
|
|
177
|
+
return turns;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function commonRequest(left, right) {
|
|
181
|
+
if ([...left.origins].some(key => right.origins.has(key))) return true;
|
|
182
|
+
if (left.user.clientMessageId && right.user.clientMessageId
|
|
183
|
+
&& left.user.clientMessageId !== right.user.clientMessageId) return false;
|
|
184
|
+
// Explicit, different origins prove these are distinct user requests.
|
|
185
|
+
if (left.explicitOrigins && right.explicitOrigins) return false;
|
|
186
|
+
if (left.legacyKey && left.legacyKey === right.legacyKey) return true;
|
|
187
|
+
// A fork made at an assistant boundary may contain only one VP's answer.
|
|
188
|
+
// Recognize overlap, then reject unequal request bodies below instead of
|
|
189
|
+
// treating that partial shared request as two independent user requests.
|
|
190
|
+
return !!sentAt(left.user) && sentAt(left.user) === sentAt(right.user)
|
|
191
|
+
&& [...left.vpTurns].some(key => right.vpTurns.has(key));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function equivalentRows(left, right, aliases) {
|
|
195
|
+
if (left.rows.length !== right.rows.length) return false;
|
|
196
|
+
function normalized(turn) {
|
|
197
|
+
const rowRefs = new Map();
|
|
198
|
+
for (const candidate of turn.input.turns) {
|
|
199
|
+
const canonical = aliases.get(candidate) || candidate;
|
|
200
|
+
const requestKey = originKey(messageOrigin(canonical.user, canonical.input.sessionId, canonical.input.streamId));
|
|
201
|
+
candidate.rows.forEach((row, index) => rowRefs.set(row.id, `${requestKey}:${index}`));
|
|
202
|
+
}
|
|
203
|
+
const clientRefs = new Map();
|
|
204
|
+
for (const candidate of turn.input.turns) {
|
|
205
|
+
if (!candidate.user.clientMessageId) continue;
|
|
206
|
+
const clientId = candidate.user.clientMessageId;
|
|
207
|
+
const canonical = rowRefs.get(candidate.user.id);
|
|
208
|
+
clientRefs.set(clientId, clientRefs.has(clientId) && clientRefs.get(clientId) !== canonical ? null : canonical);
|
|
209
|
+
}
|
|
210
|
+
const remap = value => rowRefs.get(value) || value;
|
|
211
|
+
const callRefs = new Map([...turn.links.calls].map(([key, call]) => [key, `call:${call.index}:${call.callIndex}`]));
|
|
212
|
+
return turn.rows.map((row, index) => {
|
|
213
|
+
const copy = historicalReplayRow(row);
|
|
214
|
+
for (const key of ['id', 'seq', 'sessionId', 'origin', 'cold']) delete copy[key];
|
|
215
|
+
if (row.turnId) copy.turnId = row.origin?.turnId || row.turnId;
|
|
216
|
+
if (row.causalRootId) copy.causalRootId = rowRefs.get(row.causalRootId) || clientRefs.get(row.causalRootId) || row.causalRootId;
|
|
217
|
+
for (const key of ['foldedMessageIds', 'sourceMessageIds']) {
|
|
218
|
+
if (Array.isArray(row[key])) copy[key] = row[key].map(remap);
|
|
219
|
+
}
|
|
220
|
+
if (row.quote?.messageId) copy.quote = { ...row.quote, messageId: remap(row.quote.messageId) };
|
|
221
|
+
if (Array.isArray(row.attachments)) copy.attachments = row.attachments.map(att => {
|
|
222
|
+
const { path, ...metadata } = att;
|
|
223
|
+
return metadata;
|
|
224
|
+
});
|
|
225
|
+
if (row.toolCalls) copy.toolCalls = row.toolCalls.map(call => ({ ...call, id: callRefs.get(toolScope(row, call.id)) }));
|
|
226
|
+
const call = turn.links.results.get(index);
|
|
227
|
+
if (call) copy.toolCallId = `call:${call.index}:${call.callIndex}`;
|
|
228
|
+
if (copy.presentationCallId && callRefs.has(toolScope(row, copy.presentationCallId))) {
|
|
229
|
+
copy.presentationCallId = callRefs.get(toolScope(row, copy.presentationCallId));
|
|
230
|
+
}
|
|
231
|
+
return copy;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return stableJson(normalized(left)) === stableJson(normalized(right));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 规划两份 idle Session transcript 的完整请求合并;不写磁盘。
|
|
239
|
+
* @param {Array<{sessionId:string, streamId:string, rows:object[]}>} inputs
|
|
240
|
+
* @returns {{turns:object[], aliases:Map<object,object>, sharedTurnCount:number}}
|
|
241
|
+
*/
|
|
242
|
+
export function planSessionMerge(inputs) {
|
|
243
|
+
for (const input of inputs) input.turns = requestTurns(input);
|
|
244
|
+
const [left, right] = inputs.map(input => input.turns);
|
|
245
|
+
const aliases = new Map();
|
|
246
|
+
for (const other of right) {
|
|
247
|
+
const matches = left.filter(turn => commonRequest(turn, other));
|
|
248
|
+
if (matches.length > 1) ambiguous('Shared request identity is not unique');
|
|
249
|
+
if (matches.length) {
|
|
250
|
+
if ([...aliases.values()].includes(matches[0])) ambiguous('Shared request occurs more than once');
|
|
251
|
+
aliases.set(other, matches[0]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [other, source] of aliases) {
|
|
255
|
+
if (!equivalentRows(source, other, aliases)) ambiguous('Shared human request has divergent or partial branch history');
|
|
256
|
+
}
|
|
257
|
+
// Only an aligned shared prefix precedes the chronological merge. A shared
|
|
258
|
+
// request can also occur in the middle after earlier merges (AB + BC); moving
|
|
259
|
+
// every shared request to the front would incorrectly produce B, A, C.
|
|
260
|
+
let prefixLength = 0;
|
|
261
|
+
while (prefixLength < left.length && aliases.get(right[prefixLength]) === left[prefixLength]) prefixLength++;
|
|
262
|
+
const common = left.slice(0, prefixLength);
|
|
263
|
+
const tails = [...left.slice(prefixLength), ...right.slice(prefixLength).filter(turn => !aliases.has(turn))];
|
|
264
|
+
const timestamp = turn => {
|
|
265
|
+
const value = sentAt(turn.user);
|
|
266
|
+
const parsed = typeof value === 'number' ? value : Date.parse(value);
|
|
267
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
268
|
+
};
|
|
269
|
+
// Stable tie break: source argument order, then original request order.
|
|
270
|
+
tails.sort((a, b) => timestamp(a) - timestamp(b)
|
|
271
|
+
|| inputs.indexOf(a.input) - inputs.indexOf(b.input) || a.index - b.index);
|
|
272
|
+
return { turns: [...common, ...tails], aliases, sharedTurnCount: aliases.size };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 为计划分配新 persisted IDs 并重写内部引用;每个源有独立命名空间。
|
|
277
|
+
* 附件由调用者按实际源所有权复制;provider 私有状态转为 canonical 历史。
|
|
278
|
+
* @param {object} plan planSessionMerge 的返回值
|
|
279
|
+
* @param {string} targetSessionId 新 Session
|
|
280
|
+
* @param {number} firstSeq 已保留的起始序号
|
|
281
|
+
* @returns {{rows:object[], idMaps:Map<string,Map<string,string>>}}
|
|
282
|
+
*/
|
|
283
|
+
export function materializeSessionMerge(plan, targetSessionId, firstSeq) {
|
|
284
|
+
const idMaps = new Map();
|
|
285
|
+
const mapping = input => {
|
|
286
|
+
if (!idMaps.has(input.sessionId)) idMaps.set(input.sessionId, new Map());
|
|
287
|
+
return idMaps.get(input.sessionId);
|
|
288
|
+
};
|
|
289
|
+
let seq = firstSeq;
|
|
290
|
+
for (const turn of plan.turns) {
|
|
291
|
+
for (const row of turn.rows) mapping(turn.input).set(row.id, `m${String(seq++).padStart(4, '0')}`);
|
|
292
|
+
}
|
|
293
|
+
for (const [other, source] of plan.aliases) {
|
|
294
|
+
other.rows.forEach((row, index) => mapping(other.input).set(row.id, mapping(source.input).get(source.rows[index].id)));
|
|
295
|
+
}
|
|
296
|
+
const clientMaps = new Map();
|
|
297
|
+
for (const input of new Set([...plan.turns, ...plan.aliases.keys()].map(turn => turn.input))) {
|
|
298
|
+
const clients = new Map();
|
|
299
|
+
for (const turn of input.turns) {
|
|
300
|
+
if (!turn.user.clientMessageId) continue;
|
|
301
|
+
const clientId = turn.user.clientMessageId;
|
|
302
|
+
const targetId = mapping(input).get(turn.user.id);
|
|
303
|
+
clients.set(clientId, clients.has(clientId) && clients.get(clientId) !== targetId ? null : targetId);
|
|
304
|
+
}
|
|
305
|
+
clientMaps.set(input, clients);
|
|
306
|
+
}
|
|
307
|
+
const rows = [];
|
|
308
|
+
for (const turn of plan.turns) {
|
|
309
|
+
const ids = mapping(turn.input);
|
|
310
|
+
const vpTurns = new Map();
|
|
311
|
+
const remap = id => ids.get(id) || id;
|
|
312
|
+
const callId = call => `merge_${ids.get(turn.rows[call.index].id)}_${call.callIndex}`;
|
|
313
|
+
const calls = new Map([...turn.links.calls].map(([key, call]) => [key, callId(call)]));
|
|
314
|
+
for (const [index, row] of turn.rows.entries()) {
|
|
315
|
+
const copy = { ...historicalReplayRow(row), id: ids.get(row.id), sessionId: targetSessionId,
|
|
316
|
+
origin: messageOrigin(row, turn.input.sessionId, turn.input.streamId) };
|
|
317
|
+
delete copy.seq;
|
|
318
|
+
if (row.turnId) {
|
|
319
|
+
const key = JSON.stringify([row.speakerVpId || row.vpId || '', row.turnId]);
|
|
320
|
+
if (!vpTurns.has(key)) vpTurns.set(key, `merge_${copy.id}_turn`);
|
|
321
|
+
copy.turnId = vpTurns.get(key);
|
|
322
|
+
}
|
|
323
|
+
if (row.causalRootId) copy.causalRootId = ids.get(row.causalRootId)
|
|
324
|
+
|| clientMaps.get(turn.input)?.get(row.causalRootId) || row.causalRootId;
|
|
325
|
+
for (const key of ['foldedMessageIds', 'sourceMessageIds']) {
|
|
326
|
+
if (Array.isArray(row[key])) copy[key] = row[key].map(remap);
|
|
327
|
+
}
|
|
328
|
+
if (row.quote?.messageId) copy.quote = { ...row.quote, messageId: remap(row.quote.messageId) };
|
|
329
|
+
if (row.toolCalls) copy.toolCalls = row.toolCalls.map(call => ({ ...call, id: calls.get(toolScope(row, call.id)) }));
|
|
330
|
+
if (turn.links.results.has(index)) copy.toolCallId = callId(turn.links.results.get(index));
|
|
331
|
+
if (calls.has(toolScope(row, row.presentationCallId))) copy.presentationCallId = calls.get(toolScope(row, row.presentationCallId));
|
|
332
|
+
rows.push(copy);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return { rows, idMaps };
|
|
336
|
+
}
|