@yeaft/webchat-agent 1.0.364 → 1.0.366
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/local-runtime/server/handlers/agent-output.js +3 -0
- package/local-runtime/server/handlers/client-conversation.js +4 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +93 -93
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +223 -14
- package/yeaft/web-bridge.js +71 -7
|
Binary file
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, renameSync, unlinkSync, statSync, appendFileSync } from 'fs';
|
|
21
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
21
22
|
import { join, basename } from 'path';
|
|
22
23
|
import { isPermissionError } from '../init.js';
|
|
23
24
|
import { writeAtomic } from '../storage/atomic.js';
|
|
@@ -71,6 +72,7 @@ const DELTA_TOOL_PAIR_EXTENSION_CAP = 500;
|
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
const SEGMENT_INDEX_FILE = 'index.json';
|
|
75
|
+
const SEGMENT_LINEAGE_FILE = 'lineage.json';
|
|
74
76
|
const SEGMENT_DIR = 'segments';
|
|
75
77
|
const SEGMENT_TARGET_BYTES = 1024 * 1024;
|
|
76
78
|
const SEGMENT_FIRST_NAME = '000001.jsonl';
|
|
@@ -98,7 +100,9 @@ function projectAssistantToolsForVisibleHistory(message) {
|
|
|
98
100
|
|
|
99
101
|
function emptySegmentIndex() {
|
|
100
102
|
return {
|
|
101
|
-
version:
|
|
103
|
+
version: 2,
|
|
104
|
+
streamId: null,
|
|
105
|
+
revision: 0,
|
|
102
106
|
nextSeq: 1,
|
|
103
107
|
totalMessages: 0,
|
|
104
108
|
lastMessageId: null,
|
|
@@ -700,7 +704,9 @@ class SegmentStore {
|
|
|
700
704
|
this.rootDir = rootDir;
|
|
701
705
|
this.segmentDir = join(rootDir, SEGMENT_DIR);
|
|
702
706
|
this.indexPath = join(rootDir, SEGMENT_INDEX_FILE);
|
|
707
|
+
this.lineagePath = join(rootDir, SEGMENT_LINEAGE_FILE);
|
|
703
708
|
this.index = null;
|
|
709
|
+
this.lineage = null;
|
|
704
710
|
}
|
|
705
711
|
|
|
706
712
|
ensure() {
|
|
@@ -727,8 +733,18 @@ class SegmentStore {
|
|
|
727
733
|
}
|
|
728
734
|
}
|
|
729
735
|
const indexWasStale = idx && !this.#indexMatchesDisk(idx);
|
|
730
|
-
|
|
731
|
-
|
|
736
|
+
const normalizedSource = !idx || indexWasStale ? this.#rebuildIndex() : idx;
|
|
737
|
+
this.index = this.#normalizeIndex(normalizedSource);
|
|
738
|
+
this.lineage = this.#resolveLineage(this.index, {
|
|
739
|
+
verifyAnchor: !idx || !normalizedSource?.streamId,
|
|
740
|
+
});
|
|
741
|
+
this.index.streamId = this.lineage.streamId;
|
|
742
|
+
this.index.revision = this.lineage.revision;
|
|
743
|
+
const needsMetadataUpgrade = !normalizedSource?.streamId
|
|
744
|
+
|| normalizedSource.streamId !== this.lineage.streamId
|
|
745
|
+
|| !Number.isFinite(Number(normalizedSource?.revision))
|
|
746
|
+
|| Number(normalizedSource.revision) !== this.lineage.revision;
|
|
747
|
+
if ((!existsSync(this.indexPath) || indexWasStale || needsMetadataUpgrade) && this.hasData()) this.saveIndex();
|
|
732
748
|
return this.index;
|
|
733
749
|
}
|
|
734
750
|
|
|
@@ -737,9 +753,20 @@ class SegmentStore {
|
|
|
737
753
|
writeFileSync(this.indexPath, `${JSON.stringify(this.index || emptySegmentIndex(), null, 2)}\n`, { encoding: 'utf8', mode: 0o644 });
|
|
738
754
|
}
|
|
739
755
|
|
|
756
|
+
metadata() {
|
|
757
|
+
const idx = this.loadIndex();
|
|
758
|
+
return {
|
|
759
|
+
streamId: idx.streamId,
|
|
760
|
+
revision: Number(idx.revision) || 0,
|
|
761
|
+
headSeq: Math.max(0, (Number(idx.nextSeq) || 1) - 1),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
740
765
|
append(msg) {
|
|
741
766
|
this.ensure();
|
|
742
767
|
const idx = this.loadIndex();
|
|
768
|
+
const establishesAnchor = (Number(idx.totalMessages) || 0) === 0
|
|
769
|
+
&& (!Array.isArray(idx.segments) || idx.segments.length === 0);
|
|
743
770
|
let segment = idx.segments[idx.segments.length - 1] || null;
|
|
744
771
|
let active = segment?.file || idx.activeSegment || SEGMENT_FIRST_NAME;
|
|
745
772
|
let activePath = join(this.segmentDir, active);
|
|
@@ -765,12 +792,24 @@ class SegmentStore {
|
|
|
765
792
|
idx.totalMessages = (idx.totalMessages || 0) + 1;
|
|
766
793
|
idx.lastMessageId = msg.id || null;
|
|
767
794
|
idx.nextSeq = Math.max(Number(idx.nextSeq) || 1, seq + 1);
|
|
795
|
+
const invalidatesEarlierRows = msg._reflection
|
|
796
|
+
|| (Array.isArray(msg.foldedMessageIds) && msg.foldedMessageIds.length > 0);
|
|
797
|
+
// Ordinary appends are recoverable through afterSeq. Only an append that
|
|
798
|
+
// invalidates older visible rows (fold reflection/tombstones) advances the
|
|
799
|
+
// mutation revision and forces the browser to rebuild its snapshot.
|
|
800
|
+
if (invalidatesEarlierRows) {
|
|
801
|
+
idx.revision = (Number(idx.revision) || 0) + 1;
|
|
802
|
+
}
|
|
768
803
|
if (msg._reflection && Array.isArray(msg.foldedMessageIds)) {
|
|
769
804
|
idx.foldedMessageIds = Array.from(new Set([
|
|
770
805
|
...(Array.isArray(idx.foldedMessageIds) ? idx.foldedMessageIds : []),
|
|
771
806
|
...msg.foldedMessageIds.filter(id => typeof id === 'string' && id),
|
|
772
807
|
]));
|
|
773
808
|
}
|
|
809
|
+
this.#persistCurrentLineage({
|
|
810
|
+
revision: idx.revision,
|
|
811
|
+
anchor: establishesAnchor ? this.#lineageAnchorForLine(line) : undefined,
|
|
812
|
+
});
|
|
774
813
|
this.saveIndex();
|
|
775
814
|
}
|
|
776
815
|
|
|
@@ -846,10 +885,16 @@ class SegmentStore {
|
|
|
846
885
|
if (!next || typeof next !== 'object') return null;
|
|
847
886
|
const updated = { ...next, id: rows[rowIndex].id };
|
|
848
887
|
rows[rowIndex] = updated;
|
|
888
|
+
const updatesAnchor = rowIndex === 0 && segment.file === idx.segments[0]?.file;
|
|
849
889
|
|
|
850
890
|
const body = rows.map(row => JSON.stringify(row)).join('\n') + (rows.length > 0 ? '\n' : '');
|
|
851
891
|
writeAtomic(path, body);
|
|
852
892
|
segment.bytes = Buffer.byteLength(body);
|
|
893
|
+
idx.revision = (Number(idx.revision) || 0) + 1;
|
|
894
|
+
this.#persistCurrentLineage({
|
|
895
|
+
revision: idx.revision,
|
|
896
|
+
anchor: updatesAnchor ? this.#lineageAnchorForLine(JSON.stringify(updated)) : undefined,
|
|
897
|
+
});
|
|
853
898
|
this.saveIndex();
|
|
854
899
|
return updated;
|
|
855
900
|
}
|
|
@@ -865,7 +910,10 @@ class SegmentStore {
|
|
|
865
910
|
}
|
|
866
911
|
}
|
|
867
912
|
if (existsSync(this.indexPath)) unlinkSync(this.indexPath);
|
|
868
|
-
this.
|
|
913
|
+
this.lineage = { streamId: randomUUID(), revision: 0, anchor: null };
|
|
914
|
+
this.#writeLineage(this.lineage);
|
|
915
|
+
this.index = { ...emptySegmentIndex(), streamId: this.lineage.streamId };
|
|
916
|
+
this.saveIndex();
|
|
869
917
|
}
|
|
870
918
|
|
|
871
919
|
#indexMatchesDisk(idx) {
|
|
@@ -881,8 +929,128 @@ class SegmentStore {
|
|
|
881
929
|
});
|
|
882
930
|
}
|
|
883
931
|
|
|
932
|
+
#readLineage() {
|
|
933
|
+
if (!existsSync(this.lineagePath)) return null;
|
|
934
|
+
try {
|
|
935
|
+
const parsed = JSON.parse(readFileSync(this.lineagePath, 'utf8') || '{}');
|
|
936
|
+
if (!parsed || typeof parsed !== 'object'
|
|
937
|
+
|| typeof parsed.streamId !== 'string' || !parsed.streamId
|
|
938
|
+
|| !Number.isFinite(Number(parsed.revision))
|
|
939
|
+
|| (parsed.anchor !== null && typeof parsed.anchor !== 'string')) return null;
|
|
940
|
+
return {
|
|
941
|
+
streamId: parsed.streamId,
|
|
942
|
+
revision: Math.max(0, Number(parsed.revision)),
|
|
943
|
+
anchor: parsed.anchor,
|
|
944
|
+
};
|
|
945
|
+
} catch {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
#writeLineage(lineage) {
|
|
951
|
+
this.ensure();
|
|
952
|
+
const normalized = {
|
|
953
|
+
version: 1,
|
|
954
|
+
streamId: lineage.streamId,
|
|
955
|
+
revision: Math.max(0, Number(lineage.revision) || 0),
|
|
956
|
+
anchor: typeof lineage.anchor === 'string' ? lineage.anchor : null,
|
|
957
|
+
};
|
|
958
|
+
writeAtomic(this.lineagePath, `${JSON.stringify(normalized, null, 2)}\n`);
|
|
959
|
+
this.lineage = normalized;
|
|
960
|
+
return normalized;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
#currentLineageAnchor() {
|
|
964
|
+
if (!existsSync(this.segmentDir)) return null;
|
|
965
|
+
const files = readdirSync(this.segmentDir).filter(file => file.endsWith('.jsonl')).sort();
|
|
966
|
+
for (const file of files) {
|
|
967
|
+
const path = join(this.segmentDir, file);
|
|
968
|
+
if (!existsSync(path)) continue;
|
|
969
|
+
let raw;
|
|
970
|
+
try {
|
|
971
|
+
raw = readFileSync(path, 'utf8');
|
|
972
|
+
} catch (error) {
|
|
973
|
+
if (isPermissionError(error)) return null;
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
for (const line of raw.split('\n')) {
|
|
977
|
+
if (!parseJsonLine(line)) continue;
|
|
978
|
+
return this.#lineageAnchorForLine(line);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
#lineageAnchorForLine(line) {
|
|
985
|
+
return createHash('sha256').update(String(line || '').trim()).digest('hex');
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
#legacyLineageId(anchor) {
|
|
989
|
+
return `legacy-${createHash('sha256')
|
|
990
|
+
.update(this.rootDir)
|
|
991
|
+
.update('\0')
|
|
992
|
+
.update(anchor || '<empty>')
|
|
993
|
+
.digest('hex')}`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
#resolveLineage(idx, { verifyAnchor = false } = {}) {
|
|
997
|
+
const persisted = this.#readLineage();
|
|
998
|
+
const anchor = !persisted || verifyAnchor ? this.#currentLineageAnchor() : persisted.anchor;
|
|
999
|
+
if (!persisted) {
|
|
1000
|
+
const initialStreamId = anchor ? this.#legacyLineageId(anchor) : randomUUID();
|
|
1001
|
+
const indexStreamId = typeof idx?.streamId === 'string' && idx.streamId ? idx.streamId : null;
|
|
1002
|
+
// Missing sidecar is a rolling-upgrade boundary. Recompute from the
|
|
1003
|
+
// immutable first row for existing data. A truly empty store starts a
|
|
1004
|
+
// new random lifetime so delete + same-id recreation cannot collide.
|
|
1005
|
+
const streamId = verifyAnchor ? initialStreamId : (indexStreamId || initialStreamId);
|
|
1006
|
+
return this.#writeLineage({
|
|
1007
|
+
streamId,
|
|
1008
|
+
revision: Number.isFinite(Number(idx?.revision)) ? Number(idx.revision) : 0,
|
|
1009
|
+
anchor,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
if (persisted.anchor !== anchor) {
|
|
1013
|
+
// A reader/writer that predates lineage.json can still clear, recreate,
|
|
1014
|
+
// or rewrite the first durable row. The anchor detects that mutation
|
|
1015
|
+
// without relying on metadata fields the old index writer discards.
|
|
1016
|
+
return this.#writeLineage({ streamId: randomUUID(), revision: 0, anchor });
|
|
1017
|
+
}
|
|
1018
|
+
const indexRevision = (!idx?.streamId || idx.streamId === persisted.streamId)
|
|
1019
|
+
&& Number.isFinite(Number(idx?.revision))
|
|
1020
|
+
? Math.max(0, Number(idx.revision))
|
|
1021
|
+
: 0;
|
|
1022
|
+
const revision = Math.max(persisted.revision, indexRevision);
|
|
1023
|
+
if (revision !== persisted.revision) return this.#writeLineage({ ...persisted, revision });
|
|
1024
|
+
return persisted;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#persistCurrentLineage({ revision = null, anchor = undefined } = {}) {
|
|
1028
|
+
const current = this.lineage || this.#readLineage() || {
|
|
1029
|
+
streamId: this.index?.streamId || this.#legacyLineageId(this.#currentLineageAnchor()),
|
|
1030
|
+
revision: Number(this.index?.revision) || 0,
|
|
1031
|
+
anchor: this.#currentLineageAnchor(),
|
|
1032
|
+
};
|
|
1033
|
+
const nextRevision = Number.isFinite(Number(revision)) ? Number(revision) : current.revision;
|
|
1034
|
+
const nextAnchor = typeof anchor === 'string' && anchor ? anchor : current.anchor;
|
|
1035
|
+
const needsWrite = !this.lineage
|
|
1036
|
+
|| current.revision !== nextRevision
|
|
1037
|
+
|| current.anchor !== nextAnchor;
|
|
1038
|
+
const next = needsWrite
|
|
1039
|
+
? this.#writeLineage({ ...current, revision: nextRevision, anchor: nextAnchor })
|
|
1040
|
+
: current;
|
|
1041
|
+
this.lineage = next;
|
|
1042
|
+
if (this.index) {
|
|
1043
|
+
this.index.streamId = next.streamId;
|
|
1044
|
+
this.index.revision = next.revision;
|
|
1045
|
+
}
|
|
1046
|
+
return next;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
884
1049
|
#normalizeIndex(idx) {
|
|
885
1050
|
const out = { ...emptySegmentIndex(), ...(idx || {}) };
|
|
1051
|
+
out.version = 2;
|
|
1052
|
+
out.streamId = typeof out.streamId === 'string' && out.streamId ? out.streamId : null;
|
|
1053
|
+
out.revision = Number.isFinite(Number(out.revision)) ? Math.max(0, Number(out.revision)) : 0;
|
|
886
1054
|
out.segments = Array.isArray(out.segments) ? out.segments.filter(s => s && s.file) : [];
|
|
887
1055
|
out.segments.sort((a, b) => (Number(a.firstSeq) || 0) - (Number(b.firstSeq) || 0));
|
|
888
1056
|
const maxSeq = out.segments.reduce((max, seg) => Math.max(max, Number(seg.lastSeq) || 0), 0);
|
|
@@ -1460,6 +1628,18 @@ export class ConversationStore {
|
|
|
1460
1628
|
|
|
1461
1629
|
// ─── Read API ───────────────────────────────────────────
|
|
1462
1630
|
|
|
1631
|
+
/**
|
|
1632
|
+
* Return the durable identity of one Session transcript. `streamId`
|
|
1633
|
+
* changes on clear/recreate; `revision` changes on append/update/fold.
|
|
1634
|
+
* Browser caches use both values to detect non-append mutations before
|
|
1635
|
+
* trusting an `afterSeq` delta cursor.
|
|
1636
|
+
*/
|
|
1637
|
+
getSessionHistoryMetadata(sessionId) {
|
|
1638
|
+
if (!sessionId) return null;
|
|
1639
|
+
const store = this.#segmentStoreForConversationDir(this.#sessionConversationDir(sessionId));
|
|
1640
|
+
return store.metadata();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1463
1643
|
/**
|
|
1464
1644
|
* Load recent hot messages, sliced to the last `turnsLimit` TURNS and
|
|
1465
1645
|
* sorted chronologically.
|
|
@@ -1740,17 +1920,21 @@ export class ConversationStore {
|
|
|
1740
1920
|
* without downgrading the client to a cursor-less loaded state. Hidden rows
|
|
1741
1921
|
* advance the cursor only at pair-safe boundaries; a cursor must never cross
|
|
1742
1922
|
* an assistant tool call before all of that call's result rows are included.
|
|
1923
|
+
* Both row and byte budgets cut only at those safe boundaries. `hasMoreAfter`
|
|
1924
|
+
* tells the Web client to keep draining long offline gaps without waiting for
|
|
1925
|
+
* another reconnect or Session activation.
|
|
1743
1926
|
*
|
|
1744
1927
|
* @param {string} sessionId
|
|
1745
1928
|
* @param {number|null} afterSeq — exclusive lower bound
|
|
1746
|
-
* @param {{ limit?: number }} [opts]
|
|
1747
|
-
* @returns {{ messages: object[], latestSeq: number|null }}
|
|
1929
|
+
* @param {{ limit?: number, maxBytes?: number }} [opts]
|
|
1930
|
+
* @returns {{ messages: object[], latestSeq: number|null, hasMoreAfter: boolean }}
|
|
1748
1931
|
*/
|
|
1749
1932
|
loadAfterSeqByGroup(sessionId, afterSeq, opts = {}) {
|
|
1750
|
-
if (!sessionId) return { messages: [], latestSeq: null };
|
|
1933
|
+
if (!sessionId) return { messages: [], latestSeq: null, hasMoreAfter: false };
|
|
1751
1934
|
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? opts.limit : 500;
|
|
1935
|
+
const maxBytes = Number.isFinite(opts.maxBytes) && opts.maxBytes > 0 ? opts.maxBytes : Infinity;
|
|
1752
1936
|
const cutoff = Number.isFinite(afterSeq) && afterSeq >= 0 ? afterSeq : null;
|
|
1753
|
-
if (cutoff === null) return { messages: [], latestSeq: null };
|
|
1937
|
+
if (cutoff === null) return { messages: [], latestSeq: null, hasMoreAfter: false };
|
|
1754
1938
|
const after = [];
|
|
1755
1939
|
const pendingToolResultIds = new Set();
|
|
1756
1940
|
const completedBeforeCursor = new Set();
|
|
@@ -1788,8 +1972,14 @@ export class ConversationStore {
|
|
|
1788
1972
|
? Math.max(0, earliestBoundarySeq - 1)
|
|
1789
1973
|
: cutoff;
|
|
1790
1974
|
let visibleRows = after.length;
|
|
1975
|
+
let visibleBytes = after.reduce((sum, message) => sum + Buffer.byteLength(JSON.stringify(message)), 0);
|
|
1976
|
+
let stoppedAtBudget = false;
|
|
1791
1977
|
let extensionRows = 0;
|
|
1792
|
-
|
|
1978
|
+
const deltaRows = this.#iterateSessionRows(sessionId, { afterSeq: cutoff, desc: false });
|
|
1979
|
+
while (true) {
|
|
1980
|
+
const step = deltaRows.next();
|
|
1981
|
+
if (step.done) break;
|
|
1982
|
+
const m = step.value;
|
|
1793
1983
|
if (!m || m.sessionId !== sessionId) continue;
|
|
1794
1984
|
const seq = parseSeqFromId(m.id);
|
|
1795
1985
|
const hidden = !isVisibleConversationRow(m);
|
|
@@ -1799,6 +1989,7 @@ export class ConversationStore {
|
|
|
1799
1989
|
// messages between an assistant call and that call's result.
|
|
1800
1990
|
after.push(m);
|
|
1801
1991
|
visibleRows += 1;
|
|
1992
|
+
visibleBytes += Buffer.byteLength(JSON.stringify(m));
|
|
1802
1993
|
if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {
|
|
1803
1994
|
for (const toolCall of m.toolCalls) {
|
|
1804
1995
|
if (typeof toolCall?.id === 'string' && toolCall.id) pendingToolResultIds.add(toolCall.id);
|
|
@@ -1807,11 +1998,19 @@ export class ConversationStore {
|
|
|
1807
1998
|
pendingToolResultIds.delete(m.toolCallId);
|
|
1808
1999
|
}
|
|
1809
2000
|
}
|
|
1810
|
-
if (pendingToolResultIds.size === 0 && Number.isFinite(seq))
|
|
1811
|
-
|
|
1812
|
-
|
|
2001
|
+
if (pendingToolResultIds.size === 0 && Number.isFinite(seq)) {
|
|
2002
|
+
safeCursorSeq = seq;
|
|
2003
|
+
if (visibleRows >= limit || visibleBytes >= maxBytes) {
|
|
2004
|
+
stoppedAtBudget = true;
|
|
2005
|
+
break;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
if (visibleRows < limit && visibleBytes < maxBytes) continue;
|
|
1813
2009
|
extensionRows += 1;
|
|
1814
|
-
if (extensionRows >= DELTA_TOOL_PAIR_EXTENSION_CAP)
|
|
2010
|
+
if (extensionRows >= DELTA_TOOL_PAIR_EXTENSION_CAP) {
|
|
2011
|
+
stoppedAtBudget = true;
|
|
2012
|
+
break;
|
|
2013
|
+
}
|
|
1815
2014
|
}
|
|
1816
2015
|
// If the extension cap stopped inside a malformed arc, return only the
|
|
1817
2016
|
// prefix covered by the safe cursor. Returning later rows with an earlier
|
|
@@ -1823,10 +2022,20 @@ export class ConversationStore {
|
|
|
1823
2022
|
return Number.isFinite(seq) && seq <= safeCursorSeq;
|
|
1824
2023
|
});
|
|
1825
2024
|
const sliced = pairSanitize(pairSafeRows);
|
|
2025
|
+
let hasMoreAfter = false;
|
|
2026
|
+
if (stoppedAtBudget) {
|
|
2027
|
+
hasMoreAfter = !deltaRows.next().done;
|
|
2028
|
+
} else if (typeof deltaRows.return === 'function') {
|
|
2029
|
+
deltaRows.return();
|
|
2030
|
+
}
|
|
1826
2031
|
// Never advance past a row the sanitizer had to drop. A malformed or
|
|
1827
2032
|
// over-cap tool arc must be retried from its assistant call rather than
|
|
1828
2033
|
// turning the following result into a permanent orphan on the next page.
|
|
1829
|
-
return {
|
|
2034
|
+
return {
|
|
2035
|
+
messages: projectVisibleSessionMessages(sliced),
|
|
2036
|
+
latestSeq: safeCursorSeq,
|
|
2037
|
+
hasMoreAfter,
|
|
2038
|
+
};
|
|
1830
2039
|
}
|
|
1831
2040
|
|
|
1832
2041
|
/**
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1421,7 +1421,7 @@ function projectVisibleHistoryChunkMessages(messages = []) {
|
|
|
1421
1421
|
}));
|
|
1422
1422
|
}
|
|
1423
1423
|
|
|
1424
|
-
function emitHistoryChunk({ sessionId, messages, mode = 'older', oldestSeq = null, nextBeforeSeq = null, hasMore = false, latestSeq = null, afterSeq = null, turns = null, pageKind = null, gapStopAtSeq = null, cacheEpoch = null, requestId = null, requestClientId = null, perfTraceId = null }) {
|
|
1424
|
+
function emitHistoryChunk({ sessionId, messages, mode = 'older', oldestSeq = null, nextBeforeSeq = null, hasMore = false, latestSeq = null, afterSeq = null, hasMoreAfter = false, streamId = null, revision = null, turns = null, pageKind = null, gapStopAtSeq = null, cacheEpoch = null, requestId = null, requestClientId = null, perfTraceId = null }) {
|
|
1425
1425
|
const projectedMessages = projectVisibleHistoryChunkMessages(messages);
|
|
1426
1426
|
// Empty deltas still carry the authoritative safe cursor and clear the
|
|
1427
1427
|
// browser's syncingAfterSeq fence. Dropping this envelope leaves Session
|
|
@@ -1440,6 +1440,9 @@ function emitHistoryChunk({ sessionId, messages, mode = 'older', oldestSeq = nul
|
|
|
1440
1440
|
hasMore: !!hasMore,
|
|
1441
1441
|
latestSeq,
|
|
1442
1442
|
afterSeq,
|
|
1443
|
+
hasMoreAfter: !!hasMoreAfter,
|
|
1444
|
+
streamId,
|
|
1445
|
+
revision,
|
|
1443
1446
|
turns,
|
|
1444
1447
|
...(pageKind ? { pageKind } : {}),
|
|
1445
1448
|
...(Number.isFinite(gapStopAtSeq) ? { gapStopAtSeq } : {}),
|
|
@@ -1499,6 +1502,9 @@ function emitLegacyHistoryOutputFrames(replayEntries) {
|
|
|
1499
1502
|
}
|
|
1500
1503
|
|
|
1501
1504
|
function emitVisibleHistoryReplay({ store, sessionId, limit, beforeSeq = null, mode = 'recent', requestId = null, requestClientId = null, perfTraceId = null }) {
|
|
1505
|
+
const historyMetadata = sessionId && typeof store.getSessionHistoryMetadata === 'function'
|
|
1506
|
+
? store.getSessionHistoryMetadata(sessionId)
|
|
1507
|
+
: null;
|
|
1502
1508
|
const visiblePage = sessionId
|
|
1503
1509
|
? loadVisibleGroupHistoryPage(store, sessionId, limit, beforeSeq)
|
|
1504
1510
|
: { messages: limit > 0 ? (store.loadRecent?.(limit) || []) : [], oldestSeq: null, hasMore: false };
|
|
@@ -1520,6 +1526,8 @@ function emitVisibleHistoryReplay({ store, sessionId, limit, beforeSeq = null, m
|
|
|
1520
1526
|
nextBeforeSeq: visiblePage.nextBeforeSeq,
|
|
1521
1527
|
hasMore: visiblePage.hasMore,
|
|
1522
1528
|
latestSeq: Number.isFinite(latestSeq) ? latestSeq : null,
|
|
1529
|
+
streamId: historyMetadata?.streamId || null,
|
|
1530
|
+
revision: historyMetadata?.revision ?? null,
|
|
1523
1531
|
turns: limit,
|
|
1524
1532
|
requestId,
|
|
1525
1533
|
requestClientId,
|
|
@@ -6762,7 +6770,20 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6762
6770
|
// and wants only the messages that arrived after that cursor. Returns
|
|
6763
6771
|
// mode:'delta' so the frontend can append+dedupe instead of replacing
|
|
6764
6772
|
// the pane.
|
|
6765
|
-
const
|
|
6773
|
+
const deltaLimit = Number.isFinite(msg?.maxRows)
|
|
6774
|
+
? Math.min(500, Math.max(1, Math.floor(msg.maxRows)))
|
|
6775
|
+
: 100;
|
|
6776
|
+
const deltaMaxBytes = Number.isFinite(msg?.maxBytes)
|
|
6777
|
+
? Math.min(2 * 1024 * 1024, Math.max(32 * 1024, Math.floor(msg.maxBytes)))
|
|
6778
|
+
: 512 * 1024;
|
|
6779
|
+
const historyMetadata = typeof session.conversationStore.getSessionHistoryMetadata === 'function'
|
|
6780
|
+
? session.conversationStore.getSessionHistoryMetadata(sessionId)
|
|
6781
|
+
: null;
|
|
6782
|
+
const cacheIdentityMatches = !msg?.streamId || (
|
|
6783
|
+
msg.streamId === historyMetadata?.streamId
|
|
6784
|
+
&& Number(msg.revision) === Number(historyMetadata?.revision)
|
|
6785
|
+
);
|
|
6786
|
+
const afterSeqRaw = cacheIdentityMatches && msg && Number.isFinite(msg.afterSeq) ? msg.afterSeq : null;
|
|
6766
6787
|
const afterMessageId = (msg && typeof msg.afterMessageId === 'string') ? msg.afterMessageId : null;
|
|
6767
6788
|
let afterSeq = afterSeqRaw;
|
|
6768
6789
|
if (afterSeq === null && afterMessageId && typeof session.conversationStore.getMessageSeqById === 'function') {
|
|
@@ -6770,7 +6791,10 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6770
6791
|
}
|
|
6771
6792
|
if (sessionId && afterSeq !== null && typeof session.conversationStore.loadAfterSeqByGroup === 'function') {
|
|
6772
6793
|
const loadStart = perfNowMs();
|
|
6773
|
-
const delta = session.conversationStore.loadAfterSeqByGroup(sessionId, afterSeq
|
|
6794
|
+
const delta = session.conversationStore.loadAfterSeqByGroup(sessionId, afterSeq, {
|
|
6795
|
+
limit: deltaLimit,
|
|
6796
|
+
maxBytes: deltaMaxBytes,
|
|
6797
|
+
});
|
|
6774
6798
|
traceDuration('history.store_load_delta', loadStart, { detail: { count: delta.messages?.length || 0, afterSeq } });
|
|
6775
6799
|
const emitStart = perfNowMs();
|
|
6776
6800
|
const projectedMessages = emitHistoryChunk({
|
|
@@ -6779,6 +6803,9 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6779
6803
|
mode: 'delta',
|
|
6780
6804
|
latestSeq: delta.latestSeq,
|
|
6781
6805
|
afterSeq,
|
|
6806
|
+
hasMoreAfter: delta.hasMoreAfter,
|
|
6807
|
+
streamId: historyMetadata?.streamId || null,
|
|
6808
|
+
revision: historyMetadata?.revision ?? null,
|
|
6782
6809
|
requestId,
|
|
6783
6810
|
requestClientId,
|
|
6784
6811
|
perfTraceId,
|
|
@@ -6792,6 +6819,9 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6792
6819
|
requestId,
|
|
6793
6820
|
latestSeq: delta.latestSeq,
|
|
6794
6821
|
afterSeq,
|
|
6822
|
+
hasMoreAfter: !!delta.hasMoreAfter,
|
|
6823
|
+
streamId: historyMetadata?.streamId || null,
|
|
6824
|
+
revision: historyMetadata?.revision ?? null,
|
|
6795
6825
|
}, { sessionId, requestId, requestClientId, perfTraceId });
|
|
6796
6826
|
return;
|
|
6797
6827
|
}
|
|
@@ -6832,6 +6862,8 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6832
6862
|
nextBeforeSeq: visiblePage.nextBeforeSeq,
|
|
6833
6863
|
hasMore: visiblePage.hasMore,
|
|
6834
6864
|
latestSeq,
|
|
6865
|
+
streamId: historyMetadata?.streamId || null,
|
|
6866
|
+
revision: historyMetadata?.revision ?? null,
|
|
6835
6867
|
turns: limit,
|
|
6836
6868
|
requestId,
|
|
6837
6869
|
requestClientId,
|
|
@@ -6874,12 +6906,19 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6874
6906
|
oldestSeq,
|
|
6875
6907
|
nextBeforeSeq: visiblePage.nextBeforeSeq,
|
|
6876
6908
|
latestSeq,
|
|
6909
|
+
streamId: historyMetadata?.streamId || null,
|
|
6910
|
+
revision: historyMetadata?.revision ?? null,
|
|
6877
6911
|
}, { sessionId, requestId, requestClientId, perfTraceId });
|
|
6878
6912
|
};
|
|
6879
6913
|
|
|
6880
6914
|
if (!session) {
|
|
6881
6915
|
const yeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
|
|
6882
|
-
const
|
|
6916
|
+
const deltaLimit = Number.isFinite(msg?.maxRows)
|
|
6917
|
+
? Math.min(500, Math.max(1, Math.floor(msg.maxRows)))
|
|
6918
|
+
: 100;
|
|
6919
|
+
const deltaMaxBytes = Number.isFinite(msg?.maxBytes)
|
|
6920
|
+
? Math.min(2 * 1024 * 1024, Math.max(32 * 1024, Math.floor(msg.maxBytes)))
|
|
6921
|
+
: 512 * 1024;
|
|
6883
6922
|
const afterMessageId = (msg && typeof msg.afterMessageId === 'string') ? msg.afterMessageId : null;
|
|
6884
6923
|
const limit = (typeof msg.limit === 'number') ? msg.limit : 10;
|
|
6885
6924
|
ensureYeaftConversationId();
|
|
@@ -6902,6 +6941,14 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6902
6941
|
// window immediately, then finish loadSession below for actual turns.
|
|
6903
6942
|
const coldStoreStart = perfNowMs();
|
|
6904
6943
|
const coldStore = new ConversationStore(historyYeaftDir);
|
|
6944
|
+
const historyMetadata = sessionId && typeof coldStore.getSessionHistoryMetadata === 'function'
|
|
6945
|
+
? coldStore.getSessionHistoryMetadata(sessionId)
|
|
6946
|
+
: null;
|
|
6947
|
+
const cacheIdentityMatches = !msg?.streamId || (
|
|
6948
|
+
msg.streamId === historyMetadata?.streamId
|
|
6949
|
+
&& Number(msg.revision) === Number(historyMetadata?.revision)
|
|
6950
|
+
);
|
|
6951
|
+
const afterSeqRaw = cacheIdentityMatches && msg && Number.isFinite(msg.afterSeq) ? msg.afterSeq : null;
|
|
6905
6952
|
traceDuration('history.cold_store_open', coldStoreStart);
|
|
6906
6953
|
if (sessionId && (afterSeqRaw !== null || afterMessageId)) {
|
|
6907
6954
|
let afterSeq = afterSeqRaw;
|
|
@@ -6910,8 +6957,11 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6910
6957
|
}
|
|
6911
6958
|
const loadStart = perfNowMs();
|
|
6912
6959
|
const delta = afterSeq !== null && typeof coldStore.loadAfterSeqByGroup === 'function'
|
|
6913
|
-
? coldStore.loadAfterSeqByGroup(sessionId, afterSeq
|
|
6914
|
-
|
|
6960
|
+
? coldStore.loadAfterSeqByGroup(sessionId, afterSeq, {
|
|
6961
|
+
limit: deltaLimit,
|
|
6962
|
+
maxBytes: deltaMaxBytes,
|
|
6963
|
+
})
|
|
6964
|
+
: { messages: [], latestSeq: null, hasMoreAfter: false };
|
|
6915
6965
|
traceDuration('history.store_load_delta', loadStart, { detail: { count: delta.messages?.length || 0, afterSeq, cold: true } });
|
|
6916
6966
|
const emitStart = perfNowMs();
|
|
6917
6967
|
const projectedMessages = emitHistoryChunk({
|
|
@@ -6920,12 +6970,26 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
6920
6970
|
mode: 'delta',
|
|
6921
6971
|
latestSeq: delta.latestSeq,
|
|
6922
6972
|
afterSeq,
|
|
6973
|
+
hasMoreAfter: delta.hasMoreAfter,
|
|
6974
|
+
streamId: historyMetadata?.streamId || null,
|
|
6975
|
+
revision: historyMetadata?.revision ?? null,
|
|
6923
6976
|
requestId,
|
|
6924
6977
|
requestClientId,
|
|
6925
6978
|
perfTraceId,
|
|
6926
6979
|
});
|
|
6927
6980
|
traceDuration('history.emit_chunk', emitStart, { detail: { mode: 'delta', count: projectedMessages.length, cold: true } });
|
|
6928
|
-
sendSessionEvent({
|
|
6981
|
+
sendSessionEvent({
|
|
6982
|
+
type: 'history_loaded',
|
|
6983
|
+
mode: 'delta',
|
|
6984
|
+
count: projectedMessages.length,
|
|
6985
|
+
sessionId,
|
|
6986
|
+
requestId,
|
|
6987
|
+
latestSeq: delta.latestSeq,
|
|
6988
|
+
afterSeq,
|
|
6989
|
+
hasMoreAfter: !!delta.hasMoreAfter,
|
|
6990
|
+
streamId: historyMetadata?.streamId || null,
|
|
6991
|
+
revision: historyMetadata?.revision ?? null,
|
|
6992
|
+
}, { sessionId, requestId, requestClientId, perfTraceId });
|
|
6929
6993
|
} else if (!metadataOnly) {
|
|
6930
6994
|
const replayStart = perfNowMs();
|
|
6931
6995
|
emitVisibleHistoryReplay({ store: coldStore, sessionId, limit, mode: 'recent', requestId, requestClientId, perfTraceId });
|