neuralos 3.8.2 → 3.8.3
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/bin/gybackend.cjs +175 -25
- package/package.json +1 -1
package/bin/gybackend.cjs
CHANGED
|
@@ -258480,6 +258480,9 @@ var HistorySqliteStore_exports = {};
|
|
|
258480
258480
|
__export(HistorySqliteStore_exports, {
|
|
258481
258481
|
HistorySqliteStore: () => HistorySqliteStore
|
|
258482
258482
|
});
|
|
258483
|
+
function digestFor(message) {
|
|
258484
|
+
return `${message.type}\0${message.dataJson ?? ""}`;
|
|
258485
|
+
}
|
|
258483
258486
|
var import_node_fs5, import_node_path8, HistorySqliteStore;
|
|
258484
258487
|
var init_HistorySqliteStore = __esm({
|
|
258485
258488
|
"../../packages/backend/src/services/history/HistorySqliteStore.ts"() {
|
|
@@ -258656,9 +258659,135 @@ var init_HistorySqliteStore = __esm({
|
|
|
258656
258659
|
(session) => session !== null
|
|
258657
258660
|
);
|
|
258658
258661
|
}
|
|
258662
|
+
/**
|
|
258663
|
+
* Session meta WITHOUT messages.
|
|
258664
|
+
*
|
|
258665
|
+
* Two fixed-cost hot-path bugs this closes:
|
|
258666
|
+
*
|
|
258667
|
+
* 1. `ChatHistoryService.saveSession` called `loadChatSession()` purely to
|
|
258668
|
+
* learn `createdAt`. That JSON.parse'd the ENTIRE session — measured
|
|
258669
|
+
* ~117 MB / ~1.5 s of parse for a 6k-message session — and threw every
|
|
258670
|
+
* parsed message away, on every save.
|
|
258671
|
+
* 2. `AgentService_v2.trySaveSessionFromCheckpoint` called `loadSession()`
|
|
258672
|
+
* for a default it never used, because `updateSessionFromMessages()`
|
|
258673
|
+
* rebuilds `session.messages` from scratch. Dropping that call outright
|
|
258674
|
+
* would have reset the session TITLE to "New Session" on every restore,
|
|
258675
|
+
* so the title is read here too.
|
|
258676
|
+
*/
|
|
258677
|
+
getChatSessionMeta(sessionId) {
|
|
258678
|
+
const row = this.db.prepare("SELECT created_at, title FROM chat_sessions WHERE id = ?").get(sessionId);
|
|
258679
|
+
return row ? { createdAt: row.created_at, title: row.title } : null;
|
|
258680
|
+
}
|
|
258681
|
+
/** Created-at scalar only — a single indexed row, never a message parse. */
|
|
258682
|
+
getChatSessionCreatedAt(sessionId) {
|
|
258683
|
+
return this.getChatSessionMeta(sessionId)?.createdAt;
|
|
258684
|
+
}
|
|
258685
|
+
/**
|
|
258686
|
+
* Existing message bodies as RAW TEXT, ordered by position, WITHOUT parsing.
|
|
258687
|
+
*
|
|
258688
|
+
* The raw `message_data_json` is needed for a byte-exact comparison against
|
|
258689
|
+
* the newly serialized body. Deliberately NOT JSON.parse'd: the parse is the
|
|
258690
|
+
* expensive half of the freeze (~1.5 s / 117 MB measured) and is unnecessary
|
|
258691
|
+
* for an equality test.
|
|
258692
|
+
*
|
|
258693
|
+
* (An earlier revision compared only `substr(json, 1, 200)`. That was WRONG:
|
|
258694
|
+
* a message whose content grows past the first 200 characters — exactly what
|
|
258695
|
+
* streaming does to the last AI message — leaves the prefix untouched, so the
|
|
258696
|
+
* row would be skipped and the persisted history would silently stay STALE.
|
|
258697
|
+
* Full-text comparison is what makes "unchanged => skip" safe.)
|
|
258698
|
+
*/
|
|
258699
|
+
loadSessionMessageState(sessionId) {
|
|
258700
|
+
const rows = this.db.prepare(
|
|
258701
|
+
`SELECT message_id, message_type, position, message_data_json
|
|
258702
|
+
FROM chat_session_messages
|
|
258703
|
+
WHERE session_id = ?
|
|
258704
|
+
ORDER BY position ASC`
|
|
258705
|
+
).all(sessionId);
|
|
258706
|
+
return rows.map((row) => ({
|
|
258707
|
+
id: row.message_id,
|
|
258708
|
+
type: row.message_type,
|
|
258709
|
+
position: row.position,
|
|
258710
|
+
jsonText: row.message_data_json ?? ""
|
|
258711
|
+
}));
|
|
258712
|
+
}
|
|
258713
|
+
/**
|
|
258714
|
+
* Incremental write: touch only the rows that actually moved.
|
|
258715
|
+
*
|
|
258716
|
+
* The old path ran `DELETE ALL` + `INSERT ALL` on every save, rewriting the
|
|
258717
|
+
* whole session payload each time. Here, unchanged messages stay on disk
|
|
258718
|
+
* untouched; messages that are genuinely absent (compaction, rollback) are
|
|
258719
|
+
* removed individually.
|
|
258720
|
+
*/
|
|
258721
|
+
applyChatSessionDelta(sessionId, desired, existingDigests, writeAll) {
|
|
258722
|
+
const desiredIds = /* @__PURE__ */ new Set();
|
|
258723
|
+
for (const m2 of desired) {
|
|
258724
|
+
desiredIds.add(m2.id);
|
|
258725
|
+
}
|
|
258726
|
+
const changed = writeAll ? desired : desired.filter((m2) => existingDigests.get(m2.id) !== digestFor(m2));
|
|
258727
|
+
const toRemove = [];
|
|
258728
|
+
for (const id of existingDigests.keys()) {
|
|
258729
|
+
if (!desiredIds.has(id)) {
|
|
258730
|
+
toRemove.push(id);
|
|
258731
|
+
}
|
|
258732
|
+
}
|
|
258733
|
+
if (changed.length === 0 && toRemove.length === 0) {
|
|
258734
|
+
return;
|
|
258735
|
+
}
|
|
258736
|
+
const upsert = this.db.prepare(
|
|
258737
|
+
`INSERT INTO chat_session_messages (
|
|
258738
|
+
session_id, position, message_id, message_type, message_data_json
|
|
258739
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
258740
|
+
ON CONFLICT(session_id, message_id) DO UPDATE SET
|
|
258741
|
+
position = excluded.position,
|
|
258742
|
+
message_type = excluded.message_type,
|
|
258743
|
+
message_data_json = excluded.message_data_json`
|
|
258744
|
+
);
|
|
258745
|
+
const removeOne = this.db.prepare(
|
|
258746
|
+
"DELETE FROM chat_session_messages WHERE session_id = ? AND message_id = ?"
|
|
258747
|
+
);
|
|
258748
|
+
this.db.transaction(() => {
|
|
258749
|
+
for (const id of toRemove) {
|
|
258750
|
+
removeOne.run(sessionId, id);
|
|
258751
|
+
}
|
|
258752
|
+
for (const m2 of changed) {
|
|
258753
|
+
upsert.run(sessionId, m2.position, m2.id, m2.type, m2.dataJson);
|
|
258754
|
+
}
|
|
258755
|
+
})();
|
|
258756
|
+
}
|
|
258757
|
+
/**
|
|
258758
|
+
* Full positional rewrite. Positions are `PRIMARY KEY (session_id,
|
|
258759
|
+
* position)`, so an in-place UPDATE cannot express a reorder without a
|
|
258760
|
+
* transient collision — the order guard in saveChatSession routes here when
|
|
258761
|
+
* (and only when) the surviving ids changed relative order.
|
|
258762
|
+
*
|
|
258763
|
+
* Therefore this really does DELETE + INSERT rather than upsert. An
|
|
258764
|
+
* `ON CONFLICT(session_id, message_id)` upsert looks like it would work but
|
|
258765
|
+
* does NOT: reassigning `position` collides on the (session_id, position)
|
|
258766
|
+
* PRIMARY KEY against a row that has not moved yet, and that conflict is not
|
|
258767
|
+
* absorbed by an ON CONFLICT clause targeting a different key —
|
|
258768
|
+
* SqliteError: UNIQUE constraint failed:
|
|
258769
|
+
* chat_session_messages.session_id, chat_session_messages.position
|
|
258770
|
+
* Clearing first (inside the same transaction) sidesteps the ordering
|
|
258771
|
+
* problem entirely. This path is rare by design, so the rewrite is cheap
|
|
258772
|
+
* enough, and it matches the old DELETE-ALL/INSERT-ALL semantics exactly.
|
|
258773
|
+
*/
|
|
258774
|
+
replaceAllMessages(sessionId, desired) {
|
|
258775
|
+
const removeAll = this.db.prepare(
|
|
258776
|
+
"DELETE FROM chat_session_messages WHERE session_id = ?"
|
|
258777
|
+
);
|
|
258778
|
+
const insert = this.db.prepare(
|
|
258779
|
+
`INSERT INTO chat_session_messages (
|
|
258780
|
+
session_id, position, message_id, message_type, message_data_json
|
|
258781
|
+
) VALUES (?, ?, ?, ?, ?)`
|
|
258782
|
+
);
|
|
258783
|
+
this.db.transaction(() => {
|
|
258784
|
+
removeAll.run(sessionId);
|
|
258785
|
+
desired.forEach((m2, index2) => {
|
|
258786
|
+
insert.run(sessionId, index2, m2.id, m2.type, m2.dataJson);
|
|
258787
|
+
});
|
|
258788
|
+
})();
|
|
258789
|
+
}
|
|
258659
258790
|
saveChatSession(session) {
|
|
258660
|
-
const existingCreatedAt = this.db.prepare("SELECT created_at FROM chat_sessions WHERE id = ?").get(session.id);
|
|
258661
|
-
const createdAt = existingCreatedAt?.created_at ?? session.createdAt;
|
|
258662
258791
|
const upsertSession = this.db.prepare(
|
|
258663
258792
|
`INSERT INTO chat_sessions (
|
|
258664
258793
|
id, title, last_checkpoint_offset, last_profile_max_tokens, created_at, updated_at
|
|
@@ -258671,34 +258800,45 @@ var init_HistorySqliteStore = __esm({
|
|
|
258671
258800
|
last_profile_max_tokens = excluded.last_profile_max_tokens,
|
|
258672
258801
|
updated_at = excluded.updated_at`
|
|
258673
258802
|
);
|
|
258674
|
-
const deleteMessages = this.db.prepare(
|
|
258675
|
-
"DELETE FROM chat_session_messages WHERE session_id = ?"
|
|
258676
|
-
);
|
|
258677
|
-
const insertMessage = this.db.prepare(
|
|
258678
|
-
`INSERT INTO chat_session_messages (
|
|
258679
|
-
session_id, position, message_id, message_type, message_data_json
|
|
258680
|
-
) VALUES (?, ?, ?, ?, ?)`
|
|
258681
|
-
);
|
|
258682
258803
|
this.db.transaction(() => {
|
|
258683
258804
|
upsertSession.run({
|
|
258684
258805
|
id: session.id,
|
|
258685
258806
|
title: session.title,
|
|
258686
258807
|
lastCheckpointOffset: session.lastCheckpointOffset,
|
|
258687
258808
|
lastProfileMaxTokens: session.lastProfileMaxTokens ?? null,
|
|
258688
|
-
createdAt,
|
|
258809
|
+
createdAt: session.createdAt,
|
|
258689
258810
|
updatedAt: session.updatedAt
|
|
258690
258811
|
});
|
|
258691
|
-
deleteMessages.run(session.id);
|
|
258692
|
-
session.messages.forEach((message, index2) => {
|
|
258693
|
-
insertMessage.run(
|
|
258694
|
-
session.id,
|
|
258695
|
-
index2,
|
|
258696
|
-
message.id,
|
|
258697
|
-
message.type,
|
|
258698
|
-
JSON.stringify(message.data)
|
|
258699
|
-
);
|
|
258700
|
-
});
|
|
258701
258812
|
})();
|
|
258813
|
+
const desired = session.messages.map(
|
|
258814
|
+
(message, index2) => ({
|
|
258815
|
+
id: message.id,
|
|
258816
|
+
type: message.type,
|
|
258817
|
+
position: index2,
|
|
258818
|
+
dataJson: JSON.stringify(message.data)
|
|
258819
|
+
})
|
|
258820
|
+
);
|
|
258821
|
+
const existingState = this.loadSessionMessageState(session.id);
|
|
258822
|
+
const existingDigests = /* @__PURE__ */ new Map();
|
|
258823
|
+
for (const row of existingState) {
|
|
258824
|
+
existingDigests.set(
|
|
258825
|
+
row.id,
|
|
258826
|
+
digestFor({ type: row.type, dataJson: row.jsonText })
|
|
258827
|
+
);
|
|
258828
|
+
}
|
|
258829
|
+
const storedOrder = existingState.map((row) => row.id);
|
|
258830
|
+
const desiredOrderOfStored = [];
|
|
258831
|
+
for (const m2 of desired) {
|
|
258832
|
+
if (existingDigests.has(m2.id)) {
|
|
258833
|
+
desiredOrderOfStored.push(m2.id);
|
|
258834
|
+
}
|
|
258835
|
+
}
|
|
258836
|
+
const reordered = storedOrder.length !== desiredOrderOfStored.length || storedOrder.some((id, i) => id !== desiredOrderOfStored[i]);
|
|
258837
|
+
if (reordered) {
|
|
258838
|
+
this.replaceAllMessages(session.id, desired);
|
|
258839
|
+
return;
|
|
258840
|
+
}
|
|
258841
|
+
this.applyChatSessionDelta(session.id, desired, existingDigests, false);
|
|
258702
258842
|
}
|
|
258703
258843
|
deleteChatSessions(sessionIds) {
|
|
258704
258844
|
const ids = Array.from(
|
|
@@ -371635,9 +371775,11 @@ ${reminder}`;
|
|
|
371635
371775
|
messages = [...messages, this.lastAbortedMessage];
|
|
371636
371776
|
this.lastAbortedMessage = null;
|
|
371637
371777
|
}
|
|
371638
|
-
const
|
|
371778
|
+
const sessionMeta = this.chatHistoryService.getSessionMeta(sessionId);
|
|
371779
|
+
const session = {
|
|
371639
371780
|
id: sessionId,
|
|
371640
|
-
title: "New Session",
|
|
371781
|
+
title: sessionMeta?.title || "New Session",
|
|
371782
|
+
// Placeholder only — replaced by updateSessionFromMessages() below.
|
|
371641
371783
|
messages: /* @__PURE__ */ new Map(),
|
|
371642
371784
|
lastCheckpointOffset: 0,
|
|
371643
371785
|
lastProfileMaxTokens: this.getEffectiveMaxTokensForSession(sessionId)
|
|
@@ -373138,7 +373280,7 @@ var ChatHistoryService = class {
|
|
|
373138
373280
|
this.store = options?.store || new HistorySqliteStore();
|
|
373139
373281
|
}
|
|
373140
373282
|
saveSession(session) {
|
|
373141
|
-
const
|
|
373283
|
+
const createdAt = this.store.getChatSessionCreatedAt(session.id);
|
|
373142
373284
|
const now = Date.now();
|
|
373143
373285
|
this.store.saveChatSession({
|
|
373144
373286
|
id: session.id,
|
|
@@ -373150,10 +373292,18 @@ var ChatHistoryService = class {
|
|
|
373150
373292
|
})),
|
|
373151
373293
|
lastCheckpointOffset: session.lastCheckpointOffset,
|
|
373152
373294
|
lastProfileMaxTokens: session.lastProfileMaxTokens,
|
|
373153
|
-
createdAt:
|
|
373295
|
+
createdAt: createdAt || now,
|
|
373154
373296
|
updatedAt: now
|
|
373155
373297
|
});
|
|
373156
373298
|
}
|
|
373299
|
+
/**
|
|
373300
|
+
* Session row only (title/createdAt) — never parses stored messages.
|
|
373301
|
+
* Used on the checkpoint save path, which previously paid a full session
|
|
373302
|
+
* parse just to preserve the title.
|
|
373303
|
+
*/
|
|
373304
|
+
getSessionMeta(sessionId) {
|
|
373305
|
+
return this.store.getChatSessionMeta(sessionId);
|
|
373306
|
+
}
|
|
373157
373307
|
loadSession(sessionId) {
|
|
373158
373308
|
const storedSession = this.store.loadChatSession(sessionId);
|
|
373159
373309
|
if (!storedSession) {
|