@yeaft/webchat-agent 1.0.40 → 1.0.42
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/connection/upgrade.js +80 -19
- package/package.json +1 -1
- package/service/index.js +3 -0
- package/service.js +1 -0
- package/yeaft/web-bridge.js +107 -101
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/connection/upgrade.js
CHANGED
|
@@ -2,13 +2,24 @@ import { execFile, execFileSync, spawn } from 'child_process';
|
|
|
2
2
|
import { writeFileSync, mkdirSync, existsSync, cpSync } from 'fs';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { platform } from 'os';
|
|
5
|
+
import { platform, homedir } from 'os';
|
|
6
6
|
import ctx from '../context.js';
|
|
7
|
-
import { getConfigDir } from '../service.js';
|
|
7
|
+
import { getConfigDir, getServiceName, getPm2AppName, getLaunchdPlistPath, DEFAULT_INSTANCE_ID } from '../service.js';
|
|
8
8
|
import { sendToServer } from './buffer.js';
|
|
9
9
|
import { stopAgentHeartbeat } from './heartbeat.js';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the local service instance id for this running agent. Each agent
|
|
13
|
+
* process is pinned to one instance (set in index.js from
|
|
14
|
+
* YEAFT_AGENT_INSTANCE / config), so the upgrade flow must target THAT
|
|
15
|
+
* instance's service unit — not the bare `yeaft-agent` default. Without this,
|
|
16
|
+
* a named instance (e.g. `server-e7a9eb`) installs the new package but its
|
|
17
|
+
* systemd unit `yeaft-agent@server-e7a9eb` is never restarted, leaving the
|
|
18
|
+
* agent permanently offline after a UI-triggered upgrade.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveInstanceId() {
|
|
21
|
+
return ctx.CONFIG?.instanceId || process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID;
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
// Derive absolute paths for npm/pm2 from current node executable.
|
|
14
25
|
// In launchd/systemd environments, PATH may not include nvm/node dirs,
|
|
@@ -203,11 +214,12 @@ export async function handleUpgradeAgent() {
|
|
|
203
214
|
});
|
|
204
215
|
|
|
205
216
|
const isWindows = platform() === 'win32';
|
|
217
|
+
const instanceId = resolveInstanceId();
|
|
206
218
|
|
|
207
219
|
if (isWindows) {
|
|
208
|
-
spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion);
|
|
220
|
+
spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId);
|
|
209
221
|
} else {
|
|
210
|
-
spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion);
|
|
222
|
+
spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId);
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
// On PM2: delete the app BEFORE exiting so PM2 won't auto-restart the old version.
|
|
@@ -215,7 +227,7 @@ export async function handleUpgradeAgent() {
|
|
|
215
227
|
const isPm2 = !!process.env.pm_id;
|
|
216
228
|
if (isPm2) {
|
|
217
229
|
try {
|
|
218
|
-
execFileSync(pm2Path, ['delete',
|
|
230
|
+
execFileSync(pm2Path, ['delete', getPm2AppName(instanceId)], { stdio: 'pipe', env: safeEnv, ...shellOpt });
|
|
219
231
|
console.log(`[Agent] PM2 app deleted to prevent auto-restart during upgrade`);
|
|
220
232
|
} catch {
|
|
221
233
|
console.log(`[Agent] PM2 delete skipped (app may not be registered)`);
|
|
@@ -230,9 +242,9 @@ export async function handleUpgradeAgent() {
|
|
|
230
242
|
}
|
|
231
243
|
}
|
|
232
244
|
|
|
233
|
-
function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion) {
|
|
245
|
+
function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId = DEFAULT_INSTANCE_ID) {
|
|
234
246
|
const pid = process.pid;
|
|
235
|
-
const configDir = getConfigDir();
|
|
247
|
+
const configDir = getConfigDir(instanceId);
|
|
236
248
|
mkdirSync(configDir, { recursive: true });
|
|
237
249
|
const logDir = join(configDir, 'logs');
|
|
238
250
|
mkdirSync(logDir, { recursive: true });
|
|
@@ -355,13 +367,45 @@ function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestV
|
|
|
355
367
|
sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion, pendingRestart: true });
|
|
356
368
|
}
|
|
357
369
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Build the Unix (systemd/launchd) upgrade shell script as a string.
|
|
372
|
+
*
|
|
373
|
+
* Side-effect-free apart from reading the environment FS (`existsSync` to
|
|
374
|
+
* detect which service manager is installed) — and crucially it does NOT
|
|
375
|
+
* spawn, so the generated unit names can be asserted in unit tests. The
|
|
376
|
+
* service-manager target is resolved from `instanceId` via the shared helpers
|
|
377
|
+
* (`getServiceName` / `getLaunchdPlistPath`), so a named instance restarts
|
|
378
|
+
* `yeaft-agent@<id>` instead of the bare `yeaft-agent` default.
|
|
379
|
+
*
|
|
380
|
+
* @param {object} opts
|
|
381
|
+
* @param {string} opts.pkgName npm package name
|
|
382
|
+
* @param {string} opts.installDir install dir (local install) — used as cwd
|
|
383
|
+
* @param {boolean} opts.isGlobalInstall global vs local npm install
|
|
384
|
+
* @param {number} opts.pid pid of the exiting agent to wait on
|
|
385
|
+
* @param {string} opts.configDir instance config dir (holds logs/upgrade.log)
|
|
386
|
+
* @param {string} opts.npmPath absolute npm path
|
|
387
|
+
* @param {string} opts.safePath PATH with nodeBinDir prepended
|
|
388
|
+
* @param {string} opts.instanceId local service instance id
|
|
389
|
+
* @param {boolean} opts.isDarwin whether the platform is macOS
|
|
390
|
+
* @returns {string} the upgrade.sh contents
|
|
391
|
+
*/
|
|
392
|
+
export function buildUnixUpgradeScript({
|
|
393
|
+
pkgName,
|
|
394
|
+
installDir,
|
|
395
|
+
isGlobalInstall,
|
|
396
|
+
pid,
|
|
397
|
+
configDir,
|
|
398
|
+
npmPath,
|
|
399
|
+
safePath,
|
|
400
|
+
instanceId = DEFAULT_INSTANCE_ID,
|
|
401
|
+
isDarwin = platform() === 'darwin',
|
|
402
|
+
}) {
|
|
362
403
|
const shPath = join(configDir, 'upgrade.sh');
|
|
363
|
-
const
|
|
364
|
-
const
|
|
404
|
+
const serviceName = getServiceName(instanceId);
|
|
405
|
+
const systemdUnitPath = join(homedir(), '.config', 'systemd', 'user', `${serviceName}.service`);
|
|
406
|
+
const isSystemd = existsSync(systemdUnitPath);
|
|
407
|
+
const plistPath = getLaunchdPlistPath(instanceId);
|
|
408
|
+
const isLaunchd = isDarwin && existsSync(plistPath);
|
|
365
409
|
const cwd = isGlobalInstall ? undefined : installDir;
|
|
366
410
|
|
|
367
411
|
const shLines = [
|
|
@@ -395,12 +439,11 @@ function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVers
|
|
|
395
439
|
if (isSystemd) {
|
|
396
440
|
shLines.push(
|
|
397
441
|
'# Stop systemd service to prevent restart loop',
|
|
398
|
-
|
|
442
|
+
`systemctl --user stop "${serviceName}" 2>/dev/null`,
|
|
399
443
|
'sleep 1',
|
|
400
444
|
'',
|
|
401
445
|
);
|
|
402
446
|
} else if (isLaunchd) {
|
|
403
|
-
const plistPath = join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.yeaft.agent.plist');
|
|
404
447
|
shLines.push(
|
|
405
448
|
'# Unload launchd service to prevent restart loop',
|
|
406
449
|
`launchctl unload "${plistPath}" 2>/dev/null`,
|
|
@@ -430,11 +473,10 @@ function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVers
|
|
|
430
473
|
if (isSystemd) {
|
|
431
474
|
shLines.push(
|
|
432
475
|
'# Restart systemd service',
|
|
433
|
-
|
|
476
|
+
`systemctl --user start "${serviceName}"`,
|
|
434
477
|
'echo "[Upgrade] Service restarted via systemd"',
|
|
435
478
|
);
|
|
436
479
|
} else if (isLaunchd) {
|
|
437
|
-
const plistPath = join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.yeaft.agent.plist');
|
|
438
480
|
shLines.push(
|
|
439
481
|
'# Reload launchd service',
|
|
440
482
|
`launchctl load "${plistPath}"`,
|
|
@@ -445,7 +487,26 @@ function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVers
|
|
|
445
487
|
// 清理脚本自身
|
|
446
488
|
shLines.push('', `rm -f "${shPath}"`);
|
|
447
489
|
|
|
448
|
-
|
|
490
|
+
return shLines.join('\n');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion, instanceId = DEFAULT_INSTANCE_ID) {
|
|
494
|
+
const configDir = getConfigDir(instanceId);
|
|
495
|
+
mkdirSync(configDir, { recursive: true });
|
|
496
|
+
const shPath = join(configDir, 'upgrade.sh');
|
|
497
|
+
|
|
498
|
+
const script = buildUnixUpgradeScript({
|
|
499
|
+
pkgName,
|
|
500
|
+
installDir,
|
|
501
|
+
isGlobalInstall,
|
|
502
|
+
pid: process.pid,
|
|
503
|
+
configDir,
|
|
504
|
+
npmPath,
|
|
505
|
+
safePath,
|
|
506
|
+
instanceId,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
writeFileSync(shPath, script, { mode: 0o755 });
|
|
449
510
|
const child = spawn('bash', [shPath], {
|
|
450
511
|
detached: true,
|
|
451
512
|
stdio: 'ignore',
|
package/package.json
CHANGED
package/service/index.js
CHANGED
|
@@ -34,6 +34,9 @@ export {
|
|
|
34
34
|
getLaunchdLabel,
|
|
35
35
|
getDefaultYeaftDir,
|
|
36
36
|
} from './config.js';
|
|
37
|
+
// Re-export the launchd plist-path resolver so consumers (e.g. the upgrade
|
|
38
|
+
// flow) reach it via the service barrel instead of deep-importing macos.js.
|
|
39
|
+
export { getLaunchdPlistPath } from './macos.js';
|
|
37
40
|
|
|
38
41
|
const os = platform();
|
|
39
42
|
|
package/service.js
CHANGED
package/yeaft/web-bridge.js
CHANGED
|
@@ -4827,6 +4827,110 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
4827
4827
|
sessionId ? store.loadRecentBySession(sessionId, lim) : store.loadRecent(lim);
|
|
4828
4828
|
let historyAlreadyReplayed = false;
|
|
4829
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
|
+
|
|
4830
4934
|
if (!session) {
|
|
4831
4935
|
const yeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
4832
4936
|
const afterSeqRaw = (msg && Number.isFinite(msg.afterSeq)) ? msg.afterSeq : null;
|
|
@@ -4878,6 +4982,8 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
4878
4982
|
// hydration handles it.
|
|
4879
4983
|
if (sessionId) setGroupHistory(sessionId, hydrateGroupHistory(sessionId));
|
|
4880
4984
|
} else {
|
|
4985
|
+
replayHistoryFromStore();
|
|
4986
|
+
historyAlreadyReplayed = true;
|
|
4881
4987
|
refreshLiveSessionConfig();
|
|
4882
4988
|
hydrateYeaftStatusFromSession(session, { reason: 'history_load', emitEvent: true });
|
|
4883
4989
|
}
|
|
@@ -4917,107 +5023,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
4917
5023
|
|
|
4918
5024
|
if (historyAlreadyReplayed) return;
|
|
4919
5025
|
|
|
4920
|
-
|
|
4921
|
-
// and wants only the messages that arrived after that cursor. Returns
|
|
4922
|
-
// early with mode:'delta' so the frontend can append+dedupe instead of
|
|
4923
|
-
// replacing the pane.
|
|
4924
|
-
const afterSeqRaw = (msg && Number.isFinite(msg.afterSeq)) ? msg.afterSeq : null;
|
|
4925
|
-
const afterMessageId = (msg && typeof msg.afterMessageId === 'string') ? msg.afterMessageId : null;
|
|
4926
|
-
let afterSeq = afterSeqRaw;
|
|
4927
|
-
if (afterSeq === null && afterMessageId && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
4928
|
-
afterSeq = session.conversationStore.getMessageSeqById(afterMessageId);
|
|
4929
|
-
}
|
|
4930
|
-
if (sessionId && afterSeq !== null && typeof session.conversationStore.loadAfterSeqByGroup === 'function') {
|
|
4931
|
-
const delta = session.conversationStore.loadAfterSeqByGroup(sessionId, afterSeq);
|
|
4932
|
-
const projectedMessages = emitHistoryChunk({
|
|
4933
|
-
sessionId,
|
|
4934
|
-
messages: delta.messages,
|
|
4935
|
-
mode: 'delta',
|
|
4936
|
-
latestSeq: delta.latestSeq,
|
|
4937
|
-
afterSeq,
|
|
4938
|
-
});
|
|
4939
|
-
sendSessionEvent({
|
|
4940
|
-
type: 'history_loaded',
|
|
4941
|
-
mode: 'delta',
|
|
4942
|
-
count: projectedMessages.length,
|
|
4943
|
-
sessionId,
|
|
4944
|
-
latestSeq: delta.latestSeq,
|
|
4945
|
-
afterSeq,
|
|
4946
|
-
});
|
|
4947
|
-
return;
|
|
4948
|
-
}
|
|
4949
|
-
|
|
4950
|
-
// `msg.limit` is the replay-scrollback request from the frontend (UI
|
|
4951
|
-
// history pane, not engine context). Keep the bootstrap window small so
|
|
4952
|
-
// opening a group can paint the latest messages quickly; older rows are
|
|
4953
|
-
// paged via `yeaft_load_more_history` when the user scrolls upward.
|
|
4954
|
-
const limit = (typeof msg.limit === 'number') ? msg.limit : 10;
|
|
4955
|
-
const visiblePage = sessionId
|
|
4956
|
-
? loadVisibleGroupHistoryPage(session.conversationStore, sessionId, limit)
|
|
4957
|
-
: { messages: limit > 0 ? pickRecent(session.conversationStore, limit) : [], oldestSeq: null, hasMore: false };
|
|
4958
|
-
// Legacy compact.md is a non-group fallback only. For group replay, reading
|
|
4959
|
-
// it makes every group show "has compact" once any legacy/non-scoped compact
|
|
4960
|
-
// exists, even when this group has no scoped summary.
|
|
4961
|
-
const compactSummary = sessionId ? '' : session.conversationStore.readCompactSummary();
|
|
4962
|
-
const replayEntries = sessionId
|
|
4963
|
-
? visiblePage.messages
|
|
4964
|
-
: visiblePage.messages
|
|
4965
|
-
.map(projectPersistedToVisibleHistoryEntry)
|
|
4966
|
-
.filter(Boolean);
|
|
4967
|
-
|
|
4968
|
-
let latestSeq = null;
|
|
4969
|
-
if (replayEntries.length > 0 && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
4970
|
-
const last = replayEntries[replayEntries.length - 1];
|
|
4971
|
-
if (last && last.id) latestSeq = session.conversationStore.getMessageSeqById(last.id);
|
|
4972
|
-
}
|
|
4973
|
-
|
|
4974
|
-
if (sessionId) {
|
|
4975
|
-
emitHistoryChunk({
|
|
4976
|
-
sessionId,
|
|
4977
|
-
messages: replayEntries,
|
|
4978
|
-
mode: 'recent',
|
|
4979
|
-
oldestSeq: visiblePage.oldestSeq,
|
|
4980
|
-
hasMore: visiblePage.hasMore,
|
|
4981
|
-
latestSeq,
|
|
4982
|
-
turns: limit,
|
|
4983
|
-
});
|
|
4984
|
-
} else {
|
|
4985
|
-
emitLegacyHistoryOutputFrames(replayEntries);
|
|
4986
|
-
}
|
|
4987
|
-
|
|
4988
|
-
// Compute the pagination cursor for the bootstrap load so the frontend
|
|
4989
|
-
// knows whether a "Load older messages" hint should be shown and where
|
|
4990
|
-
// to start the next page. For group history, this is computed from the
|
|
4991
|
-
// visible projected page, not raw persisted rows, so reflection/internal
|
|
4992
|
-
// tail rows cannot consume the bootstrap window or create false hasMore.
|
|
4993
|
-
let hasMore = false;
|
|
4994
|
-
let oldestSeq = null;
|
|
4995
|
-
if (sessionId) {
|
|
4996
|
-
hasMore = visiblePage.hasMore;
|
|
4997
|
-
oldestSeq = visiblePage.oldestSeq;
|
|
4998
|
-
}
|
|
4999
|
-
|
|
5000
|
-
// hasCompactSummary used to read a single session-global file, so it was
|
|
5001
|
-
// always true once ANY group/VP in the session had compacted. For group
|
|
5002
|
-
// replay, only scoped per-(group, vp) summaries count; legacy compact.md is
|
|
5003
|
-
// reserved for non-group / pre-scoped 1:1 callers.
|
|
5004
|
-
let hasCompactSummaryFlag = !!compactSummary;
|
|
5005
|
-
if (sessionId && typeof session.conversationStore.hasAnyCompactSummaryForSession === 'function') {
|
|
5006
|
-
hasCompactSummaryFlag = session.conversationStore.hasAnyCompactSummaryForSession(sessionId);
|
|
5007
|
-
}
|
|
5008
|
-
|
|
5009
|
-
sendSessionEvent({
|
|
5010
|
-
type: 'history_loaded',
|
|
5011
|
-
mode: 'recent',
|
|
5012
|
-
count: replayEntries.length,
|
|
5013
|
-
hasCompactSummary: hasCompactSummaryFlag,
|
|
5014
|
-
totalHot: session.conversationStore.countHot(),
|
|
5015
|
-
totalCold: session.conversationStore.countCold(),
|
|
5016
|
-
sessionId,
|
|
5017
|
-
hasMore,
|
|
5018
|
-
oldestSeq,
|
|
5019
|
-
latestSeq,
|
|
5020
|
-
});
|
|
5026
|
+
replayHistoryFromStore();
|
|
5021
5027
|
}
|
|
5022
5028
|
|
|
5023
5029
|
/**
|