@yeaft/webchat-agent 0.1.733 → 0.1.735
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.
|
@@ -36,7 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
|
36
36
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
37
37
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
38
38
|
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
|
|
39
|
-
import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyUpdateGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger, broadcastLanguageChange } from '../unify/web-bridge.js';
|
|
39
|
+
import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyLoadMoreHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyUpdateGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger, broadcastLanguageChange } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -403,6 +403,10 @@ export async function handleMessage(msg) {
|
|
|
403
403
|
await handleUnifyLoadHistory(msg);
|
|
404
404
|
break;
|
|
405
405
|
|
|
406
|
+
case 'unify_load_more_history':
|
|
407
|
+
await handleUnifyLoadMoreHistory(msg);
|
|
408
|
+
break;
|
|
409
|
+
|
|
406
410
|
case 'unify_mode_switch':
|
|
407
411
|
handleUnifyModeSwitch(msg);
|
|
408
412
|
break;
|
package/package.json
CHANGED
|
@@ -55,6 +55,20 @@ export function estimateTokens(text) {
|
|
|
55
55
|
return Math.ceil(text.length / 4);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Parse the global monotonic sequence number out of a message id of the
|
|
60
|
+
* form `m####`. Returns NaN for malformed ids. Used by the pagination
|
|
61
|
+
* cursor (`loadOlderByGroup`) to compare ids numerically without having
|
|
62
|
+
* to trust file-system sort order.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} id
|
|
65
|
+
* @returns {number}
|
|
66
|
+
*/
|
|
67
|
+
export function parseSeqFromId(id) {
|
|
68
|
+
const m = String(id || '').match(/^m(\d+)$/);
|
|
69
|
+
return m ? parseInt(m[1], 10) : NaN;
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
// ─── Frontmatter helpers ─────────────────────────────────────
|
|
59
73
|
|
|
60
74
|
/**
|
|
@@ -527,6 +541,66 @@ export class ConversationStore {
|
|
|
527
541
|
return this.loadRecentByGroup(groupId, Infinity);
|
|
528
542
|
}
|
|
529
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Pagination-cursor read: load the page of `turnsLimit` TURNS that ends
|
|
546
|
+
* just before `beforeSeq` (exclusive) for the given `groupId`. Used by
|
|
547
|
+
* the Unify "Load older messages" UI to walk backwards through history
|
|
548
|
+
* one click at a time.
|
|
549
|
+
*
|
|
550
|
+
* Crucially, this scans BOTH hot (`messages/`) and cold (`cold/`) dirs
|
|
551
|
+
* — `#getNextSeq` is global across both, and `moveToCold` is a `rename`
|
|
552
|
+
* that never reseqs, so cold ids are strictly < hot ids and a flat
|
|
553
|
+
* `[...cold, ...hot]` concat is already chronological. Crossing the
|
|
554
|
+
* hot→cold boundary is therefore transparent to the caller.
|
|
555
|
+
*
|
|
556
|
+
* `hasMore` is computed in TURNS (not raw message count). It's true iff
|
|
557
|
+
* the slice we returned still leaves an earlier turn boundary unread in
|
|
558
|
+
* the filtered prefix — i.e. there's at least one more page to fetch.
|
|
559
|
+
*
|
|
560
|
+
* `pairSanitize` runs as a defensive secondary pass. Turn-boundary cuts
|
|
561
|
+
* are already pair-safe, but historical / hand-edited stores may
|
|
562
|
+
* contain orphan tool_use/tool_result pairs.
|
|
563
|
+
*
|
|
564
|
+
* @param {string} groupId — required; null/empty returns empty result
|
|
565
|
+
* @param {number|null} beforeSeq — exclusive upper bound on message
|
|
566
|
+
* sequence id. Special cases:
|
|
567
|
+
* - `null` / `undefined` / non-finite (e.g. `Infinity`, `NaN`) → start
|
|
568
|
+
* from the newest (no upper bound).
|
|
569
|
+
* - `0` is a VALID finite cutoff that excludes everything (since seqs
|
|
570
|
+
* start at 1). Distinct from `null`. A caller writing
|
|
571
|
+
* `loadOlderByGroup(g, store.firstSeq || 0, ...)` will silently get
|
|
572
|
+
* an empty page — pass `null` if you mean "from newest".
|
|
573
|
+
* @param {number} [turnsLimit=DEFAULT_RECENT_TURNS] — max turns per page
|
|
574
|
+
* @returns {{ messages: object[], oldestSeq: number|null, hasMore: boolean }}
|
|
575
|
+
*/
|
|
576
|
+
loadOlderByGroup(groupId, beforeSeq, turnsLimit = DEFAULT_RECENT_TURNS) {
|
|
577
|
+
if (!groupId) return { messages: [], oldestSeq: null, hasMore: false };
|
|
578
|
+
const hot = this.#loadFromDir(this.#msgDir, Infinity);
|
|
579
|
+
const cold = this.#loadFromDir(this.#coldDir, Infinity);
|
|
580
|
+
// Cold ids strictly < hot ids by construction → chronological concat.
|
|
581
|
+
const all = [...cold, ...hot];
|
|
582
|
+
const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
|
|
583
|
+
const prefix = all.filter(m => m && m.groupId === groupId
|
|
584
|
+
&& parseSeqFromId(m.id) < cutoff);
|
|
585
|
+
if (prefix.length === 0) return { messages: [], oldestSeq: null, hasMore: false };
|
|
586
|
+
const sliced = pairSanitize(sliceLastNTurns(prefix, turnsLimit));
|
|
587
|
+
// Turn-based hasMore: there's an EARLIER turn boundary we didn't keep.
|
|
588
|
+
// Compare seqs (not object identity) — pairSanitize / sliceLastNTurns
|
|
589
|
+
// return references today, but a future normalization pass that clones
|
|
590
|
+
// rows would silently flip identity-compare to always-true.
|
|
591
|
+
const oldestSlicedSeq = sliced.length ? parseSeqFromId(sliced[0].id) : NaN;
|
|
592
|
+
const oldestPrefixSeq = parseSeqFromId(prefix[0].id);
|
|
593
|
+
const hasMore = sliced.length > 0
|
|
594
|
+
&& Number.isFinite(oldestSlicedSeq)
|
|
595
|
+
&& Number.isFinite(oldestPrefixSeq)
|
|
596
|
+
&& oldestSlicedSeq > oldestPrefixSeq;
|
|
597
|
+
// Defend the cursor at the source: a malformed id surfaces as NaN here
|
|
598
|
+
// and a NaN cursor would round-trip back as a poison `beforeSeq` that
|
|
599
|
+
// degrades to "give me the newest page again".
|
|
600
|
+
const oldestSeq = Number.isFinite(oldestSlicedSeq) ? oldestSlicedSeq : null;
|
|
601
|
+
return { messages: sliced, oldestSeq, hasMore };
|
|
602
|
+
}
|
|
603
|
+
|
|
530
604
|
/**
|
|
531
605
|
* Count hot messages.
|
|
532
606
|
*
|
package/unify/web-bridge.js
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
import { createFeatureArc } from './feature-arc.js';
|
|
54
54
|
import { getFeatureStore } from './tools/feature-tools.js';
|
|
55
55
|
import { persistUnifyAttachments, attachmentsForPersistence } from './attachments.js';
|
|
56
|
+
import { parseSeqFromId } from './conversation/persist.js';
|
|
56
57
|
|
|
57
58
|
/** @type {import('./session.js').Session | null} */
|
|
58
59
|
let session = null;
|
|
@@ -2594,6 +2595,32 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
2594
2595
|
}
|
|
2595
2596
|
}
|
|
2596
2597
|
|
|
2598
|
+
// Compute the pagination cursor for the bootstrap load so the frontend
|
|
2599
|
+
// knows whether a "Load older messages" hint should be shown and where
|
|
2600
|
+
// to start the next page. The cursor is the seq of the oldest replayed
|
|
2601
|
+
// message; `hasMore` is true iff there's an earlier message in the
|
|
2602
|
+
// group that we did NOT replay.
|
|
2603
|
+
let hasMore = false;
|
|
2604
|
+
let oldestSeq = null;
|
|
2605
|
+
if (groupId && messages.length > 0) {
|
|
2606
|
+
const firstId = messages[0].id;
|
|
2607
|
+
const seq = parseSeqFromId(firstId);
|
|
2608
|
+
// Defend against malformed ids: a NaN cursor would round-trip back as
|
|
2609
|
+
// a poison `beforeSeq` and degrade subsequent paginations to "give me
|
|
2610
|
+
// the newest page again". Surface as null instead.
|
|
2611
|
+
oldestSeq = Number.isFinite(seq) ? seq : null;
|
|
2612
|
+
if (oldestSeq != null) {
|
|
2613
|
+
// Consult the store for whether anything older exists in the same
|
|
2614
|
+
// group. Cheap: a single extra `loadOlderByGroup` with turns=1.
|
|
2615
|
+
try {
|
|
2616
|
+
const probe = session.conversationStore.loadOlderByGroup(groupId, oldestSeq, 1);
|
|
2617
|
+
hasMore = probe.messages.length > 0;
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
console.error('[Unify] history-load probe failed:', err.message);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2597
2624
|
sendUnifyEvent({
|
|
2598
2625
|
type: 'history_loaded',
|
|
2599
2626
|
count: messages.length,
|
|
@@ -2601,6 +2628,71 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
2601
2628
|
totalHot: session.conversationStore.countHot(),
|
|
2602
2629
|
totalCold: session.conversationStore.countCold(),
|
|
2603
2630
|
groupId,
|
|
2631
|
+
hasMore,
|
|
2632
|
+
oldestSeq,
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
/**
|
|
2637
|
+
* Handle a "load older messages" pagination request. Reads `turns` more
|
|
2638
|
+
* turns of history strictly older than `beforeSeq` for `groupId`, and
|
|
2639
|
+
* emits them in a single `unify_history_chunk` envelope (NOT a
|
|
2640
|
+
* `unify_output` — that pipeline appends, but the frontend needs to
|
|
2641
|
+
* PREPEND these older messages above what it already has).
|
|
2642
|
+
*
|
|
2643
|
+
* Tool replay is NOT included in this PR — same projection as
|
|
2644
|
+
* `handleUnifyLoadHistory` (user / assistant text only). On any internal
|
|
2645
|
+
* failure we still emit an empty chunk so the spinner clears.
|
|
2646
|
+
*
|
|
2647
|
+
* @param {object} msg — { groupId, beforeSeq, turns }
|
|
2648
|
+
*/
|
|
2649
|
+
export async function handleUnifyLoadMoreHistory(msg) {
|
|
2650
|
+
const groupId = (msg && typeof msg.groupId === 'string' && msg.groupId) || null;
|
|
2651
|
+
const emit = (payload) => sendToServer({
|
|
2652
|
+
type: 'unify_history_chunk',
|
|
2653
|
+
conversationId: unifyConversationId,
|
|
2654
|
+
groupId,
|
|
2655
|
+
...payload,
|
|
2656
|
+
});
|
|
2657
|
+
|
|
2658
|
+
if (!session || !groupId) {
|
|
2659
|
+
emit({ messages: [], oldestSeq: null, hasMore: false });
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
const beforeSeq = (typeof msg.beforeSeq === 'number') ? msg.beforeSeq : null;
|
|
2664
|
+
const turns = (typeof msg.turns === 'number' && msg.turns > 0) ? msg.turns : 20;
|
|
2665
|
+
|
|
2666
|
+
let result;
|
|
2667
|
+
try {
|
|
2668
|
+
result = session.conversationStore.loadOlderByGroup(groupId, beforeSeq, turns);
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
console.error('[Unify] loadOlderByGroup failed:', err.message);
|
|
2671
|
+
result = { messages: [], oldestSeq: null, hasMore: false };
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Wire shape mirrors handleUnifyLoadHistory's projection: only user /
|
|
2675
|
+
// assistant text rows. Tool_use / tool_result replay is out of scope
|
|
2676
|
+
// for this PR (today's bootstrap path drops them too).
|
|
2677
|
+
//
|
|
2678
|
+
// We intentionally do NOT ship `id` or `time` over the wire:
|
|
2679
|
+
// `handleUnifyHistoryChunk` reads only role / content / groupId. The
|
|
2680
|
+
// pagination cursor (`oldestSeq`) is sent once at the envelope level,
|
|
2681
|
+
// so per-row ids are dead weight. `time` would be useful if we render
|
|
2682
|
+
// "5 days ago" stamps on older history rows — when that ships, add it
|
|
2683
|
+
// back here and consume it in conversationHandler.
|
|
2684
|
+
const projected = (result.messages || [])
|
|
2685
|
+
.filter(m => m && (m.role === 'user' || m.role === 'assistant'))
|
|
2686
|
+
.map(m => ({
|
|
2687
|
+
role: m.role,
|
|
2688
|
+
content: m.content,
|
|
2689
|
+
groupId: m.groupId || null,
|
|
2690
|
+
}));
|
|
2691
|
+
|
|
2692
|
+
emit({
|
|
2693
|
+
messages: projected,
|
|
2694
|
+
oldestSeq: result.oldestSeq,
|
|
2695
|
+
hasMore: !!result.hasMore,
|
|
2604
2696
|
});
|
|
2605
2697
|
}
|
|
2606
2698
|
|