@yeaft/webchat-agent 1.0.39 → 1.0.41
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/buffer.js +1 -0
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +4 -0
- package/yeaft/engine.js +2 -1
- package/yeaft/stop-hooks.js +4 -0
- package/yeaft/web-bridge.js +120 -105
package/connection/buffer.js
CHANGED
|
@@ -5,6 +5,7 @@ import { encrypt, decrypt, isEncrypted } from '../encryption.js';
|
|
|
5
5
|
// 需要在断连期间缓冲的消息类型(CLI / Session 输出相关的关键消息)
|
|
6
6
|
export const BUFFERABLE_TYPES = new Set([
|
|
7
7
|
'claude_output', 'yeaft_output', 'yeaft_session_output', 'session_output',
|
|
8
|
+
'yeaft_history_chunk',
|
|
8
9
|
'turn_completed', 'conversation_closed',
|
|
9
10
|
'session_id_update', 'compact_status', 'slash_commands_update',
|
|
10
11
|
'background_task_started', 'background_task_output',
|
package/package.json
CHANGED
|
@@ -206,6 +206,7 @@ function serializeMessage(msg) {
|
|
|
206
206
|
// routing can filter/replay by thread without rescanning JSON blobs.
|
|
207
207
|
// Defaults to 'main' for legacy messages (see migrate-messages-threadid.js).
|
|
208
208
|
fm.push(`threadId: ${msg.threadId || 'main'}`);
|
|
209
|
+
if (msg.turnId) fm.push(`turnId: ${msg.turnId}`);
|
|
209
210
|
// task-313: when a thread is merged into another, the messages keep
|
|
210
211
|
// their original thread id in `sourceThreadId` so the UI can still
|
|
211
212
|
// render a small "#source" pill next to each bubble.
|
|
@@ -216,6 +217,7 @@ function serializeMessage(msg) {
|
|
|
216
217
|
// shows an empty pane.
|
|
217
218
|
if (msg.sessionId) fm.push(`sessionId: ${msg.sessionId}`);
|
|
218
219
|
if (msg.chatId) fm.push(`chatId: ${msg.chatId}`);
|
|
220
|
+
if (msg.clientMessageId) fm.push(`clientMessageId: ${msg.clientMessageId}`);
|
|
219
221
|
// Session attribution: when a VP authors an assistant turn (either
|
|
220
222
|
// its own reply or a route_forward injection from another VP), stamp
|
|
221
223
|
// the speaker so the UI can render the message on the correct VP track.
|
|
@@ -333,9 +335,11 @@ export function parseMessage(raw) {
|
|
|
333
335
|
case 'isError': msg.isError = value === 'true'; break;
|
|
334
336
|
case 'tokens_est': msg.tokens_est = parseInt(value, 10); break;
|
|
335
337
|
case 'threadId': msg.threadId = value; break;
|
|
338
|
+
case 'turnId': msg.turnId = value; break;
|
|
336
339
|
case 'sourceThreadId': msg.sourceThreadId = value; break;
|
|
337
340
|
case 'sessionId': msg.sessionId = value; break;
|
|
338
341
|
case 'chatId': msg.chatId = value; break;
|
|
342
|
+
case 'clientMessageId': msg.clientMessageId = value; break;
|
|
339
343
|
case 'speakerVpId': msg.speakerVpId = value; break;
|
|
340
344
|
case 'attachmentsB64':
|
|
341
345
|
try {
|
package/yeaft/engine.js
CHANGED
|
@@ -1597,7 +1597,7 @@ export class Engine {
|
|
|
1597
1597
|
* string-prompt shape (no regression for existing callers).
|
|
1598
1598
|
* @yields {EngineEvent}
|
|
1599
1599
|
*/
|
|
1600
|
-
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
|
|
1600
|
+
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
|
|
1601
1601
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
1602
1602
|
yield {
|
|
1603
1603
|
type: 'error',
|
|
@@ -2833,6 +2833,7 @@ export class Engine {
|
|
|
2833
2833
|
// history replay can re-stamp them on reload.
|
|
2834
2834
|
sessionId,
|
|
2835
2835
|
threadId,
|
|
2836
|
+
turnId: vpTurnId || queryTurnId,
|
|
2836
2837
|
vpId: this.#vpId,
|
|
2837
2838
|
// Multi-VP fan-out (history-dedup): skip the user-row append
|
|
2838
2839
|
// in stop-hooks when the orchestrator already wrote it once
|
package/yeaft/stop-hooks.js
CHANGED
|
@@ -65,6 +65,7 @@ export async function runStopHooks(context) {
|
|
|
65
65
|
// history replay can route messages back into the originating group.
|
|
66
66
|
sessionId,
|
|
67
67
|
threadId,
|
|
68
|
+
turnId,
|
|
68
69
|
// Group VP attribution: persist the engine-bound VP id on assistant/tool
|
|
69
70
|
// rows so history replay can route replies back to the same visible VP.
|
|
70
71
|
vpId,
|
|
@@ -167,6 +168,9 @@ export async function runStopHooks(context) {
|
|
|
167
168
|
// Bug 6: stamp sessionId / threadId so replay can re-route by group.
|
|
168
169
|
if (sessionId) record.sessionId = sessionId;
|
|
169
170
|
if (threadId) record.threadId = threadId;
|
|
171
|
+
if (turnId && (msg.role === 'assistant' || msg.role === 'tool')) {
|
|
172
|
+
record.turnId = turnId;
|
|
173
|
+
}
|
|
170
174
|
if (vpId && (msg.role === 'assistant' || msg.role === 'tool')) {
|
|
171
175
|
record.speakerVpId = vpId;
|
|
172
176
|
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -712,8 +712,9 @@ function projectPersistedToHistoryEntry(m) {
|
|
|
712
712
|
const entry = { role: m.role, content: m.role === 'tool' ? m.content : __testNormalizePersistedVisibleContent(m.content) };
|
|
713
713
|
if (m.id) entry.id = m.id;
|
|
714
714
|
entry.threadId = m.threadId || m.turnId || 'main';
|
|
715
|
-
entry.turnId = m.turnId
|
|
715
|
+
if (m.turnId) entry.turnId = m.turnId;
|
|
716
716
|
if (m.sessionId) entry.sessionId = m.sessionId;
|
|
717
|
+
if (m.clientMessageId) entry.clientMessageId = m.clientMessageId;
|
|
717
718
|
if (m.speakerVpId) entry.speakerVpId = m.speakerVpId;
|
|
718
719
|
if (m.toolCallId) entry.toolCallId = m.toolCallId;
|
|
719
720
|
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
@@ -813,8 +814,9 @@ function projectVisibleHistoryChunkMessages(messages = []) {
|
|
|
813
814
|
content: m.content,
|
|
814
815
|
ts: m.ts || null,
|
|
815
816
|
sessionId: m.sessionId || null,
|
|
817
|
+
...(m.clientMessageId ? { clientMessageId: m.clientMessageId } : {}),
|
|
816
818
|
threadId: m.threadId || m.turnId || 'main',
|
|
817
|
-
turnId: m.turnId
|
|
819
|
+
...(m.turnId ? { turnId: m.turnId } : {}),
|
|
818
820
|
...(Array.isArray(m.attachments) && m.attachments.length > 0 ? { attachments: hydrateHistoryAttachmentPreviews(m.attachments) } : {}),
|
|
819
821
|
...(m.speakerVpId ? { speakerVpId: m.speakerVpId } : {}),
|
|
820
822
|
...(Number.isFinite(m.toolSummaryCount) && m.toolSummaryCount > 0
|
|
@@ -1392,6 +1394,7 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
1392
1394
|
attachments: Array.isArray(envelope?.msg?.meta?.attachments) ? envelope.msg.meta.attachments : [],
|
|
1393
1395
|
internal: isInternalAppend,
|
|
1394
1396
|
ts: envelope?.msg?.ts || null,
|
|
1397
|
+
clientMessageId: envelope?.msg?.meta?.clientMessageId || null,
|
|
1395
1398
|
});
|
|
1396
1399
|
thread.updatedAt = Date.now();
|
|
1397
1400
|
try {
|
|
@@ -1489,6 +1492,7 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
|
|
|
1489
1492
|
attachments: Array.isArray(meta.attachments) ? meta.attachments : [],
|
|
1490
1493
|
internal: isInternal,
|
|
1491
1494
|
ts: envelope?.msg?.ts || null,
|
|
1495
|
+
clientMessageId: meta.clientMessageId || null,
|
|
1492
1496
|
});
|
|
1493
1497
|
}
|
|
1494
1498
|
} catch { /* never crash WS pipeline */ }
|
|
@@ -3152,6 +3156,7 @@ export async function handleYeaftSessionSend(msg) {
|
|
|
3152
3156
|
mentions,
|
|
3153
3157
|
// Persisted form (no base64) — safe for jsonl-log.
|
|
3154
3158
|
attachments: persistedAttachments,
|
|
3159
|
+
clientMessageId: typeof msg.id === 'string' && msg.id ? msg.id : null,
|
|
3155
3160
|
},
|
|
3156
3161
|
// Live form — adapters need the base64 image blocks; runVpTurn
|
|
3157
3162
|
// reads `_promptParts` off the envelope rather than going
|
|
@@ -3691,6 +3696,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3691
3696
|
// would render the user's prompt N times.
|
|
3692
3697
|
userAlreadyPersisted: true,
|
|
3693
3698
|
threadId,
|
|
3699
|
+
vpTurnId: turnId,
|
|
3694
3700
|
drainPendingUserMessages: () => {
|
|
3695
3701
|
if (!thread || !Array.isArray(thread.pendingQueries) || thread.pendingQueries.length === 0) return [];
|
|
3696
3702
|
return thread.pendingQueries.splice(0);
|
|
@@ -3945,11 +3951,11 @@ function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistan
|
|
|
3945
3951
|
* refresh replay can render chips without leaking image source data into
|
|
3946
3952
|
* the message body.
|
|
3947
3953
|
*
|
|
3948
|
-
* @param {{ msgId:string, text:string, sessionId:string, role?:string, speakerVpId?:string|null, attachments?:Array<object>, internal?:boolean, ts?:string|null }} args
|
|
3954
|
+
* @param {{ msgId:string, text:string, sessionId:string, role?:string, speakerVpId?:string|null, attachments?:Array<object>, internal?:boolean, ts?:string|null, clientMessageId?:string|null }} args
|
|
3949
3955
|
* @returns {boolean} true if this call wrote the row, false if a prior
|
|
3950
3956
|
* call already wrote it (dedup hit).
|
|
3951
3957
|
*/
|
|
3952
|
-
function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = 'main', role, speakerVpId, attachments, internal = false, ts = null }) {
|
|
3958
|
+
function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = 'main', role, speakerVpId, attachments, internal = false, ts = null, clientMessageId = null }) {
|
|
3953
3959
|
if (!session?.conversationStore) return false;
|
|
3954
3960
|
// No msgId means no dedup key — caller is responsible for guarding.
|
|
3955
3961
|
// Both call sites already do (`if (envMsgId && text)` and
|
|
@@ -3993,6 +3999,9 @@ function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = '
|
|
|
3993
3999
|
threadId: threadId || 'main',
|
|
3994
4000
|
};
|
|
3995
4001
|
if (sessionId) record.sessionId = sessionId;
|
|
4002
|
+
if (persistRole === 'user' && clientMessageId && typeof clientMessageId === 'string') {
|
|
4003
|
+
record.clientMessageId = clientMessageId;
|
|
4004
|
+
}
|
|
3996
4005
|
// Stamp speakerVpId so the UI's loadHistory replay can route the row
|
|
3997
4006
|
// to the correct VP block. Only meaningful when role='assistant'; for
|
|
3998
4007
|
// a real user message we leave it unset (the UI's user track is
|
|
@@ -4818,6 +4827,110 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
4818
4827
|
sessionId ? store.loadRecentBySession(sessionId, lim) : store.loadRecent(lim);
|
|
4819
4828
|
let historyAlreadyReplayed = false;
|
|
4820
4829
|
|
|
4830
|
+
const replayHistoryFromStore = () => {
|
|
4831
|
+
// Delta path: caller knows the latest seq (or message id) it has cached
|
|
4832
|
+
// and wants only the messages that arrived after that cursor. Returns
|
|
4833
|
+
// mode:'delta' so the frontend can append+dedupe instead of replacing
|
|
4834
|
+
// the pane.
|
|
4835
|
+
const afterSeqRaw = (msg && Number.isFinite(msg.afterSeq)) ? msg.afterSeq : null;
|
|
4836
|
+
const afterMessageId = (msg && typeof msg.afterMessageId === 'string') ? msg.afterMessageId : null;
|
|
4837
|
+
let afterSeq = afterSeqRaw;
|
|
4838
|
+
if (afterSeq === null && afterMessageId && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
4839
|
+
afterSeq = session.conversationStore.getMessageSeqById(afterMessageId);
|
|
4840
|
+
}
|
|
4841
|
+
if (sessionId && afterSeq !== null && typeof session.conversationStore.loadAfterSeqByGroup === 'function') {
|
|
4842
|
+
const delta = session.conversationStore.loadAfterSeqByGroup(sessionId, afterSeq);
|
|
4843
|
+
const projectedMessages = emitHistoryChunk({
|
|
4844
|
+
sessionId,
|
|
4845
|
+
messages: delta.messages,
|
|
4846
|
+
mode: 'delta',
|
|
4847
|
+
latestSeq: delta.latestSeq,
|
|
4848
|
+
afterSeq,
|
|
4849
|
+
});
|
|
4850
|
+
sendSessionEvent({
|
|
4851
|
+
type: 'history_loaded',
|
|
4852
|
+
mode: 'delta',
|
|
4853
|
+
count: projectedMessages.length,
|
|
4854
|
+
sessionId,
|
|
4855
|
+
latestSeq: delta.latestSeq,
|
|
4856
|
+
afterSeq,
|
|
4857
|
+
});
|
|
4858
|
+
return;
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
// `msg.limit` is the replay-scrollback request from the frontend (UI
|
|
4862
|
+
// history pane, not engine context). Keep the bootstrap window small so
|
|
4863
|
+
// opening a group can paint the latest messages quickly; older rows are
|
|
4864
|
+
// paged via `yeaft_load_more_history` when the user scrolls upward.
|
|
4865
|
+
const limit = (typeof msg.limit === 'number') ? msg.limit : 10;
|
|
4866
|
+
const visiblePage = sessionId
|
|
4867
|
+
? loadVisibleGroupHistoryPage(session.conversationStore, sessionId, limit)
|
|
4868
|
+
: { messages: limit > 0 ? pickRecent(session.conversationStore, limit) : [], oldestSeq: null, hasMore: false };
|
|
4869
|
+
// Legacy compact.md is a non-group fallback only. For group replay, reading
|
|
4870
|
+
// it makes every group show "has compact" once any legacy/non-scoped compact
|
|
4871
|
+
// exists, even when this group has no scoped summary.
|
|
4872
|
+
const compactSummary = sessionId ? '' : session.conversationStore.readCompactSummary();
|
|
4873
|
+
const replayEntries = sessionId
|
|
4874
|
+
? visiblePage.messages
|
|
4875
|
+
: visiblePage.messages
|
|
4876
|
+
.map(projectPersistedToVisibleHistoryEntry)
|
|
4877
|
+
.filter(Boolean);
|
|
4878
|
+
|
|
4879
|
+
let latestSeq = null;
|
|
4880
|
+
if (replayEntries.length > 0 && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
4881
|
+
const last = replayEntries[replayEntries.length - 1];
|
|
4882
|
+
if (last && last.id) latestSeq = session.conversationStore.getMessageSeqById(last.id);
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
if (sessionId) {
|
|
4886
|
+
emitHistoryChunk({
|
|
4887
|
+
sessionId,
|
|
4888
|
+
messages: replayEntries,
|
|
4889
|
+
mode: 'recent',
|
|
4890
|
+
oldestSeq: visiblePage.oldestSeq,
|
|
4891
|
+
hasMore: visiblePage.hasMore,
|
|
4892
|
+
latestSeq,
|
|
4893
|
+
turns: limit,
|
|
4894
|
+
});
|
|
4895
|
+
} else {
|
|
4896
|
+
emitLegacyHistoryOutputFrames(replayEntries);
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// Compute the pagination cursor for the bootstrap load so the frontend
|
|
4900
|
+
// knows whether a "Load older messages" hint should be shown and where
|
|
4901
|
+
// to start the next page. For group history, this is computed from the
|
|
4902
|
+
// visible projected page, not raw persisted rows, so reflection/internal
|
|
4903
|
+
// tail rows cannot consume the bootstrap window or create false hasMore.
|
|
4904
|
+
let hasMore = false;
|
|
4905
|
+
let oldestSeq = null;
|
|
4906
|
+
if (sessionId) {
|
|
4907
|
+
hasMore = visiblePage.hasMore;
|
|
4908
|
+
oldestSeq = visiblePage.oldestSeq;
|
|
4909
|
+
}
|
|
4910
|
+
|
|
4911
|
+
// hasCompactSummary used to read a single session-global file, so it was
|
|
4912
|
+
// always true once ANY group/VP in the session had compacted. For group
|
|
4913
|
+
// replay, only scoped per-(group, vp) summaries count; legacy compact.md is
|
|
4914
|
+
// reserved for non-group / pre-scoped 1:1 callers.
|
|
4915
|
+
let hasCompactSummaryFlag = !!compactSummary;
|
|
4916
|
+
if (sessionId && typeof session.conversationStore.hasAnyCompactSummaryForSession === 'function') {
|
|
4917
|
+
hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForSession(sessionId);
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
sendSessionEvent({
|
|
4921
|
+
type: 'history_loaded',
|
|
4922
|
+
mode: 'recent',
|
|
4923
|
+
count: replayEntries.length,
|
|
4924
|
+
hasCompactSummary: hasCompactSummaryFlag,
|
|
4925
|
+
totalHot: session.conversationStore.countHot(),
|
|
4926
|
+
totalCold: session.conversationStore.countCold(),
|
|
4927
|
+
sessionId,
|
|
4928
|
+
hasMore,
|
|
4929
|
+
oldestSeq,
|
|
4930
|
+
latestSeq,
|
|
4931
|
+
});
|
|
4932
|
+
};
|
|
4933
|
+
|
|
4821
4934
|
if (!session) {
|
|
4822
4935
|
const yeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
4823
4936
|
const afterSeqRaw = (msg && Number.isFinite(msg.afterSeq)) ? msg.afterSeq : null;
|
|
@@ -4869,6 +4982,8 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
4869
4982
|
// hydration handles it.
|
|
4870
4983
|
if (sessionId) setGroupHistory(sessionId, hydrateGroupHistory(sessionId));
|
|
4871
4984
|
} else {
|
|
4985
|
+
replayHistoryFromStore();
|
|
4986
|
+
historyAlreadyReplayed = true;
|
|
4872
4987
|
refreshLiveSessionConfig();
|
|
4873
4988
|
hydrateYeaftStatusFromSession(session, { reason: 'history_load', emitEvent: true });
|
|
4874
4989
|
}
|
|
@@ -4908,107 +5023,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
4908
5023
|
|
|
4909
5024
|
if (historyAlreadyReplayed) return;
|
|
4910
5025
|
|
|
4911
|
-
|
|
4912
|
-
// and wants only the messages that arrived after that cursor. Returns
|
|
4913
|
-
// early with mode:'delta' so the frontend can append+dedupe instead of
|
|
4914
|
-
// replacing the pane.
|
|
4915
|
-
const afterSeqRaw = (msg && Number.isFinite(msg.afterSeq)) ? msg.afterSeq : null;
|
|
4916
|
-
const afterMessageId = (msg && typeof msg.afterMessageId === 'string') ? msg.afterMessageId : null;
|
|
4917
|
-
let afterSeq = afterSeqRaw;
|
|
4918
|
-
if (afterSeq === null && afterMessageId && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
4919
|
-
afterSeq = session.conversationStore.getMessageSeqById(afterMessageId);
|
|
4920
|
-
}
|
|
4921
|
-
if (sessionId && afterSeq !== null && typeof session.conversationStore.loadAfterSeqByGroup === 'function') {
|
|
4922
|
-
const delta = session.conversationStore.loadAfterSeqByGroup(sessionId, afterSeq);
|
|
4923
|
-
const projectedMessages = emitHistoryChunk({
|
|
4924
|
-
sessionId,
|
|
4925
|
-
messages: delta.messages,
|
|
4926
|
-
mode: 'delta',
|
|
4927
|
-
latestSeq: delta.latestSeq,
|
|
4928
|
-
afterSeq,
|
|
4929
|
-
});
|
|
4930
|
-
sendSessionEvent({
|
|
4931
|
-
type: 'history_loaded',
|
|
4932
|
-
mode: 'delta',
|
|
4933
|
-
count: projectedMessages.length,
|
|
4934
|
-
sessionId,
|
|
4935
|
-
latestSeq: delta.latestSeq,
|
|
4936
|
-
afterSeq,
|
|
4937
|
-
});
|
|
4938
|
-
return;
|
|
4939
|
-
}
|
|
4940
|
-
|
|
4941
|
-
// `msg.limit` is the replay-scrollback request from the frontend (UI
|
|
4942
|
-
// history pane, not engine context). Keep the bootstrap window small so
|
|
4943
|
-
// opening a group can paint the latest messages quickly; older rows are
|
|
4944
|
-
// paged via `yeaft_load_more_history` when the user scrolls upward.
|
|
4945
|
-
const limit = (typeof msg.limit === 'number') ? msg.limit : 10;
|
|
4946
|
-
const visiblePage = sessionId
|
|
4947
|
-
? loadVisibleGroupHistoryPage(session.conversationStore, sessionId, limit)
|
|
4948
|
-
: { messages: limit > 0 ? pickRecent(session.conversationStore, limit) : [], oldestSeq: null, hasMore: false };
|
|
4949
|
-
// Legacy compact.md is a non-group fallback only. For group replay, reading
|
|
4950
|
-
// it makes every group show "has compact" once any legacy/non-scoped compact
|
|
4951
|
-
// exists, even when this group has no scoped summary.
|
|
4952
|
-
const compactSummary = sessionId ? '' : session.conversationStore.readCompactSummary();
|
|
4953
|
-
const replayEntries = sessionId
|
|
4954
|
-
? visiblePage.messages
|
|
4955
|
-
: visiblePage.messages
|
|
4956
|
-
.map(projectPersistedToVisibleHistoryEntry)
|
|
4957
|
-
.filter(Boolean);
|
|
4958
|
-
|
|
4959
|
-
let latestSeq = null;
|
|
4960
|
-
if (replayEntries.length > 0 && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
4961
|
-
const last = replayEntries[replayEntries.length - 1];
|
|
4962
|
-
if (last && last.id) latestSeq = session.conversationStore.getMessageSeqById(last.id);
|
|
4963
|
-
}
|
|
4964
|
-
|
|
4965
|
-
if (sessionId) {
|
|
4966
|
-
emitHistoryChunk({
|
|
4967
|
-
sessionId,
|
|
4968
|
-
messages: replayEntries,
|
|
4969
|
-
mode: 'recent',
|
|
4970
|
-
oldestSeq: visiblePage.oldestSeq,
|
|
4971
|
-
hasMore: visiblePage.hasMore,
|
|
4972
|
-
latestSeq,
|
|
4973
|
-
turns: limit,
|
|
4974
|
-
});
|
|
4975
|
-
} else {
|
|
4976
|
-
emitLegacyHistoryOutputFrames(replayEntries);
|
|
4977
|
-
}
|
|
4978
|
-
|
|
4979
|
-
// Compute the pagination cursor for the bootstrap load so the frontend
|
|
4980
|
-
// knows whether a "Load older messages" hint should be shown and where
|
|
4981
|
-
// to start the next page. For group history, this is computed from the
|
|
4982
|
-
// visible projected page, not raw persisted rows, so reflection/internal
|
|
4983
|
-
// tail rows cannot consume the bootstrap window or create false hasMore.
|
|
4984
|
-
let hasMore = false;
|
|
4985
|
-
let oldestSeq = null;
|
|
4986
|
-
if (sessionId) {
|
|
4987
|
-
hasMore = visiblePage.hasMore;
|
|
4988
|
-
oldestSeq = visiblePage.oldestSeq;
|
|
4989
|
-
}
|
|
4990
|
-
|
|
4991
|
-
// hasCompactSummary used to read a single session-global file, so it was
|
|
4992
|
-
// always true once ANY group/VP in the session had compacted. For group
|
|
4993
|
-
// replay, only scoped per-(group, vp) summaries count; legacy compact.md is
|
|
4994
|
-
// reserved for non-group / pre-scoped 1:1 callers.
|
|
4995
|
-
let hasCompactSummaryFlag = !!compactSummary;
|
|
4996
|
-
if (sessionId && typeof session.conversationStore.hasAnyCompactSummaryForSession === 'function') {
|
|
4997
|
-
hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForSession(sessionId);
|
|
4998
|
-
}
|
|
4999
|
-
|
|
5000
|
-
sendSessionEvent({
|
|
5001
|
-
type: 'history_loaded',
|
|
5002
|
-
mode: 'recent',
|
|
5003
|
-
count: replayEntries.length,
|
|
5004
|
-
hasCompactSummary: hasCompactSummaryFlag,
|
|
5005
|
-
totalHot: session.conversationStore.countHot(),
|
|
5006
|
-
totalCold: session.conversationStore.countCold(),
|
|
5007
|
-
sessionId,
|
|
5008
|
-
hasMore,
|
|
5009
|
-
oldestSeq,
|
|
5010
|
-
latestSeq,
|
|
5011
|
-
});
|
|
5026
|
+
replayHistoryFromStore();
|
|
5012
5027
|
}
|
|
5013
5028
|
|
|
5014
5029
|
/**
|